从零开始教你做辅助(二) –攻击怪物功能

这次主要讲解下怎么找到攻击怪物的函数调用地址,及完成辅助中的攻击模块,大家如果看完觉得有哪些写的不好的,可以留言或者提issue,我后面再把文档更新下.

这期内容的markdown我同样也会同步放到我的gayhub项目中的https://github.com/shanlihou/hack_warspear,的tutorial文件夹下面

实现打怪功能

0x01:找到打怪的调用堆栈

方法选择

跟上次我们讲的内容差不多,都是先要找到调用堆栈(其实上次没用到,哈哈,这次是肯定要用到了)
找攻击的调用堆栈,有两种方法.
方法一:我们注意到每次玩家点击怪物之后会多出一个攻击目标,所以至少内存中会存在一块数据用来保存当前攻击目标,我们首先就是找到是哪里会改写这块内存.
方法二:我们可以通过在send函数上加断点,并输出调用堆栈来查找,毕竟每次攻击行为最终还是要把数据发送给服务器的(不过可能因为和服务端通信并不是每次直接发数据,而先把要发送内容放在队列里,下一帧才进行发送,这种情况追踪堆栈可能是追不到的).
方法三:还记得我们上一课讲过怎么找到怪物列表吗,在攻击时候可能会访问到怪物列表,所以可以通过ce在怪物上查看什么访问了怪物地址来找调用堆栈.
我们这里就使用第二种方法来

开始查找

首先我们打开immunity debugger, 然后再在下方命令窗口输入b send(这个命令会在win api send上面加断点),然后我们通过打开断点窗口就能得到send的调用地址

find_send_addr

上图中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,所以也没法获取

call_send
上图是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地址,找到是谁改写了这个地址.

modify_buf
如上图所示,用ida查看第一个位置代码,并转c++查看
clear_buf
从上图可以看出,对目的buf的写入用了同个字符,那这个函数大概就是个类似memset的函数了,这肯定不是我们要找的.
buf_copy
从上图可以看出,应该是拷贝操作了,这就是我们要的,那么我们找到调用这个函数位置,加个断点,找到入参地址(也就是拷贝的原地址),并找到是谁改写了这个地址

这次又找到两个地址.
pack_buff

我们先来查看地址1这个位置,发现这里既不是buf拷贝操作,也不是memset操作,那么这里可能是封装点.然后写出如下lua代码
forward_2

 复制代码 隐藏代码
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这个位置就有我们要找的攻击调用地址

act_case

如上图, 这个switch应该就是玩家操作最终走到的选择的switch
图1位置case 10001:就是0x76a777这个位置,调用了一个0x769c50.
图2位置case 10006:这个还没仔细分析,不过在玩家拾取怪物掉落物品时候会走到这个逻辑

然后就是分析0x769c50的传参了,通过打印传参并对比我们之前获取的怪物列表发现.
arg
图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");
    }
}

final_attack
如上图,编译运行后,点击攻击就会对怪物发起攻击了.

其实仔细想想,如果直接就根据怪物在内存中实体列表中的地址来找,可能一下就找到了.

免责声明:法海所发布的一切资源仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。
法海资源网 » 从零开始教你做辅助(二) –攻击怪物功能