PwnCollege - RE - Patching Code

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}