2026.8.11(2)【Reverse】except_expert

📌 题目信息

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

🛠️ 使用工具

  • IDA Pro(反汇编 + F5 反编译)
  • Python 3(TEA 变种解密脚本)

📝 解题思路

except_expert 主逻辑详解
先要知道:程序在干嘛
这个程序是个 C++ 写的 Windows 控制台程序(x86)。它做的事很简单:

  1. 让你输入一串字符(作为 flag)
  2. 对这串字符做一系列"加工"(异或 + TEA 加密变换)
  3. 拿加工结果和一个程序内置的密文比较
  4. 完全一样 → 输出 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注册 VCHsub_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加密,而这些异常处理器来回横跳、相互接力,全局变量 sumdword_42BFA0)的值和实际跑的轮数在静态分析时极容易算错。静态看到的 16/32 轮都不是真实轮数。

⚠️ 注意:这类"异常流接力"的题目,千万不要相信单段代码里的轮数。异常处理链会多次进入/退出,累计的轮数和 sum 会和静态看到的大不相同。要么动态跟,要么用"已知明文 + 爆破"来确定真实参数。

第 5 步:用已知明文头爆破真实参数(顺带解密flag)

由于静态极难算准(sum 初值和真实轮数),采用已知明文爆破法,因为正常 flag 以可打印字符开头(这里队名是 qsnctf,flag 形如 qsnctf{...}),即flag的前面几个字符已知。

已知条件:

  1. flag 前几个字符已知qsnctf{ (共 7 个字符)
  2. 验证等式TEA解密( 0x66 ^ flag ) == v4
    • 也就是:0x66 ^ flag 经过解密后,应该等于 v4(48 字节内置密文)
    • 反过来说:把 v4 加密,得到 0x66 ^ flag,再异或 0x66 得 flag
  3. 加密参数未知:真实轮数(怀疑 16/32/64…)和 sum 初值(怀疑是某个数)都不确定,静态算错

爆破策略:
我们要找的 flag 应该以 qsnctf{ 开头,这给了一个合法性判据。由于 sum 是 32 位整数、盲扫范围大,而轮数相对有限,先固定最可能的轮数,再只爆破 sum

  1. 固定轮数(回合 64)对 v4 做 TEA 解密 → 得到 X
  2. flag = X ^ 0x66666666(还原初始化时的逐字节异或 0x66)
  3. 检查 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!!!}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值