2026.8.19(1)【AES/逆向】GWCTF 2019 re3

📌 题目信息

项目内容
题目名称[GWCTF 2019]re3
题目来源BUUCTF Reverse
题目类型Reverse(逆向工程)
子分类SMC自修改代码 / 密码学逆向
考点SMC自修改代码、AES-ECB加密、MD5密钥派生、动态调试
难度⭐⭐⭐
最终 Flagflag{924a9ab2163d390410d0a1f670}

这道题虽然是逆向题,但是有用到AES,还是放到密码学里面。

🛠️ 使用工具

  • IDA Pro(静态分析 + 远程动态调试)
  • FindCrypt插件(识别加密算法特征)
  • Python 3 + PyCryptodome(AES解密脚本)

📝 解题思路

这是一道结合了 SMC(Self-Modifying Code,自修改代码)AES加密 的逆向题。程序运行时会先对自身代码进行异或解密,然后使用AES-ECB模式加密输入并与密文比对。

第 1 步:初步分析——发现SMC自修改代码

将程序拖入IDA 64位分析,查看 main 函数伪代码,步骤如下:

  1. 在函数列表里,找到 main 这个函数,双击它。
  2. 这时主视图会跳转到 main 函数的汇编代码处。
  3. 按下键盘上的 F5 键(这是接下来要用的最核心快捷键)。如果弹出一个提示框说“Pseudo-code viewer”,点确定就行。
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int i; // [rsp+8h] [rbp-48h]
  char s[40]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+48h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  __isoc99_scanf("%39s", s);
  if ( (unsigned int)strlen(s) != 32 )
  {
    puts("Wrong!");
    exit(0);
  }
  
  // 修改内存权限:将 0x400000 开始的 0xF000 字节设为可读可写可执行
  mprotect(&dword_400000, 0xF000uLL, 7);
  
  // SMC自修改:将 sub_402219 处的 224 个字节逐字节与 0x99 异或
  for ( i = 0; i <= 223; ++i )
    *((_BYTE *)sub_402219 + i) ^= 0x99u;   // SMC自修改:逐字节异或0x99
  sub_40207B(&unk_603170);                 // 生成AES密钥
  sub_402219(s);                           // 加密输入并校验
}

这里有两个关键点:

  1. mprotect(&dword_400000, 0xF000uLL, 7):将0x400000开始的0xF000(60KB)内存区域设置为**可读、可写、可执行。这是SMC自修改代码能够运行的前提。
  2. for循环异或0x99:将sub_402219函数地址处的224个字节逐字节与0x99异或。程序运行时会先执行这个异或操作,把加密的代码还原成真正的函数代码,然后再调用它。

💡 SMC(Self-Modifying Code):程序在运行时动态修改自身代码的一种技术,常用于反调试和增加逆向难度。本题中,sub_402219 在运行前是一堆"乱码",异或 0x99 后才变成真正的加密函数。

第 2 步:编写IDC脚本解密SMC代码

在IDA中按 Shift + F2 打开脚本编辑器,选择 IDC 语言,运行以下脚本对 sub_402219 进行解密:

#include <idc.idc>
static main()
{
    auto addr = 0x402219;
    auto i = 0;
    for ( i = 0; i <= 223; ++i ) {
        PatchByte(addr + i, Byte(addr + i) ^ 0x99);
    }
}

⚠️ 注意:运行脚本后操作顺序:

  1. 运行脚本后,回到 IDA 的主反汇编窗口,将光标定位到 0x402219按下 G 键,在弹出的输入框中输入 0x402219,回车跳转到该地址)。
  2. 先按 U 键(Undefine,取消当前地址的所有定义)。
  3. 再按 C 键(Code,强制将该区域识别为代码)。
  4. 然后按 P 键(Procedure,将其声明为函数)。
  5. 最后按 F5 即可生成伪代码。

如果跳过 U 直接按 C,IDA 可能会因为残留的数据定义而灰显 C 选项,导致无法转换。

第 3 步:分析核心加密逻辑

解密后,sub_402219 的伪代码如下:

// 验证函数:接收用户输入的32字节flag,返回1表示正确,0表示错误
__int64 __fastcall sub_402219(__int64 a1)   // a1 指向用户输入的字符串
{
  unsigned int v2;          // 存放校验结果(默认为1正确)
  int i;                    // 循环变量
  _BYTE v4[200];            // 缓冲区,用于存放生成的AES密钥
  unsigned __int64 v5;      // 栈保护cookie(canary),防溢出
  v5 = __readfsqword(0x28u); // 获取canary存入v5

  // 【第1步】生成AES密钥
  // sub_400A71内部对全局数据unk_603170(即Base64编码表)进行两次MD5运算,
  // 计算出的16字节哈希值作为AES-128的密钥,保存到v4中
  sub_400A71(v4, &unk_603170);

  // 【第2步】AES-ECB加密
  // sub_40196E是AES加密函数,采用ECB模式(无IV),每次加密16字节。
  // 因为输入共32字节,所以分前后两段分别加密,加密结果直接覆盖原输入
  sub_40196E(v4, a1);        // 加密前16字节(a1[0] ~ a1[15])
  sub_40196E(v4, a1 + 16);   // 加密后16字节(a1[16] ~ a1[31])

  // 【第3步】与硬编码密文对比
  v2 = 1;                    // 先假设校验通过
  for ( i = 0; i <= 31; ++i )
  {
    // 加密后的数据与全局数组byte_6030A0逐字节比较
    // byte_6030A0中存储的是程序预设的正确flag加密后的32字节密文
    if ( *(_BYTE *)(i + a1) != byte_6030A0[i] )
      v2 = 0;                // 有任何一字节不同,校验失败
  }
  return v2;                 // 返回1代表输入正确,0代表错误
}

逻辑分析

  1. sub_400A71 使用 &unk_603170 作为密钥初始化AES上下文
  2. sub_40196E 是AES加密函数,将输入的32位分成前后各16字节分别加密
  3. 加密结果与 byte_6030A0 处的硬编码密文逐字节比较

上面的如果不知道怎么分析可以使用FindCrypt插件
识别加密算法:使用 FindCrypt插件 扫描(需要IDA pro版本),发现程序包含 MD5AES 的特征。进一步分析 sub_40196E 中的 byte_4023A0 数组,这是AES的标准S盒,因此可以确定是 AES-128-ECB模式

第 4 步:动态调试提取AES密钥

sub_40207B(&unk_603170) 函数负责生成AES密钥。分析发现该函数对 Base64密码表 进行了两次MD5加密,密钥与用户输入无关,是固定的。

因此通过 IDA远程动态调试 提取密钥:

  1. 在IDA中配置 Linux远程调试(需将程序拷贝到Linux虚拟机或WSL中运行)
  2. sub_40207B 函数执行完毕后下断点
  3. 运行程序,断下后查看 unk_603170 地址处的内存(注意程序需要输入一个 32 字符的字符串,内容随意)


    提取到的密钥为:
CB8D493521B47A4CC1AE7E62229266CE

第 5 步:提取密文

根据上面的分析可知,加密结果需要与 byte_6030A0 处的硬编码密文逐字节比较,因此 byte_6030A0 处其实就是密文,从 byte_6030A0 处提取硬编码的32字节密文:

BC0AADC0147C5ECCE0B140BC9C51D52B46B2B9434DE5324BAD7FB4B39CDB4B5B

第 6 步:编写Python解密脚本

有了密钥和密文,使用AES-ECB解密:

from Crypto.Cipher import AES
from Crypto.Util.number import long_to_bytes

# 密钥(从动态调试中提取)
key = long_to_bytes(0xcb8d493521b47a4cc1ae7e62229266ce)

# 密文(从 byte_6030A0 提取)
ciphertext = long_to_bytes(0xbc0aadc0147c5ecce0b140bc9c51d52b46b2b9434de5324bad7fb4b39cdb4b5b)

# AES-ECB解密
aes = AES.new(key, AES.MODE_ECB)
flag = aes.decrypt(ciphertext)

print(flag.decode('utf-8'))

运行结果:

⚠️ 注意:密钥字符串长度为32位(16字节),对应 AES-128long_to_bytes() 会自动将十六进制数转为字节串。

🏁 最终 Flag

flag{924a9ab2163d390410d0a1f670}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值