Infobahn CTF 2025 Reverse-disthis WriteUp

4.3k 词

pyc文件python版本为3.13.8,而且似乎有混淆,无法反编译,先进行模糊测试:

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
import sys, os, marshal, threading

def run_pyc_with_opcode_count(pyc_path):
count = 0
use_mon = hasattr(sys, "monitoring")
if use_mon:
mon = sys.monitoring
TOOL = mon.DEBUGGER_ID
def on_instr(code, offset):
nonlocal count
count += 1
mon.use_tool_id(TOOL, "opcount")
mon.register_callback(TOOL, mon.events.INSTRUCTION, on_instr)
mon.set_events(TOOL, mon.events.INSTRUCTION)
else:
def tracer(frame, event, arg):
nonlocal count
if event == 'call':
frame.f_trace_opcodes = True
return tracer
if event == 'opcode':
count += 1
return tracer
return tracer
sys.settrace(tracer)
threading.settrace(tracer)
class _HardExit(Exception): pass
orig_sys_exit, orig_os_exit = sys.exit, os._exit
def fake_os_exit(code=0): raise _HardExit(code)
sys.exit = lambda code=0: (_ for _ in ()).throw(SystemExit(code))
os._exit = fake_os_exit
try:
with open(pyc_path, 'rb') as f:
f.read(16)
code = marshal.load(f)

g = {
"__name__": "__main__",
"__file__": pyc_path,
"__package__": None,
"__builtins__": __builtins__,
}
try:
exec(code, g, None)
except SystemExit as e:
print(f"[caught SystemExit] exit code={getattr(e, 'code', 0)}")
except _HardExit as e:
code_ = e.args[0] if e.args else 0
print(f"[caught os._exit] exit code={code_}")
finally:
sys.exit = orig_sys_exit
os._exit = orig_os_exit
if use_mon:
mon.set_events(TOOL, mon.events.NO_EVENTS)
mon.register_callback(TOOL, mon.events.INSTRUCTION, None)
else:
sys.settrace(None)
threading.settrace(None)
print(f"Executed opcodes: {count}")
run_pyc_with_opcode_count('output.pyc')

发现文件错误时指令数量为610,含有文件但长度错误时指令数量为768

爆破长度,发现直到10KB都处于768,尝试反汇编前768条指令

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
import sys, os, marshal, dis, threading, builtins, io, weakref, argparse, types

class _HardExit(Exception): pass
_offset_cache = weakref.WeakKeyDictionary()
def _build_map(code: types.CodeType):
m = {}
try:
for ins in dis.get_instructions(code): m[ins.offset] = ins
except Exception: pass
_offset_cache[code] = m
return m

def _lookup_instr(code, offset):
m = _offset_cache.get(code) or _build_map(code)
ins = m.get(offset)
if ins is not None:
lineno = None
pos = getattr(ins, "positions", None)
if pos is not None: lineno = getattr(pos, "lineno", None)
if lineno is None: lineno = ins.starts_line
return {
"name": ins.opname,
"argrepr": ins.argrepr or "",
"lineno": lineno,
}
try:
op = code.co_code[offset]
name = dis.opname[op]
except Exception: name = "<??>"
return {"name": name, "argrepr": "", "lineno": None}

_trace_lines = []
_seq = 0
def _record(code, offset):
global _seq
info = _lookup_instr(code, offset)
fn = code.co_filename
func = getattr(code, "co_qualname", code.co_name)
tid = threading.get_ident()
_seq += 1
line = info["lineno"] if info["lineno"] is not None else "?"
argpart = (" " + info["argrepr"]) if info["argrepr"] else ""
_trace_lines.append(
f"{_seq:05d} T{tid} {os.path.basename(fn)}:{line} {func} "
f"off={offset:04d} {info['name']}{argpart}"
)

def _install_tracer():
if hasattr(sys, "monitoring"):
mon = sys.monitoring
TOOL = mon.DEBUGGER_ID
def on_instr(code, offset): _record(code, offset)
mon.use_tool_id(TOOL, "op-trace")
mon.register_callback(TOOL, mon.events.INSTRUCTION, on_instr)
mon.set_events(TOOL, mon.events.INSTRUCTION)
return ("mon", mon, TOOL)
def tracer(frame, event, arg):
if event != "opcode":
try:
frame.f_trace_opcodes = True
except Exception: pass
return tracer
_record(frame.f_code, frame.f_lasti)
return tracer
sys.settrace(tracer)
threading.settrace(tracer)
return ("trace", tracer, None)

def _uninstall_tracer(state):
kind, a, TOOL = state
if kind == "mon":
mon = a
mon.set_events(TOOL, mon.events.NO_EVENTS)
mon.register_callback(TOOL, mon.events.INSTRUCTION, None)
else:
sys.settrace(None)
threading.settrace(None)

def run_pyc_with_trace(pyc_path: str, send_line: str = None, use_crlf: bool=False):
orig_sys_exit, orig_os_exit = sys.exit, os._exit
sys.exit = lambda code=0: (_ for _ in ()).throw(SystemExit(code))
os._exit = lambda code=0: (_ for _ in ()).throw(_HardExit(code))
orig_input = builtins.input
orig_stdin = sys.stdin
if send_line is not None:
eol = "\r\n" if use_crlf else "\n"
def _fake_input(prompt=None):
if prompt is not None:
try:
sys.stdout.write(str(prompt))
sys.stdout.flush()
except Exception:
pass
return send_line
builtins.input = _fake_input
sys.stdin = io.StringIO(send_line + eol)

state = None
try:
state = _install_tracer()
with open(pyc_path, "rb") as f:
f.read(16)
code = marshal.load(f)
g = {
"__name__": "__main__",
"__file__": pyc_path,
"__package__": None,
"__builtins__": __builtins__,
}
try: exec(code, g, None)
except SystemExit as e: print(f"[caught SystemExit] exit code={getattr(e, 'code', 0)}", file=sys.stderr)
except _HardExit as e:
ec = e.args[0] if e.args else 0
print(f"[caught os._exit] exit code={ec}", file=sys.stderr)
finally:
if state: _uninstall_tracer(state)
sys.exit = orig_sys_exit
os._exit = orig_os_exit
if send_line is not None:
builtins.input = orig_input
sys.stdin = orig_stdin
out = sys.stdout
for line in _trace_lines: out.write(line + "\n")
out.write(f"Executed opcodes: {_seq}\n")
out.flush()

def main():
ap = argparse.ArgumentParser(description="Trace executed Python bytecode instructions from a .pyc")
ap.add_argument("pyc", help="path to .pyc (sourceless) file")
ap.add_argument("--send", help="simulate typing this single line to stdin/input() (e.g., 'flag.txt')", default=None)
ap.add_argument("--crlf", action="store_true", help="use CRLF for the simulated input line ending (Windows style)")
args = ap.parse_args()
if not os.path.exists(args.pyc): ap.error(f"pyc not found: {args.pyc}")
run_pyc_with_trace(args.pyc, send_line=args.send, use_crlf=args.crlf)

if __name__ == "__main__":
main()

发现前面加载了一个351项的常量,这个常量可以在pyc文件的尾部看到,实际上是一个00FF的int类型整数表和一个32126的可打印字符表,不确定其是同时加载到一个组中还是分开加载的,按顺序的351项如下:

1
6c77e381e668654641a02c6776351a72bc7dcf6109994773549058aa085005454404373671229f5c003376fbb95aea8d98efbf69ffad4406bd6b4cc5787c3c8a52d225474ea32a589160394dacce4b49437b9cf4573807a52eda2785cc6ebb422d3af333378fc4a9f651a17b1618790b0caeb65974606de9a25b95272f83fef11924873b452425d1657020e55c9a3deed84166d93f53a65975359754fd0a4c787e4a02f77a4363b1dd932667361e035eb74fec1bc9c3f2665d5e522c79467aab6fdfb30149172b2e3dd6c76228343c61e8562f88dc947c89234877e4b5b27312af6d103bc12ac0641f1557cbf9b89beb5d749672d0510ee7804f1d6353267132502d690d5ffcba3e40dee2281cc223b43a5b84d7f86a406ecd5f308e62f0fa4e8bca9d3f2b8211d570315aa829f54d8c7d7f4a0f6f306814be568621c632346a216c9e6b644b927520c8d348e1383ea429e042dbedd439a7b055225531137e

可打印字符:

1
lwFgv5r}aGTE7q"\D|%N*X`MKIC{Wn3Y'$e =A?LxJ&6f^R,yzo+.b(4</Hsm;d]tQOcS2P-i_#:[j@0p1ZhV!ku8>)B9U~

int:

1
e381e6686541a02c1abccf0999739058aa0850054404369f003376fbb95aea8d98efbf69ffad06bd6b4cc5783c8a52d247a39139acce9cf43807a52eda2785ccbb422d3af3378fc4a9f651a17b1618790b0caeb674606de9a25b952f83fef11924873b4525d170e55c9aeed866d953a65975359754fd0a7e02f77a4363b1dd93671e035eb74fec1bc9c3f25d46abdfb30149173dd6c761e85688dc947c892377e4b5b212af10c12ac01f1557cbf9b89beb9672d00ee7801d26710dfcba3e40dee2281cc2b484d7f86ecd5f8e62f0fa4e8bca9d3f2b8211d5a829f54d8c7d7f4a0f6f3014be8621c632346a6c9e644b9220c8d348e1a4e0dbedd4a7b022553113

然后接下来的逻辑:

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
00489 T46924 check.py:False debug_func off=0896 LOAD_CONST None
00490 T46924 check.py:False debug_func off=0898 LOAD_CONST None
00491 T46924 check.py:False debug_func off=0900 LOAD_CONST None
00492 T46924 check.py:False debug_func off=0902 LOAD_CONST None
00493 T46924 check.py:False debug_func off=0904 LOAD_CONST None
00494 T46924 check.py:False debug_func off=0906 EXTENDED_ARG
00495 T46924 check.py:False debug_func off=0908 EXTENDED_ARG
00496 T46924 check.py:False debug_func off=0910 EXTENDED_ARG
00497 T46924 check.py:? debug_func off=0912 LOAD_FAST
00498 T46924 check.py:? debug_func off=0914 LOAD_FAST
00499 T46924 check.py:? debug_func off=0916 EXTENDED_ARG
00500 T46924 check.py:? debug_func off=0918 LOAD_FAST
00501 T46924 check.py:? debug_func off=0920 LOAD_FAST
...
00538 T46924 check.py:? debug_func off=0998 EXTENDED_ARG
00539 T46924 check.py:? debug_func off=1000 LOAD_FAST
00540 T46924 check.py:? debug_func off=1002 LOAD_FAST
00541 T46924 check.py:? debug_func off=1004 BUILD_STRING
00542 T46924 check.py:? debug_func off=1006 CALL
00543 T46924 trace.py:118 run_pyc_with_trace.<locals>._fake_input off=0004 LOAD_FAST prompt
00544 T46924 trace.py:118 run_pyc_with_trace.<locals>._fake_input off=0006 POP_JUMP_IF_NONE to L1
00545 T46924 trace.py:120 run_pyc_with_trace.<locals>._fake_input off=0010 NOP
00546 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0012 LOAD_GLOBAL sys
00547 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0022 LOAD_ATTR stdout
00548 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0042 LOAD_ATTR write + NULL|self
00549 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0062 LOAD_GLOBAL str + NULL
00550 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0072 LOAD_FAST prompt
00551 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0074 CALL
00552 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0082 CALL
00553 T46924 trace.py:121 run_pyc_with_trace.<locals>._fake_input off=0090 POP_TOP
00554 T46924 trace.py:122 run_pyc_with_trace.<locals>._fake_input off=0092 LOAD_GLOBAL sys
00555 T46924 trace.py:122 run_pyc_with_trace.<locals>._fake_input off=0102 LOAD_ATTR stdout
00556 T46924 trace.py:122 run_pyc_with_trace.<locals>._fake_input off=0122 LOAD_ATTR flush + NULL|self
00557 T46924 trace.py:122 run_pyc_with_trace.<locals>._fake_input off=0142 CALL
00558 T46924 trace.py:122 run_pyc_with_trace.<locals>._fake_input off=0150 POP_TOP
00559 T46924 trace.py:125 run_pyc_with_trace.<locals>._fake_input off=0152 LOAD_DEREF send_line
00560 T46924 trace.py:125 run_pyc_with_trace.<locals>._fake_input off=0154 RETURN_VALUE
00561 T46924 check.py:? debug_func off=1014 LOAD_FAST
00562 T46924 check.py:? debug_func off=1016 LOAD_FAST
00563 T46924 check.py:? debug_func off=1018 BUILD_STRING
00564 T46924 check.py:? debug_func off=1020 CALL

在这个call处,如果文件不存在,则后续进入报错并退出,否则继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00565 T46924 check.py:? debug_func off=1028 PUSH_NULL
00566 T46924 check.py:? debug_func off=1030 EXTENDED_ARG
00567 T46924 check.py:? debug_func off=1032 EXTENDED_ARG
00568 T46924 check.py:? debug_func off=1034 EXTENDED_ARG
00569 T46924 check.py:? debug_func off=1036 LOAD_FAST
00570 T46924 check.py:? debug_func off=1038 LOAD_FAST
00571 T46924 check.py:? debug_func off=1040 LOAD_FAST
00572 T46924 check.py:? debug_func off=1042 LOAD_FAST
...
00696 T46924 check.py:? debug_func off=1350 BUILD_STRING
00697 T46924 check.py:? debug_func off=1352 BINARY_SUBSCR
00698 T46924 check.py:? debug_func off=1356 PUSH_NULL
00699 T46924 check.py:? debug_func off=1358 EXTENDED_ARG
00700 T46924 check.py:? debug_func off=1360 LOAD_FAST
00701 T46924 check.py:? debug_func off=1362 BUILD_STRING
00702 T46924 check.py:? debug_func off=1364 CALL
00703 T46924 check.py:? debug_func off=1372 CALL

在这个call后程序退出,在此之前有大量加载指令,尝试把这些加载指令的参数也hook出来,发现hook不到,该思路不通.

回归到反编译问题中,注意到dis.dis迅速崩溃,解析发现是因为存在大量指令47 FF 47 FF 47 FF 55 FB,对应的逻辑是LOAD_FAST(4294967291),这是根本不可能的,猜测是无效代码,批量替换为0x30(NOP)或修改成加载其他索引发现运行崩溃,说明在0xFFFFFFFB的位置真的有一个局部变量,或者是某种其他混淆操作,导致dis完全无法解析,尝试手动模拟pythonVM来解析:

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
pyc_path = 'output.pyc'
with open(pyc_path, 'rb') as f: bin = f.read()

const_pool = [None] + [i for i in range(256)] + [i for i in range(32, 127)]
cptr = 0x2C
ARG = 0
CON = 0
EXT_NUM = 0
CON_STK = []
STACK = []
RES = []

while cptr < len(bin):
if bin[cptr] == 0x47: # EXTENDED_ARG
arg = bin[cptr+1]
EXT_NUM += 1
ARG += arg << (8*EXT_NUM)
print(f"[{hex(cptr)}]\t\tEXTENDED_ARG\t{hex(arg)}")
cptr += 2
elif bin[cptr] == 0x53: # LOAD_CONST
ARG += bin[cptr+1]
CON = const_pool[ARG]
if CON is not None: CON_STK.append(CON)
print(f"[{hex(cptr)}]\t\tLOAD_CONST\tidx[0x{ARG:08x}]\t\t{bytes([CON]) if not CON is None else CON}")
ARG = 0
EXT_NUM = 0
cptr += 2
elif bin[cptr] == 0x55: # LOAD_FAST
ARG += bin[cptr+1]
if ARG < 351:
STACK.append(CON_STK[ARG])
print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]\t\t{bytes([CON_STK[ARG]])}")
else:
print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]")
ARG = 0
EXT_NUM = 0
cptr += 2
elif bin[cptr] == 0x33: # BUILD_STRING
tmp = bytes(STACK)
RES.append(tmp)
print(f"[{hex(cptr)}]\t\tBUILD_STRING\t{hex(bin[cptr+1])}\t\t\t{tmp}")
STACK = []
cptr += 2
elif bin[cptr] == 0x05: # BINARY_SUBSCR
print(f"[{hex(cptr)}]\t\tBINARY_SUBSCR")
cptr += 4
elif bin[cptr] == 0x22: # PUSH_NULL
print(f"[{hex(cptr)}]\t\tPUSH_NULL")
cptr += 2
elif bin[cptr] == 0x35: # CALL
print(f"[{hex(cptr)}]\t\tCALL\t\t{hex(bin[cptr+1])}")
cptr += 8
else:
cptr+=2
print(RES)

过程中注意到以下输出:

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
[0x3bc]         LOAD_FAST       idx[0x000000c0]         b'o'
[0x3be] EXTENDED_ARG 0x1
[0x3c0] LOAD_FAST idx[0x00000128] b'p'
[0x3c2] LOAD_FAST idx[0x00000088] b'e'
[0x3c4] LOAD_FAST idx[0x0000005d] b'n'
[0x3c6] BUILD_STRING 0x4 b'open'
[0x3c8] BINARY_SUBSCR
[0x3cc] PUSH_NULL
[0x3ce] EXTENDED_ARG 0xff
[0x3d0] EXTENDED_ARG 0xff
[0x3d2] EXTENDED_ARG 0xff
[0x3d4] LOAD_FAST idx[0xfffffffb]
[0x3d6] EXTENDED_ARG 0x1
[0x3d8] LOAD_FAST idx[0x00000102] b'i'
[0x3da] LOAD_FAST idx[0x0000005d] b'n'
[0x3dc] EXTENDED_ARG 0x1
[0x3de] LOAD_FAST idx[0x00000128] b'p'
[0x3e0] EXTENDED_ARG 0x1
[0x3e2] LOAD_FAST idx[0x00000147] b'u'
[0x3e4] LOAD_FAST idx[0x000000f1] b't'
[0x3e6] BUILD_STRING 0x5 b'input'
[0x3e8] BINARY_SUBSCR
[0x3ec] PUSH_NULL
[0x3ee] LOAD_FAST idx[0x00000073] b'Y'
[0x3f0] LOAD_FAST idx[0x000000c0] b'o'
[0x3f2] EXTENDED_ARG 0x1
[0x3f4] LOAD_FAST idx[0x00000147] b'u'
[0x3f6] LOAD_FAST idx[0x0000000f] b'r'
[0x3f8] LOAD_FAST idx[0x0000008a] b' '
[0x3fa] LOAD_FAST idx[0x000000b7] b'f'
[0x3fc] LOAD_FAST idx[0x00000000] b'l'
[0x3fe] LOAD_FAST idx[0x00000013] b'a'
[0x400] LOAD_FAST idx[0x0000000b] b'g'
[0x402] LOAD_FAST idx[0x0000008a] b' '
[0x404] LOAD_FAST idx[0x000000b7] b'f'
[0x406] EXTENDED_ARG 0x1
[0x408] LOAD_FAST idx[0x00000102] b'i'
[0x40a] LOAD_FAST idx[0x00000000] b'l'
[0x40c] LOAD_FAST idx[0x00000088] b'e'
[0x40e] LOAD_FAST idx[0x0000008a] b' '
[0x410] EXTENDED_ARG 0x1
[0x412] LOAD_FAST idx[0x0000014e] b'>'
[0x414] LOAD_FAST idx[0x0000008a] b' '
[0x416] BUILD_STRING 0x11 b'Your flag file > '
[0x418] CALL 0x1
[0x420] LOAD_FAST idx[0x0000000f] b'r'
[0x422] LOAD_FAST idx[0x000000cb] b'b'
[0x424] BUILD_STRING 0x2 b'rb'
[0x426] CALL 0x2
[0x42e] PUSH_NULL
[0x430] EXTENDED_ARG 0xff
[0x432] EXTENDED_ARG 0xff
[0x434] EXTENDED_ARG 0xff
[0x436] LOAD_FAST idx[0xfffffffb]
[0x438] LOAD_FAST idx[0x0000000b] b'g'
[0x43a] LOAD_FAST idx[0x00000088] b'e'
[0x43c] LOAD_FAST idx[0x000000f1] b't'
[0x43e] LOAD_FAST idx[0x00000013] b'a'
[0x440] LOAD_FAST idx[0x000000f1] b't'
[0x442] LOAD_FAST idx[0x000000f1] b't'
[0x444] LOAD_FAST idx[0x0000000f] b'r'
[0x446] BUILD_STRING 0x7 b'getattr'
[0x448] BINARY_SUBSCR
[0x44e] LOAD_FAST idx[0x0000000f] b'r'
[0x450] LOAD_FAST idx[0x00000088] b'e'
[0x452] LOAD_FAST idx[0x00000013] b'a'
[0x454] LOAD_FAST idx[0x000000e7] b'd'
[0x456] BUILD_STRING 0x4 b'read'
[0x458] CALL 0x2
[0x460] PUSH_NULL
[0x462] CALL 0x0
[0x46a] EXTENDED_ARG 0x1
[0x46e] EXTENDED_ARG 0xff
[0x470] EXTENDED_ARG 0xff
[0x472] EXTENDED_ARG 0xff
[0x474] LOAD_FAST idx[0xffffff01fb]
[0x476] LOAD_FAST idx[0x00000000] b'l'
[0x478] LOAD_FAST idx[0x00000088] b'e'
[0x47a] LOAD_FAST idx[0x0000005d] b'n'
[0x47c] BUILD_STRING 0x3 b'len'
[0x47e] BINARY_SUBSCR
[0x482] PUSH_NULL
[0x484] EXTENDED_ARG 0x1
[0x486] LOAD_FAST idx[0x0000015f]
[0x488] CALL 0x1
[0x490] EXTENDED_ARG 0x1
[0x494] EXTENDED_ARG 0x1
[0x496] LOAD_FAST idx[0x00010160]
[0x498] EXTENDED_ARG 0xff
[0x49a] EXTENDED_ARG 0xff
[0x49c] EXTENDED_ARG 0xff
[0x49e] LOAD_FAST idx[0xfffffffb]
[0x4a0] EXTENDED_ARG 0x1
[0x4a2] LOAD_FAST idx[0x00000102] b'i'
[0x4a4] LOAD_FAST idx[0x0000005d] b'n'
[0x4a6] LOAD_FAST idx[0x000000f1] b't'
[0x4a8] BUILD_STRING 0x3 b'int'
[0x4aa] BINARY_SUBSCR
[0x4ae] PUSH_NULL
[0x4b0] LOAD_FAST idx[0x00000063] b'3'
[0x4b2] EXTENDED_ARG 0x1
[0x4b4] LOAD_FAST idx[0x00000156] b'9'
[0x4b6] EXTENDED_ARG 0x1
[0x4b8] LOAD_FAST idx[0x00000156] b'9'
[0x4ba] LOAD_FAST idx[0x000000cd] b'4'
[0x4bc] LOAD_FAST idx[0x000000cd] b'4'
[0x4be] BUILD_STRING 0x5 b'39944'
[0x4c0] CALL 0x1
[0x4e6] EXTENDED_ARG 0xff
[0x4e8] EXTENDED_ARG 0xff
[0x4ea] EXTENDED_ARG 0xff
[0x4ec] LOAD_FAST idx[0xfffffffb]
[0x4ee] EXTENDED_ARG 0x1
[0x4f0] LOAD_FAST idx[0x00000128] b'p'
[0x4f2] LOAD_FAST idx[0x0000000f] b'r'
[0x4f4] EXTENDED_ARG 0x1
[0x4f6] LOAD_FAST idx[0x00000102] b'i'
[0x4f8] LOAD_FAST idx[0x0000005d] b'n'
[0x4fa] LOAD_FAST idx[0x000000f1] b't'
[0x4fc] BUILD_STRING 0x5 b'print'
[0x4fe] BINARY_SUBSCR
[0x502] PUSH_NULL
[0x504] LOAD_FAST idx[0x0000004f] b'I'
[0x506] LOAD_FAST idx[0x0000005d] b'n'
[0x508] LOAD_FAST idx[0x000000fb] b'c'
[0x50a] LOAD_FAST idx[0x000000c0] b'o'
[0x50c] LOAD_FAST idx[0x0000000f] b'r'
[0x50e] LOAD_FAST idx[0x0000000f] b'r'
[0x510] LOAD_FAST idx[0x00000088] b'e'
[0x512] LOAD_FAST idx[0x000000fb] b'c'
[0x514] LOAD_FAST idx[0x000000f1] b't'
[0x516] LOAD_FAST idx[0x0000008a] b' '
[0x518] EXTENDED_ARG 0x1
[0x51a] LOAD_FAST idx[0x00000110] b':'
[0x51c] LOAD_FAST idx[0x000000cc] b'('
[0x51e] BUILD_STRING 0xc b'Incorrect :('
[0x520] CALL 0x1
[0x546] EXTENDED_ARG 0xff
[0x548] EXTENDED_ARG 0xff
[0x54a] EXTENDED_ARG 0xff
[0x54c] LOAD_FAST idx[0xfffffffb]
[0x54e] LOAD_FAST idx[0x00000088] b'e'
[0x550] LOAD_FAST idx[0x0000009f] b'x'
[0x552] EXTENDED_ARG 0x1
[0x554] LOAD_FAST idx[0x00000102] b'i'
[0x556] LOAD_FAST idx[0x000000f1] b't'
[0x558] BUILD_STRING 0x4 b'exit'
[0x55a] BINARY_SUBSCR
[0x55e] PUSH_NULL
[0x560] EXTENDED_ARG 0xff
[0x562] EXTENDED_ARG 0xff
[0x564] EXTENDED_ARG 0xff
[0x566] LOAD_FAST idx[0xfffffffb]
[0x568] EXTENDED_ARG 0x1
[0x56a] LOAD_FAST idx[0x00000102] b'i'
[0x56c] LOAD_FAST idx[0x0000005d] b'n'
[0x56e] LOAD_FAST idx[0x000000f1] b't'
[0x570] BUILD_STRING 0x3 b'int'
[0x572] BINARY_SUBSCR
[0x576] PUSH_NULL
[0x578] EXTENDED_ARG 0x1
[0x57a] LOAD_FAST idx[0x00000129] b'1'
[0x57c] BUILD_STRING 0x1 b'1'
[0x57e] CALL 0x1
[0x586] CALL 0x1
[0x5a0] EXTENDED_ARG 0xff
[0x5a2] EXTENDED_ARG 0xff
[0x5a4] EXTENDED_ARG 0xff
[0x5a6] LOAD_FAST idx[0xfffffffb]
[0x5a8] EXTENDED_ARG 0x1
[0x5aa] LOAD_FAST idx[0x00000102] b'i'
[0x5ac] LOAD_FAST idx[0x0000005d] b'n'
[0x5ae] LOAD_FAST idx[0x000000f1] b't'
[0x5b0] BUILD_STRING 0x3 b'int'
[0x5b2] BINARY_SUBSCR
[0x5b6] PUSH_NULL
[0x5b8] EXTENDED_ARG 0x1
[0x5ba] LOAD_FAST idx[0x0000011a] b'0'
[0x5bc] BUILD_STRING 0x1 b'0'
[0x5be] CALL 0x1
[0x5c6] EXTENDED_ARG 0x1
[0x5ca] EXTENDED_ARG 0x1
[0x5cc] LOAD_FAST idx[0x00010161]
[0x5ce] EXTENDED_ARG 0x1
[0x5d0] LOAD_FAST idx[0x0000015f]
[0x5d2] EXTENDED_ARG 0xff
[0x5d4] EXTENDED_ARG 0xff
[0x5d6] EXTENDED_ARG 0xff
[0x5d8] LOAD_FAST idx[0xfffffffb]
[0x5da] EXTENDED_ARG 0x1
[0x5dc] LOAD_FAST idx[0x00000102] b'i'
[0x5de] LOAD_FAST idx[0x0000005d] b'n'
[0x5e0] LOAD_FAST idx[0x000000f1] b't'
[0x5e2] BUILD_STRING 0x3 b'int'
[0x5e4] BINARY_SUBSCR
[0x5e8] PUSH_NULL
[0x5ea] EXTENDED_ARG 0x1
[0x5ec] LOAD_FAST idx[0x0000011a] b'0'
[0x5ee] BUILD_STRING 0x1 b'0'

注意到文件长度应该是39944,后续看起来像很多比较操作

RES是一个43万项的数组,其中有所有的string和int,去除string,得到22万组数据,查看前几项:

1
2
3
4
5
6
7
8
9
10
39944, 1, # exit(1)
0, 0, 196, 71, 255, 148,
1, 70, 255, 102, 240,
2, 69, 255, 68, 77,
3, 5, 255, 160, 226,
4, 6, 4, 2, 255, 136, 203,
5, 252, 14, 129, 51, 74,
6, 81, 166, 124, 199, 86,
7, 163, 94, 255, 75,
...

发现并不是简单的直接比较,放弃trace,直接同构PythonVM虚拟机(opcode来自github上的cpython3.13):

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
pyc_path = 'output.pyc'
with open(pyc_path, 'rb') as f: bin = f.read()

const_pool = [None] + [i for i in range(256)] + [i for i in range(32, 127)]
cptr = 0x2C
ARG = 0
CON = 0
EXT_NUM = 0
VAL_LST = [] # 局部变量表
STACK = [] # 栈
RES = []
VAL_INT = False # build_string时是否为int
BOL_RES = False
BOOLS = []
NB_OPS = [
"NB_ADD", # 0
"NB_AND", # 1
"NB_FLOOR_DIVIDE", # 2
"NB_LSHIFT", # 3
"NB_MATRIX_MULTIPLY", # 4
"NB_MULTIPLY", # 5
"NB_REMAINDER", # 6
"NB_OR", # 7
"NB_POWER", # 8
"NB_RSHIFT", # 9
"NB_SUBTRACT", # 10
"NB_TRUE_DIVIDE", # 11
"NB_XOR", # 12
"NB_INPLACE_ADD", # 13
"NB_INPLACE_AND", # 14
"NB_INPLACE_FLOOR_DIVIDE", # 15
"NB_INPLACE_LSHIFT", # 16
"NB_INPLACE_MATRIX_MULTIPLY", # 17
"NB_INPLACE_MULTIPLY", # 18
"NB_INPLACE_REMAINDER", # 19
"NB_INPLACE_OR", # 20
"NB_INPLACE_POWER", # 21
"NB_INPLACE_RSHIFT", # 22
"NB_INPLACE_SUBTRACT", # 23
"NB_INPLACE_TRUE_DIVIDE", # 24
"NB_INPLACE_XOR", # 25
]
TMP_EXP = []
EXPS = []

debug = False
allow_junk = False

payload_path = 'flag.txt'
with open(payload_path, "w") as f: f.write("T" * 39944)
with open(payload_path, 'rb') as f: payload = f.read()

SF_TIMES = -2
def getFast():
global SF_TIMES
if SF_TIMES == -2: res = "payload"
elif SF_TIMES == -1: res = len(payload)
else:
res = payload[SF_TIMES]
STACK.append(res)
SF_TIMES += 1
return res

try:
while cptr < len(bin):
# while cptr < 0x1000:
if bin[cptr] == 0x47: # EXTENDED_ARG
arg = bin[cptr+1]
EXT_NUM += 1
ARG += arg << (8*EXT_NUM)
if debug: print(f"[{hex(cptr)}]\t\tEXTENDED_ARG\t{hex(arg)}")
elif bin[cptr] == 0x53: # LOAD_CONST
ARG += bin[cptr+1]
CON = const_pool[ARG]
if CON is not None: VAL_LST.append(CON)
if debug: print(f"[{hex(cptr)}]\t\tLOAD_CONST\tidx[0x{ARG:08x}]\t\t{bytes([CON]) if not CON is None else CON}")
ARG = 0
EXT_NUM = 0
elif bin[cptr] == 0x55: # LOAD_FAST
ARG += bin[cptr+1]
if ARG > 0xFFFF:
if debug: print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]")
elif ARG != 0x15f and cptr > 0x5A0:
# print(hex(ARG))
STACK.append(VAL_LST[ARG])
if debug:
try: # 整数
val = int(VAL_LST[ARG])
print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]\t\t{bytes([val]) if val in range(256) else val}")
except: # 字符串
print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]\t\t{VAL_LST[ARG]}")
else:
if debug: print(f"[{hex(cptr)}]\t\tLOAD_FAST\tidx[0x{ARG:08x}]")
ARG = 0
EXT_NUM = 0
elif bin[cptr] == 0x6E: # STORE_FAST
ARG += bin[cptr+1]
size = len(VAL_LST)
if size <= ARG: VAL_LST += [0]*(ARG+1-size)
VAL_LST[ARG] = getFast()
if debug: print(f"[{hex(cptr)}]\t\tSTORE_FAST\tidx[0x{ARG:08x}]\t\t{VAL_LST[ARG]}")
STACK = []
ARG = 0
EXT_NUM = 0
elif bin[cptr] == 0x33: # BUILD_STRING
# print(STACK)
tmp = bytes(STACK[-bin[cptr+1]:])
STACK = STACK[:-bin[cptr+1]]
if VAL_INT == True and not cptr == 0x57C: # 这里有一个exit(1)
STACK.append(int(tmp.decode()))
if int(tmp.decode()) + 1 != SF_TIMES: TMP_EXP.append(int(tmp.decode())) # 将操作数载入表达式结构体
# print(STACK)
RES.append(tmp)
if debug: print(f"[{hex(cptr)}]\t\tBUILD_STRING\t{hex(bin[cptr+1])}\t\t\t{tmp}")
VAL_INT = True if tmp == b'int'else False
# print(STACK)
elif bin[cptr] == 0x05: # BINARY_SUBSCR
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tBINARY_SUBSCR")
elif bin[cptr] == 0x22: # PUSH_NULL
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tPUSH_NULL")
elif bin[cptr] == 0x35: # CALL
if debug:
if bin[cptr+1] == 1: print(f"[{hex(cptr)}]\t\tCALL\t\t{hex(bin[cptr+1])}\t\t\tVALUE_CONSTRUCT")
else: print(f"[{hex(cptr)}]\t\tCALL\t\t{hex(bin[cptr+1])}")
elif bin[cptr] == 0x73: # SWAP
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tSWAP\t\t{hex(bin[cptr+1])}")
elif bin[cptr] == 0x3D: # COPY
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tCOPY\t\t{hex(bin[cptr+1])}")
elif bin[cptr] == 0x3A: # COMPARE_OP
# print(STACK)
# print(TMP_EXP)
if len(TMP_EXP) != 0: EXPS.append(TMP_EXP[1:] if TMP_EXP[0] == 'NB_ADD' else TMP_EXP) # 在这里转移表达式
if len(STACK) >= 2:
BOL_RES = True if STACK[0] == STACK[-1] else False
if debug: print(f"[{hex(cptr)}]\t\tCOMPARE_OP\t{hex(bin[cptr+1])}\t\t\t{STACK[-2]} == {STACK[-1]}? => {BOL_RES}")
STACK = STACK[1:-1]
else:
if debug: print(f"[{hex(cptr)}]\t\tCOMPARE_OP\t{hex(bin[cptr+1])}")
TMP_EXP = []
# print(STACK)
elif bin[cptr] == 0x28: # TO_BOOL
BOOLS.append(BOL_RES)
if debug: print(f"[{hex(cptr)}]\t\tTO_BOOL\t\t{BOL_RES}")
elif bin[cptr] == 0x1E: # NOP
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tNOP")
elif bin[cptr] == 0x61: # POP_JUMP_IF_FALSE
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tPOP_JUMP_IF_FALSE\t\t\t{hex(bin[cptr+1])}")
elif bin[cptr] == 0x20: # POP_TOP
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tPOP_TOP\t\t{STACK}")
elif bin[cptr] == 0x4F: # JUMP_FORWARD
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tJUMP_FORWARD\t{hex(bin[cptr+1])}")
elif bin[cptr] == 0x2D: # BINARY_OP
TMP_EXP.append(NB_OPS[bin[cptr+1]]) # 在这里装入运算符
if debug: print(f"[{hex(cptr)}]\t\tBINARY_OP\t{NB_OPS[bin[cptr+1]]}")
elif bin[cptr] == 0x00: # CACHE
if debug and allow_junk: print(f"[{hex(cptr)}]\t\tCACHE")
else: # UNK
if debug: print(f"[{hex(cptr)}]\t\tUNKNOWN\t\t{bin[cptr]}")
break
debug = False
# debug= True
# debug = True if cptr in range(0x5A0, 0x800) else False
# if debug: print(STACK)
cptr += 2
# print(RES)
except: # 最后会崩溃退出
for i in range(len(EXPS)): print(EXPS[i])

利用python的值加载规则即可提取参数和运算符,处理输出的EXP表达式列表(上面的脚本把所有和索引相同的数值删掉了,所以在前面会有几处丢失,在丢失位补上索引即可)(29、54、141、228行末项,52、78、98行xor后方,79、148行首项,158、174行xor前方,256行and前方):

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
def apply_op(left, op_str, right):
left &= 0xFF
right &= 0xFF
if op_str == 'NB_XOR':
return (left ^ right) & 0xFF
elif op_str == 'NB_ADD':
return (left + right) & 0xFF
elif op_str == 'NB_SUBTRACT':
return (left - right) & 0xFF
elif op_str == 'NB_AND':
return (left & right) & 0xFF
elif op_str == 'NB_OR':
return (left | right) & 0xFF
elif op_str == 'NB_LSHIFT':
return (left << (right & 0x1F)) & 0xFF
elif op_str == 'NB_RSHIFT':
return (left >> (right & 0x1F)) & 0xFF
else:
raise ValueError(f"Unknown op: {op_str}")

def solve_line(tokens_with_target):
target = tokens_with_target[-1]
tokens = tokens_with_target[:-1]
if ('NB_LSHIFT' in tokens and
'NB_RSHIFT' in tokens and
'NB_OR' in tokens):
try:
lshift_idx = tokens.index('NB_LSHIFT')
rshift_idx = tokens.index('NB_RSHIFT')
or_idx = tokens.index('NB_OR')
if (lshift_idx > 0 and rshift_idx > 0 and
or_idx > rshift_idx and
or_idx == rshift_idx + 1 and
rshift_idx == lshift_idx + 2):
a = tokens[lshift_idx - 1]
b = tokens[rshift_idx - 1]
rest = tokens[or_idx + 1:]
for m in range(256):
val = ((m << a) | (m >> b)) & 0xFF
current = val
i = 0
ok = True
while i < len(rest):
if i + 1 >= len(rest):
ok = False
break
num = rest[i]
op = rest[i+1]
if not isinstance(num, int) or not isinstance(op, str):
ok = False
break
current = apply_op(current, op, num)
i += 2
if ok and current == target: return m
except: pass
for m in range(256):
current = m
i = 0
ok = True
while i < len(tokens):
if i + 1 >= len(tokens):
ok = False
break
num = tokens[i]
op = tokens[i+1]
if not isinstance(num, int) or not isinstance(op, str):
ok = False
break
current = apply_op(current, op, num)
i += 2
if ok and current == target: return m
return None

results = []
with open("exp.txt", "r") as f:
for line in f:
line = line.strip()
if not line: continue
import ast
tokens = ast.literal_eval(line)
m = solve_line(tokens)
if m is None: raise RuntimeError(f"Failed to solve line: {line}")
results.append(m)

with open("recover.txt", "wb") as f: f.write(bytes(results))
print(f"Recovered {len(results)} bytes.")

恢复出来是一张图片:

infobahn{this_is_by_far_the_worst_obfuscator_ive_had_the_displeasure_of_writing_i_dont_even_know_how_the_code_ran_e891ac534881}

最终两个脚本可以合并:

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
with open('output.pyc', 'rb') as f: bin = f.read()
CON_POL = [None] + [i for i in range(256)] + [i for i in range(32, 127)]
ARG, CON, EXT_NUM = 0, 0, 0
VAL_INT = False
NB_OPS = ["+", "&", "", "<<", "", "*", "", "|", "", ">>", "-", "", "^"]
VAL_LST, STACK, EXP, RES = [], [], [], bytearray()

def solve_exp(EXP): # Reverse all expressions
if "|" in EXP: RES.append((((EXP[-1] ^ EXP[-3]) << EXP[-8]) | ((EXP[-1] ^ EXP[-3]) >> EXP[-11])) & 0xFF)
elif len(EXP) > 1 and len(EXP) < 12:
for i in range(len(EXP)-2, 0, -2):
if EXP[i] == '^': EXP[-1] ^= EXP[i-1]
elif EXP[i] == '+': EXP[-1] -= EXP[i-1]
elif EXP[i] == '-': EXP[-1] += EXP[i-1]
RES.append(EXP[-1] & 0xFF)
return []

for cptr in range(0x2C, len(bin), 2):
if bin[cptr] == 0x47: EXT_NUM += 1; ARG += bin[cptr+1] << (8*EXT_NUM) # EXTENDED_ARG
elif bin[cptr] == 0x53 or bin[cptr] == 0x55 or bin[cptr] == 0x6E: # LOAD_CONST, LOAD_FAST, STORE_FAST
ARG += bin[cptr+1]
if bin[cptr] == 0x53: VAL_LST.append(CON_POL[ARG]) # LOAD_CONST
if bin[cptr] == 0x55: STACK += [VAL_LST[ARG]] if ARG < 0x15f and cptr > 0x5D0 else [] # LOAD_FAST
if bin[cptr] == 0x6E: VAL_LST += [0]*(ARG+1-len(VAL_LST)) if len(VAL_LST) <= ARG else []; VAL_LST[ARG] = 0 # STORE_FAST
ARG, EXT_NUM = 0, 0
elif bin[cptr] == 0x33: EXP += [int(bytes(STACK[-bin[cptr+1]:]).decode())] if VAL_INT else []; VAL_INT = True if bytes(STACK[-bin[cptr+1]:]) == b'int'else False # BUILD_STRING,load values here
elif bin[cptr] == 0x2D: EXP.append(NB_OPS[bin[cptr+1]]) # BINARY_OP,load ops here
elif bin[cptr] == 0x3A: EXP = solve_exp(EXP) # COMPARE_OP,solve exps here

with open('flag.png', 'wb') as f: f.write(RES)