舒默哦 发表于 2021-3-28 15:20

【vc】【笔记】游戏逆向分析和商业挂研究

本帖最后由 舒默哦 于 2021-3-28 15:20 编辑

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

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

为了不把时间浪费在窗体和排版上,我分析了另一款商业挂:热血神器。简单说说热血神器,这是一款存在了将近10年时间的官方挂,期间更新了很多版本,到现在为止很少有BUG了,因此非常优秀,此外,这款外挂在窗体和排版方面,看起来令人舒适。
各种有限自动机的编写,比如自动任务、自动打怪等等,在打怪的自动机编写,自动机里有打怪->检测是否满足回城条件->捡物->检测加蓝加血等,回城时要牵涉物品去向问题的处理,物品的存取和买卖状态需要设计一个格式来保存到配置文件中,
配置文件的读取和保存处理起来比较繁杂,下面有章节会讲到。
当然,我本人能力有限,在找数据方面,有些东西还是模模糊糊的,有些数据找起来麻烦,我避重就轻,转而分析热血神器对同样的数据的处理办法。即使如此,有些数据找起来也是困难重重,比如卡墙打怪问题,
没分析明白,我把这些问题放到了最后,抛砖引玉。欢迎大家一起交流。

https://static.52pojie.cn/static/image/hrline/1.gif

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

https://static.52pojie.cn/static/image/hrline/1.gif

## 热血神器分析

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


https://static.52pojie.cn/static/image/hrline/4.gif

接下来分析热血神器里的文件:
窗口资源全在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
$ ==>   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 65angz$.06a8a9c97e
$+20      66 64 31 66 38 62 33 33 35 34 66 62 64 65 38 32fd1f8b3354fbde82
$+30      62 31 66 39 33 39 38 38 62 61 04 00 24 C5 B2 73b1f93988ba..$Å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来创建进程。

对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?参数似乎是端口。
模拟鼠标登录的代码:
char login = { 67,55,65,78,80, 71,85,79,88,73,65,78,71,90 };//账号
char passwdS = { 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 = "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, 0, 0, 0);
      keybd_event(login, 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, 0, 0, 0);
      keybd_event(passwdS, 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的格式,字符数组来搜索)
//地址的格式
$ ==>   00AE62A0client.&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
//小地图属性的对象地址
+0x230//名称


//这儿是0x627
+0x230//左上角坐标


https://static.52pojie.cn/static/image/hrline/2.gif


### 跨图算法

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


假如玩家要从百武关到泫勃派,热血神器的逻辑是先检测背包是否有泫勃派的回城符,有则直接用回城符,
如果没有,则要寻找去泫勃派的路径,百武关->神武门->柳正关->泫勃派,
然后根据ItemDate.dll里的#npc和#mapdate的数据,匹配坐标,之后调用寻路CALL,最终回到泫勃派。
CTP教程里寻找路径用到的是广度优先搜索算法,就是用一个递归函数实现全遍历,最终找到目标地图,
CTP用的是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)

      fori=1,mapnum do
                ifmapforfather.curmap == SonMap then
                        mapforfather.father=FatherMap
                end      
      end
      
end

--查询父节点
function QuryFatherNode()

      for i=1,mapnumdo
                print("子地图:",mapforfather.curmap,"父地图:",mapforfather.father)
      end

end

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

      fori=1,closenum do
                if closelist == Mapthen
                        return true
                end
      end
      return false
end

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

      ifQuryCloseMap(Map) == false then
                closenum=closenum +1
                closelist=Map
      end
end

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

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

SpanMapWayFinding("百武关","泫勃派")
--QuryFatherNode()
跨图寻路function SpanMapWayFinding(StartMap,EndMap)是一个递归函数,它的执行逻辑如下:

最终输出:


https://static.52pojie.cn/static/image/hrline/2.gif


### 模拟小地图功能

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

地图资源全在热血神器的Map文件里,下面贴出模拟小地图功能的代码:
//重写初始化函数
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:%dy:%d\n", point.x, point.y);
      LONG x = point.x;
      LONG y = point.y;
      if (y<321 && x<321)//在地图范围内才捕获坐标
      {
                char str = { 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 = { 0 };
      msgGetRoleCurXY(nXY);//获取X和y的坐标
      //DbgPrintMine("捕获坐标:%d   %d\n", nXY, nXY);
      int x = (nXY+2560)/ 16;
      int y = (2560-nXY) / 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);
}
输出结果:

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



## 人物穿墙

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


先把+1A6C添加到CE,点击小地图,观察目标x坐标的值,在拐弯时会改写,直线时这个值在途中不会变,查看什么地方改写了目标x坐标。
Client.exe+14E768:
0054E75C - D9 9E 701A0000- fstp dword ptr
0054E762 - D9 85 BCFEFFFF- fld dword ptr
- D9 9E 741A0000- fstp dword ptr <<
0054E76E - EB 16 - jmp Client.exe+14E786
0054E770 - 33 C0- xor eax,eax
在CE中搜到了其他数据,但是上面列出的才是关键数据。打开OD往上跟,查找数据的来源。


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


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


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

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


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


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

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

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

Client.exe+158117:
0055810B - 89 86 A4420000- mov ,eax
00558111 - 8B 96 A4420000- mov edx,
00558117 - C6 82 E4010000 01 - mov byte ptr ,01 <<
0055811E - D9 45 D8- fld dword ptr
00558121 - 8B 86 A4420000- mov eax,

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

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


https://static.52pojie.cn/static/image/hrline/5.gif


## 配置文件

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

热血神器把配置文件数据,放到Item.dll和ItemDate.dll里了,在自动打怪的有限状态机中,配置文件在回城买卖和回城寻路时会用到,比如哪些物品该卖、哪些物品保存、哪些物品过滤不管。
自动打怪的有限状态机,就是开启一个线程,写个死循环,循环里有自动打怪->拾取->检测回城条件->检测是否加血加蓝,当然顺序可以打乱。
代码如下:
//捡物、打怪、检测回城补给条件是否满足、检测是否加血加蓝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:%dg_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的数据。
//处理物品的结构体
typedef struct TGoodSManageOnSupply
{
      char szGoodsName;//物品名称
      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;//石头名称
      //union stonetype
      //{
      //      DWORD64 placeholder;//占位符
                DWORD value;//石头的数值
                DWORDCheckBoxID;//热血石编号ID热血石对象地址+D44偏移的位置,占4个字节
                BOOL   IsSelectSatus;//勾选状态
      //}u;
}_TStoneManageOnSupply;

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

配置文件的加载时机在初始化窗体时,可以创建一个线程函数来加载Item.dll和ItemDate.dll,如果不这样做,在加载数据过程中主线程会卡死,游戏会退出。
部分代码贴在下面:
//设置物品的拾取、卖还是存仓库的状态
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 = 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.insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
                g_cAutoPlay.map_GoodsSet = goodtemp;
                return TRUE;
      }

      strcpy_s(goodtemp.szGoodsName, strlist1);
      int premark = 0;
      char tempd = { 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 = 0;
                }
                else
                {
                        tempd = 1;
                }
                if (backmark == -1)
                {
                        break;
                }
                ++j;
      } while (true);
      goodtemp.GoodsFlag_GODEPOT = tempd;
      goodtemp.GoodsFlag_GOSHOP = tempd;
      goodtemp.GoodsFlag_NOPICK = tempd;
      //g_cAutoPlay.map_GoodsSet.insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
      g_cAutoPlay.map_GoodsSet = 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 = 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.insert(map<int, _TGoodSManageOnSupply>::value_type(k++, goodtemp));
               
                g_cAutoPlay.map_StoneSet[++i] = 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 = 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 = goodtemp;

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

      return TRUE;
}



//读取配置文件信息 初始化物品设置的界面
BOOL CPageMainTab::RGoodsAndTone_ReadConfigDataForFile(const char* szpConfigFile)
{
      fstream fs;
      //char szpLine;
      DWORD buffersize = 1024 * 1024;
      char* szpLine = new char;
      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;
      //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;
}

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

      //宠物药;人物药;弓手箭;首饰;甲手鞋;衣服;武器;制作;其它物品;
      //种类下拉列表初始化
      map<int, TGoodSManageOnSupply>::iterator iter_good;// map_GoodsSet;
      for (int i = 0; i < STONEMAPNUM; i++)
      {
                iter_good = g_cAutoPlay.map_GoodsSet.begin();
                if (iter_good == g_cAutoPlay.map_GoodsSet.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.begin();
      for (int i = 0; i < STONEMAPNUM; i++)
      {
                iter = g_cAutoPlay.map_StoneSet.begin();
                if (iter == g_cAutoPlay.map_StoneSet.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.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;
}



加载后的数据:




https://static.52pojie.cn/static/image/hrline/2.gif


回城补给,物品买卖或者保存,就可以依照map散列表里储存的数据来具体处理了。
### 个人配置文件
个人配置文件就是点击应用设置时,会生成的个人玩家当前辅助设置的信息,保存以玩家命名的文本文件。
下面分析燃情骚男z.txt里的数据。

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

RxSItem表示的是热血石,StoneItem包括宠物石头、普通石头、奇遇石等等,这些数据在石头设置中。
GoodItem是物品列表,这些数据只是在物品设置中的部分数据。
点击应用设置时,代码如下:
//保存配置文件
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;//物品设置状态改变时,保存的副本
      for (int i = 0; i < STONEMAPNUM; i++)
      {
                iter_mapgoods = g_cAutoPlay.map_GoodsSet_Copy.begin();
                if (iter_mapgoods == g_cAutoPlay.map_GoodsSet_Copy.end())
                {
                        DbgPrintMine("物品设置列表空:%d\r\n", i);
                        continue;
                }
                for (; iter_mapgoods != g_cAutoPlay.map_GoodsSet_Copy.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;//石头设置
      DbgPrintMine("石头设置列表");
      strCfg += "StoneItem=";
      for (int i = 0; i < STONEMAPNUM; i++)
      {
                iter_mapstone = g_cAutoPlay.map_StoneSet.begin();
                if (iter_mapstone == g_cAutoPlay.map_StoneSet.end())
                {
                        DbgPrintMine("物品设置列表空:%d\r\n", i);
                        continue;
                }
                char strvalue;
                if (strcmp(iter_mapstone->second.szStoneName,"#heatstone") == 0)
                {//热血石 RxSItem=
                        strCfg += "\n";
                        strCfg += "RxSItem=";
                        do
                        {
                              ++iter_mapstone;
                              if (iter_mapstone == g_cAutoPlay.map_StoneSet.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.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;
}


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


https://static.52pojie.cn/static/image/hrline/5.gif

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

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



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

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


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

第二条数据
Client.exe+1433BA:
005433AF - 8D 04 80   - lea eax,
005433B2 - 0FB6 8C 83 A9240000- movzx ecx,byte ptr
005433BA - 89 4A 14- mov ,ecx <<
005433BD - 8B 83 E0250000- mov eax,
005433C3 - 8D 84 80 29090000- lea eax,
从第二条数据往回溯,找到无论是在石头中或者正常打怪的地方都能够断下的地方。
最终找到下面这个位置。



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


### 土灵符问题

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


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



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


//保存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开始,依次类推)

$ ==>   100C0000nvd3dum.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:

mov eax,0x4FD7F0
call eax
add esp,0x1C
实验:用以上代码注入游戏,地址保存成功了。


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

//移动到 保存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:
mov eax,0x4FD7F0
call eax
add esp,0x10

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


注入之后移动成功:

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


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

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

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

https://static.52pojie.cn/static/image/hrline/5.gif


## 结语

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

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

很多学外挂的都拿热血江湖来练手,包括我:lol

skynet996 发表于 2021-3-30 09:23

膜拜大神, 这帖子分析开阔了很多思路

淡灬看夏丶恋雨 发表于 2021-3-30 07:38

太牛逼了。

lyl610abc 发表于 2021-3-28 15:34

商业挂研究× 牢房直通车√{:301_978:}
楼主太强了,分析得很透彻,学习了{:1_893:}

52changew 发表于 2021-3-28 15:59

大佬; 厉害; 膜拜膜拜; 学习学习下; 谢谢 分享!

lsy832 发表于 2021-3-28 16:12

帖子先收藏 慢慢品

w2pj123 发表于 2021-3-28 16:49

感谢提供的研究思路,学习了{:1_921:}

零点 发表于 2021-3-28 16:51

我想知道他是怎么做到DLL注入游戏还能使用皮肤的,正常来说是没法使用的

舒默哦 发表于 2021-3-28 17:17

零点 发表于 2021-3-28 16:51
我想知道他是怎么做到DLL注入游戏还能使用皮肤的,正常来说是没法使用的

注入游戏,要开启管理员权限

jiangmikim 发表于 2021-3-28 19:10

很有帮助。谢谢。:victory:

yyq45 发表于 2021-3-28 20:29

很全面 很详细 感谢大神的分析和分享~
页: [1] 2 3 4 5 6 7 8 9 10
查看完整版本: 【vc】【笔记】游戏逆向分析和商业挂研究