简明、现代而优雅的破解技术笔记 第一课
(论坛的自动超链接机制会让我的标题出bug,这个问题修复之前我的 Markdown 标题中不能出现“破解技术”四个字)
需要提前准备安装的工具环境
正向开发:开始设计密码判断程序
即使你是一个连C++也没有接触过的新手,也不要害怕,下面我会演示如何用 Visual Studio 默认创建的 Hello World 代码修改出一个判断密码的小程序。
在 Visual Studio 中创建工程
- 打开 Visual Studio
- 选择
创建新项目
- 语言选择
C++
,平台选择 Windows
。
- 在列表中找到
控制台应用
,并点击下一步。
- 把项目位置改成你希望用来盛放你的作业的位置,并且给项目起个名字,比如
我的第一个密码判断程序
。
- 勾选
将解决方案和项目放在同一目录中
,本例我们不需要复杂的解决方案与项目管理功能,勾选这一项可以减少文件夹层级,看起来顺眼一些。
- 点击
创建
。
我们可以看到,Visual Studio 自动为我们配置好了工程,并生成了一个源代码文件,文件的内容是一个 Hello World 程序。
修改 Visual Studio 生成的初始程序代码
在上方的 Debug x86
中把 x86
换成 x64
,并点击右侧带有绿色小三角的 本地Windows调试器
,就可以看到 Hello World 程序的运行结果。
这就是这个程序代码的作用。将这个调试窗口关掉,我们继续查看程序代码本身。
如果你接触过一些基础的C++课程,或者至少知道如何编写Hello World,你会觉得很熟悉,这里面除去大括号之外的三行代码,第一行是引入 iostream
这个C++模块,第二行是定义了一个 main
函数,而第三行则是调用 iostream
模块中在 std
命名空间中定义的流对象 cout
,并通过 <<
运算符来向 cout
中输出一个字符串 "Hello World!\n"
。
如果你没有接触过C++,完全不知道这三行代码是什么意思,也不要着急,上期所述的C++标准委员会的官方文档中对这些东西有详细解释,搜索关键字即可。
在C++标准委员会的文档中,我们可以搜索到 cout
和 cin
两个流的具体用法和示例代码,请点击链接:cin cout
根据以上查到的用法和示例代码,我们可以发现,如果我们想把用户输入读入一个变量,那么就可以 cin >> 这个变量;
,如果想要输出一个东西,我们就可以 cout << 这个东西;
。
在C++中,想使用的变量需要预先定义,而且需要声明该变量的数据类型。变量声明的语法是 变量类型 变量名;
。
于是,我们现在可以读入用户输入了,并且把它输出出来试一下。代码如下:
#include <iostream>
int main()
{
std::string 用户输入的内容;
std::cin >> 用户输入的内容;
std::cout << 用户输入的内容;
}
再次点击运行,你会发现你制作了一款复读机。
知道了如何输入和输出,我们现在只需判断一下用户的输入与真码是不是一致就可以了。另外,为了让程序独立运行时最后能够停住看结果,需要在最后加一个等待用户输入的命令。所以最后的代码变成了这样:
#include <iostream>
int main()
{
std::string 用户输入的内容;
std::string 真码 = "52Pojie-AngeloTheCat";
std::cin >> 用户输入的内容;
if (用户输入的内容 == 真码)
std::cout << "密码正确!\n";
else
std::cout << "密码错误!\n";
system("pause");
}
再次点击运行,至此,一个小小的密码验证程序就完成了。
这个密码验证程序似乎对中文的控制台用户输入有bug,也可能是我的系统开了强制Unicode内码的缘故,暂时没有解决。可能使用区分宽窄字符的C++标准库,并且系统窄字符默认编码是UTF-8时就会出现这个问题,如果想要bug少一些,处理输入的时候请尽量使用 conio.h
或Windows API。
逆向分析:使用IDA查看编译好的密码判断程序
IDA的安装与配置
站长发的 IDA 7.5 是绿色版的,只需要绿化即可。
为了能够让IDA搜索中文字符串,需要在绿化后桌面上出现的 IDA 快捷方式中添加参数 -dCULTURE=all
,如图:
开始我们的第一次逆向分析
将我们编译好的程序拖入64位 IDA 的快捷方式,将会打开 IDA ,初次打开 IDA 时,会让你同意一个用户协议,同意即可。一般来讲,将程序拖入 IDA 后,将会弹出选择架构与 ABI 的界面,默认为 AMD64 架构和 Windows ABI ,这是正确的,所以这里点击OK。
之后 IDA 会弹窗告诉你,你要分析的这个exe文件存在一个调试符号表(PDB)文件,并询问你是否需要加载这个文件。这里我们选择“是”,看看得出来的结果是什么。
由于我们的程序非常小,IDA 很快就会加载完成。IDA 加载完成后会自动跳转到 main 函数的位置,这时显示的是 IDA 的流程框图,可以看到,好像已经出现了什么不得了的东西……
我们按下 F5
,这是 IDA 的反编译快捷键,将会反编译当前选择的函数。
简直是不讲武德!我们刚才在main函数中所编写的逻辑,几乎一点不差地还原了出来。可以大致看到,if语句成立的条件是 v7==v8
,而 v7 是调用了 cin
获取的用户输入,v8 则是密码的明文。
对于有C++基础的人,可以看到,从反编译后的第16行代码开始,和我们刚刚写的代码完全一致。先构造两个 string
对象 v7
和 v8
,并且将v8初始化为真码的内容。再调用 cin
的 >>
运算符并且右操作数是 v7
这个 string
。然后调用 string
类的 ==
运算符判断两个string,v7和v8是否相等,如果相等就调用 cout
对象的 <<
运算符,输出密码正确的提示,否则就还是调用 cout
对象的 <<
运算符,输出密码错误的提示。
而反编译的9-15行代码,则是 VC 的 Debug 版生成出来的程序自带的 Debug 环境初始化代码。
至此,由于我们写了一个实在太简单的程序,并且给IDA提供了符号表,所以我们轻而易举地就看到了这个程序的全部代码。我们现在来尝试修改这个程序的代码,将if的条件反向,密码正确时输出密码错误,密码错误时输出密码正确。
对第一个程序的修改(Hacking)
返回 IDA View 视图(框图),可以看到主函数的执行逻辑,左边是密码正确的逻辑,右边是密码错误的逻辑。区分走哪边是靠上面框中最后一条指令 jz
来实现的。
我们将光标定位在 jz
处,点击 Edit -> Patch program -> Assemble。
在弹出来的框中将 jz
改为 jnz
。
点击 OK 后 IDA 会自动给你下一个地址处的汇编让你改,此处我们不需要继续再改了,应该停止修改,按 Cancel。
jz
是一条汇编指令,它一般和 test
一同出现。在上一条 test
中,如果用于比较的两个寄存器与起来的值是0(也就是说两个寄存器都是0),那么 jz
就会跳转。jz
的意思是 jump zero,为零时跳转,jnz
也是一条汇编指令,意思是不为零时就跳转。jz
和 jnz
也并不是一定要和 test
一同出现,他们判断为不为0的其实是CPU的状态寄存器。test
是一种会修改状态寄存器的指令。
再次按下 F5 ,看看生成的反编译代码 B 和之前的反编译代码 A 有什么区别。
点击 Edit -> Patch program -> Apply patches to input file。勾选 Create Backup
是个好习惯。
点击OK。运行一下你刚保存的程序,看看和你编写出来的原程序有什么区别。
恭喜你完成了人生中第一次破解自己写的程序!
在下一期内容里,我将带领大家对这个小程序做一些改进,至少不会让IDA一载入就在第一屏看见所有逻辑。