2024 国赛 Reverse 方向
Reverse rand0m 得到一个 rand0m.pyd 和 generate.py,rand0m.pyd 被去符号了,先随便写一个 py,然后用 python3.12 的 cython 编译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 def check (flag ): a = 1 * 1 b = 2 * 2 c = 2 ^ 3 a = [0 ] * 6 b = 0 while b: print ("11" ) b -= 1 rand0m(flag) return a + b + c + flag def rand0m (x ): return x ^ 123 __test__ = {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 from setuptools import setup, Extensionfrom Cython.Build import cythonizeext_module = [ Extension( name = "test" , sources=["test.py" ], extra_compile_args=["/Zi" ], extra_link_args=["/DEBUG" ] ) ] setup( name="test" , ext_modules=cythonize(ext_module, annotate=True ), )
python build.py build_ext --inplace
,用编译后的 pyd 去和 rand0m.pyd bindiff 恢复符号
第二步去找那个全局调用表 _pyx_mstate_global
,恢复符号后找到 _Pyx_InitStrings_0
,里面有 调用表的初始化,先写几个结构体导入 ida
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 typedef struct { __int64 *__pyx_0; __int64 *__pyx_1; __int64 *__pyx_2; __int64 *__pyx_3; __int64 *__pyx_4; __int64 *__pyx_5; __int64 *__pyx_6; __int64 *__pyx_7; __int64 *__pyx_8; __int64 *__pyx_9; __int64 *__pyx_10; __int64 *__pyx_11; __int64 *__pyx_12; __int64 *__pyx_13; __int64 *__pyx_14; __int64 *__pyx_15; __int64 *__pyx_16; __int64 *__pyx_17; __int64 *__pyx_18; __int64 *__pyx_19; __int64 *__pyx_20; __int64 *__pyx_21; __int64 *__pyx_22; __int64 *__pyx_23; __int64 *__pyx_24; __int64 *__pyx_25; __int64 *__pyx_26; __int64 *__pyx_27; __int64 *__pyx_28; __int64 *__pyx_29; __int64 *__pyx_30; __int64 *__pyx_31; __int64 *__pyx_32; __int64 *__pyx_33; __int64 *__pyx_34; __int64 *__pyx_35; __int64 *__pyx_36; __int64 *__pyx_37; __int64 *__pyx_38; __int64 *__pyx_39; } __pyx_mstate_me; struct __Pyx_StringTabEntry { __int64 p, s, n, encoding; __int8 is_unicode, is_str, intern; __int8 b1, b2, b3, b4, b5; };
导入后手动设置类型,可以恢复大部分 _Pyx_InitStrings_0
之后手动 重建结构体
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 typedef struct { __int64 *__pyx_d; __int64 *__pyx_b; __int64 *__pyx_cython_runtime; __int64 *__pyx_empty_tuple; __int64 *__pyx_empty_bytes; __int64 *__pyx_empty_unicode; __int64 *__pyx_CyFunctionType; __int64 *__pyx_wenhao; __int64 *__pyx_asyncio_coroutines; __int64 *__pyx_check; __int64 *__pyx__class_getitem__; __int64 *__pyx_cline_in_traceback; __int64 *__pyx_delta; __int64 *__pyx_enc; __int64 *__pyx_flag; __int64 *__pyx_i; __int64 *__pyx__is_coroutine; __int64 *__pyx___main__; __int64 *__pyx___name__; __int64 *__pyx_rand0m; __int64 *__pyx_rand0m_pyx; __int64 *__pyx_range; __int64 *__pyx_ret; __int64 *__pyx_right; __int64 *__pyx_seed; __int64 *__pyx_sum; __int64 *__pyx___test__; __int64 *__pyx_tmp; __int64 *__pyx_x; } __pyx_mstate_me_2;
之后可以开始分析了
先看 rand0m 下的 rand0m 方法
找到绑定的函数后
这里是一个字符串转int的函数 等于 int(v0, 16)
这里这个 v8 这么奇怪其实是被上面我们重建的调用表结构体影响了,这里我们要去找全局初始化整数的地方 _Pyx_CreateStringTabAndInitStrings
也可以看到确实是 int(v0, 16)
明白这些之后接下来只要循规蹈矩的一点点分析就好了
这两句翻译过来就是
1 2 3 def rand0m_fake (x ): v6 = x ^ 0x9e3779b9 v3 = x >> 5
1 v22 = (x << 4 ) & 0xfa3affff
同样的 rand0m 分析完就是
1 2 3 4 5 6 def rand0m_fake (x ): v25 = ((x << 4 ) & 0xfa3affff ) + (x >> 28 ) v27 = (x ^ 0x9e3779b9 ) >> 11 v27 = v27 ** 65537 v27 %= 0xfffffffd return (v27, v25)
在分析 check 函数
这里新建一个长度为 8 的密文列表,按照上面的方法设置值后得到
1 nums = [0x12287f38 , 0x98d24b3a , 0x4a30f74d , 0xe0f1db77 , 0x23a1268 , 0xadf38403 , 0x88108807 , 0xd8499bb6 ]
这里是一个数组切片 分析以下就是8个8个切片去加密
然后获得结果后用 PyObject_RichCompare
去和密文比较
总体就是每次截取8个获得两个密文,然后比较,用 z3 去解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 for i in range (0 , 8 , 2 ): nums = [0x12287f38 , 0x98d24b3a , 0x4a30f74d , 0xe0f1db77 , 0x23a1268 , 0xadf38403 , 0x88108807 , 0xd8499bb6 ] res = [nums[i + 1 ], nums[i]] a = BitVec("data" , 32 ) s = Solver() dt = ((a << 4 ) & 0xfa3affff ) + (a >> 28 ) s.add(And(res[1 ] - 0x10 <= dt, dt <= res[1 ] + 0x10 )) while s.check() == sat: module = s.model() result = module[a].as_long() chec = rand0m.rand0m(hex (result)[2 :]) if chec[0 ] == res[0 ] and chec[1 ] == res[1 ]: print (f"{result:#x} {chec[0 ]:#x} {chec[1 ]:#x} " ) break s.add(a != result)
这里奇偶数有误差,所有放大一点,然后可以解出来 flag
1 2 3 4 0x813a97f3 0x98d24b3a 0x12287f38 0xd4b34f74 0xe0f1db77 0x4a30f74d 0x802ba126 0xadf38403 0x23a1268 0x78950880 0xd8499bb6 0x88108807
拼接以下可以得到 flag
flag{813a97f3d4b34f74802ba12678950880}
cython exe 解包后有一个 pyc 和 一个 pyd,pyc 有不支持的 opcode 所以直接看字节码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for i in range (len (flag1) - 3 ): b = (ord (flag1[i]) << 24 ) | (ord (flag1[i + 1 ]) << 16 ) | (ord (flag1[i + 2 ]) << 8 ) | ord (flag1[i + 3 ]) value.append(b) i += 4 key = [102 , 108 , 97 , 103 ] flag_encrypt = [] print (value)for i in range (6 ): res = ez.encrypt(value[i], value[i + 1 ], key[i % len (key)]) flag_encrypt.append(res) ck = ez.check(flag_encrypt)
翻译过来就是这个
之后逆向 ez.pyd 前面部分同上,恢复符号后重建调用表
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 typedef struct { __int64 *__pyx_d; __int64 *__pyx_b; __int64 *__pyx_cython_runtime; __int64 *__pyx_empty_tuple; __int64 *__pyx_empty_bytes; __int64 *__pyx_empty_unicode; __int64 *__pyx_CyFunctionType; __int64 *__pyx_kp_s_11; __int64 *__pyx_V0; __int64 *__pyx_V1; __int64 *__pyx_wenhao; __int64 *__pyx_asyncio_coroutines; __int64 *__pyx_c_uint32; __int64 *__pyx_check; __int64 *__pyx__class_getitem__; __int64 *__pyx_cline_in_traceback; __int64 *__pyx_ctypes; __int64 *__pyx_data; __int64 *__pyx_delta; __int64 *__pyx_encrypt; __int64 *__pyx_ez; __int64 *__pyx_ez_py; __int64 *__pyx_flag_encrypt; __int64 *__pyx_i; __int64 *__pyx___import__; __int64 *__pyx__is_coroutine; __int64 *__pyx_key; __int64 *__pyx___main__; __int64 *__pyx___name__; __int64 *__pyx_range; __int64 *__pyx___test__; __int64 *__pyx_total; __int64 *__pyx_zero_v; __int64 *__pyx_v1; __int64 *__pyx_value; } __pyx_mstate_me_2; struct __Pyx_StringTabEntry { __int64 p, s, n, encoding; __int8 is_unicode, is_str, intern; __int8 b1, b2, b3, b4, b5; };
然后分析 encrypt
其实就是一个魔改 xtea,魔改了 delta 轮数 还有移位数
这里就是
1 ctypes.uint32_t(v0).value
这里翻译过来是
1 v0 += (((v1 << 3 ) ^ (v1 >> 6 )) + v1) ^ (sum + key[sum & 3 ]);
这里翻译过来是
1 v1 += (((v0 << 3 ) ^ (v0 >> 6 )) + v0) ^ (sum + key[(sum >> 11 ) & 3 ]);
这里 加了 delta,然后就可以写一个脚本解密了
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 #include "defs.h" #include "stdio.h" #include "stdint.h" void decipher (unsigned int num_rounds, uint32_t v[2 ], uint32_t const key[4 ]) { unsigned int i; uint32_t v0 = v[0 ], v1 = v[1 ], delta = 0x54646454 , sum = delta * num_rounds; for (i = 0 ; i < num_rounds; i++) { v1 -= (((v0 << 3 ) ^ (v0 >> 6 )) + v0) ^ (sum + key[(sum >> 11 ) & 3 ]); sum -= delta; v0 -= (((v1 << 3 ) ^ (v1 >> 6 )) + v1) ^ (sum + key[sum & 3 ]); } v[0 ] = v0; v[1 ] = v1; } int main () { uint32 v[] = { 874806256 , 3846097014 , 2954414927 , 947032420 , 142915619 , 250019369 }; uint32 key[] = { 102 , 108 , 97 , 103 }; for (int i = 0 ; i < 6 ; i += 2 ) { decipher (64 , v + i, key); } uint8 *ptr = (uint8 *) v; for (int i = 0 ; i < 24 ; i += 4 ) { printf ("%c%c%c%c" , ptr[i + 3 ], ptr[i + 2 ], ptr[i + 1 ], ptr[i]); } return 0 ; }
flag{bfXtvbVZfRfeqoSXWD}
dump 到手一个 re.exe,随便输入点东西发现会根据你输入的内容给出加密的内容
发现是一个单字节加密,直接输入所有可打印字符串导出表
1 2 3 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ! 001c1d000000000000001e1f202122232425262728292a2b2c2d2e2f303132333435363702030405060708090a0b0c0d0e0f101112131415161718191a1b000000000000000000000000000000000100000000000000000038003900
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import binasciis = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&()*+,-./:;<=>?@[\\]^_`{|}~ " d = list (binascii.unhexlify("001c1d000000000000001e1f202122232425262728292a2b2c2d2e2f303132333435363702030405060708090a0b0c0d0e0f101112131415161718191a1b000000000000000000000000000000000100000000000000000038003900" )) mp = {} idx = 0 for i in range (len (d)): mp[d[i]] = s[i] data = open ("flag" , "rb" ).read() for i in data: print (mp[i], end="" )
flag{MTczMDc~MzQ2Ng==}
这个 ~ 是因为密文里是 00,而加密后是 00 太多了不知道是什么,这个 flag 包裹的内容很像 base64,先随便填一下,很像时间戳,测试后发现 4 以后的数字还有 英文字母都是无效时间戳,随便爆破下,4是正确答案
flag{MTczMDc4MzQ2Ng==}