逆向分析基础

现在就要跟随 Eastmount 老师学习系统安全与恶意代码分析了,是的,终于要真正的系统地学习了~加油小凉!

1. 逆向分析学习路线

2. 逆向分析的典型应用

2.1 病毒分析

逆向分析主要是剖析病毒,包括:

  • 获取病毒传播方法,遏制病毒传播
  • 获取病毒隐藏手段,根除病毒
  • 获取功能目的,溯源定位攻击者

2.2 游戏保护

这个主要是用来做游戏外挂。比如修改攻击力、防御值、金币等。

2.3 漏洞挖掘

逆向应用还包括漏洞挖掘和漏洞利用,其中黑客挖掘漏洞的常用方法为:

  • 通过分析开源软件的源代码,获取漏洞
  • 通过分析产品本身,获取漏洞
  • 通过分析可以利用漏洞的软件样本
  • 通过比较软件前后补丁的差异

比如官方软件在网上有安全更新,关注安全行情和漏洞公告的行当或企业会对比官方的补丁,在拿到官方升级后的软件,他们会对两个软件流程作比较,分析补丁补了哪里,再详细分析为什么多了这个检测。注意,官方公告通常会非常简略(补丁号、造成后果、影响范围)。比如某个MP3播放器在播放某个冷门格式的音频文件时,会触发一个远程溢出问题,我们就需要去逆向分析,下载升级前后版本做流程对比。

2.4 电子取证

通过样本试图找出是谁(Who)、在什么时间(When)、在哪里(Where)、怎样地(How)进行了什么(What)(非法)活动。

2.5 无文档学习

表示没有源码的情况下获取程序信息,称为竞品分析。假设某个公司对同行的产品很感兴趣,想知道为什么他们的算法比我们好,然后需要去分析和算法还原,这也是逆向分析的主要应用。

3. 扫雷游戏逆向分析

扫雷中肯定有雷区的定义,作为程序员,你会怎样定义有雷或无雷,或者插旗子状态呢?我们会使用一个二维数组来存储。那么,什么时候肯定会访问这个二维数组呢?在绘制整个游戏区、点击方格的时候都会访问到。

在绘制游戏区时,Windows编程有个关键函数BeginPaint(),它为指定窗口进行绘图工作的准备,并用将和绘图有关的信息填充到一个PAINTSTRUCT结构中,所以它将是一个突破口。

在逆向分析中,动态分析和静态分析非常多,动静结合也是常用的分析手段。

  • 静态分析:程序并未运行,通过分析文件的结构(格式)获取其内部原理。
  • 动态分析:在程序的运行过程中,分析其内部原理。
  • 灰盒分析:既不静态也不动态调试,通过一堆监控软件(注册表监控、文件监控、进程监控、敏感API监控)在虚拟机中跑程序,再分析恶意软件的大体行为,并形成病毒分析报告。

至于哪种方法更好?具体问题具体分析。如果分析扫雷,因为没有危害可以动态调试,但如果是WannaCry蠕虫,就不能在真机上动态调试。同时,很多安全公司为了及时响应各种安全事件,会把样本自动上传到服务器中,他们每天会收到成千上万的恶意样本,但可能存在某些未知样本只上传部分的原因,比如某个未知样本是个动态链接库,此时没有运行条件,只能进行静态分析或者模拟接口分析。

3.1 OD动态分析

我们采用动态分析的方法分析扫雷程序。之前我们猜测游戏中存在一个二维数组,当我们显示界面时会访问这个二维数组,并且调用BeginPaint()函数来显示页面,所以接下来需要找到调用BeginPaint()的位置。

将程序载入OD,Ctrl + N 查找当前模块中的名称,输入BeginPaint,右键 -> 在每个参考上设置断点。

Alt + B 去到断点窗口,发现只有一个。F9 运行程序至断点处,此时程序界面还没出来。

发现BeginPaint()函数下面还有一个EndPaint(),表示绘图结束,也就是游戏结束。所以这两个函数之间的数据就是我们玩游戏的过程。两个系统函数之间只有一个程序函数01002AC3(),选中该行 Enter 跟随该函数。

发现这里面也有几个程序函数,一个个看后发现只有010026A7()里有双重循环,也就是构成二维数组的基本条件。

当然,这种方法太过草率也太耗费时间了,如果遇到大一点的程序,工作量还是挺大的。可以使用另一种方法。当我们在玩扫雷时,它的界面并没有闪烁,所以怀疑使用了双缓存技术。

双缓存是在缓存中一次性绘制,再把绘制的结果返回到界面上。比如要在屏幕上绘制一个圆、正方形、直线,需要调用GDI的显示函数,操作显卡画一个圆,再画一个正方形、一条直线,需要访问硬件3次,此时依赖硬件的访问速度。为了减少硬件操作,我们在内存中把需要绘制的图像准备好,一切妥当后再提交给硬件显示。

BitBlt()函数是将内存中的数据提交到显示器上,该函数对指定的源设备环境区域中的像素进行位块转换,以传送到目标设备环境。同样方法查找BitBlt()函数,设置断点,运行,程序停在了010026A7()函数里的BitBlt()函数中。需要注意的是,调用BitBlt()函数有两处地方,为了验证这里是否是我们要找的地方,可以单步调试看看游戏界面情况。

绘制一个个方块的过程,也就是初始化“有雷”和“无雷”的过程,说明我们之前找的地方没错。

另一处调用BitBlt()函数,是点击方块时,绘制该方块是“数字”还是“雷”的过程。这时候只是将这个过程显示在用户界面上,对我们来说只是一个验证作用。

010026A7()函数里的BitBlt()函数在界面初始化“有雷”和“无雷”,那肯定将这些数据存在了某个地方。接下来就是分析这双重循环。

经过mov al,byte ptr ds:[ebx+esi]可以知道al的值是取数据段寄存器中以ebx为基址,esi为偏移的地址的内容。

所以ebx存的就是“有雷”和“无雷”二维数组的首地址。我们知道,一行有9个方块,根据规律可以猜测,10作为边界,0F表示空,8F就是雷。

将所有断点取消,数据窗口 Ctrl + G 定位到地址01005360,验证猜测。

注意,如果第一次点击的就是雷的话,会改变雷的位置(可能是避免倒霉孩子没有游戏体验吧)。如果方块中是旗子显示8E,方块中是空白显示40,方块中是1则显示41,2是42,以此类推。雷被点中后将8F改为CC,将剩余的雷改为8A。经过多次游戏,证实了上面的猜测。

3.2 逆向辅助工具CE

Cheat Engine又称CE修改器,是一款内存修改编辑工具。可以通过Cheat Engine来修改游戏中的内存数据、人物属性、金币数值等等。

我们现在的目的是利用CE获取第一个方块的地址,验证与在OD找的是否一致。运行扫雷,打开CE,附加扫雷进程。在OD中看,一个字节存储在一个方块中,所以将数值类型设为“字节”,扫描类型设为“未知的初始数值”,首次扫描。

此时显示1056768个数据。接着点击第一个方块,该方块由0F变为40,所以在扫描类型中选择“变动的数值”,再次扫描。

点击扫雷,由于第一个方块不再变化数值,所以选择“未变动的数值”进行筛选,再次扫描,连续几次,发现数据的个数一直在变小,说明经过几轮筛选逐渐缩小范围。如果出现地雷则选择“未变动的数值”,再次扫描。

点击笑脸重新开始游戏,此时第一个方块从40变为0F,所以扫描类型修改为“变动的数值”,再次扫描。

重复上述步骤,直到结果为1。

这个地址刚好是我们在OD中找的第一个方块的地址。

第二步验证扫雷的边界。自定义扫雷的高度为9,扫出来有1627个数据。再次定义高度为16,从9变到16的数据有4个。再次定义高度为24,从16变到24的有2个。因为边界需要两个值来定义,所以就是01005338010056A8

同样筛选出存储宽度的地址,分别是01005334010056AC。筛选出雷数的存储地址为01005330

后面就可以利用这些地址开始学习研究了,比如一秒实现扫雷等。

4. 吕布传游戏逆向分析

关于NPC说话太慢,找到快速跳过对话的方法。

Ekd5.exe载入OD,查找当前模块中的名称,查看调用了哪些函数。发现程序竟然有几个钩子函数。

钩子函数是Windows消息处理机制的一部分,通过设置“钩子”,应用程序可以在系统级对所有消息、事件进行过滤,访问在正常情况下无法访问的消息。钩子的本质是一段用以处理系统消息的程序,通过系统调用,把它挂入系统。

  • SetWindowsHookEx:设置钩子函数
  • CallNextHookEx:将钩子信息传递到当前钩子链中的下一个子程,一个钩子程序可以调用这个函数之前或之后处理钩子信息
  • UnhookWindowsHookEx:上一个函数SetWindowsHookEx()的返回值,钩子在使用完之后需要用该函数卸载

在每个SetWindowsHookEx()处下断,一共两处。运行,停在了00429EF7处。可以看到该钩子函数是通过键盘输入触发,回调函数的地址为0040D307,也就是触发后会去到该地址处。

Ctrl + G 去到该地址处,下断,运行。游戏载入,随意从键盘上输入。

此时触发钩子函数,使汇编去到0040D307处。我输入的是“a”,运行到cmp指令时,eax存的值就是“a”的ASCII码的十六进制形式,与0x20(空格)进行对比,往下看还有与0x30(“0”)、0x35(“5”)对比的。

先看0x20。重载运行,在键盘按下空格键。对比通过,进入00406A33函数,这个函数里有一个创建线程函数,线程在00406A7F处。继续跟随到该地址,下断运行。

00406A7F函数中,有两个PostMessage()函数,该函数的作用是将一条消息放入消息队列中。一个是“鼠标按下”,另一个是“鼠标弹起”,中间还有个sleep()函数,这个过程是模拟玩家点击鼠标的操作。

那么,我们就找到了一个快速跳过对话的方法,就是按空格键。要想取消快速对话,同样也是按空格键。ds:[0x500E02]中存储着跳过与否的值,“1”表示快速跳过,“0”表示不跳过。

0x30(“0”)、0x35(“5”)没什么用的,可能只是过滤玩家的不合法输入。

5. 植物大战僵尸游戏逆向分析

5.1 CE逆向修改阳光值

修改阳光值首先要知道存储阳光值的地址在哪里。通过CE找到该地址,为25B42938

打开资源管理器,查看这个游戏的进程ID,为12064。

在修改阳光值之前,要先确定我们要修改值的窗口是哪一个,可以通过API函数FindWindow()来查找。这个函数检索处理顶级窗口的类名和窗口名称匹配指定的字符串,这个函数不搜索子窗口。

1
2
3
4
HWND FindWindow(
LPCSTR lpClassName,
LPCSTR lpWindowName
);
  • lpClassName:指向一个以NULL字符结尾的、用来指定类名的字符串或一个可以确定类名字符串的原子。如果该参数为null时,将会寻找任何与lpWindowName参数匹配的窗口。
  • lpWindowName:指向一个以NULL字符结尾的、用来指定窗口名(即窗口标题)的字符串。如果此参数为null,则匹配所有窗口名。

FindWindow()需要传入两个参数,即窗口的类型和窗口的标题。这里可以用到Visual Studio中的Spy++工具来查看在本机中运行的窗口的相关信息。

句柄为00600BEE,标题为Plants vs. Zombies 1.2.0.1073 RELEASE,类为MainWindow

当然,每次运行的句柄和进程ID都不一样,千万不要把这两个值写死,而是通过API函数自动获取这些信息。

接下来介绍几个等下要用到的API函数:

  • 通过GetWindowThreadProcessld()函数找到进程ID。
1
2
3
4
DWORD GetWindowThreadProcessld(
HWND hwnd, //窗口句柄
LPDWORD lpdwProcessld //接收进程标识的32位值的地址
);
  • 通过OpenProcess()函数打开一个已存在的进程对象,并返回进程的句柄。
1
2
3
4
5
HANDLE OpenProcess(
DWORD dwDesiredAccess, //渴望得到的访问权限(标志)
BOOL bInheritHandle, //是否继承句柄
DWORD dwProcessId //进程标示符
);

dwDesiredAccess可分为以下几种:

字段值 含义
PROCESS_ALL_ACCESS 获取所有权限
PROCESS_CREATE_PROCESS 创建进程
PROCESS_CREATE_THREAD 创建线程
PROCESS_DUP_HANDLE 使用DuplicateHandle()函数复制一个新句柄
PROCESS_QUERY_INFORMATION 获取进程的令牌、退出码和优先级等信息
PROCESS_QUERY_LIMITED_INFORMATION 获取进程特定的某个信息
PROCESS_SET_INFORMATION 设置进程的某种信息
PROCESS_SET_QUOTA 使用SetProcessWorkingSetSize()函数设置内存限制
PROCESS_SUSPEND_RESUME 暂停或者恢复一个进程
PROCESS_TERMINATE 使用Terminate()函数终止进程
PROCESS_VM_OPERATION 在进程的地址空间执行操作
PROCESS_VM_READ 使用ReadProcessMemory()函数在进程中读取内存
PROCESS_VM_WRITE 使用WriteProcessMemory()函数在进程中写入内存
SYNCHRONIZE 使用wait()函数等待进程终止
  • 通过WriteProcessMemory()函数写入某一进程的内存区域。注意,直接写入会出现“Access Violation”错误,故需此函数入口区必须可以访问,否则操作失败。
1
2
3
4
5
6
7
BOOL WriteProcessMemory(
HANDLE hProcess, //由OpenProcess返回的进程句柄
LPVOID lpBaseAddress, //要写入的内存首地址
LPVOID lpBuffer, //指向数据当前存放的地址
DWORD nSize, //数据的长度
LPDWORD lpNumberOfBytesWritten
);

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<windows.h>

int main()
{
//输入值作为修改阳光参数
int x;
scanf("%d", &x);

//进程ID
DWORD pid;

//1.找到游戏窗口 窗口类型、窗口标题
HWND hwnd = FindWindow(NULL, L"Plants vs. Zombies 1.2.0.1073 RELEASE");

//2.通过窗口找到进程ID
GetWindowThreadProcessId(hwnd, &pid);

//3.通过进程id打开进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);

//4.通过打开进程修改游戏内容
WriteProcessMemory(hProcess, (LPVOID)0x25B42938, (LPVOID)&x, sizeof(x), &pid);

return 0;
}

芜湖实现阳光自由了~

经多次实验发现,每次存储阳光值的地址都不同,所以每次都需要用CE找到其地址再进行修改。

注意,如果游戏存在地址保护的情况,可以尝试注入进行修改。(我还没到那种水平,遇到再说)

5.2 OD逆向自动拾取阳光

拾取阳光的关键是点击鼠标,点击到阳光,阳光值会增加。所以我们希望在阳光出现的时候触发点击阳光事件,初步预测涉及两个call:

  • 阳光出现call
  • 判断是否点击到阳光然后增加阳光值call

使用CE定位阳光值地址,选中该地址右键 -> 找出是什么改写了这个地址。当再次拾取阳光时,阳光值从75变到了100,同时CE的小窗口出现了一条记录。

选中这条记录,下面会出现相关的汇编指令和当前寄存器的值。eax寄存器存的就是每次拾取阳光增加的数值25。

CE的工作到这里就结束了,接下来将游戏载入OD,定位到0043A7F5处,下断运行。当鼠标点击拾取阳光后,程序停在断点处。

在查看上面的一连串跳转指令中,发现有个jnz跳过了“增加阳光值”的操作。但给它下断运行,捡了几次阳光,都没有经过这个跳转指令,所以暂时先不管它。

往上拉拉发现0043A7F5所在的函数的功能仅仅是改变数据段中的阳光值。Ctrl + F9 执行到返回,F7 去到它的上一层函数。发现这个jnz指令有可能会绕过增加阳光值call,给它下个断点,运行几次。

发现阳光每往左上方移一段路程就要经过这个jnz指令,直到阳光到达指定位置才进入增加阳光值的call。

那这个jnz指令可以不管它,把它的断点取消。继续返回到父函数,看到有一个jnz指令可以跳到增加阳光call,下断运行。(那些call + jmp指令我们基本不会去动的,否则很容易导致程序运行出错)

此时阳光已经出现了,但jnz跳转没有实现,也就是还不能进入增加阳光call。

那怎样才能进入呢?对玩家来说,肯定是要用鼠标点击阳光才能增加阳光值。也就是触发鼠标点击阳光事件才能让jnz跳转指令实现。那我们要实现自动拾取功能,也就是鼠标不点击阳光也能使阳光值增加,怎么办?让这个jnz指令失去它的判断功能,改为无条件跳转指令jmp。

然后就会发现阳光一出现就被迅速移到指定位置,增加阳光值啦~

(注意,新手教程一定要点击一下阳光才能继续游戏)

6. Cs游戏逆向分析

逆向上面几个游戏时已经学会了一点关于CE的使用方法,接下来继续利用CE逆向Cs,了解更多的CE基础用法,完成CE的入门。

Cs 1.6下载地址:https://www.cybersports.lt/

进入游戏,取消全屏,设置视频为“Run in a window”,方便调试和动态分析。

将每局的时间设置得久一点,方便调试。

随机选择一幅地图开始游戏,此时子弹数为20。按下ESC键,回到桌面打开CE。

之后再怎么变化子弹的数值,CE扫描出的结果不再减少。Ctrl + A选中所有结果 -> 将选中的地址添加到地址列表。

结合二分法,按住Shift键选中一半地址(大概就行),右键更改记录 -> 数值,假如将数值修改为80,回到游戏开一枪看子弹数量是否有变化。如果有变化表明存放子弹数量的地址就在选中其中,否则不在。删除不包含存放子弹数量地址的部分,从包含的部分继续进行二分法,以此类推,直到确定子弹数量地址。该地址为01191CAC。

当我们按Q键切换武器为匕首或其它枪时,子弹数不变,所以这个地址存储的内容应该特指手枪的子弹数。选中该条记录,在“描述”中双击修改为“手枪子弹数”进行备注。我们在游戏中发射子弹,在CE中的数值也有相应的变化。单击“描述”前面的方框,变成“×”,表示将该内存地址的数值固定,也就是射击后子弹数量不再变化,实现手枪无限子弹的目的。

经过进一步的试验后发现地图不同,子弹数存放的地址也不同;枪的类型不同,对应枪的子弹数的地址也不同。为什么呢?

要想解决这个问题,先看结果中有两种颜色的地址,这是什么含义呢?

  • 绿色是基址,只要程序启动,这些地址就归游戏使用;

  • 黑色是临时申请使用的。

基址:不会改变,用于存放血量、金钱等。当它不够或需要存放更多数据时,它会跟系统申请地址,这个地址是系统随机分配的。所以,变换地图后显示的子弹数地址也会发生变化,我们需要找到其变化规律(偏移地址)即可。

我们刚才找的“手枪子弹数”地址都是临时申请的地址,因此需要找到一个绿色地址,也就是找到内存地址中存放子弹且不改变的地址。

由于我们最终找到的都是黑色地址,都是临时申请的,所以程序可能使用了指针指向了临时申请的地址,而绿色地址存放的可能是指针或多层指针。

按照这个思路,基址可能存放的是临时地址,即基址的值是临时地址。所以我们用CE找到数值为临时地址的地址,就很有可能是基址。

复制该地图的“手枪子弹数”地址,点击“新的扫描” -> 勾选“十六进制” -> 粘贴地址 -> “首次扫描”。扫描结果为0,原因可能是该地址为偏移地址。比如基址从01234567开始,往后数第3个是存放子弹数量的地址,系统寻址时则表示为01234567+3,而不是直接表示0123456A。但在我们扫描数值时,给出来的地址是0123456A,不能逆推出01234567+3,所以扫描不出结果。

右键 -> 找出是什么改写了这个地址。

回到游戏界面开一枪后,可以看到CE出现一条记录:

  • 计数:调用次数
  • 指令:汇编代码
  • 同时给出该汇编指令的上下文及当前寄存器的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1BA109BF - 8B CE  - mov ecx,esi
1BA109C1 - 4D - dec ebp
1BA109C2 - 89 AE CC000000 - mov [esi+000000CC],ebp <<
1BA109C8 - D9 96 BC000000 - fst dword ptr [esi+000000BC]
1BA109CE - D9 9E B8000000 - fstp dword ptr [esi+000000B8]

EAX=00000009
EBX=00000000
ECX=01320EB0
EDX=050FD0E0
ESI=01320EB0
EDI=06A96750
ESP=0019F140
EBP=00000008
EIP=1BA109C8

根据当下情况来看,原本子弹数为9,该值存储在EAX中,开一枪后的子弹数为8,存储在EBP中。将EBP的值赋给[ESI + 000000CC],ESI=01320EB0,所以[ESI + 000000CC]=[01320EB0 + 000000CC]=[01320F7C]=8,刚好对应上我们找出来的地址和数值。

扫描十六进制数值01320EB0,找到存储该值的地址。

(由于在游戏中我“死”得太快了,所以每次都要重新找子弹数量的地址,导致图与文字描述的数值不符,问题不大,只要知道操作步骤就好)

有4条结果,但都不是绿色的,都不是我们最终要找的。这4个黑色地址说明它们是存放临时数据的临时地址,其结果有可能如下图所示。

将这4条结果添加到地址列表,依次扫描这4个地址看是否能找到绿色地址,结果4个地址扫描的结果都为0。

思路同上,有没有可能这4条结果也存在偏移呢?emmmm“死”得好快,不想玩了。