
今天的题目也非常精妙,它不仅融合了上一关的 XOR 内存解密机制,还引入了逆向工程中极其重要的一个门槛:反调试(Anti-Debug)。
可以把这一关定义为“迈向游戏/软件安全与对抗的敲门砖”,进入贴近实战的场景
先补一句,所有的案例资源下载都在 www.52xuejishu.com,0基础学逆向,就找小迪老师
那么现在,跟上小迪老师的节奏,不要迷路,不要掉队哦:
阶段一:迎头痛击(认识反调试)
“同学们,当我们像往常一样重命名完 printf 和 scanf,准备一路平推的时候,程序在入口处给我们设下了一个路障:sub_4038BE()。
大家跟进这个函数到底层(sub_4088C0),会发现一个金光闪闪的 Windows API:IsDebuggerPresent()。

这是什么?这是所有安全对抗、反作弊防护的起点!这个 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键转换)

也就是说,只要我们输入一个长度刚好为 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 *
瞬间变得清清爽爽

推演计算密码:
“又是我们最喜欢的单字节异或(XOR)!这次的密钥变成了 0x5A。我们把内存里的乱码 )3=7; 逐个拿出来跟 0x5A 碰一碰,看看会变出什么魔法。”
- 字符
)的 ASCII 是十六进制0x29。0x29 ^ 0x5A = 0x73(字母 s) - 字符
3的 ASCII 是十六进制0x33。0x33 ^ 0x5A = 0x69(字母 i) - 字符
=的 ASCII 是十六进制0x3D。0x3D ^ 0x5A = 0x67(字母 g) - 字符
7的 ASCII 是十六进制0x37。0x37 ^ 0x5A = 0x6D(字母 m) - 字符
;的 ASCII 是十六进制0x3B。0x3B ^ 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;
}
接下来为大家详细拆解这个过程的底层原理:
- 传参阶段:传的是“地址”而不是“内容”
在main_0函数中,String2被定义为一个局部数组:CHAR String2[8]; // [ebp-8h]当代码调用Decrypt_Real_Password(String2);时,C 语言并不会把")3=7;"这串字符复制一份传给函数。相反,数组名String2会自动退化为一个指针,传递的是这个数组在栈上的起始内存地址(即[ebp-8h]的绝对地址)。 - 执行阶段:指针的“原地手术”
我们进入底层的sub_4088E0(int a1)函数。这里的参数a1接收到的,正是String2的首地址。
看这行没有被美化修复的核心加密/解密代码:*(_BYTE *)(i + a1) ^= 0x5Au;a1:是原始数组String2的首地址。i + a1:是指向当前要处理的具体某一个字符的内存地址。*(解引用):顺着这个内存地址,直接摸到了main_0函数栈空间里的那个物理字节。^=(异或并赋值):将取出来的字节与0x5A异或后,直接写回原地址。 - 结果验证
正因为这种操作是“原地”覆写的,当Decrypt_Real_Password(String2)函数执行完毕并返回到main_0时,原始String2所在的内存空间里的数据已经被改写了。原来的")3=7;"已经变成了明文"sigma"。
所以,紧接着执行的lstrcmpA(String1, String2),实际上是在比较用户的输入(String1)和解密后的明文(变成"sigma"的String2)。
核心总结
在 C/C++ 逆向分析中,只要看到把一个数组的首地址(或者指针)作为参数传进一个函数,并且函数内部对该指针进行了解引用赋值(如 *ptr = ... 或 ptr[i] = ...),就意味着原数据会被直接修改。 这是一个非常典型的 In-place Modification(原地修改)特征。
阶段四:大师级代码重构(课后复盘)
最后,小迪老师教大家将这段丑陋的反编译代码,还原成赏心悦目的源码形态:
- 把
sub_4038BE重命名为Check_Debugger。 - 把
sub_404796重命名为Verify_Fake_Password。 - 把
sub_401FFF重命名为Decrypt_Real_Password。 - 在
*Str == 65处按R键,将数字变成字符'A'。 - 在异或循环中,将参数的
int类型通过快捷键Y修复为char *,消除那堆恶心的指针转换。



重构后的主逻辑将变得如同一本清晰的说明书,直接拉满我们的逆向成就感。
在后续的实战课程中,我们将不仅掌握静态算法分析,还会带着大家切入动态调试(比如打开 x64dbg 真刀真枪地把这个 IsDebuggerPresent 给爆破掉),是不是很激动呢?激动就抓紧联系小迪老师,学习逆向吧!!!



没有回复内容