Loading... # L3HCTF RE wp  冠军!这次真的起飞了,正式打CTF一年零两个月终于拿下了第一个大赛的冠军 RE7道题出了道,不过有俩题都是队友给的思路我把解flag脚本搓出来了,其中TemporaPardox拿下1血,AWayOut2拿下2血,Good! 其他方向队友均很给力,每个方向都有血,其中Web甚至6道拿下4道1血,SU战队也是在包队带领下起飞一次了,希望以后愈战愈勇! ## TemporalParadox main开头有花指令,nop掉跳转jmp即可反编译。动态调试跑一轮就知道各个函数的功能 ~~~c++ __int64 __fastcall sub_140001D05(__int64 a1, __int64 a2) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] sub_140002180(a1, a2); sub_14000A510(a1, a2, v2, (unsigned int)v53, v3, v4); v58 = get_time(a1, a2, v5, 0, v6, v7); if ( v58 > 1751990400 && v58 <= 1752052051 ) { gen_query(a1, a2, v8, (unsigned int)v49, v9, v10); v57 = &v54; v12 = std::string::c_str(a1, a2, v11, v49); v15 = md5(a1, a2, v12, (unsigned int)v53, v13, v14, v41); sub_14000A820(a1, a2, v15, (unsigned int)v50, (unsigned int)&v54, v16, v42, v47); sub_14000A6E0(a1, a2, v17, (unsigned int)&v54, v18, v19, v43); v20 = std::operator<<<std::char_traits<char>>(a1, a2, "query: ", &std::cout); v21 = std::operator<<<char>(a1, a2, v49, v20); std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v21); v22 = std::operator<<<char>(a1, a2, v50, &std::cout); std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v22); std::string::~string(a1, a2, v23, v50); std::string::~string(a1, a2, v24, v49); } std::string::basic_string(a1, a2, v8, v52); v25 = std::operator<<<std::char_traits<char>>(a1, a2, "Please input the right query string I used:", &std::cout); std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v25); std::operator>><char>(a1, a2, v52, &std::cin); v56 = &v55; v27 = std::string::c_str(a1, a2, v26, v52); v30 = md5(a1, a2, v27, (unsigned int)v53, v28, v29, v41); sub_14000A820(a1, a2, v30, (unsigned int)v51, (unsigned int)&v55, v31, v44, v47); sub_14000A6E0(a1, a2, v32, (unsigned int)&v55, v33, v34, v45); if ( (unsigned __int8)sub_14000A8E0( a1, a2, (unsigned int)"8a2fc1e9e2830c37f8a7f51572a640aa", (unsigned int)v51, v35, v36, v46, v48) ) v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Congratulations!", &std::cout); else v37 = std::operator<<<std::char_traits<char>>(a1, a2, "Wrong!", &std::cout); std::ostream::operator<<(a1, a2, &std::endl<char,std::char_traits<char>>, v37); std::string::~string(a1, a2, v38, v51); std::string::~string(a1, a2, v39, v52); return 0LL; } ~~~ 可以看出我们需要得到正确的query并满足query md5加密后的值等于8a2fc1e9e2830c37f8a7f51572a640aa;if里是对时间的判断显然是告诉我们要爆破的话时间范围是(1751990400,1752052051) 进入gen_query可以看到各个参数的生成,可以看到两种query,一种是满足pow_like函数的判断则没有a、b、x、y参数,但多了cipher参数;get_rand是模拟生成随机数 ~~~c++ __int64 __fastcall sub_140001963( __time64_t *a1, __int64 a2, int a3, __int64 a4, int a5, int a6, double a7, double a8, double a9, double a10) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] sub_140001518((_DWORD)a1, a2, a3, (unsigned int)v51, a5, a6); time = get_time(a1); sub_1400014B5((_DWORD)a1, a2, v10, time, v11, v12); v58 = 0; v57 = 0; v56 = 0; v55 = 0; for ( i = 0; i < (int)gen_rand(); ++i ) { v58 = gen_rand(); v57 = gen_rand(); v56 = gen_rand(); v55 = gen_rand(); } v52 = gen_rand(); std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::basic_stringstream(a1, a2, v13, v49); v14 = (double)dword_14000B0E0; v15 = (double)(int)(v58 | v56); v18 = v14 * pow_like(v15, 2.0, v15, a10, v16, v17, (double)dword_14000B0E0); v19 = (double)dword_14000B0E4; if ( v18 == pow_like((double)(int)(v57 | v55), 2.0, v15, (double)(int)(v57 | v55), v20, v21, v18) * v19 ) { v22 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50); v23 = std::operator<<<char>(a1, a2, v51, v22); v24 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v23); v25 = std::ostream::operator<<(a1, a2, time, v24); v26 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v25); v27 = std::ostream::operator<<(a1, a2, v52, v26); v28 = std::operator<<<std::char_traits<char>>(a1, a2, "&cipher=", v27); v31 = sub_14000184D((_DWORD)a1, a2, time, v52, v29, v30, v48); std::ostream::operator<<(a1, a2, v31, v28); } else { v32 = std::operator<<<std::char_traits<char>>(a1, a2, "salt=", v50); v33 = std::operator<<<char>(a1, a2, v51, v32); v34 = std::operator<<<std::char_traits<char>>(a1, a2, "&t=", v33); v35 = std::ostream::operator<<(a1, a2, time, v34); v36 = std::operator<<<std::char_traits<char>>(a1, a2, "&r=", v35); v37 = std::ostream::operator<<(a1, a2, v52, v36); v38 = std::operator<<<std::char_traits<char>>(a1, a2, "&a=", v37); v39 = std::ostream::operator<<(a1, a2, v58, v38); v40 = std::operator<<<std::char_traits<char>>(a1, a2, "&b=", v39); v41 = std::ostream::operator<<(a1, a2, v57, v40); v42 = std::operator<<<std::char_traits<char>>(a1, a2, "&x=", v41); v43 = std::ostream::operator<<(a1, a2, v56, v42); v44 = std::operator<<<std::char_traits<char>>(a1, a2, "&y=", v43); std::ostream::operator<<(a1, a2, v55, v44); } std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::str(a1, a2, v49, a4); std::basic_stringstream<char,std::char_traits<char>,std::allocator<char>>::~basic_stringstream(a1, a2, v45, v49); std::string::~string(a1, a2, v46, v51); return a4; } __int64 gen_rand() { unsigned int v1; // [rsp+Ch] [rbp-4h] v1 = (((dword_14000B040 << 13) ^ (unsigned int)dword_14000B040) >> 17) ^ (dword_14000B040 << 13) ^ dword_14000B040; dword_14000B040 = (32 * v1) ^ v1; return dword_14000B040 & 0x7FFFFFFF; } __int64 __fastcall sub_1400014B5(_DWORD a1, _DWORD a2, _DWORD a3, unsigned int a4) { __int64 result; // rax unsigned int v5; // [rsp+10h] [rbp+10h] v5 = a4; if ( !a4 ) v5 = 1; result = v5; dword_14000B040 = v5; return result; } ~~~ 调试可以发现dword_14000B040初始值为get_time返回的time,此外salt值固定为tlkyeueq7fej8vtzitt26yl24kswrgm5,因此a、b、x、y实际上都和t相关 因此首先我写了个python脚本来爆破(c不擅长,部分函数如sub_14000184D直接让gemini分析生成模拟代码,但事后发现其实没用到) ~~~python import math from hashlib import md5, sha1 def gen(dword): v1 = ((((dword << 13)&0xffffffff) ^ dword) >> 17) ^ ((dword << 13)&0xffffffff) ^ dword dword = (((32 * v1)&0xffffffff) ^ v1) &0xffffffff return dword, dword & 0x7FFFFFFF S_BOX_TABLE_7FF65E2BC020 = [0x0000000E, 0x00000004, 0x0000000D, 0x00000001, 0x00000002, 0x0000000F, 0x0000000B, 0x00000008, 0x00000003, 0x0000000A, 0x00000006, 0x0000000C, 0x00000005, 0x00000009, 0x00000000, 0x00000007] P_BOX_TABLE_7FF65E2BC0A0 = [0x00000001, 0x00000005, 0x00000009, 0x0000000D, 0x00000002, 0x00000006, 0x0000000A, 0x0000000E, 0x00000003, 0x00000007, 0x0000000B, 0x0000000F, 0x00000004, 0x00000008, 0x0000000C, 0x00000010] def to_u32(n): """将一个数转换为32位无符号整数""" return n & 0xFFFFFFFF def to_s32(n): """将一个数转换为32位有符号整数""" n = n & 0xFFFFFFFF if n & 0x80000000: return n - 0x100000000 return n def generate_salt(dword_array): """ 对应 C++ 函数 sub_7FF65E2B1518 根据硬编码的 dword 数组生成一个32字符的 salt 字符串。 """ if not dword_array: raise ValueError("错误: dword_7FF65E2BB060 数组为空,请填写数据。") s = [] for i in range(32): v9 = dword_array[i] v10 = 0 # C++ int 是32位的,Python int 是无限精度的,需要模拟32位行为 v9_s32 = to_s32(v9) if v9_s32 >= 0: v10 = v9_s32 / 3 + 48 elif v9_s32 >= -728: # ~v9 在C++中是对32位整数按位取反 v10 = ~v9_s32 & 0xFFFFFFFF else: # 这里的 sub_7FF65E2B31D0 / 1.0986... 被我们分析为 log3 # math.log(x) 是 ln(x),math.log(3) 是 ln(3) # log3(x) = ln(x) / ln(3) try: log_val = math.log(-v9_s32) / math.log(3) v10 = log_val - 6.0 + 48.0 except ValueError: # 如果 -v9_s32 <= 0,log会出错,这里设置一个默认值 v10 = 48 # '0' # 将计算结果转换为字符 s.append(chr(int(v10) & 0xFF)) return "".join(s) def s_box_transform(state, s_box_table): """ 对应 C++ 函数 sub_7FF65E2B16C1 (S-盒替换) """ if not s_box_table: raise ValueError("错误: S_BOX_TABLE_7FF65E2BC020 数组为空,请填写数据。") s = to_u32(state) for _ in range(4): # 提取高4位作为索引 index = (s >> 12) & 0xF sbox_val = s_box_table[index] # (16 * s) 等价于 (s << 4) s = sbox_val | (s << 4) return to_u32(s) def p_box_transform(state, p_box_table): """ 对应 C++ 函数 sub_7FF65E2B1785 (P-盒置换) """ if not p_box_table: raise ValueError("错误: P_BOX_TABLE_7FF65E2BC0A0 数组为空,请填写数据。") s = to_u32(state) new_state = 0 for i in range(16): # 获取源比特的位置 (C数组是1-based, Python是0-based) source_bit_pos = p_box_table[i] - 1 # 检查源比特是否为1 if (s >> source_bit_pos) & 1: # 如果是1,则在目标位置i设置比特 new_state |= (1 << i) return new_state def round_function(state, s_box_table, p_box_table): """ 对应 C++ 函数 sub_7FF65E2B17F7 (轮函数) """ state = s_box_transform(state, s_box_table) state = p_box_transform(state, p_box_table) return state def generate_round_key(key, round_num): """ 对应 C++ 函数 sub_7FF65E2B16A0 (轮密钥生成) """ key_u32 = to_u32(key) shift_amount = 4 * (round_num - 1) # C++ 代码中 (unsigned int) >> 是逻辑右移 shifted_key = key_u32 << shift_amount return to_u32(shifted_key) >> 16 def encrypt_token(timestamp, r_key, s_box_table, p_box_table): """ 对应 C++ 函数 sub_7FF65E2B184D (加密主函数) """ state = to_u32(timestamp) # 循环 3 轮 for i in range(1, 4): round_key = generate_round_key(r_key, i) state ^= round_key state = round_function(state, s_box_table, p_box_table) # 循环后的第4步 round_key_4 = generate_round_key(r_key, 4) state ^= round_key_4 state = s_box_transform(state, s_box_table) # 最终返回前的第5步 round_key_5 = generate_round_key(r_key, 5) final_state = state ^ round_key_5 return to_u32(final_state) for t in range(1751990400, 1752052052): dword = t dword, ret = gen(dword) cnt = ret i = 0 while i < cnt: dword, ret = gen(dword) a = ret dword, ret = gen(dword) b = ret dword, ret = gen(dword) x = ret dword, ret = gen(dword) y = ret dword, ret = gen(dword) cnt = ret i+=1 dword, ret = gen(dword) r = ret # pow(a | x, 2) val1 = math.pow(float(to_s32(a) | to_s32(x)), 2.0) # pow(b | y, 2) val2 = math.pow(float(to_s32(b) | to_s32(y)), 2.0) if math.isclose(0x61 * val1, 0xb * val2): cipher = encrypt_token( t, r, S_BOX_TABLE_7FF65E2BC020, P_BOX_TABLE_7FF65E2BC0A0 ) query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&cipher={cipher}" else: query = f"salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t={t}&r={r}&a={a}&b={b}&x={x}&y={y}" print(t, query) if md5(query.encode()).hexdigest() == "8a2fc1e9e2830c37f8a7f51572a640aa": print(sha1(query.encode()).hexdigest()) ~~~ 但python爆破速度非常慢,直接让gemini转为c语言脚本 ~~~c #include <stdio.h> #include <stdint.h> #include <stdlib.h> #include <string.h> #include <math.h> // 引入 OpenSSL 库头文件 #include <openssl/md5.h> #include <openssl/sha.h> // 伪随机数生成器,对应 python 的 gen 函数 // 使用指针来返回两个值 void gen(uint32_t* dword, uint32_t* ret) { uint32_t v1 = ((((*dword << 13) ^ *dword) >> 17) ^ ((*dword << 13) ^ *dword)); *dword = (32 * v1) ^ v1; *ret = *dword & 0x7FFFFFFF; } // 辅助函数:将二进制哈希值转换为十六进制字符串 void bytes_to_hex(const unsigned char* bytes, char* hex_string, size_t len) { for (size_t i = 0; i < len; ++i) { sprintf(hex_string + (i * 2), "%02x", bytes[i]); } hex_string[len * 2] = '\0'; } int main() { const char* target_md5 = "8a2fc1e9e2830c37f8a7f51572a640aa"; for (uint32_t t = 1751990400; t < 1752052052; ++t) { uint32_t dword = t; uint32_t ret; // 初始 gen 调用 gen(&dword, &ret); uint32_t cnt = ret; uint32_t a = 0, b = 0, x = 0, y = 0; int i = 0; while (i < cnt) { gen(&dword, &a); gen(&dword, &b); gen(&dword, &x); gen(&dword, &y); gen(&dword, &cnt); i++; } uint32_t r; gen(&dword, &r); // C中需要更大的缓冲区来格式化字符串 char query[512]; snprintf(query, sizeof(query), "salt=tlkyeueq7fej8vtzitt26yl24kswrgm5&t=%u&r=%u&a=%u&b=%u&x=%u&y=%u", t, r, a, b, x, y); printf("%u %s\n", t, query); fflush(stdout); // 强制刷新输出缓冲区,确保立即看到打印 // 计算 MD5 unsigned char md5_result[MD5_DIGEST_LENGTH]; MD5((unsigned char*)query, strlen(query), md5_result); char md5_hex[MD5_DIGEST_LENGTH * 2 + 1]; bytes_to_hex(md5_result, md5_hex, MD5_DIGEST_LENGTH); // 比较 MD5 if (strcmp(md5_hex, target_md5) == 0) { printf("Found MD5 match!\n"); // 计算并打印 SHA1 unsigned char sha1_result[SHA_DIGEST_LENGTH]; SHA1((unsigned char*)query, strlen(query), sha1_result); char sha1_hex[SHA_DIGEST_LENGTH * 2 + 1]; bytes_to_hex(sha1_result, sha1_hex, SHA_DIGEST_LENGTH); printf("SHA1: %s\n", sha1_hex); break; // 找到结果,退出循环 } } return 0; } ~~~ gcc ./paradox_solve.c -o solve -lssl -lcrypto -lm  得到正确query的sha1结果 题目很简单后面就爆了 ## 终焉之门 动调发现出现了代码字符串,后续分析代码发现有处地方做了异或解密 ~~~c __int64 sub_140091CC0() { sub_140001450(byte_140093A60, 2318LL); return sub_140001450(byte_140093380, 1760LL); } __int64 __fastcall sub_140001450(_BYTE *a1, __int64 a2) { unsigned __int64 v3; // rcx __int64 result; // rax if ( a2 != 1 ) { *a1 ^= 0x56u; v3 = 1LL; if ( a2 != 2 ) { do { result = (unsigned __int8)aVm0xd1ntuxlwa1[v3 % 0x1CC]; a1[v3++] ^= result; } while ( v3 != a2 - 1 ); } } return result; } ~~~ 提取出来解密得到vm代码 ~~~ #version 430 core layout(local_size_x = 1, local_size_y = 1, local_size_z = 1) in; layout(std430, binding = 0) buffer OpCodes { int opcodes[]; }; layout(std430, binding = 2) buffer CoConsts { int co_consts[]; }; layout(std430, binding = 3) buffer Cipher { int cipher[16]; }; layout(std430, binding = 4) buffer Stack { int stack_data[256]; }; layout(std430, binding = 5) buffer Out { int verdict; }; const int MaxInstructionCount = 1000; void main() { if (gl_GlobalInvocationID.x > 0) return; uint ip = 0u; int sp = 0; verdict = -233; while (ip < uint(MaxInstructionCount)) { int opcode = opcodes[int(ip)]; int arg = opcodes[int(ip)+1]; switch (opcode) { case 2: stack_data[sp++] = co_consts[arg]; break; case 7: { int b = stack_data[--sp]; int a = stack_data[--sp]; stack_data[sp++] = a + b; break; } case 8: { int a = stack_data[--sp]; int b = stack_data[--sp]; stack_data[sp++] = a - b; break; } case 14: { int b = stack_data[--sp]; int a = stack_data[--sp]; stack_data[sp++] = a ^ b; break; } case 15: { int b = stack_data[--sp]; int a = stack_data[--sp]; stack_data[sp++] = int(a == b); break; } case 16: { bool ok = true; for (int i = 0; i < 16; i++) { if (stack_data[i] != (cipher[i] - 20)) { ok = false; break; } } verdict = ok ? 1 : -1; return; } case 18: { int c = stack_data[--sp]; if (c == 0) ip = uint(arg); break; } default: verdict = 500; return; } ip+=2; } verdict = 501; } ~~~ 需要去寻找opcode等参数,来到main发现开头出现很多数组初始化,且大小正好对应vm函数的传参 ~~~c __int64 sub_140091CF0() { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] v0 = 0; sub_14008E370(); sub_140023480(8256LL); sub_14001F730(1280LL, 800LL, "Password Checker"); sub_140021100(&v26, 0LL, byte_140093380); v1 = _mm_loadu_si128(&v26); v2 = sub_14000E700(byte_140093A60, 37305LL); v20 = sub_14000EEE0(v2); v21 = sub_14000EFF0(672LL, &opcodes, 35050LL); v3 = sub_14000EFF0(128LL, &co_consts, 35050LL); v22 = sub_14000EFF0(64LL, &cipher, 35050LL); v23 = sub_14000EFF0(1024LL, &stack_data, 35050LL); v4 = sub_14000EFF0(4LL, &verdict, 35050LL); v33[0] = 0LL; v24 = v4; *(_QWORD *)Str = 0LL; v29 = 0LL; v30 = 0LL; v31 = 0LL; memset(v32, 0, sizeof(v32)); *(_QWORD *)((char *)v33 + 5) = 0LL; sub_1400231A0(60LL); while ( !(unsigned __int8)sub_14001CAC0() ) { v5 = sub_140025A40(); if ( v5 > 0 && v0 <= 99 ) { v6 = v0 + 1; do { Str[v6 - 1] = v5; v0 = v6; v5 = sub_140025A40(); v7 = (int)v6++ <= 99; } while ( v7 && v5 > 0 ); } v8 = sub_1400258E0(259LL); if ( v0 > 0 && v8 ) Str[--v0] = 0; if ( (unsigned __int8)sub_1400258E0(257LL) && strlen(Str) == 40 && !strncmp(Str, "L3HCTF{", 7uLL) && HIBYTE(v32[0]) == '}' ) { v25 = v0; v12 = &Str[7]; v13 = 0; do { v17 = *v12; v18 = v12[1]; if ( v17 > 96 ) v14 = v17 - 87; else v14 = v17 - 48; v19 = 16 * v14; v15 = v13; v16 = v18 - 48; if ( v18 >= 97 ) v16 = v18 - 87; v12 += 2; v13 += 4; v27 = v16 + v19; sub_14000F0B0(v3, &v27, 4LL, v15); } while ( (char *)v32 + 7 != v12 ); v0 = v25; sub_14000C100(v20); sub_14000F180(v21, 0LL); sub_14000F180(v3, 2LL); sub_14000F180(v22, 3LL); sub_14000F180(v23, 4LL); sub_14000F180(v24, 5LL); sub_14000EFE0(1LL, 1LL, 1LL); sub_14000F140(v24, &verdict, 4LL, 0LL); sub_14000C110(); } sub_14001FC90(); v26 = v1; sub_140020650(&v26); v9 = sub_14001E170(); v26 = v1; *(float *)&v9 = v9; v27 = LODWORD(v9); v10 = sub_140021440(&v26, "time"); v26 = v1; sub_140021460(&v26, v10, &v27, 0LL); sub_14003B9D0(0, 0, 1280, 800, -1); sub_140020690(); sub_14004DA20((unsigned int)Str, 100, 200, 40, -16777216); if ( verdict == 1 ) sub_14004DA20((unsigned int)"success", 100, 300, 40, -13863680); else sub_14004DA20((unsigned int)"wrong password", 100, 300, 40, -13162010); sub_14004DA20((unsigned int)"Type password and press [Enter] to check!", 100, 100, 20, -8224126); sub_14004DA20((unsigned int)"Press [Backspace] to delete characters.", 100, 130, 20, -8224126); sub_140025CE0(); } sub_14001FAA0(); return 0LL; } ~~~ 可以看到要求flag格式,且内容为16进制字符串32位;此外可以看到v3即co_consts存储了16个十六进制字节 可以模拟vm跑一遍流程了 ~~~python KNOWN_CIPHER = [243, 130, 6, 509, 336, 56, 178, 222, 346, 407, 156, 471, 110, 40, 326, 151] class VM: def __init__(self): self.opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000, 0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001, 0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003, 0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000, 0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013, 0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000, 0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005, 0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007, 0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000, 0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017, 0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000, 0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009, 0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B, 0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000, 0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B, 0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000, 0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D, 0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F, 0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000, 0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000, 0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001] self.co_consts = [0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x000000B0, 0x000000C8, 0x000000FA, 0x00000086, 0x0000006E, 0x0000008F, 0x000000AF, 0x000000BF, 0x000000C9, 0x00000064, 0x000000D7, 0x000000C3, 0x000000E3, 0x000000EF, 0x00000087, 0x00000000] self.co_consts[:16] = list(range(16)) self.stack_data = [0] * 256 self.ip = 0 self.sp = 0 self.max_instructions = 1000 def run(self): while self.ip < len(self.opcodes) and self.ip < self.max_instructions * 2: opcode = self.opcodes[self.ip] arg = self.opcodes[self.ip + 1] print(opcode, end=" ") if opcode == 2: # LOAD_CONST self.stack_data[self.sp] = self.co_consts[arg] self.sp += 1 print(f"push {hex(self.co_consts[arg])}({arg})") elif opcode == 7: # BINARY_ADD b = self.stack_data[self.sp - 1] a = self.stack_data[self.sp - 2] self.sp -= 1 self.stack_data[self.sp - 1] = a + b print(f"add {b}, {a}") elif opcode == 8: # BINARY_SUBTRACT b = self.stack_data[self.sp - 1] a = self.stack_data[self.sp - 2] self.sp -= 1 self.stack_data[self.sp - 1] = b - a print(f"sub {b}, {a}") elif opcode == 14: # BINARY_XOR b = self.stack_data[self.sp - 1] a = self.stack_data[self.sp - 2] self.sp -= 1 self.stack_data[self.sp - 1] = a ^ b print(f"xor {b}, {a}") elif opcode == 15: # COMPARE_OP (==) b = self.stack_data[self.sp - 1] a = self.stack_data[self.sp - 2] self.sp -= 1 self.stack_data[self.sp - 1] = 1 if a == b else 0 print(f"cmp {b}, {a}") elif opcode == 18: # POP_JUMP_IF_FALSE c = self.stack_data[self.sp - 1] self.sp -= 1 if c == 0: self.ip = arg continue print("pop") else: print("cmp") return self.ip += 2 if __name__ == "__main__": vm = VM() input = [val for val in vm.co_consts[:16]] print(input) vm.run() correct_cipher = [val for val in vm.stack_data[:16]] print(correct_cipher) print(f"CIPHER (16个整数): {[i-20 for i in KNOWN_CIPHER]}") ~~~ 分析下日志,可以看到每次先异或下一个字节,然后结果一个常数加或减异或结果 ~~~ 2 push 0x0(0) 2 push 0x1(1) 2 push 0x0(0) 14 xor 0, 1 2 push 0xb0(16) 8 sub 176, 1 2 push 0x2(2) 2 push 0x1(1) 14 xor 1, 2 2 push 0xc8(17) 8 sub 200, 3 2 push 0x3(3) 2 push 0x2(2) 14 xor 2, 3 2 push 0xfa(18) 7 add 250, 1 ~~~ 可以逆回去 ~~~python cipher = [0xF3, 0x82, 0x06, 0x1FD, 0x150, 0x38, 0xB2, 0xDE, 0x15A, 0x197, 0x9C, 0x1D7, 0x6E, 0x28, 0x146, 0x97] CoConsts = [0xB0, 0xC8, 0xFA, 0x86, 0x6E, 0x8F, 0xAF, 0xBF, 0xC9, 0x64, 0xD7, 0xC3, 0xE3, 0xEF, 0x87, 0x00] opcodes = [0x00000002, 0x00000000, 0x00000002, 0x00000001, 0x00000002, 0x00000000, 0x0000000E, 0x00000000, 0x00000002, 0x00000010, 0x00000008, 0x00000000, 0x00000002, 0x00000002, 0x00000002, 0x00000001, 0x0000000E, 0x00000000, 0x00000002, 0x00000011, 0x00000008, 0x00000000, 0x00000002, 0x00000003, 0x00000002, 0x00000002, 0x0000000E, 0x00000000, 0x00000002, 0x00000012, 0x00000007, 0x00000000, 0x00000002, 0x00000004, 0x00000002, 0x00000003, 0x0000000E, 0x00000000, 0x00000002, 0x00000013, 0x00000007, 0x00000000, 0x00000002, 0x00000005, 0x00000002, 0x00000004, 0x0000000E, 0x00000000, 0x00000002, 0x00000014, 0x00000008, 0x00000000, 0x00000002, 0x00000006, 0x00000002, 0x00000005, 0x0000000E, 0x00000000, 0x00000002, 0x00000015, 0x00000007, 0x00000000, 0x00000002, 0x00000007, 0x00000002, 0x00000006, 0x0000000E, 0x00000000, 0x00000002, 0x00000016, 0x00000007, 0x00000000, 0x00000002, 0x00000008, 0x00000002, 0x00000007, 0x0000000E, 0x00000000, 0x00000002, 0x00000017, 0x00000007, 0x00000000, 0x00000002, 0x00000009, 0x00000002, 0x00000008, 0x0000000E, 0x00000000, 0x00000002, 0x00000018, 0x00000007, 0x00000000, 0x00000002, 0x0000000A, 0x00000002, 0x00000009, 0x0000000E, 0x00000000, 0x00000002, 0x00000019, 0x00000007, 0x00000000, 0x00000002, 0x0000000B, 0x00000002, 0x0000000A, 0x0000000E, 0x00000000, 0x00000002, 0x0000001A, 0x00000007, 0x00000000, 0x00000002, 0x0000000C, 0x00000002, 0x0000000B, 0x0000000E, 0x00000000, 0x00000002, 0x0000001B, 0x00000008, 0x00000000, 0x00000002, 0x0000000D, 0x00000002, 0x0000000C, 0x0000000E, 0x00000000, 0x00000002, 0x0000001C, 0x00000008, 0x00000000, 0x00000002, 0x0000000E, 0x00000002, 0x0000000D, 0x0000000E, 0x00000000, 0x00000002, 0x0000001D, 0x00000007, 0x00000000, 0x00000002, 0x0000000F, 0x00000002, 0x0000000E, 0x0000000E, 0x00000000, 0x00000002, 0x0000001E, 0x00000008, 0x00000000, 0x00000010, 0x00000000, 0x00000002, 0x00000010, 0x00000002, 0x00000011, 0x0000000F, 0x00000000, 0x00000012, 0x00000054, 0x00000002, 0x0000001F, 0x00000001, 0x00000000, 0x00000003, 0x00000001] op_pattern = [i for i in opcodes[10:160:10]] print(op_pattern) targets = [c - 20 for c in cipher] inputs = [0] * 16 inputs[0] = targets[0] for i in range(1, 16): prev_input = inputs[i - 1] target = targets[i] const = CoConsts[i - 1] op_type = op_pattern[i - 1] if op_type == 7: # ADD intermediate = (target - const)&0xff elif op_type == 8: # SUB intermediate = (const - target)&0xff inputs[i] = intermediate ^ prev_input print(bytes(inputs).hex()) # df9d4ba41258574ccb7155b9d01f5c58 ~~~ ## obfuscate ida反编译错误,main有爆红,不用管直接分析其他函数,分别找到如下函数 首先是3处有效反调试 1. ptrace ~~~c __int64 sub_7E20() { __int64 result; // rax result = ptrace(PTRACE_TRACEME, 0LL, 0LL); if ( result == -1 ) _exit(1); return result; } ~~~ 2. 读取文件Tracerid值(动调跟进解密后的字符串可以看到Tracerid),可以发现读取了"/proc/self/status"并寻找"TracerPid:" ~~~c __int64 sub_55555555BE50() { __int64 result; // rax int v1; // [rsp+0h] [rbp-10h] BYREF __int64 v2; // [rsp+5h] [rbp-Bh] BYREF __int16 v3; // [rsp+Dh] [rbp-3h] char v4; // [rsp+Fh] [rbp-1h] v2 = 0xFBF31E8058E31D9LL; v3 = 31209; v4 = -17; v1 = 1726956429; sub_55555555C1D0((__int64)&v2, (__int64)&v1, 11LL, 4LL); result = sub_55555555BEC0((char *)&v2); if ( (int)result > 0 ) _exit(1); return result; } __int64 __fastcall sub_55555555BEC0(char *a1) { char s[140]; // [rsp+10h] [rbp-D0h] BYREF unsigned int v3; // [rsp+9Ch] [rbp-44h] BYREF FILE *stream; // [rsp+A0h] [rbp-40h] char *needle; // [rsp+A8h] [rbp-38h] int v7; // [rsp+B4h] [rbp-2Ch] BYREF char filename[18]; // [rsp+BAh] [rbp-26h] BYREF int v9; // [rsp+CCh] [rbp-14h] BYREF char modes[2]; // [rsp+D2h] [rbp-Eh] BYREF int v11; // [rsp+D4h] [rbp-Ch] BYREF int v12; // [rsp+DAh] [rbp-6h] BYREF __int16 v13; // [rsp+DEh] [rbp-2h] v12 = 1053795514; v13 = -10245; v11 = 468703135; sub_55555555C1D0((__int64)&v12, (__int64)&v11, 6LL, 4LL); *(_WORD *)modes = 29201; v9 = 233665123; sub_55555555C1D0((__int64)modes, (__int64)&v9, 2LL, 4LL); qmemcpy(filename, "uU \\9\n!V6C}@.D&F)%", sizeof(filename)); v7 = 861021530; sub_55555555C1D0((__int64)filename, (__int64)&v7, 18LL, 4LL); needle = a1; stream = fopen(filename, modes); if ( stream ) { v3 = -1; while ( fgets(s, 128, stream) && (!(unsigned int)__isoc99_sscanf(s, &v12, s, &v3) || !strstr(s, needle)) ) // "TracerPid:" ; fclose(stream); return v3; } else { return (unsigned int)-1; } } ~~~ 3. getpid ~~~c unsigned __int64 sub_8030() { unsigned __int64 v0; // rax unsigned __int64 v1; // rax unsigned __int64 result; // rax __int64 v3; // [rsp+8h] [rbp-8h] v0 = __rdtsc(); v3 = v0; getpid(); v1 = __rdtsc(); result = v1 - v3; if ( result > 0x186A0 ) _exit(1); return result; } ~~~ 后来发现还有个不怎么常见的 ~~~c const char **sub_55555555C070() { const char **result; // rax const char **i; // [rsp+8h] [rbp-28h] _QWORD v2[4]; // [rsp+10h] [rbp-20h] BYREF v2[0] = "LD_PRELOAD"; v2[1] = "gdb"; v2[2] = "TERM=linux"; v2[3] = qword_55555555EDD8; for ( i = (const char **)v2; ; ++i ) { result = i; if ( !*i ) break; if ( getenv(*i) ) _exit(1); } return result; } ~~~ 通过检测LD_PRELOAD和gdb等环境变量来反调试 一个个force jmp或者nop后patch即可动调 比较函数如下 ~~~c int __fastcall sub_6180(__int64 a1) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] *(_QWORD *)format = 0x61C477DB26D672BDLL; v11 = 0x41BD3F9C2FD86CACLL; v9 = 1102520059; xor_dec(format, &v9, 16LL, 4LL); memcpy(dest, &unk_9031, sizeof(dest)); v7 = 1350490027; xor_dec(dest, &v7, 38LL, 4LL); memcpy(src, &cmp, 0x21uLL); v5 = 1189641421; xor_dec(src, &v5, 33LL, 4LL); v4 = a1; memcpy(v3, src, 0x21uLL); for ( i = 0; i < 32; ++i ) { if ( *(unsigned __int8 *)(v4 + i) != (unsigned __int8)v3[i] ) { printf(format); exit(1); } } return printf(dest); } ~~~ 以及两个混淆比较厉害的加密函数(分析可知是rc5加密),可以借助ida9自带的goomba插件解除部分混淆,右键 De-obfuscate即可,但是得到的函数依然存在很多逻辑混淆 比如恒真恒假跳转 ~~~c if ( unk_B1C8 < 10 && unk_B1C8 >= 10 ) // 恒假 goto LABEL_26; if ( unk_B218 >= 10 || unk_B218 < 10 ) // 恒真 break; ~~~ 减1 ~~~c *v37 - 1067854539 + 1067854538 // 等价于*v37 - 1 ~~~ 异或 ~~~c *v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48 // 等价于*v24^0x51BF6B48 ~~~ 偶数次异或值不变 ~~~c (*v24 & 0xAE4094B7 | ~*v24 & 0x51BF6B48) ^ (*v23 & 0xAE4094B7 | ~*v23 & 0x51BF6B48) // 等价于*v24^*v23 ~~~ 或 ~~~c v9 ^ v8 | v9 & v8 // 等价于v9|v8 ~~~ 最后可以得到两份干净简洁的伪代码交给gemini分析下 ~~~c __int64 __fastcall sub_555555555250(__int64 a1, __int64 a2) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] v41 = a1; v42 = a2; v2 = HIDWORD(v42); v32 = &v27 - 2; v33 = &v27 - 2; v34 = &v27 - 2; v35 = &v27 - 2; v36 = &v27 - 2; v37 = &v27 - 2; v38 = &v27 - 2; v39 = &v27 - 2; v40 = &v27 - 2; *(&v27 - 2) = v41; HIDWORD(v27) = v2; LODWORD(v27) = 0; LABEL_3: v31 = *v37 < 4u; if ( v31 ) { v3 = v38; *(v34 + *v37) = 0; *v3 = 0; while ( 1 ) { if ( *v38 >= 4u ) { ++*v37; goto LABEL_3; } *(v34 + *v37) = *(*v33 + (*v38 + 4 * *v37)) + (*(v34 + *v37) << 8); ++*v38; } } v4 = v37; **v32 = 0xB7E15163; *v4 = 1; while ( 1 ) { if ( *v37 >= 0x1Au ) break; *(*v32 + 4LL * *v37) = *(*v32 + 4LL * (*v37 - 1)) - 0x61C88647; ++*v37; } v7 = v35; v8 = v36; v9 = v37; *v38 = 0; *v9 = 0; *v8 = 0; *v7 = 0; *v39 = 0; while ( 2 ) { if ( *v39 < 78 ) { v10 = v38; v11 = v36; v12 = v34; v13 = v35; v15 = ((*v35 + *(*v32 + 4LL * *v37) + *v36) >> 29) | ((*v35 + *(*v32 + 4LL * *v37) + *v36) << 3); *(*v32 + 4LL * *v37) = v15; *v13 = v15; v18 = v37; v19 = ((*v11 + *v35 + *(v12 + *v10)) >> ((*v11 + *v35) + 32)) | ((*v13 + *(v12 + *v10) + *v11) << (*v13 + *v11)); *(v12 + *v10) = v19; *v11 = v19; v20 = v38; *v18 = (*v18 + 1) % 0x1A; *v20 = (*v20 + 1) & 3; ++*v39; } break; } result = 1; return result; } int *__fastcall sub_555555555E80(_DWORD *a1, __int64 a2, _DWORD *a3) { // [COLLAPSED LOCAL DECLARATIONS. PRESS NUMPAD "+" TO EXPAND] v26 = a1; v27 = a2; v28 = a3; v3 = v28; v22 = &v21 - 2; v23 = (&v21 - 2); v24 = &v21 - 2; v25 = &v21 - 2; *(&v21 - 2) = v26; v21 = v3; LODWORD(v21) = **(&v21 - 2) + *v3; *(&v21 - 4) = (*(&v21 - 2))[1] + v21[1]; *(&v21 - 4) = 1; while ( *v25 <= 0xCu ) { *v23 = *(*v21 + 4LL * (2 * *v25)) + (((*v24 ^ *v23) << *v24) | ((*v24 ^ *v23) >> (32 - *v24))); *v24 = (((*v23 ^ *v24) >> (32 - *v23)) | ((*v23 ^ *v24) << *v23)) + *(*v21 + 4LL * (2 * *v25 + 1)); *v23 = *v24 ^ *v23; ++*v25; } v11 = v22; v12 = v24; **v22 = *v23; result = *v11; result[1] = *v12; return result; } ~~~ 分析可知是RC5的密钥扩展和加密函数,其中加密函数地方做了魔改,对在轮加密种A多异或了B,解密脚本如下 还有个很坑的点卡了很久,密钥是"cleWtemoH3Lo!FTC",而不是WelcometoL3HCTF!,正好是后者小端存储形式的字符串(需要动调去密钥扩展里看从内存里读取的到底是什么值) ~~~python import struct class RC5: def __init__(self, key: bytes): self.w = 32 # 字长(比特) self.r = 12 # 轮数 self.b = len(key) # 密钥长度 self.t = 2 * (self.r + 1) # 密钥表大小 self.mod = 1 << self.w # 模数 self.S = self._expand_key(key) def _expand_key(self, key: bytes) -> list: # 初始化常量 P, Q = 0xB7E15163, 0x61C88647 # 初始化密钥表 S = [P] for i in range(1, self.t): S.append((S[i - 1] - Q) % self.mod) # 将密钥转换为字列表 c = max(len(key) // 4, 1) L = [0] * c for i in range(len(key)): idx = i // 4 shift = 8 * (i % 4) L[idx] = (L[idx] + (key[i] << shift)) % self.mod # 混合密钥 i = j = 0 A = B = 0 for _ in range(3 * max(self.t, c)): A = S[i] = self.rotl((S[i] + A + B) % self.mod, 3) B = L[j] = self.rotl((L[j] + A + B) % self.mod, (A + B) % self.w) i = (i + 1) % self.t j = (j + 1) % c return S def rotl(self, x: int, n: int) -> int: n %= self.w return ((x << n) | (x >> (self.w - n))) % self.mod def rotr(self, x: int, n: int) -> int: n %= self.w return ((x >> n) | (x << (self.w - n))) % self.mod def decrypt_block(self, data: bytes) -> bytes: # 解析输入块 A = struct.unpack('<I', data[:4])[0] B = struct.unpack('<I', data[4:8])[0] # 解密过程 for i in range(self.r, 0, -1): A = A ^ B B = self.rotr((B - self.S[2 * i + 1]) % self.mod, A) ^ A A = self.rotr((A - self.S[2 * i]) % self.mod, B) ^ B B = (B - self.S[1]) % self.mod A = (A - self.S[0]) % self.mod # 打包输出 return struct.pack('<II', A, B) def encrypt_block(self, data: bytes) -> bytes: """加密一个64位数据块""" A = struct.unpack('<I', data[:4])[0] B = struct.unpack('<I', data[4:8])[0] # 初始白化 A = (A + self.S[0]) % self.mod B = (B + self.S[1]) % self.mod # 轮函数 for i in range(1, self.r + 1): A = (self.rotl((A ^ B), B) + self.S[2 * i]) % self.mod B = (self.rotl((B ^ A), A) + self.S[2 * i + 1]) % self.mod A ^= B return struct.pack('<II', A, B) def encrypt(self, plaintext: bytes) -> bytes: """加密任意长度数据""" # 分块加密 blocks = [plaintext[i:i+8] for i in range(0, len(plaintext), 8)] ciphertext = b'' for block in blocks: ciphertext += self.encrypt_block(block) return ciphertext def decrypt(self, ciphertext: bytes) -> bytes: # 处理填充(示例使用PKCS#7) blocks = [ciphertext[i:i + 8] for i in range(0, len(ciphertext), 8)] plaintext = b'' for block in blocks: plaintext += self.decrypt_block(block) return plaintext if __name__ == "__main__": key = b"cleWtemoH3Lo!FTC" rc5 = RC5(key) plainttext = b"flag{11111222222333333333333334}" ciphertext = rc5.encrypt(plainttext) print(ciphertext.hex()) ciphertext = bytes([0x1B, 0xBB, 0xA1, 0xF2, 0xE9, 0x7C, 0x87, 0x21, 0x8A, 0x37, 0xFD, 0x0A, 0x94, 0x1A, 0x81, 0xBC, 0x40, 0x1E, 0xE3, 0xAA, 0x73, 0x2E, 0xD8, 0x3F, 0x84, 0xB8, 0x71, 0x42, 0xCC, 0x35, 0x8B, 0x39]) plaintext = rc5.decrypt(ciphertext) print(f"Decrypted: {plaintext}") ~~~ ## easyvm 调试可以发现是类tea加密,8字节一组变化 最开始做复杂了一点点去分析VM里每个指令的作用并试图模拟,最后才想到直接ida下条件断点在重要运算指令上即可,把两个操作数打印下就知道每一步计算都做了什么 找到vm计算指令的位置(tea中主要为add、sub、xor、shl、shr),简单写下idapython脚本,以此来模拟trace获得加密log  ~~~python import idc, idaapi op1_val = idc.get_reg_value("EDX") op2_val = idc.get_reg_value("ECX") & 0xFF result_val = idc.get_reg_value("EDX") print(f"shl {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val<<op2_val)&0xffffffff)}") import idc, idaapi op1_val = idc.get_reg_value("EDX") op2_val = idc.get_reg_value("ECX") & 0xFF print(f"shr {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val>>op2_val)&0xffffffff)}") import idc, idaapi op1_val = idc.get_reg_value("EAX") rbp_val = idc.get_reg_value("RBP") mem_addr = rbp_val + 0x4C op2_val = idc.get_wide_dword(mem_addr) print(f"xor {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val^op2_val)&0xffffffff)}") import idc, idaapi op1_val = idc.get_reg_value("EAX") rbp_val = idc.get_reg_value("RBP") mem_addr = rbp_val + 0x1C op2_val = idc.get_wide_dword(mem_addr) print(f"sub {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val-op2_val)&0xffffffff)}") import idc, idaapi op1_val = idc.get_reg_value("EAX") op2_val = idc.get_reg_value("EDX") print(f"add {hex(op1_val)}, {hex(op2_val)} = {hex((op1_val+op2_val)&0xffffffff)}") ~~~ log取一轮加密来分析 ~~~ shl 0x32323232, 0x3 = 0x91919190 add 0xa56babcd, 0x91919190 = 0x36fd3d5d add 0x0, 0x32323232 = 0x32323232 add 0x0, 0x32323232 = 0x32323232 xor 0x32323232, 0x36fd3d5d = 0x4cf0f6f shr 0x32323232, 0x4 = 0x3232323 add 0xffffffff, 0x3232323 = 0x3232322 xor 0x4cf0f6f, 0x3232322 = 0x7ec2c4d add 0x31313131, 0x7ec2c4d = 0x391d5d7e add 0x11223344, 0x0 = 0x11223344 shl 0x391d5d7e, 0x2 = 0xe47575f8 add 0xffffffff, 0xe47575f8 = 0xe47575f7 add 0x11223344, 0x391d5d7e = 0x4a3f90c2 add 0xabcdef01, 0x4a3f90c2 = 0xf60d7fc3 xor 0xf60d7fc3, 0xe47575f7 = 0x12780a34 shr 0x391d5d7e, 0x5 = 0x1c8eaeb add 0xa56babcd, 0x1c8eaeb = 0xa73496b8 xor 0x12780a34, 0xa73496b8 = 0xb54c9c8c add 0x32323232, 0xb54c9c8c = 0xe77ecebe sub 0x40, 0x1 = 0x3f ~~~ 可以看到三个密钥0xa56babcd、0xffffffff、0xabcdef01以及delta=0x11223344 往后分析看所有轮加密完total做了什么,可以发现下一组加密8字节用的total值是上一组结束后的total值 ~~~ add 0x11223344, 0x488cd100 = 0x59af0444 // 上一组total shl 0x6bc23e4e, 0x2 = 0xaf08f938 add 0xffffffff, 0xaf08f938 = 0xaf08f937 add 0x59af0444, 0x6bc23e4e = 0xc5714292 add 0xabcdef01, 0xc5714292 = 0x713f3193 xor 0x713f3193, 0xaf08f937 = 0xde37c8a4 shr 0x6bc23e4e, 0x5 = 0x35e11f2 add 0xa56babcd, 0x35e11f2 = 0xa8c9bdbf xor 0xde37c8a4, 0xa8c9bdbf = 0x76fe751b add 0x34343434, 0x76fe751b = 0xab32a94f sub 0x40, 0x1 = 0x3f shl 0xab32a94f, 0x3 = 0x59954a78 add 0xa56babcd, 0x59954a78 = 0xff00f645 add 0x59af0444, 0xab32a94f = 0x4e1ad93 add 0x0, 0x4e1ad93 = 0x4e1ad93 xor 0x4e1ad93, 0xff00f645 = 0xfbe15bd6 shr 0xab32a94f, 0x4 = 0xab32a94 add 0xffffffff, 0xab32a94 = 0xab32a93 xor 0xfbe15bd6, 0xab32a93 = 0xf1527145 add 0x6bc23e4e, 0xf1527145 = 0x5d14af93 add 0x11223344, 0x59af0444 = 0x6ad13788 // 新一组total ~~~ 搞懂加密逻辑直接开逆 ~~~python from ctypes import c_uint32 def tea_encrypt(r, v, key, delta): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) total = c_uint32(0) for i in range(r): v0.value += ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1]) total.value += delta v1.value += ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0]) return v0.value, v1.value def tea_decrypt(r, v, key, delta, id): v0, v1 = c_uint32(v[0]), c_uint32(v[1]) total = c_uint32(delta * r * (id//2+1)) for i in range(r): v1.value -= ((v0.value << 2) + key[1]) ^ (v0.value + total.value + key[2]) ^ ((v0.value >> 5) + key[0]) total.value -= delta v0.value -= ((v1.value << 3) + key[0]) ^ (v1.value + total.value) ^ ((v1.value >> 4) + key[1]) return v0.value, v1.value v = [2272944806, 1784017395, 2920892487, 2984657895, 2840586369, 2613617290, 3301943967, 4053798049] k = [0xa56babcd, 0xffffffff, 0xabcdef01] delta = 0x11223344 for i in range(0, len(v), 2): v[i:i+2] = tea_decrypt(64, v[i:i+2], k, delta, i) print(list(map(hex, v))) v = "".join([int.to_bytes(v[i], byteorder='little', length=4).decode() for i in range(len(v))]) print(v) ~~~ ## AWayOut2 这个题感觉现出的,一股纯纯为难人的味道 > 我整体的思路很取巧,就是侧信道爆破,大部分人应该没想到 最开始看了眼很明显代码根本读不懂,动调还会跑飞,一开始目标就挺明确,就是侧信道爆破。借助的是pintool,在以往好多比较难分析逻辑的题发挥了一定作用。因此这次比赛我也是毫不犹豫直接上来爆破分析,爆破第一位字母发现hjk指令数和其他异常,后续逐渐发现hjkl为输入范围 ~~~ a 419744191 k 419730508 h 419730512 j 419730514 l 419744180 # x la 640207084 lk 640193401 ll 640207073 # x lj 640193407 lla 860727831 llk 860714148 lll 860714144 llj 860727830 # x ~~~ 可以发现输入正确的指令数会和输入不是hjkl指令数相差较小,而其他的指令数绝对值差都大于1000,因此可以逐位进行爆破,使用DFS遍历所有可能 由于输入后返回结果时间较长,不使用多线程爆破时间太长了,因此我写了份基本单线程脚本 ~~~python import os import collections import time from pwn import * # --- 配置 --- context.log_level = 'error' PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"] # 字符集顺序现在变得更重要。将常见字符放在前面可能会提高效率。 CHARSET = "{hjkl" FLAG_LENGTH = 118 ANOMALY_THRESHOLD = 9000 # 显著差异的阈值,可以微调 # --- 辅助函数 (与之前相同) --- def get_instruction_count(test_flag): """ 运行 PIN 工具并获取给定 flag 的指令数。 """ try: p = process(PIN_COMMAND) p.recvuntil(b'try:\n', timeout=5) p.sendline(test_flag.encode()) output = p.recvall(timeout=20) p.close() lines = output.strip().splitlines() if lines and lines[-1].isdigit(): return int(lines[-1]) else: return -1 except Exception as e: return -1 # --- 新的主逻辑 (激进 DFS) --- def solve_aggressive_dfs(known_prefix=""): """ 使用激进的深度优先搜索策略。 一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。 """ # 基本情况:如果长度达到目标,说明成功了 if len(known_prefix) == FLAG_LENGTH: print("\n" + "=" * 60) print(f"[*] 成功!找到完整 Flag: {known_prefix}") print("=" * 60) return True current_pos = len(known_prefix) print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...") # 用于存储当前层级已知的最小指令数 min_count_for_this_level = 1000000 total_chars = len(CHARSET) for i, char in enumerate(CHARSET): test_flag = known_prefix + char progress_text = f" -> 进度: {i + 1}/{total_chars} | 测试: '{test_flag}'" print(progress_text, end=' ') count = get_instruction_count(test_flag) print(count) if count == -1: continue # 跳过执行失败的尝试 if i == 0: tmp = count continue # 核心逻辑:检查当前字符的指令数是否比“已知最小”还要显著降低 # ANOMALY_THRESHOLD 是你说的 "10000左右" if abs(tmp - count) > ANOMALY_THRESHOLD: # continue print(f"\n [*] 发现显著更优字符 '{char}'...") print(f" [*] 立即深入 DFS 搜索 '{test_flag}'...") # 立即递归,不再测试当前层级的其他字符 if solve_aggressive_dfs(test_flag): return True # 如果这条路成功了,直接返回 True else: # 如果深入后发现是死胡同,打印回溯信息并继续在当前层级搜索 print(f"\n[!] 路径 '{test_flag}' 是死胡同, 回溯到第 {current_pos + 1} 位, 继续搜索...") # 即使是死胡同,这个 count 也是一个新的有效最小值,需要更新 min_count_for_this_level = count # 如果不是显著降低,就只更新当前层级的最小指令数 elif count < min_count_for_this_level: min_count_for_this_level = count # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的 print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...") return False # --- 程序入口 --- if __name__ == "__main__": print("=" * 60) print("开始使用“激进”DFS 策略进行全长度 Flag 爆破") print("=" * 60) # 假设 'h' 仍然是正确的第一个字符 if not solve_aggressive_dfs(""): # 如果不确定第一个字符,使用下面这行: # if not solve_aggressive_dfs(""): print("\n" + "=" * 60) print("[!] 未能找到完整的 Flag。") print("=" * 60) ~~~ 上面的是最基础的脚本,后面多线程发现了更多bug - 要限制方向,不能跑反方向,比如你之前向右走了你下一步不能再向左了,中午跑的时候还没发现,下午才发现结果里不停的jkjkjk - timeout加得大大的,越往后越慢,出现了好几次获取指令数为-1,直接把我dfs搞乱了,跑了一下午才发现出错了,赶紧把跑出来的路径打印出来,果然出现了一些很不合理的路径如下,还好前面的都没问题不至于再从头开始跑 <img src="http://xherlock.top/usr/uploads/2025/07/3479053359.png" alt="288ea616-82c6-470a-9528-e9670fd33236" style="zoom:50%;" style=""> - threshold指令差值应该看绝对值,最开始爆破时候没发现l方向完指令数增大,导致做差出现负数,所以一直没有l方向 - 要有一个错误基准,因此设置了一个{输入,后续输入4个方向和输入`{`指令数进行比较,绝对值差在1000以内是可以走的方向 爆破脚本如下 ~~~python #!/usr/bin/env python3 import os from pwn import * from concurrent.futures import ThreadPoolExecutor, as_completed PIN_COMMAND = ["./pin", "-t", "inscount0.so", "--", "./AWayOut2"] CHARSET = "jkhl{" FLAG_LENGTH = 118 MAX_THREADS = 16 def get_instruction_count(test_flag): """ 运行 PIN 工具并获取给定 flag 的指令数。 """ try: p = process(PIN_COMMAND) p.recvline(timeout=200) p.sendline(test_flag.encode()) output = p.recvall(timeout=200) p.close() lines = output.strip().splitlines() if lines and lines[-1].isdigit(): return int(lines[-1]) else: return -1 except Exception as e: return -1 # --- 新的主逻辑 (激进 DFS) --- def solve_aggressive_dfs(known_prefix=""): """ 使用激进的深度优先搜索策略。 一旦发现一个字符比当前层级已知的最小指令数显著更低,就立即深入。 """ # 基本情况:如果长度达到目标,说明成功了 if len(known_prefix) == FLAG_LENGTH: print("\n" + "="*60) print(f"[*] 成功!找到完整 Flag: {known_prefix}") print("="*60) return True current_pos = len(known_prefix) print(f"\n[+] 正在爆破第 {current_pos + 1} 位字符 (前缀: '{known_prefix}')...") # 1. 获取基准字符 '{' 的指令数 (此步骤仍然串行) print(f" -> 获取基准指令数 (字符: '{{')...", end='') ref_flag = known_prefix + "{" ref_count = get_instruction_count(ref_flag) print(f" 指令数: {ref_count}") # 2. 准备所有要并行测试的任务 if known_prefix: if known_prefix[-1] == "l": chars_to_test = ["l", "j", "k"] elif known_prefix[-1] == "h": chars_to_test = ["h", "j", "k"] elif known_prefix[-1] == "j": chars_to_test = ["l", "j", "h"] elif known_prefix[-1] == "k": chars_to_test = ["l", "h", "k"] else: chars_to_test = ["h", "l", "j", "k"] flags_to_test = [known_prefix + char for char in chars_to_test] candidates = [] # 3. 使用线程池并行爆破所有字符 with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor: # executor.map 会将 get_instruction_count 函数应用到 flags_to_test 中的每一项 # 它会按顺序返回结果,这非常方便 all_counts = executor.map(get_instruction_count, flags_to_test) # 4. 收集并处理结果 print(" -> 所有并行任务已完成,正在分析结果...") for char, count in zip(chars_to_test, all_counts): if count != -1: diff = abs(ref_count - count) print(f"{known_prefix+char} (指令数: {count}, 差值: {diff})") if diff < 1000: candidates.append((char, count)) # 对所有候选路径并行发起 DFS 递归 if candidates: with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor: # 提交所有递归任务 futures = [executor.submit(solve_aggressive_dfs, known_prefix + char) for char, _ in candidates] # 等待第一个成功返回的结果 for future in as_completed(futures): if future.result(): # 如果某个子任务返回 True return True # 立刻将成功信号向上传递 # 如果遍历完所有字符都没有找到一条成功的路径,则说明当前前缀是错误的 print(f"\n[-] 在位置 {current_pos + 1} 处所有尝试均失败。回溯...") return False # --- 程序入口 --- if __name__ == "__main__": context.log_level = 'error' print("="*60) print("开始使用“激进”DFS 策略进行全长度 Flag 爆破") print("="*60) if not solve_aggressive_dfs("lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjl"): print("\n" + "="*60) print("[!] 未能找到完整的 Flag。") print("="*60) ~~~ 跑到最后基本就看出来路径没问题了, <img src="http://xherlock.top/usr/uploads/2025/07/1437856745.png" alt="d177101f-3d17-4d53-9632-214263c460a8" style="zoom:50%;" style=""> 正确路径为lljjljjhhjjjjjllkkkllljjlljjjhhhhhhjjjjjlljjhhhjjjjlllkklllllkkkllkkkklkllllljjjlllllklllllljjjhhhjjjljjllljjhhjjljjlj md5后即为flag 应该有更好的思路,但已经没心思研究这个史题了 最后修改:2025 年 07 月 28 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 如果觉得我的文章对你有用,请随意赞赏