现在就要跟随 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个。因为边界需要两个值来定义,所以就是01005338
和010056A8
。
同样筛选出存储宽度的地址,分别是01005334
和010056AC
。筛选出雷数的存储地址为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 | HWND FindWindow( |
- 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 | DWORD GetWindowThreadProcessld( |
- 通过
OpenProcess()
函数打开一个已存在的进程对象,并返回进程的句柄。
1 | HANDLE OpenProcess( |
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 | BOOL WriteProcessMemory( |
完整代码如下:
1 |
|
芜湖实现阳光自由了~
经多次实验发现,每次存储阳光值的地址都不同,所以每次都需要用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 | 1BA109BF - 8B CE - mov ecx,esi |
根据当下情况来看,原本子弹数为9,该值存储在EAX中,开一枪后的子弹数为8,存储在EBP中。将EBP的值赋给[ESI + 000000CC],ESI=01320EB0,所以[ESI + 000000CC]=[01320EB0 + 000000CC]=[01320F7C]=8,刚好对应上我们找出来的地址和数值。
扫描十六进制数值01320EB0,找到存储该值的地址。
(由于在游戏中我“死”得太快了,所以每次都要重新找子弹数量的地址,导致图与文字描述的数值不符,问题不大,只要知道操作步骤就好)
有4条结果,但都不是绿色的,都不是我们最终要找的。这4个黑色地址说明它们是存放临时数据的临时地址,其结果有可能如下图所示。
将这4条结果添加到地址列表,依次扫描这4个地址看是否能找到绿色地址,结果4个地址扫描的结果都为0。
思路同上,有没有可能这4条结果也存在偏移呢?emmmm“死”得好快,不想玩了。