搜索人物血量基址
人物血量是游戏中的一个关键数据,由于人物血量包含在人物数据结构中,因此通过对血量基址的寻找,能够定位到人物数据结构基址,从而遍历出人物的其他数据属性。
通过使游戏中人物血量变动,用 CE 工具来定位到人物血量的虚拟地址,由于该虚拟地址是通过基址偏移而来
注:基址也称为静态地址(
静态地址
在 CE 中显示为绿色
,动态地址
在 CE 中显示为黑色
),是被写入游戏 PE 文件结构中的全局静态数据(一般位于.data
段,该段存储已初始化的数据
),因此每次游戏重启后都会变动,但是通过基址 + 偏移
的形式我们能够定位到该变动的虚拟地址。
在定位到人物血量的虚拟地址后,有两种方法来搜索其基址
和偏移
:
- 对人物血量的虚拟地址下
硬件访问断点
进行回溯搜索 - 对人物血量的虚拟地址下
硬件写入断点
进行回溯搜索
第一种方法能够对血量的基址进行定位,但是不一定能定位到人物基址,因为内存中每时每刻都有很多地方需要对人物血量进行访问,断下后不一定能追溯到人物结构基址。
第二种方法采用硬件写入断点
,当人物血量进行变动的时候游戏会首先读取人物数据结构中的血量,然后将这个血量值复制给人物血量对应的虚拟地址,因此通过这种方法能够回溯到人物结构地址。
因此更加推荐通过硬件写入断点
来对人物血量进行基址
和偏移
的搜索。
一、笑傲江湖血量基址搜索(x32)
1 通过 CE 搜索人物血量虚拟地址
在游戏中脱下装备使得血量变少,然后用 CE 工具指定精确数值
和4 字节
进行搜索:
随后再穿上装备使得血量提升,填入血量值,点击再次扫描
:
得到三个人物血量虚拟地址,我们应该如何确定使用哪个地址来进行搜索呢?我们让怪物攻击角色使得其血量变动,可以在 CE 中看到,第二个值不变,因此排除,而第一个值先于第三个值变动,说明第三个值来源于第一个值,因此我们优先选择第一个值来进行搜索:
2 通过 x64dbg 下硬件访问断点搜索基址和偏移
得到该虚拟地址后,我们在 x64dbg 中的内存窗口转到该地址,并写入 4 个字节的硬件访问断点
,随后在下面代码处断下:
00AEA454 | FF15 0C822201 | call dword ptr ds:[<&GetTickCount>] |
00AEA45A | 807E 2C 00 | cmp byte ptr ds:[esi+2C],0 |
00AEA45E | 8B6C24 18 | mov ebp,dword ptr ss:[esp+18] |
00AEA462 | 894424 10 | mov dword ptr ss:[esp+10],eax |
00AEA466 | 0F85 9B010000 | jne xajh.AEA607 |
00AEA46C | 8B7E 1C | mov edi,dword ptr ds:[esi+1C] | 1 追踪 esi==4E9DEDE8
00AEA46F | 85FF | test edi,edi |
00AEA471 | 0F8C 90010000 | jl xajh.AEA607 |
00AEA477 | 395E 24 | cmp dword ptr ds:[esi+24],ebx |
可以看到,[esi+1C]
为人物的血量,而 mov edi,dword ptr ds:[esi+1C]
这条语句访问了人物血量并将其赋值给了 edi
,此时得到了最外层的偏移为 1C
,因此我们需要继续往上追踪 esi
的来源。
对于这种被操作数为
[寄存器+偏移]
需要往上追踪寄存器
的情况,在断点断下的时候最好标记该寄存器的值,在往前追溯的过程中能够通过确认该寄存器值没有变化来保证自己的追踪路线没有出现错误。
往上追踪得到 esi
的值来源于 ecx
:
00AEA440 | 8BF1 | mov esi,ecx | 2
这种纯寄存器赋值的形式不会产生偏移,因此可以不用特意标记寄存器的值,后续为了节约篇幅,后面会将不产生偏移的代码合并到其他代码中,且只复制一行代码。
继续往上追踪,得到倒数第二层偏移为:2A0
00A37811 | 50 | push eax |
00A37812 | E8 59A86B00 | call xajh.10F2070 |
00A37817 | 50 | push eax |
00A37818 | 8D8E A0020000 | lea ecx,dword ptr ds:[esi+2A0] | 3 追踪 esi==4E9DEB48
00A3781E | E8 0D2C0B00 | call xajh.AEA430 |
00A37823 | D94424 18 | fld st(0),dword ptr ss:[esp+18] |
00A37827 | 83EC 10 | sub esp,10 |
00A3782A | DD5C24 08 | fstp qword ptr ss:[esp+8],st(0) |
00A3742F | 8BF1 | mov esi,ecx | 4
00A381D4 | 8BF1 | mov esi,ecx | 5
00E8F6DD | 8BCE | mov ecx,esi | 6
00E8F4B6 | 8BF1 | mov esi,ecx | 7
00EA11BE | 8BCB | mov ecx,ebx | 8
继续往上追踪,得到倒数第三层偏移为:10
,这里 eax
经过测试恒定为 10:
00EA11A7 | E8 E44AD4FF | call xajh.BE5C90 |
00EA11AC | 84C0 | test al,al |
00EA11AE | 74 16 | je xajh.EA11C6 |
00EA11B0 | 8B86 C8000000 | mov eax,dword ptr ds:[esi+C8] | esi+C8:"啜揘埅揘s"
00EA11B6 | 8B1C03 | mov ebx,dword ptr ds:[ebx+eax] | 9 追踪 eax==4E93A8E0,ebx 恒定为10
00EA11B9 | 8B13 | mov edx,dword ptr ds:[ebx] |
00EA11BB | 8B42 14 | mov eax,dword ptr ds:[edx+14] |
00EA11BE | 8BCB | mov ecx,ebx | 8
00EA11C0 | FFD0 | call eax |
00EA104E | 8BF1 | mov esi,ecx | 11
00EE4674 | 8BCE | mov ecx,esi | 12
00EE459A | 8BF1 | mov esi,ecx | 13
0088C403 | 8BCE | mov ecx,esi | 14
0088B81B | 8BF1 | mov esi,ecx | 15
继续往上追踪,得到倒数第四层偏移为:1*4+320
,这里 eax
经过测试恒定为 1:
008C83E4 | 8B86 30030000 | mov eax,dword ptr ds:[esi+330] |
008C83EA | 83F8 FF | cmp eax,FFFFFFFF |
008C83ED | 74 1D | je xajh.8C840C |
008C83EF | 83BC86 20030000 | cmp dword ptr ds:[esi+eax*4+320],0 |
008C83F7 | 74 13 | je xajh.8C840C |
008C83F9 | 8B8C86 20030000 | mov ecx,dword ptr ds:[esi+eax*4+320] | 16 追踪 esi==257B2008,eax 恒定为1
008C8400 | 8B11 | mov edx,dword ptr ds:[ecx] | edx:&" 蚌"
008C8402 | 8B4424 0C | mov eax,dword ptr ss:[esp+C] |
008C8406 | 8B52 20 | mov edx,dword ptr ds:[edx+20] |
008C83E2 | 8BF1 | mov esi,ecx | 17
继续往上追踪,得到倒数第五层偏移为 8
:
004ACACF | 74 15 | je xajh.4ACAE6 |
004ACAD1 | C705 2C975201 11 | mov dword ptr ds:[152972C],11 |
004ACADB | 8B4D 08 | mov ecx,dword ptr ss:[ebp+8] | 18 追踪ebp==1111A898==[xajh.exe+11297AC]==[015297AC]
004ACADE | 8B11 | mov edx,dword ptr ds:[ecx] |
004ACAE0 | 8B42 20 | mov eax,dword ptr ds:[edx+20] |
004AC60D | 8BE9 | mov ebp,ecx | 19
此处将
ebp
寄存器的值丢到 CE 里面搜索得到了静态地址[xajh.exe+11297AC]==[015297AC]
。
继续往上追踪,得到倒数第六层偏移为 24
:
0048A74C | 52 | push edx |
0048A74D | E8 9E7F8400 | call xajh.CD26F0 |
0048A752 | 8B4424 3C | mov eax,dword ptr ss:[esp+3C] |
0048A756 | C705 30975201 64 | mov dword ptr ds:[1529730],64 | 64:'d'
0048A760 | 8B4B 24 | mov ecx,dword ptr ds:[ebx+24] | 20 追踪ebx==01529788=[xajh.exe+11282D8]==[015282D8]
0048A763 | 50 | push eax |
0048A764 | E8 871E0200 | call xajh.4AC5F0 |
0048A769 | 84C0 | test al,al |
0048A3B4 | 8BD9 | mov ebx,ecx | 21
此处将
ebx
寄存器的值丢到 CE 里面搜索得到了静态地址[xajh.exe+11282D8]==[015282D8]
。
继续往上追踪:
00F4D65A | A0 5DB64E01 | mov al,byte ptr ds:[14EB65D] |
00F4D65F | 84C0 | test al,al |
00F4D661 | 74 0D | je xajh.F4D670 |
00F4D663 | 8B4E 04 | mov ecx,dword ptr ds:[esi+4] | 22 追踪 esi==0019F1F0, 此处 esi 已经为一个栈地址,应该停止追踪
00F4D666 | 8B11 | mov edx,dword ptr ds:[ecx] |
00F4D668 | 8B06 | mov eax,dword ptr ds:[esi] |
00F4D66A | 8B52 08 | mov edx,dword ptr ds:[edx+8] |
00F4D53F | 8B7424 08 | mov esi,dword ptr ss:[esp+8] | 23 call的第一个参数
00485F2C | 52 | push edx | 24 追踪 edx
00485F28 | 8D5424 04 | lea edx,dword ptr ss:[esp+4] | 25 edx 为栈地址
追踪到这层我们可以看到,esi
寄存器中保存的已经是一个栈地址了,继续往下追到 edx
寄存器来源于 esp+4
,而 esp+4
也是一个栈地址,无法继续再往上追溯 esp
,那怎么办呢?
我们倒回去看,在倒数第六层偏移和倒数第五层偏移的时候,将要追溯的寄存器值丢到 CE 中已经能够搜索到绿色的静态地址了,那么我们就可以用这个作为血量基址来定位变动的血量虚拟地址,然后从第一层偏移开始对基址和偏移进行组合,得到两个关于血量的基址和偏移组合:
- [[[[[015297AC]+8]+1*4+320]+C8]+10]+2A0+1C
- [[[[[[015282D8]+24]+8]+1*4+320]+C8]+10]+2A0+1C
但是经过测试,这两个基址和偏移的组合无法定位到人物数据结构,因此我们只能通过硬件写入断点
来进一步寻找人物的数据结构基址。
2.1 [esp+偏移] 的寻址
在上面追溯的的过程中可能会碰到 mov edx, dword ptr [esp+偏移]
的这种形式的汇编代码,[esp+偏移]
其实是一个栈地址,栈在程序的执行过程中用来存放函数的返回地址
、函数参数
、函数局部变量
等。那我们如何区分 [esp+偏移]
到底表示是哪种呢?
其中 [esp+偏移]
为返回地址
是最好区分的,在堆栈的注释区会显示返回到 xxx
:
注:x64dbg 错误的将
793F0E30
判断为返回地址
,构建了一个虚假的栈帧!!!
然后接下来是区分函数参数
和函数局部变量
,在要观察的包含 [esp+偏移]
的汇编代码处下断点并断下,然后通过 esp
定位到栈顶,双击 esp
所指向的堆栈地址,会显示出偏移量:
随后我们按 ctrl+F9
,直接运行到返回:
可以从上图的堆栈中看到,从 [esp]
至 [esp+44]
全部都为局部变量,因此[esp+18]
也是,图中 [esp+48]
为真正的返回地址,在其下面的为这个函数的参数,但本函数没有参数。
注:特别注意
[esp+10]
这个返回地址
仅仅是一个局部变量而已,万万不可通过这个返回地址来对函数参数和函数局部变量进行区分!!!
进一步的,如果确定 [esp+偏移]
为函数参数
,则分析是第几个参数,根据函数的调用约定,我们这里以 32 位系统下常用的调用约定 stdcall
和 cdecl
为例,都是从右自左来进行压栈的,因此从返回地址
处往下数,依次是第一个、第二个...这样的顺序。
C/C++ 程序默认使用
cdecl
调用约定,由调用者清理堆栈,因此该约定支持使用可变数量的参数的函数;而 Windows 系统函数一般使用stdcall
调用约定,由被调用者清理堆栈,由于被调用者不知道参数的个数,因此不支持可变参数的函数。
如果确定 [esp+偏移]
为函数局部变量
,如果往上回溯函数体太长,难以找到是哪条汇编语句对该局部变量进行了赋值,我们可以通过程序的多次中断来在栈窗口中观察这个 [esp+偏移]
所包含的局部变量,如果固定不变,那么我们可以直接在该函数的头部下端,并在 内存窗口中转到 [esp+偏移]
处,下一个 4 字节的硬件写入断点
,运行程序即可观察到哪条汇编指令对该局部变量进行了赋值。
2.2 快速判定 call 的参数个数
如果是外平栈或函数体较短的内平栈,可以直接通过平栈语句来判断函数有多少个参数。对于函数体较长的内平栈情况,翻到函数体底部非常的繁琐,可以直接对该函数下一个断点,断下后在堆栈窗口双击 esp
指向的栈地址显示出偏移,然后单步步过
该函数,通过 esp
指针的变化来快速判断函数的参数个数。
3 通过 x64dbg 下硬件写入断点搜索基址和偏移
将 CE 得到的血量动态地址在 x64dbg 的内存区中转到该地址,然后下 4 个字节的硬件写入断点
,在下面代码处断下:
00AE9DCD | 8D4B 04 | lea ecx,dword ptr ds:[ebx+4] |
00AE9DD0 | 896B 18 | mov dword ptr ds:[ebx+18],ebp |
00AE9DD3 | 897B 1C | mov dword ptr ds:[ebx+1C],edi | 1 edi 为血量,追溯 edi 来源
00AE9DD6 | 8973 20 | mov dword ptr ds:[ebx+20],esi |
00AE9DD9 | 8943 28 | mov dword ptr ds:[ebx+28],eax | ebx+28:"應H"
00AE9D95 | 8B7C24 34 | mov edi,dword ptr ss:[esp+34] | 2 第2个参数
00AEA5F6 | 55 | push ebp | 3
00AEA45E | 8B6C24 18 | mov ebp,dword ptr ss:[esp+18] | 4 第1个参数
继续往上追踪:
00A37808 | E8 63A86B00 | call xajh.10F2070 |
00A3780D | D84424 2C | fadd st(0),dword ptr ss:[esp+2C] |
00A37811 | 50 | push eax |
00A37812 | E8 59A86B00 | call xajh.10F2070 | 6 输入参数eax==最大血量 输出实际血量
00A37817 | 50 | push eax | 5
00A37818 | 8D8E A0020000 | lea ecx,dword ptr ds:[esi+2A0] |
通过对上面的代码进行测试,发现 eax
中存储的人物血量来源于 call xajh.10F2070
这个函数,我们跟进这个函数内部:
eax
这个寄存器固定用来存放函数返回值,如果函数有返回值的话。
010F2070 | 833D 90D6B903 00 | cmp dword ptr ds:[3B9D690],0 |
010F2077 | 74 2D | je xajh.10F20A6 |
010F2079 | 55 | push ebp |
010F207A | 8BEC | mov ebp,esp |
010F207C | 83EC 08 | sub esp,8 |
010F207F | 83E4 F8 | and esp,FFFFFFF8 |
010F2082 | DD1C24 | fstp qword ptr ss:[esp],st(0) |
010F2085 | F2:0F2C0424 | cvttsd2si eax,qword ptr ss:[esp] |
010F208A | C9 | leave |
010F208B | C3 | ret |
通过粗略分析可以看到,这段代码实际上是将一个浮点数转换为一个整数,因此可以推断出血量其实是一个浮点数,我们之前 CE 找到的整数型血量是被处理过然后显示在游戏界面中的,进一步分析得到其实血量来自 st(0)
寄存器。
在搜索血量的时候可以将数据类型调整为
单浮点
,但是经测试该游戏搜索不到。
那么我们退出上面的函数,继续往上追踪 :
00A37808 | E8 63A86B00 | call xajh.10F2070 |
00A3780D | D84424 2C | fadd st(0),dword ptr ss:[esp+2C] | 7 st(0)中的浮点数血量来源于[esp+2C]
00A37811 | 50 | push eax |
00A37812 | E8 59A86B00 | call xajh.10F2070 | 6 输入参数eax==最大血量 输出实际血量
00A37817 | 50 | push eax | 5
00A37818 | 8D8E A0020000 | lea ecx,dword ptr ds:[esi+2A0] |
这里
fadd
其实是一个将目标操作浮点数和被操作浮点数相加并存入目标操作数的指令,st(0)
中存放的值为0.5
,由于数值不大,我们此处忽略原始值或者直接在定位到的血量值基础上加上0.5
即可。
我们继续追踪 [esp+2C]
,通过观察堆栈窗口发现这是一个局部变量,且也来源于 st(0)
,而 st(0)
来源于 [ebp+7D5]
,此处就是倒数第一层偏移 7D5
:
00A376E4 | 8B95 A2070000 | mov edx,dword ptr ss:[ebp+7A2] |
00A376EA | D985 D5070000 | fld st(0),dword ptr ss:[ebp+7D5] | 9 追踪ebp==4FB53170
00A376F0 | 895424 14 | mov dword ptr ss:[esp+14],edx |
00A376F4 | D95C24 24 | fstp dword ptr ss:[esp+24],st(0) | 8 在函数头部对局部变量下硬件断点得知该局部变量来自此处的st(0)
00A376F8 | D9E8 | fld1
找到赋值语句后继续追踪 ebp
:
00A37489 | 8BCE | mov ecx,esi |
00A3748B | E8 70604500 | call xajh.E8D500 |
00A37490 | E8 CB79E9FF | call xajh.8CEE60 |
00A37495 | 8BE8 | mov ebp,eax | 10 eax 来源于上面的call
00A37497 | 85ED | test ebp,ebp
进 call 里面继续追踪 eax
,得到倒数第二层偏移 8C
:
004AE400 | A1 D8825201 | mov eax,dword ptr ds:[15282D8] |
004AE405 | 85C0 | test eax,eax |
004AE407 | 74 0E | je xajh.4AE417 |
004AE409 | 8B40 24 | mov eax,dword ptr ds:[eax+24] |
004AE40C | 85C0 | test eax,eax |
004AE40E | 74 07 | je xajh.4AE417 |
004AE410 | 8B80 8C000000 | mov eax,dword ptr ds:[eax+8C] | 11 追踪eax==2562DD88
004AE416 | C3 | ret |
找到了倒数第三层偏移 24
:
004AE400 | A1 D8825201 | mov eax,dword ptr ds:[15282D8] |
004AE405 | 85C0 | test eax,eax |
004AE407 | 74 0E | je xajh.4AE417 |
004AE409 | 8B40 24 | mov eax,dword ptr ds:[eax+24] | 12 追踪eax==01529788
004AE40C | 85C0 | test eax,eax |
004AE40E | 74 07 | je xajh.4AE417 |
004AE410 | 8B80 8C000000 | mov eax,dword ptr ds:[eax+8C] | 11 追踪eax
004AE416 | C3 | ret |
继续往上追踪便可得到基址 15282D8
:
004AE400 | A1 D8825201 | mov eax,dword ptr ds:[15282D8] | 13 追踪到基址
004AE405 | 85C0 | test eax,eax |
004AE407 | 74 0E | je xajh.4AE417 |
004AE409 | 8B40 24 | mov eax,dword ptr ds:[eax+24] | 12 追踪eax
004AE40C | 85C0 | test eax,eax | eax:"€坮?"
004AE40E | 74 07 | je xajh.4AE417 |
004AE410 | 8B80 8C000000 | mov eax,dword ptr ds:[eax+8C] | 11 追踪eax
004AE416 | C3 | ret |
最后我们从头将基址和偏移进行组合,得到了人物数据结构中的血量基址:
[[[15282D8]+24]+8C]+7D5
3.1 通过血量基址分析人物结构
得到血量基址后,我们很容易分析出人物数据结构地址:
[[[15282D8]+24]+8C]
而一般血量基址附近的是蓝量等基址,因此我们可以通过 +4
和 -4
来推断蓝量和其他类似属性偏移:
+7D5:血量偏移
+7D9:蓝量偏移
在 CE 中有一个非常方便的功能,可以对整片内存数据结构进行分析,我们可以打开CE,点击查看内存
->工具
->分析数据/遍历
->结构
->定义新结构
来分析内存数据。
人物血量基址的倒数第二层偏移 [[[15282D8]+24]+8C]
即为人物结构数据,将该地址包含的值放入新建的结构中:
通过移动游戏中的人物,可以很轻易的通过数据结构中的变动数据找到角色坐标偏移:
+3C:x 坐标
+40:z 坐标
+4C:y 坐标
人物的其他属性也可以通过这种动态的差异化方式进行寻找。
我们将 [[[15282D8]+24]+8C]
人物结构基址丢到 x64dbg 的内存区进行观察,可以看到角色名字的偏移:
+4B0:角色名字
该游戏的中文编码为
UNICODE
,如果 x64dbg 中不显示,我们可以通过 CE 搜索角色名字对应的UNICODE
字节码,然后在字节码尾巴追加00 00
用 CE 进行搜索,通过对搜索出来的角色名字进行更改来确定哪一个是关键的角色名字,然后用这个虚拟地址和人物角色结构中保存的值进行比较,找到对应的偏移。
通过对比发现,人物 ID 的偏移为:
+140:人物 ID