本帖最后由 舒默哦 于 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个月时间,用了两张至尊热血符。关于源代码,我不会发出来,有些不法分子会用来获取商业利益,再有本来就是技术交流贴,目的是提高自己逆向分析和代码编写能力。共勉! |