一年一度的重装系统“大工程”又浩浩荡荡的开工了。
整理去年一年的工具及资料,今天起陆续放一点以前给客户做的游戏辅助的手记。(一年多了,客户应该不会介意吧)
废话不多说,今天是第一篇。
《龙翔密传》分析手记
选怪
突破口:
CE搜索变化值,不停选怪取消怪,定位到如下代码:
00413b5e - 89 be b0 00 00 00 - mov [esi+000000b0],edi
稍微回溯一下
00435BDC /E9 4B090000 jmp 0043652C
00435BE1 |8B0D 08536300 mov ecx, dword ptr [635308]
00435BE7 |8B11 mov edx, dword ptr [ecx]
00435BE9 |8B46 0C mov eax, dword ptr [esi+C]
00435BEC |8B52 34 mov edx, dword ptr [edx+34]
00435BEF |6A 00 push 0 ; 固定参数
00435BF1 |50 push eax ; 怪物ID
00435BF2 |FFD2 call edx ; 选怪CALL edx = 0048ac00
该CALL就是选怪CALL。
该CALL 可以直接调用实现选怪
_asm
{
push 0 // 固定参数
push id // 怪物ID
call 0x0048ac00
}
怪物列表
突破口:
根据选怪的反汇编代码,跟进00435BF2这个CALL发现了另一个CALL:
00413AA9 |. 53 push ebx ; 怪物ID
00413AAA |. FFD2 call edx ; 根据怪物ID,取对象基址
再跟进此CALL发现另一个CALL:
00412CA5 |. 50 push eax ; 怪物ID指针
00412CA6 |. 8D4C24 14 lea ecx, dword ptr [esp+14]
00412CAA |. 51 push ecx ; 上层CALL的返回地址
00412CAB |. E8 30260000 call 004152E0 ; 获得怪物列表
//
00412CAB |. E8 30260000 call 004152E0 ; 获得怪物列表
这个CALL很关键了,是遍历怪物列表的关键,以下是该CALL的分析:
004152E0 /$ 55 push ebp
004152E1 |. 8BEC mov ebp, esp
004152E3 |. 8B4F 18 mov ecx, dword ptr [edi+18] ; 超级像怪物列表(ecx = [01c398b8 + 18] = 01c45390)
004152E6 |. 8B41 04 mov eax, dword ptr [ecx+4] ; eax = [[[635210]+90+18]+4] = 20904618
004152E9 |. 83EC 10 sub esp, 10
004152EC |. 8078 15 00 cmp byte ptr [eax+15], 0
004152F0 |. 53 push ebx
004152F1 |. 56 push esi
004152F2 |. 8BF1 mov esi, ecx
004152F4 |. 75 1E jnz short 00415314
004152F6 |. 8B4D 0C mov ecx, dword ptr [ebp+C] ; 怪物ID
004152F9 |. 8B09 mov ecx, dword ptr [ecx]
004152FB |. EB 03 jmp short 00415300
004152FD | 8D49 00 lea ecx, dword ptr [ecx]
00415300 |> 3948 0C /cmp dword ptr [eax+C], ecx
00415303 |. 7D 05 |jge short 0041530A
00415305 |. 8B40 08 |mov eax, dword ptr [eax+8]
00415308 |. EB 04 |jmp short 0041530E
0041530A |> 8BF0 |mov esi, eax ; esi 将作为本CALL 的返回值
0041530C |. 8B00 |mov eax, dword ptr [eax]
0041530E |> 8078 15 00 |cmp byte ptr [eax+15], 0
00415312 |.^ 74 EC \je short 00415300
经过查看00415300 到00415312代码所访问的内存模型,判断怪物列表为一颗二叉树. 上面首先获得eax,即怪物二叉树的根节点. eax = [[[[635210]+90+18]+4] 然后从00415300 到00415312是重点,这里遍历二叉树找出对应ID的对象. 左树是小([eax]),右树是大([eax+8]).[eax+4]是父节点.[eax+c]是ID. [eax+15]有点特别,是一个bool类型,通过反汇编代码可以看出,此变量为整个遍历的结束条件. 所以,大胆推测[eax+15]标示了该节点的前一个节点是否是选中状态.如果为选中状态则为true.
通过调用此CALL的上层CALL : 00412CEF |> \8B47 10 mov eax, dword ptr [edi+10] ; 可以看出[eax+10]就是怪物对象地址 所以可以声明一个数据结构如下:
typedef struct _NPC
{
NPC* LeftSmall;
NPC* Parent;
NPC* RightBig;
int id;
DATA* pData;
}NPC;
根据反汇编代码004152EC到00415312写出大致的模拟算法如下:
NPC* GetAddressByID(int nID)
{
NPC* a = A.Head;
NPC* temp = NULL;
NPC* find = NULL;
if(a->selected == false)
{
int id = nID;
do
{
if(a->id < id)// 如果当前ID小于给定的ID,则往大的方向遍历
{
temp = a->RigthBig;
}
else
{
find = a;
a = a->LeftSmall;
}
}while(a->selecte != false);
}
return find;
}
下面是根据分析资料实现遍历怪物列表的算法:(前序递归)
CArray<DATA*,DATA*&> NPCArray; void PreOrder(NPC* Tree) { if(Tree->id != 0) { NPCArray.Add(Tree->pData); PreOrder(Tree->LeftSmall); PreOrder(Tree->RightBig); } }
注意用此根节点遍历出来包括了怪物对象,还包括其他一些无用的对象,需要过滤.
这里过滤的方式是物品的话,最大血量及当前血量都是0,可以以此作为判断是否是物品的根据.肯定二叉树叶子节点有一个变量是标明该对象的类型的,是背包还是怪物还是地上的物品等等.目前没有分析了.暂时用最大血量跟当前血量做判断.
打怪
突破口:
OD在GameClient模块搜索"skill"查找到Send_Skill... 顺藤摸瓜,回溯几层CALL后,发现一个switch分支,当为7的分支的时候就是打怪
00423317 |. E8 14200000 call 00425330
0042331C |. 5F pop edi
0042331D |. 5E pop esi
0042331E |. 8BE5 mov esp, ebp
00423320 |. 5D pop ebp
00423321 |. C3 retn
00423322 |> 56 push esi ; esi = 上层ECX (DWORD [esi] 恒定为5e6e68); Case 7 of switch 0042327E
00423323 |. E8 D81C0000 call 00425000 ; **************真正的打怪CALL 距离不够会自动靠近一步
00423328 |. 5F pop edi
00423329 |. 5E pop esi
0042332A |. 8BE5 mov esp, ebp
0042332C |. 5D pop ebp
0042332D |. C3 retn
直接CALL 00425000 就会对当前选定的怪进行攻击.
esi = 上层ECX。
以下是上层代码:
00418F0F |. 8B8B CC010000 mov ecx, dword ptr [ebx+1CC] ; ecx = [上层ecx + 1cc] 上层ECX = 1EB35128
00418F15 |. 85C9 test ecx, ecx
00418F17 |. 74 06 je short 00418F1F
00418F19 |. 8B01 mov eax, dword ptr [ecx]
00418F1B |. 8B10 mov edx, dword ptr [eax]
00418F1D |. FFD2 call edx ; edx = 00423210,内含真正的打怪CALL
该层的ECX又等于[该层上层ECX + 1cc],经过实测该层上层ECX恒定为1EB35128(此值非常眼熟,挺像怪物列表里的一个值,有待考证)
经过以上分析,打怪CALL的调用代码如下:
_asm
{
mov eax,[1EB35128+1CC]
push eax
call 0x00425000
}
上面的ECX = 1EB35128,此值非恒定,经过跟踪发现此为一个NEW出来的地址.
寻找过程:
发现DWORD [esi]恒定为56e6e8,直接搜索常数,发现有两个引用的地方,一个是构造函数,一个是析构函数,用构造函数来突破.构造函数附近代码如下:
0041FC91 |. E8 08771300 call <jmp.&MSVCR90.operator new> ; !!!!!!!!!!!!!这里NEW了一个对象,打怪CALL需要用到
0041FC96 |. 83C4 04 add esp, 4
0041FC99 8945 F0 mov dword ptr [ebp-10], eax
0041FC9C C645 FC 01 mov byte ptr [ebp-4], 1
0041FCA0 3BC7 cmp eax, edi
0041FCA2 |. 74 09 je short 0041FCAD
0041FCA4 |. 56 push esi
0041FCA5 |. 50 push eax
0041FCA6 |. E8 D5330000 call 00423080
OK,既然是NEW出来的,那就给它做个内存补丁:
在下面找一空白处(关键是见缝插针,不能覆盖原来的代码)
填写如下代码:
0041FD31 A3 0AFD4100 mov dword ptr [41FD0A], eax
0041FD36 8945 F0 mov dword ptr [ebp-10], eax
0041FD39 E9 15010000 jmp 0041FE53
0041FE53 C645 FC 01 mov byte ptr [ebp-4], 1
0041FE57 ^ E9 44FEFFFF jmp 0041FCA0
修改完毕后,原来的代码变成了这样:
0041FC91 |. E8 08771300 call <jmp.&MSVCR90.operator new> ; !!!!!!!!!!!!!这里NEW了一个对象,打怪CALL需要用到
0041FC96 |. 83C4 04 add esp, 4
0041FC99 E9 93000000 jmp 0041FD31
0041FC9E 90 nop
0041FC9F 90 nop
0041FCA0 |. 3BC7 cmp eax, edi
0041FCA2 |. 74 09 je short 0041FCAD
0041FCA4 |. 56 push esi
0041FCA5 |. 50 push eax
0041FCA6 |. E8 D5330000 call 00423080 ; 此CALL里eax填充56E6E8
0041FCAB |. EB 02 jmp short 0041FCAF
注意:因为新增的代码是在.text区段,而该区段是不能写的,所以在测试的时候用PE Explorer修改该区段为可写.否则
004236D9 A3 D8364200 mov dword ptr [4236D8], eax这句代码将出错.用代码实现的时候注意修改内存属性。
(更新)在第一个测试版本中发现该CALL因为会判断怪的距离,不够的话会自动靠近.经过实测,当此CALL尚未执行完毕(即正在自动靠近怪物的时候),又调用此CALL的时候会让游戏崩溃,由此判定此CALL不是线程安全的函数,需要调用者确保线程安全.所以这个CALL太鸡肋!继续找个完美的CALL。。。
004235B5 |> \8B42 08 mov eax, dword ptr [edx+8] ; Case 3 of switch 00423555
004235B8 |. 8B4A 0C mov ecx, dword ptr [edx+C]
004235BB |. 8B52 10 mov edx, dword ptr [edx+10]
004235BE |. 52 push edx ; edx = 1eb怪物ID
004235BF |. 51 push ecx ; 1
004235C0 |. 50 push eax ; 0
004235C1 |. E8 9A120000 call 00424860 ;
之前的CALL实际上是消息循环自动调用的,检查某个标记存在则自动调用之前找到的打怪CALL.而这里这个CALL可以理解为正是设定标志的.设定后可以让消息循环自动调用.
注意!!!!!!!!!!!调用此CALL,需要内部的ESI赋值
以下是上个CALL的ESI赋值
0048B85A |. /74 54 je short 0048B8B0
0048B85C |. |8B0D 10526300 mov ecx, dword ptr [635210]
0048B862 |. |8B51 50 mov edx, dword ptr [ecx+50]
0048B865 |. |8B8A CC010000 mov ecx, dword ptr [edx+1CC] ; 为打怪CALL,提供的ESI
0048B86B |. |C74424 50 020>mov dword ptr [esp+50], 2
0048B873 |. |BA 03000000 mov edx, 3
0048B878 |. |66:895424 08 mov word ptr [esp+8], dx
0048B87D |. |8B10 mov edx, dword ptr [eax]
打怪代码实现:
_asm
{
mov ecx,[635210]
mov edx,[ecx+50]
mov esi,[edx+1cc]
push 0x6d//怪物ID
push 1
push 0
call 00424860
}
怪物对象:
+14 ID范围的最大值
+48 怪物ID
+11C X坐标
+120 Y坐标
名字:[[+158]+8]+8+4
突破口:CE搜名字,找到对应的怪后,下内存访问断点
血量:
当前:[[[+158]+8]+80]
最大:[[[+158]+8]+80+8]
004191A2 |. 8B86 58010000 mov eax, dword ptr [esi+158] ; 读名字的关键开始
004191A8 |. 8B40 08 mov eax, dword ptr [eax+8]
004191AB |. 83C0 08 add eax, 8
004191AE |. 8378 18 10 cmp dword ptr [eax+18], 10
004191B2 |. 72 05 jb short 004191B9
004191B4 |. 8B40 04 mov eax, dword ptr [eax+4]
004191B7 |. EB 03 jmp short 004191BC
004191B9 |> 83C0 04 add eax, 4
004191BC |> 8D50 01 lea edx, dword ptr [eax+1]
004191BF |. 90 nop
004191C0 |> 8A08 /mov cl, byte ptr [eax]
004191C2 |. 40 |inc eax
004191C3 |. 84C9 |test cl, cl
004191C5 |.^ 75 F9 \jnz short 004191C0
当前选中对象的分析:
00436848 |. 8B4424 10 mov eax, dword ptr [esp+10]
0043684C |. 8BF0 mov esi, eax
0043684E |. 75 0F jnz short 0043685F
00436850 |. C780 E8000000>mov dword ptr [eax+E8], 3
0043685A |. E9 8E010000 jmp 004369ED
0043685F |> C780 E8000000>mov dword ptr [eax+E8], 2
00436869 |. D903 fld dword ptr [ebx]
0043686B |. D998 A0000000 fstp dword ptr [eax+A0] ; 当前选中的物品ID
00436871 |. D943 08 fld dword ptr [ebx+8]
00436874 |. C780 A8000000>mov dword ptr [eax+A8], 1
0043687E |. D998 A4000000 fstp dword ptr [eax+A4] ; 当前选中的怪ID eax = 01c36c80 恒定
00436884 |. E9 64010000 jmp 004369ED
00436889 |> 8B17 mov edx, dword ptr [edi] ; Case 3 of switch 004367E7
0043688B |. 8B42 18 mov eax, dword ptr [edx+18]
0043688E |. 8BCF mov ecx, edi
00436890 |. FFD0 call eax
此段代码会不停在浮点寄存器中压栈出栈,eax = 01c36c80正是当前所选对象的基址.
eax = 01c36c80恒定,为确保以后游戏升级后能顺利查找到该地址,下面是eax的特征码
首先eax = [esp] + 10
查看函数开头,实际是ebx的压栈,继续回溯ebx
则得到下列代码
0051FF10 |. 395D C0 cmp dword ptr [ebp-40], ebx
0051FF13 |. 0F85 87020000 jnz 005201A0
0051FF19 |. 8B0D 34526300 mov ecx, dword ptr [635234]
0051FF1F |. 8B1D 1C526300 mov ebx, dword ptr [63521C] ; 当前所选对象基地址
0051FF25 |. 8B01 mov eax, dword ptr [ecx]
0051FF27 |. 8B33 mov esi, dword ptr [ebx] ; GameClie.005E7EFC
0051FF29 |. 8B50 40 mov edx, dword ptr [eax+40]
0051FF2C |. 83C6 4C add esi, 4C
0051FF2F |. FFD2 call edx
如上所示:[63521C]即为eax的基地址
+A0 当前选中物品的ID
+A4 当前选中怪的ID
物品列表
突破口
CE监控 改写 地址 01c36c80 + A0的代码:
0043686b - d9 98 a0 00 00 00 - fstp dword ptr [eax+000000a0]// 排除掉了
0042fbf5 - 89 47 08 - mov [edi+08],eax
0041ff3e - c7 40 08 00 00 00 00 - mov [eax+08],00000000// 排除掉了
0042fbf5 - 89 47 08 - mov [edi+08],eax是关键
一路回溯,发现遍历物品列表的函数 跟 遍历怪物的函数是一样的.
注意用此根节点遍历出来包括了怪物对象,还包括其他一些无用的对象,需要过滤.
这里过滤的方式是物品的话,最大血量及当前血量都是0,可以以此作为判断是否是物品的根据.肯定二叉树叶子节点有一个变量是标明该对象的类型的,是背包还是怪物还是地上的物品等等.目前没有分析了.
技能
004235EC |. 53 push ebx ; -1
004235ED |. 8B5C24 18 mov ebx, dword ptr [esp+18]
004235F1 |. 53 push ebx ; -1
004235F2 |. 8B5C24 18 mov ebx, dword ptr [esp+18]
004235F6 |. 53 push ebx ; -1
004235F7 |. 57 push edi ; 怪物ID
004235F8 |. 52 push edx ; -1
004235F9 |. 51 push ecx ; 2
004235FA |. 50 push eax ; 0x16 技能代码
004235FB |. 56 push esi ; esi = 01c28BE8 恒定
004235FC |. E8 0F0B0000 call 00424110 ; 这个就是技能CALL