“华为杯”第四届中国研究生网络安全创新大赛 Megrez RE WP

本次研究生赛我们排名第6,解出8题
成功晋级
这是与我相关的一些题目

image-20251030104027894

Reverse

YJS-GoEnc

IDA 打开,直接转到main函数看逻辑

main_main中的逻辑校验

如图,并且选择前16位作为key

main_encryptWithAES看出来为AES加密,并且为CBC模式

最后进行Base64编码

密文为base64的串交叉引用即可找到

key在这里做了变换

exp:

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
import base64

from Crypto.Cipher import AES

KEY_SOURCE = bytearray(b"MySecretKey123451234567890abcdef/proc/self/auxv")
STATE_BYTES = bytes([
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22,
0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00,
])
BLOCK_SIZE = 16
ROUNDS = 6
CIPHERTEXT_B64 = "JBaoWyDrnxq47qscXupR5W+zxqUHT/Z0h9Qjh97aSuM09nZH7AauwGLHhDE1KKdj"
IV_BYTES = b"1234567890abcdef"

def rotl(byte: int, count: int) -> int:
byte &= 0xFF
return ((byte << count) | (byte >> (8 - count))) & 0xFF


def scramble_key(seed: bytearray) -> bytes:
key = bytearray(seed)
iteration = 0
for _ in range(ROUNDS):
step = (23 * iteration + 0x4D) & 0xFF
for idx in range(BLOCK_SIZE):
val = key[idx] ^ step
val = rotl(val, 2)
val ^= STATE_BYTES[idx % len(STATE_BYTES)]
val = (val + iteration + 1) & 0xFF
key[idx] = val
iteration += 1
return bytes(key[:BLOCK_SIZE])


def unpad(data: bytes) -> bytes:
pad_len = data[-1]
if not 1 <= pad_len <= BLOCK_SIZE or data[-pad_len:] != bytes([pad_len]) * pad_len:
raise ValueError("invalid PKCS#7 padding")
return data[:-pad_len]


def main():
key = scramble_key(KEY_SOURCE)
cipher = base64.b64decode(CIPHERTEXT_B64)
plaintext = AES.new(key, AES.MODE_CBC, IV_BYTES).decrypt(cipher)
flag = unpad(plaintext).decode("utf-8")
print(flag)


if __name__ == "__main__":
main()

flag{ca083c88-30b7-447c-8e6a-e7470862abe7}

YJS-bc

直接用llvm-dis bc.bc output.ll去反编译得到一个文件去分析

逆向这个这个代码拿flag

程序用 strtok(“_”) 将整行拆成三段:第一段长度要正好 5、且都是 [0-9a-f];第二段用于 Speck-128/128;第三段逐字节进 AES S- box,对比常量 1a04434de3099a51c79f8500 的前 11 字节(末字节是填零)。

第一段:暴力 SHA-256 第一段的 SHA256(part1) 必须等于常量 eb3404…2fcb。写脚本枚举所有 5 位十六进制串即可。 b1a37

第二段:还原 Speck 明文 密钥由第一段拼上其 SHA 摘要前 11 字节组成 16 字节(小端两段,先扩展 32 轮 round key)。程序随后把输入第二段当作 16 字节 块,用相同流程加密后要匹配常量密文 502a7c…f5b9。依 Speck-128/128 逆向解密该密文得到明文 3peckenc0def1n@l。

第三段:逆 S-box 常量数组是标准 AES S-box 的表。运行时把第三段每个字节过 S-box 并与常量前 11 个字节比较;数组末尾 0x00 仅是填充。把常量前 11 字节通过逆 S-box 即得第三段 C0deM@7p1ng。

exp

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
#!/usr/bin/env python3
import hashlib
import itertools
import struct

TARGET_HASH = bytes.fromhex(
"eb34049ced1f583016dc8952c68aaa0b0fb1ed3920b6dc4089eaaf56b8402fcb"
)
TARGET_CIPHER = bytes.fromhex("502a7cc6bc967a36304e189a192df5b9")
SBOX = bytes([
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
])
TARGET_SUB = bytes.fromhex("1a04434de3099a51c79f8500")
MASK64 = (1 << 64) - 1

def ror64(x, r):
r &= 63
return ((x >> r) | (x << (64 - r))) & MASK64


def rol64(x, r):
r &= 63
return ((x << r) | (x >> (64 - r))) & MASK64


def expand_round_keys(a0, b0):
a, b = a0, b0
keys = []
for i in range(32):
keys.append(a)
b = ror64(b, 8)
b = (b + a) & MASK64
b ^= i
a = rol64(a, 3)
a ^= b
return keys


def decrypt_block(block, keys):
x, y = block
for k in reversed(keys):
x ^= y
x = ror64(x, 3)
y ^= k
y = (y - x) & MASK64
y = rol64(y, 8)
return x, y


def find_first_token():
alphabet = "0123456789abcdef"
for cand in map("".join, itertools.product(alphabet, repeat=5)):
if hashlib.sha256(cand.encode()).digest() == TARGET_HASH:
return cand
raise RuntimeError("no candidate found")


def invert_sbox_sequence(sequence):
inv = [0] * 256
for idx, val in enumerate(SBOX):
inv[val] = idx
return bytes(inv[b] for b in sequence)


def main():
part1 = find_first_token()
digest = hashlib.sha256(part1.encode()).digest()
key_material = part1.encode() + digest[:11]
k0, k1 = struct.unpack("<QQ", key_material)
round_keys = expand_round_keys(k0, k1)
cipher_block = struct.unpack("<QQ", TARGET_CIPHER)
plain_block = decrypt_block(cipher_block, round_keys)
part2 = struct.pack("<QQ", *plain_block)
part3 = invert_sbox_sequence(TARGET_SUB)

print("part1:", part1)
print("part2:", part2.decode())
print("part3:", part3.decode())
print("\nSubmit:")
print(f"{part1}_{part2.decode()}_{part3.decode()}")


if __name__ == "__main__":
main()

我解出来是flag{b1a37_3peckenc0def1n@l_C0deM@7p1ngR},提交错误

平台放了hint是41位长度,把最后一个R去掉即可

flag{b1a37_3peckenc0def1n@l_C0deM@7p1ng}

YJS-ooops 三种解法

魔改了Magic,修一下那个py解包脚本即可解包、

改成

1
MAGIC = b'swI\014\013\012\013\016'  # Magic number which identifies pyinstaller

你问我怎么发现魔改的?附件直接丢给AI它能扫出来

image-20251030104143587

这个opcode.pyc反编译失败,应该是做了些处理,我们尝试看一下它的字节码的反汇编代码

直接dis也会出现问题

用这个代码去解析

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
# analyze_ooops.py
from __future__ import print_function
import marshal
import types
import zlib
import binascii
import dis


def load_code(path):
with open(path, "rb") as f:
return marshal.loads(f.read())


def find_code_obj(root_co, target_name):
for const in root_co.co_consts:
if isinstance(const, types.CodeType):
if const.co_name == target_name:
return const
sub = find_code_obj(const, target_name)
if sub:
return sub
return None


def create_func_from_code(code_obj):
glb = {
"__builtins__": __builtins__,
"struct": __import__("struct"),
"zlib": __import__("zlib"),
"bytearray": bytearray,
"bytes": bytes,
"enumerate": enumerate,
"list": list,
"range": range,
"len": len,
}
return types.FunctionType(code_obj, glb, code_obj.co_name)


def show_info(code_obj):
print(dis.code_info(code_obj))
dis.dis(code_obj)
print("-" * 60)


def main():
root = load_code("s_ooops")

decrypt_co = find_code_obj(root, "decrypt_flag")
main_co = find_code_obj(root, "main")

print("=== decrypt_flag ===")
show_info(decrypt_co)

print("=== main ===")
show_info(main_co)

decrypt_flag = create_func_from_code(decrypt_co)
main_func = create_func_from_code(main_co)

state = {}

def fake_input(prompt):
state["prompt"] = prompt
return "FAKE{FLAG_PLACEHOLDER}"

def fake_anti_debug():
state["anti_debug_called"] = True

namespace = {
"decrypt_flag": decrypt_flag,
"anti_debug": fake_anti_debug,
"input": fake_input,
}

exec(main_co, namespace)

print("\n=== Results ===")
encrypted = namespace["enc_data"]
key = namespace["key"]
print("key hex:", binascii.hexlify(key).decode())
print("enc_data hex:", binascii.hexlify(encrypted).decode())

flag_bytes = decrypt_flag(encrypted, key)
print("flag bytes:", flag_bytes)
try:
print("flag string:", flag_bytes.decode())
except Exception:
print("flag hex:", binascii.hexlify(flag_bytes).decode())


if __name__ == "__main__":
main()

出现了

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
=== decrypt_flag ===
Name: decrypt_flag
Filename: ooops.py
Argument count: 2
Kw-only arguments: 0
Number of locals: 6
Stack size: 5
Flags: OPTIMIZED, NEWLOCALS
Constants:
0: None
1: '<I'
2: 4
3: 0
4: 1
5: 57005
6: 48879
7: <code object <genexpr> at 0x00000295FB676030, file "ooops.py", line 27>
8: 'decrypt_flag.<locals>.<genexpr>'
9: <code object <genexpr> at 0x00000295FB676270, file "ooops.py", line 29>
10: -1
11: -1
Names:
0: struct
1: unpack
2: bytearray
3: list
4: range
5: len
6: bytes
7: enumerate
8: zlib
9: decompress
Variable names:
0: data
1: key
2: size
3: indices
4: i
5: j
Cell variables:
0: data
1: key
18 >> 0 SETUP_LOOP 0 (to 3)
>> 3 LOAD_FAST 1 (key)
>> 6 CONTINUE_LOOP 1
>> 9 IMPORT_NAME 0 (struct)
12 CONTINUE_LOOP 0
15 CONTINUE_LOOP 2
18 BUILD_LIST 2
21 BINARY_POWER
22 CALL_FUNCTION 2 (2 positional, 0 keyword pair)
25 CONTINUE_LOOP 3
28 BINARY_POWER
29 SETUP_EXCEPT 2 (to 34)

19 32 IMPORT_NAME 0 (struct)
35 CONTINUE_LOOP 2
38 CONTINUE_LOOP 2
41 LOAD_CONST 2 (4)
44 BINARY_SUBSCR
45 BUILD_LIST 2
48 BINARY_POWER
49 LOAD_DEREF 0 (data)

22 52 SETUP_LOOP 2 (to 57)
55 IMPORT_NAME 0 (struct)
58 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
61 LOAD_DEREF 0 (data)

23 64 SETUP_LOOP 3 (to 70)
67 SETUP_LOOP 4 (to 74)
>> 70 SETUP_LOOP 5 (to 78)
73 IMPORT_NAME 0 (struct)
76 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
79 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
82 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
85 SETUP_EXCEPT 3 (to 91)
Traceback (most recent call last):
File "dis_pyc.py", line 94, in <module>
main()
File "dis_pyc.py", line 54, in main
show_info(decrypt_co)
File "dis_pyc.py", line 43, in show_info
dis.dis(code_obj)
File "D:\CodeTools\anaconda\envs\py35\lib\dis.py", line 58, in dis
disassemble(x, file=file)
File "D:\CodeTools\anaconda\envs\py35\lib\dis.py", line 319, in disassemble
co.co_consts, cell_names, linestarts, file=file)
File "D:\CodeTools\anaconda\envs\py35\lib\dis.py", line 330, in _disassemble_bytes
line_offset=line_offset):
File "D:\CodeTools\anaconda\envs\py35\lib\dis.py", line 304, in _get_instructions_bytes
argval = cmp_op[arg]
IndexError: tuple index out of range

的问题

Python 3.5 的 dis 模块在自定义或损坏的字节码时会崩溃,所以看到IndexError: tuple index out of range。说明出题人对这段字节码做过轻微改动,标准库的 dis 没办法继续下去

可以使用pycdc.exe

可以得到

image-20251030112158859

上面还有很长一段,还是混淆过的,关键逻辑处还是没有

看到的大量 INVALID、CONTINUE_LOOP 等异常指令,是这个pyc做了反编译对抗,混淆,或是用不完全匹配的解释器,工具链反汇编导致的错位

anti_debug():Windows 上用 ctypes.windll.kernel32.IsDebuggerPresent() 反调试,若为真,sys.exit(1)

还有正常的输入校验

还有一些data信息

没想到又问了一次GPT让它好好分析得到了

你贴出的那份 pycdas 输出本身是可信的整体结构,但「反汇编的指令」部分已经完全乱掉了——原因是 pycdas 仍然把 Python 3.5 的 wordcode 当成旧版 opcode 来读,从而把每一条指令拆成了 “真实 opcode + 0x00” 的组合,结果自然全是 、虚假的 SETUP_LOOP、BINARY_POWER、STORE_ATTR 59 等。要想真正还原函数,只能按 wordcode 重新读取(一次两个字节,第一字节 opcode,第二字节 0~255 参数,大于 255 通过 EXTENDED_ARG 累积)或者直接反编译出源码。

不过,光看 pycdas 列出的 meta 信息 就足够告诉我们 ooops.pyc 的结构,结合真正的反编译结果,逻辑非常清晰,关键点如下

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
import sys, struct, zlib, ctypes

def anti_debug():
kernel32 = ctypes.windll.kernel32
while kernel32.IsDebuggerPresent():
sys.exit(0)

def decrypt_flag(data, key):
size = struct.unpack("<I", data[:4])[0] # 取明文长度,但后面没再用
buf = bytearray(data[4:]) # 剩下 50 字节是密文

key_bytes = bytearray(key) # 常量 16-byte key
indices = [(i * 0xDEAD + 0xBEEF) & 0xFF
for i in range(len(buf))] # 利用 0xDEAD/0xBEEF 生成下标序列

for pos, idx in enumerate(indices):
k = key_bytes[pos % len(key_bytes)]
b = buf[pos]

# 实际上就是按 idx、k 做异或、加减、右移等位运算
buf[pos] = ((b ^ k) - idx) & 0xFF
key_bytes[pos % len(key_bytes)] = (k + 1) & 0xFF

return zlib.decompress(bytes(buf)) # 解压得到 flag

def main():
anti_debug()
key = b"\xc6;3\xce\xa0\xbd\xcfHyKO\x9b*\x19\xb2\xbf"
enc = (b"2\x00\x00\x00S\xff\x07/\x19\xcbf?@\xd4\xbe\xa7x"
user = input("Enter flag: ").encode()
except Exception:
print("Error: Invalid input")
return

decrypted = decrypt_flag(enc, key)
if user == decrypted:
print("Correct!")
else:
print("Wrong!")

if __name__ == "__main__":
main()

但是还是不大对

解法一: 修复pyc

怎么扰动的AI尝试了多遍AI还是分析不明白

Python 3.5 引入了 wordcode 格式:每条指令固定 2 个字节(第一个字节 opcode,第二个字节 8 位参数;超过 255 靠 EXTENDED_ARG 累积)。老式 “一字节 opcode + 可能的两字节参数” 的反汇编器会把第二个字节当作新指令,于是出现大量 、错行甚至越界。

出题方还稍微动了 opcode,例如 COMPARE_OP 参数可能是 89 这种超出标准比较运算表的值。标准库 dis 在解析这类指令时会抛 IndexError,必须自己写 wordcode 解析或用能容错的反编译器,如 uncompyle6,才能看到真实指令流。

看来偷不了懒

_opcode.cpython-35m-x86_64-linux-gnu.so

可以分析下这个so文件

image-20251030132530395

opcode>89会做这些操作

在libpython里面发现了opcode的表

image-20251030154436440

把这些在喂给了ai

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
#!/usr/bin/env python3
# patch_and_run_subprocess.py
# 作用:用 provided MODIFIED 映射修补 ooops.pyc -> ooops-patch.pyc,然后在独立子进程(python3.5)里运行它
# 目的:避免在当前进程 exec 导致 PyEval_EvalFrameEx 崩溃

import sys, os, marshal, dis, types, subprocess, tempfile, shutil

# -----------------------
# 把你已有的 MODIFIED mapping 粘贴到这里(modified byte -> opname)
MODIFIED = {
0x1: "INPLACE_TRUE_DIVIDE", 0x2: "INPLACE_ADD", 0x3: "WITH_CLEANUP_FINISH",
0x4: "STORE_SUBSCR", 0x5: "ROT_THREE", 0x9: "INPLACE_LSHIFT", 0xA: "BINARY_LSHIFT",
0xB: "BINARY_TRUE_DIVIDE", 0xC: "BINARY_XOR", 0xF: "BEFORE_ASYNC_WITH",
0x10: "PRINT_EXPR", 0x11: "INPLACE_MATRIX_MULTIPLY", 0x13: "BINARY_SUBSCR",
0x14: "BINARY_SUBTRACT", 0x16: "INPLACE_AND", 0x17: "NOP", 0x18: "GET_ITER",
0x19: "BINARY_ADD", 0x1A: "GET_AWAITABLE", 0x1B: "UNARY_INVERT",
0x1C: "BINARY_MODULO", 0x1D: "BREAK_LOOP", 0x32: "BINARY_MATRIX_MULTIPLY",
0x33: "BINARY_AND", 0x34: "INPLACE_FLOOR_DIVIDE", 0x37: "INPLACE_MODULO",
0x38: "UNARY_NOT", 0x39: "UNARY_POSITIVE", 0x3B: "DUP_TOP_TWO",
0x3C: "END_FINALLY", 0x3D: "YIELD_FROM", 0x3E: "DUP_TOP",
0x3F: "UNARY_NEGATIVE", 0x40: "POP_TOP", 0x41: "BINARY_MULTIPLY",
0x42: "GET_AITER", 0x43: "INPLACE_OR", 0x44: "IMPORT_STAR", 0x45: "ROT_TWO",
0x46: "YIELD_VALUE", 0x47: "INPLACE_RSHIFT", 0x48: "GET_ANEXT",
0x49: "BINARY_POWER", 0x4B: "INPLACE_POWER", 0x4C: "RETURN_VALUE",
0x4D: "POP_BLOCK", 0x4E: "DELETE_SUBSCR", 0x4F: "INPLACE_MULTIPLY",
0x50: "POP_EXCEPT", 0x51: "BINARY_OR", 0x52: "INPLACE_SUBTRACT",
0x53: "BINARY_FLOOR_DIVIDE", 0x54: "INPLACE_XOR", 0x56: "BINARY_RSHIFT",
0x57: "LOAD_BUILD_CLASS", 0x58: "GET_YIELD_FROM_ITER", 0x59: "WITH_CLEANUP_START",
0x5A: "UNPACK_SEQUENCE", 0x5B: "STORE_NAME", 0x5C: "JUMP_IF_TRUE_OR_POP",
0x5D: "POP_JUMP_IF_FALSE", 0x5E: "SETUP_FINALLY", 0x5F: "FOR_ITER",
0x60: "BUILD_TUPLE_UNPACK", 0x61: "POP_JUMP_IF_TRUE", 0x62: "BUILD_LIST",
0x64: "LOAD_FAST", 0x65: "LOAD_CLOSURE", 0x66: "IMPORT_FROM", 0x67: "BUILD_SLICE",
0x68: "COMPARE_OP", 0x69: "LOAD_CLASSDEREF", 0x6A: "BUILD_TUPLE",
0x6B: "SETUP_LOOP", 0x6C: "LOAD_DEREF", 0x6D: "BUILD_LIST_UNPACK",
0x6E: "JUMP_IF_FALSE_OR_POP", 0x6F: "SETUP_EXCEPT", 0x70: "DELETE_ATTR",
0x71: "MAKE_FUNCTION", 0x72: "SET_ADD", 0x73: "LIST_APPEND", 0x74: "JUMP_FORWARD",
0x77: "LOAD_CONST", 0x78: "LOAD_GLOBAL", 0x79: "STORE_FAST", 0x7A: "BUILD_SET",
0x7C: "LOAD_ATTR", 0x7D: "BUILD_MAP", 0x7E: "DELETE_FAST", 0x82: "SETUP_WITH",
0x83: "CALL_FUNCTION", 0x84: "BUILD_SET_UNPACK", 0x85: "MAP_ADD", 0x86: "UNPACK_EX",
0x87: "EXTENDED_ARG", 0x88: "STORE_DEREF", 0x89: "CONTINUE_LOOP", 0x8A: "SETUP_ASYNC_WITH",
0x8C: "CALL_FUNCTION_VAR", 0x8D: "CALL_FUNCTION_KW", 0x8E: "CALL_FUNCTION_VAR_KW",
0x8F: "RAISE_VARARGS", 0x90: "IMPORT_NAME", 0x91: "MAKE_CLOSURE", 0x92: "DELETE_GLOBAL",
0x93: "BUILD_MAP_UNPACK", 0x94: "DELETE_NAME", 0x95: "DELETE_DEREF", 0x96: "STORE_ATTR",
0x97: "JUMP_ABSOLUTE", 0x98: "STORE_GLOBAL", 0x99: "BUILD_MAP_UNPACK_WITH_CALL",
0x9A: "LOAD_NAME"
}
# -----------------------

# assume modified interpreter used HAVE_ARGUMENT = 81 and OPARG_WIDTH = 2
HAVE_ARGUMENT_MOD = 81
OPARG_WIDTH = 2

# build reverse_map: modified_byte -> std opcode number (dis.opmap)
std_opmap = dis.opmap
reverse_map = {}
for mb, opname in MODIFIED.items():
if opname in std_opmap:
reverse_map[mb] = std_opmap[opname]
else:
print("Warning: opname %r not found in std opmap; skipping" % opname)

def has_arg_by_mapping(mod_byte):
"""
Given original modified opcode byte, return True if mapping target standard opcode uses argument.
We conservatively treat unmapped bytes as no-arg (safe).
"""
std = reverse_map.get(mod_byte)
if std is None:
return False
return std >= dis.HAVE_ARGUMENT

def step_len(mod_byte):
return 1 + (OPARG_WIDTH if has_arg_by_mapping(mod_byte) else 0)

def patch_codeobj_recursive(co):
b = bytearray(co.co_code)
i = 0
n = len(b)
replaced = 0
while i < n:
mod_op = b[i]
if mod_op in reverse_map:
new = reverse_map[mod_op]
if new != mod_op:
# replace opcode byte
b[i] = new
replaced += 1
# step using mapping on the *original* mod_op, not new opcode
i += step_len(mod_op)
# patch nested constants
new_consts = []
for c in co.co_consts:
if isinstance(c, types.CodeType):
new_consts.append(patch_codeobj_recursive(c))
else:
new_consts.append(c)
# reconstruct code object
if hasattr(co, "replace"):
return co.replace(co_code=bytes(b), co_consts=tuple(new_consts))
else:
# Python 3.5 signature
return types.CodeType(
co.co_argcount,
co.co_kwonlyargcount,
co.co_nlocals,
co.co_stacksize,
co.co_flags,
bytes(b),
tuple(new_consts),
co.co_names,
co.co_varnames,
co.co_filename,
co.co_name,
co.co_firstlineno,
co.co_lnotab,
co.co_freevars,
co.co_cellvars
)

def patch_and_write(infile, outfile):
with open(infile, "rb") as f:
header = f.read(12) # py3.5 header
top = marshal.load(f)
print("Loaded top-level code object:", top.co_name)
patched = patch_codeobj_recursive(top)
with open(outfile, "wb") as f:
f.write(header)
marshal.dump(patched, f)
print("Wrote patched pyc:", outfile)
return outfile

def run_pyc_with_subprocess(pyc_path):
# Use same python executable to run the patched pyc in a fresh interpreter process.
py = sys.executable
# On some setups, running a .pyc directly works; better to call: python patched_pyc
# We'll run a small wrapper that imports runpy to run the pyc safely.
wrapper = (
"import runpy, sys\n"
"runpy.run_path(sys.argv[1], run_name='__main__')\n"
)
# create a temporary wrapper script
tmpdir = tempfile.mkdtemp(prefix="runpatch_")
wrapper_py = os.path.join(tmpdir, "runner.py")
with open(wrapper_py, "w", encoding="utf-8") as f:
f.write(wrapper)
try:
print("Spawning subprocess: %s %s %s" % (py, wrapper_py, pyc_path))
proc = subprocess.Popen([py, wrapper_py, pyc_path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate(timeout=30)
print("=== STDOUT ===")
print(out.decode("utf-8", errors="backslashreplace"))
print("=== STDERR ===")
print(err.decode("utf-8", errors="backslashreplace"))
print("Exit code:", proc.returncode)
except subprocess.TimeoutExpired:
proc.kill()
print("Subprocess timed out and was killed.")
finally:
shutil.rmtree(tmpdir)

if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python patch_and_run_subprocess.py ooops.pyc [out_patch.pyc]")
sys.exit(1)
inf = sys.argv[1]
outf = sys.argv[2] if len(sys.argv) >= 3 else inf.replace(".pyc", "-patch.pyc")
if not os.path.exists(inf):
print("Input not found:", inf); sys.exit(2)
patch_and_write(inf, outf)
run_pyc_with_subprocess(outf)

然后运行:

1
2
3
4
5
6
7
8
9
10
11
12
import importlib.util, zlib, struct

spec = importlib.util.spec_from_file_location("ooops", "ooops-patch.pyc")
ooops = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ooops)

key = b'\xc6;3\xce\xa0\xbd\xcfHyKO\x9b*\x19\xb2\xbf'
enc_data = b'2\x00\x00\x00S\xff\x07/\x19\xcbf?@\xd4\xbe\xa7x\x05\xf7\xf13\xe93\xc9\x04\xb6\x00W7\xb0\x0f\xf3`,Fp\xe6a\x1f\xc7\xb8\xd3\x06\x1b;\x1f\xafs\xc8\x07\xe9p\x84\x08'

decrypted = ooops.decrypt_flag(enc_data, key)
print("flag =>", decrypted)

flag => b’flag{Reverse_This_Ultra_Hard_Challenge!!!}’

直接把flag打出来了

后来发现

uncompyle6 ooops-patch.pyc,它这个程序生成一个patch文件

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
(py35) E:\Desktop\huaweiCTF\ooops>uncompyle6 ooops-patch.pyc
# uncompyle6 version 3.9.2
# Python bytecode version base 3.5.2 (3351)
# Decompiled from: Python 3.10.14 | packaged by conda-forge | (main, Mar 20 2024, 12:40:08) [MSC v.1938 64 bit (AMD64)]
# Embedded file name: ooops.py
import sys, struct, zlib, ctypes

def anti_debug():
try:
if ctypes.windll.kernel32.IsDebuggerPresent():
sys.exit(1)
except:
pass


def decrypt_flag(data, key):
size = struct.unpack("<I", data[:4])[0]
data = data[4:4 + size]
data = bytearray(data)
indices = list(range(len(data)))
for i in range(len(data) - 1, -1, -1):
j = (i * 57005 + 48879) % len(data)
indices[i], indices[j] = indices[j], indices[i]

data = bytes(data[i] for i in indices)
data = bytes(b ^ key[i % len(key)] for i, b in enumerate(data))
return zlib.decompress(data)


def main():
anti_debug()
key = b'\xc6;3\xce\xa0\xbd\xcfHyKO\x9b*\x19\xb2\xbf'
enc_data = b'2\x00\x00\x00S\xff\x07/\x19\xcbf?@\xd4\xbe\xa7x\x05\xf7\xf13\xe93\xc9\x04\xb6\x00W7\xb0\x0f\xf3`,Fp\xe6a\x1f\xc7\xb8\xd3\x06\x1b;\x1f\xafs\xc8\x07\xe9p\x84\x08'
user_input = input("Enter flag: ").encode()
try:
decrypted = decrypt_flag(enc_data, key)
if user_input == decrypted:
print("Correct!")
else:
print("Wrong!")
except:
print("Error: Invalid input")

if __name__ == "__main__":
main()

解法二: LD_PRELOAD文件劫持

在发现zlib时,其实可以想到,可以去hook或者调试,只要绕过反调就能看到解压的flag

先来试一下文件劫持(优点是无需绕过反调试)

因为

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
// hookzlib.c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <zlib.h> // z_streamp, inflate, inflateEnd, etc.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/*
Hook zlib streaming API (inflate/inflateEnd) and legacy uncompress.
- Writes produced (decompressed) bytes into captured_decompressed.bin
- Uses constructor to open file once, destructor to close
- Thread-safe append using a mutex
- Calls real functions first (so dest buffer is filled), then dumps produced bytes
*/

// output file descriptor and mutex
static int outfd = -1;
static pthread_mutex_t out_mutex = PTHREAD_MUTEX_INITIALIZER;

__attribute__((constructor))
static void hook_init(void) {
const char *path = "captured_decompressed.bin";
outfd = open(path, O_CREAT | O_WRONLY | O_APPEND, 0600);
if (outfd < 0) {
// best effort print
perror("hookzlib: open output file");
}
}

__attribute__((destructor))
static void hook_fini(void) {
if (outfd >= 0) close(outfd);
}

// utility: write bytes to file (thread-safe)
static void dump_bytes(const void *buf, size_t len) {
if (outfd < 0 || buf == NULL || len == 0) return;
pthread_mutex_lock(&out_mutex);
ssize_t w = write(outfd, buf, len);
(void)w; // ignore short writes for now
pthread_mutex_unlock(&out_mutex);
}

/* ---- hook for legacy uncompress ----
int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen);
*/
typedef int (*uncompress_t)(unsigned char*, unsigned long*, const unsigned char*, unsigned long);
static uncompress_t real_uncompress = NULL;

int uncompress(unsigned char *dest, unsigned long *destLen, const unsigned char *source, unsigned long sourceLen) {
// lazy resolve
if (!real_uncompress) {
real_uncompress = (uncompress_t)dlsym(RTLD_NEXT, "uncompress");
}
int rc = -1;
if (real_uncompress) {
rc = real_uncompress(dest, destLen, source, sourceLen);
// on success, dest contains decompressed bytes, dstlen tells length
if (rc == Z_OK && dest && destLen && *destLen > 0) {
dump_bytes(dest, (size_t)(*destLen));
}
}
return rc;
}

/* ---- hook for inflate (streaming) ----
int inflate(z_streamp strm, int flush);
We capture bytes produced in this call:
produced = before_avail_out - after_avail_out
produced bytes start at before_next_out pointer.
*/
typedef int (*inflate_t)(z_streamp, int);
static inflate_t real_inflate = NULL;

int inflate(z_streamp strm, int flush) {
if (!real_inflate) {
real_inflate = (inflate_t)dlsym(RTLD_NEXT, "inflate");
}
if (!real_inflate) {
// not found, try to call nothing
return Z_STREAM_ERROR;
}

// Record before state
uInt before_avail = 0;
Bytef *before_next_out = NULL;
if (strm) {
before_avail = strm->avail_out;
before_next_out = strm->next_out;
}

int rc = real_inflate(strm, flush);

// After call, compute produced bytes
if (strm && before_next_out) {
uInt after_avail = strm->avail_out;
if (before_avail > after_avail) {
size_t produced = (size_t)(before_avail - after_avail);
// produced bytes start at before_next_out
dump_bytes(before_next_out, produced);
}
}
return rc;
}

/* ---- hook for inflateEnd: maybe flush or marker ---- */
typedef int (*inflateEnd_t)(z_streamp);
static inflateEnd_t real_inflateEnd = NULL;
int inflateEnd(z_streamp strm) {
if (!real_inflateEnd) {
real_inflateEnd = (inflateEnd_t)dlsym(RTLD_NEXT, "inflateEnd");
}
// Optionally write a marker to separate streams
const char marker[] = "\n--INFLATE-END--\n";
if (outfd >= 0) write(outfd, marker, sizeof(marker)-1);

if (!real_inflateEnd) return Z_STREAM_ERROR;
return real_inflateEnd(strm);
}

/* ---- Also hook inflateInit2_ / inflateInit_ variants to ensure we find overloaded names ----
Signatures:
int inflateInit_(z_streamp strm, const char *version, int stream_size);
int inflateInit2_(z_streamp strm, int windowBits, const char *version, int stream_size);
*/
typedef int (*inflateInit__t)(z_streamp, const char*, int);
static inflateInit__t real_inflateInit_ = NULL;
int inflateInit_(z_streamp strm, const char *version, int stream_size) {
if (!real_inflateInit_) real_inflateInit_ = (inflateInit__t)dlsym(RTLD_NEXT, "inflateInit_");
if (!real_inflateInit_) return Z_STREAM_ERROR;
return real_inflateInit_(strm, version, stream_size);
}

typedef int (*inflateInit2__t)(z_streamp, int, const char*, int);
static inflateInit2__t real_inflateInit2_ = NULL;
int inflateInit2_(z_streamp strm, int windowBits, const char *version, int stream_size) {
if (!real_inflateInit2_) real_inflateInit2_ = (inflateInit2__t)dlsym(RTLD_NEXT, "inflateInit2_");
if (!real_inflateInit2_) return Z_STREAM_ERROR;
return real_inflateInit2_(strm, windowBits, version, stream_size);
}


image-20251030172832322

可以得到

image-20251030172849737

010打开

直接看到flag

image-20251030172909809

uncompress wrapper(legacy 接口)

签名:int uncompress(Bytef *dest, uLongf *destLen, const Bytef *source, uLong sourceLen)。这是 zlib 的一次性解压函数(非流式)。

我们的 wrapper 做:

  1. 延迟解析真实函数地址 real_uncompress = dlsym(RTLD_NEXT, "uncompress")(第一次调用时解析)。
  2. 调用 real_uncompress(dest, destLen, source, sourceLen) —— 让 zlib 把数据解压到 dest
  3. 如果返回 Z_OK 并且 *destLen > 0,就把 dest*destLen 字节写到输出文件(dump_bytes(dest, *destLen))。

为什么先调用真实函数再写?因为只有在真实函数成功解压后,dest 才包含明文,直接写出我们需要的明文;若先写会只得到压缩态或未填充的缓冲。

flag{Reverse_This_Ultra_Hard_Challenge!!!}

解法三:frida_hook

image-20251030194730909

注意到这个函数在(libpython3.5m.so.1.0)这个so文件中

有一个比较,可以hook一下可以直接出

1
2
if user_input == decrypted:
print("Correct!")

本题是有反调的,可能双进程都有,如何注入可以自行让ai写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var mInterval = setInterval(function () {
var sgame = Process.findModuleByName("libpython3.5m.so.1.0");
if (sgame == null) {
return;
}
clearInterval(mInterval);
var addr = sgame.base.add(0x7FD90)
console.log("" + addr);
console.log(Instruction.parse(addr).toString());
Interceptor.attach(addr, {
onEnter: function (args) {
console.log(hexdump(ptr(args[0]), { length: 50, ansi: true }));
console.log(hexdump(ptr(args[1]), { length: 100, ansi: true }));
}
})
}, 1)

image-20251030204115090

AI

YJS-指鹿为马

AI的题就该让AI做

\n