0%

FPS 3DHook

D3D Hook

之前做FPS的透视自瞄,都是通过三维坐标进行三角函数计算实现的方框自瞄,但是这种确实是有一定的占用内存的嫌疑,所以使用D3DHook是相对而言比较轻松的一种写法,而且也是比较固定的一个框架。

简单的D3D知识

1
2
// 从索引缓存区绘制图元,参数 1  为图元格式,参数 4  为顶点数,参数 6  为三角形数
m_pDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4 );

这句代码就是用来绘制我们的模型的,是位于:

1
2
3
4
m_pDevice->BeginScene();

/**/
m_pDevice->EndScene();

之间的一句代码,如果将其注释掉,那么将不会再绘制模型了,这也是我们实现Hook的一个关键点,因为我们在绘制完毕模型之后,我们就可以调用类中的一个方法:m_pDevice->SetRenderState( D3DRS_ZENABLE, D3DZB_FALSE );这句代码如果不使用,默认为FALSE,但是开发人员为了将我们的模型隐藏在建筑物的后面,他会将我们的这个代码设置为TRUE,以此进行任务的遮挡,其实也就是我们的一个前面的物体遮挡后面物体的一个效果。
所以我们的基本思路也就定下来了,也就是我们Hook了DrawIndexedPrimitive之后,然后将指定的模型SetRenderState为FALSE就可以了。

实现

因为我们要Hook的DrawIndexedPrimitive方法是一个成员方法,所以我们需要使用基址加偏移的方法进行一个Hook,其实C++的一个类成员方法的调用,第一个参数传递的就是成员本身,所以说,也就是调用了一个全局方法,然后将对象自身传递过去就实现了所谓的类方法调用,所以我们是要找到这个类方法的一个地址,但是这个地址比较特殊,我们是要使用基址加偏移的方法进行定位。
首先我们自己写一个这个方法,然后找到对应的地址:

1
2
3
4
5
6
7
8
9
10
11
00AA19A6 8B 46 78             mov         eax,dword ptr [esi+78h]  
00AA19A9 8B 08 mov ecx,dword ptr [eax]
00AA19AB 8B 91 48 01 00 00 mov edx,dword ptr [ecx+148h]
00AA19B1 6A 04 push 4
00AA19B3 6A 00 push 0
00AA19B5 6A 04 push 4
00AA19B7 6A 00 push 0
00AA19B9 6A 00 push 0
00AA19BB 6A 04 push 4
00AA19BD 50 push eax
00AA19BE FF D2 call edx

可以看到他call的函数并不是一个固定的地址,而是一个寄存器,寄存器的地址,其实也可以一步步推出来,比较关键的就是那个ESI,他的值是:0x012ffd38,但是我没校验是不是基地址,一般可以我感觉,但是这样有点麻烦,比较简单的方法就是直接进入edx那个地址,然后计算相对于DLL的一个偏移量,计算的时候用模块基地址加上偏移量就可以了。
然后我们跟进去看一下:

1
2
3
547348C0 8B FF                mov         edi,edi  
547348C2 55 push ebp
547348C3 8B EC mov ebp,esp

模块是:d3d9.DLL,一计算发现RVA为:0x548c0,前五个字节是正好的,所以我们也就没啥麻烦的了,直接Hook就好了。
这里其他的一些代码我就不列出来了,因为不关键,我就写关键的:
首先我们需要一个计算地址的函数:

1
2
3
4
5
6
7
ULONG_PTR GetDrawIndexedPrimitiveAddress()
{
HANDLE h = GetModuleHandle("d3d9.dll");
if(h==INVALID_HANDLE_VALUE)
return NULL;
return (ULONG_PTR)h+0x548c0;
}

这样子就能找到函数的地址了,因为我们的DLL地址会变,所以每次都要找的。
然后当我们的DLL被注入的时候我们就去HOOKDrawIndexedPrimitive这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool HookDrawIndexedPrimitive()
{
ULONG_PTR address = GetDrawIndexedPrimitiveAddress();
jmpto =address+5;
DWORD oldPro;
if(VirtualProtect((LPVOID)address,5,PAGE_EXECUTE_READWRITE,&oldPro))
{
DWORD value = (DWORD)MyDrawIndexedPrimitive-address-5;
_asm
{
mov eax,address
mov byte ptr[eax],0xe9
add eax,1
mov ebx,value
mov dword ptr[eax],ebx
}
VirtualProtect((LPVOID)address,5,oldPro,&oldPro);
}
return true;
}

获取到函数地址之后,我们就要写HOOK,MyDrawIndexedPrimitive这个使我们要跳到的地址,我们这里使用的HOOK方法我是看别人这么写的,大同小异,也就是第一个e9为jmp,然后再将我们的地址放进去就好了,简单计算一下得到value就可以了。

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
//这个WINAPI加不加代表返回的时候是retn还是retn 1c
//被这个关键字修饰的函数,其参数都是从右向左通过堆栈传递的(__fastcall 的前面部分由ecx,edx传), 函数调用在返回前要由被调用者清理堆栈(这句话是关键)。自己在退出时清空堆栈,所以这里要加否则堆栈不平衡
HRESULT WINAPI MyDrawIndexedPrimitive(LPDIRECT3DDEVICE9 pDevice,D3DPRIMITIVETYPE dtype,INT BaseVertexIndex,UINT MinVertexIndex,UINT NumVertices,UINT startIndex,UINT primCount)
{
IDirect3DVertexBuffer9* pStreamData = NULL;
UINT iOffsetInBytes,iStride;
if (pDevice->GetStreamSource(0,&pStreamData,&iOffsetInBytes,&iStride)==D3D_OK)
{
pStreamData->Release();
}
if (iStride==16)//深度缓存的值
{
pDevice->SetRenderState(D3DRS_ZENABLE,FALSE);
}
return OriDrawIndexedPrimitive(pDevice,dtype,BaseVertexIndex,MinVertexIndex,NumVertices,startIndex,primCount);
/*
_asm
{
mov esp,ebp
pop ebp
//原代码
mov edi,edi
push ebp
mov ebp,esp
mov eax,jmpto
jmp eax
}*/
//_asm
//{
// mov esp,ebp
// pop ebp
// retn 0x1c
//}
}

这里可以看到,原函数其实是6个参数,但是我们要七个参数,其实也就是我们要先接受自身,然后我们在最后的时候要跳回去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD jmpto = 0;

__declspec(naked) HRESULT WINAPI OriDrawIndexedPrimitive(LPDIRECT3DDEVICE9 pDevice,D3DPRIMITIVETYPE dtype,INT BaseVertexIndex,UINT MinVertexIndex,UINT NumVertices,UINT startIndex,UINT primCount)
{
_asm
{
mov edi,edi
push ebp
mov ebp,esp
mov eax,jmpto
jmp eax
}

}

一个空函数,jmpto是在我们之前定义的,这里需要注意的是我们需要使用WINAPI表示我们这个函数是stdcall的调用约定,这里我踩坑了,否则我们在函数执行结束之后是不能进行自动清理堆栈的。
这里我们就是一个基本的D3D操作,意思就是说iStride深度缓存的值未16的时候我们就让他透视,其实这个16是我们需要调试寻找的,可以通过很多手段,例如共享内存操作之类的,也可以使用设置新的lang来实现,反正很多都是可以找到的,但是我们找到16之后并不是万事大吉,因为进入游戏发现地图也没有了,所以我们要判断NumVertices的值,这个是模型的顶点数,也是需要寻找的,但是却不是最好的方法,最好的方法是我们通过附加游戏找到调用DrawIndexedPrimitive这个函数的外层call找到有没有一个标识来代表是否可以表示人,我们再通过hook之类的判断来告诉我们是不是需要清除深度缓存。