hack_warspear
0x01:必要工具
1.immunity debugger
之所以选择 immunity 而不用od是因为这个调试器支持python,实现用python来简化一些重复性工作,比如在断点时候打印一些寄存器信息,内存信息等等,虽然od应该也可以做,不过对我这种不太习惯的人来说学习成本有点高。
2.cheat engine
郁金香老师的外挂视频教程中也经常使用的一个查基址,甚至调试的软件。而且我发现ce也支持脚本语言,你是可以通过编写lua脚本来实现很多功能,比如查看堆栈等等
0x02:目录结构
GameData
InjectDll
UpdateBase
hackWarspear
immunity:该目录存放为immunity debugger写的一些python脚本
info:该目录存放一些lua脚本及其他一些调试中间的信息记录
上面前面4个目录是当时写的外挂软件目录,不过本文不涉及
0x03:ce + lua
先给大家分享下ce的lua api网址,里面有ce提供的各种lua接口
https://wiki.cheatengine.org/index.php?title=Lua
ce通常的使用方法相信应该很多朋友是知道的,比如我想知道怪物的血量信息:
1.找到怪物血量信息在内存中的地址
2.再attach到游戏进程中查看是哪里改写了这个地址
3.然后根据堆栈找到基址
但是我不知道ce当前代码对应的堆栈,所以就需要用到调试器附加到进程加上断点之后再重复上述操作。
不过有了ce+lua后,我们就能通过编写lua脚本来直接从ce里获取堆栈,然后就可以直接根据堆栈用ida或其他查看静态汇编的软件来分析了。
function print_stack(ebp, deep)
--deep:堆栈深度
--ebp:栈低寄存器
if deep == 0
then
return ""
end
local ebp_4 = readInteger(ebp + 4) --ebp + 4 就是调用过来时候的eip寄存器存的地址,根据这个就能倒推堆栈
local str_ret = string.format("%x->", ebp_4)
local next_ebp = readInteger(ebp) -- 然后当前ebp里面存的就是上一个栈低的地址,也就是上一个ebp里面存的地址
str_ret = str_ret .. print_stack(next_ebp, deep - 1) --剩下的就是递归调用了,最终会把堆栈保存到str_ret里面
return str_ret
end
上面代码中用到了几个ce提供的lua接口,比如readInteger(addr)就是从传入的addr对应的地址中读出内存中四字节数,其他都是lua语法了.
当然少不了加断点,以及断点自动执行lua脚本的方法了,毕竟ce可是具备了调试器的.
function debugger_onBreakpoint() --通用断点回调,每个断点都会进到这里
local ebp_4 = readInteger(EBP + 4)
if EIP == 0x77E114 -- 根据当前eip地址来做函数分发
then
debugger_0077E114()
else
debugger_784a21()
end
return 1
end
function clear_debug()
local tbl = debug_getBreakpointList() --获取当前加上的所有断点列表
if tbl == nil
then
return
end
for i,v in ipairs(tbl) do
print(string.format("%4d 0x%X",i,v))
debug_removeBreakpoint(v) --根据地址删除断点
end
end
clear_debug()
debug_setBreakpoint(0x77E114) --设置断点到地址0x77E114
如上代码展示了ce的断点相关操作,其实debug_setBreakpoint,我看官方文档最近版本应该是可以指定函数做入参了,之前我写这个脚本时候貌似还没有.
这里只是展示了ce+lua脚本的一小部分功能,还有很多很方便的用法,有兴趣的同学可以试一试.毕竟人生苦短,我用(python or lua or ...),能提高一点效率是一点.
0x04: immunity debugger
相信immunity 的python调试功能很多同学都用过,我在分析这款游戏时候,首先也是用的immunity,不过后来发现ce能支持lua后大部分工作就放在ce上了.两个各有优缺点吧.
immunity:
优点: 调试功能还是比较全的,毕竟是名字里带着debugger的调试器
缺点: 断点之后+python着实优点肿了,很多时候他的断点函数执行会导致进程变的极度缓慢.但是如果你用ce+lua就完全不会,一秒钟进入几十次断点游戏运行速度几乎不受影响
ce+lua:
优点: 如上所说,断点调试非常顺滑,甚至调用频率比较高的接口都敢放心加断点.
缺点: lua跟python比提供的功能还是太简单了.而且这个lua的import貌似有问题,import之后的函数还是找不到,所以基本上代码只能写到一个文件中,一些自己封装的接口也没法通用.
其实大部分功能还是相似的,比如常用的断点执行某个函数等功能,主要区别是因为python的库比较多,可以实现一些自己的小工具等.
比如根目录下immunity/rec_down.py这个文件是我实现的一个方便查看内存的脚本.不过我也是在网上找了一个现成的别人对递归下降算法的实现,然后改了一下,让他能支持对简单的语法解析,里面内容有点长,这里就不展示了,原理是利用递归下降算法来做语法解析,有兴趣的同学可以了解下.
用法就是在immunity的命令行窗口下
!rec_down [eax]: 这个命令可以用来查看eax寄存器对应地址的内容
!rec_down [ebx + [eax + 4]] 这个命令是先读取eax+4地址对应内容再 加上ebx的值,得到的地址,再从中获取值
!rec_down [ecx+[ebx + [eax+4]]] 哈哈,这个主要就是可以无限嵌套.
immunity 的脚本执行需要通过在命令窗口执行 "!脚本名 脚本参数1 脚本参数2" 以这种方式执行(不带双引号)
在有了这个小工具后,你就可以把他当做一个库来用在其他脚本里了,比如
from immlib import *
import rec_down # 引入刚才咱们介绍的小工具
import utils
class HookStack(LogBpHook):
def __init__(self, desp):
LogBpHook.__init__(self)
self.desp = desp
#########################################################################
def run(self, regs):
imm = Debugger()
#ret = rec_down.parse_exprs(imm, regs, '[esp],[esp+8],[esp+12],[esp+16]') 这是之前注释的一个,
data_len = rec_down.parse_expr(imm, regs, '[esp+12]') # 这个就是获取 esp+12 再取地址内容存入data_len
if data_len != 6:
data_addr = rec_down.parse_expr(imm, regs, '[esp+8]')
ret = imm.readMemory(data_addr, data_len) #从地址data_addr 处读取 data_len长度的数据到ret中
ret = list(map(lambda x:ord(x), ret))
imm.log("data_addr:{:x}, len:{}, ret:{}".format(data_addr, data_len, ret))
if data_len == 30:
stacks = imm.callStack() # 如果数据长度等于30的话,就获取堆栈并把堆栈输出到imm的log窗口中
for stack in stacks:
imm.log(str(stack))
# stack_set = imm.getKnowledge('stack')
# if not stack_set:
# stack_set = set()
# stack_set.add(rec_down.parse_expr(imm, regs, '[esp+4]'))
# imm.addKnowledge('stack', stack_set)
@staticmethod
def cat_stack_set():
imm = Debugger()
ret = imm.getKnowledge('stack') # getKnowledge是获取用户自定义的之前存入stack中的值,相当于一个全局变量
imm.log(str(ret))
def main(args): # immunity的可执行脚本都要有main函数,然后参数从这里传入
imm = Debugger()
utils.clear_hooks(imm)
opr = args[0]
if opr == 'hook':
# ---------------------------- send start ------------------------------------
addr = '76C76C19' # bp send
#addr = '008563D1' # 8418a6
#addr = '841871'
# ---------------------------- send end --------------------------------------
#addr = '5a926a' # HookClick
#addr = '5a9210' # click header
h = HookStack(addr) # 这个钩子是我们刚刚定义的,现在把它实例化
ret = h.add(h.desp, int(addr, 16)) # 这个就是加断点了,当断下来的时候就会回调到我们的钩子函数里面
if ret == -1:
imm.log("hook send failed!")
elif opr == 'cat':
HookStack.cat_stack_set()
elif opr == 'clear':
utils.clear_hooks(imm)
return "ok"
上面展示的就是加断点并且附带一个钩子函数,当断点断下来时候会执行成员函数run里面的内容.