IDA Pro 实战逆向破解入门到大师06:反调试初识 · 字符串解密

file-20260718201626820

今天的题目也非常精妙,它不仅融合了上一关的 XOR 内存解密机制,还引入了逆向工程中极其重要的一个门槛:反调试(Anti-Debug)

可以把这一关定义为“迈向游戏/软件安全与对抗的敲门砖”,进入贴近实战的场景

先补一句,所有的案例资源下载都在 www.52xuejishu.com0基础学逆向,就找小迪老师

那么现在,跟上小迪老师的节奏,不要迷路,不要掉队哦:

阶段一:迎头痛击(认识反调试)

“同学们,当我们像往常一样重命名完 printfscanf,准备一路平推的时候,程序在入口处给我们设下了一个路障:sub_4038BE()

大家跟进这个函数到底层(sub_4088C0),会发现一个金光闪闪的 Windows API:IsDebuggerPresent()

file-20260718201937899


这是什么?这是所有安全对抗、反作弊防护的起点!这个 API 会去检查当前程序是不是正在被调试器(比如 OD、x64dbg、IDA 动态调试)附加。如果是,程序就会打印
Debugger detected! 然后直接退出,根本不给你输入密码的机会!”

核心知识点:

当然了,在静态分析(只看不运行)时,这个反调试形同虚设;但如果在实战中需要动态调试,就必须在汇编层面把调用这个 API 后面的条件跳转指令(比如 jz 改成 jmp)给 Patch 掉,这就是俗称的“爆破”。

阶段二:第一道伪装门(重温 ASCII 与指针)

越过了反调试的拦截,程序来到了第一个密码验证逻辑 sub_404796(底层是 sub_408920)。

BOOL __cdecl sub_408920(char *Str)
{
  return j__strlen(Str) == 6 && *Str == 65;
}

“如果你能看懂这行代码,说明大家对上一节课的知识吸收得非常好。*Str 就是字符串的第一个字符,65 就是大写字母 A(还记得吗,可以按下R键转换)

file-20260718202405010


也就是说,只要我们输入一个长度刚好为 6,且以
A 开头的字符串,比如 A12345 或者 A_bcde,程序就会直接提示 Success。但作为一个追求极致的逆向工程师,我们都知道这只是开发者的‘障眼法’,真正的硬核密码藏在后面的 else 分支里!”

阶段三:硬核解密(XOR 加密的实战推演)

如果第一道门验证失败,程序会把那个长得像乱码一样的 String2 (")3=7;") 送进 sub_401FFF 进行处理,然后再跟我们的输入进行比较。

让我们跟进加密核心函数 sub_4088E0

  for ( i = 0; *(_BYTE *)(i + a1); ++i )
  {
    *(_BYTE *)(i + a1) ^= 0x5Au;
    result = i + 1;
  }

这边我们直接修复一下函数,将参数的 int 类型通过快捷键 Y 修复为 char *
瞬间变得清清爽爽

file-20260718202640044

推演计算密码:

“又是我们最喜欢的单字节异或(XOR)!这次的密钥变成了 0x5A。我们把内存里的乱码 )3=7; 逐个拿出来跟 0x5A 碰一碰,看看会变出什么魔法。”

  • 字符 ) 的 ASCII 是十六进制 0x290x29 ^ 0x5A = 0x73(字母 s
  • 字符 3 的 ASCII 是十六进制 0x330x33 ^ 0x5A = 0x69(字母 i
  • 字符 = 的 ASCII 是十六进制 0x3D0x3D ^ 0x5A = 0x67(字母 g
  • 字符 7 的 ASCII 是十六进制 0x370x37 ^ 0x5A = 0x6D(字母 m
  • 字符 ; 的 ASCII 是十六进制 0x3B0x3B ^ 0x5A = 0x61(字母 a

推演结果:真正的终极密码是 sigma

** 字符串解密过程讲解:**

有很多同学困惑:sub_401FFF(String2);在调用完以后会把string解密更新吗?为什么,是因为sub_4088E0函数会直接修改原字符串?

为了解答这个新手问题,我们先把相关的函数代码列出来:

Decrypt_Real_Password(String2);

// attributes: thunk
int __cdecl Decrypt_Real_Password(int a1)
{
  return sub_4088E0(a1);
}

int __cdecl sub_4088E0(char *a1)
{
  int result; // eax
  int i; // [esp+0h] [ebp-4h]

  for ( i = 0; a1[i]; ++i )
  {
    a1[i] ^= 0x5Au;
    result = i + 1;
  }
  return result;
}

接下来为大家详细拆解这个过程的底层原理:

  1. 传参阶段:传的是“地址”而不是“内容”
    main_0 函数中,String2 被定义为一个局部数组: CHAR String2[8]; // [ebp-8h] 当代码调用 Decrypt_Real_Password(String2); 时,C 语言并不会把 ")3=7;" 这串字符复制一份传给函数。相反,数组名 String2 会自动退化为一个指针,传递的是这个数组在栈上的起始内存地址(即 [ebp-8h] 的绝对地址)。
  2. 执行阶段:指针的“原地手术”
    我们进入底层的
    sub_4088E0(int a1) 函数。这里的参数 a1 接收到的,正是 String2 的首地址。
    看这行没有被美化修复的核心加密/解密代码:
    *(_BYTE *)(i + a1) ^= 0x5Au;
    a1:是原始数组 String2 的首地址。
    i + a1:是指向当前要处理的具体某一个字符的内存地址。
    *(解引用):顺着这个内存地址,直接摸到了 main_0 函数栈空间里的那个物理字节。
    ^=(异或并赋值):将取出来的字节与 0x5A 异或后,直接写回原地址
  3. 结果验证
    正因为这种操作是“原地”覆写的,当
    Decrypt_Real_Password(String2) 函数执行完毕并返回到 main_0 时,原始 String2 所在的内存空间里的数据已经被改写了。原来的 ")3=7;" 已经变成了明文 "sigma"
    所以,紧接着执行的
    lstrcmpA(String1, String2),实际上是在比较用户的输入(String1)和解密后的明文(变成 "sigma"String2)。

核心总结
在 C/C++ 逆向分析中,
只要看到把一个数组的首地址(或者指针)作为参数传进一个函数,并且函数内部对该指针进行了解引用赋值(如 *ptr = ...ptr[i] = ...),就意味着原数据会被直接修改。 这是一个非常典型的 In-place Modification(原地修改)特征。

阶段四:大师级代码重构(课后复盘)

最后,小迪老师教大家将这段丑陋的反编译代码,还原成赏心悦目的源码形态:

  1. sub_4038BE 重命名为 Check_Debugger
  2. sub_404796 重命名为 Verify_Fake_Password
  3. sub_401FFF 重命名为 Decrypt_Real_Password
  4. *Str == 65 处按 R 键,将数字变成字符 'A'
  5. 在异或循环中,将参数的 int 类型通过快捷键 Y 修复为 char *,消除那堆恶心的指针转换。

    file-20260718202405010

    file-20260718202804057

    file-20260718202747676

重构后的主逻辑将变得如同一本清晰的说明书,直接拉满我们的逆向成就感。

在后续的实战课程中,我们将不仅掌握静态算法分析,还会带着大家切入动态调试(比如打开 x64dbg 真刀真枪地把这个 IsDebuggerPresent 给爆破掉),是不是很激动呢?激动就抓紧联系小迪老师,学习逆向吧!!!

请登录后发表评论

    没有回复内容