梦醒暗黑廿年
向抗击疫情的英雄们致敬
宇春秋
第二篇:NPC篇
一、基本猜想
上一篇找玩家遍历没找到反而找到地图元素,这回再仔细看看d2hackmap的代码。
一般来说游戏里面NPC、玩家的结构或类的对象都是从一个父类中派生出来,存储和处理代码都是一致的。很多游戏的NPC和玩家基址都在一起,可能前一个基址是玩家,+0x04之后就存放着玩家的基址。甚至有的游戏NPC和玩家都混在一个表中,处理的时候仅靠一个标志位来区分。所以在d2hackmap中重点看下面的函数:
void __fastcall DrawAutomapBlob(int xpos, int ypos, DWORD dwColor,UnitAny *pUnit)
为什么会以这个函数为突破口呢?
1、这个函数和地图元素的处理函数一样,也有xpos和ypos,应该是有坐标处理的。(虽然这个坐标是对应的小地图坐标)
2、在这个函数中有这样的代码:
celltype = pUnit->dwUnitType;
celltype==UNITNO_PLAYER
...
celltype==UNITNO_MONSTER
...
celltype==UNITNO_ITEM
很明显这个函数中区分了对象是玩家还是怪物,而且印证了开始时的猜想–NPC和玩家是混在一起的,仅靠一个标志位来区分。虽然第三个UNITNO_ITEM不能猜出具体对应的是什么,毕竟ITEM的含义比较广,但当最后遍历看打印出信息了就一目了然了。(其实ITEM对应的是地面物品,并不太出乎意料)
3、这个函数里面虽然没有太多的特征方便我们迅速定位,但这个函数是一个__fastcall调用,而且是一个__declspec(naked)调用的一部分,而这个naked调用是很有特征的:
void __declspec(naked) DrawBlobPath_ASM()
{
__asm {
mov edx , eax
push esi
push [esp+8]
call DrawAutomapBlob
ret 4
}
}
二、OD上手
找个能用的地图进入游戏,找到hackmap模块,按CTRL+S进入查找命令序列,将
mov edx , eax
push esi
push [esp+8]
复制到查找窗口,很快就找到了d2hackmap代码地址:
0F064C50 8BD0 mov edx, eax
0F064C52 56 push esi
0F064C53 FF7424 08 push dword ptr [esp+0x8]
0F064C57 E8 94FDFFFF call 0F0649F0
0F064C5C C2 0400 retn 0x4
0F064C5F CC int3
在0F064C50这儿下断, call 0F0649F0中的代码就不用看了,具体的分析过程直接有源代码可看,我们按F8单步返回到上一层看是怎么处理的。
上一层是在D2Client 的6FB119E0函数中。简单看下6FB119E0函数,里面有段比较熟悉的代码:
6FB11A49 |. 57 push edi
6FB11A4A |. 56 push esi
6FB11A4B |. E8 82A7FAFF call <jmp.&D2Common.#10651>
6FB11A50 |. 56 push esi
6FB11A51 |. 8BF8 mov edi, eax
6FB11A53 |. E8 9EA7FAFF call <jmp.&D2Common.#11142>
6FB11A58 |. 8B1D B016BA6F mov ebx, dword ptr [0x6FBA16B0]
6FB11A5E |. 8BC8 mov ecx, eax
6FB11A60 |. 8BC7 mov eax, edi
6FB11A62 |. 99 cdq
6FB11A63 |. F7FB idiv ebx
6FB11A65 |. 8B15 F8C1BC6F mov edx, dword ptr [0x6FBCC1F8]
6FB11A6B |. 8BF8 mov edi, eax
6FB11A6D |. 2BFA sub edi, edx
6FB11A6F |. 8BC1 mov eax, ecx
6FB11A71 |. 99 cdq
6FB11A72 |. F7FB idiv ebx
6FB11A74 |. 8B15 FCC1BC6F mov edx, dword ptr [0x6FBCC1FC]
6FB11A7A |. 83C7 08 add edi, 0x8
6FB11A7D |. 8BD8 mov ebx, eax
6FB11A7F |. A1 28C2BC6F mov eax, dword ptr [0x6FBCC228]
6FB11A84 |. 2BDA sub ebx, edx
6FB11A86 |. 83EB 08 sub ebx, 0x8
6FB11A89 |. 3BF8 cmp edi, eax
0x6FBA16B0这个基址在地图元素的遍历中用到过,而且前面的代码简单转成C代码大概是这样:
Temp1=D2Common.#10651(Addr)/0x6FBA16B0
Temp2=D2Common.# 11142(Addr)/0x6FBA16B0
是不是和地图元素的坐标处理xpos(ypos)*5<<1/ [0x6FBA16B0]有点像?那大胆的猜一下Addr会不会就是NPC结构的地址,而Temp1和Temp2就是x和y?
其实如果要我写游戏的代码,我也很大机率会这样写:D。
在6FB119E0中没有循环或递归调用的迹象,于是我们再返回上一层到了D2Client的6FB12410函数,而且正好落在下面这段代码中:
6FB12413 |. A1 FCBBBC6F mov eax, dword ptr [0x6FBCBBFC]
6FB12418 |. 53 push ebx
6FB12419 |. 55 push ebp
6FB1241A |. 8B2D 14BCBC6F mov ebp, dword ptr [0x6FBCBC14]
6FB12420 |. 56 push esi
6FB12421 |. 57 push edi
6FB12422 |. 8D4C24 10 lea ecx, dword ptr [esp+0x10]
6FB12426 |. 51 push ecx
6FB12427 |. 8D5424 18 lea edx, dword ptr [esp+0x18]
6FB1242B |. 52 push edx
6FB1242C |. 33FF xor edi, edi
6FB1242E |. 50 push eax
6FB1242F |. 897C24 20 mov dword ptr [esp+0x20], edi
6FB12433 |. 897C24 1C mov dword ptr [esp+0x1C], edi
6FB12437 |. E8 B49DFAFF call <jmp.&D2Common.#10331>
6FB1243C |. 50 push eax
6FB1243D |. E8 9A9FFAFF call <jmp.&D2Common.#10383>
6FB12442 |. 8B4424 10 mov eax, dword ptr [esp+0x10]
6FB12446 |. 3BC7 cmp eax, edi
6FB12448 |. 8BD8 mov ebx, eax
6FB1244A |. 7E 2A jle short 6FB12476
6FB1244C |. 8D6424 00 lea esp, dword ptr [esp]
6FB12450 |> 8B4424 14 /mov eax, dword ptr [esp+0x14]
6FB12454 |. 8B0CB8 |mov ecx, dword ptr [eax+edi*4]
6FB12457 |. 8B71 74 |mov esi, dword ptr [ecx+0x74]
6FB1245A |. 85F6 |test esi, esi
6FB1245C |. 74 13 |je short 6FB12471
6FB1245E |. 8BFF |mov edi, edi
6FB12460 |> 8BC6 |/mov eax, esi
6FB12462 |. E8 79F5FFFF ||call 6FB119E0
6FB12467 |. 8BB6 E8000000 ||mov esi, dword ptr [esi+0xE8]
6FB1246D |. 85F6 ||test esi, esi
6FB1246F |.^ 75 EF |\jnz short 6FB12460
6FB12462的call 6FB119E0正好是我们刚才分析的代码,而且下一句mov esi, dword ptr [esi+0xE8]对应的C代码是Addr=*(ULONG *)(Addr+0xE8)
,接下来两句test esi, esi和jnz short 6FB12460明显是个循环判断,基本可以判定是找到NPC遍历了。再仔细看下汇编,遍历的基址正好是在函数头中出现的0x6FBCBBFC。
现在可以写出遍历代码:
// GetD2DllBase包含了以后所有的基址和暗黑的导出函数初始化,以后的代码将不再出现这个函数
void GetD2DllBase()
{
uD2ClientAddr=(ULONG)::GetModuleHandleA("D2Client.dll");
uD2CommonAddr=(ULONG)::GetModuleHandleA("D2Common.dll");
uD2WinAddr=(ULONG)::GetModuleHandleA("D2Win.dll");
uD2Multi=(ULONG)::GetModuleHandleA("D2Multi.dll");
uD2Launch = (ULONG)::GetModuleHandleA("d2launch.dll");
uD2Common_10331=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10331);
uD2Common_10383=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10383);
uD2Common_10651=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10651);
uD2Common_11142=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)11142);
uD2Common_10014=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10014);
uD2Common_10801=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10801);
uD2Common_10973=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10973);
uD2Common_11017=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)11017);
uD2Common_10107=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10107);
uD2Common_10867=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10867);
uD2Common_10750=(ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10750);
uD2Common_10455 = (ULONG)::GetProcAddress((HMODULE)uD2CommonAddr, (char *)10455);
uD2Win_10042=(ULONG)::GetProcAddress((HMODULE)uD2WinAddr, (char *)10042);
uD2Client_GetItemName=uD2ClientAddr+0x914F0;
uD2Client_GetActorName=uD2ClientAddr+0x11BC14;
uD2Client_SendPacketCall=uD2ClientAddr+0x143E0;
uD2Win_House=uD2WinAddr+0x214B7;
uD2Multi_CreateRoom=uD2Multi+0x39D0B;
uD2Multi_JoinRoom = uD2Multi + 0x39F20;
uD2Client_OpenBag=uD2ClientAddr+0xFAD84;
uD2Client_Depository=uD2ClientAddr+0xFADE4;
uD2Client_MouseMoveAddr=uD2ClientAddr+0x2A7C0;
uD2Client_BuyItem1=uD2ClientAddr+0x11973B;
uD2Client_BuyItem2=uD2ClientAddr+0x11C390;//难度
uD2Win_InputRoomName = uD2WinAddr + 0x214B0;
uD2Client_RoomName = uD2ClientAddr + 0x119940;
uD2Client_SelectTalk=uD2ClientAddr+0x11BC00;
uD2Client_SetCursorPos = uD2ClientAddr + 0x1621D;
uD2multi_GetCurActorWork = uD2Multi + 0x39F7C;
}
void GetNpcArr(std::vector<ULONG> &arrNpc)
{
ULONG uBase=*(ULONG *)( uD2ClientAddr +0x11BBFC);
if (IsBadReadPtr((LPVOID)uBase, 0x20) != 0)return;
ULONG uBase1=uD2Common_10331;
ULONG uBase2=uD2Common_10383;
ULONG uBegin=0,uEnd=0;
_asm
{
pushad
lea eax,uBegin
lea ebx,uEnd
push ebx
push eax
push uBase
call uBase1
push eax
call uBase2
popad
}
ULONG Count=0;
if(uEnd>0)
{
do
{
for (ULONG i = *(ULONG *)(*(ULONG *)(uBegin + 4 * Count) + 0x74); i; i = *(ULONG *)(i + 0xE8) )
arrNpc.push_back(i);
++Count;
}
while ( Count < uEnd );
}
}
三、查缺补漏
1、现在已经有NPC遍历和NPC坐标了,还差一个NPC名字,这一点在目前找到的代码中是没有反映的。其实这是个小问题,一般来说NPC名字肯定是在每个NPC结构中的,从NPC结构的首地址起,每个有效的地址进去看一下就好了,开始以为是个体力活,没想到每种不同类型的结构对应的名字偏移竟然不一样,从一个体力活变成了一个大大的体力活。以下是总结的四种不同结构获取名方法:
bool GetNpcName(ULONG NpcAddr,char *pNameBuf)
{
ULONG Addr=NpcAddr;
if (IsBadReadPtr((LPVOID)Addr,0x100) != 0)
return false;
ULONG NpcType=*(ULONG *)Addr;
if(NpcType==0)
{
//type==0,玩家,0x14就是名字ansi
ULONG uNpcNameAddr=*(ULONG *)(Addr+0x14);
if(IsBadReadPtr((LPVOID)uNpcNameAddr,0x50)!=0)
return false;
strncpy(pNameBuf,(char *)uNpcNameAddr,0x50);
return true;
}
else if(NpcType==1)
{
//type==1,npc,怪0x14]+0x2c就是名字unicode
ULONG uNpcInfoAddr=*(ULONG *)(Addr+0x14);
if(IsBadReadPtr((LPVOID)uNpcInfoAddr,0x100)!=0)
return false;
ULONG uNpcNameAddr=*(ULONG *)(uNpcInfoAddr+0x2C);
if(IsBadReadPtr((LPVOID)uNpcNameAddr,0x100)!=0)
return false;
WCHAR temp[128]={0};
wcsncpy(temp,(WCHAR *)uNpcNameAddr,128);
Unicode2Ansi(temp,pNameBuf);
}
else if(NpcType==2)
{
//type==2桶之类的
ULONG uNpcInfoAddr=*(ULONG *)(Addr+0x14);
if(IsBadReadPtr((LPVOID)uNpcInfoAddr,0x10)!=0)
return false;
ULONG uNpcNameAddr=*(ULONG *)(uNpcInfoAddr);
if(IsBadReadPtr((LPVOID)uNpcNameAddr,0x100)!=0)
return false;
WCHAR temp[128]={0};
wcsncpy(temp,(WCHAR *)(uNpcNameAddr+0x40),128);
Unicode2Ansi(temp,pNameBuf);
}
else if(NpcType==4)
{
//=4是地面上的物品
ULONG ItemAddr=Addr;
WCHAR wszNameBuf[0x100]={0};
_asm
{
pushad
push 0x100
lea edx,wszNameBuf;
push edx
mov eax,ItemAddr
push eax
call uD2Client_GetItemName
popad
}
Unicode2Ansi(wszNameBuf,pNameBuf);
}
return true;
}
2、另外在上文中已经说过取NPC坐标是通过D2Common模块中的第10651号和第11142号函数取得的,现在写出取坐标函数:
void GetNpcPointInMap(ULONG NpcAddr,int &realx,int &realy)
{
int MiniMapCellLen=*(int *)(uD2ClientAddr+0xF16B0);
if (IsBadReadPtr((LPVOID)NpcAddr, 0x200) != 0)return;
int tempX=0,tempY=0,tempX2=0,tempY2=0;
_asm
{
pushad
mov esi,NpcAddr;
push esi
call uD2Common_10651
push esi
mov edi, eax
call uD2Common_11142
mov ebx, MiniMapCellLen
mov ecx, eax
mov eax, edi
cdq
idiv ebx
mov edi, eax
mov tempX2,eax
sub edi, edx
mov eax, ecx
cdq
idiv ebx
add edi, 8
mov ebx, eax
mov tempY2,eax
sub ebx, edx
sub ebx, 8
mov tempX,edi
mov tempY,ebx//原来的做法是从汇编代码计算的,但后来发现只要人物一动,所有的相对坐标都要重算
//所以改成直接用人物坐标,不用算小地图
popad
}
//这个取real坐标没有像取小地图坐标一样x+8,y-8
realx=tempX2;
realy=tempY2;
}
3、由于游戏还有尸爆和寻找物品技能会对怪物尸体进行二次处理,所以遍历出来的数据除了存在的怪外,还会有死去的怪,通过对比找出了一个不够完美的标志位来判断是否存活。目前玩家类结构还没有处理,只处理了NPC类和地面附加物(如桶)代码如下:
ULONG IsNpcLive(ULONG NpcAddr)
{
ULONG Addr=NpcAddr;
if (IsBadReadPtr((LPVOID)Addr,0x100) != 0)
return false;
ULONG NpcType=*(ULONG *)Addr;
if(NpcType==0)
{
return false;
}
else if(NpcType==1)
{
//type==1,npc,怪
return *(ULONG *)(Addr+0x44);//死了就=0,不死就非0
}
else if(NpcType==2)
{
//type==2桶类
ULONG uNpcInfoAddr=*(ULONG *)(Addr+0x14);
if(IsBadReadPtr((LPVOID)uNpcInfoAddr,0x100)!=0)
return false;
return !*(WORD *)(uNpcInfoAddr+0x42);//如果是1就是桶爆了,0就是没打开,取反对应函数名
}
return true;
}
测试代码如下:
void testGetNpcInfo()
{
std::vector<ULONG> arrNpc;
GetNpcArr(arrNpc);
//0x00标志玩家00,npc01,桶02,地面物品04,
for(ULONG i=0;i<arrNpc.size();i++)
{
char szName[256]={0};
int realx=0,realy=0;
GetNpcPointInMap(arrNpc[i],realx,realy);
GetNpcName(arrNpc[i], szName);
ULONG Other = *(ULONG *)(arrNpc[i] + 0x14);//地面物品的颜色
ULONG Color = 0;
if (IsBadReadPtr((LPVOID)Other, 0x100) == 0)
{
Color = *(ULONG *)(Other);
}
char str[256]={0};
sprintf(str,"index=%d,type=%d,Addr=%08X,Name=%s,Face=%d,Point:%d,%d,status=%d,%d,Color=%d",i,*(ULONG *)arrNpc[i],arrNpc[i], szName,*(ULONG *)(arrNpc[i]+0x04),realx,realy,IsNpcLive(arrNpc[i]),*(ULONG *)(arrNpc[i]+0x10),Color);
WriteLog(str);
}
}
测试结果如下:
10:21:13 index=2,type=1,Addr=09100D00,Name=恰西,Face=154,Point:855,4360,status=17408,8,Color=157069056
10:21:13 index=3,type=2,Addr=09100700,Name=一股邪惡力量,Face=37,Point:860,4363,status=1,2,Color=159420184
10:21:13 index=4,type=1,Addr=09D2AD00,Name=一股邪惡力量,Face=149,Point:864,4367,status=768,1,Color=157066936
10:21:13 index=5,type=2,Addr=09101300,Name=一股邪惡力量,Face=37,Point:839,4362,status=1,2,Color=159420184
10:21:13 index=6,type=2,Addr=09101100,Name=一股邪惡力量,Face=37,Point:821,4362,status=1,2,Color=159420184
10:21:13 index=7,type=1,Addr=09100900,Name=一股邪惡力量,Face=152,Point:816,4363,status=5308,1,Color=157068208
10:21:13 index=8,type=1,Addr=09D29900,Name=一股邪惡力量,Face=149,Point:814,4364,status=883,1,Color=157066936
10:21:13 index=9,type=1,Addr=09D29A00,Name=一股邪惡力量,Face=149,Point:852,4367,status=1024,1,Color=157066936
10:21:13 index=10,type=2,Addr=09100F00,Name=一股邪惡力量,Face=37,Point:829,4375,status=1,2,Color=159420184
10:21:13 index=11,type=1,Addr=09100B00,Name=一股邪惡力量,Face=152,Point:820,4376,status=490,1,Color=157068208
10:21:13 index=12,type=1,Addr=09D29800,Name=一股邪惡力量,Face=149,Point:840,4376,status=1024,1,Color=157066936
10:21:13 index=13,type=1,Addr=09101500,Name=一股邪惡力量,Face=179,Point:880,4364,status=240,1,Color=157079656
10:21:13 index=14,type=2,Addr=09101700,Name=一股邪惡力量,Face=37,Point:883,4370,status=1,2,Color=159420184
10:21:13 index=15,type=2,Addr=09101B00,Name=小站,Face=119,Point:908,4381,status=1,2,Color=159456920
10:21:13 index=16,type=2,Addr=09101900,Name=一股邪惡力量,Face=37,Point:917,4390,status=1,2,Color=159420184
10:21:13 index=17,type=2,Addr=09101D00,Name=一股邪惡力量,Face=37,Point:928,4396,status=1,2,Color=159420184
10:21:13 index=18,type=1,Addr=09D24600,Name=一股邪惡力量,Face=149,Point:788,4383,status=1152,1,Color=157066936
10:21:13 index=19,type=1,Addr=09D24500,Name=一股邪惡力量,Face=149,Point:802,4390,status=1339,1,Color=157066936
10:21:13 index=20,type=1,Addr=09D24400,Name=一股邪惡力量,Face=149,Point:788,4374,status=305,1,Color=157066936
10:21:13 index=21,type=2,Addr=09D2E400,Name=一股邪惡力量,Face=37,Point:812,4386,status=1,2,Color=159420184
10:21:13 index=22,type=2,Addr=09D2E200,Name=一股邪惡力量,Face=37,Point:800,4374,status=1,2,Color=159420184
10:21:13 index=23,type=2,Addr=09D2E000,Name=一股邪惡力量,Face=37,Point:792,4388,status=1,2,Color=159420184
10:21:13 index=24,type=1,Addr=09101F00,Name=基得,Face=147,Point:807,4388,status=128,2,Color=157066088
10:21:13 index=25,type=1,Addr=09D2E800,Name=一股邪惡力量,Face=152,Point:855,4373,status=4865,1,Color=157068208
10:21:13 index=26,type=2,Addr=09D2EE00,Name=你的私人儲藏箱,Face=267,Point:865,4380,status=1,0,Color=159523224
10:21:13 index=27,type=2,Addr=09D2F000,Name=一股邪惡力量,Face=37,Point:849,4381,status=1,2,Color=159420184
10:21:13 index=28,type=2,Addr=09D2EC00,Name=一股邪惡力量,Face=37,Point:887,4384,status=1,2,Color=159420184
10:21:13 index=29,type=1,Addr=09D2E600,Name=瓦瑞夫,Face=155,Point:872,4386,status=3968,1,Color=157069480
10:21:13 index=30,type=0,Addr=090E8A00,Name=ddd,Face=4,Point:874,4388,status=0,5,Color=6579300
10:21:13 index=31,type=2,Addr=09D2EF00,Name=火焰,Face=39,Point:864,4389,status=1,0,Color=159421080
10:21:13 index=32,type=4,Addr=090E9300,Name=手斧【普通】 (1),Face=0,Point:872,4390,status=1,3,Color=2
10:21:13 index=33,type=4,Addr=090E9400,Name=圓盾【普通】【輕】 (1),Face=328,Point:873,4390,status=1,3,Color=2
10:21:13 index=34,type=2,Addr=09D2F200,Name=一股邪惡力量,Face=385,Point:870,4392,status=1,0,Color=159576088
10:21:13 index=35,type=1,Addr=09D2EA00,Name=卡夏,Face=150,Point:889,4394,status=1280,1,Color=157067360
宇春秋原创,转载请注明。
第二篇NPC篇结束。(第三篇包裹篇待续)