实现打怪功能
0x01:找到打怪的调用堆栈
方法选择
跟上次我们讲的内容差不多,都是先要找到调用堆栈(其实上次没用到,哈哈,这次是肯定要用到了)
找攻击的调用堆栈,有两种方法.
方法一:我们注意到每次玩家点击怪物之后会多出一个攻击目标,所以至少内存中会存在一块数据用来保存当前攻击目标,我们首先就是找到是哪里会改写这块内存.
方法二:我们可以通过在send函数上加断点,并输出调用堆栈来查找,毕竟每次攻击行为最终还是要把数据发送给服务器的(不过可能因为和服务端通信并不是每次直接发数据,而先把要发送内容放在队列里,下一帧才进行发送,这种情况追踪堆栈可能是追不到的).
方法三:还记得我们上一课讲过怎么找到怪物列表吗,在攻击时候可能会访问到怪物列表,所以可以通过ce在怪物上查看什么访问了怪物地址来找调用堆栈.
我们这里就使用第二种方法来
开始查找
首先我们打开immunity debugger, 然后再在下方命令窗口输入b send(这个命令会在win api send上面加断点),然后我们通过打开断点窗口就能得到send的调用地址
上图中75324CF0 WS2_32.send 就是send函数调用地址
有了这个地址就可以打开ce,并编写如下
function print_stack(ebp, deep)
if deep == 0
then
return ""
end
local ebp_4 = readInteger(ebp + 4)
local str_ret = string.format("%x->", ebp_4)
local next_ebp = readInteger(ebp)
str_ret = str_ret .. print_stack(next_ebp, deep - 1)
return str_ret
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
function debugger_onBreakpoint()
if EIP == 0x75324CF0
then
print(string.format("up frame:0x%X", readInteger(ESP)))
local stack_str = print_stack(EBP, 10)
print(string.format("stack:%s", stack_str))
end
return 1
end
clear_debug()
print(1)
--debug_setBreakpoint(0x75324CF0)
得到如下打印
up frame:0x883C82
stack:86eee6->87c587->86052b->763547ab->76332aac->763344db->763341e0->8602c6->85fb76->4e4558->
其中0x883c82 调用send的地址
stack后面的是调用堆栈,这里大家可能会有疑问,为什么调用堆栈第一层是86eee6,而不是0x883C82.
这是因为,我们这种获取堆栈的方式,针对的是汇编函数开头一定要先进行push EBP, MOV EBP ESP这种操作的函数,这种通常是为了定义很多局部变量,为了不污染上层堆栈空间,所以先把EBP push进堆栈.但是对于没有这两步操作的就无能为力了.
并且其实我们断点加在了send函数的头位置,这里还没来得及push,所以也没法获取
上图是0x883c7c位置汇编代码截图,就是调用send函数的位置,这里重点关注下EAX寄存器,这个寄存器里存了send数据的长度,我们可以通过这个长度来大致判断调用到send这里时候,是否为攻击的调用过程,这里我就补贴代码了,否则看起来有点乱.
从我加的打印来看,游戏的心跳包长度为6,攻击包长度为14
限于ce获取堆栈比较麻烦,现在还是回归immunity debugger吧,编写如下python代码.
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()
data_len = regs['EAX']
imm.log('eax is:{}'.format(data_len)) # 打印出send函数将要发送数据长度
if data_len != 6: # 如果send函数发送数据长度不为6,我们就打印堆栈
stacks = imm.callStack() # 获取堆栈列表
for stack in stacks:
imm.log(str(stack)) # 打印堆栈到log窗口
def main(args):
imm = Debugger()
utils.clear_hooks(imm)
opr = args[0]
if opr == 'hook':
# ---------------------------- send start ------------------------------------
addr = '883c7c' # 在调用send函数处下断点
h = HookStack(addr)
ret = h.add(h.desp, int(addr, 16))
if ret == -1:
imm.log("hook send failed!")
return "ok"
现在我们得到如下调用堆栈
0BADF00D 0x0019F970 warspear.00883BD0 0x0086EEE3 0x0019F96C [0x00559CCF 0x009A5798 0x00000000]
0BADF00D 0x0019F988 warspear.0086EEB0 0x0087C56A 0x0019F984 [0x000A09E4 0x80006010 0x00000001]
0BADF00D 0x0019F9B8 ? warspear.0087C460 0x00860526 0x0019F9B4 [0x00000001 0x0019FAC4 0x008D58DA]
0BADF00D 0x0019F9D0 warspear.00860490 0x763547A9 0x0019F9CC [0x000A09E4 0x00000113 0x00000000]
0BADF00D 0x0019F9D4 Arg1 = 000A09E4 0x763547A9 0x0019F9CC [0x00000000 0x00000000 0x00000000]
0BADF00D 0x0019F9D8 Arg2 = 00000113 0x763547A9 0x0019F9CC [0x00000000 0x00000000 0x00000000]
0BADF00D 0x0019F9DC Arg3 = 00000000 0x763547A9 0x0019F9CC [0x00000000 0x00000000 0x00000000]
0BADF00D 0x0019F9E0 Arg4 = 00559CCF 0x763547A9 0x0019F9CC [0x00000000 0x00000000 0x00000000]
哈哈,很遗憾,找到这里我发现堆栈中不包含攻击操作,因为这个调用堆栈是从消息队列里面取出攻击包然后发给服务器.
那么我们现在要换个思路了
0x02:找到消息封装的函数位置
因为之前的方法行不通,那就只能找到封装攻击包的位置了.
我们先在调用send函数的位置下断点,然后查看buf地址,找到是谁改写了这个地址.
如上图所示,用ida查看第一个位置代码,并转c++查看
从上图可以看出,对目的buf的写入用了同个字符,那这个函数大概就是个类似memset的函数了,这肯定不是我们要找的.
从上图可以看出,应该是拷贝操作了,这就是我们要的,那么我们找到调用这个函数位置,加个断点,找到入参地址(也就是拷贝的原地址),并找到是谁改写了这个地址
这次又找到两个地址.
我们先来查看地址1这个位置,发现这里既不是buf拷贝操作,也不是memset操作,那么这里可能是封装点.然后写出如下lua代码
function print_str(src, len)
local ret = readBytes(src, len, true) --- 下面要调用的读缓冲区的函数
local str_ret = ""
for k,v in ipairs(ret) do
str_ret = str_ret .. string.format("%x ", v)
end
return str_ret
end
function debugger_onBreakpoint()
if EIP == 0x0050e35c and EDX > 4 --这个是我测得时候发现,如果是心跳包,dex一定小于等于4,所以这里过滤掉
then
-- 因为上面,是从dl中读取值,所以我们这里打印EDX的值
print(string.format("eax:0x%X", EDX))
local stack = print_stack(EBP, 10)
print(stack) -- 打印堆栈
elseif EIP == 0x00883c7c -- send函数调用位置
then
local len = EAX -- send 的缓冲区大小
local src = readInteger(ESI + 0x44) -- send 缓冲区指针
local ret = print_str(src, len) -- 这个函数 从缓冲区地址src中读出len个字符到ret中
print(ret)
end
return 1
end
clear_debug()
print(1)
debug_setBreakpoint(0x0050e35c) -- 猜测的封装函数地址,查看用于封装的值
debug_setBreakpoint(0x00883c7c) -- send函数的调用地址,打印出buff的信息,用来对比
下面是攻击时候的打印,可以看到,进行了两次复制0xc到缓冲区的操作,刚好是缓冲区前两字节,这时候基本已经找到我们需要的调用堆栈了,我们一层一层网上搜寻
eax:0xC, arg0:0xC //写入攻击buff第一个字节
50dd1c->7a714e->7a22aa->769cae->76a77c->76a5c6->5c72b5->7af2f9->7e8c79->5c77df->
eax:0xC, arg0:0xC //写入攻击buff第二字节
50dd22->7a714e->7a22aa->769cae->76a77c->76a5c6->5c72b5->7af2f9->7e8c79->5c77df-> // 攻击调用堆栈
c c f 7 5a d8 f6 5 0 0 1 77 69 80 // 这个是打印出来的攻击buff里面的内容
通过对上面调用堆栈的跟踪,我们发现0x76a777这个位置就有我们要找的攻击调用地址
如上图, 这个switch应该就是玩家操作最终走到的选择的switch
图1位置case 10001:就是0x76a777这个位置,调用了一个0x769c50.
图2位置case 10006:这个还没仔细分析,不过在玩家拾取怪物掉落物品时候会走到这个逻辑
然后就是分析0x769c50的传参了,通过打印传参并对比我们之前获取的怪物列表发现.
图1位置:这里edi的值为要攻击的实体列表中怪物地址
图2位置:这里ecx的值为实体列表(之前叫错了,不应该叫怪物列表)中玩家的地址
0x03:编写辅助攻击函数
代码位置 $(ROOT)/hackWarspear/StructGame.cpp
void EntityBase::attack(DWORD monsterPtr)
{
log_debug("enter attack\n");
try
{
DWORD selfPtr = this->basePtr;
log_debug("will attack %p, %p\n", selfPtr, monsterPtr);
__asm
{
mov eax, monsterPtr
push eax //怪物的地址
mov ecx, selfPtr //自己的地址
mov eax, 0x00769c50 //我们辛辛苦苦找到的攻击call地址(这里一定不能直接调用,要先存入某个寄存器中)
CALL eax
}
}
catch (...)
{
}
}
EntityBase::EntityBasePtr EntityMgr::getEntityByHp(DWORD hp)
{
for (auto ptr : this->entities)
{
if (ptr.second->HP == hp)
{
log_debug("find hp\n");
return EntityBase::EntityBasePtr(ptr.second);
}
}
return EntityBase::EntityBasePtr();
}
EntityBase::EntityBasePtr EntityMgr::getEntityByType(DWORD type)
{
for (auto ptr : this->entities)
{
if (ptr.second->type == type)
{
return EntityBase::EntityBasePtr(ptr.second);
}
}
return EntityBase::EntityBasePtr();
}
void EntityMgr::attack()
{
try
{
log_debug("avatar attack\n");
//这里默认我们已经获取到怪物列表了.上期有讲到过,最后会把怪物列表内容存到一个map里面
auto avatar = this->getEntityByType(EntityType::SELF);//根据类型找到玩家地址
auto mon = this->getEntityByHp(470);//找到一个血量为470的怪物
if (mon) {
log_debug("avatar :%d\n", avatar->HP, mon->basePtr);
avatar->attack(mon->basePtr);
}
}
catch (...)
{
log_debug("error happened!\n");
}
}
如上图,编译运行后,点击攻击就会对怪物发起攻击了.