梦醒暗黑廿年(二)

梦醒暗黑廿年

向抗击疫情的英雄们致敬
宇春秋

第二篇: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篇结束。(第三篇包裹篇待续)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值