技术逆向32位逆向
ZEROKO1432位游戏逆向经验
口袋西游实战
数组:追背包数量
背包小血药地址:2A935D64
小血药数量地址=[[eax+ebx*4]+14]
此处继续追eax,ebx==5表示背包中第六个格子
1
| 0048580F |. 8B7424 1C mov esi, dword ptr [esp+1C] ; 小血药数量地址=[[[[esp+1c]+C]+5*4]+14]
|
小血药数量地址=[[[[ecx+AD8]+C]+5*4]+14]
1
| 006522D1 |. 8B4C85 1C |mov ecx, dword ptr [ebp+eax*4+1C] ; 小血药数量地址=[[[[[[ebp+eax*4+1C]+8]+28]+0AD8]+C]+5*4]+14
|
此处eax为某种数组
小血药数量地址=[[[[[[[[[[ecx+1c]+68]+4]+8]+14+1C]+8]+28]+0AD8]+C]+54]+14
ecx==00D11A50(是个基地址)
万能控件call
背包开关状态内存地址:
14411820
改写代码:
008DC7FE - C6 86 90000000 01 - mov byte ptr [esi+00000090],01
008DC83B - C6 86 90000000 00 - mov byte ptr [esi+00000090],00
调用堆栈: 主线程
地址 堆栈 函数过程 / 参数 调用来自 结构
0019EFD8 00903DA3 ? ELEMENTC.008DC600 ELEMENTC.00903D9E
万能控件call位置
0062968E > \53 push ebx ; a 001E0001 s 001f0001 d 00200001 i 00170001 o 00180001 b 002F0001
0062968F . 55 push ebp ; 41大写A键码 61小写a键码
00629690 . 50 push eax ; 00000100按下标志 102弹起标志
00629691 . 8BCE mov ecx, esi ; 24B76E98 里:00BF84AC 1563B300(追上去疑似二叉树)
00629693 . E8 58000000 call 006296F0 ; 万能控件call
万能控件call
push 002F0001
push 42
push 100
mov ecx,24B76E98
call 006296F0
push 002F0001
push 42
push 102
mov ecx,24B76E98
call 006296F0
00E76804
创建带对话框的MFC DLL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| CMydllApp theApp; MainDialog a;
void WINAPI ShowDialog() { AFX_MANAGE_STATE(AfxGetStaticModuleState()); OutputDebugStringA("测试,Mydll准备显示窗口"); a.DoModal(); OutputDebugStringA("测试,Mydll准备退出自身dll"); FreeLibraryAndExitThread(theApp.m_hInstance,123456); }
BOOL CMydllApp::InitInstance() { CWinApp::InitInstance(); OutputDebugStringA("测试,Mydll在运作创建线程"); ::CreateThread(0, 0, (LPTHREAD_START_ROUTINE)ShowDialog, 0, 0, 0); return TRUE; }
|
幻想神域
追血量数据来源找人物对象数组
CE寻到血量地址为:15A0D808 000001F4
下访问断,打开人物对话框,断下位置:
1 2
| 009A389E 8378 08 00 cmp dword ptr ds:[eax+0x8],0x0 ; 血量地址==eax+0x8
|
向上追:
追到此处发现:

血量地址==[eax+0c]+0x8,这里的eax为call 00665847的返回值,追参数来源如图
代码注入器如下代码可获得血量
1 2 3 4 5 6 7 8
| mov ecx,[0F84B74] mov eax,[ecx+40c] add ecx,410 push eax call 00665870 mov eax,[eax+0c] mov eax,[eax+0x8] mov [00178004],eax
|
00178004地址确实是血的结果如下:

继续进该call追,发现问题所在

ebp-4表示局部变量,局部变量在本层call中未改变,说明ebp-4来源于内部call中,经单步观察是来源于0066588b地址这个call中。内部call想改变外部call只可能是通过指针的方式传进去内部call改变
简单分析:

call内继续往上追:

追到一个循环结构,应该是某种数据结构。然后发现该循环结构只是决定于ebx套几层循环,事实上是ebx不做任何改变也能dd出血地址,因此直接略过该循环结构。

1
| 008A555B |. 8B5CD8 04 mov ebx, dword ptr [eax+ebx*8+4] ; 血量地址==[[[eax+ebx*8+4]+0c]+0c]+8==15A0D808
|
此时,ebx为数组下标,此处ebx==4E或ebx==34都可以dd出血地址,此处的ebx并不是连续的,因此此处的ebx疑似加密的数组下标
一路向上追,最终结果是:
$$
血量地址==[[[[[0F84B74]+424]+34*8+4]+0c]+0c]+8
$$
回到前面的ebx出继续追加密数组下标来源:
1
| 加密数组下标EBX==([[[0F84B74]+40C]*41+[[0F84B74]+40C]/4+9e3779b9]&[[0F84B74]+410+20])*2
|
因为血量地址==[[[[[0F84B74]+424]+34*8+4]+0c]+0c]+8
通过ce设置区间搜索发现其他数据结构分析:

对象+8 ID
[对象+C]+8 血量
[对象+C]+10 当前等级
[对象+C]+18 移动速度
[对象+C]+24 最大血量
[对象+C]+30 暴击伤害
[对象+C]+34 命中率
[对象+C]+38 治疗比例
[对象+C]+68 经验
[对象+C]+27C 剩余攻击点数
[对象+C]+280 剩余防御点数
[对象+10]+154 X
[对象+10]+158 Y
[对象+10]+15C Z
读取人物信息代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| __try { DWORD 人物对象 = 0; __asm { mov ecx, 0x0F84B74 mov ecx, [ecx] mov eax, [ecx + 0x40c] add ecx, 0x410 push eax mov eax, 0x00665870 call eax mov 人物对象, eax } DWORD 人物属性 = *(DWORD *)(人物对象 + 0x0c); d血量 = *(DWORD *)(人物属性 + 0x8); d最大血量 = *(DWORD *)(人物属性 + 0x24); d当前等级 = *(DWORD *)(人物属性 + 0x10); DWORD 坐标信息 = *(DWORD *)(人物对象 + 0x10); fX = *(FLOAT *)(坐标信息 + 0x154); fY = *(FLOAT *)(坐标信息 + 0x158); fZ = *(FLOAT *)(坐标信息 + 0x15c); } __except (1) { Call_输出调试信息("幻想神域 读取人物信息出错!"); }
|
遍历数组
因为数组为EBX*8,可知这个数组每项隔8
分析可知,[[0F84B74]+424为数组头指针
数组的开始结束标志位一般是数组头指针的+4偏移地址里存的数组尾,od中观察确实[[0F84B74]+424与[[0F84B74]+428地址中存的是比较接近的一个地址。
发现部分情况下数组当前项对象有时候会为0,if判断跳过这些情况
代码如下:
1 2 3 4 5 6
| T数组信息遍历 t; t.初始化对象信息(); for (int i = 0;i<(int)t.d数量;i++) { Call_输出调试信息("幻想神域 血量:%d/%d 当前等级:%d 坐标:(%f,%f,%f)", t.对象信息[i].d血量, t.对象信息[i].d最大血量, t.对象信息[i].d当前等级, t.对象信息[i].fX, t.对象信息[i].fY, t.对象信息[i].fZ); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void T数组信息遍历::初始化对象信息() { __try { DWORD 基地址中 = *(DWORD*)0x0F84B74; DWORD 数组头 = *(DWORD*)(基地址中 + 0x424); DWORD 数组尾 = *(DWORD*)(基地址中 + 0x428); d数量 = (数组尾 - 数组头)/8; int sum=0; for (int i = 0;i<d数量;i++) { DWORD 当前数组项= *(DWORD*)(数组头 + i*8+4); DWORD 当前对象= *(DWORD*)(当前数组项 +0x0c); if (当前对象!=0) { sum++; DWORD 人物属性 = *(DWORD *)(当前对象 + 0x0c); 对象信息[i].d血量 = *(DWORD *)(人物属性 + 0x8); 对象信息[i].d最大血量 = *(DWORD *)(人物属性 + 0x24); 对象信息[i].d当前等级 = *(DWORD *)(人物属性 + 0x10); DWORD 坐标信息 = *(DWORD *)(当前对象 + 0x10); 对象信息[i].fX = *(FLOAT *)(坐标信息 + 0x154); 对象信息[i].fY = *(FLOAT *)(坐标信息 + 0x158); 对象信息[i].fZ = *(FLOAT *)(坐标信息 + 0x15c); } } d数量 = sum; } __except (1) { Call_输出调试信息("幻想神域 数组信息遍历出错!"); } }
|
调试效果如下:

无法确定这个数组是存的什么数据。。。
追血量来源找对象结构链表
上面是打开人物属性对话框断下的位置,此处通过选中自身访问自己的血量来断下血量数据内存上下的访问断。因为选中单位这个cal涉及到所有对象,因此t通过过这个找到的血量基地址加偏移应该蕴含对象结构的关系。
依然是先用ce找到血量地址:307CD808 000003B1
断下好几个位置,反复运行发现只要一直选中自己就一直断下一个位置上,即此处:
1
| 00669A1A |. DB40 08 fild dword ptr [eax+8] ;
|
往上追到了一个数据结构处,如下:

可以发现这是一个链表,循环多少次决定了要把esi换成几层[esi]。
在未进循环之前dd每种情况发现本人的血地址只要套一层[esi]即可跳出循环,
继续向上追得到基地址加偏移:
$$
血地址==[[[[[[0F84B74]+410+8]]]+0c]+0c]+8
$$
分析结构:
$$
[[[0F84B74]+410+8]]=链表头
$$
链表入口套几层[],代表不同的对象,其中[链表头]代表本人对象
找出口分析如下:

$$
当前链表=[[[0F84B74]+410+8]]套几层循环==[[0F84B74]+410+8]的时候跳出循环
$$
遍历链表代码如下:
调用:
1 2 3 4 5 6
| T链表信息遍历 t; t.初始化对象信息(); for (int i = 0; i < (int)t.d数量; i++) { Call_输出调试信息("幻想神域 血量:%d/%d 当前等级:%d 坐标:(%f,%f,%f)", t.对象信息[i].d血量, t.对象信息[i].d最大血量, t.对象信息[i].d当前等级, t.对象信息[i].fX, t.对象信息[i].fY, t.对象信息[i].fZ); }
|
结构:
1 2 3 4 5 6
| struct T链表信息遍历 { T人物信息 对象信息[1000]; DWORD d数量; void 初始化对象信息(); };
|
结构中初始化函数的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| void T链表信息遍历::初始化对象信息() { __try { DWORD 基地址中 = *(DWORD*)0x0F84B74; DWORD tmp = *(DWORD*)(基地址中 + 0x418); DWORD 链表头 = *(DWORD*)(tmp); DWORD 当前链表 = *(DWORD*)(tmp); int i=0; for (; 当前链表!=tmp; i++) { DWORD 当前对象 = *(DWORD*)(当前链表+0x0c);
DWORD 人物属性 = *(DWORD *)(当前对象 + 0x0c); 对象信息[i].d血量 = *(DWORD *)(人物属性 + 0x8); 对象信息[i].d最大血量 = *(DWORD *)(人物属性 + 0x24); 对象信息[i].d当前等级 = *(DWORD *)(人物属性 + 0x10); DWORD 坐标信息 = *(DWORD *)(当前对象 + 0x10); 对象信息[i].fX = *(FLOAT *)(坐标信息 + 0x154); 对象信息[i].fY = *(FLOAT *)(坐标信息 + 0x158); 对象信息[i].fZ = *(FLOAT *)(坐标信息 + 0x15c);
当前链表 = *(DWORD*)(当前链表); } d数量 = i; } __except (1) { Call_输出调试信息("幻想神域 数组信息遍历出错!"); } }
|
调试输出:(很明显是生物链表,包含自己)

找名称
中文会有不同的编码问题,但是若起名为英文字母+数字,则只需要看ASCII和UNICODE来判断。
我们的名称本身是英文+数字的组合,因此直接上我们找到的对象属性中去寻找。
od中dd [[[[[[0F84B74]+410+8] ] ]+0c]+0c](链表中的人物属性),直接发现了自己的名字

由此确定:
$$
名称地址=[[[[[[0F84B74]+410+8]]]+0c]+0c]+100
$$
因此: [对象+C]+100表示对象名称
在找到的名称处下访问断,断下的地方观察跳转,编译器会为字符串提供一个区间,字符串长度大于该区间则存为指针,若小于等于则直接存名字内容
名字下访问断断下分析:

由上图分析得知 [对象+C]+114表示对象名称长度
编写代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13
| DWORD 名称长度 = *(DWORD *)(人物属性 + 0x114); if (名称长度 <= 0x10) { 对象信息[i].p名字 = (char *)(人物属性 + 0x100); strcpy(对象信息[i].cGBK名字, 对象信息[i].p名字); BIG52GBK(对象信息[i].cGBK名字); } else { 对象信息[i].p名字 = (char*)*(DWORD *)(人物属性 + 0x100); strcpy(对象信息[i].cGBK名字, 对象信息[i].p名字); BIG52GBK(对象信息[i].cGBK名字); }
|
效果:

不知道为什么区域恶霸显示是这样的?可能是%s 放前面的原因,放在后面就没问题了。
将id也打印出来,效果如下:

追背包中物品数量找背包数据结构二叉树
找背包数据结构二叉树
先用ce找到背包第六格小血瓶数量
4字节搜到:111E18C0,每当打开背包,更改的数值被重置。根据地址下访问断,断下位置向上追,追到结果为如下:
我的人物对象==[[[[0F84B74]+410+8] ]+0c]
[[[[[[[[我的人物对象+0C]+3C0]+4]+4]+10]+8]+ecx4]+28]==背包物品数量
$$
word\ ptr\ [[[[[[[[[[[[0F84B74]+410+8]]+0c]+0C]+3C0]+4]+4]+10]+8]+ecx4]+28]==我的背包物品数量
$$
上面式子中,ecx代表背包格子序号,由零开始。
但并没有发现二叉树结构
进行分析,每当打开背包或关闭背包,更改的数值被修正,这种修正只有两种可能,第一种可能就是被本地数据修正,第二种可能是被服务器修正,而正常的一个游戏打开背包关闭背包不应该发送封包的,因此极大概率是本地修正,若是本地修正也说明我们现在找到的只是个假地址,还有一个真正的地址是用来存本地真正的背包数量的。因此我们要找到本地修正的那个地址。由于4字节搜索只搜到一个,因此尝试搜索其他类型。
搜索二字节找到两个数据,有一个和前面找到那个是同一个地址,即那个假地址,因此排除。另外一个地址更改其中数值发现不会影响开关背包数据。
改地址为:212517A8;
下word访问断点,只断到此处:
1
| 009602EA |. 0FB756 28 ||movzx edx, word ptr [esi+28] ; [esi+28]==背包物品数量 esi+28==背包物品数量地址
|
向上追一条追到数组结构:

此处又发现了数组结构,并且通过执行断点,发现数组结构中的下标ecx是分段式连续的,从0-1d,0-13。从1d进入下一段的时候eax发生变化,eax有两种情况。结合游戏:

游戏自带的基本背包格子数是30个,16进制为1e,0-1d正好是1e个序号,而现在身上只装了如图的一个背包,该背包能提供20个额外的格子,16进制为14,也正好对应0-13的序号范围。这样也就看出来了此时这里是两个数组,ecx代表背包内的格子序号,而eax代表背包对象,相当于有几个背包则有几个背包格子数组。eax遍历每个背包,ecx遍历背包中每个格子,继续向上追背包对象的来源。
追此数据发现直接是追上次数据的中间位置。
继续向上追:

追到此处发现:eax来源于图中的call,该call的esi参数断下后传入的依次是0-5,结合游戏有5个妆效背包的位置和原有的默认初始提供的30格空间,相当于6个背包,正好是0-5表示序号。即可确定该call是背包对象的来源。进入:

call中除了调了一个call外啥也没做,并且该call的eax参数依然是装背包的位置的格子序号,显然要继续进call:

继续逆向,发现追到[ebp-4],而[ebp-4]来源于圈起来的call中。断下多次dd发现[ebp-4]只有两种变化,正对应现在只带了一个额外背包的情况,因此要继续进call追。
[[[[0F84B74]+410+8] ]+0c]
[[[[[[[[0F84B74]+410+8] ]+0c]+0C]+3C0]+4]+4]为二叉树根节点
[[[[[[[[0F84B74]+410+8] ]+0c]+0C]+3C0]+4]+4]为二叉树根节点,+0]为左子树,+8]为右子树
跳出二叉树的标志位为:byte ptr [[[[[[[[[0F84B74]+410+8] ]+0c]+0C]+3C0]+4]+4]+15],当标志位不等于0时表示该二叉树没有子项了。
背包序号地址=二叉树节点+C
每个二叉树背包节点下挂个物品格子数组:
物品格子数组的开始地址=[二叉树节点+10]+8
物品格子数组的结束地址=[二叉树节点+10]+0c
物品格子数组项地址=[[二叉树节点+10]+8]+ecx*4(ecx表示格子序号)
物品数量地址(word)= [物品格子数组项]+28
物品最大数量地址(word)= [物品格子数组项]+2A
[[[[[[[[[[[[0F84B74]+410+8] ]+0c]+0C]+3C0]+4]+4]+10]+8]+ecx*4]+28]== 初始背包物品数量
代码如下:
结构代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| struct T背包信息 { DWORD dID; DWORD d数量; DWORD d最大数量; DWORD d位置; DWORD d所在背包; }; struct T遍历背包信息 { T背包信息 背包列表[1000]; DWORD d数量; void 初始化背包信息(); void 递归遍历二叉树信息(DWORD); };
|
函数实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| int g_背包列表下标=0; void T遍历背包信息::初始化背包信息() { __try { g_背包列表下标 = 0; T人物信息 me; me.初始化人物信息(); DWORD 本人对象= *(DWORD*)(me.d对象 +0x0c); DWORD tmp = *(DWORD*)(本人对象 + 0x3c0); tmp = *(DWORD*)(tmp + 0x4); DWORD 背包二叉树根节点 = *(DWORD*)(tmp + 0x4); 递归遍历二叉树信息(背包二叉树根节点); d数量 = g_背包列表下标; } __except (1) { Call_输出调试信息("幻想神域 背包二叉树出错!"); } } void T遍历背包信息::递归遍历二叉树信息(DWORD 背包二叉树根节点) { BYTE 标志位= *(BYTE*)(背包二叉树根节点 + 0x15); DWORD 左节点 = *(DWORD*)(背包二叉树根节点 + 0x0); DWORD 右节点 = *(DWORD*)(背包二叉树根节点 + 0x8); if (标志位==0) { DWORD 背包对象= *(DWORD*)(背包二叉树根节点 + 0x10); DWORD 背包序号 = *(WORD*)(背包二叉树根节点 + 0x0C); DWORD 数组开始地址= *(DWORD*)(背包对象 + 0x8); DWORD 数组结束地址 = *(DWORD*)(背包对象 + 0x0c); DWORD 数组长度 = (数组结束地址 - 数组开始地址) / 4; d数量 = 数组长度; Call_输出调试信息("幻想神域 背包序号:%d 背包对象:%X 背包长度:%d\r\n ", 背包序号, 背包对象, d数量); for (int i = 0;i<数组长度;i++) { DWORD 物品信息 = *(DWORD*)(数组开始地址 + i * 0x4); DWORD ID=*(DWORD*)(物品信息); if (ID!=0) { 背包列表[g_背包列表下标].dID = ID; 背包列表[g_背包列表下标].d数量 = *(WORD*)(物品信息 + 0x28); 背包列表[g_背包列表下标].d最大数量 = *(WORD*)(物品信息 + 0x2a); 背包列表[g_背包列表下标].d所在背包 = 背包序号; 背包列表[g_背包列表下标].d位置 = i; g_背包列表下标++; } } 递归遍历二叉树信息(左节点); 递归遍历二叉树信息(右节点); } }
|
调用代码:
1 2 3 4 5 6
| T遍历背包信息 t; t.初始化背包信息(); for (int i = 0; i < (int)t.d数量; i++) { Call_输出调试信息("幻想神域 物品id:%x 物品数量:%d/%d 所在背包序号:%d 位置序号:%d", t.背包列表[i].dID, t.背包列表[i].d数量, t.背包列表[i].d最大数量, t.背包列表[i].d所在背包, t.背包列表[i].d位置); }
|
分析物品名称
物品遍历以后未发现物品名称,因此还需要单独找物品名称。
由于游戏是繁体字,并且是BIG5编码
打开big5转换工具
搜狗拼音输入法设置为繁体通用,输入如下

然后点击GBK到BIG5 ,变为如下图:

1
| BB B4 AB AC 48 50 C3 C4 A4 F4 00
|
用CE字节集扫描搜索:BB B4 AB AC 48 50 C3 C4 A4 F4 00(00结尾的目的是不让他跟别的字符串)

找到5个结果,逐个修改,发现只有一个有效,即地址为:0A2536FC
并且发现更改了名字后所有同id的物品名字都变了,这说明很有可能有一个物品库,用物品id获取物品名字。od附近,dd 09E536FC,下硬件访问断点。
断在线标明位置:

因为是硬件断,所以实际的物品名称为[esi+ecx*4-8],此处断下时ecx==2,追esi。
因为是字符串,尝试跳出最底层堆栈。

找到此处为该call第三个参数,跳过去。
继续分析:找到字符串处理分叉:

继续向上追,追eax追到eax来源于这个call,发现这个call的参数是物品id,可以直接调用这个call获取eax返回值,该call应该是通过物品id取物品对象的call。

该call类似于通过id汉化物品名字

代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| void T遍历背包信息::递归遍历二叉树信息(DWORD 背包二叉树根节点) { BYTE 标志位= *(BYTE*)(背包二叉树根节点 + 0x15); DWORD 左节点 = *(DWORD*)(背包二叉树根节点 + 0x0); DWORD 右节点 = *(DWORD*)(背包二叉树根节点 + 0x8); if (标志位==0) { DWORD 背包对象= *(DWORD*)(背包二叉树根节点 + 0x10); DWORD 背包序号 = *(WORD*)(背包二叉树根节点 + 0x0C); DWORD 数组开始地址= *(DWORD*)(背包对象 + 0x8); DWORD 数组结束地址 = *(DWORD*)(背包对象 + 0x0c); DWORD 数组长度 = (数组结束地址 - 数组开始地址) / 4; d数量 = 数组长度; Call_输出调试信息("幻想神域 背包序号:%d 背包对象:%X 背包长度:%d\r\n ", 背包序号, 背包对象, d数量); for (int i = 0;i<数组长度;i++) { DWORD 物品信息 = *(DWORD*)(数组开始地址 + i * 0x4); DWORD ID=*(DWORD*)(物品信息); if (ID!=0) { 背包列表[g_背包列表下标].dID = ID; 背包列表[g_背包列表下标].d数量 = *(WORD*)(物品信息 + 0x28); 背包列表[g_背包列表下标].d最大数量 = *(WORD*)(物品信息 + 0x2a); 背包列表[g_背包列表下标].d所在背包 = 背包序号; 背包列表[g_背包列表下标].d位置 = i; DWORD 名称对象; DWORD temp; 背包列表[g_背包列表下标].p名字 = ""; __asm { mov ecx,0x1789168 mov ecx,[ecx] mov eax,[ecx] push ID mov edx,[eax+0x0A8] call edx mov 名称对象,eax } DWORD 名称长度 = *(DWORD*)(名称对象 + 0x0F8 + 0x018); if (名称长度>=0x10) { temp = *(DWORD*)(名称对象 + 0x0fc); 背包列表[g_背包列表下标].p名字 = (char*)temp;
} else { 背包列表[g_背包列表下标].p名字 =(char*)(名称对象 + 0x0fc); } strcpy(背包列表[g_背包列表下标].cGBK名字, 背包列表[g_背包列表下标].p名字); BIG52GBK(背包列表[g_背包列表下标].cGBK名字); g_背包列表下标++; } } 递归遍历二叉树信息(左节点); 递归遍历二叉树信息(右节点); } }
|
找技能数据结构
通过技能cd来找技能结构。
找一个比较长cd的技能,使用后cd开始倒计时用ce先搜未知初始值,再用ce搜减少的。

显然,比对游戏内显示,2D3132B0最像CD的秒数。
cd结束后发现该位置被清空Nan。
1
| 00ABF638 |. D851 10 ||fcom dword ptr [ecx+10] ; 断在此处,ecx疑是进入cd的技能对象,当没有技能处在cd中时该代码不执行
|
向上追,追到链表结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| 00ABF5C0 /$ 55 push ebp ; [[[[[17EA2B0]+30]+4+0c*0+4]]+10]+10==进入cd的技能的冷却时间的地址 00ABF5C1 |. 8BEC mov ebp, esp ; [[[[17EA2B0]+30]+4+0c*0+4]]表示技能链表头 +4]取下一个技能 +10]+10表示cd倒数秒数的地址 00ABF5C3 |. 83EC 1C sub esp, 1C 00ABF5C6 |. 53 push ebx 00ABF5C7 |. 56 push esi 00ABF5C8 |. 57 push edi 00ABF5C9 |. 8D59 04 lea ebx, dword ptr [ecx+4] ; [[[ecx+4+0c*n+4]]+10]+10==进入cd的技能的冷却时间的地址 当前链表项目==[[[17EA2B0]+30]+4+n*0c+4]时候跳出链表 00ABF5CC |. C745 FC 070000>mov dword ptr [ebp-4], 7 00ABF5D3 |> 837B 08 00 /cmp dword ptr [ebx+8], 0 ; 这个循环可忽略,因为不变的eax已经在其内了 00ABF5D7 |. 0F84 E2000000 |je 00ABF6BF 00ABF5DD |. 8B43 04 |mov eax, dword ptr [ebx+4] ; [[[ebx+4]]+10]+10==进入cd的技能的冷却时间的地址 00ABF5E0 |. 8B38 |mov edi, dword ptr [eax] ; [[eax]+10]+10==进入cd的技能的冷却时间的地址 此处两个技能cd状态eax不变化 00ABF5E2 |. 8BF3 |mov esi, ebx 00ABF5E4 |. 897D F8 |mov dword ptr [ebp-8], edi ; [edi+10]+10==进入cd的技能的冷却时间的地址 [[edi+4]+10]+10==进入cd的技能的冷却时间的地址 00ABF5E7 |. 8975 F4 |mov dword ptr [ebp-C], esi 00ABF5EA |. 8D9B 00000000 |lea ebx, dword ptr [ebx] 00ABF5F0 |> 85F6 |/test esi, esi 00ABF5F2 |. 8B4B 04 ||mov ecx, dword ptr [ebx+4] ; 当前链表项目==[ebx+4]时候跳出链表 00ABF5F5 |. 894D F0 ||mov dword ptr [ebp-10], ecx ; 当前链表项目==ecx时候跳出链表 00ABF5F8 |. 74 04 ||je short 00ABF5FE 00ABF5FA |. 3BF3 ||cmp esi, ebx 00ABF5FC |. 74 05 ||je short 00ABF603 00ABF5FE |> E8 28AE95FF ||call 0041A42B 00ABF603 |> 3B7D F0 ||cmp edi, dword ptr [ebp-10] ; 当前链表项目==[ebp-10]时候跳出链表 00ABF606 |. 0F84 B3000000 ||je 00ABF6BF 00ABF60C |. 85F6 ||test esi, esi 00ABF60E |. 75 05 ||jnz short 00ABF615 00ABF610 |. E8 16AE95FF ||call 0041A42B 00ABF615 |> 3B7E 04 ||cmp edi, dword ptr [esi+4] 00ABF618 |. 75 05 ||jnz short 00ABF61F 00ABF61A |. E8 0CAE95FF ||call 0041A42B 00ABF61F |> 8B4F 10 ||mov ecx, dword ptr [edi+10] ; [edi+10]+10==进入cd的技能的冷却时间的地址 此处edi还是多技能在走这代码 00ABF622 |. 85C9 ||test ecx, ecx 00ABF624 |. 75 10 ||jnz short 00ABF636 00ABF626 |. 8D4D F4 ||lea ecx, dword ptr [ebp-C] ; 不断 00ABF629 |. E8 5212CEFF ||call 007A0880 00ABF62E |. 8B7D F8 ||mov edi, dword ptr [ebp-8] 00ABF631 |. 8B75 F4 ||mov esi, dword ptr [ebp-C] 00ABF634 |.^ EB BA ||jmp short 00ABF5F0 ; 循环尾部1 00ABF636 |> D9EE ||fldz 00ABF638 |. D851 10 ||fcom dword ptr [ecx+10] ; 断在此处,ecx疑是进入cd的技能对象,当没有技能处在cd中时该代码不执行 [ecx+10]==进入cd的技能的冷却时间 00ABF63B |. DFE0 ||fstsw ax 00ABF63D |. F6C4 01 ||test ah, 1 00ABF640 |. 75 28 ||jnz short 00ABF66A 00ABF642 |. 51 ||push ecx 00ABF643 |. DDD8 ||fstp st 00ABF645 |. E8 D64695FF ||call 00413D20 00ABF64A |. 83C4 04 ||add esp, 4 00ABF64D |. 8D4D F4 ||lea ecx, dword ptr [ebp-C] 00ABF650 |. E8 2B12CEFF ||call 007A0880 00ABF655 |. 57 ||push edi 00ABF656 |. 56 ||push esi 00ABF657 |. 8D55 E4 ||lea edx, dword ptr [ebp-1C] 00ABF65A |. 52 ||push edx 00ABF65B |. 8BCB ||mov ecx, ebx 00ABF65D |. E8 5E60DEFF ||call 008A56C0 00ABF662 |. 8B7D F8 ||mov edi, dword ptr [ebp-8] 00ABF665 |. 8B75 F4 ||mov esi, dword ptr [ebp-C] 00ABF668 |.^ EB 86 ||jmp short 00ABF5F0 ; 循环尾部2 00ABF66A |> D941 0C ||fld dword ptr [ecx+C] 00ABF66D |. D841 08 ||fadd dword ptr [ecx+8] 00ABF670 |. D945 08 ||fld dword ptr [ebp+8] 00ABF673 |. D8D1 ||fcom st(1) 00ABF675 |. DFE0 ||fstsw ax 00ABF677 |. F6C4 01 ||test ah, 1 00ABF67A |. 75 09 ||jnz short 00ABF685 00ABF67C |. DDD9 ||fstp st(1) 00ABF67E |. DDD8 ||fstp st 00ABF680 |. D951 10 ||fst dword ptr [ecx+10] 00ABF683 |. EB 05 ||jmp short 00ABF68A 00ABF685 |> DEE9 ||fsubp st(1), st 00ABF687 |. D959 10 ||fstp dword ptr [ecx+10] 00ABF68A |> D851 10 ||fcom dword ptr [ecx+10] 00ABF68D |. DFE0 ||fstsw ax 00ABF68F |. F6C4 41 ||test ah, 41 00ABF692 |. 75 16 ||jnz short 00ABF6AA 00ABF694 |. D959 10 ||fstp dword ptr [ecx+10] 00ABF697 |. 8D4D F4 ||lea ecx, dword ptr [ebp-C] 00ABF69A |. E8 E111CEFF ||call 007A0880 00ABF69F |. 8B7D F8 ||mov edi, dword ptr [ebp-8] 00ABF6A2 |. 8B75 F4 ||mov esi, dword ptr [ebp-C] 00ABF6A5 |.^ E9 46FFFFFF ||jmp 00ABF5F0 ; 循环尾部3 00ABF6AA |> 8D4D F4 ||lea ecx, dword ptr [ebp-C] ; ecx+4==ebp-8 [[[ebp-8]+4]+10]+10==进入cd的技能的冷却时间的地址 00ABF6AD |. DDD8 ||fstp st 00ABF6AF |. E8 CC11CEFF ||call 007A0880 ; [ebp-8]来源于这个call [[[ecx+4]+4]+10]+10==进入cd的技能的冷却时间的地址 00ABF6B4 |. 8B7D F8 ||mov edi, dword ptr [ebp-8] ; [[ebp-8]+10]+10==进入cd的技能的冷却时间的地址 00ABF6B7 |. 8B75 F4 ||mov esi, dword ptr [ebp-C] 00ABF6BA |.^ E9 31FFFFFF |\jmp 00ABF5F0 ; 循环尾部4 00ABF6BF |> 83C3 0C |add ebx, 0C ; [[[ebx+0c+4]]+10]+10==进入cd的技能的冷却时间的地址 ebx+0c*n的数组 当前链表项目==[ebx+n*0c+4]时候跳出链表 00ABF6C2 |. 836D FC 01 |sub dword ptr [ebp-4], 1 00ABF6C6 |.^ 0F85 07FFFFFF \jnz 00ABF5D3 ; [[[ebx+4]]+10]+10==进入cd的技能的冷却时间的地址 00ABF6CC |. 5F pop edi 00ABF6CD |. 5E pop esi 00ABF6CE |. 5B pop ebx 00ABF6CF |. 8BE5 mov esp, ebp 00ABF6D1 |. 5D pop ebp 00ABF6D2 \. C2 0400 retn 4
|
分析如下:
[[[[[17EA2B0]+30]+4+0c*0+4] ]+10]+10==进入cd的技能的冷却时间的地址
[[[[17EA2B0]+30]+4+0c*0+4] ]表示技能链表头 +4]取下一个技能 +10]+10表示cd倒数秒数的地址
当前链表项目==[[[17EA2B0]+30]+4+0*0c+4]时候链表结束
CE附加发现并不是链表。

由于此处分析过于复杂,尝试下访问断跳转分析位置。
1 2
| 00ABF61F |> \8B4F 10 ||mov ecx, dword ptr [edi+10] ; [edi+10]+10==进入cd的技能的冷却时间的地址 此处edi还是多技能在走这代码
|
此处dd edi下访问断。此处访问断是下给edi里面的地址对应的值有没有人访问。断到的位置为
1
| 0084CC3C |. 8B00 |mov eax, dword ptr [eax] ; [eax+10]+10==进入cd的技能的冷却时间的地址 此处eax变化种类数为进入cd技能数量 eax==35873300
|
由于下断的是[edi],下的是访问断,因此是把[edi]换成[eax],即edi换成eax
继续往上追,到了二叉树结构。

分析如下:
cd中技能二叉树根节点==[[[[17EA2B0]+30]+0*4+4+4]+4]
+0]表示左子树
+8]表示右子树
+0c]表示技能id
+15](byte)!=0表示到头了
[二叉树项+10]+10 表示技能cd倒数秒数
CE分析:
没放技能的时候:

两个技能时候:

放三个技能时候:

代码:
结构代码:
1 2 3 4 5 6 7 8 9 10 11 12
| struct TCD中技能信息 { DWORD dID; float fCD; }; struct T遍历CD中技能信息 { TCD中技能信息 技能列表[1000]; DWORD d数量; void 初始化CD中技能信息(); void 递归遍历二叉树信息(DWORD); };
|
函数实现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| DWORD g_冷却技能数量 = 0; void T遍历CD中技能信息::递归遍历二叉树信息(DWORD 根节点) { BYTE 标志位 = *(BYTE*)(根节点 + 0x15); DWORD 左子树 = *(DWORD*)(根节点 + 0x0); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); if (标志位==0) { 技能列表[g_冷却技能数量].dID= *(DWORD*)(根节点 + 0x0c); g_冷却技能数量++; 递归遍历二叉树信息(左子树); 递归遍历二叉树信息(右子树); } } void T遍历CD中技能信息::初始化CD中技能信息() { __try { g_冷却技能数量 = 0; DWORD tmp = *(DWORD*)(0x17ea2b0); tmp = *(DWORD*)(tmp+0x30); tmp = *(DWORD*)(tmp + 0x8); DWORD 二叉树根节点 = *(DWORD*)(tmp + 0x4); 递归遍历二叉树信息(二叉树根节点); d数量 = g_冷却技能数量; Call_输出调试信息("幻想神域 技能冷却数量:%d\r\n",d数量); } __except (1) { Call_输出调试信息("幻想神域 初始化CD中技能信息出错!"); } }
|
调用代码:
1 2 3 4 5 6
| T遍历CD中技能信息 t; t.初始化CD中技能信息(); for (int i = 0;i<t.d数量;i++) { Call_输出调试信息("幻想神域 冷却技能id:%X\r\n ",t.技能列表[i].dID); }
|
找技能名

转成BIG5:

1
| B6 C2 AA A2 B3 B4 A8 C0 00
|
上ce搜索:

对半修改筛选,最终定位到唯一一个地址:0C77CF04
od附加dd下访问断,尽量断技能栏的访问,若断到快捷栏的访问可能会断到技能快捷栏相关的东西
追到此处,找到了取技能对象call:
1 2 3 4 5 6
| 008EFF36 |. 8B45 10 mov eax, dword ptr [ebp+10] 008EFF39 |. 8B92 C4000000 mov edx, dword ptr [edx+C4] 008EFF3F |. 50 push eax ; 传进技能对象 008EFF40 |. 89B5 3CFFFFFF mov dword ptr [ebp-C4], esi 008EFF46 |. FFD2 call edx ; eax来源于该call 传进技能id传出技能名称 edx==[[[1789168]]+0c4] 008EFF48 |. 8BF0 mov esi, eax ; 技能名称地址==eax+100+4==0C77CF04 +10(WORD)疑似表示放在技能格子的位置
|
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| DWORD g_冷却技能数量 = 0; void T遍历CD中技能信息::递归遍历二叉树信息(DWORD 根节点) { BYTE 标志位 = *(BYTE*)(根节点 + 0x15); DWORD 左子树 = *(DWORD*)(根节点 + 0x0); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); if (标志位==0) { 技能列表[g_冷却技能数量].dID= *(DWORD*)(根节点 + 0x0c); DWORD fCD = *(DWORD*)(根节点 + 0x10); 技能列表[g_冷却技能数量].fCD = *(FLOAT*)(fCD + 0x10); 技能列表[g_冷却技能数量].p名字 = ""; DWORD ID = 技能列表[g_冷却技能数量].dID; DWORD 名称对象; DWORD Temp; __asm { mov ecx,0x1789168 mov ecx,[ecx] mov edx,[ecx] push ID mov edx,[edx+0x0c4] call edx mov 名称对象,eax } DWORD 名称长度 = *(DWORD*)(名称对象 + 0x100 + 0x18); if (名称长度>=0x10) { Temp = *(DWORD*)(名称对象 + 0x104); 技能列表[g_冷却技能数量].p名字 = (char*)Temp; } else { 技能列表[g_冷却技能数量].p名字 = (char*)名称对象 + 0x104; } strcpy(技能列表[g_冷却技能数量].cGBK名字, 技能列表[g_冷却技能数量].p名字); BIG52GBK(技能列表[g_冷却技能数量].cGBK名字);
g_冷却技能数量++; 递归遍历二叉树信息(左子树); 递归遍历二叉树信息(右子树); } } void T遍历CD中技能信息::初始化CD中技能信息() { __try { g_冷却技能数量 = 0; DWORD tmp = *(DWORD*)(0x17ea2b0); tmp = *(DWORD*)(tmp+0x30); tmp = *(DWORD*)(tmp + 0x8); DWORD 二叉树根节点 = *(DWORD*)(tmp + 0x4); 递归遍历二叉树信息(二叉树根节点); d数量 = g_冷却技能数量; Call_输出调试信息("幻想神域 技能冷却数量:%d\r\n",d数量); } __except (1) { Call_输出调试信息("幻想神域 初始化CD中技能信息出错!"); } }
|
调用代码:
1 2 3 4 5 6
| T遍历CD中技能信息 t; t.初始化CD中技能信息(); for (int i = 0;i<t.d数量;i++) { Call_输出调试信息("幻想神域 冷却技能id:%X 剩余冷却时间:%f秒 技能名称:%s\r\n ",t.技能列表[i].dID, t.技能列表[i].fCD, t.技能列表[i].cGBK名字); }
|
调试效果如下:

针对前面找到的cd中技能二叉树进行分析:
cd中技能二叉树根节点==[[[[17EA2B0]+30]+ecx*4+4+4]+4],此处的ecx位置明显是个数组
dd [[17EA2B0]+30]这个位置,观察数组项进行分析:

015E9BEC 人物cd中技能数
015E9BF8 cd中道具数
015E9C04 未知
0E519C10 宠物技能cd数
,,,底下的也暂时未知
可以得出结论这里的全是各种冷却二叉树结构!
全技能遍历
第一·次找全技能,结果找到鼠标坐标call
寻找全技能遍历的突破口。之前,通过技能名称找到的是名称库,通过技能释放剩下的cd秒数找到的是冷却中的技能结构。尝试通过技能id找全技能结构。
到通过技能id获取技能对象call的位置继续往上追其参数。
追到此处:
1
| 008FA35F |. 8B4D 10 mov ecx, dword ptr [ebp+10] ; 技能id==[ebp+10]
|
此处准备出call,若鼠标指向快捷栏中断下则出别的地方,处的那个地方再指向技能列表中发现不会再断,由于跟随技能快捷栏很容易追到ui结构中去。因此我们选择指向技能列表中的技能断下。
追到这:
1
| 00CB3019 |. 56 push esi ; 技能id==[esi+1C8]==0CAC1 此处开始在ui上胡乱断了
|
此时因为已经开始胡乱断了,验证方法是od窗口压在游戏窗口上方,是鼠标移出去位置正好对应技能位置,如下:

从而避免别的UI干扰,得以验证对错。
继续向上追,追到此处:
1
| 00CC292C |. 8B4E 3C mov ecx, dword ptr [esi+3C] ; 技能id==[[esi+3c]+1C8]==0CAC1 此处不能通过完美验证法,需要追[esi+3c]的整体
|
新知识点!!完美验证法!!
继续向上追:

eax来源于上图箭头指向的call,此时发现该call的参数进一层是鼠标x,y相对于游戏窗口的坐标。
到此说明追技能结构失败,此处已经与技能结构没有关系了,而是一个类似于所有ui界面的一个遍历。
第二次找全技能结构失败,找到了已使用过的技能结构
既然第一次尝试失败了,找别的突破口。比如说从释放技能call作为突破口
用ce找到同一个技能的所有技能id,每个到od中下访问断,通过断到的位置和之前找过的代码是否相同或访问技能是否断下来做判断进行排除法。
技能id到ce中搜索:

由下往上排除:
结果第一个访问断就断到了一个新的二叉树:
1 2 3 4 5 6 7 8 9 10 11
| 006E001D | 8D49 00 lea ecx, dword ptr [ecx] 006E0020 |> 66:3970 0C /cmp word ptr [eax+C], si ; 断在此处 二叉树结构 eax+0]左子树 eax+8]右子树 eax+11]!=0跳出循环 eax+0c]为技能id地址 [EAX+0C]==0CAC1 006E0024 |. 73 05 |jnb short 006E002B 006E0026 |. 8B40 08 |mov eax, dword ptr [eax+8] 006E0029 |. EB 04 |jmp short 006E002F 006E002B |> 8BD0 |mov edx, eax 006E002D |. 8B00 |mov eax, dword ptr [eax] 006E002F |> 8078 11 00 |cmp byte ptr [eax+11], 0 006E0033 |.^ 74 EB \je short 006E0020 006E0035 |> 3B51 04 cmp edx, dword ptr [ecx+4] 006E0038 |. 8BFA mov edi, edx
|
分析如下:
二叉树根节点:[[[[链表项+0C]+2C]+70+4]+4]
+0]左子树 +8]右子树
二叉树节点+11](byte)!=0表示跳出循环
再往上找,此处有个链表结构:该链表就是以前找到的遍历所有角色对象的链表。
1
| 006D7D5A |> \8B4E 0C |mov ecx, dword ptr [esi+C] ; 血量地址==[[esi+0c]+0c]+8==307CD808 技能id==[[[[[[ESI+0C]+2C]+70+4]+4]]+0C]==0CAC1
|
技能id==[[[[[[链表项+0C]+2C]+70+4]+4] ]+0C]==0CAC1
ESI链表头=[[[0F84B74]+410+8] ],esi可以套n层[],
[[[0F84B74]+410+8] ]套几层[]表示当前链表项==[[0F84B74]+410+8] 时候跳出链表
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| void T遍历自己的技能信息::初始化技能信息() { __try { g_冷却技能数量 = 0; DWORD 人物对象 = 0; __asm { mov ecx, 0x0F84B74 mov ecx, [ecx] mov eax, [ecx + 0x40c] add ecx, 0x410 push eax mov eax, 0x00665870 call eax mov 人物对象, eax } DWORD 二叉树根节点= *(DWORD*)(人物对象 + 0x2c); 二叉树根节点 = *(DWORD*)(二叉树根节点 + 0x74); 二叉树根节点 = *(DWORD*)(二叉树根节点 + 0x4); 递归遍历二叉树信息(二叉树根节点); d数量 = g_冷却技能数量; Call_输出调试信息("幻想神域 已使用过的技能数量:%d\r\n", d数量); } __except (1) { Call_输出调试信息("幻想神域 初始化技能信息出错!"); } }
void T遍历自己的技能信息::递归遍历二叉树信息(DWORD 根节点) { BYTE 标志位 = *(BYTE*)(根节点 + 0x11); DWORD 左子树 = *(DWORD*)(根节点 + 0x0); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); if (标志位 == 0) { 技能列表[g_冷却技能数量].dID = *(DWORD*)(根节点 + 0x0c); 技能列表[g_冷却技能数量].p名字 = ""; DWORD ID = 技能列表[g_冷却技能数量].dID; DWORD 名称对象; DWORD Temp; __asm { mov ecx, 0x1789168 mov ecx, [ecx] mov edx, [ecx] push ID mov edx, [edx + 0x0c4] call edx mov 名称对象, eax } DWORD 名称长度 = *(DWORD*)(名称对象 + 0x100 + 0x18); if (名称长度 >= 0x10) { Temp = *(DWORD*)(名称对象 + 0x104); 技能列表[g_冷却技能数量].p名字 = (char*)Temp; } else { 技能列表[g_冷却技能数量].p名字 = (char*)名称对象 + 0x104; } strcpy(技能列表[g_冷却技能数量].cGBK名字, 技能列表[g_冷却技能数量].p名字); BIG52GBK(技能列表[g_冷却技能数量].cGBK名字);
g_冷却技能数量++; 递归遍历二叉树信息(左子树); 递归遍历二叉树信息(右子树); } }
|
调试过程如下:

实际跑的时候发现该结构存的并非全技能,而是类似于用过的技能结构。使用过就能遍历出来,未使用就不存在于这个结构。
第三次找全技能
继续在ce找技能id筛除:

逐个用od来dd地址下访问断,0的表示查看和释放技能都不断的,找到了能断下的,并且是一个二叉树结构。

断下的位置向上追找到二叉树:
1 2 3 4 5 6 7 8 9 10
| 0070790A |. 8D9B 00000000 lea ebx, dword ptr [ebx] 00707910 |> 66:3970 0C /cmp word ptr [eax+C], si ; [eax+0c]==0CAC1==技能id 二叉树结构 二叉树根节点=[[[人物对象+0c]+444+4]+4] +0]左子树 +8]右子树 +21]!=0跳出二叉树 00707914 |. 73 05 |jnb short 0070791B 00707916 |. 8B40 08 |mov eax, dword ptr [eax+8] 00707919 |. EB 04 |jmp short 0070791F 0070791B |> 8BD0 |mov edx, eax 0070791D |. 8B00 |mov eax, dword ptr [eax] 0070791F |> 8078 21 00 |cmp byte ptr [eax+21], 0 00707923 |.^ 74 EB \je short 00707910 00707925 |> 8B41 04 mov eax, dword ptr [ecx+4]
|
向上追到二叉树根节点来源于该call:

分析得:
二叉树根节点=[[[人物对象+0c]+444+4]+4]
+0]左子树 +8]右子树 +21]!=0跳出循环
调试过程中发现了一个严重的bug,上面所有的技能id取值的时候实际上应该取的是WORD,而非DWORD,用DWORD可能崩溃。
改过之后没事了。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| void T遍历我的技能信息::初始化我的技能信息() { __try { g_冷却技能数量 = 0; DWORD 人物对象 = 0; __asm { mov ecx, 0x0F84B74 mov ecx, [ecx] mov eax, [ecx + 0x40c] add ecx, 0x410 push eax mov eax, 0x00665870 call eax mov 人物对象, eax } DWORD 二叉树根节点 = *(DWORD*)(人物对象 + 0x0c); 二叉树根节点 = *(DWORD*)(二叉树根节点 + 0x448); 二叉树根节点 = *(DWORD*)(二叉树根节点 + 0x4); 递归遍历二叉树信息(二叉树根节点); d数量 = g_冷却技能数量; Call_输出调试信息("幻想神域 我的技能数量:%d\r\n", d数量); } __except (1) { Call_输出调试信息("幻想神域 初始化我的技能信息出错!"); } }
void T遍历我的技能信息::递归遍历二叉树信息(DWORD 根节点) { BYTE 标志位 = *(BYTE*)(根节点 + 0x21); DWORD 左子树 = *(DWORD*)(根节点 + 0x0); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); if (标志位 == 0) { 技能列表[g_冷却技能数量].dID = *(WORD*)(根节点 + 0x0c); 技能列表[g_冷却技能数量].p名字 = ""; DWORD ID = 技能列表[g_冷却技能数量].dID; DWORD 名称对象; DWORD Temp; __asm { mov ecx, 0x1789168 mov ecx, [ecx] mov edx, [ecx] push ID mov edx, [edx + 0x0c4] call edx mov 名称对象, eax } DWORD 名称长度 = *(DWORD*)(名称对象 + 0x100 + 0x18); if (名称长度 >= 0x10) { Temp = *(DWORD*)(名称对象 + 0x104); 技能列表[g_冷却技能数量].p名字 = (char*)Temp; } else { 技能列表[g_冷却技能数量].p名字 = (char*)名称对象 + 0x104; } strcpy(技能列表[g_冷却技能数量].cGBK名字, 技能列表[g_冷却技能数量].p名字); BIG52GBK(技能列表[g_冷却技能数量].cGBK名字);
g_冷却技能数量++; 递归遍历二叉树信息(左子树); 递归遍历二叉树信息(右子树); } }
|
调试结果:

终于成功找到了全技能遍历!
找明文封包call
确定有无crc检测?
给代码下访问断点,发现没有访问代码,说明没有crc检测
判断是哪种发包?
WSASend断下,判断是WSASend发包
判断是否线程发包?
喊话的调用堆栈如下:
调用堆栈: 主线程
地址 堆栈 函数过程 / 参数 调用来自 结构
0019FB8C 00B921B6 ws2_32.WSASend game.00B921B0 0019FBDC
0019FB90 00000A74 Socket = A74
0019FB94 0019FBC0 pBuffers = 0019FBC0
0019FB98 00000001 nBuffers = 1
0019FB9C 0019FBBC pBytesSent = 0019FBBC
0019FBA0 00000000 Flags = 0
0019FBA4 00000000 pOverlapped = NULL
0019FBA8 00000000 Callback = NULL
0019FBD0 008861A0 game.00B91FA0 game.0088619B 0019FBDC
0019FBE0 00B90F27 包含game.008861A0 game.00B90F25 0019FBDC
0019FBF0 76EA5CAB 包含game.00B90F27 user32.76EA5CA9 0019FC18
0019FC1C 76E967BC user32.76EA5C80 user32.76E967B7 0019FC18
0019FD00 76E958FB user32.76E96410 user32.76E958F6 0019FCFC
0019FD74 76E956D0 user32.76E956E0 user32.76E956CB 0019FD70
0019FD80 00504E2D user32.DispatchMessageW game.00504E27 0019FD7C
0019FD84 0019FE9C pMsg = MSG(9C40) hw = C07DA (“192.
0019FEDC 00416EF9 ? game.00504AD0 game.00416EF4 0019FED8
走路的调用堆栈如下:
调用堆栈: 主线程
地址 堆栈 函数过程 / 参数 调用来自 结构
0019FB8C 00B921B6 ws2_32.WSASend game.00B921B0 0019FBDC
0019FB90 00000A74 Socket = A74
0019FB94 0019FBC0 pBuffers = 0019FBC0
0019FB98 00000001 nBuffers = 1
0019FB9C 0019FBBC pBytesSent = 0019FBBC
0019FBA0 00000000 Flags = 0
0019FBA4 00000000 pOverlapped = NULL
0019FBA8 00000000 Callback = NULL
0019FBD0 008861A0 game.00B91FA0 game.0088619B 0019FBDC
0019FBE0 00B90F27 包含game.008861A0 game.00B90F25 0019FBDC
0019FBF0 76EA5CAB 包含game.00B90F27 user32.76EA5CA9 0019FC18
0019FC1C 76E967BC user32.76EA5C80 user32.76E967B7 0019FC18
0019FD00 76E958FB user32.76E96410 user32.76E958F6 0019FCFC
0019FD74 76E956D0 user32.76E956E0 user32.76E956CB 0019FD70
0019FD80 00504E2D user32.DispatchMessageW game.00504E27 0019FD7C
0019FD84 0019FE9C pMsg = MSG(9C40) hw = C07DA (“192.
0019FEDC 00416EF9 ? game.00504AD0 game.00416EF4 0019FED8
由上可以发现喊话和走路的调用堆栈完全一样,因此可知极大概率是线程发包
跳出线程发包
判断该线程发包是否地址不变?变的话向上追来源,找到不变的一层地址即为跳出线程发包的突破口。
WSASend多次下断找到包内容地址并不相同,说明需要向上追内容来源找到不变的包内容地址作为跳出线程发包的地址。
向上追来源

[该call第二个参数+4]为包地址

此处edx+esi由于多次下断edx均为0,直接视edx为0,继续追esi。

任何主动行为都是edi==0D64200C一直不变,某个心跳包会出现edi==2986f00c
edi+2888追到此

或者直接给edi+2888的地址下写入断点,发现一次是在发包call之后的清零操作,一次就是上图这次的赋值操作。
此时在上图处下断会发现(下断后观察调用堆栈得出)还是在线程发包的调用流程中。
因此继续往上追

任何主动行为都是edx==2169A7D0,但心跳包偶尔会出现edx==34DC97B0
edx下写入断
断下两处位置,观察调用堆栈得出两次位置均已跳出发包线程,两处断下一次为赋值操作,不同行为赋不同值,另一个断点为设置其值为自身地址的清零操作。
跳出线程后位置:

同时下断WSASend处和上图处观察包内容是否一致来验证逆向追来源是否依然正确?
验证后得知 [[ebp+8]+4]==包地址 该表达式正确!
通过调用堆栈那观察是否有喊话call判断是否真正跳出线程发包。由此确定已跳出线程。
找明文封包call
跳出线程位置下断逐层分析。发现跳出一层就已经是铭文包了,利用喊话来寻找明文。发现该call的[edi+4]为明文地址

喊话一长串1断下后找到313131313131的长串字符,并且该call无论是走路还是喊话都会断下,说明返回一层的call就是明文封包call。
找加密call
由此可知该明文封包call内到线程发包跳出点之间一定有加密call,并且发现明文所在地址和密文所在地址为同一个地址,即不改存位置,直接在原地址加密。因此断下单步执行观察明文何时变成密文直接确定加密call位置。
确定加密call位置,分析如下:

逆向寻找密钥来源:
$$
疑似指向密钥的指针==[[[[[0F84BA4]]+4]+0c+8]]+54
$$
p.s.该加密call的ecx无用,因为call内第一句有关ecx的代码是用别的值覆盖ecx。
代码注入器测试加密call:(12345678会崩溃,实际上要找一个无用的地址)
1 2 3 4 5 6 7 8 9 10 11 12
| push 12345678 push 12345678 push 11 mov ecx,[00F84BA4] mov ecx,[ecx] mov ecx,[ecx+4] mov ecx,[ecx+14] mov ecx,[ecx] lea ecx,[ecx+54] push ecx call 00B94700 add esp,10
|
7E 00 00 00 00 02 00 31 31 FF FF FF FF 00 00 00 00 00 00 00
p.s.
包地址开始处:
30A72F40 007E0016 game.007E0016
30A72F44 07000000
30A72F48 31313100
30A72F4C 31313131
30A72F50 FFFFFFFF
30A72F54 00000000
加密开始处:
30A72F3E 00160000
30A72F42 0000007E
30A72F46 31000700
30A72F4A 31313131
30A72F4E FFFF3131
30A72F52 0000FFFF
30A72F56 00000000
测试加密前:
12345678 0000007E
1234567C 31000200
12345680 FFFFFF31
12345684 000000FF
12345688 00000000
测试加密后:
12345678 6BA1F264
1234567C 0E5FA901
12345680 6DD87FAF
12345684 16781C93
12345688 0000009A
vs借用游戏代码实现喊话功能
逆向得到socket套接字如下:

socket套接字的地址为:GetWindowLongW的返回值+38
$$
套接字为socket套接字=[GetWindowLongW的返回值+38]
$$
vs代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| byte 包内容[100] = {0x11,0x00,0x7E,0x00, 0x00,0x00,0x00,0x02, 0x00,0x31,0x31,0xFF, 0xFF,0xFF,0xFF,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00}; DWORD 包长 = 0x13; DWORD 加密长 = (DWORD)包长 - 2; DWORD 加密地址 = (DWORD)包内容 + 2; __asm { push 加密地址 push 加密地址 push 加密长 mov ecx, 0x00F84BA4 mov ecx,[ecx] mov ecx, [ecx] mov ecx, [ecx + 0x4] mov ecx, [ecx + 0x14] mov ecx, [ecx] lea ecx, [ecx + 0x54] push ecx mov eax, 0x00B94700 call eax add esp, 0x10 } HWND hwnd = (HWND)FindWindowA("Lapis Network Class", 0); DWORD socket = GetWindowLongW(hwnd, GWL_USERDATA); socket = *(DWORD*)(socket + 0x38); send(socket, (const char*)包内容, 包长, 0);
|
偷功能(不走游戏代码)
加密call全部汇编如下:(该范围内所有跳转不会跳出这个范围,说明这是完整的加密call)(内部无其他call)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
| 00B94700 /$ 55 push ebp 00B94701 |. 53 push ebx 00B94702 |. 56 push esi 00B94703 |. 57 push edi 00B94704 |. 8B7C24 14 mov edi, dword ptr [esp+14] 00B94708 |. 8B5424 18 mov edx, dword ptr [esp+18] 00B9470C |. 8B7424 1C mov esi, dword ptr [esp+1C] 00B94710 |. 8B6C24 20 mov ebp, dword ptr [esp+20] 00B94714 |. 31C0 xor eax, eax 00B94716 |. 31DB xor ebx, ebx 00B94718 |. 83FA 00 cmp edx, 0 00B9471B |. 0F84 56010000 je 00B94877 00B94721 |. 8A07 mov al, byte ptr [edi] 00B94723 |. 8A5F 04 mov bl, byte ptr [edi+4] 00B94726 |. 83C7 08 add edi, 8 00B94729 |. 8D0C16 lea ecx, dword ptr [esi+edx] 00B9472C |. 29F5 sub ebp, esi 00B9472E |. 894C24 18 mov dword ptr [esp+18], ecx 00B94732 |. FEC0 inc al 00B94734 |. 83BF 00010000>cmp dword ptr [edi+100], -1 00B9473B |. 0F84 FF000000 je 00B94840 00B94741 |. 8B0C87 mov ecx, dword ptr [edi+eax*4] 00B94744 |. 83E2 FC and edx, FFFFFFFC 00B94747 |. 0F84 B3000000 je 00B94800 00B9474D |. 8D5416 FC lea edx, dword ptr [esi+edx-4] 00B94751 |. 895424 1C mov dword ptr [esp+1C], edx 00B94755 |. 896C24 20 mov dword ptr [esp+20], ebp 00B94759 |. 90 nop 00B9475A |. 90 nop 00B9475B |. 90 nop 00B9475C |. 90 nop 00B9475D |. 90 nop 00B9475E |. 90 nop 00B9475F |. 90 nop 00B94760 |> 00CB /add bl, cl 00B94762 |. 8B149F |mov edx, dword ptr [edi+ebx*4] 00B94765 |. 890C9F |mov dword ptr [edi+ebx*4], ecx 00B94768 |. 891487 |mov dword ptr [edi+eax*4], edx 00B9476B |. 01CA |add edx, ecx 00B9476D |. FEC0 |inc al 00B9476F |. 81E2 FF000000 |and edx, 0FF 00B94775 |. 8B0C87 |mov ecx, dword ptr [edi+eax*4] 00B94778 |. 8B2C97 |mov ebp, dword ptr [edi+edx*4] 00B9477B |. 00CB |add bl, cl 00B9477D |. 8B149F |mov edx, dword ptr [edi+ebx*4] 00B94780 |. 890C9F |mov dword ptr [edi+ebx*4], ecx 00B94783 |. 891487 |mov dword ptr [edi+eax*4], edx 00B94786 |. 01CA |add edx, ecx 00B94788 |. FEC0 |inc al 00B9478A |. 81E2 FF000000 |and edx, 0FF 00B94790 |. C1CD 08 |ror ebp, 8 00B94793 |. 8B0C87 |mov ecx, dword ptr [edi+eax*4] 00B94796 |. 0B2C97 |or ebp, dword ptr [edi+edx*4] 00B94799 |. 00CB |add bl, cl 00B9479B |. 8B149F |mov edx, dword ptr [edi+ebx*4] 00B9479E |. 890C9F |mov dword ptr [edi+ebx*4], ecx 00B947A1 |. 891487 |mov dword ptr [edi+eax*4], edx 00B947A4 |. 01CA |add edx, ecx 00B947A6 |. FEC0 |inc al 00B947A8 |. 81E2 FF000000 |and edx, 0FF 00B947AE |. C1CD 08 |ror ebp, 8 00B947B1 |. 8B0C87 |mov ecx, dword ptr [edi+eax*4] 00B947B4 |. 0B2C97 |or ebp, dword ptr [edi+edx*4] 00B947B7 |. 00CB |add bl, cl 00B947B9 |. 8B149F |mov edx, dword ptr [edi+ebx*4] 00B947BC |. 890C9F |mov dword ptr [edi+ebx*4], ecx 00B947BF |. 891487 |mov dword ptr [edi+eax*4], edx 00B947C2 |. 01CA |add edx, ecx 00B947C4 |. FEC0 |inc al 00B947C6 |. 81E2 FF000000 |and edx, 0FF 00B947CC |. C1CD 08 |ror ebp, 8 00B947CF |. 8B4C24 20 |mov ecx, dword ptr [esp+20] 00B947D3 |. 0B2C97 |or ebp, dword ptr [edi+edx*4] 00B947D6 |. C1CD 08 |ror ebp, 8 00B947D9 |. 332E |xor ebp, dword ptr [esi] 00B947DB |. 3B7424 1C |cmp esi, dword ptr [esp+1C] 00B947DF |. 892C31 |mov dword ptr [ecx+esi], ebp 00B947E2 |. 8D76 04 |lea esi, dword ptr [esi+4] 00B947E5 |. 8B0C87 |mov ecx, dword ptr [edi+eax*4] 00B947E8 |.^ 0F82 72FFFFFF \jb 00B94760 00B947EE |. 3B7424 18 cmp esi, dword ptr [esp+18] 00B947F2 |. 0F84 77000000 je 00B9486F 00B947F8 |. 8B6C24 20 mov ebp, dword ptr [esp+20] 00B947FC |. 90 nop 00B947FD |. 90 nop 00B947FE |. 90 nop 00B947FF |. 90 nop 00B94800 |> 00CB /add bl, cl 00B94802 |. 8B149F |mov edx, dword ptr [edi+ebx*4] 00B94805 |. 890C9F |mov dword ptr [edi+ebx*4], ecx 00B94808 |. 891487 |mov dword ptr [edi+eax*4], edx 00B9480B |. 01CA |add edx, ecx 00B9480D |. FEC0 |inc al 00B9480F |. 81E2 FF000000 |and edx, 0FF 00B94815 |. 8B1497 |mov edx, dword ptr [edi+edx*4] 00B94818 |. 3216 |xor dl, byte ptr [esi] 00B9481A |. 8D76 01 |lea esi, dword ptr [esi+1] 00B9481D |. 8B0C87 |mov ecx, dword ptr [edi+eax*4] 00B94820 |. 3B7424 18 |cmp esi, dword ptr [esp+18] 00B94824 |. 885435 FF |mov byte ptr [ebp+esi-1], dl 00B94828 |.^ 0F82 D2FFFFFF \jb 00B94800 00B9482E |. E9 3C000000 jmp 00B9486F 00B94833 | 90 nop 00B94834 | 90 nop 00B94835 | 90 nop 00B94836 | 90 nop 00B94837 | 90 nop 00B94838 | 90 nop 00B94839 | 90 nop 00B9483A | 90 nop 00B9483B | 90 nop 00B9483C | 90 nop 00B9483D | 90 nop 00B9483E | 90 nop 00B9483F | 90 nop 00B94840 |> 0FB60C07 movzx ecx, byte ptr [edi+eax] 00B94844 |> 00CB /add bl, cl 00B94846 |. 0FB6141F |movzx edx, byte ptr [edi+ebx] 00B9484A |. 880C1F |mov byte ptr [edi+ebx], cl 00B9484D |. 881407 |mov byte ptr [edi+eax], dl 00B94850 |. 00CA |add dl, cl 00B94852 |. 0FB61417 |movzx edx, byte ptr [edi+edx] 00B94856 |. 04 01 |add al, 1 00B94858 |. 3216 |xor dl, byte ptr [esi] 00B9485A |. 8D76 01 |lea esi, dword ptr [esi+1] 00B9485D |. 0FB60C07 |movzx ecx, byte ptr [edi+eax] 00B94861 |. 3B7424 18 |cmp esi, dword ptr [esp+18] 00B94865 |. 885435 FF |mov byte ptr [ebp+esi-1], dl 00B94869 |.^ 0F82 D5FFFFFF \jb 00B94844 00B9486F |> FEC8 dec al 00B94871 |. 885F FC mov byte ptr [edi-4], bl 00B94874 |. 8847 F8 mov byte ptr [edi-8], al 00B94877 |> 5F pop edi 00B94878 |. 5E pop esi 00B94879 |. 5B pop ebx 00B9487A |. 5D pop ebp 00B9487B \. C3 retn;
|
处理步骤:
- 十进制转换为十六进制
- mov exx,[基地址]这种情况需要处理
- 跳转处理
处理后的代码封装成加密call函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| __declspec(naked) void 加密call(DWORD 密钥, DWORD 加密长度, DWORD 加密地址, DWORD 加密地址2) { __asm { push ebp push ebx push esi push edi mov edi, dword ptr[esp + 0x14] mov edx, dword ptr[esp + 0x18] mov esi, dword ptr[esp + 0x1C] mov ebp, dword ptr[esp + 0x20] xor eax, eax xor ebx, ebx cmp edx, 0 je label1 mov al, byte ptr[edi] mov bl, byte ptr[edi + 4] add edi, 8 lea ecx, dword ptr[esi + edx] sub ebp, esi mov dword ptr[esp + 0x18], ecx inc al cmp dword ptr[edi + 0x100], -1 je label2 mov ecx, dword ptr[edi + eax * 4] and edx, 0xFFFFFFFC je label3 lea edx, dword ptr[esi + edx - 4] mov dword ptr[esp + 0x1C], edx mov dword ptr[esp + 0x20], ebp nop nop nop nop nop nop nop label4 : add bl, cl mov edx, dword ptr[edi + ebx * 4] mov dword ptr[edi + ebx * 4], ecx mov dword ptr[edi + eax * 4], edx add edx, ecx inc al and edx, 0x0FF mov ecx, dword ptr[edi + eax * 4] mov ebp, dword ptr[edi + edx * 4] add bl, cl mov edx, dword ptr[edi + ebx * 4] mov dword ptr[edi + ebx * 4], ecx mov dword ptr[edi + eax * 4], edx add edx, ecx inc al and edx, 0x0FF ror ebp, 8 mov ecx, dword ptr[edi + eax * 4] or ebp, dword ptr[edi + edx * 4] add bl, cl mov edx, dword ptr[edi + ebx * 4] mov dword ptr[edi + ebx * 4], ecx mov dword ptr[edi + eax * 4], edx add edx, ecx inc al and edx, 0x0FF ror ebp, 8 mov ecx, dword ptr[edi + eax * 4] or ebp, dword ptr[edi + edx * 4] add bl, cl mov edx, dword ptr[edi + ebx * 4] mov dword ptr[edi + ebx * 4], ecx mov dword ptr[edi + eax * 4], edx add edx, ecx inc al and edx, 0x0FF ror ebp, 8 mov ecx, dword ptr[esp + 0x20] or ebp, dword ptr[edi + edx * 4] ror ebp, 8 xor ebp, dword ptr[esi] cmp esi, dword ptr[esp + 0x1C] mov dword ptr[ecx + esi], ebp lea esi, dword ptr[esi + 4] mov ecx, dword ptr[edi + eax * 4] jb label4 cmp esi, dword ptr[esp + 0x18] je label5 mov ebp, dword ptr[esp + 0x20] nop nop nop nop label3 : add bl, cl mov edx, dword ptr[edi + ebx * 4] mov dword ptr[edi + ebx * 4], ecx mov dword ptr[edi + eax * 4], edx add edx, ecx inc al and edx, 0x0FF mov edx, dword ptr[edi + edx * 4] xor dl, byte ptr[esi] lea esi, dword ptr[esi + 1] mov ecx, dword ptr[edi + eax * 4] cmp esi, dword ptr[esp + 0x18] mov byte ptr[ebp + esi - 1], dl jb label3 jmp label5 nop nop nop nop nop nop nop nop nop nop nop nop nop label2 : movzx ecx, byte ptr[edi + eax] label6 : add bl, cl movzx edx, byte ptr[edi + ebx] mov byte ptr[edi + ebx], cl mov byte ptr[edi + eax], dl add dl, cl movzx edx, byte ptr[edi + edx] add al, 1 xor dl, byte ptr[esi] lea esi, dword ptr[esi + 1] movzx ecx, byte ptr[edi + eax] cmp esi, dword ptr[esp + 0x18] mov byte ptr[ebp + esi - 1], dl jb label6 label5 : dec al mov byte ptr[edi - 4], bl mov byte ptr[edi - 8], al label1 : pop edi pop esi pop ebx pop ebp retn } }
|
不走游戏代码实现喊话:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| byte 包内容[100] = { 0x11,0x00,0x7E,0x00, 0x00,0x00,0x00,0x02, 0x00,0x31,0x31,0xFF, 0xFF,0xFF,0xFF,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00 }; DWORD 包长 = 0x13; DWORD 加密长 = (DWORD)包长 - 2; DWORD 加密地址 = (DWORD)包内容 + 2; DWORD 密钥=0; __asm { mov ecx, 0x00F84BA4 mov ecx, [ecx] mov ecx, [ecx] mov ecx, [ecx + 0x4] mov ecx, [ecx + 0x14] mov ecx, [ecx] lea ecx, [ecx + 0x54] mov 密钥,ecx } 加密call(密钥, 加密长, 加密地址, 加密地址); HWND hwnd = (HWND)FindWindowA("Lapis Network Class", 0); DWORD socket = GetWindowLongW(hwnd, GWL_USERDATA); socket = *(DWORD*)(socket + 0x38); send(socket, (const char*)包内容, 包长, 0);
|
找明文收包
判断是哪种收包,下断能断发现是WSARecv.
结合api分析:

收包内容地址固定,在比较远的位置下硬件访问断来断下我们的超长文本收包,然后到游戏中喊话一大段1111……(如图位置下断避开其他包的干扰)(喊话既发包,也收包)。此处访问断断下的位置要么是解密call,要么是位置迁移(此处下软件断会崩溃)

断在此处:
1
| 0041608A . F3:A5 rep movs dword ptr es:[edi], dword ptr [esi] ; 收包内容从缓存区取出位置
|
保持断下状态,删除硬件断点再在edi指向位置下硬件访问断(断下位置依然要么是位置迁移,要么是解密)
又断下
1
| 00B947DB |. 3B7424 1C |cmp esi, dword ptr [esp+1C] ; 解密中位置
|
此时发现数据区如下:

其中出现明文,可知此处是正在解密中的位置,ctrl+f9返回上一层即解密call

由此发现,此处找到的解密call和上面找到的加密call为同一个call,说明该加密为对称性加密,此call为加解密call。
分析:

由此可知,ebx为明文收包
HOOK明文发包
hook此处:
1 2
| 00B92C82 |> \8B46 08 mov eax, dword ptr [esi+8] 00B92C85 |. 2BC1 sub eax, ecx
|
不用改的工具函数如下
提升权限函数代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| BOOL Call_提升权限(BOOL bEnable) { BOOL foK = FALSE; HANDLE hToken; if (OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid); tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL); foK = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return foK; }
|
将调试信息输出到debug view上的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void Call_输出调试信息(char* pszFormat, ...) { #ifdef _DEBUG char szbufFormat[0x1000]; char szbufFormat_Game[0x1100] = ""; va_list arglst; va_start(arglst, pszFormat); vsprintf_s(szbufFormat,pszFormat, arglst); strcat_s(szbufFormat_Game, szbufFormat); OutputDebugStringA(szbufFormat_Game); va_end(arglst); #endif }
|
hook需要改动的函数如下
用内存的方式找窗口句柄:(通过spy++配合ce搜索找基地址)
1 2 3 4 5 6 7
| HWND Call_获取窗口句柄() { HWND hGame; hGame = *(HWND*)0x00F7BFB0; return hGame; }
|
HOOK跳转目标的子函数的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| __declspec(naked) void 明文包HOOKcalll() { __asm { pushad mov ecx, [esi + 4] mov eax, [esi + 8] mov g_包地址, ecx sub eax, ecx mov g_包长, eax }
Call_提升权限(TRUE);
GetWindowThreadProcessId(Call_获取窗口句柄(), &pid); hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
p = new byte[g_包长]; ReadProcessMemory(hProcess, (LPCVOID)g_包地址,p, g_包长, 0); *(WORD*)p = g_包长 - 2;
for (int i = 0; i < (int)g_包长;i++) { sprintf(s, "%02X", p[i]); strcat_s(a, s);
} Call_输出调试信息("幻想神域 包长:%x 包地址%x 包内容:%s\r\n", g_包长, g_包地址,a); sprintf(a, "%s", ""); delete p;
__asm { popad mov eax,[esi+8] sub eax,ecx retn } }
|
p.s.补上游戏需要的包长的意思是,由于我们在游戏中hook的位置是游戏本身补上包长之前,因此手动补上
hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| DWORD Hook地址 = 0x00B92C82; DWORD Hook子程序指针 = (DWORD)明文包HOOKcalll; DWORD 跳转值 = Hook子程序指针 - Hook地址 - 5;
DWORD old = 0;
VirtualProtect((PVOID)0x00B92C82, 100, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)0x00B92C82 = 0xE8; *(DWORD*)(0x00B92C82+1) = 跳转值;
VirtualProtect((PVOID)0x00B92C82, 100, old, &old);
|
还原hook代码如下:
1 2 3 4 5 6 7 8 9 10
| DWORD old = 0;
VirtualProtect((PVOID)0x00B92C82, 100, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)0x00B92C82 = 0x8B; *(DWORD*)(0x00B92C82 + 1) = 0xC12B0846;
VirtualProtect((PVOID)0x00B92C82, 100, old, &old);
|
HOOK明文收包
hook此处
1
| 00B92931 |. 8BAE 74280000 |mov ebp, dword ptr [esi+2874]
|
hook代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| DWORD g_收包地址=0; DWORD g_收包包长 =0;
void 明文收包通用解决办法() { Call_提升权限(TRUE);
GetWindowThreadProcessId(Call_获取窗口句柄(), &pid);
p = new byte[g_收包包长]; hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); BOOL isok=ReadProcessMemory(hProcess, (LPCVOID)g_收包地址, p, g_收包包长, 0); if (isok) { for (int i = 0; i < (int)g_收包包长; i++) { sprintf(s, "%02X", p[i]); strcat_s(a, s);
} Call_输出调试信息("幻想神域 收包地址%x 收包包长:%x 收包内容:%s\r\n", g_收包地址, g_收包包长, a); sprintf(a, "%s", ""); } else { Call_输出调试信息("幻想神域 错误码:%d", GetLastError()); }
delete p; }
__declspec(naked) void 明文收包HOOKcalll() { __asm { pushad mov edi,[edi] mov ecx,[edi+4] mov g_收包地址,ecx mov eax,[edi+8] sub eax,ecx mov g_收包包长, eax } 明文收包通用解决办法(); __asm { popad mov ebp,[esi+0x2874] retn } }
void MyDialog::OnBnClickedButton6() { DWORD Hook地址 = 0x00B92931; DWORD Hook子程序指针 = (DWORD)明文收包HOOKcalll; DWORD 跳转值 = Hook子程序指针 - Hook地址 - 5;
DWORD old = 0; VirtualProtect((PVOID)0x00B92931, 100, PAGE_EXECUTE_READWRITE, &old);
*(BYTE*)0x00B92931 = 0xE8; *(DWORD*)(0x00B92931 + 1) = 跳转值; *(BYTE*)(0x00B92931+5) = 0x90;
VirtualProtect((PVOID)0x00B92931, 100, old, &old); }
void MyDialog::OnBnClickedButton7() { DWORD old = 0; VirtualProtect((PVOID)0x00B92931, 100, PAGE_EXECUTE_READWRITE, &old);
*(WORD*)0x00B92931 = 0xAE8B; *(DWORD*)(0x00B92931 + 2) = 0x00002874;
VirtualProtect((PVOID)0x00B92931, 100, old, &old); }
|
明文收包通用解决办法函数是为了解决不明所以的问题,不嵌套这层函数游戏会崩溃。
至于为什么,不能理解。
发送封包实现任意功能
单个封包的功能是很单一的,比方说让他打怪就一定不会有走路
工具函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| BYTE Call_2字符转Byte(char ch1, char ch2) { int S; if ((ch1>=48)&&(ch1<=57)) { S = ch1 - 48; } else if ((ch1 >= 65) && (ch1 <= 70)) { S = ch1 - 65 + 10; }
if ((ch2 >= 48) && (ch2 <= 57)) { return S * 16 + ch2 - 48; } else if ((ch2 >= 65) && (ch2 <= 70)) { return S * 16 + ch2 - 65 + 10; } else return -1;
}
|
发送封包函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| void MyDialog::OnBnClickedButton8() { UpdateData(TRUE); CString 输入的待发包长 = EditKitsize; CString 输入的待发包内容 = EditKitContent; int 待发包长 = strtol((const char*)CW2A(输入的待发包长.GetBuffer(0)),NULL,16); string str = CW2A(输入的待发包内容.GetString()); byte 待发包内容[0x200]; int len = str.size() / 2; for (int i = 0; i < len;i++) { 待发包内容[i] = Call_2字符转Byte(str[2 * i], str[2 * i + 1]); }
DWORD 加密长 = (DWORD)待发包长 - 2; DWORD 加密地址 = (DWORD)待发包内容 + 2; DWORD 密钥 = 0; __asm { mov ecx, 0x00F84BA4 mov ecx, [ecx] mov ecx, [ecx] mov ecx, [ecx + 0x4] mov ecx, [ecx + 0x14] mov ecx, [ecx] lea ecx, [ecx + 0x54] mov 密钥, ecx } 加密call(密钥, 加密长, 加密地址, 加密地址); HWND hwnd = (HWND)FindWindowA("Lapis Network Class", 0); DWORD socket = GetWindowLongW(hwnd, GWL_USERDATA); socket = *(DWORD*)(socket + 0x38); send(socket, (const char*)待发包内容, 待发包长, 0); }
|
测试发现,此处在测试框发包只能发大写才有效,小写字母无法识别。
吃药封包call
通用功能函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void Call_发送封包(DWORD 待发包长,DWORD 待发包内容) { DWORD 加密长 = (DWORD)待发包长 - 2; DWORD 加密地址 = (DWORD)待发包内容 + 2; DWORD 密钥 = 0; __asm { mov ecx, 0x00F84BA4 mov ecx, [ecx] mov ecx, [ecx] mov ecx, [ecx + 0x4] mov ecx, [ecx + 0x14] mov ecx, [ecx] lea ecx, [ecx + 0x54] mov 密钥, ecx } 加密call(密钥, 加密长, 加密地址, 加密地址); HWND hwnd = (HWND)FindWindowA("Lapis Network Class", 0); DWORD socket = GetWindowLongW(hwnd, GWL_USERDATA); socket = *(DWORD*)(socket + 0x38); send(socket, (const char*)待发包内容, 待发包长, 0); }
|
分析明文发包发现:

实现吃药封包call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void Call_吃药封包(char* 药品名称) { byte b[0x16] = { 0x14,00,0x4f,00,00,00,00,00,00,00,00 ,00,00,00,00 ,00,00,00,00 ,00,00,00 }; T遍历背包信息 t; t.初始化背包信息(); for (int i = 0; i < t.d数量;i++) { if (strcmp(药品名称,t.背包列表[i].cGBK名字)==0) { *(WORD*)(b + 6) = t.背包列表[i].d所在背包; *(WORD*)(b + 8) = t.背包列表[i].d位置; Call_发送封包(0x16, (DWORD)b); break; } } }
|
调用吃药封包call
自动吃药
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| DWORD g_自动吃药开关标志 = 0; void 自动吃药线程函数() { T人物信息 t; while (g_自动吃药开关标志) { t.初始化人物信息(); if (t.d对象<t.d最大血量*0.8) { Call_吃药封包("輕型HP藥水"); Sleep(3000); } Sleep(1000); } }
void MyDialog::OnBnClickedButton16() { if (g_自动吃药开关标志) { g_自动吃药开关标志 = 0; SetDlgItemText(IDC_BUTTON16, L"开启自动吃药"); } else { ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)自动吃药线程函数, NULL, NULL,NULL); g_自动吃药开关标志 = 1; SetDlgItemText(IDC_BUTTON16, L"关闭自动吃药"); } }
|
攻击性技能封包call
hook明文发包,放3号键格子里的该技能id为CAC3分析:
[5956] 幻想神域 包长:c 包地址1e1fb2d0 包内容:0A005500C3CA46FFFEFF0000
[5956] 幻想神域 包长:c 包地址1e228a00 包内容:0A005500C3CA47FCFFFF0000
该技能放在5号格子里释放:
[5956] 幻想神域 包长:c 包地址1e1fcc90 包内容:0A005500C3CA21FFFEFF0000
放在2号格子里的技能id为CABE的技能
[5956] 幻想神域 包长:c 包地址1e1fdb20 包内容:0A005500BECA53B0FEFF0000
可以看出来技能封包结构为:
1
| 包长:c 包内容:0A00(真实包长) + 5500(放技能头) + BECA(技能id) + 53B0FEFF(怪物id) +0000 (这个实际上也和蓄力有关)
|
每次放完技能后都会有一个这样的封包:
1
| 包长:6 包内容:040043000000 似乎是心跳包
|
这个直接不发送也可以,但为了尽量保证安全,要尽量和游戏本身发送的封包一致,避免被封禁。游戏中每次放技能都会带一个这个心跳包,所以我们也照做。
用发包封包函数尝试判断分析无误
明文包加密发包工具:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| void Call_发送封包(DWORD 待发包长,DWORD 待发包内容) { DWORD 加密长 = (DWORD)待发包长 - 2; DWORD 加密地址 = (DWORD)待发包内容 + 2; DWORD 密钥 = 0; __asm { mov ecx, 0x00F84BA4 mov ecx, [ecx] mov ecx, [ecx] mov ecx, [ecx + 0x4] mov ecx, [ecx + 0x14] mov ecx, [ecx] lea ecx, [ecx + 0x54] mov 密钥, ecx } 加密call(密钥, 加密长, 加密地址, 加密地址); HWND hwnd = (HWND)FindWindowA("Lapis Network Class", 0); DWORD socket = GetWindowLongW(hwnd, GWL_USERDATA); socket = *(DWORD*)(socket + 0x38); send(socket, (const char*)待发包内容, 待发包长, 0); }
|
其他代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void Call_释放攻击性技能(DWORD 技能id,DWORD 怪物id) { BYTE a[12] = { 0x0A,0x00,0x55,0x00,0x00,0x00,0x00,0x00 ,0x00 ,0x00 ,0x00 ,0x00 }; *(WORD*)(a + 4) = (WORD)技能id; *(DWORD*)(a + 6) =怪物id; Call_发送封包(0xc, (DWORD)a);
BYTE b[6] = { 0x04,0x00,0x43,0x00,0x00,0x00 }; Call_发送封包(0x6, (DWORD)b); } bool g_自动打怪开关标志 = 0; void 打怪线程() { while (g_自动打怪开关标志) { Call_自动打一只最近的目标怪("暴躁鸚鵡"); Sleep(1000); } }
void MyDialog::OnBnClickedButton19() { if (g_自动打怪开关标志) { g_自动打怪开关标志 = 0; SetDlgItemText(IDC_BUTTON19, L"开启自动打怪"); } else { g_自动打怪开关标志 = 1; ::CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)打怪线程, NULL, NULL, NULL); SetDlgItemText(IDC_BUTTON19, L"关闭自动打怪"); } }
|
发现BUG:从没目标怪的区域跑进目标怪区域会崩溃!
寻路CALL
一般来说,得到了加密封包就可以实现几乎所有功能,但是封包只能用来实现走路,却很难实现寻路,因此寻路还是要靠调用游戏自身的寻路call来实现。游戏没有自带寻路功能的话就可以录制寻路点,或者用A星算法写自动寻路。
因此,寻找寻路call:
寻路call一般是线程循环寻路,所以单纯用发包断很多游戏是断不到的。因此可以通过寻路的目的地或寻路的标志位来找。
用寻路的目的地来找的时候,浮点数区间一定要大。尝试后,扫不到就把范围变大。不移动的时候目标点坐标不变化,寻路开始的时候目标点先变化一次,然后寻路过程中目标点一直不变化。
找到的所有结果如下:

内存布局和下写入断断下与否分析:

走路(直接点地形)也会断,说明与寻路call关联小一些。
此处分析过程有待完善!!!!!!!!!
跳过各种分析过程,找到参数比较多的call比较好,因为便于分析。
最终定位到寻路call位置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 009265B2 |> \0FBF96 C80100>movsx edx, word ptr [esi+1C8] 009265B9 |. 6A 01 push 1 ; 1 009265BB |. 6A 00 push 0 ; 0 009265BD |. 6A 00 push 0 ; 0 009265BF |. 8D4D E4 lea ecx, dword ptr [ebp-1C] 009265C2 |. 51 push ecx ; 目标坐标结构体(浮点x,浮点y,整形x,整形y) 009265C3 |. 8B0D 744BF800 mov ecx, dword ptr [F84B74] 009265C9 |. 52 push edx ; 3 疑似地图id,一般人物对象中有挂着,实际位置为[人物信息+3a4],没挂着就往上找来源呗 009265CA |. E8 711DDBFF call 006D8340 ; 取人物对象 009265CF |. 8BC8 mov ecx, eax ; ecx==人物对象 009265D1 |. E8 8AA7DCFF call 006F0D60 ; 寻路call 009265D6 |. 5F pop edi 009265D7 |. B0 01 mov al, 1 009265D9 |. 5E pop esi
|
用代码注入器进行检测:
先给12345678地址写入目标坐标结构体:

然后代码注入器内代码为:
1 2 3 4 5 6 7 8 9
| push 1 push 0 push 0 push 12345678 push 3 mov ecx,[0F84B74] call 06d8340 mov ecx,eax call 06f0d60
|
成功寻路!
因此该call可行。
寻路工具函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| void Call_寻路(float x, float y) { __try { byte 开辟的目标地址[0x10] = { 0 }; *(float*)开辟的目标地址 = x; *(float*)(开辟的目标地址 + 4) = y; *(int*)(开辟的目标地址 + 8) = (int)x; *(int*)(开辟的目标地址 + 0x0c) = (int)y; DWORD A=(DWORD)开辟的目标地址; __asm { pushad push 1 push 0 push 0 push A mov ecx, 0x0F84B74 mov ecx, [ecx] mov eax, 0x06d8340 call eax mov ecx, eax mov eax,[eax+0x0c] mov eax, [eax + 0x3a4] push eax mov eax, 0x06f0d60 call eax popad } } __except (1) { Call_输出调试信息("幻想神域 寻路失败!"); } }
|
调用过程代码:
1 2 3 4 5 6 7
| CString Xstr,Ystr; GetDlgItemTextW(IDC_EDIT1, Xstr); GetDlgItemTextW(IDC_EDIT2, Ystr); float x,y; x = _tstof(Xstr); y= _tstof(Ystr); Call_寻路(x, y);
|
这里当频繁调用该call的时候会卡死或者崩溃
重点:主线程调用
排除了所有可能的其他bug外的情况下,这是由于调用的这个call比较复杂,但我们频繁调用call的时候出现访问冲突。
解决方案:主线程调用。(就是说把call放到主线程调用,所有的功能都是按照一个队列执行的,这样不会产生冲突)
很多游戏短时间内运行不崩溃是运气好,当长时间运行的话比如说1,2个小时后崩溃就是因为没有主线程调用的原因。
让call由主线程来实现的原理就是:通过setwindowshookex来hook主线程,因为回调函数是主线程调用的,通过回调函数中调call来实现主线程调用call。然后用发送消息给主线程的消息队列来操作主线程帮你调用call。
代码如下:
发送消息并且通过lparam和rparam携带结构信息来通知主线程的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| void Msg_发送明文包(DWORD 包长, DWORD 包地址) { T封包参数 封包; 封包.包长 = 包长; 封包.包地址 = 包地址; SendMessageA(Call_获取窗口句柄(), g_My消息ID, ID_发送封包, (LPARAM)&封包); }
void Msg_寻路(float x, float y) { T寻路参数 寻路; 寻路.fx = x; 寻路.fy = y; SendMessageA(Call_获取窗口句柄(), g_My消息ID, ID_寻路, (LPARAM)&寻路); }
|
结构以及预定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #define ID_发送封包 1 #define ID_寻路 2
HHOOK g_Hook返回;
const DWORD g_My消息ID = RegisterWindowMessageA("MyMsyCode"); struct T封包参数 { DWORD 包长; DWORD 包地址; }; struct T寻路参数 { float fx; float fy; };
|
hook主线程和取消hook主线程的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| DWORD Call_Hook主线程() { HWND hGame = Call_获取窗口句柄(); DWORD ndThreadId = GetWindowThreadProcessId(hGame, NULL); if (ndThreadId != 0) { g_Hook返回 = SetWindowsHookEx(WH_CALLWNDPROC, Call_主线程回调函数, NULL, ndThreadId); } return 1; }
DWORD Call_卸载Hook主线程() { UnhookWindowsHookEx(g_Hook返回); return 1; }
|
主线程回调函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| LRESULT CALLBACK Call_主线程回调函数(int nCode, WPARAM wParam, LPARAM lparam) { CWPSTRUCT *lpArg = (CWPSTRUCT*)lparam; if (nCode == HC_ACTION) { if (lpArg->hwnd == Call_获取窗口句柄() && lpArg->message == g_My消息ID) { switch (lpArg->wParam) { T封包参数 *封包; T寻路参数 *寻路; case ID_发送封包: Call_输出调试信息("幻想神域 主线程调用发包\r\n"); 封包 = (T封包参数*)lpArg->lParam; Call_发送封包(封包->包长, 封包->包地址); return 1; break; case ID_寻路: Call_输出调试信息("幻想神域 主线程调用寻路\r\n"); 寻路 = (T寻路参数*)lpArg->lParam; Call_寻路(寻路->fx, 寻路->fy); return 1; break; } } } return CallNextHookEx(g_Hook返回, nCode, wParam, lparam);
}
|
然后自动打怪中只需要把原本的call_寻路换成msg_寻路就可以实现主线程调用call来代替dll的线程来调用call!
然后经测试发现长久时间后,自动打怪也不会让游戏崩溃,但是随后又发现有卡地形的情况出现。
完善自动打怪
寻路改善为防卡怪寻路,代码如下:(如果5秒没有到达目标点就跳出函数,重新遍历找最新怪物)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| void Call_寻到(float x, float y) { int i = 0; T人物信息 人物; 人物.初始化人物信息(); DWORD 人物对象 = 人物.d对象; float 距离; do { Msg_寻路(x, y); Sleep(500); DWORD Temp = *(DWORD*)(人物对象 + 0x10); FLOAT X = *(FLOAT*)(Temp + 0x154); FLOAT Y = *(FLOAT*)(Temp + 0x158); 距离 = sqrt((X - x)*(X - x) + (Y - y)*(Y - y)); i++; if (i>10) { break; } } while (距离>5); }
|
防止卡点打一只怪物的函数代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void Call_自动打一只最近的目标怪(char* 怪物名称) { int n=0; T链表信息遍历 t; t.距离排序信息(); for (int i = 0;i<t.d数量; i++) { if (strcmp(t.对象信息[i].cGBK名字, 怪物名称) == 0) { if (t.对象信息[i].d血量== t.对象信息[i].d最大血量) { Call_寻到(t.对象信息[i].fX, t.对象信息[i].fY); while (t.对象信息[i].d血量>0) { n++; DWORD temp2 = *(DWORD*)(t.对象信息[i].d对象 + 0x10); t.对象信息[i].fX = *(FLOAT*)(temp2 + 0x154); t.对象信息[i].fY = *(FLOAT*)(temp2 + 0x158); Call_寻到(t.对象信息[i].fX, t.对象信息[i].fY); Call_输出调试信息("幻想神域 攻击怪物\r\n"); Call_释放攻击性技能(0xCABD, t.对象信息[i].dID); Sleep(1000); DWORD temp = *(DWORD*)(t.对象信息[i].d对象 + 0xC); t.对象信息[i].d血量 = *(DWORD*)(temp + 0x8); if(n>20) { break; } } if (n > 20) { n = 0; continue; } break; } }
} }
|
上面代码中continue是精髓。
上面代码第一行未赋初始值0会直接崩溃,要看看是什么情况。
上面代码用于防止一直想走到怪的位置,但是怪的位置不可达时候可以刷新怪物位置,重新尝试寻路到那个怪新的位置。
1 2 3 4 5
| if (n > 20)//防止卡点(多久跳出循环,和跳出循环条件需要改善) { n = 0; continue;//不找最近的怪物,试着找下个近的怪物 }
|
上面代码用于当那个怪物半天都打不到的时候卡住了,跳出循环后依然可能打不到,因此跳出循环换一只怪
技能循环
判断是否有该技能代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| bool Call_是否有该技能(char* 技能名) { T遍历我的技能信息 t; t.初始化我的技能信息(); for (int i = 0;i<t.d数量;++i) { if (strcmp(t.技能列表[i].cGBK名字,技能名)==0) { return TRUE; } } return FALSE; }
|
判断技能是否无冷却代码:
1 2 3 4 5 6 7 8 9 10 11 12 13
| bool Call_该技能是否无冷却(char* 技能名) { T遍历CD中技能信息 t; t.初始化CD中技能信息(); for (int i = 0; i < t.d数量; ++i) { if (strcmp(t.技能列表[i].cGBK名字, 技能名) == 0) { return FALSE; } } return TRUE; }
|
技能优先级循环代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void Call_智能使用技能(DWORD 怪物id) { if (Call_是否有该技能("瞄準射擊")&& Call_该技能是否无冷却("瞄準射擊")) { Call_释放攻击性技能(0x0CABD, 怪物id); } else if (Call_是否有该技能("音速彈") && Call_该技能是否无冷却("音速彈")) { Call_释放攻击性技能(0x0CAC3, 怪物id); } else if (Call_是否有该技能("砲轟") && Call_该技能是否无冷却("砲轟")) { Call_释放攻击性技能(0x0CABE, 怪物id); } }
|
重点:设置不同的技能的时候,一定要有公共cd的考量。
能实现放不同的技能打不同的怪物。
依然有bug:
1.某些情况按了自动打怪停止以后依然会寻怪
2.放技能时候误伤到沿路的怪物过来打你,由于血条不满,因此人物对其无反应。
判断障碍点
在游戏中,当寻路为无法前进的点的时候会显示:

用该字符串作为突破口。
将“目的地為阻擋點,無法前進! ”转成字符集为:
1
| A5 D8 AA BA A6 61 AC B0 AA FD BE D7 C2 49 A1 41 B5 4C AA 6B AB 65 B6 69 A1 49 00
|
到ce中搜索:

对半修改发发现第一个地址是真正的字符串地址。
od中dd后下硬件访问断点
下了访问断点后发现正常寻路不会断下,只有走隔绝点才会断下
断下位置如下:
1
| 004149B1 |> /8A08 /mov cl, byte ptr [eax] ; 隔绝点访问断在此处
|
断下位置直接返回上一层到call处进行分析
1
| 008CB460 |. E8 2B95B4FF call 00414990 ; 隔绝点访问断在此处1
|
此处正常寻路不断下,非正常寻路断下。
继续返回上一层call:
1
| 0089290F |. FFD0 call eax ; 隔绝点访问断在此处2
|
断下秒断,说明此处除了非正常寻路,别的功能也回执行此处。
继续返回上一层call:
1
| 00889044 |. E8 17980000 call 00892860 ; 隔绝点访问断在此处3
|
也是断下秒断
继续返回上一层call:
1
| 00765A2D |. E8 BE351200 call 00888FF0 ; 隔绝点访问断在此处4
|
此处正常寻路不断下,非正常寻路断下。
继续返回上一层call:
1
| 006E7BC2 |. E8 99DC0700 call 00765860 ; 隔绝点访问断在此处5
|
此处断下不秒断了。但正常寻路和非正常寻路都会断下,说明此call到内层call(隔绝点访问断在此处4位置)之前有跳转,该跳转决定了是否为正常寻路!
正常寻路与非正常寻路分别单步运行观察跳转情况得出该跳转为关键跳转,如下:
1 2 3
| 007659C9 |. 33FF xor edi, edi ; di==0 007659CB |. 66:39BE 7C010>cmp word ptr [esi+17C], di ; 障碍点关键跳转 [esi+17C]==0为遇到障碍点 007659D2 |. 0F85 B4000000 jnz 00765A8C
|
由于此处[esi+17C]是个值,此处追esi没有意义,我们要的是esi+17c地址中的值得来源,因此我们需要的是追[esi+17c]这个整体。
然而向上追的过程中直到函数头esi被改变都没有看见[esi+17c],因此判断是在该过程中的call内改变的。从函数头esi被赋值开始往下单步并且dd esi+17c看果然发现经过以下call值发生变化。
1
| 0076594E |. FFD0 call eax ; [esi+17c]值清零
|
以及:
1
| 007659C4 |. E8 E77E0100 call 0077D8B0 ; [esi+17c]值的真正来源
|
进入上面这个真正来源的call进行单步执行分别判断正常寻路与非正常寻路进行分析:
定位到是哪里改变了[esi+17c]
1
| 0077D9D6 |> \66:8943 64 mov word ptr [ebx+64], ax ; 此处真正改变障碍点标志位
|
发现此处的ebx+64正是外面的esi+17c的地址.
分析发现也就是在外面的call没执行前的ecx+64 为标志位地址,执行完外面的call后改变该地址的值后则可用于判断是否有障碍点。
因此回到上一层call分析ecx的来源。
1 2 3 4 5 6 7 8 9
| 007659B0 |> \8B0D 7443F800 mov ecx, dword ptr [F84374] 007659B6 |. E8 C59EF1FF call 0067F880 ; [人物对象+2c]+118+64==障碍标志地址 007659BB |. 8BCF mov ecx, edi ; ecx+64==esi+17c==障碍标志地址 ecx==22BC5918 目的地结构信息 [ecx+10]==x [ecx+14]==y ecx== [人物对象+2c]+118 007659BD |. C745 FC 00000>mov dword ptr [ebp-4], 0 007659C4 |. E8 E77E0100 call 0077D8B0 ; [esi+17c]值的真正来源 标志位的值==word(2AAAAAAB*2AAAAAAB*([ecx+34+8]-[ecx+34+4])>>1) 007659C9 |. 33FF xor edi, edi ; di==0 007659CB |. 66:39BE 7C010>cmp word ptr [esi+17C], di ; 障碍点关键跳转 [esi+17C]==0为遇到障碍点 007659D2 |. 0F85 B4000000 jnz 00765A8C 007659D8 |. 393D 7443F800 cmp dword ptr [F84374], edi
|
上面的代码分析有误,仔细分析后结构如下:
ecx=[人物对象+2c]+118=寻路结构(该寻路结构在未寻路之前是未被初始化状态的)
ecx+10=目的点x
ecx+14=目的点y
ecx+1C=下一个要寻往的中间点x
ecx+20=下一个要寻往的中间点y
ecx+38=当前点和最终目标点的中间点数组起始地址
ecx+3c=当前点和最终目标点的中间点数组结束地址
ecx+64==上面数组的中间点个数
通过判断下一个要寻往的中间点x,y坐标来确定是否有障碍点,寻路后若有障碍点,该值为0!
因此寻路后通过判断该值来判断是否是障碍点。
封住函数如下:
1 2 3 4 5 6 7 8 9 10
| DWORD Call_寻路是否成功() { T人物信息 me; me.初始化人物信息(); DWORD Temp = *(DWORD*)(me.d对象 + 0x2c); DWORD A= *(DWORD*)(Temp +0x118+ 0x1c); Call_输出调试信息("幻想神域 寻路是否成功,返回%X\r\n",A); return A; }
|
打怪优化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| void Call_自动打一只最近的目标怪(char* 怪物名称) { int n=0; T链表信息遍历 t; t.距离排序信息(); for (int i = 0;i<t.d数量; i++) { if (strcmp(t.对象信息[i].cGBK名字, 怪物名称) == 0) { if (t.对象信息[i].d血量== t.对象信息[i].d最大血量) { Msg_寻路(t.对象信息[i].fX, t.对象信息[i].fY); Sleep(100); if (Call_寻路是否成功()) { Call_寻到(t.对象信息[i].fX, t.对象信息[i].fY); } else { Call_输出调试信息("幻想神域 卡点,找下只近的怪\r\n"); continue; } while (t.对象信息[i].d血量>0) { n++; DWORD temp2 = *(DWORD*)(t.对象信息[i].d对象 + 0x10); t.对象信息[i].fX = *(FLOAT*)(temp2 + 0x154); t.对象信息[i].fY = *(FLOAT*)(temp2 + 0x158); Msg_寻路(t.对象信息[i].fX, t.对象信息[i].fY); if (Call_寻路是否成功()) { Call_寻到(t.对象信息[i].fX, t.对象信息[i].fY); } else { Call_输出调试信息("幻想神域 正在攻击怪物不可寻路\r\n"); break; } Call_输出调试信息("幻想神域 攻击怪物\r\n"); Call_智能使用技能(t.对象信息[i].dID); Sleep(1000); DWORD temp = *(DWORD*)(t.对象信息[i].d对象 + 0xC); t.对象信息[i].d血量 = *(DWORD*)(temp + 0x8); } break; } }
} }
|
经测试,已无大碍,但背包满的时候会周围遍历异常,然后崩溃。

异常bug修复
实际上当掉落物品时候人物卡住不动是因为上来就遍历周围所有信息的时候,地上的物品也遍历进去了,然而地上的物品是没有血量等信息的,却还尝试读取血量偏移等信息,因此出现异常。
解决代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| void T链表信息遍历::初始化对象信息() { __try { ZeroMemory(对象信息, sizeof(T人物信息) * 1000); T人物信息 me; me.初始化人物信息(); DWORD 基地址中 = *(DWORD*)0x0F84B74; DWORD tmp = *(DWORD*)(基地址中 + 0x418); DWORD 链表头 = *(DWORD*)(tmp); DWORD 当前链表 = *(DWORD*)(tmp); int i=0; for (; 当前链表!=tmp; i++) { DWORD 当前对象 = *(DWORD*)(当前链表+0x0c); 对象信息[i].d对象 = 当前对象; 对象信息[i].dID= *(DWORD*)(当前对象 + 0x8); DWORD 人物属性 = *(DWORD *)(当前对象 + 0x0c); DWORD 名称长度 = 0; if (人物属性!=0) { 对象信息[i].d血量 = *(DWORD *)(人物属性 + 0x8); 对象信息[i].d最大血量 = *(DWORD *)(人物属性 + 0x24); 对象信息[i].d当前等级 = *(DWORD *)(人物属性 + 0x10); 名称长度 = *(DWORD *)(人物属性 + 0x114); if (名称长度 <= 0x10) { 对象信息[i].p名字 = (char *)(人物属性 + 0x100); strcpy(对象信息[i].cGBK名字, 对象信息[i].p名字); BIG52GBK(对象信息[i].cGBK名字); } else { 对象信息[i].p名字 = (char*)*(DWORD *)(人物属性 + 0x100); strcpy(对象信息[i].cGBK名字, 对象信息[i].p名字); BIG52GBK(对象信息[i].cGBK名字); } } DWORD 坐标信息 = *(DWORD *)(当前对象 + 0x10); if (坐标信息!=0) { 对象信息[i].fX = *(FLOAT *)(坐标信息 + 0x154); 对象信息[i].fY = *(FLOAT *)(坐标信息 + 0x158); 对象信息[i].fZ = *(FLOAT *)(坐标信息 + 0x15c); } 对象信息[i].f距离 = sqrt((对象信息[i].fX - me.fX)*(对象信息[i].fX - me.fX) + (对象信息[i].fY - me.fY)*(对象信息[i].fY - me.fY) + (对象信息[i].fZ - me.fZ)*(对象信息[i].fZ - me.fZ)); 当前链表 = *(DWORD*)(当前链表); } d数量 = i; } __except (1) { Call_输出调试信息("幻想神域 链表信息遍历出错!"); } }
|
自动任务
通过任务完成与未完成状态来找,或者任务是否接了来做突破口,或者通过任务id来做突破口。
最好的一般是通过任务完成状态来找最佳
一般是两种方案:
- 收集类的任务,收了足够的数量的物品任务完成,丢掉一件,任务恢复未完成状态。
- 接任务再放弃任务可以改变任务是否接了的状态,但很多游戏一旦放弃任务对象很可能被初始化了,因此此种方法很可能不行。
搜索状态类,一般没有都是0,但有的情况什么都有可能。
先尝试方案2,三种状态,任务完成,人物未接,任务接到是各自的标志位置。并且任务未接的时候状态为0。
用ce未知初始值进行筛选突破口。
最终找到唯一一个突破口:2ABAD3AE,二字节

od中dd该地址下访问断,
1
| 007080A4 |> \66:8B46 0E mov ax, word ptr [esi+E] ; 访问任务状态标志位断在此处 word ptr[esi+0e]==4==任务状态标志
|
经过一个二叉树结构追到头:
1
| 00AE1B8B |. E8 8086BFFF call 006DA210 ; 取[人物对象+0c],即人物信息的call word ptr[[[[[人物对象+0c]+3C4]+0C+4]+4]+0e]==4==任务状态标志
|
途中经过的二叉树结构如下:
1 2 3 4 5 6 7 8 9 10 11
| 00707F8A |. 8D9B 00000000 lea ebx, dword ptr [ebx] ; 当前任务正好在该二叉树根节点 word ptr[eax+0e]==4==任务状态标志 00707F90 |> 66:3970 0C /cmp word ptr [eax+C], si ; 疑似任务信息二叉树结构 根节点=eax +0]左子树 +8]右子树 +11]标志位!=0跳出循环 00707F94 |. 73 05 |jnb short 00707F9B 00707F96 |. 8B40 08 |mov eax, dword ptr [eax+8] 00707F99 |. EB 04 |jmp short 00707F9F 00707F9B |> 8BD0 |mov edx, eax 00707F9D |. 8B00 |mov eax, dword ptr [eax] 00707F9F |> 8078 11 00 |cmp byte ptr [eax+11], 0 00707FA3 |.^ 74 EB \je short 00707F90 00707FA5 |> 8B41 04 mov eax, dword ptr [ecx+4] 00707FA8 |. 3BD0 cmp edx, eax
|
该二叉树根节点:[[[[人物对象+0c]+3C4]+0C+4]+4]
+0]左子树 +8]右子树 +11]标志位!=0表示跳出循环 (BYTE)(经常忘记标志位类型!!)
+0C]为任务id(WORD) +0e]为任务完成状态(WORD)
任务完成状态标志=word ptr[[[[[人物对象+0c]+3C4]+0C+4]+4]+0e] (0表示未接取,3表示未完成状态,4表示完成状态)
代码如下:
结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| struct T任务 { WORD wID; WORD w完成度; DWORD d对象; char* p名称; char pgbk名称[100]; };
struct T任务遍历 { T任务 列表[1000]; DWORD d数量; void c初始化任务信息(); void c递归遍历(DWORD); };
|
函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| DWORD g_任务下标 = 0; void T任务遍历::c递归遍历(DWORD 根节点) { BYTE 标志位 = *(DWORD*)(根节点+0x11); if (标志位==0) { DWORD 左子树 = *(DWORD*)(根节点); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); 列表[g_任务下标].wID= *(WORD*)(根节点 + 0x0c); 列表[g_任务下标].d对象 = 根节点; 列表[g_任务下标].w完成度= *(WORD*)(根节点 + 0x0e); g_任务下标++; c递归遍历(左子树); c递归遍历(右子树); } }
void T任务遍历::c初始化任务信息() { __try { g_任务下标 = 0; T人物信息 me; me.初始化人物信息(); DWORD 根节点 = *(DWORD*)(me.d对象 + 0x0c); 根节点 = *(DWORD*)(根节点 + 0x3c4); 根节点 = *(DWORD*)(根节点 + 0x10); 根节点 = *(DWORD*)(根节点 + 0x4); c递归遍历(根节点); d数量 = g_任务下标; } __except(1) { Call_输出调试信息("幻想神域 初始化任务信息出错!");
} }
|
调用如下:
1 2 3 4 5 6 7
| T任务遍历 t; t.c初始化任务信息(); Call_输出调试信息("幻想神域 任务数量:%d\r\n",t.d数量); for (int i = 0;i<t.d数量;i++) { Call_输出调试信息("幻想神域 任务id:%X 任务对象:%X 任务完成度:%X\r\n",t.列表[i].wID, t.列表[i].d对象, t.列表[i].w完成度); }
|
效果如下:

分析可知,
- 任务完成度为0表示当前可接而未接的任务
- 任务完成度为1表示已经完成提交的任务
- 任务完成度为3表示已接但未完成的任务
- 任务完成度为4表示已接并且完成可交的任务
然而实际上它只能把已完成和已接取的任务显示出来,可接的任务能不能显示是不一定的
完善任务遍历名字信息(比较难,有待提升)
直接通过任务名字找找看也行。
或者到明文发包call调用链中分析过程,在明文封包call上通过取消任务来断下,在堆栈中直接搜索该任务id,直接定位到取消任务call。取消任务call传进去的id的上一层偏移很有可能是任务对象,里面可能储存着任务名字。
按照上面的方案:

返回到划线地址到达取消任务call如下:
1 2 3 4 5 6
| 00B3BDB8 |. 8B40 18 mov eax, dword ptr [eax+18] 00B3BDBB |. 68 FFFF0000 push 0FFFF 00B3BDC0 |. 6A 05 push 5 00B3BDC2 |. 50 push eax ; 任务id 00B3BDC3 |. E8 C866B7FF call 006B2490 ; 取消任务call 00B3BDC8 |. 83C4 0C add esp, 0C
|
如上面汇编,追eax追到[eax+18],00B3BDB8处的eax很可能为任务对象,在此处dd eax观察成员元素,发现了疑似中文字符


与游戏中的任务名称相对应。
追到此处:
1
| 00B3BDAE |. 8B86 3C010000 mov eax, dword ptr [esi+13C] ; 取消任务的任务id==[[esi+13c]+18]==3ed
|
小重点难点
发现esi替换成寄存器的值,放开断点不断下来了!!!!!!!!
说明这个esi+13c 地址中存的是个临时会变的值,应该追[esi+13c]的整体。但该函数上面到函数头已经没有得追了。可以直接对esi+13c所在地址下写入断,来追[esi+13c]的整体来源
esi+13c地址中存进的值会随着玩家选不同任务而存不同的值
断到此处:
1
| 00B3DF33 |. 8986 3C010000 mov dword ptr [esi+13C], eax ; 取消任务的任务id==[eax+18]==3ed
|
将[esi+13c]整个替换为eax。
继续向上追:
1 2 3 4 5 6 7
| 00B3E1C6 |. 8B11 mov edx, dword ptr [ecx] 00B3E1C8 |. 50 push eax 00B3E1C9 |. 8B82 E8000000 mov eax, dword ptr [edx+E8] 00B3E1CF |. 897D 08 mov dword ptr [ebp+8], edi 00B3E1D2 |. FFD0 call eax ; eax=[[[1789168]]+0E8] 00B3E1D4 |. 8BF0 mov esi, eax ; 取消任务的任务id==[eax+18]==3ed 00B3E1D6 |. 33DB xor ebx, ebx
|
eax来源于上面的call
进call追:
又追到这个call
1 2 3 4 5 6 7
| 007C4A53 |. 8B45 08 mov eax, dword ptr [ebp+8] 007C4A56 |. 50 push eax ; 任务id 007C4A57 |. 68 8C927E01 push 017E928C 007C4A5C |. E8 1FF3E9FF call 00663D80 ;通过任务id取任务对象call 007C4A61 |. 83C4 08 add esp, 8 007C4A64 |. 5D pop ebp 007C4A65 \. C2 0400 retn 4 ; 取消任务的任务id==[eax+18]==3ed
|
尝试代码注入器:
1 2 3 4 5
| push 3ED push 017E928C call 00663d80 add esp,8 mov [00178000],eax
|
成功观察到结果:

因此追到该call传进任务id获得任务对象后
+18] 表示任务id
+20]表示任务名称
+30]表示任务名称长短
继续往里分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 00663D87 |. 56 push esi 00663D88 |. 57 push edi 00663D89 |. 8B7D 08 mov edi, dword ptr [ebp+8] 00663D8C |. 8D45 0C lea eax, dword ptr [ebp+C] 00663D8F |. 50 push eax 00663D90 |. 8D4D F0 lea ecx, dword ptr [ebp-10] 00663D93 |. 51 push ecx 00663D94 |. 8BCF mov ecx, edi 00663D96 |. E8 B5D91600 call 007D1750 ; eax来源于该call,并且eax是个栈地址 00663D9B |. 8B30 mov esi, dword ptr [eax] 00663D9D |. 85F6 test esi, esi ; game.017E92A4 00663D9F |. 8B57 04 mov edx, dword ptr [edi+4] 00663DA2 |. 8B58 04 mov ebx, dword ptr [eax+4] ; 取消任务的任务id==[[[eax+4]+10]+18]==3ed 00663DA5 |. 8955 FC mov dword ptr [ebp-4], edx 00663DA8 |. 74 04 je short 00663DAE 00663DAA |. 3BF7 cmp esi, edi
|
此处eax往上追发现eax是个栈地址。
遇到这种情况,有巧妙的办法就是进行替换。
断下00663D9B的时候发现eax和ebp的关系是eax相当于ebp-10
便宜表达式直接替换成追:取消任务的任务id==[[[ebp-10+4]+10]+18]==3ed
相当于要追[ebp-0C],相当于追第一个参数+4的地址中的值是什么
进call继续分析。发现:

继续向上追追到二叉树。
取消任务的任务id==[[[[017E928C+4]+4]+10]+18]==3ed
+4]+4]后为根节点。结构分析如下:

名称长度处理都不用分析了,因为一个游戏里都是一样的。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| void T任务库遍历::c递归遍历(DWORD 根节点) { BYTE 标志位 = *(BYTE*)(根节点 + 0x15); if (标志位 == 0) { DWORD 左子树 = *(DWORD*)(根节点); DWORD 右子树 = *(DWORD*)(根节点 + 0x8); 列表[g_任务下标].d对象 = *(DWORD*)(根节点 + 0x10); 列表[g_任务下标].wID = *(WORD*)(列表[g_任务下标].d对象 + 0x18); DWORD 名称长度 = *(DWORD*)(列表[g_任务下标].d对象 + 0x1c+0x18); if (名称长度>=0x10) { DWORD temp=*(DWORD*)(列表[g_任务下标].d对象 + 0x1c + 0x4); 列表[g_任务下标].p名称 = (char*)temp; } else { 列表[g_任务下标].p名称 = (char*)(列表[g_任务下标].d对象 + 0x1c + 0x4); } strcpy(列表[g_任务下标].pgbk名称, 列表[g_任务下标].p名称); BIG52GBK(列表[g_任务下标].pgbk名称); g_任务下标++; c递归遍历(左子树); c递归遍历(右子树); } }
void T任务库遍历::c初始化任务库信息() { __try { g_任务下标 = 0; DWORD 根节点 = *(DWORD*)(0x17E928C+0x4); 根节点 = *(DWORD*)(根节点 + 0x4); c递归遍历(根节点); d数量 = g_任务下标; Call_输出调试信息("幻想神域 任务库数量:%d\r\n", d数量); } __except (1) { Call_输出调试信息("幻想神域 读取任务库信息出错!");
} }
|
效果图:

注意点:

不要在递归中嵌套递归,否则很容易出问题!!!!
完善之前的任务遍历:
任务遍历的关联任务名称的改善的调用代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void MyDialog::OnBnClickedButton22() { T任务遍历 t; t.c初始化任务信息(); T任务库遍历 t2; t2.c初始化任务库信息(); for (int i = 0; i < t.d数量; i++) { for (int j = 0; j < t2.d数量; j++) { if (t.列表[i].wID == t2.列表[j].wID) { t.列表[i].p名称 = t2.列表[j].p名称; strcpy(t.列表[i].pgbk名称, t.列表[i].p名称); BIG52GBK(t.列表[i].pgbk名称); Call_输出调试信息("幻想神域 任务id:%X 任务对象:%X 任务完成度:%X 任务名称:%s\r\n", t.列表[i].wID, t.列表[i].d对象, t.列表[i].w完成度, t.列表[i].pgbk名称); } } } }
|
效果图:

交接任务封包
hook明文发包,进行分析
放弃任务封包:
[2924] 幻想神域 包长:c 包地址30f4ef60 包内容:0A001D000500ED030000FFFF
[2924] 幻想神域 包长:8 包地址3a52e500 包内容:0600C50000000000
[2924] 幻想神域 包长:d 包地址3a4a8ee0 包内容:0B00C50001000000ED03000002
打开npc安妮塔对话框封包:
[2924] 幻想神域 包长:8 包地址3aedf850 包内容:06000300D9FEFFFF
[2924] 幻想神域 包长:8 包地址3a52e698 包内容:0600030000000000
[2924] 幻想神域 包长:8 包地址3aedf858 包内容:06005800D9FEFFFF
打开npc丝卡蒂对话框封包:
[2924] 幻想神域 包长:8 包地址120af320 包内容:06000300E0FEFFFF
[2924] 幻想神域 包长:8 包地址120afff0 包内容:0600030000000000
[2924] 幻想神域 包长:8 包地址121ec0a0 包内容:06005800E0FEFFFF
[2924] 幻想神域 包长:6 包地址3aedf7a8 包内容:040057000000
关闭npc安妮塔对话框封包:
[2924] 幻想神域 包长:4 包地址3aedcb38 包内容:02006900
关闭npc丝卡蒂对话框封包::
[2924] 幻想神域 包长:4 包地址2298dd20 包内容:02006900
接收丝卡蒂任务LV1.逃脱任务封包:
[2924] 幻想神域 包长:c 包地址357383c0 包内容:0A001D000100ED030000FFFF
[2924] 幻想神域 包长:4 包地址3aede3d8 包内容:02006900
[2924] 幻想神域 包长:8 包地址3a52e530 包内容:0600C50000000000
[2924] 幻想神域 包长:d 包地址3ae93700 包内容:0B00C50001000000ED03000001
结合id进行分析:

如果我们发送可以实现功能的封包以后,游戏会自动帮我们发后面的跟包,那就不需要我们自己手动发跟包了。
测试发现接任务的时候只发*[2924] 幻想神域 包长:c 包地址357383c0 包内容:0A001D000100ED030000FFFF* 这个包就直接游戏会自动帮我们发后面的三个包,并且游戏里任务也接取了。这说明其他三个包类似说拜拜。
也就是说接任务只需要分析如下包:
交接任务封包分析
[2924] 幻想神域 包长:c 包地址357383c0 包内容:0A001D000100ED030000FFFF
0A00 1D00 0100 ED030000 FFFF
放弃任务和上面同理,也只需要分析这个:
[2924] 幻想神域 包长:c 包地址30f4ef60 包内容:0A001D000500ED030000FFFF
0A00 1D00 0500 ED030000 FFFF
这两封包唯一的不同只有0100和0500的差异
分析结果:
| 0A00 |
1D00 |
0500 |
ED030000 |
FFFF |
| 包长 |
包头 |
操作方式 |
任务id |
包尾(暂时固定) |
操作方式: 1表示接受任务,2表示交任务,5表示放弃任务
开关npc对话框封包分析
并且实际测试发现取消任务并不需要开关npc对话框。(不开关npc对话框就交接任务有隐性的被封禁风险)
而交和接任务却必须打开npc对话框才能成功执行
以上符合游戏操作逻辑,并且测试发现有交接任务的距离限制
分析打开npc丝卡蒂对话框封包:
[2924] 幻想神域 包长:8 包地址120af320 包内容:06000300E0FEFFFF
[2924] 幻想神域 包长:8 包地址120afff0 包内容:0600030000000000
[2924] 幻想神域 包长:8 包地址121ec0a0 包内容:06005800E0FEFFFF 发送这个包对话框打开
[2924] 幻想神域 包长:6 包地址3aedf7a8 包内容:040057000000 会自动跟这个包
有两个包不会自动跟。因此这两个封包我们要自己发。
比对这组发包进行分析:
打开npc安妮塔对话框封包:
[2924] 幻想神域 包长:8 包地址3aedf850 包内容:06000300D9FEFFFF
[2924] 幻想神域 包长:8 包地址3a52e698 包内容:0600030000000000
[2924] 幻想神域 包长:8 包地址3aedf858 包内容:06005800D9FEFFFF
测试发现选择某个npc的时候会发送06000300 D9FEFFFF,而取消选择npc的时候会发送这个06000300 00000000
但发送这两个包本身不能反映在游戏内有变化
06000300 D9FEFFFF 选择id为D9FEFFFF 的npc
06000300 00000000 清空选择id为D9FEFFFF 的npc
06005800 D9FEFFFF 打开id为D9FEFFFF的npc
最后,关闭对话框封包也不会在游戏中有反应
之前一直都有的一个严重的bug未解决

有些npc的名称中带有换行符。之前的遍历中之所以看不到是因为我们过滤掉了没有幻想神域这四个字的调试行,因此看不见蓝圈的信息。
因此名字信息写入的时候要写全。比如”村長夫人\n絲卡蒂”才是对象为1E57F100对应的npc的完整的名称。
函数实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| void Call_交接任务封包(char* 任务名称, DWORD 操作类型) { DWORD 任务id = 0; byte b[0xc] = { 0x0a,00,0x1d,00,00,00,00,00,00,00,0xff,0xff }; T任务遍历 t; t.c初始化任务信息(); T任务库遍历 t2; t2.c初始化任务库信息(); for (int i = 0; i < t.d数量; i++) { for (int j = 0; j < t2.d数量; j++) { if (t.列表[i].wID == t2.列表[j].wID) { t.列表[i].p名称 = t2.列表[j].p名称; strcpy(t.列表[i].pgbk名称, t.列表[i].p名称); BIG52GBK(t.列表[i].pgbk名称); } } } for (int i = 0; i < t.d数量;i++) { if (strcmp(任务名称,t.列表[i].pgbk名称)==0) { 任务id = t.列表[i].wID; break; } } *(WORD*)(b + 4) = (WORD)操作类型; *(DWORD*)(b + 6) = 任务id; Msg_发送明文包(0xc,(DWORD)b); } void Call_打开NPC对话框封包(char* NPC名称) { DWORD NPCID = 0; byte bbb[0x8] = { 0x06,00,0x03,00,00,00,00,00 }; byte bb[0x8] = { 0x06,00,0x03,00,00,00,00,00 }; byte b[0x8] = { 0x06,00,0x58,00,00,00,00,00 }; T链表信息遍历 t; t.初始化对象信息(); for (int i = 0; i < t.d数量;i++) { if (strcmp(t.对象信息[i].cGBK名字, NPC名称)==0) { NPCID = t.对象信息[i].dID; break; } } *(DWORD*)(b + 4) = NPCID; *(DWORD*)(bb + 4) = NPCID; Msg_发送明文包(0x8, (DWORD)bb); Msg_发送明文包(0x8, (DWORD)bbb); Msg_发送明文包(0x8, (DWORD)b);
}
|
调用代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| void MyDialog::OnBnClickedButton24() { Call_打开NPC对话框封包("村長夫人\n絲卡蒂"); }
void MyDialog::OnBnClickedButton25() { Call_交接任务封包("Lv1.送貨",1); }
|
先hook主线程,然后站在村長夫人\n絲卡蒂身边,按打开npc按钮,再按接任务按钮,效果顺利达成。
自动任务逻辑


注意,快速循环若无延时会崩溃
任务判读通过判断上一个任务已完成并且下一个任务未完成来决定下一个任务做哪个任务
一键定位基地址和偏移
定位工具:
注入定位工具,点新建,如下填写

游戏更新一般改变:
- 偏移
- 基地址
- call的目标地址
od中尽量复制完整的子程序
用一个记事本把内容装进去,尽量别留空行。
定位工具中选择打开之前新建的.xml后缀的定位文件,显示如下:

在上图中双击想要定位的基地址:
出现一个箭头如下:

然后在这周围选中一片区域,点设置特征,如下:

然后在自己存子程序的txt文件右键点设置,如下:

定位F3C570的例子填写如下

然后在自己存子程序的txt文件右键点开始定位:输出结果:

只显示一个代表定位成功!
如果显示很多条,效果如下

代表游戏中具备该特征的不止一个,因此无效
选择特征区域的大小很重要,选得多,特征多,但失效几率也大,特征码要在能够少的情况下尽量少。
先筛除到只有一条,然后逐渐尝试减小特征码
直接在特征码字眼上右键开始定位,可以一步定位所有结果
定位call的时候注意属性要选call,如下:

int3是不能当特征码来用的
小技巧
(1)有些极特殊的情况能用来定位的特征区域太少了,子程序很短的情况下,右键查找所有的常量,换地方定位。(但偏移不能换位置定位,只有基地址和call可以换地方定位)
(2)定位call内部某个什么的时候,可以用定位到call的目标地址加个偏移拿到目标定位
33后esp=前面esp+c
【后esp+10】==[前面esp+c+10]
参数和局部变量可以直接跳堆栈(查找地址,ctrl+L)