前言
上期教程更侧重于学习,并没有什么实际的功能效果,于是为了学以致用,今天给大家带来CE HOOK实现pvz子弹分身教程
适合学习人群:比萌新强两丝丝足矣
上期教程链接:https://www.52pojie.cn/thread-1361473-1-1.html
效果图
先给大家看看效果
事前准备
本人使用的工具:CE7.2汉化版(不用在意版本问题)
论坛有:https://down.52pojie.cn/Tools/Debuggers/Cheat%20Engine%20v7.2.exe
代码注入工具:https://www.52pojie.cn/thread-963707-1-1.html
PS:OD什么的不需要啦
本人使用的游戏:植物大战僵尸原版的汉化版(非年度版) 就是阳光基址是006A9EC0+768+5560的那个
附带一个下载地址:https://lanzoui.com/i9u7o3i
教程内容
思路分析
首先搞清楚我们想要实现的功能:让发射的一个子弹变成多个
很明显与子弹的产生有关,于是我们的目的转化为:找到产生子弹的CALL→HOOK这个CALL让它一次产生多个子弹
如何找到子弹产生的这个CALL?寻找相关数据来获得
我们先暂且不论子弹产生的内部细节,但有一点显而易见的便是子弹产生后一定会引起场上子弹数量的增加
function generateBullet()
.......
bulletNum++; --子弹数量增加
.......
return ?
end
于是我们便以这个场上的子弹数量为突破口开启找CALL之路
寻找子弹产生CALL
首先我们要找到这个记录场上子弹数量的地址
通过CE过滤子弹数量,当子弹数为1时搜1,为2时搜2.........最终可以得到记录当前场上子弹数量的地址
然后右键 找出是什么改写了这个地址
这里有2条记录,一条是子弹产生时引起数量增加,还有一条是命中僵尸后子弹消失引起的数量减少
这里的第一条便是我们子弹产生时让子弹数量增加对应的汇编指令
我们点击显示反汇编程序进去看看
PlantsVsZombies.exe+1DFC9 - 01 47 10 - add [edi+10],eax { 子弹增加 }
找到这一里以后要干什么?向上一层找
为什么要向上一层找?
我们前面已经分析过子弹数量的增加只是子弹产生里的一个小步骤,可能是如下结构
function bulletNumAdd() --子弹增加的函数
bulletNum++; --我们看到的add [edi+10],eax就对应这里
return ?
end
function generateBullet()
.......
bulletNumAdd() --调用子弹增加的函数
.......
return ?
end
这里我们想要调用产生子弹的CALL就要返回到调用CALL的同一层来查看具体的参数
于是我们可以在这里下个断点,让它断下
接着F8单步步过,在ret的这里停下,先不要返回
我们这是可以看一下右下角的堆栈窗口,PS:如果显示不同 可以在这个窗口这里 右键"堆栈跟踪"
堆栈窗口浅谈
首先我们都知道CALL XXXX以后是要返回的,要返回到哪里的相关数据都存在堆栈里,所以我们可以通过这个堆栈地址返回到上一层 上上一层 上上上层......,而这些返回地址的上一句就是CALL XXXX调用我们这个子弹数量增加的CALL
我们可以通过这里顺腾摸瓜 子弹数量增加语句→子弹数量增加CALL→子弹产生CALL (实际的执行顺序是反过来的)
如果我们不通过这个堆栈窗口,而是一直F8单步步过的到ret再返回实际结果也是如此
所以我们知道了 这个堆栈窗口里的某个CALL 一定就是产生子弹的CALL(我们可以先记录下这些CALL的位置 在相应CALL位置 右键设置书签)
如何确定是哪个CALL?
看参数,产生子弹至少需要哪两个参数?,答案呼之欲出:坐标,子弹的X坐标和Y坐标,于是我们的这个函数至少有2个参数
如何确定一个CALL的参数?
看CALL里面的返回值 ret xx
拿堆栈里的一个返回地址来举例,我们双击它来跳转到那里
PlantsVsZombies.exe+D62A - E8 31090100 - call PlantsVsZombies.exe+1DF60 { 子弹数量增加call }
如何确定这个call的参数呢?进到CALL里面去查看返回值
我们Ctrl+G或者右键转到地址
这里想跳转的地址填CALL XXXX 里的XXXX 我们这里就是填PlantsVsZombies.exe+1DF60
跳转后我们就已经进入到了CALL的内部,在下面还能看到我们之前的子弹数量增加语句(验证了堆栈窗口的作用,存储返回地址的相关数据,注意我这里说的是相关数据,不一定就直接是返回地址,但我们(系统)能够用相关数据算出返回地址,这涉及到段寄存器的相关知识,这里不做重点)
我们直接到函数的尾部查看返回值
PlantsVsZombies.exe+1E001 - C3 - ret
我们可以看到这里没有返回值 直接就ret了
那是不是说明这个函数不需要参数,直接就可以调用了?并不是
我们先说一下
ret xxx和参数的关系
在汇编中 一个子程序(call)有几个参数(push)就需要几个RETURN(堆栈平衡) 因为push压入的是四字节 所以有几个push 也就需要几个
ret push数量*4) 打个比方如果压入了5个参数 则ret 的返回数值为 ret 0x14 注意这里是十六进制 0x14=20=5×4
push 只是将参数传给call的手段之一,也可以通过mov 寄存器,xxx等给寄存器赋值的方法来传递参数
如果我们这里直接用代码注入器调用这个CALL,没有给它传递参数,那么游戏直接崩溃,PS:代码注入器是拿来给我们外部直接调用CALL的
我们可以看一下CALL前面的代码:
PlantsVsZombies.exe+D620 - 56 - push esi
PlantsVsZombies.exe+D621 - 57 - push edi
PlantsVsZombies.exe+D622 - 8B F8 - mov edi,eax
PlantsVsZombies.exe+D624 - 81 C7 C8000000 - add edi,000000C8 { 200 }
PlantsVsZombies.exe+D62A - E8 31090100 - call PlantsVsZombies.exe+1DF60 { 子弹数量增加call}
可以看到mov edi,eax 我们怀疑可以怀疑它将edi作为了参数,所以我们可以在call这里下个断点,然后将edi的值复制下来:
这里的EDI为15A8D050
我们修改一下代码注入器里的内容,然后重新测试
代码注入以后,我们发现游戏并没有崩溃,同时场上子弹数量增加了1,且屏幕左上角出现了一个子弹
这里主要是为了说明call的参数可能不只是push给的,可能还与寄存器的值有关,这个CALL我们没有找到与坐标相关的数据,于是看下一个CALL(之前堆栈窗口里的第二个)
用同样的方法,查看这个call的参数(跳转地址到CALL XXXX里的XXXX,然后到尾部看返回值)
PlantsVsZombies.exe+D653 - C2 1400 - ret 0014 { 20 }
从这里的ret 0014我们可以得到push了20/4=5个参数
于是我们在回到这个CALL这里 下个断点 看看它参数的内容
PlantsVsZombies.exe+672A5 - 50 - push eax {子弹类型}
PlantsVsZombies.exe+672A6 - 8B 45 04 - mov eax,[ebp+04] {植物基址}
PlantsVsZombies.exe+672A9 - 53 - push ebx {行数}
PlantsVsZombies.exe+672AA - 83 E9 01 - sub ecx,01 { 1 }
PlantsVsZombies.exe+672AD - 51 - push ecx {未知,貌似不影响结果,可以直接填0}
PlantsVsZombies.exe+672AE - 56 - push esi { y坐标 }
PlantsVsZombies.exe+672AF - 57 - push edi { x坐标 }
PlantsVsZombies.exe+672B0 - E8 6B63FAFF - call PlantsVsZombies.exe+D620 { 子弹产生call }
我们通过分析数值 可以得出ESI和EDI分别是子弹的Y坐标和X坐标,如何确定?
在PUSH 之前修改寄存器的值,修改后F8单步步过,确保被修改过的寄存器压入到堆栈中,然后返回游戏,可以发现子弹产生的位置发生了改变
由此我们可以认为这个CALL便是子弹产生的关键CALL,于是依葫芦画瓢,我们把填入相关的参数然后调用这个CALL
这里我们可以看到子弹成功产生了,所以这个CALL就是产生子弹的关键CALL
如何获得参数的值
在参数压入之前下断
断下:
push eax 则把eax 变成此时的push 0x0
mov eax,[ebp+04]则把eax变成赋值后的数值即mov eax,0x13587F08(记得要F8单步步过一步步下来哦)
push ebx则把ebx变成此时的push 0x3
....以此类推
我们只需要关注call之前push xxx和mov xxx的值即可,然后把相关数值给它即可
如何得知相关参数含义
然后我们可以修改相关数值,比如把push edi 的edi由0x69改成0x169 可以发现子弹的x坐标发生了改变来确定相关参数的含义
也可以观察不同位置豌豆引发中断时寄存器数值的不同来分析出各参数的含义
得出注入代码
于是我们就得出了上面要注入的代码,mov eax,xxxx后面的那个值要修改成你自己的EAX的值
push 0x0
mov eax,0x13587F08
push 0x3
mov ecx,0x513DD
push 0x000513DC
push 0x186
push 0x69
call 0040D620
对照
PlantsVsZombies.exe+672A5 - 50 - push eax {子弹类型}
PlantsVsZombies.exe+672A6 - 8B 45 04 - mov eax,[ebp+04] {植物基址}
PlantsVsZombies.exe+672A9 - 53 - push ebx {行数}
PlantsVsZombies.exe+672AA - 83 E9 01 - sub ecx,01 { 1 }
PlantsVsZombies.exe+672AD - 51 - push ecx {未知,貌似不影响结果,可以直接填0}
PlantsVsZombies.exe+672AE - 56 - push esi { y坐标 }
PlantsVsZombies.exe+672AF - 57 - push edi { x坐标 }
PlantsVsZombies.exe+672B0 - E8 6B63FAFF - call PlantsVsZombies.exe+D620 { 子弹产生call }
到这一步我们就已经找到产生子弹的CALL了,但是我们会发现参数好多啊,我们想让子弹分身只需要修改子弹的行数和子弹的y坐标就可以了,其它的参数和原本一致就行,我们无需关心。那么能不能换个参数少点的CALL呢,自然可以,我们直接去到下一个CALL(之前堆栈窗口的第三个)
PlantsVsZombies.exe+64BBF - 6A 00 - push 00 { 0 }
PlantsVsZombies.exe+64BC1 - 51 - push ecx {行数}
PlantsVsZombies.exe+64BC2 - 6A 00 - push 00 { 0 }
PlantsVsZombies.exe+64BC4 - 57 - push edi {植物基址}
PlantsVsZombies.exe+64BC5 - E8 36220000 - call PlantsVsZombies.exe+66E00 { call 植物动态基址 }
用同样的方法得出这个CALL的参数是4个 然后我们试着调用一下这个CALL
push 0x0
push 0x3
push 0x0
push 0x15B01A48
call 00466E00
对比
PlantsVsZombies.exe+64BBF - 6A 00 - push 00 { 0 }
PlantsVsZombies.exe+64BC1 - 51 - push ecx {行数}
PlantsVsZombies.exe+64BC2 - 6A 00 - push 00 { 0 }
PlantsVsZombies.exe+64BC4 - 57 - push edi {植物基址}
PlantsVsZombies.exe+64BC5 - E8 36220000 - call PlantsVsZombies.exe+66E00 { call 植物动态基址 }
我们这里只给了两个参数,还有两个参数固定为0 就成功调用了子弹的产生
说一下原理:子弹的产生是由具体的某一个植物产生的,子弹产生首先要获取植物的基址,通过植物基址可以获取到植物的X坐标和Y坐标 以及子弹类型等等参数,然后根据植物的X坐标和Y坐标生成对应的子弹的坐标
所以其实只需要传入一个植物的基址就可以完成,但这里它额外多加了一个行数的参数
CALL调用流程
从前面的分析 我们可以知道调用的关系是
植物产生子弹(植物基址,行数)→产生子弹(子弹类型,植物基址,行数,未知,X坐标,Y坐标)→子弹数量增加()→子弹数量++
我们这里就选择HOOK 最外层的这个CALL 植物产生子弹(植物基址,行数),当然也可以HOOK 后面的那个产生子弹(CALL),但要填的参数较多就是了,感兴趣可以当作业自己做一下~~~
修改参数测试CALL
前面我们用原本的参数填充了CALL,发现子弹可以正常产生没有问题,但是当我们想要让子弹换个行产生,我们修改一下行数的参数发现:子弹的显示位置还是在下面,但是阴影在上面
怎么解决呢?前面的原理说过子弹的产生是由植物的X坐标和Y坐标计算而来的,我们这里之所以子弹的Y坐标没有改变自然是因为植物的Y位置还是原本的那个值,所以我们如果想要子弹的Y坐标改变就得改变植物的Y坐标,然后再恢复植物的Y坐标即可
那么植物的Y坐标的偏移是多少呢?可以从前面一个CALL来追溯(较麻烦不推荐)
PlantsVsZombies.exe+672A5 - 50 - push eax {子弹类型}
PlantsVsZombies.exe+672A6 - 8B 45 04 - mov eax,[ebp+04] {植物基址}
PlantsVsZombies.exe+672A9 - 53 - push ebx {行数}
PlantsVsZombies.exe+672AA - 83 E9 01 - sub ecx,01 { 1 }
PlantsVsZombies.exe+672AD - 51 - push ecx {未知,貌似不影响结果,可以直接填0}
PlantsVsZombies.exe+672AE - 56 - push esi { y坐标 }
PlantsVsZombies.exe+672AF - 57 - push edi { x坐标 }
PlantsVsZombies.exe+672B0 - E8 6B63FAFF - call PlantsVsZombies.exe+D620 { 子弹产生call }
我们知道前面的这个CALL的 esi是植物的y坐标,于是我们可以追溯这个y坐标的来源
PlantsVsZombies.exe+671DD - 8D 74 08 DF - lea esi,[eax+ecx-21]
于是我们得到了子弹是eax+ecx-21得来的,很显然是通过计算得来的,我们这里去追溯eax和ecx就可以得到植物的偏移
另一个办法就比较简单,遍历植物的基址,我们可以通过遍历基址比对来分析植物的数据结构,这里碍于篇幅问题就不具体展开了
植物的数据结构分析教程不少,疑惑的可以去看看,这里也不做重点,直接给出植物的y坐标偏移是0c
于是我们修改一下注入代码
注入以后发现子弹能够在我们指定的位置产生了
贴上注入代码:
pushad //将所有的32位通用寄存器压入堆栈
pushfd //将32位标志寄存器EFLAGS压入堆栈
mov eax,0x15B01A48 //植物基址
mov ebx,0x64 //要修改的植物y坐标
mov ecx,[eax+c] //保存修改前的植物y坐标,修改完要还原
mov [eax+c],ebx //修改植物的y坐标
mov edx,0x98000 //此处的0x98000为任意一处可读写空地址
mov [edx],ecx //将到时候要还原的y坐标保存到空地址中
push 0x00 //固定值0
push 0x00 //行数 从0开始 0是第一行 4是最后一行
push 0x00 //固定值0
push eax //植物基址
call 00466E00
mov eax,0x15B01A48 //植物基址
mov ebx,0x98000 //之前保存y坐标的地址
mov ebx,[ebx] //取出y坐标
mov [eax+c],ebx //还原y坐标
popfd //将32位标志寄存器EFLAGS取出堆栈
popad //将所有的32位通用寄存器取出堆栈
为什么要额外用空地址来保存修改前的y坐标? CALL执行后寄存器和堆栈的数据可能会发生变化
这里的空地址地址是哪来的?
CE直接搜索数组 十六进制然后填一堆零 即可得到空地址 搜索选项下面的可写记得勾上
这里因为00098000已经被我修改过了,所以没在搜索结果里
HOOK 子弹产生
前面我们已经实现了在任意行发射子弹,接下来我们就是寻找要HOOK的点并HOOK
要在哪里HOOK呢,首先HOOK的点肯定是要在子弹发射的时候,并且能够获得植物基址
这里我选择了在我们CALL返回的位置HOOK
先贴修改前的代码:
PlantsVsZombies.exe+64BCA - 5F - pop edi
PlantsVsZombies.exe+64BCB - 5E - pop esi
PlantsVsZombies.exe+64BCC - 5B - pop ebx
PlantsVsZombies.exe+64BCD - 8B E5 - mov esp,ebp
PlantsVsZombies.exe+64BCF - 5D - pop ebp
PlantsVsZombies.exe+64BD0 - C3 - ret
修改后的代码:
PlantsVsZombies.exe+64BCA - E9 31B45900 - jmp 00A00000
PlantsVsZombies.exe+64BCF - 5D - pop ebp
PlantsVsZombies.exe+64BD0 - C3 - ret
HOOK的代码:
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
alloc(oriaddr,32) //申请地址 用来存储原本植物基址
alloc(oriy,16) //申请地址 用来存储原本植物的y坐标
alloc(orirow,16) //申请地址 用来存储用本植物的行数
alloc(inity,16) //申请地址 用来存储初始的植物y坐标,每次增加0x64=100=一行的间隔
alloc(initrow,16) //申请地址 用来存储初始的值与行数,每次增加1
label(loopcode) //要循环的代码段
label(returnhere)
label(originalcode)
label(exit)
label(endcode) //退出循环后要复原的代码
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
pushad
pushfd
mov [oriaddr],edi //保存植物基址
mov [inity],0x0 //要改变的植物y坐标
mov [initrow],0x0 //要改变的植物行数
mov ecx,[edi+c] //暂存植物y坐标,植物基址+c偏移为植物y坐标
mov [oriy],ecx //保存植物y坐标到申请的空间
mov ecx,[edi+1c] //暂存植物行数,植物基址+1c偏移为植物行数
mov [orirow],ecx //保存植物行数到申请的空间
jmp loopcode
originalcode:
pop edi
pop esi
pop ebx
mov esp,ebp
exit:
jmp returnhere
loopcode:
cmp [initrow],0x5 //比较是否到了最后一行,如果是则跳出循环
je endcode
mov edi,[oriaddr]
add [inity],0x64
add [initrow],0x1
mov ebx,[inity] //要改变的植物y坐标
mov [edi+c],ebx //改变植物的y坐标
mov ebx,[initrow] //要改变的植物行数
mov [edi+1c],ebx //改变植物行数
mov ebx,[initrow]
sub ebx,0x1
cmp ebx,[orirow] //比较是否和原来的行相同,不重复发射
je loopcode //相同则跳过,不重复发射
push 0x00
push ebx
push 0x00
push edi
call 00466E00
mov ecx,[inity]
cmp [inity],0x1f4 //这里的1f4=500 即比较是否到了最后一行
jb loopcode //如果小于,则继续循环
jmp endcode
endcode:
mov eax,[oriaddr]
mov ebx,[oriy]
mov [eax+c],ebx //还原植物的y坐标
mov ebx,[orirow]
mov [eax+1c],ebx //还原植物的行数
popfd
popad
pop edi
pop esi
pop ebx
mov esp,ebp
jmp 00464BCF
"PlantsVsZombies.exe"+64BCA:
jmp newmem
returnhere:
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
//申请的空间记得撤销
dealloc(newmem)
dealloc(oriaddr)
dealloc(oriy)
dealloc(inity)
"PlantsVsZombies.exe"+64BCA:
pop edi
pop esi
pop ebx
mov esp,ebp
//Alt: db 5F 5E 5B 8B E5
CT代码里已经写好了注释,主体思想就是从第一行开始到第五行,依次调用前面我们的CALL来产生子弹,期间加入了变量的保存和读取以及重复子弹的判断
总结(注意事项)
找CALL最重要的就是思路,根据相关变量来确定CALL,本教程的相关变量是场上的子弹数量
找CALL过滤CALL的时候可以看参数来过滤,因此分析目标CALL的结构至关重要
找CALL看CALL的参数时可以通过CALL里的ret返回值来判断PUSH的参数,RET和PUSH是对应的
CALL的参数传值不只局限于PUSH(堆栈传值),也有可能通过寄存器来传值
分析CALL参数含义时可以通过修改CALL在 PUSH前寄存器的值来验证 也可以通过不同的植物产生子弹的调用比较不同参数来判断
测试CALL的时候一定要注意堆栈平衡,该给的参数一定不能少,不然很容易造成游戏崩溃
HOOK的时候一定要注意保存现场,确保HOOK后寄存器和堆栈中的值没有受到影响,以此保证程序的正常运转
作业
HOOK前面找到的参数较多 较为里面那一层的CALL来实现相同的功能
找一找植物的基址,分析出其数据结构
个人感言
这个教程肝了我一天(是我比较菜的原因),后面那部分HOOK子弹产生的讲解比较少,主要都是代码的实现,思路在前面就已经铺开了,可能不是很好理解,本人能力水平也不是很高,望大家高抬贵手,多多担待,如果这个教程对你有用的话,希望能给我点点赞,你们的支持是对我最大的鼓励
最后附上CT表,(CT表里包含了上次教程的作业答案----通过HOOK实现秒杀所有僵尸 包括僵尸BOSS)
CT表截图