📌 题目信息
| 项目 | 内容 |
|---|---|
| 题目名称 | except_expert(异常专家) |
| 题目来源 | 青少年CTF |
| 题目类型 | Reverse(逆向) |
| 子分类 | Windows x86 / C++ SEH 异常处理 / TEA 变种 |
| 考点 | 异常处理机制(VEH/SEH/顶层过滤器)控制流分析、TEA 变种还原 |
| 难度 | ⭐⭐⭐⭐ |
| 最终 Flag | qsnctf{Th3_w1Nd0wS_cPP_Exc3P710N_1S_s0oO_FuN!!!} |

🛠️ 使用工具
- IDA Pro(反汇编 + F5 反编译)
- Python 3(TEA 变种解密脚本)
📝 解题思路
except_expert 主逻辑详解
先要知道:程序在干嘛
这个程序是个 C++ 写的 Windows 控制台程序(x86)。它做的事很简单:
- 让你输入一串字符(作为 flag)
- 对这串字符做一系列"加工"(异或 + TEA 加密变换)
- 拿加工结果和一个程序内置的密文比较
- 完全一样 → 输出
Yes!!!;不一样 → 输出No!!!
所以解题 = 逆推出"哪个输入才能通过验证"。
第 1 步:识别程序特征
TlsCallback_0(0x401630):调用NtSetInformationThread(GetCurrentThread(), 17, 0, 0)→ 反调试(把当前线程从调试器隐藏),所以不能靠动态调试,主要静态分析。
- 关键字符串:
"Hello windows x86 exception!!!"、"Input your flag: "、"Yes!!!"、"No!!!"。


- 程序是 x86 C++。

第 2 步:定位主逻辑
根据上面得到的关键字符串,可以搜索"Input your flag: "等,查询该字符串的位置,从而找到主函数或者其他线索。


找到main函数:

main(0x401B50)流程:
// ============================================================
// main (源码主函数, IDA里是 sub_401B50, 0x401B50)
// 全流程: 提示输入 -> 读入48字节flag -> 核心验证 -> Yes/No
// ============================================================
int __cdecl main(int argc, const char **argv, const char **envp)
{
char v4; // [ebp-54h] ← 其实是 std::string input 的内部存储区(栈上)
int v5; // [ebp-50h] ← input 的第二个dword (string对象的一部分)
int (__cdecl *v6)(int); // [ebp-40h] ← 临时: 传参给输出流用的函数指针
int v7; // [ebp-3Ch] ← input 内部字段(引用计数/长度区)
char *v8; // [ebp-38h] ← 临时指针(后面用来取字符串数据)
int v9; // [ebp-34h] ← 成功分支的返回值(0)
int v10; // [ebp-30h] ← 失败分支的返回值(0)
char v11; // [ebp-29h] ← 存 sub_4019F0 的返回值(bool)
_BYTE v12[24]; // [ebp-28h] ←另一个std::string对象 (其实是eof/输入缓冲对象)
int v13; // [ebp-4h] ← 栈cookie/SEH 相关的状态标志
// ---- ① 打印提示 ----
sub_401FA0(&unk_42C180, "Input your flag: ");
// ↑ 等价于 std::cout << "Input your flag: ";
// &unk_42C180 是全局 stdout 流对象(unk_42C180 别名可能就是 __stdout)
// ---- ② 准备 cin 流 (cin 绑定到 stdout/控制台) ----
sub_402D90(v7, v8); // 内部是把全局 cin 流 与 控制台输入绑定/初始化
v13 = 0; // 初始化栈状态标志
// ---- ③ 读入一行输入到 v12 (std::string input) ----
sub_401D20(&dword_42C108, v12);
// ↑ dword_42C108 = 全局 std::cin 流对象
// v12 = 目标 std::string (存放用户输入)
// 等价于 std::cin >> input; (此题的编译器实现)
// ---- ④ 长度检查 + 核心验证 ----
if ( unknown_libname_24(v12) == 48 // unknown_libname_24 = std::string::length()
&& // ↑ 必须先等于 48 字节才能继续
(v8 = &v4, // 取 input 的底层字符指针
sub_402C10(v12), // 把 v12 拷贝到栈上那个 string(构造 v4/input)
(v11 = sub_4019F0(v4, v5)) != 0) ) // ★★★调用核心验证, 返回非0=通过(关键!!)
{
// ---- ⑤ 验证通过 -> 输出 "Yes!!!" ----
v6 = sub_402970; // 取 endl/换行处理器
sub_401FA0(&unk_42C180, "Yes!!!"); // std::cout << "Yes!!!"
sub_403A00(v6); // std::cout << endl; (刷行)
sub_40C616("pause"); // system("pause"); (暂停)
v9 = 0; // main 返回值 = 0
v13 = -1; // 结束标志
sub_403680(v12); // std::string 析构 (释放 v12)
return v9; // return 0
}
else
{
// ---- ⑥ 验证失败 -> 输出 "No!!!" ----
v6 = sub_402970; // endl
sub_401FA0(&unk_42C180, "No!!!"); // std::cout << "No!!!"
sub_403A00(v6); // std::cout << endl;
sub_40C616("pause"); // system("pause");
v10 = 0; // main 返回值 = 0
v13 = -1;
sub_403680(v12); // std::string 析构
return v10; // return 0
}
}
main函数中的核心验证函数 sub_4019F0是if判断条件中的关键函数,查看该函数:

核心验证函数 sub_4019F0的流程如下:
先看一个必须注意的坑:函数声明是
sub_4019F0(char a1),表面看参数是个char(单字符),但实际上这是 IDA 的反编译假象。真正传进来的是input字符串(栈上那一片 v4/v5 区域),IDA 因为 std::string 识别不出,把它"塌缩"成了单个 char 的引用&a1。
// ============================================================
// sub_4019F0 —— 核心验证函数(阅卷老师)
// 职责: 摆出48字节期望密文 v4 → 把你的输入做(XOR+解密) →
// 解密结果 vs v4 比对, 相等返回 true(通过), 否则 false
// ============================================================
BOOL __cdecl sub_4019F0(char a1) // ⚠️ 表面是char, 实际是"输入字符串"的引用
{
int v1; // 临时: 指向输入字符串的C指针
bool v3; // 最终返回值(是否通过)
_BYTE v4[48]; // 48字节"期望密文"缓冲(程序内置, 比对目标)
int v5; // 栈cookie/结束标志(编译器用的)
v5 = 0; // 初始化栈安全标志(IDA固定模板)
// ---- ① 填充48字节期望密文 v4 (写死的"标准答案") ----
qmemcpy(v4, "r8", 2); // 前2字节 = "r8" (0x72 0x38)
v4[2] = -44; // ⚠️ 注意是 signed char! -44 = 0xD4
v4[3] = -124; // -124 = 0x84
v4[4] = 112; // 112 = 0x70
v4[5] = 4; // = 0x04
v4[6] = -109; // 0x93
v4[7] = 94; // 0x5E
v4[8] = -76; // 0xB4
v4[9] = -6; // 0xFA
v4[10] = -99; // 0x9D
v4[11] = 33; // 0x21
v4[12] = 59; // 0x3B
v4[13] = -29; // 0xE3
v4[14] = 110; // 0x6E
v4[15] = -53; // 0xCB
v4[16] = -105; // 0x97
v4[17] = 59; // 0x3B
v4[18] = -95; // 0xA1
v4[19] = -82; // 0xAE
v4[20] = -59; // 0xC5
v4[21] = 81; // 0x51
v4[22] = 0x80; // 128 = 0x80
v4[23] = 37; // 0x25
v4[24] = -72; // 0xB8
v4[25] = 43; // 0x2B
v4[26] = -39; // 0xD9
v4[27] = 13; // 0x0D
v4[28] = -41; // 0xD7
v4[29] = -56; // 0xC8
v4[30] = -20; // 0xEC
v4[31] = 3; // 0x03
v4[32] = -25; // 0xE7
v4[33] = 62; // 0x3E
v4[34] = -39; // 0xD9
v4[35] = -39; // 0xD9
v4[36] = 57; // 0x39
v4[37] = -122; // 0x86
v4[38] = 26; // 0x1A
v4[39] = 2; // 0x02
v4[40] = -76; // 0xB4
v4[41] = 87; // 0x57
v4[42] = -109; // 0x93
v4[43] = -111; // 0x91
v4[44] = -46; // 0xD2
v4[45] = -41; // 0xD7
v4[46] = -7; // 0xF9
v4[47] = -39; // 0xD9
// ↑ 这一整段就是把"标准答案密文"写进 v4[48];
// 转成无符号字节是: 72 38 d4 84 70 04 93 5e b4 fa 9d 21 3b e3 6e cb
// 97 3b a1 ae c5 51 80 25 b8 2b d9 0d d7 c8 ec 03
// e7 3e d9 d9 39 86 1a 02 b4 57 93 91 d2 d7 f9 d9
// ---- ② 取出输入字符串的C指针 ----
v1 = sub_404D60(&a1);
// ↑ sub_404D60 = std::string::c_str() 之类
// 拿到"你的输入"的原始字符数组首地址 (真正传进来的48字节flag)
// ---- ③ 处理输入: 异或 + 解密 ----
sub_401890(v1);
// ↑ sub_401890 → 内部调 sub_4017E0:
// ① byte_42BF60[i] ^= input[i] (全局缓冲 XOR 输入, 初始是0x66)
// ② sub_401670 → 抛异常 → 异常链里跑TEA解密(真实64轮,sum=0xcdf03780)
// 处理完, byte_42BF60 里就是"你的输入经解密后的结果"
// ---- ④ 比对: 解密结果 vs 期望密文 v4 ----
v3 = sub_408ACE(&byte_42BF60, v4, 48) == 0;
// ↑ sub_408ACE = 内存比较(memcmp风格): 比较 byte_42BF60 和 v4 各48字节
// ==0 表示两者相等 → v3 = true (通过)
// 验证等式: TEA解密( 0x66 ^ input ) == v4
// 所以正确答案: input = TEA加密(v4) ^ 0x66666666
v5 = -1; // 栈安全 cookie 复位(IDA固定收尾模板)
sub_403680(&a1); // std::string 析构 (释放输入对象)
return v3; // 返回是否通过 (1=Yes, 0=No)
}
第 3 步:异或与 TEA 解密流程
上面sub_4019F0函数中的sub_401890(int a1)函数负责处理用户输入(对输入进行异或 + 解密),查看该函数:
// ============================================================
// sub_401890(a1) —— 中转/跳板函数
// 作用: 初始化一个SEH(异常处理)局部帧, 真正干活的是它调用的sub_4017E0(对输入做异或+解密)
// ============================================================
int __cdecl sub_401890(int a1) // a1 = 输入字符串的C指针(48字节 flag)
{
_DWORD v2[15]; //[esp+0h]栈上分配15个dword的局部缓冲(60字节),这是SEH帧记录的老空间
v2[11] = v2; // 把v2自身的地址存进 v2[11],这是"帧内自引用", 用来记录栈帧边界
v2[14] = 0; // v2[14] = 0, 初始化 SEH 帧的状态字段
// ---- 真正的工作在这里 ----
return sub_4017E0(a1);
// ↑ sub_4017E0 干两件事:
// ① 异或
// ② sub_401670(); → 抛异常 → 异常链里跑TEA解密(64轮)
}
可以发现上面的sub_401890函数只是一个中转函数,真正干活的函数是sub_4017E0(int a1),查看该函数:
// ============================================================
// sub_4017E0(a1) —— 对输入做异或, 然后调 sub_401670 解密
// a1 = 输入的C字符串首地址 (48字节 flag)
// 副作用: 修改全局缓冲区 byte_42BF60
// ============================================================
int __cdecl sub_4017E0(int a1) // a1 是整数,但实际是"字符数组指针",所以这里是IDA误判
{
unsigned int i; // 循环变量
// ---- ① 循环 0x30(=48) 次, 逐字节异或 ----
for ( i = 0; i < 0x30; ++i )
byte_42BF60[i] ^= *(_BYTE *)(i + a1);
// ↑ 等价于: byte_42BF60[i] = byte_42BF60[i] ^ input[i]
// *(_BYTE *)(i + a1) = 取 (a1+ i) 这个地址处的1个字节 = input[i]
// ---- ② 调 sub_401670 (内部会抛异常, 异常链里跑TEA解密) ----
return sub_401670();
}
说明: 在后面可以得知,byte_42BF60 在初始化时被 memset(..., 0x66, 0x30) 填成 0x66,所以 byte_42BF60 = 0x66 ^ input。
上面的函数对用户输入的字符串逐字节异或后,还调用了sub_401670()函数,查看该函数:
// ============================================================
// sub_401670 —— "假解密"函数 (出题人的干扰项)
// 表面: 有一整套 TEA 解密逻辑 (32轮)
// 实际: 函数开头就抛异常, 下面的 TEA 代码永远不会执行!
// ============================================================
void *__cdecl sub_401670()
{
int v1; // [ebp-40h] —— 栈上SEH帧数据(异常框架用)
int v2; // [ebp-30h] —— 暂存 delta 初值
const char *v3; // [ebp-2Ch] —— 异常对象/消息字符串指针
unsigned int *v4; // [ebp-28h] —— 指向 dword_42BF64[i] 的指针
char *v5; // [ebp-24h] —— 指向 byte_42BF60[4*i] 的指针
unsigned int j; // 内层循环变量 (轮数)
unsigned int i; // 外层循环变量 (分组)
unsigned int v8; // 当前组的右 dword (变量 y)
unsigned int v9; // 当前组的左 dword (变量 x)
int *v10; // [ebp-10h] SEH帧相关
int v11; // [ebp-4h] SEH状态标志
// ---- ① 初始化栈帧(异常框架) ----
v10 = &v1; // 记录栈帧位置
v11 = 0; // SEH 状态清零
// ---- ② ★ 抛异常 (关键!) ★ ----
v3 = "Hello windows x86 exception!!!"; // 异常携带的消息
sub_40A5FC(&v3, &PA.deinit);
// ↑ sub_40A5FC内部调用RaiseException(0xE06D7363, 1u, 3u, Arguments);
// 一执行到这里, 函数立刻中断, 控制权交给Windows异常机制!
// ★ 下面的所有代码都不会执行 ★
// ---- ③ 下面这些"看起来像解密"的代码 永远不会跑到 ----
v2 = dword_42BFA0; // 取 delta 初值
for ( i = 0; i < 0xC; i += 2 ) // 外层: 分组 (i=0,2,4,6,8,10, 共6组)
{
dword_42BFA0 = v2; // 每组重置 delta 为初值
v5 = &byte_42BF60[4 * i]; // 指向第 i 个 dword (左 x)
v4 = &dword_42BF64[i]; // 指向相邻 dword (右 y)
v9 = *(_DWORD *)v5; // x = 左dword
v8 = *v4; // y = 右dword
for ( j = 0; j < 0x20; ++j ) // 内层: 32轮 TEA 解密
{
// TEA变种解密公式 (delta=0x1337C0DE=322420958)
v8 -= ((v9 >> 4) + 22) ^ (dword_42BFA0 + v9) ^ (32 * v9 + 11);
v9 -= ((v8 >> 4) + 44) ^ (dword_42BFA0 + v8) ^ (32 * v8 + 33);
dword_42BFA0 -= 322420958; // delta 递减
}
*(_DWORD *)v5 = v9; // 结果写回缓冲区
*v4 = v8;
}
// ---- ④ 收尾 (同样不会执行 - 被throw跳过) ----
sub_40A5FC(0, 0); // 结束异常上下文
return &loc_4017B5; // 返回一个标签地址(异常跳转点)
}
说明(关键!):sub_401670(0x401670)函数开头就直接抛 C++ 异常,紧跟的 32 轮 TEA 代码根本不会执行——这是出题人放的假代码。
第 4 步:分析真实异常控制流(关键!)
上面的函数通过sub_40A5FC(_DWORD *a1, _BYTE *a2)函数来抛出异常,查看该函数:
// ============================================================
// sub_40A5FC —— C++ "throw" 的底层实现 (抛异常函数)
// 本质: 调用 Windows API RaiseException 主动抛出一个 C++ 异常
// 对应源码: throw <异常对象>;
// ============================================================
void __stdcall sub_40A5FC(_DWORD *a1, _BYTE *a2)
// a1 = 指向异常消息(对象)的指针 ("Hello windows x86 exception!!!")
// a2 = 指向异常类描述信息(作用域表/类型信息)的指针, 可能为 0
{
_BYTE *v2; // 异常对象的类型描述指针
ULONG_PTR v3; // 异常类型标记 (后面传给RaiseException)
int v4; // 临时: 指向异常对象元数据
ULONG_PTR Arguments[3]; // 传给 RaiseException 的3个参数数组
v2 = a2; // 先用传入的作用域信息
v3 = 429065504; // 429065504 = 0x19930520
// → 这是"C++异常"的默认标记(标识这是个C++ throw)
// ---- ① 处理异常对象的"描述信息"(作用域/析构相关, 逻辑复杂但非核心) ----
if ( a2 ) // 若带了作用域表
{
// 判断类型描述的标志位。若 (*a2 & 0x10)==0 → 简单情况, 直接往下
if ( (*a2 & 0x10) == 0
|| (v4 = *a1 - 4, // 取异常对象头部的元数据
v2 = *(BYTE**)(*(DWORD*)v4 + 24), // 从元数据里取出"类型信息/析构函数"指针
(*(void(__thiscall**)(DWORD,int))(*(DWORD*)v4 + 32))(...), // 调用"拷贝/ctor"函数
v2) )
{
// 若类型描述标志里有 0x08 (说明需要特定处理)
if ( (*v2 & 8) != 0 )
v3 = 26820608; // 26820608 = 0x19930520 的另一变体(带flag的C++标记)
}
}
// ↑ 这一段本质是在"C++运行时代码"里解析异常的__eh/类型信息, 确定正确的异常标记
// 对理解题目不重要, 知道它是在准备异常描述即可
// ---- ② 组装传给 RaiseException 的参数 ----
Arguments[1] = (ULONG_PTR)a1; // 参数1: 异常对象指针(消息字符串)
Arguments[0] = v3; // 参数0: C++异常类型标记 (0x19930520或其变体)
Arguments[2] = (ULONG_PTR)v2; // 参数2: 类型描述/作用域指针
// ---- ③ ★ 灵魂: 主动抛异常 ★ ----
RaiseException(0xE06D7363, 1u, 3u, Arguments);
// ↑ Windows API: 人为制造一个"C++异常"
// 异常代码 = 0xE06D7363 (MSVC C++异常的专属标识)
// flags = 1 (nonexecutable)
// 参数数量 = 3
// Arguments = 上面组装的那3个
// 执行到这里, 函数不再返回 —— 控制权交给Windows异常分发!
}
说明:sub_40A5FC 就是MSVC C++ 的 throw 底层实现:前面一大段是解析异常对象的元数据(编译器样板代码,不是业务逻辑),最后调用 RaiseException(0xE06D7363, ...) 主动抛出一个 C++ 异常,异常对象就是那串 "Hello windows x86 exception!!!"。这段"异常"不是程序出错,而是出题人用它当开关,去触发后面注册的异常处理器(真正的 TEA 解密)。
异常抛出后,靠异常机制接力:
关键证据 :看"注册了谁"(谁被挂进异常机制)
异常处理器必须先被注册,否则 Windows 不会调用它。因此去查那些"注册异常处理器的 API"被谁调用:
查看AddVectoredExceptionHandler被谁注册:


注意: 这里可以发现,byte_42BF60的48个字节全部被填成0x66

sub_401270注册了AddVectoredContinueHandler:

查看SetUnhandledExceptionFilter被谁注册:


总结:
| 注册 API | 作用 | 谁调用了它(xrefs 查证到) |
|---|---|---|
AddVectoredExceptionHandler | 注册 VEH(向量异常处理) | sub_4015D0(0x4015dd)→ 注册的是 sub_401270 |
AddVectoredContinueHandler | 注册 VCH | sub_401270(0x40128e)→ 里面注册 Handler |
SetUnhandledExceptionFilter | 设置顶层异常过滤器 | sub_401160(0x40125f)→ 设的是 TopLevelExceptionFilter |
注册树:
sub_4015D0 (初始化总入口)
│
└─ 注册 #1: AddVectoredExceptionHandler(1, sub_401270) ← ① VEH
│
└─ sub_401270 运行时(认到C++异常后):
注册 #2: AddVectoredContinueHandler(1, Handler) ← ② VCH
sub_401160 (C++ 异常过滤器, 由 CRT 的 __CxxFrameHandler 框架触发)
│
└─ SetUnhandledExceptionFilter(TopLevelExceptionFilter) ← ③ 顶层过滤器
核心难点:由于只有在异常发生时,才执行TEA加密,而这些异常处理器来回横跳、相互接力,全局变量 sum(dword_42BFA0)的值和实际跑的轮数在静态分析时极容易算错。静态看到的 16/32 轮都不是真实轮数。
⚠️ 注意:这类"异常流接力"的题目,千万不要相信单段代码里的轮数。异常处理链会多次进入/退出,累计的轮数和 sum 会和静态看到的大不相同。要么动态跟,要么用"已知明文 + 爆破"来确定真实参数。
第 5 步:用已知明文头爆破真实参数(顺带解密flag)
由于静态极难算准(sum 初值和真实轮数),采用已知明文爆破法,因为正常 flag 以可打印字符开头(这里队名是 qsnctf,flag 形如 qsnctf{...}),即flag的前面几个字符已知。
已知条件:
- flag 前几个字符已知:
qsnctf{(共 7 个字符) - 验证等式:
TEA解密( 0x66 ^ flag ) == v4- 也就是:
0x66 ^ flag经过解密后,应该等于 v4(48 字节内置密文) - 反过来说:把 v4 加密,得到
0x66 ^ flag,再异或 0x66 得 flag
- 也就是:
- 但加密参数未知:真实轮数(怀疑 16/32/64…)和 sum 初值(怀疑是某个数)都不确定,静态算错
爆破策略:
我们要找的 flag 应该以 qsnctf{ 开头,这给了一个合法性判据。由于 sum 是 32 位整数、盲扫范围大,而轮数相对有限,先固定最可能的轮数,再只爆破 sum。
- 用固定轮数(回合 64)对 v4 做 TEA 解密 → 得到
X flag = X ^ 0x66666666(还原初始化时的逐字节异或 0x66)- 检查 flag 前几个字符是否为
qsnctf{→ 是,说明该 sum 正确;否则换下一个 sum
因为"TEA 强度足够 + flag 前缀足够独特",只有真正正确的 sum 才能解出 qsnctf{...} 这种可读开头,其余候选都是乱码。
// brute_sum_omp.c
// except_expert —— C + OpenMP 多线程 全扫 sum(0~2^32)
// 找使得"解密 v4第一组 后 == 'qsnctf{'" 的 sum 初值
// 编译: gcc -O3 -fopenmp -march=native -o brute_sum_omp brute_sum_omp.c
// 运行: ./brute_sum_omp (建议用全部核)
#include <stdio.h>
#include <stdint.h>
#include <omp.h>
#define DELTA 0x1337C0DEu
#define MASK 0xFFFFFFFFu
// 64 轮变种 TEA 解密第一组 (L0,R0), 返回是否得到 'qsnctf' 前缀
static inline int check(uint32_t L0, uint32_t R0, uint32_t s0,
uint32_t wL, uint32_t wR3) {
uint32_t L = L0, R = R0, s = s0;
#pragma unroll
for (int j = 0; j < 64; j++) {
R = R - ((((L >> 4) + 44) ^ (s + L) ^ ((L << 5) + 33)) & MASK);
L = L - ((((R >> 4) + 22) ^ (s + R) ^ ((R << 5) + 11)) & MASK);
s = s - DELTA;
}
return ((L ^ 0x66666666u) == wL) && (((R ^ 0x66666666u) & 0xFFFFFFu) == wR3);
}
int main(void) {
// v4 前 8 字节 = 第一组密文 (小端)
unsigned char v4[8] = {0x72, 0x38, 0xd4, 0x84, 0x70, 0x04, 0x93, 0x5e};
uint32_t L0 = v4[0] | (v4[1] << 8) | (v4[2] << 16) | ((uint32_t)v4[3] << 24);
uint32_t R0 = v4[4] | (v4[5] << 8) | (v4[6] << 16) | ((uint32_t)v4[7] << 24);
// 目标: 解密后 (L^0x66666666) == "qsnc"; (R^0x66666666)低3字节 == "tf{"
uint32_t wL = 'q' | ('s' << 8) | ('n' << 16) | ((uint32_t)'c' << 24);
uint32_t wR3 = 't' | ('f' << 8) | ((uint32_t)'{' << 16); // 只取低24位
const uint64_t TOTAL = 0x100000000ULL; // 2^32
volatile int found = 0;
uint32_t result = 0;
double t0 = omp_get_wtime();
// OpenMP 并行: 每个线程扫一段, 找到就设 found 提前退出
#pragma omp parallel
{
uint32_t L = L0, R = R0;
#pragma omp for schedule(static)
for (int64_t i = 0; i < (int64_t)TOTAL; i++) {
if (found) continue; // 已被别的线程找到, 跳过
if (check(L, R, (uint32_t)i, wL, wR3)) {
#pragma omp critical
{ if (!found) { found = 1; result = (uint32_t)i; } }
}
}
}
double t1 = omp_get_wtime();
if (found) printf("FOUND sum = 0x%08X (%.2fs, %d threads)\n",
result, t1 - t0, omp_get_max_threads());
else printf("NOT FOUND (%.2fs)\n", t1 - t0);
return 0;
}
gcc -O3 -fopenmp -march=native -o 1.exe 1.c
.\1.exe

# -*- coding: utf-8 -*-
# 用爆破得到的 (rounds=64, sum=0xCDF03780) 解出完整 flag
MASK = 0xFFFFFFFF
DELTA = 0x1337C0DE
# v4: 48字节期望密文 (从 IDA 提取, 已转无符号)
v4_signed = [114,56,-44,-124,112,4,-109,94,-76,-6,-99,33,
59,-29,110,-53,-105,59,-95,-82,-59,81,128,37,
-72,43,-39,13,-41,-56,-20,3,-25,62,-39,-39,
57,-122,26,2,-76,87,-109,-111,-46,-41,-7,-39]
v4 = bytes([x & 0xFF for x in v4_signed])
rounds = 64
sum_init = 0xCDF03780 # 爆破得到!
flag = bytearray()
for i in range(0, 48, 8): # 6 组
L = int.from_bytes(v4[i:i+4], 'little')
R = int.from_bytes(v4[i+4:i+8], 'little')
s = sum_init
for _ in range(rounds): # 64 轮解密
R = (R - ((((L >> 4) + 44) ^ (s + L) ^ ((L << 5) + 33)) & MASK)) & MASK
L = (L - ((((R >> 4) + 22) ^ (s + R) ^ ((R << 5) + 11)) & MASK)) & MASK
s = (s - DELTA) & MASK
L ^= 0x66666666
R ^= 0x66666666
flag += L.to_bytes(4, 'little')
flag += R.to_bytes(4, 'little')
print(flag.decode('utf-8', 'ignore'))

第 6 步:最终 Flag
qsnctf{Th3_w1Nd0wS_cPP_Exc3P710N_1S_s0oO_FuN!!!}
【Reverse】except_expert&spm=1001.2101.3001.5002&articleId=163949156&d=1&t=3&u=db340016c23c4fa0b1e5f882bb950ded)
3326

被折叠的 条评论
为什么被折叠?



