吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 17323|回复: 111
上一主题 下一主题
收起左侧

[C&C++ 原创] 【vc】【笔记】游戏逆向分析和商业挂研究

    [复制链接]
跳转到指定楼层
楼主
舒默哦 发表于 2021-3-28 15:20 回帖奖励
本帖最后由 舒默哦 于 2021-3-28 15:20 编辑

总概


行事曾叫众口哗,本来白璧有微瑕。少年琐事零星步,曾到拉萨卖酒家。--仓央嘉措

对于网络游戏,我以前玩过的只有热血江湖和CF,热血江湖和CF比较,我选择热血江湖来研究,这款游戏没有反调试和游戏检测,可以把精力集中到游戏数据查找和分析中来。在对游戏整个分析过程中,个人感觉查找人物穿墙的数据是最难的,下面有章节详细的分析了怎么查找人物穿墙,郁金香教程里障碍判断他有些地方没有分析到,我提供了思路和详细的分析过程。



为了不把时间浪费在窗体和排版上,我分析了另一款商业挂:热血神器。简单说说热血神器,这是一款存在了将近10年时间的官方挂,期间更新了很多版本,到现在为止很少有BUG了,因此非常优秀,此外,这款外挂在窗体和排版方面,看起来令人舒适。
各种有限自动机的编写,比如自动任务、自动打怪等等,在打怪的自动机编写,自动机里有打怪->检测是否满足回城条件->捡物->检测加蓝加血等,回城时要牵涉物品去向问题的处理,物品的存取和买卖状态需要设计一个格式来保存到配置文件中,
配置文件的读取和保存处理起来比较繁杂,下面有章节会讲到。


当然,我本人能力有限,在找数据方面,有些东西还是模模糊糊的,有些数据找起来麻烦,我避重就轻,转而分析热血神器对同样的数据的处理办法。即使如此,有些数据找起来也是困难重重,比如卡墙打怪问题,
没分析明白,我把这些问题放到了最后,抛砖引玉。欢迎大家一起交流。





参考资料:热血神器(官方挂),重楼教程、CTP教程、郁金香教程,以及吾爱破解和看雪上的一些帖子。
感谢这些朋友,让我学到很多东西。编写的框架是照搬郁金香的,窗体和排版照搬热血神器的。



热血神器分析



在游戏里按End键,呼出的外挂:(下面是整个辅助的外观)




接下来分析热血神器里的文件:
窗口资源全在rxkd.dll这个动态库中。

Item.dll和ItemDate.dll并不是动态库,而是文本文件。


Item.dll里存放的数据有这些:
/宠物药
/人物药
/弓箭手
/首饰(包括戒指、项链和耳环)
/甲手鞋
/衣服                                                   
/武器
/制作
/其他物品
-------------------------------------------------------------
ItemDate.dll里存放的数据有这些:
#stone(普通石头)
#heatstone(热血石头)
#poison(毒药)
#Goodheavy(药材)
#npc(游戏npc名称,包含编号和npc所在地的坐标)
#mapdate(包含两个地图之间入口或者出口的坐标点,以及能移动到其他地图的NPC的坐标点)
#map(地图的像素和地图名字)
#task(任务时打怪的地点坐标,自动任务时要用到)
#服务器
#气功
#Drug(南林药材)
#cri(绝命技)
#QGJN(群攻技能)
#script(地图名称)
-----------------------------------------------------------------------
Item.dll的全部数据加载到了 买和卖->物品设置 这个页面:


ItemDate.dll的部分数据(#stone和#heatstone)加载到了 买和卖->石头设置 这个页面:

--------------------------------------------------------------------------
Map文件里的地图,这些地图用于“地图/PK”的页面。


-----------------------------------------------
燃情骚男z.txt是个人的配置文件,点击应用设置时,会保存或者更新这个文件:

小结


在编写窗体和排版时,可以复制rxkd.dll的各种控件资源,这样可以节省时间,把更多的精力投入到游戏数据查找以及数据分析中。
Item.dll和ItemDate.dll里的数据没有加密,在后面编写总的配置文件时可以直接拿来用,不用去官方网站拷贝资源了。


自动登录


自动登录,模拟鼠键登录是最简单的,另外是发包登录,这个需要查找登录的发包CALL。

发包登录



数据包的发送通常要到三个函数,分别是WSASend、send和sendto,分别在命令行下断,
然后点击确认按钮,发现数据在send处断下,去掉断点回溯,很容易就能找到登录发包CALL 。

查看堆栈可知数据包的缓冲区大小为0x50
[Asm] 纯文本查看 复制代码
$ ==>     00 80 4C 00 0E 00 63 68 61 6E 67 67 75 6F 78 69  ..L...changguoxi  
$+10      61 6E 67 7A 24 00 30 36 61 38 61 39 63 39 37 65  angz$.06a8a9c97e  
$+20      66 64 31 66 38 62 33 33 35 34 66 62 64 65 38 32  fd1f8b3354fbde82  
$+30      62 31 66 39 33 39 38 38 62 61 04 00 24 C5 B2 73  b1f93988ba..$Å2s  
$+40      00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................  

+0x6 //账号
+0x16 //密码加密后的一串哈希值

//密码加密后的大小为32个字节
$ ==>     63623430  
$+4       64646462  
$+8       39656136  
$+C       66396230  
$+10      30343732  
$+14      61636665  
$+18      35613639  
$+1C      64326465  
$+20      65616339  

接下来寻找密码的算法加密。
思路:登录发包call下断,往回跟,多跟几层,
找到密码未加密前的数据,再下断往后找就能找到加密CALL:

这儿并不需要分析加密算法,登录的时候,把加密CALL抠出来给密码加密,
加密后数据直接填充数据包就行了。注意:数据包给send函数之前时,还需要知道ecx这个寄存器的值,不然即使账号密码正确程序也会直接退出。

思路:仍然在登录发包CALL下断,往回溯,在第四层就能找到ecx的来源,它来源于GetWindowLongA这个函数的返回值。

模拟鼠键登录


登录器的分析吾爱破解有人分析过,所以不用多此一举,登录器最终会调用ShellExecuteExA来创建进程。

[Asm] 纯文本查看 复制代码
对Client.exe后面的参数做个分析:
电信一区:D:\热血江湖谁与争锋\Client\Client.exe sessiond1.rxjh.cdcgames.net 13100 
电信六区:D:\热血江湖谁与争锋\Client\Client.exe sessiond6.rxjh.cdcgames.net 13103 
网通一区:D:\热血江湖谁与争锋\Client\Client.exe sessionw1.rxjh.cdcgames.net 13102
分析:
电信一区是sessiond1,电信六区是sessiond6,网通一区是sessionw1。说明“d”代表电信,“w“代表网通,后面的具体数字就是几区的意思。
最后面的13100?参数似乎是端口。

模拟鼠标登录的代码:
[C] 纯文本查看 复制代码
char login[16] = { 67,55,65,78,80, 71,85,79,88,73,65,78,71,90 };//账号
char passwdS[16] = { 67,44,65,78,80, 71,85,79,88,33,65,78,71,90};//密码
//注:上面的账号、密码是错的,实验时请用自己的账号密码
void OnBnClickedButton15()
{

    //定义创建进程需要用的结构体                                                                        
    STARTUPINFO si = { 0 };
    PROCESS_INFORMATION pi;
    si.cb = sizeof(si);
    const char* pfile = "Client.exe";
    const TCHAR* szpath = "D:\\热血江湖谁与争锋\\Client\\";
    const TCHAR* pparameter = "sessiond1.rxjh.cdcgames.net 13102";//登录到电信一区的参数
    SHELLEXECUTEINFO shellinfo = { 0 };
    shellinfo.cbSize = sizeof(shellinfo);
    shellinfo.fMask = 0x40;
    shellinfo.lpFile = pfile;
    shellinfo.lpParameters = pparameter;
    shellinfo.lpDirectory = szpath;
    shellinfo.nShow = TRUE;

    //创建子进程                                                                        
    ShellExecuteEx(&shellinfo);
    //Sleep(5000);


    //定义创建进程需要用的结构体                                                                        
    //STARTUPINFO si = { 0 };
    //PROCESS_INFORMATION pi;
    //si.cb = sizeof(si);
    //char szBuffer[256] = "D:\\热血江湖谁与争锋\\launcher.exe";
    ////创建子进程                                                                        
    //BOOL res = CreateProcessA(
    //    szBuffer,
    //    NULL,
    //    NULL,
    //    NULL,
    //    FALSE ,
    //    CREATE_SUSPENDED,
    //    NULL,
    //    "D:\\热血江湖谁与争锋\\",
    //    &si, 
    //    &pi);

    //ResumeThread(pi.hThread);

    HWND hwnd = NULL;
    while (!hwnd)
    {
        hwnd = ::FindWindowW(L"D3D Window", L"YB_OnlineClient");
    }
    Sleep(3000);//可以多等一点时间  让窗口显示完整
    printf("句柄:%d", hwnd);
    
    //定位输入密码的坐标
    RECT r;
    ::GetWindowRect(hwnd, &r);
    //设置鼠标位置
    ::SetCursorPos(r.left + 206, r.top + 237);

    //我的输入法默认是中文模式,中英文切换时是按下shift键盘
    keybd_event(VK_LSHIFT, 0, 0, 0);                // 按下shift
    keybd_event(VK_LSHIFT, 0, KEYEVENTF_KEYUP, 0);  // 松开shift

    //鼠标左键单击
    ::mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);        //点下左键
    ::mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);        //松开左键
    Sleep(300);

    //循环输入账号
    for (int i = 0; i < sizeof(login) / sizeof(char); i++)
    {
        keybd_event(login[i], 0, 0, 0);
        keybd_event(login[i], 0, KEYEVENTF_KEYUP, 0);
        Sleep(50);
    }
    Sleep(500);

    //Tab键 切换密码编辑框
    keybd_event(VK_TAB, 0, 0, 0);
    keybd_event(VK_TAB, 0, KEYEVENTF_KEYUP, 0);
    Sleep(300);
    for (int i = 0; i < sizeof(passwdS) / sizeof(char); i++)
    {
        keybd_event(passwdS[i], 0, 0, 0);
        keybd_event(passwdS[i], 0, KEYEVENTF_KEYUP, 0);
        Sleep(50);
    }

    //按回车键登录游戏
    keybd_event(VK_RETURN, 0, 0, 0);
    keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0);
    return;
}

int main()
{
    OnBnClickedButton15();
    return 0;
}

查询游戏分区是否满线



这个帖子分析得非常清楚了,源代码也给出了,有兴趣的朋友可以去看看。
https://www.52pojie.cn/forum.php?mod=viewthread&tid=1199201&highlight=%C8%C8%D1%AA%BD%AD%BA%FE

地图

小地图


寻找右上角小地图的基址,思路:人物停留再泫勃派,之后在CE上搜泫勃派,然后换到三邪关再搜三邪关,
可以得到三个地址,然后改名,看小地图的名字是否会变,最终确定一个地址。
最后,用OD打开,在数据窗口往回找,能找到所有对象数组下的ID。
(注意:在CE中搜索汉字要用GB2312的格式,字符数组来搜索)
[Asm] 纯文本查看 复制代码
//地址的格式
$ ==>     00AE62A0  client.&sub_870030
$+4       27A4A698  
$+8       00000020  
$+C       00000629    //所有对象数组下的ID值
$+10      0000000E  
$+14      FFFFFFFF  
$+18      00000000  
$+1C      3F66E601  
$+20      00000000  
$+24      00000000  
$+28      00000000  
$+2C      3F66E6E8  

#define BaseAllObjList 0x2E79D00        //所有对象数组 dd [0x2E79D00+4*i]
[0x2E79D00+4*0x629]  //小地图属性的对象地址
+0x230  //名称  


[0x2E79D00+4*0x627]  //这儿是0x627
+0x230  //左上角坐标





跨图算法



热血江湖的打怪地图关系网如下:


假如玩家要从百武关到泫勃派,热血神器的逻辑是先检测背包是否有泫勃派的回城符,有则直接用回城符,
如果没有,则要寻找去泫勃派的路径,百武关->神武门->柳正关->泫勃派,
然后根据ItemDate.dll里的#npc和#mapdate的数据,匹配坐标,之后调用寻路CALL,最终回到泫勃派。
CTP教程里寻找路径用到的是广度优先搜索算法,就是用一个递归函数实现全遍历,最终找到目标地图,
CTP用的是Lua脚本写的跨图算法(Lua脚本语言的基本用法可以参考郁金香的教程和菜鸟教程),下面贴出代码:
[Lua] 纯文本查看 复制代码
--我用的LUA是5.4.1版本,官网http://www.lua.org/

mapandmaplist = 
{
    {curmap="百武关",        deamap="神武门"},
        {curmap="神武门",        deamap="百武关"},
        {curmap="神武门",        deamap="柳正关"},
        {curmap="神武门",        deamap="地灵洞一层"},
        {curmap="地灵洞一层",    deamap="神武门"},
        {curmap="地灵洞一层",    deamap="地灵洞二层"},
        {curmap="地灵洞二层",    deamap="地灵洞一层"},
        {curmap="地灵洞二层",    deamap="地灵洞三层"},
        {curmap="地灵洞三层",    deamap="地灵洞二层"},
        {curmap="柳正关",        deamap="神武门"},
        {curmap="柳正关",        deamap="泫勃派"},
        {curmap="泫勃派",        deamap="柳正关"},
        {curmap="泫勃派",        deamap="南明湖"},
        {curmap="泫勃派",        deamap="北海冰宫"},
        {curmap="泫勃派",        deamap="虎峡谷"},
        {curmap="泫勃派",        deamap="花亭平原"},
        {curmap="泫勃派",        deamap="南林"},
        {curmap="泫勃派",        deamap="伏魔洞"},
        {curmap="泫勃派",        deamap="三邪关"},
        {curmap="泫勃派",        deamap="九泉之下"},
        {curmap="泫勃派",        deamap="天魔神宫"},
        {curmap="南明湖",        deamap="泫勃派"},
        {curmap="南明湖",        deamap="南明洞"},
        {curmap="南明洞",        deamap="南明湖"},
        {curmap="北海冰宫",      deamap="泫勃派"},
        {curmap="北海冰宫",      deamap="北海冰宫幻影"},
        {curmap="虎峡谷",        deamap="泫勃派"},
        {curmap="虎峡谷",        deamap="地下密路"},
        {curmap="花亭平原",      deamap="泫勃派"},
        {curmap="花亭平原",      deamap="燕飞阁"},
        {curmap="南林",          deamap="泫勃派"},
        {curmap="伏魔洞",        deamap="泫勃派"},
        {curmap="三邪关",        deamap="泫勃派"},
        {curmap="三邪关",        deamap="柳善提督府"},
        {curmap="柳善提督府",    deamap="三邪关"},
        {curmap="柳善提督府",    deamap="松月关"},
        {curmap="柳善提督府",    deamap="血魔洞一层"},
        {curmap="松月关",        deamap="柳善提督府"},
        {curmap="血魔洞一层",    deamap="柳善提督府"},
        {curmap="血魔洞一层",    deamap="血魔洞二层"},
        {curmap="血魔洞二层",    deamap="血魔洞三层"},
        {curmap="血魔洞二层",    deamap="血魔洞一层"},
        {curmap="血魔洞三层",    deamap="血魔洞二层"},
}

mapforfather = 
{
    {curmap="百武关"        ,father=0},
        {curmap="神武门"        ,father=0},
        {curmap="柳正关"        ,father=0},
        {curmap="泫勃派"        ,father=0},
        {curmap="地灵洞一层"    ,father=0},
        {curmap="地灵洞二层"    ,father=0},
        {curmap="地灵洞三层"    ,father=0},
        {curmap="南明湖"        ,father=0},
        {curmap="南明洞"        ,father=0},
        {curmap="北海冰宫"      ,father=0},
        {curmap="北海冰宫幻影"  ,father=0},
        {curmap="虎峡谷"        ,father=0},
        {curmap="地下密路"      ,father=0},
        {curmap="九泉之下"      ,father=0},
        {curmap="天魔神宫"      ,father=0},
        {curmap="花亭平原"      ,father=0},
        {curmap="燕飞阁"        ,father=0},
        {curmap="南林"          ,father=0},
        {curmap="伏魔洞"        ,father=0},
        {curmap="三邪关"        ,father=0},
        {curmap="柳善提督府"    ,father=0},
        {curmap="松月关"        ,father=0},
        {curmap="血魔洞一层"    ,father=0},
        {curmap="血魔洞二层"    ,father=0},
        {curmap="血魔洞三层"    ,father=0},
}

mapandmapnum = 43
mapnum =25
head =1    --头指针
tail=1        ---尾指针
openlist = {}   --打开列表
closelist = {}        --关闭列表
closenum = 0

--插入父节点
function InsertFatherNode(SonMap,FatherMap)

        for  i=1,mapnum do
                if  mapforfather[i].curmap == SonMap then
                        mapforfather[i].father=FatherMap
                end        
        end
        
end

--查询父节点
function QuryFatherNode()

        for i=1,mapnum  do
                print("子地图:",mapforfather[i].curmap,"父地图:",mapforfather[i].father)
        end

end

--查询关闭地图
function QuryCloseMap(Map)

        for  i=1,closenum do
                if closelist[i] == Map  then
                        return true
                end
        end
        return false
end 

--插入关闭地图
function InsertCloseMap(Map)

        if  QuryCloseMap(Map) == false then
                closenum=closenum +1
                closelist[closenum]=Map
        end
end

--路径  参数EndMap表示目标地图
function Path(EndMap)
        for i=1,mapnum do
                if mapforfather[i].curmap == EndMap and  mapforfather[i].father == 0 then
                        return
                end
                if        mapforfather[i].curmap == EndMap then
                        print("当前地图:",EndMap,"上个地图:",mapforfather[i].father)
                        return Path(mapforfather[i].father)
                end
        end
end

--跨图寻路
function SpanMapWayFinding(StartMap,EndMap)
        if        StartMap == EndMap then
                print("找到",EndMap)
                Path(EndMap)
                return
        end
        
        openlist[head]=StartMap
        InsertCloseMap(StartMap)
        print("head:",openlist[head])
        
        for i=1,mapandmapnum do
        
                if        mapandmaplist[i].curmap == StartMap and  QuryCloseMap(mapandmaplist[i].deamap) ==false then 
                        tail = tail + 1
                        InsertFatherNode(mapandmaplist[i].deamap,StartMap)
                        openlist[tail] = mapandmaplist[i].deamap
                end
        end
        
        for        i = head,tail do
                head = head + 1
                return SpanMapWayFinding(openlist[head],EndMap)
        end
end

SpanMapWayFinding("百武关","泫勃派")
--QuryFatherNode()

跨图寻路function SpanMapWayFinding(StartMap,EndMap)是一个递归函数,它的执行逻辑如下:

最终输出:





模拟小地图功能



热血神器在地图/PK页面的图大小是320*320:
(功能:图中的小圆点表示玩家的位置,鼠标在图上移动时会实时捕获像素位置并显示在左下角,鼠标左击时,玩家会移动到点击的坐标位置)

地图资源全在热血神器的Map文件里,下面贴出模拟小地图功能的代码:
[C++] 纯文本查看 复制代码
//重写初始化函数
BOOL CPAGE_MapAndPK::OnInitDialog()
{
        CDialogEx::OnInitDialog();
        SetTimer(2, 20, NULL);

        return TRUE;  // return TRUE unless you set the focus to a control
                                  // 异常: OCX 属性页应返回 FALSE
}

//重写绘制函数
void CPAGE_MapAndPK::OnPaint()
{
        CPaintDC dc(this); // device context for painting
                                           // TODO: 在此处添加消息处理程序代码                                           
                                           // 不为绘图消息调用 CDialogEx::OnPaint()
}


//鼠标移动消息
void CPAGE_MapAndPK::OnMouseMove(UINT nFlags, CPoint point)
{
        // TODO: 在此添加消息处理程序代码和/或调用默认值
        
        //DbgPrintMine("x:%d  y:%d\n", point.x, point.y);
        LONG x = point.x;
        LONG y = point.y;
        if (y<321 && x<321)//在地图范围内才捕获坐标
        {
                char str[20] = { 0 };
                sprintf_s(str, "%d,%d", -2560 + (x * 16), 2560 - (y * 16));
                m_PointXY.SetWindowText(str);
        }
        CDialogEx::OnMouseMove(nFlags, point);
}

//定时器消息
void CPAGE_MapAndPK::OnTimer(UINT_PTR nIDEvent)
{
        // TODO: 在此添加消息处理程序代码和/或调用默认值
        int nXY[2] = { 0 };
        msgGetRoleCurXY(nXY);//获取X和y的坐标
        //DbgPrintMine("捕获坐标:%d   %d\n", nXY[0], nXY[1]);
        int x = (nXY[0]+2560)  / 16;
        int y = (2560-nXY[1]) / 16;
        DrawMap(x, y);
        CDialogEx::OnTimer(nIDEvent);
}

//更新图片和画人物小圆点
void CPAGE_MapAndPK::DrawMap(int x, int y)
{
        CClientDC dc(this); // device context for painting
                                           // TODO: 在此处添加消息处理程序代码                                           
                                           // 不为绘图消息调用 CDialogEx::OnPaint()
//加载游戏图片        
 CImage img;
        CString sPath = _T("C:\\Map\\101.JPG");
        img.Load(sPath);
        dc.SetStretchBltMode(HALFTONE);//设置拉伸模式
        img.Draw(dc.GetSafeHdc(), 0, 0, 320, 320);

        //画小圆点
        CRect EllRect;
        CBrush MapBrush;
        MapBrush.CreateSolidBrush(RGB(255, 0, 0));
        dc.SelectObject(&MapBrush);
        EllRect.left = x - 3;
        EllRect.top = y - 3;
        EllRect.bottom = y + 3;
        EllRect.right = x + 3;
        dc.Ellipse(&EllRect);
}

//鼠标左击地图位置,寻路到这个位置
void CPAGE_MapAndPK::OnLButtonDown(UINT nFlags, CPoint point)
{
        // TODO: 在此添加消息处理程序代码和/或调用默认值
        LONG x = point.x;
        LONG y = point.y;
        if (y < 321 && x < 321)//在地图范围内才捕获坐标
        {
                int cx = -2560 + (x * 16);
                int cy = 2560 - (y * 16);
                msgFindWay(cx, cy);//寻路CALL                
        }

        CDialogEx::OnLButtonDown(nFlags, point);
}

输出结果:


跨图时,更换地图可以利用定时器来检测,直接在CPAGE_MapAndPK::DrawMap(int x, int y)里面来判断处理。
更换地图后地图的起点坐标可以通过ItemDate.dll里#map(地图的像素和地图名字)来获取。



人物穿墙



个人感觉人物穿墙的数据是最难找的。找坐标数组参照了郁金香的教程,判断障碍是自己寻找的方法。
[Asm] 纯文本查看 复制代码
[0x2E79CFC]  //玩家对象基址
+0x1cA0  //x坐标
+0x1CA8  //y坐标
+0x1c64   //目标x坐标
+0x1c6c  //目标y坐标
+0x1A6C    //目的坐x标
+0x1A74    //目的坐y标



先把[0x2E79CFC]+1A6C添加到CE,点击小地图,观察目标x坐标的值,在拐弯时会改写,直线时这个值在途中不会变,查看什么地方改写了目标x坐标。
[Asm] 纯文本查看 复制代码
Client.exe+14E768:
0054E75C - D9 9E 701A0000  - fstp dword ptr [esi+00001A70]
0054E762 - D9 85 BCFEFFFF  - fld dword ptr [ebp-00000144]
- D9 9E 741A0000  - fstp dword ptr [esi+00001A74] <<
0054E76E - EB 16 - jmp Client.exe+14E786
0054E770 - 33 C0  - xor eax,eax

在CE中搜到了其他数据,但是上面列出的才是关键数据。打开OD往上跟,查找数据的来源。


跟进call 0x536060函数,找到如下数据:


再次跟进call 413460这个函数,可以看到如果遇到障碍时,就会执行方框里的代码,改写[esi+0x2424]地址的数组坐标。


[Asm] 纯文本查看 复制代码
//+2424和+2428里存的目的坐标地址的数组
//具体逻辑:点击一个地方,会判断是否遇到障碍物时,如果有障碍,经过计算得到的若干目的地坐标
//的地址会写入到[[0x2E79CFC]+2424]这个地址以及这个地址的偏移地址中,构成一个坐标数组,+2428这个地址记录的数组的最后一个值,开始寻路
//后,每到拐角处,就会从数组里取出下一个目标地址,+2428所记录的地址则减少0x1C,直到+2424和+2428值相同,则表示寻路完成。
[0x2E79CFC]  //玩家对象基址
+2424  //目的地址(头:存放的是最终目标地址)
+2428  //目的地址(尾)


把[[0x2E79CFC]+0x2424]添加到CE里,看什么地方访问了这个地址。为什么就知道找谁访问了这个地址呢?这是因为人物移动到目标地址时,依靠的的是+1A6C(x)、+1A70(z)、+0x1A74(y)这个地方的目标地址,
既然这个地方遇到拐角处会改写,那么可以猜测是[[0x2E79CFC]+2428]这个地址的值写入或者间接改写的。
[Asm] 纯文本查看 复制代码
用CE搜的数据
Client.exe+1DEEF2:
005DEEE9 - 8B 4E 14  - mov ecx,[esi+14]
005DEEEC - D9 9D 30FFFFFF  - fstp dword ptr [ebp-000000D0]
005DEEF2 - D9 46 08  - fld dword ptr [esi+08] <<
005DEEF5 - 89 8D 40FFFFFF  - mov [ebp-000000C0],ecx
005DEEFB - 8B 4B 0C  - mov ecx,[ebx+0C]

用OD查找关键数据判断就在0x005DEEF2附近


跟进去,可以知道je这个地方是关键跳,怎么知道je是关键跳呢,很简单,在je后面下断,玩家直线走路时跳过je后面的代码,有拐弯就会断下。


经过上面分析可以知道,[[0x2E79CFC]+0x2424]地址里存的数组谁访问了,用CE来跟踪,最终可以找到关键位置。
热血神器开启穿墙后,会把je  client.5DE45A改为jmp  client.5DE45A,修改之后,点击穿墙时,不会寻路了,会走直线,但是遇到墙面时仍然会停下,说明还有一个障碍物判断。下面分析怎么寻找障碍物判断。
把je  client.5DE45A改为jmp  client.5DE45A后,然后把玩家对象里的所有目标x坐标,添加到CE中观察,
经过多次测试+0x1c64的目标x坐标,遇到障碍物时,值会改写(说明遇到障碍会执行一段代码),用CE搜索谁改写了这个地方。
[Asm] 纯文本查看 复制代码
//CE搜到的数据
Client.exe+14E1DD:
0054E1D1 - 8B 95 44FEFFFF  - mov edx,[ebp-000001BC]
0054E1D7 - 8B 85 48FEFFFF  - mov eax,[ebp-000001B8]
0054E1DD - 89 8F 641C0000  - mov [edi+00001C64],ecx << //遇到障碍物时,会执行到这里,往回溯。
0054E1E3 - 89 97 681C0000  - mov [edi+00001C68],edx
0054E1E9 - 89 87 6C1C0000  - mov [edi+00001C6C],eax


用OD回溯很快能定位到关键位置:

热血神器开启穿墙后,会把je client.54E0CE这行代码nop掉。经过测试,可以实现穿墙了。
但是,有些地方仍然是不能穿墙的,点击那个地方不能形成光标点。
比如在南林有些地方不能穿墙的,因为点击一些位置没有光标点,所以要寻找光标点的关键判断。
具体思路,点击一个地方会形成一个光标点,到达之后光标点会消失,在CE上搜0和1,光标点存在的时候搜1,消失了搜0,反复搜,
最终可以确定三个地址,点击一个地方,修改三个地址的值为0,看哪个地址修改了光标会消失。最终找到一个地址,附加看是什么地方改写了这个值。
[Asm] 纯文本查看 复制代码
CE上搜到的数据

Client.exe+158117:
0055810B - 89 86 A4420000  - mov [esi+000042A4],eax
00558111 - 8B 96 A4420000  - mov edx,[esi+000042A4]
00558117 - C6 82 E4010000 01 - mov byte ptr [edx+000001E4],01 <<
0055811E - D9 45 D8  - fld dword ptr [ebp-28]
00558121 - 8B 86 A4420000  - mov eax,[esi+000042A4]


用OD附加,往回溯,找到不管是点击哪个位置都能断下的位置。

当nop掉之后,仍然不能形成光标点,往下跟,还有一个判断,nop掉之后就可以实现窗墙了。





配置文件



分析热血神器的时候,知道了Item.dll和ItemDate.dll是辅助的配置文件,燃情骚男z.txt是个人配置文件,下面主要说一下配置文件的逻辑和解析格式。

总的配置文件



热血神器把配置文件数据,放到Item.dll和ItemDate.dll里了,在自动打怪的有限状态机中,配置文件在回城买卖和回城寻路时会用到,比如哪些物品该卖、哪些物品保存、哪些物品过滤不管。
自动打怪的有限状态机,就是开启一个线程,写个死循环,循环里有自动打怪->拾取->检测回城条件->检测是否加血加蓝,当然顺序可以打乱。
代码如下:
[/md]
[Asm] 纯文本查看 复制代码
//捡物、打怪、检测回城补给条件是否满足、检测是否加血加蓝void CAutoPlay::ThreadProc_AutoBeatMonsterAndPcikGoods()
{
        while (true)
        {
                if (g_cAutoPlay.IsRequireSupply())
                {//判断回城条件是否满足,满足则回城补给
                        g_cAutoPlay.GoToCityForSupply();                        
                }
                
                //捡物
                msgPickUpGoods();

                //打怪
                if (g_cAutoPlay.IsAutoBeatMonster)//判断自动打怪的复选框是否勾选
                {
                        msgAutoBeatMosterForSkill("攻击");
                        //msgAutoBeatMosterForSkill("星雨漫天");
                }
                
                //低HP时 使用物品
                if (g_cAutoPlay.IsAutoUseHpGoods)//判断加血复选框是否勾选
                {                        
                        //判断是百分比还是数值
                        if (g_cAutoPlay.szValueOrPercentHp == 0)
                        {                                
                                if (g_tRoleProperty.GetData()->GetPercentHp()< g_cAutoPlay.ndPercentHp)
                                {//判断加血速度
                                        switch (g_cAutoPlay.szDetectionHpSpeed)
                                        {
                                        case 0:Sleep(90);break;                                                                                
                                        case 1:Sleep(270);break;                                                                                
                                        case 2:Sleep(450);break;                                                                                
                                        default:        break;
                                        }
                                        msgUseGoodsForName(g_cAutoPlay.szGoodsmHpName);
                                }
                        }
                        else
                        {
                                /*DbgPrintMine("g_tRoleProperty.GetData()->ndRoleHP:%d  g_cAutoPlay.ndPercentHp:%d",
                                        g_tRoleProperty.GetData()->ndRoleHP, g_cAutoPlay.ndPercentHp);*/
                                if (g_tRoleProperty.GetData()->ndRoleHP< g_cAutoPlay.ndPercentHp)
                                {//判断加血速度
                                        switch (g_cAutoPlay.szDetectionHpSpeed)
                                        {
                                        case 0:Sleep(90); break;
                                        case 1:Sleep(270); break;
                                        case 2:Sleep(450); break;
                                        default:        break;
                                        }
                                        msgUseGoodsForName(g_cAutoPlay.szGoodsmHpName);
                                }
                        }                                                                
                }

                //低MP时 使用物品
                if (g_cAutoPlay.IsAutoUseMpGoods)
                {

                        //判断是百分比还是数值
                        if (g_cAutoPlay.szValueOrPercentMp == 0)
                        {
                                if (g_tRoleProperty.GetData()->GetPercentMp() < g_cAutoPlay.ndPercentMp)
                                {//判断加蓝速度
                                        switch (g_cAutoPlay.szDetectionMpSpeed)
                                        {
                                        case 0:Sleep(90); break;
                                        case 1:Sleep(270); break;
                                        case 2:Sleep(450); break;
                                        default:        break;
                                        }
                                        msgUseGoodsForName(g_cAutoPlay.szGoodsMpName);
                                }
                        }
                        else
                        {
                                if (g_tRoleProperty.GetData()->ndRoleMP < g_cAutoPlay.ndPercentMp)
                                {//判断加蓝速度
                                        switch (g_cAutoPlay.szDetectionMpSpeed)
                                        {
                                        case 0:Sleep(90); break;
                                        case 1:Sleep(270); break;
                                        case 2:Sleep(450); break;
                                        default:        break;
                                        }
                                        msgUseGoodsForName(g_cAutoPlay.szGoodsMpName);
                                }
                        }                        
                }

                //等待多少毫秒
                Sleep(g_cAutoPlay.ndFrequencyforBeatPick);
        }
        return;
}


配置文件的编写处理起来比较繁杂,首先设计处理物品的结构体和处理石头的结构体,
其次,可以用map散列表来储存Item.dll和ItemDate.dll的数据。
[C] 纯文本查看 复制代码
//处理物品的结构体
typedef struct TGoodSManageOnSupply
{
        char szGoodsName[0x30];//物品名称
        unsigned char GoodsFlag_NO : 1;  //不处理  留在背包里边        
        unsigned char GoodsFlag_GOSHOP : 1;//卖商店
        unsigned char GoodsFlag_GODEPOT : 1;//存仓库 
        unsigned char GoodsFlag_NOPICK : 1;//不拾取
}_TGoodSManageOnSupply;

//处理石头的结构体
typedef struct TStoneManageOnSupply
{
        char szStoneName[30];//石头名称
        //union stonetype
        //{
        //        DWORD64 placeholder;//占位符
                DWORD value;//石头的数值
                DWORD  CheckBoxID;//热血石编号ID  热血石对象地址+D44偏移的位置,占4个字节
                BOOL   IsSelectSatus;//勾选状态
        //}u;
}_TStoneManageOnSupply;

//map散列表
#define STONEMAPNUM 20                
map<int, TGoodSManageOnSupply> map_GoodsSet[STONEMAPNUM] ;//物品设置
map<int, TStoneManageOnSupply> map_StoneSet[STONEMAPNUM];//石头设置

配置文件的加载时机在初始化窗体时,可以创建一个线程函数来加载Item.dll和ItemDate.dll,如果不这样做,在加载数据过程中主线程会卡死,游戏会退出。
部分代码贴在下面:
[C++] 纯文本查看 复制代码
//设置物品的拾取、卖还是存仓库的状态
BOOL CPageMainTab::SGoodesAndTone_SetVarCgfData(CString szpLine)
{
        _TGoodSManageOnSupply goodtemp = { 0 };
        static int first = 0;
        static int i = 0;
        static int k = 0;
        if (szpLine.Find('/') == 0)//返回的是位置
        {
                if (first == 0)
                {
                        ++first;                        
                }
                else
                {
                        ++i;
                }                
                k = 0;
                strcpy_s(goodtemp.szGoodsName, szpLine.Mid(1));
                g_cAutoPlay.map_GoodsSet[i][k++] = goodtemp;
                return TRUE;
        }
        CString strlist1, strlist2;
        strlist2 = szpLine;
        int backmark = strlist2.Find(',');
        strlist1 = strlist2.Mid(0, backmark);
        
        if (szpLine.Find("■") == 0)
        {
                strcpy_s(goodtemp.szGoodsName, szpLine);                
                //g_cAutoPlay.map_GoodsSet[i].insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
                g_cAutoPlay.map_GoodsSet[i][k++] = goodtemp;
                return TRUE;
        }

        strcpy_s(goodtemp.szGoodsName, strlist1);
        int premark = 0;
        char tempd[3] = { 0 };
        int j = 0;
        do
        {
                strlist2 = strlist2.Mid(backmark + 1);
                backmark = strlist2.Find(',');
                strlist1 = strlist2.Mid(0, 1);

                //DbgPrintMine("%s ", strlist1);
                if (strcmp(strlist1, "0") == 0)
                {
                        tempd[j] = 0;
                }
                else
                {
                        tempd[j] = 1;
                }
                if (backmark == -1)
                {
                        break;
                }
                ++j;
        } while (true);
        goodtemp.GoodsFlag_GODEPOT = tempd[1];
        goodtemp.GoodsFlag_GOSHOP = tempd[0];
        goodtemp.GoodsFlag_NOPICK = tempd[2];
        //g_cAutoPlay.map_GoodsSet[i].insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
        g_cAutoPlay.map_GoodsSet[i][k++] = goodtemp;
        return TRUE;
}

//设置石头的状态
BOOL CPageMainTab::SSTone_SetVarCgfData(CString szpLine)
{
        _TStoneManageOnSupply goodtemp = { 0 };
        static int first = 0;
        static int i = -1;
        static int k = 0;
        if (szpLine.Find('#') == 0)
        {
                k = 0;
                if (first == 0)
                {
                        ++first;
                        return TRUE;
                }

                ++i;
                if (szpLine.Find("#heatstone") == 0)
                {
                        strcpy_s(goodtemp.szStoneName, szpLine);
                        g_cAutoPlay.map_StoneSet[i][k++] = goodtemp;
                        DbgPrintMine("%s\r\n", goodtemp.szStoneName);
                }                                                                                                
                return TRUE;                
        }
        CString strlist1, strlist2;
        strlist2 = szpLine;
        int backmark = strlist2.Find('|');
        strlist1 = strlist2.Mid(0, backmark);

        if (szpLine.Find("/") == 0)
        {
                k = 0;
                strcpy_s(goodtemp.szStoneName, szpLine.Mid(1));
                //g_cAutoPlay.map_GoodsSet[i].insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
                
                g_cAutoPlay.map_StoneSet[++i][k++] = goodtemp;
                DbgPrintMine("%s\r\n", goodtemp.szStoneName);
                return TRUE;
        }

        strcpy_s(goodtemp.szStoneName, strlist1);

        strlist2 = strlist2.Mid(backmark + 1);
        backmark = strlist2.Find('|');
        if (backmark == -1)
        {
                goodtemp.value = atoi(strlist2);
                g_cAutoPlay.map_StoneSet[i][k++] = goodtemp;
                //DbgPrintMine("%s\r\n", goodtemp.szStoneName);
                return TRUE;;
        }

        goodtemp.CheckBoxID = atoi(strlist2);
        strlist2 = strlist2.Mid(backmark + 1);
        goodtemp.IsSelectSatus = atoi(strlist2);
        g_cAutoPlay.map_StoneSet[i][k++] = goodtemp;

        //DbgPrintMine("%s\r\n", goodtemp.szStoneName);

        return TRUE;
}



//读取配置文件信息 初始化物品设置的界面
BOOL CPageMainTab::RGoodsAndTone_ReadConfigDataForFile(const char* szpConfigFile)
{
        fstream fs;
        //char szpLine[100];
        DWORD buffersize = 1024 * 1024;
        char* szpLine = new char[buffersize];
        fs.open(szpConfigFile, ios::in);//in是输入 out是输出
        while (true)
        {        
                if (fs.eof())
                {
                        break;
                }
                fs.getline(szpLine, buffersize);
                
                if (strcmp(szpLine,"") == 0)
                {
                        //DbgPrintMine("数据为空%s=\r\n", szpLine);
                        break;
                }
                //读取一行数据
                SGoodesAndTone_SetVarCgfData(szpLine);//给相应的物品设置状态
                //DbgPrintMine("%s\r\n", szpLine);
        }
        fs.close();
        //释放内存
        delete[]szpLine;
        return TRUE;
}

//读取石头设置的配置文件
BOOL CPageMainTab::RStone_ReadConfigDataForFile(const char* szpConfigFile)
{
        fstream fs;
        DWORD buffersize = 1024 * 1024;
        char *szpLine = new char[buffersize];
        //CString szpLine;
        fs.open(szpConfigFile, ios::in);//in是输入 out是输出
        while (true)
        {
                if (fs.eof())
                {
                        break;
                }
                fs.getline(szpLine, buffersize);

                if (strcmp(szpLine, "") == 0)
                {
                        //DbgPrintMine("数据为空%s=\r\n", szpLine);
                        break;
                }
                //读取一行数据
                SSTone_SetVarCgfData(szpLine);//给相应的物品设置状态
                //DbgPrintMine("%s\r\n", szpLine);
        }
        fs.close();
        //释放内存
        delete[]szpLine;
        return TRUE;
}

//线程回调函数
void  ThreadProc(PVOID param)
{
        Sleep(1000);
        CPageMainTab* cpgemb =(CPageMainTab *)param;
        //加载物品设置的配置文件
        cpgemb->RGoodsAndTone_ReadConfigDataForFile(g_GoodsConfigFile);

        //宠物药;人物药;弓手箭;首饰;甲手鞋;衣服;武器;制作;其它物品;
        //种类下拉列表初始化 
        map<int, TGoodSManageOnSupply>::iterator iter_good;// map_GoodsSet[STONEMAPNUM];
        for (int i = 0; i < STONEMAPNUM; i++)
        {
                iter_good = g_cAutoPlay.map_GoodsSet[i].begin();
                if (iter_good == g_cAutoPlay.map_GoodsSet[i].end())
                {
                        //DbgPrintMine("遍历完成 %d\r\n", i);
                        break;
                }
                cpgemb->m_PageBuyAndSell.m_PageSupply_GoodsManage.m_GoodsType.AddString(iter_good->second.szGoodsName);//.m_StoneType_Combox_Ctrl.AddString(iter->second.szStoneName);
        }

        //加载石头设置的配置文件
        cpgemb->RStone_ReadConfigDataForFile(g_StoneConfigFile);

        //石头类型下拉列表初始化 m_StoneType_Combox_Ctrl  //石头设置窗口
        map<int, _TStoneManageOnSupply>::iterator iter;// = g_cAutoPlay.map_StoneSet[1].begin();
        for (int i = 0; i < STONEMAPNUM; i++)
        {
                iter = g_cAutoPlay.map_StoneSet[i].begin();
                if (iter == g_cAutoPlay.map_StoneSet[i].end())
                {
                        //DbgPrintMine("遍历完成 %d\r\n", i);
                        break;
                }
                if (strcmp(iter->second.szStoneName,"#heatstone")==0)
                {
                        //初始化热血石的状态信息
                        //m_HeatStone_CheckList.AddString("暗影绝杀");
                        //m_HeatStone_CheckList.AddString("霸气破甲");
                        do
                        {
                                ++iter;
                                if (iter == g_cAutoPlay.map_StoneSet[i].end())
                                {
                                        break;
                                }
                                cpgemb->m_PageBuyAndSell.m_Page_StoneManage.m_HeatStone_CheckList.AddString(iter->second.szStoneName);
                        } while (true);
                        continue;
                }
                cpgemb->m_PageBuyAndSell.m_Page_StoneManage.m_StoneType_Combox_Ctrl.AddString(iter->second.szStoneName);
        }
        return;
}



加载后的数据:







回城补给,物品买卖或者保存,就可以依照map散列表里储存的数据来具体处理了。

个人配置文件


个人配置文件就是点击应用设置时,会生成的个人玩家当前辅助设置的信息,保存以玩家命名的文本文件。
下面分析燃情骚男z.txt里的数据。

由分析可以得到,燃情骚男z.txt里得格式是根据窗体的页面,从左到右,从上到下,分类编号,最后储存。
比如,D01CHECK01=1    D01表示保护选项卡,CHECK01表示保护选项卡里的第一个复选框,等于1表示勾选上了。
再如,D02COMBO05=3    D02表示挂机选项卡,COMBO05表示挂机选项卡的第五个下拉列表,等于3表示下拉列表的第三个值。

RxSItem表示的是热血石,StoneItem包括宠物石头、普通石头、奇遇石等等,这些数据在石头设置中。
GoodItem是物品列表,这些数据只是在物品设置中的部分数据。
点击应用设置时,代码如下:
[Asm] 纯文本查看 复制代码
//保存配置文件
BOOL CPageMainTab::SaveConfigDataToFile(const char* szpConfigFile)
{
        CString strCfg;
        CString strTmp;
        DWORD CheckCount, EditCount, ComboCount, ListBoxCtlCount;//复选框、编辑框、下拉列表、列表控件计数器
/*----------------------------------------------------------------------------------*/
/*        格式参照热血神器,格式从左到右,从上到下,分类编号,然后储存。
*        例如,D01CHECK01=1    D01表示保护选项卡,CHECK01表示保护选项卡里的第一个复选框,等于1表示勾选上了。
*        再如,D02COMBO05=3    D02表示挂机选项卡,COMBO05表示挂机选项卡的第五个下拉列表,等于3表示下拉列表的第三个值。*/        
/*----------------------------------------------------------------------------------*/

//------------------------------保护选项卡(1)----------------------- 
        CheckCount = EditCount = ComboCount = ListBoxCtlCount =0;
        //---复选框CHECK---
        //当血量低于(一级) m_IsAutoUseHpGoods
        strTmp.Format("D01CHECK%02d=%d\n", CheckCount++, m_PageProtect.m_IsAutoUseHpGoods);
        strCfg += strTmp;
        //当内力低于        m_IsAutoUseMpGoods
        strTmp.Format("D01CHECK%02d=%d\n", CheckCount++, m_PageProtect.m_IsAutoUseMpGoods);
        strCfg += strTmp;

        //---编辑框EDIT---
        //DWORD m_ndPercentHp;
        strTmp.Format("D01EDIT%02d=%d\n", EditCount++, m_PageProtect.m_ndPercentHp);
        strCfg += strTmp;
        //DWORD m_ndPercentMp;
        strTmp.Format("D01EDIT%02d=%d\n", EditCount++, m_PageProtect.m_ndPercentMp);
        strCfg += strTmp;

        //---下拉列表COMBO---
        //CComboBox m_szValueOrPercentHp;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szValueOrPercentHp.GetCurSel());
        strCfg += strTmp;
        //CComboBox m_szGoodsm_HpName;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szGoodsm_HpName.GetCurSel());
        strCfg += strTmp;
        //CComboBox m_szValueOrPercentMp;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szValueOrPercentMp.GetCurSel());
        strCfg += strTmp;
        //CComboBox m_szGoodsMpName;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szGoodsMpName.GetCurSel());
        strCfg += strTmp;
        //CComboBox m_szDetectionHpSpeed;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szDetectionHpSpeed.GetCurSel());
        strCfg += strTmp;
        //CComboBox m_szDetectionMpSpeed;
        strTmp.Format("D01COMBO%02d=%d\n", ComboCount++, m_PageProtect.m_szDetectionMpSpeed.GetCurSel());
        strCfg += strTmp;

//...............省略

//------------------------------挂机选项卡(2)----------------------- 
        CheckCount = EditCount = ComboCount = ListBoxCtlCount = 0;
        //---复选框CHECK---
        //BOOL m_chk_IsAutoBeatMonster_b;//是否自动打怪
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++,m_PageGuaJi.m_chk_IsAutoBeatMonster_b);
        strCfg += strTmp;
        //BOOL m_IsLimitRadii;//是否定点打怪
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++, m_PageGuaJi.m_IsLimitRadii);
        strCfg += strTmp;
        //BOOL m_IsMonstorFilter;// 过滤怪物
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++, m_PageGuaJi.m_IsMonstorFilter);
        strCfg += strTmp;
        //BOOL m_IsAutoFightback;// 自动反击
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++, m_PageGuaJi.m_IsAutoFightback);
        strCfg += strTmp;
        //BOOL m_IsFastAttack;// 快速攻击
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++, m_PageGuaJi.m_IsFastAttack);
        strCfg += strTmp;
        //BOOL m_Chg_AttackDisTance_b;// 是否调整攻击距离;
        strTmp.Format("D02CHECK%02d=%d\n", CheckCount++, m_PageGuaJi.m_Chg_AttackDisTance_b);
        strCfg += strTmp;
//.................有二十多个页面的数据需要保存,代码很多,此处省略,直接跳到最后保存数据
//这儿有必要把物品设置和石头设置贴出来,物品设置是选存(就是在物品设置页面改变了物品的买卖以及是否拾取状态,
//这些数据会记录到一个副本中,点击应用设置后,副本中的数据会更新到个人配置文件中), 石头设置是全存,热血神器也是这么做的。
//---物品设置列表----
        strCfg += "GoodItem=";
        DbgPrintMine("物品设置列表");
        map<int, TGoodSManageOnSupply>::iterator iter_mapgoods; //= map_GoodsSet_Copy[STONEMAPNUM];//物品设置状态改变时,保存的副本
        for (int i = 0; i < STONEMAPNUM; i++)
        {
                iter_mapgoods = g_cAutoPlay.map_GoodsSet_Copy[i].begin();
                if (iter_mapgoods == g_cAutoPlay.map_GoodsSet_Copy[i].end())
                {
                        DbgPrintMine("物品设置列表空:%d\r\n", i);
                        continue;
                }
                for (; iter_mapgoods != g_cAutoPlay.map_GoodsSet_Copy[i].end();++iter_mapgoods)
                {
                        strCfg = strCfg + iter_mapgoods->second.szGoodsName+",";
                        if (iter_mapgoods->second.GoodsFlag_GOSHOP)//是否卖商店
                        {
                                strCfg += "1,";
                        }
                        else
                        {
                                strCfg += "0,";
                        }
                        if (iter_mapgoods->second.GoodsFlag_GODEPOT)//是否存仓库
                        {
                                strCfg += "1,";
                        }
                        else
                        {
                                strCfg += "0,";
                        }
                        if (iter_mapgoods->second.GoodsFlag_NOPICK)//是否不拾取
                        {
                                strCfg += "1/";
                        }
                        else
                        {
                                strCfg += "0/";
                        }
                }
        }
        strCfg += "\n";

        //---石头设置列表----
        //普通石头 StoneItem= 
        map<int, TStoneManageOnSupply>::iterator iter_mapstone;// map_StoneSet[STONEMAPNUM];//石头设置
        DbgPrintMine("石头设置列表");
        strCfg += "StoneItem=";
        for (int i = 0; i < STONEMAPNUM; i++)
        {
                iter_mapstone = g_cAutoPlay.map_StoneSet[i].begin();
                if (iter_mapstone == g_cAutoPlay.map_StoneSet[i].end())
                {
                        DbgPrintMine("物品设置列表空:%d\r\n", i);
                        continue;
                }
                char strvalue[20];
                if (strcmp(iter_mapstone->second.szStoneName,"#heatstone") == 0)
                {//热血石 RxSItem= 
                        strCfg += "\n";
                        strCfg += "RxSItem=";
                        do
                        {
                                ++iter_mapstone;
                                if (iter_mapstone == g_cAutoPlay.map_StoneSet[i].end())
                                {
                                        break;
                                }
                                strCfg += iter_mapstone->second.szStoneName;
                                strCfg += ",";
                                sprintf_s(strvalue,"%d",iter_mapstone->second.IsSelectSatus);//int类型转换为字符串类型
                                strCfg = strCfg + strvalue + "/";
                        } while (true);
                        
                        break;
                }
                CString stotalstone;
                stotalstone = iter_mapstone->second.szStoneName;
                do
                {
                        ++iter_mapstone;
                        if (iter_mapstone == g_cAutoPlay.map_StoneSet[i].end())
                        {
                                break;
                        }
                        strCfg += stotalstone + "," + iter_mapstone->second.szStoneName;
                        strCfg += ",";
                        sprintf_s(strvalue,"%d",iter_mapstone->second.value);//int类型转换为字符串类型
                        strCfg = strCfg + strvalue + "/";
                } while (true);
                
        }        
        strCfg += "\n";
 
 //.......................省略,跳到最后
 
 //--------------------保存文件--------------------------
        FILE* pfile;
        fopen_s(&pfile, szpConfigFile, "w");
        fputs(strCfg, pfile);
        fclose(pfile);
        //DbgPrintMine("保存路径%s\r\n", szpConfigFile);
        return TRUE;
}


点击应用设置后,保存的数据如下:




读取数据,和上面保存数据一样的,只不过是反向解析。

其他


玩家卡在石头不能打怪,基本上在障碍物里都不能打怪,但有些地方是可以卡怪的,这种地方很少。
这儿寻找看看能不能在任意障碍物里打怪,用弓箭手测试,因为弓箭手攻击距离远。



具体思路,打怪时,在人物对象里必定有一个标志位表示此时在打怪,[[0x2E79CFC]+0x1A64]表示是否选中怪物。
[Asm] 纯文本查看 复制代码
[0x2E79CFC]  //玩家对象基址
+1A64    //0xFFFF    表示未选中怪物 选中时表示的是怪物ID


打开OD,在数据找到[0x2E79CFC]+0x1A64这个位置,在打怪时观察附近的值有没有变化


在打怪时,+0x1A80处的值会改变,脱离OD,打开CE,看什么地方改写了这个地址的值。
[Asm] 纯文本查看 复制代码
从CE搜到的数据:
第一条数据
Client.exe+14D9EE:
0054D9E4 - 89 8F 7C1A0000  - mov [edi+00001A7C],ecx
0054D9EA - 0FBE 46 44  - movsx eax,byte ptr [esi+44]
0054D9EE - 89 87 801A0000  - mov [edi+00001A80],eax <<
0054D9F4 - C6 87 8C1A0000 01 - mov byte ptr [edi+00001A8C],01
0054D9FB - 0FBE 4E 46  - movsx ecx,byte ptr [esi+46]

第二条数据
Client.exe+1433BA:
005433AF - 8D 04 80   - lea eax,[eax+eax*4]
005433B2 - 0FB6 8C 83 A9240000  - movzx ecx,byte ptr [ebx+eax*4+000024A9]
005433BA - 89 4A 14  - mov [edx+14],ecx <<
005433BD - 8B 83 E0250000  - mov eax,[ebx+000025E0]
005433C3 - 8D 84 80 29090000  - lea eax,[eax+eax*4+00000929]

从第二条数据往回溯,找到无论是在石头中或者正常打怪的地方都能够断下的地方。
最终找到下面这个位置。



返回eax为1则说明可以打怪,为0不能打怪。jne改为jmp后,就能在任何障碍物内打怪了。事实上,改了这个位置,不能在任何障碍物内打怪,似乎还有其他判断。
那么,正常跳转时,要记录哪些地方跳,哪些地方不跳,然后到达第二条数据这个地方,之后跑到石头中,再和前面的正常跳转的数据对比。
但是,在追查数据的过程中有来回跳的点,而且在0x541061与0x005433BA之间的指令非常多,跳来跳去搞蒙了,最终放弃查找了。
不知道是不是思路错了,还是所在的坐标点要在服务器上验证、判断之后才能打怪?


土灵符问题



这儿只讨论至尊热血符的中土灵符。


土灵符有两个操作,一个是保存地点,一个是移动到哪儿,至尊热血符的土灵符可以保存30个坐标位置。



保存地点和移动都可以从WSASend断下,然后回溯。
先分析保存点的发包CALL:


[Asm] 纯文本查看 复制代码
//保存8   名称6789
$ ==>     00 00 0C 10 11 00 36 37 38 39 00 00 00 00 00 00  ......6789......  
$+10      00 00 00 00 00 11 00 00 00 00 00 00 00 00 00 00  ................  

//保存7   名称8888
$ ==>     00 00 0C 10 11 00 38 38 38 38 00 00 00 00 00 00  ......8888......  
$+10      00 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00  ................  

分析得出:
+0x6  //保存的名称
+0x15  //保存地址的编号(注意:/保存1 是从A开始,依次类推)

$ ==>     100C0000  nvd3dum.100C0000
$+4       36360011  
$+8       00003636  
$+C       00000000  
$+10      00000000  
$+14      00001900  
$+18      00000000  



push 0x0
push 0x00001900  
push 0x0
push 0x0
push 0x00003636  
push 0x36360011  
push 0x100C0000  

mov eax,esp
push 0x17  //数据包大小
push eax
mov ecx,dword ptr ds:[0x11A91C0]

mov eax,0x4FD7F0
call eax
add esp,0x1C

实验:用以上代码注入游戏,地址保存成功了。


-------------------------------------------------------------------------------------------------------
再分析移动土灵符的发包CALL:

[Asm] 纯文本查看 复制代码
//移动到 保存1
$ ==>     00 00 05 10 06 00 00 00 0A 00 01 00 00 00 00 00  

//移动到 保存2
$ ==>     00 00 05 10 06 00 00 00 0B 00 01 00 00 00 00 00  ................  

经过测试得出:除了+0x8的地址的值在变化,其余没变
+0x8  //保存地址的编号(注意:/保存1 是从A开始,依次类推)



push 0x0
push 0x1000B  //跳转到保存2的地址
push 0x6
push 0x10050000

mov eax,esp
push 0xc  //数据包大小
push eax
mov ecx,dword ptr ds:[0x11A91C0]
mov eax,0x4FD7F0
call eax
add esp,0x10


实验:跳到 /保存28这个位置


注入之后移动成功:

无论是移动地点还是保存地点,都没有看见有坐标的数据发送,在send或者sendto下断没有反应,那么,接下来寻找土灵符对象的地址,看能不能找到每个保存地点的坐标的相关信息。
这儿,为什么非要找到土灵符坐标的相关信息呢,因为在保存有关地点的坐标时,可以不用跑到那个地方,直接填写坐标然后发送数据包就能保存坐标信息了。
寻找土灵符对象地址的思路:打开CE,搜索土灵符保存地址的字符串。(注意:在CE中搜索汉字要用GB2312的格式,字符数组来搜索)最终搜到存放这个字符串的位置,然后用OD打开:


在数据窗口往回溯,能搜到土灵符的对象地址。方法好像不对,我忘了怎么搜的了,下面贴出基址和偏移:
[Asm] 纯文本查看 复制代码
 0x1267B54 土灵符窗口的基址      在基址头文件有定义#define BaseF1_F10ArgEcx 0x1267B54        

//保存移动地点名称
+0xbb9+0x96    //保存1
+0xbb9+0x96+0xf*1    //保存2
+0xbb9+0x96+0xf*2    //保存3
.......

[ 0x1267B54]+0xbb9+0x96 

在土灵符对象的地址中也没有保存坐标点的相关信息,或许坐标的获取在服务器取得的,至此,寻找保存地址的坐标作罢。




结语



对这款游戏分析和代码编写,前前后后总共用了2个月时间,用了两张至尊热血符。关于源代码,我不会发出来,有些不法分子会用来获取商业利益,再有本来就是技术交流贴,目的是提高自己逆向分析和代码编写能力。共勉!

点评

小老弟真的是太6了。看完之后茅塞顿开。  发表于 2021-3-30 16:33

免费评分

参与人数 67威望 +2 吾爱币 +161 热心值 +59 收起 理由
vanillasky0220 + 1 + 1 用心讨论,共获提升!
diwuc + 1 我很赞同!
DustCen + 1 我很赞同!
Kcass774 + 1 + 1 用心讨论,共获提升!
changchen320 + 1 谢谢@Thanks!
peterzzx + 1 我很赞同!
薛定乐 + 1 + 1 我很赞同!
shuai23long + 1 + 1 我很赞同!
21MyCode + 1 + 1 我很赞同!
qw754852 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
塞北的雪 + 1 + 1 用心讨论,共获提升!
f4cku + 1 + 1 我很赞同!
wwh0791 + 1 + 1 谢谢@Thanks!
ALCATEL + 1 + 1 发帖不易
kdrew + 1 + 1 热心回复!
weiye588 + 1 + 1 我很赞同!
aaa661179 + 1 + 1 我很赞同!
pelephone + 1 + 1 谢谢@Thanks!
独行风云 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
wws天池 + 1 + 1 我很赞同!
NanKeYM + 1 + 1 用心讨论,共获提升!
经典柚子 + 1 + 1 我很赞同!
月光下の狼 + 1 用心讨论,共获提升!
hjthack + 1 + 1 我很赞同!
fujianguo + 1 + 1 用心讨论,共获提升!
KylinYang + 1 + 1 谢谢@Thanks!
dmix + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
m762 + 1 + 1 我很赞同!
干杯 + 1 + 1 用心讨论,共获提升!
siuhoapdou + 1 + 1 用心讨论,共获提升!
f18574141141 + 1 + 1 我很赞同!
川木 + 1 谢谢@Thanks!
gogobn + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
qiaoyong + 1 + 1 热心回复!
Waik + 1 谢谢@Thanks!
FY-1573 + 1 热心回复!
chuxia12 + 1 + 1 感谢您的宝贵建议,我们会努力争取做得更好!
叶隽 + 1 用心讨论,共获提升!
sapin + 1 + 1 用心讨论,共获提升!
rekaytang + 1 + 1 我很赞同!
ma4907758 + 1 + 1 谢谢@Thanks!
遇日不归 + 1 + 1 我很赞同!
YURYOM + 1 + 1 热心回复!
lanwd + 1 + 1 鼓励转贴优秀软件安全工具和文档!
azcolf + 1 + 1 热心回复!
SmallBridge + 1 + 1 我很赞同!
小脚jio + 1 + 1 我很赞同!
gaosld + 1 + 1 用心讨论,共获提升!
zhuzhuxia111 + 1 + 1 我很赞同!
skynet996 + 1 + 1 我很赞同!
love514415 + 1 + 1 谢谢@Thanks!
淡灬看夏丶恋雨 + 1 我很赞同!
nosilence + 1 + 1 用心讨论,共获提升!
linden007x + 1 用心讨论,共获提升!
klcszm + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
天上飞来一只 + 1 热心回复!
fengbolee + 1 + 1 用心讨论,共获提升!
sam喵喵 + 1 + 1 谢谢@Thanks!
5ud0 + 1 + 1 我很赞同!
唐.吉诘 + 1 + 1 谢谢@Thanks!
ll996075dd + 1 + 1 干的漂亮,断绝不法分子的想法
lmh314 + 1 谢谢@Thanks!
zhaooptimus + 1 + 1 谢谢@Thanks!
lyl610abc + 1 + 1 我很赞同!
lookerJ + 1 + 1 我很赞同!
asaSKTY + 2 我很赞同!
苏紫方璇 + 2 + 100 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

推荐
ifthen 发表于 2021-3-28 16:30
玩游戏是学技术的一大推进力。
推荐
mr.lance 发表于 2021-4-13 08:59
看到这外挂截图就想起了当年熬夜写外挂的时光,太亲切了
头像被屏蔽
推荐
公考资料助手 发表于 2021-4-6 20:19
头像被屏蔽
推荐
公考资料助手 发表于 2021-4-5 17:59
提示: 作者被禁止或删除 内容自动屏蔽
推荐
GeaC 发表于 2021-4-5 17:38
很全面的解析,不错
推荐
cenoser795 发表于 2021-4-3 20:55
郁金香真是个启蒙
推荐
xpz84 发表于 2021-4-2 23:08
很多学外挂的都拿热血江湖来练手,包括我
推荐
skynet996 发表于 2021-3-30 09:23
膜拜大神, 这帖子分析开阔了很多思路
推荐
淡灬看夏丶恋雨 发表于 2021-3-30 07:38
太牛逼了。
沙发
lyl610abc 发表于 2021-3-28 15:34
商业挂研究× 牢房直通车√
楼主太强了,分析得很透彻,学习了
3#
52changew 发表于 2021-3-28 15:59
大佬; 厉害; 膜拜膜拜; 学习学习下; 谢谢 分享!
4#
lsy832 发表于 2021-3-28 16:12
帖子先收藏 慢慢品
6#
w2pj123 发表于 2021-3-28 16:49
感谢提供的研究思路,学习了
7#
零点 发表于 2021-3-28 16:51
我想知道他是怎么做到DLL注入游戏还能使用皮肤的,正常来说是没法使用的
8#
 楼主| 舒默哦 发表于 2021-3-28 17:17 |楼主
零点 发表于 2021-3-28 16:51
我想知道他是怎么做到DLL注入游戏还能使用皮肤的,正常来说是没法使用的

注入游戏,要开启管理员权限
9#
jiangmikim 发表于 2021-3-28 19:10
很有帮助。谢谢。
10#
yyq45 发表于 2021-3-28 20:29
很全面 很详细 感谢大神的分析和分享~
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-15 08:31

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表