欢迎转载,转载请注明出处:http://blog.csdn.net/gnorth/article/details/9327971
说白了,也就是HOOK掉Present,这种代码,其实百度上某些地方有,但是很多人估计不知道怎样得到Present的地址。
所以就有些奇葩的例子:
先到游戏的登录器内把CreateProcess之类的HOOK掉,让游戏进程暂停启动,然后注入游戏 HOOK Direct3DCreate9 得到 IDirect3D9 对象之后,又得到 IDirect3DDevice9 对象,最终得到Present,反正挺蛋疼的,而且就是在游戏创建对象前要HOOK掉,用户体验非常的差。
其实自己创建个对象,从虚函数表就拿到真正的函数地址了。
本文的一大堆废话只是为了说明实现流程,由于写得比较仓促,所以嘛,估计要有一定基础的人才看得懂,最后面有完整的代码,你不想理解流程的话直接复制去用吧。
创建对象的代码如下:
BOOL InitializeD3D9(HWND hWnd, IDirect3D9 **ppD3D9, IDirect3DDevice9 **ppDev)
{
*ppD3D9 = Direct3DCreate9(D3D_SDK_VERSION);
if(!(*ppD3D9))
return FALSE;
D3DDISPLAYMODE d3ddm;
HRESULT hr = (*ppD3D9)->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
if(FAILED(hr))
{
(*ppD3D9)->Release();
return FALSE;
}
D3DPRESENT_PARAMETERS d3dpp = {0};
d3dpp.Windowed= TRUE;
d3dpp.SwapEffect= D3DSWAPEFFECT_DISCARD;
d3dpp.BackBufferFormat= d3ddm.Format;
hr = (*ppD3D9)->CreateDevice(
D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
ppDev);
if(FAILED(hr))
{
(*ppD3D9)->Release();
return FALSE;
}
return TRUE;
}
void ReleaseD3D9(IDirect3D9 *pD3D9, IDirect3DDevice9 *pDev)
{
if(pDev)
pDev->Release();
if(pD3D9)
pD3D9->Release();
}
LPVOID GetDirectDeviceMemberProc(LPVOID pDev, DWORD dwOffset) //根据偏移从虚函数表获取函数地址
{
LPVOID pRet = NULL;
__asm{
mov eax, dword ptr [pDev]
mov ecx, dword ptr [eax]
mov ebx, dwOffset
mov eax, dword ptr [ecx + ebx]
mov pRet, eax
}
return pRet;
}
这个代码要有DXSDK才行,需要依赖这两个,d3d9.h d3d9.lib,由于这种比较恶心的代码,一般就是静态库里一丢就永远不管了,但是呢,丢静态库里,假如不用这种功能的代码调用静态库时,就要出警告提示你,你木有用到d3d9.dll中的代码云云,非常不爽。
所以针对这个问题,我就自己调试了下,写了一个动态调用创建D3D设备对象的代码出来:最后面有完整的代码,你不想理解流程的话直接复制去用吧。
大概流程如下:
首先 Direct3DCreate9 是个标准API导出函数,所以,直接加载d3d9.dll,获取它即可
HMODULE hD3d9 = LoadLibraryA("d3d9.dll");
LPVOID _Direct3DCreate9 = (LPVOID)GetProcAddress(hD3d9, "Direct3DCreate9");
__asm{
push 32 //D3D_SDK_VERSION 32
call _Direct3DCreate9
mov pD3D9, eax
}
if(!pD3D9)
return false;
这样就得到了接口对象,接下来是Release 以及 GetAdapterDisplayMode
77: (*ppD3D9)->Release();
00401243 8B 4D 0C mov ecx,dword ptr [ebp+0Ch]
00401246 8B 11 mov edx,dword ptr [ecx]
00401248 8B 45 0C mov eax,dword ptr [ebp+0Ch]
0040124B 8B 08 mov ecx,dword ptr [eax]
0040124D 8B 01 mov eax,dword ptr [ecx]
0040124F 8B F4 mov esi,esp
00401251 52 push edx
00401252 FF 50 08 call dword ptr [eax+8]
它的偏移是8
GetAdapterDisplayMode
72: HRESULT hr = (*ppD3D9)->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &d3ddm);
00401209 8B F4 mov esi,esp
0040120B 8D 45 EC lea eax,[ebp-14h]
0040120E 50 push eax
0040120F 6A 00 push 0
00401211 8B 4D 0C mov ecx,dword ptr [ebp+0Ch]
00401214 8B 11 mov edx,dword ptr [ecx]
00401216 8B 45 0C mov eax,dword ptr [ebp+0Ch]
00401219 8B 08 mov ecx,dword ptr [eax]
0040121B 8B 01 mov eax,dword ptr [ecx]
0040121D 52 push edx
0040121E FF 50 20 call dword ptr [eax+20h]
偏移为0x20
LPVOID d3d9_Release = GetDirectDeviceMemberProc(pD3D9, 0x08);
LPVOID _GetAdapterDisplayMode = GetDirectDeviceMemberProc(pD3D9, 0x20);
//先调用_GetAdapterDisplayMode
//这里要传入一个 D3DDISPLAYMODE结构指针,但是外面并不需要对这个结构进行操作,所以这里只需要知道结构大小,然后分配一个一样大的内存给它就好。
//sizeof(D3DDISPLAYMODE) 大小为 0x10
BYTE d3ddm[0x10]
HRESULT hr = NULL;
__asm{
lea eax, d3ddm
push eax
push 0
push pD3D9
call _GetAdapterDisplayMode
mov hr, eax
}
if(FAILED(hr)) // hr != S_OK;就释放对象,说明到这里就创建失败了,具体为啥失败我不懂
{
__asm{
push pD3D9
call d3d9_Release
}
return false;
}
接下来要用到 D3DPRESENT_PARAMETERS 结构,这个结构要在外面填参数了,所以,就要去看他的原型,看哪些是我们需要填的偏移。
它的大小是0x38, 注意我后面注释的值是偏移。
typedef struct _D3DPRESENT_PARAMETERS_
{
UINT BackBufferWidth; //0
UINT BackBufferHeight; //4
D3DFORMAT BackBufferFormat; //8
UINT BackBufferCount; //c
D3DMULTISAMPLE_TYPE MultiSampleType; //10 enum
DWORD MultiSampleQuality; //14
D3DSWAPEFFECT SwapEffect; //18 enum
HWND hDeviceWindow; //1C
BOOL Windowed; //20
BOOL EnableAutoDepthStencil;//24
D3DFORMAT AutoDepthStencilFormat;//28 enum
DWORD Flags; //2c
/* FullScreen_RefreshRateInHz must be zero for Windowed mode */
UINT FullScreen_RefreshRateInHz;//30
UINT PresentationInterval; //34
} D3DPRESENT_PARAMETERS;
我们可以从结构上看到,需要操作的偏移如下
Windowed; //20
SwapEffect; //18
BackBufferFormat; //8
然后代码中可以看到,BackBufferFormat是从d3ddm里过来的
结构是这样, Format的偏移为0x0c
typedef struct _D3DDISPLAYMODE
{
UINT Width;
UINT Height;
UINT RefreshRate;
D3DFORMAT Format; //0x0c
} D3DDISPLAYMODE;
所以代码如下:
BYTE d3dpp[0x38];
memset(d3dpp, 0, 0x38);//如果不把结构填充为0,创建设备就会失败。
__asm{
lea eax, d3dpp
mov dword ptr [eax + 0x20], 1
mov dword ptr [eax + 0x18], 1
lea ebx, d3ddm
mov ecx, dword ptr [ebx + 0x0C]
mov dword ptr [eax + 8], ecx
}
最后就是创建设备的代码
86: hr = (*ppD3D9)->CreateDevice(
87: D3DADAPTER_DEFAULT,
88: D3DDEVTYPE_HAL,
89: hWnd,
90: D3DCREATE_SOFTWARE_VERTEXPROCESSING,
91: &d3dpp,
92: ppDev);
00401287 8B F4 mov esi,esp
00401289 8B 55 10 mov edx,dword ptr [ebp+10h]
0040128C 52 push edx
0040128D 8D 45 B4 lea eax,[ebp-4Ch]
00401290 50 push eax
00401291 6A 20 push 20h
00401293 8B 4D 08 mov ecx,dword ptr [ebp+8]
00401296 51 push ecx
00401297 6A 01 push 1
00401299 6A 00 push 0
0040129B 8B 55 0C mov edx,dword ptr [ebp+0Ch]
0040129E 8B 02 mov eax,dword ptr [edx]
004012A0 8B 4D 0C mov ecx,dword ptr [ebp+0Ch]
004012A3 8B 11 mov edx,dword ptr [ecx]
004012A5 8B 0A mov ecx,dword ptr [edx]
004012A7 50 push eax
004012A8 FF 51 40 call dword ptr [ecx+40h] <-----------偏移0x40
LPVOID _CreateDevice = GetDirectDeviceMemberProc(pD3D9, 0x40);
//0x40
__asm{
lea eax, pD3D9Dev
push eax
lea eax, d3dpp
push eax
push 0x20
push hWnd
push 1
push 0
push pD3D9
call _CreateDevice
mov hr, eax
}
if(FAILED(hr))
{
__asm{
push pD3D9
call d3d9_Release
}
return false;
}
最终得到设备对象后,就用同样的方法,从虚函数表获取真正的函数地址, Present的偏移为0x44
inline钩子函数,大概如下:
typedef HRESULT (WINAPI *DEFPRESENT)(LPDIRECT3DDEVICE9,CONST RECT *,CONST RECT *,HWND,CONST RGNDATA *);
DEFPRESENT PresentData = NULL;
HRESULT WINAPI _Present(LPDIRECT3DDEVICE9 p1, CONST RECT *p2, CONST RECT *p3, HWND p4, CONST RGNDATA *p5)
{
Sleep(64);//这里也就是降帧了
return PresentData(p1, p2, p3, p4, p5);
}
但是,这种代码,一般得写在DLL里才行,要是DLL卸载出来了,代码自然就没了,你不卸载HOOK就是一个字,“崩”!
而有些非常时期,却又不得不卸载DLL。所以嘛,就自然有了ShellCode的代码。
ShellCode不必完全模仿完善的inline hook 钩子函数,因为要模仿起来太累了, 因为存在PresentData中间代码实际上也是ShellCode。
所以大概原型如下:
__asm{
pushad
pushfd
//这里call Sleep
以及执行它的前5个字节
mov edi, edi
push ebp
mov ebp, esp 好像是这个样子
popfd
popad
jmp 原始Present + 5
}
这个代码实现起来不难,要注意的是,Sleep是不能直接调用的,因为Sleep实际上会被编译器进行包装,分配一个指针来包装它,然后call dword ptr [包装的指针]
而这个指针,在你的dll卸载时,也会无效掉。所以我们应该自己包装Sleep
PBYTE psc = (PBYTE)VirtualAllocEx(GetCurrentProcess(), NULL, 4, MEM_COMMIT | MEM_RESERVE, 0x40);
*(PDWORD)psc = (DWORD)Sleep;
//使用VirtualAllocEx进行分配内存,使用MEM_COMMIT | MEM_RESERVE模式,这样分配出来的内存不会跟随你的DLL一起被释放掉。
//其他的几种方式 new 会跟随dll释放
//C函数库的 malloc也会释放
//GlobalAlloc 不能改变为可执行可读写保护模式
//还好最后剩VirtualAllocEx能用
//其实我早就感觉,dll卸载时会把dll new 出来的各种内存都释放掉了,也就是通过这个例子,我终于证实了这个结论。
ShellCode 如下
BYTE pCode[22] = {
0x60, //pushad
0x9C, //pushfd
0x6A, bTime, //Sleep参数
0xFF, 0x15, 0x00, 0x00, 0x00, 0x00, //call dword ptr [psc] 暂时留空,在下面会对这里写入
0x9D, //popfd
0x61, //popad 我断了下Sleep看过,其实被改变的通用寄存器好像只有eax ecx edx这几个,但为了代码更简单点,所以直接pushad pushfd popad popfd
0x8B, 0xFF, //mov edi, edi 实际上这个代码是可以不写的
0x55, // push ebp
0x8B, 0xEC, //mov ebp, esp
0xE9, 0x00, 0x00, 0x00, 0x00, //jmp 回 Present + 5 留到下面去写入跳转相对地址
};
*((PDWORD)(pCode + 6)) = (DWORD)psc; //把Sleep的包装地址写入进去,由于call dword ptr 不是用的相对地址,所以直接写入地址进去即可
pCode是在局部分配来写ShellCode的,
所以需要分配一个能够保留的地址,拷贝一份代码过去。
PBYTE pShellCode = (PBYTE)VirtualAllocEx(GetCurrentProcess(), NULL, 22, MEM_COMMIT | MEM_RESERVE, 0x40);
memcpy(pShellCode, pCode, 22);
PDWORD pAddr = (PDWORD)(pShellCode + 18);//得到最后4字节的指针
*pAddr = (DWORD)pPresent - ((DWORD)pAddr - 1);//计算相对地址并写入
//这里普及下常识, 0xE8 call 0xE9 jmp 这种远跳 调用的代码
//它的值为相对地址,这个地址的计算公式为: 目标地址 - (指令地址 + 指令长度)
// (DWORD)pPresent - ((DWORD)pAddr - 1); 根据公式的话,这里看起来很诡异吧
//首先,我们要在这里跳回Present + 5 那么目标就是 (Present + 5 ) 然后,我们跳转的指令地址是在这里 0xE9, 0x00, 0x00, 0x00, 0x00, 从0xE9开始
//但是(PDWORD)(pShellCode + 18);//这里取得最后的4字节的地址时,已经到0xE9后面的一个字节去了, 所以:跳转地址实际上为(pShellCode - 1 + 5)
根据公式: 就应该是这样 (Present + 5 ) - (pShellCode - 1 + 5) 即: (DWORD)pPresent - ((DWORD)pAddr - 1);
一切准备完毕之后,用同样的方法,让原始的Present头部跳到pShellCode头部,这里需要VirtualProtect 改变一下要操作的那几个字节为可读写可执行,操作完后改回去
DWORD dwProtect = 0;
VirtualProtect(pPresent, 5, 0x40, &dwProtect);
*(PBYTE)pPresent = 0xE9;
*((PDWORD)((PBYTE)pPresent + 1)) = (DWORD)pShellCode - (DWORD)pPresent - 5;
VirtualProtect(pPresent, 5, dwProtect, NULL);
完整的代码如下: