最近好迷茫,所以又斥巨资买了一本充满力量的书,那就是看雪的《加密与解密》,也来彻底地玩一玩逆向吧。

Windows操作系统

曾经的我,认为 Linux 天下第一好用,(虽然现在我也那么认为)但是 windows 作为受众很广的操作系统,很多 windows 的程序也是有必要去学习一下的。

win32API

当今大部分 windows 程序都是 GUIGUI 是通过一系列 API 来完成的,具体帮助手册自行下载。

我们只需要知道一部分的,并且用到什么就学什么好了,比如常见的从控件中获取输入值,用接口 GetDlgItemTextA,弹窗反馈 MessageBoxA,当然这是比较常见的,具体调试的时候还得具体情况具体分析。

windows 程序运行比较依赖动态链接库(dll),可以理解为 Linux 下的 .so 文件。windows比较内核的三个动态链接库为

  • kernel(kernel32.dll):提供操作系统的核心服务。
  • user(user32.dll):提供用户输入和输出的接口。
  • GDI(GDI32.dll):提供图形设备接口。

这里需要注意一点,就是很多接口的后缀 A 或者 W 表示它处理字符的一个字符集, A 表示 ASCII 码,w 表示 unicode 码。

动态调试器

这本书介绍了很多的动态调试器,这里我还是比较喜欢 x32dbg,所以其它的我也不一一解读了,但是相同的特性也能拿来讲一讲记一记。

直接调试

打开调试器,在文件 -> 打开 中选择自己要调试的文件,然后载入。

附加调试

打开调试器,在文件 -> 附加 中选择自己要附加调试的进程,然后载入。


感觉附加调试会好一点吧,毕竟能先让程序运行到自己想调试的点然后再去调试,这样省了前面入口点的一些操作,但是我们要调试关键信息,一定是要下断点(break point)的。

这里我们直接用 chap02\OllyDbg调试器\2.1.4 基本操作\bin\ASCII版\TraceMe.exe 来试试手。

调试器整体呈这样,最上面一行肯定是标签栏,工具栏,选项卡。剩余窗口大致分为四块。CPU 选项卡中的窗口表现出来就是代码窗口,最右边的窗口时寄存器窗口,左下角的窗口是内存窗口,右下角的窗口是栈窗口,最下面一行的文本框可以用于打命令使用。

CPU窗口

大概分了五列,如下图所示。

  • 对于第一列,更多的是标识跳转的作用,以及设置断点。
  • 第二列标识了当前内存地址的地址,双击可以显示改行的相对地址偏移,再次双击恢复。
  • 第三列标识了该地址的内存字节,双击可以下断点。
  • 第四列标识了该字节码的反汇编代码,选中使用 空格 键可以修改汇编代码。
  • 第五列提供了一些内存地址或者是寄存器的值,也可以用 ; 键去添加注释。

断点

断点大致分为以下几种类型

  • INT 3 断点
  • 硬件断点
  • 内存断点
  • 消息断点
  • 条件断点

INT 3断点

INT 3 是一条汇编指令,其机器码是 0xCC 所以也叫 CC 指令。执行这个指令的时候,会抛出一个 break point exception 异常,这个异常会被调试器捕获到,因此能达到断点的目的。在打断点的时候,会把这个地址设置为 0xCC ,也就是 INT 3 的机器码。优点是可以设置很多个断点,因为我们只要想,可以在任意地址把值改成 0xCC 以此达到断点的目的。但是带来的缺点就是会修改程序的内存,改变了原机器码,可能会被程序检测到。

比如这样的一个 MFC 程序,附件下载

source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void CTrackMeDlg::OnBnClickedButton1()
{
// TODO: 在此添加控件通知处理程序代码
HWND hWnd = AfxGetMainWnd()->m_hWnd;
QWORD Uaddr;
BYTE Mark = 0;
Uaddr = (QWORD)MessageBoxA;
Mark = *(BYTE*)Uaddr;
if (Mark == 0xCC) {
MessageBoxA(hWnd, "be tracked", "MessageBox", 0);
return;
}
MessageBoxA(hWnd,"Very OK","MessageBox",0);
}

正常运行结果如图所示

如果放到调试器,对 MessageBoxA 这个函数下断点的话,那么就会导致出现另一个不同的运行结果。

因为我们断点设置在 MessageBoxA 上,而程序判断了 MessageBoxA 调用地址是否为 INT 3 指令(0xCC),有的话就直接输出 be tracked 说明检测到这里下了 INT 3 断点。

如果我们要绕过检测同时又要求能断下来,那么我们可以在函数调用中间或者是末尾下 INT 3 断点。

硬件断点

硬件断点主要是通过 DRx 寄存器实现的,DR0~DR3 分别用于保存硬件断点的地址,那我们也可以看出来它最多能同时存在四个硬件断点。DR4-DR5 未公开具体作用, DR6 用于保存寄存器组状态, DR7 用于保存寄存器组控制。硬件断点不会改变程序字节码,因此它更难被检测,在断点选项中可以设置硬件执行断点,同样,对于一般内存来说,我们可以设置硬件访问断点。

设置完成之后我们在寄存器窗口拉到最下面,可以看到 DR 寄存器,我们可以从前四个寄存器中找到我们下的硬件断点的位置。硬件断点的优势劣势与前面的 INT 3 断点相对,硬件断点不能同时大量设置,但是它不会更改进程代码段的内存。

内存断点

内存(读/写/执行)断点是通过将某一块内存页标记为对应的不可(读/写/执行),当程序尝试对该内存(读/写/执行)的时候就会抛出异常,转而给调试器进行异常处理,若发现访问内存刚好是断点位置时,程序断住,否则正常进行(读/写/执行)操作。由于在执行的时候,会有一个读取命令的操作,因此我们如果在代码断设置了内存访问断点,执行到指定位置时同样会被断住。

比如这个程序,我们在某一条指令上下内存读取断点

我们再次按 F9 继续执行可以发现程序停在了我们下的断点位置。

不知为何在经过一番激烈的讨论之后,认定 x32dbg 应该是这里的技术细节没有实现,所以导致它只能在一个内存页设置,不能保证在指定位置断住,因此在一个内存页中可能多次被这个点断住,因为它可能没有比较访存位置与内存断点位置。

内存断点分持久的和一次性断点,一次性断点在断住之后即被删除。

消息断点

条件断点

我们如果希望在一个地方满足一定条件才断下来,这个时候我们可以 shift+F2 设置条件断点,比如我想在一个 10000 次的循环当中,看第 5000 次的执行结果,那么我们正常操作就是给循环体一个断点,然后 F9 5000 次,显然这么做会很麻烦,那么我们可以设置一个条件,让它在指定条件才断住,加入循环变量存储在 rcx 当中,那么我们可以设置 rcx==5000

source:

1
2
3
4
5
6
7
8
9
10
#include<bits/stdc++.h>

int main(){
int sum=0;
system("pause");
for(int i=1;i<=10000;i++){
sum+=i;
printf("%d\n",sum);
}
}

我们可以很清晰看到这里的循环结构,然后我们看到循环变量存在 ebx 寄存器中,我们在循环体中下一个条件断点,观察程序的运行。

然后发现程序成功在我们指定的条件下面断住了。

静态调试

待更