好久没写博客了, 一个月一篇还是要尽量保证,今天谈下Hook技术。

在Window平台上开发任何稍微底层一点的东西,基本上都是Hook满天飞, 普通应用程序如此,安全软件更是如此, 这里简单记录一些常用的Hook技术。

SetWindowsHookEx
基本上做Windows开发都知道这个API, 它给我们提供了一个拦截系统事件和消息的机会, 并且它可以将我们的DLL注入到其他进程。
但是随着64位时代的到来和Vista之后的UAC机制开启,这个API很多时候不能正常工作了:

首先,32位DLL没法直接注入到64位的应用程序里面, 因为他们的地址空间完全不一样的。当然尽管没法直接注入,但是在权限范围内,系统会尽量以消息的方式让你能收到64位程序的消息事件。

其次,UAC打开的情况下低权限程序没法Hook高权限程序, 实际上低权限程序以高权限程序窗口为Owner创建窗口也会失败, 低权限程序在高权限程序窗口上模拟鼠标键盘也会失败。

有人说我们可以关闭UAC, Win7下你确实可以,但是Win8下微软已经不支持真正关闭UAC, 从这里我们也可以看到微软技术过渡的方式, 中间会提供一个选项来让你慢慢适应,最后再把这个选项关掉, UAC和Aero模式都是如此。

那么我们如何解决这些问题?

对于64位问题 , 解决方法是提供2个DLL,分别可以Hook32和64位程序。

对于权限问题, 解决方法是提升权限, 通过注册系统服务, 由服务程序创建我们的工作进程。这里为什么要创建一个其他进程而不直接在服务进程里干活? 因为Vista后我们有了Session隔离机制,服务程序运行在Session 0,我们的其他程序运行在Session 1, Session 2等, 如果我们直接在服务程序里干活,我们就只能在Session 0里工作。通过创建进程,我们可以在DuplicateTokenEx后将Token的SessionID设置成目标Session,并且在CreateProcessAsUser时指定目标WinStation和Desktop, 这样我们就既获得了System权限,并且也可以和当前桌面进程交互了。

SetWinEventHook
很多人可能都不知道这个API, 但是这个API其实挺重要的, 看名字就知道它是Hook事件(Event)的, 具体哪些事件可以看这里.

为什么说这个API重要, 因为这个API大部分时候没有SetWindowsHookEx的权限问题, 也就是说这个API可以让你Hook到高权限程序的事件, 它同时支持进程内(WINEVENT_INCONTEXT)和进程外(WINEVENT_OUTOFCONTEXT)2种Hook方式, 你可以以进程外的方式Hook到64位程序的事件。

为什么这个API没有权限问题, 因为它是给Accessibility用的, 也就是它是给自动测试和残障工具用的, 所以它要保证有效。

我曾经看到这样一个程序,当任何程序(无论权限高低)有窗口拖动(拖标题栏改变位置或是拖边框改变大小), 程序都能捕获到, 当时很好奇它是怎么做到的?
Spy了下窗口消息, 知道有这样2个消息:WM_ENTERSIZEMOVE和WM_EXITSIZEMOVE表示进入和退出这个事件, 但是那也只能获得自己的消息,其他程序的消息它是如何捕获到的?当时怀疑用的是Hook, 却发现没有DLL注入。查遍了Windows API 也没有发现有API可以查询一个窗口是否在这个拖动状态。最后发现用的是SetWinEventHookEVENT_SYSTEM_MOVESIZESTART和EVENT_SYSTEM_MOVESIZEEND。

API Hook
常见的API Hook包括2种, 一种是基于PE文件的导入表(IAT), 还有一种是修改前5个字节直接JMP的inline Hook.

对于基于IAT的方式, 原理是PE文件里有个导入表, 代表该模块调用了哪些外部API,模块被加载到内存后, PE加载器会修改该表,地址改成外部API重定位后的真实地址, 我们只要直接把里面的地址改成我们新函数的地址, 就可以完成对相应API的Hook。《Windows核心编程》里第22章有个封装挺好的CAPIHook类,我们可以直接拿来用。
我曾经用API Hook来实现自动测试,见这里 API Hook在TA中的应用

对于基于Jmp方式的inline hook, 原理是修改目标函数的前5个字节, 直接Jmp到我们的新函数。虽然原理挺简单, 但是因为用到了平台相关的汇编代码, 一般人很难写稳定。真正在项目中用还是要求稳定, 所以我们一般用微软封装好的Detours, 对于Detours的原理,这里有篇不错的文章 微软研究院Detour开发包之API拦截技术

比较一下2种方式: 
IAT的方式比较安全简单, 但是只适用于Hook导入函数方式的API。
Inline Hook相对来说复杂点, 但是它能Hook到任何函数(API和内部函数),但是它要求目标函数大于5字节, 同时把握好修改时机或是Freeze其他线程, 因为多线程中改写可能会引起冲突。

还有一种是Hook被调用模块的导出表(EAT), 但是感觉一般用得用的不多。原理是调用模块里IAT中的函数地址是通过被调用模块的EAT获取的, 所以你只要修改了被调用模块的EAT中的函数地址,对方的调用就自然被你Hook了。但是这里有个时机问题, 就是你替换EAT表要足够早,要在IAT使用它之前才行。但是感觉这个行为是由PE加载器决定的, 一般人很难干涉, 不知道大家有什么好方法? 

我们一般可以将IAT Hook和EAT Hook结合起来使用, 先枚举所有模块Hook IAT,这样当前已有模块的API都被你Hook了,然后再Hook EAT, 这样后续的模块也被你Hook了(因为要通过EAT获取函数地址)。 

如果你只用Hook IAT而不Hook EAT, 当有新模块加载时,你要Hook它的IAT, 所以你就要Hook LoadLibrary(Ex)和GetProcAddress来拦截新模块的加载。所以理论上感觉 Hook EAT不是很有必要, 因为单用Hook IAT已经可以解决我们所有的问题了, HOOK IAT还有一种优势是我们可以过滤某个模块不Hook,而一旦hook EAT, 它就会影响我们所有调用该函数的模块。

COM Hook
Window上因为有很多开发包是以COM方式提供的(比如DirectX), 所以我们就有了拦截COM调用的COM Hook。
因为COM里面很关键的是它的接口是C++里虚表的形式提供的, 所以COM的Hook很多是时候其实就是虚表(vtable)的Hook。
关于C++ 对象模型和虚表可以看我这篇 探索C++对象模型

对于COMHook,考虑下面2种case:

一种是我们Hook程序先运行,然后启动某个游戏程序(DirectX 9), 我们想Hook游戏的绘画内容。

这种方式下, 我们可以先Hook API Direct3DCreate9, 然后我们继承于IDirect3D9, 自己实现一个COM对象返回回去, 这样我们就可以拦截到所有对该对象的操作,为所欲为了, 当然我们自己现实的COM对象内部会调用真正的Direct3DCreate9,封装真正的IDirect3D9。

当然有时我们可能不用替代整个COM组件,我们只需要修改其中一个或几个COM函数, 这种情况下我们可以创建真正的IDirect3D9对象后直接修改它的虚表, 把其中某些函数改成我们自己的函数地址就可以了。

其实ATL就是用接口替代的方式来调试和记录COM接口引用计数的次数, 具体可以看我这篇 理解ATL中的一些汇编代码

还有一种case是游戏程序已经在运行了, 然后才启动我们的Hook进程, 我们怎么样才能Hook到里面的内容?

这种情况下我们首先要对程序内存有比较详细的认识, 才能思考创建出来的D3D对象的虚表位置, 从而进行Hook, 关于程序内存布局,可见我这篇 理解程序内存

理论上说COM对象如果是以C++接口的方式实现, 虚表会位于PE文件的只读数据节(.rdata), 并且所有该类型的对象都共享该虚表, 所以我们只要创建一个该类型对象,我们就可以获得其他人创建的该类型对象的虚表位置,我们就可以改写该虚表实现Hook(实际操作时需要通过VirtualProtect修改页面的只读属性才能写入)。

但是实际上COM的虚表只是一块内存, 它并不一定是以C++实现, 所以它可以存在于任何内存的任何地方。另外对象的虚表也不一定是所有同类型的对象共享同一虚表, 我们完全可以每个对象都有自己的一份虚表。比如我发现IDirect3D9是大家共享同一虚表的(存在D3D9.dll), 但是IDirect3DDevice9就是每个对象都有自己的虚表了(存在于堆heap)。所以如果你要Hook IDirect3DDevice9接口,通过修改虚表实际上没法实现。

但是尽管有时每个对象的虚表不一样,同类型对象虚表里的函数地址却都是一样的, 所以这种情况下我们可以通过inline Hook直接修改函数代码。当然有些情况下如果是静态链接库,即使函数代码也是每个模块都有自己的一份, 这种情况下就只能反汇编获取虚表和函数的地址了。

最后,总结一下, 上面主要探讨了Windows上的各种Hook技术,通过将这些Hook技术组起来, 可以实现很多意想不到的功能, 比如我们完全可以通过Hook D3D实现Win7任务栏那种Thumbnail预览的效果(当然该效果可以直接由DWM API实现, 但是如果我们可以通过HOOK以动画的方式实现是不是更有趣 )
posted on 2013-10-30 11:03 Richard Wei 阅读(29739) 评论(13)  编辑 收藏 引用 所属分类: windows desktop

FeedBack:
# re: HOOK技术的一些简单总结
2013-10-30 20:58 | zgpxgame
mark  回复  更多评论
  
# re: HOOK技术的一些简单总结
2013-10-30 21:04 | StarsunYzL
总结的不错,API Hook还可以加一个EAT Hook。但是要是程序加了壳,IAT/EAT Hook通常就会失效,因为导入/导出表已被加密,所以最通用的还是Inline Hook,同时也是最复杂的,主要是要修正包含相对地址的指令。  回复  更多评论
  
# re: HOOK技术的一些简单总结[未登录]
2013-10-31 10:16 | 春秋十二月
搞windows技术,就是要让你不断跟着微软跑,兼容性是个很大的问题  回复  更多评论
  
# re: HOOK技术的一些简单总结
2013-10-31 10:34 | Richard Wei
@StarsunYzL
已经加上, 如有不正确的地方,欢迎指正.  回复  更多评论
  
# re: HOOK技术的一些简单总结
2013-10-31 16:38 | Richard Wei
@春秋十二月
同意,相对Unix/Linux的稳定, Windows开发要累得多  回复  更多评论
  
# re: HOOK技术的一些简单总结
2013-11-09 16:30 | 红色代码
好文.再可以加上SSDT HOOK等驱动级的文章,就更全面了  回复  更多评论
  
# re: HOOK技术的一些简单总结
2014-02-07 15:42 | allen
“ 但是IDirect3DDevice9就是每个对象都有自己的虚表了(存在于堆heap)。所以如果你要Hook IDirect3DDevice9接口,通过修改虚表实际上没法实现。”

这个说法是错误的,只要是C++对象,虚表都只会存在一份。  回复  更多评论
  
# re: HOOK技术的一些简单总结
2014-02-07 16:13 | Richard Wei
@allen
嗯, 单看这句话确实是错的, 但是你要结合我的上下文来看。
我的意思是你这里修改IDirect3DDevice9的虚表内容, 修改的也仅是你当前对象的, 而不会影响其他IDirect3DDevice9对象, 因为他们不是所有对象共享同一虚表的, 但是同一标准C++类的所有对象会共享同一虚表。  回复  更多评论
  
# re: HOOK技术的一些简单总结
2014-05-27 19:06 | allen
@Richard Wei
这也错了,我hook过IDirect3DDevice9的虚表函数,按你的说法我去hook这个借口完全无作用无意义,因为修改的都是自己的表,不会影响其它的IDirect3DDevice9。但实际上,IDirect3DDevice9只有一个虚函数表,对某个对象的虚函数表进行修改则影响所有的对象。  回复  更多评论
  
# re: HOOK技术的一些简单总结
2014-05-27 19:57 | Richard Wei
@allen
刚才有简单测试了下, 测试代码如下:
D3DPRESENT_PARAMETERS ps = {0};
ps.Windowed = TRUE;
ps.SwapEffect = D3DSWAPEFFECT_DISCARD;
ps.BackBufferFormat = D3DFMT_UNKNOWN;

pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL
, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &ps, &g_pD3dDevice);
if(g_pD3dDevice == NULL) break;


LPDIRECT3DDEVICE9 pNewD3dDevice = NULL;
pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL
, hWnd, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &ps, &pNewD3dDevice);

创建了2个IDirect3DDevice9对象, 然后分别打印它们的虚表指针和内容:

0:000> ?? g_pD3dDevice
struct IDirect3DDevice9 * 0x02815c00
+0x000 __VFN_table : 0x0281899c
0:000> ?? pNewD3dDevice
struct IDirect3DDevice9 * 0x0241bfe0
+0x000 __VFN_table : 0x0241ed7c
0:000> dps 0x0281899c
0281899c 6fe56f19 d3d9!CBaseDevice::QueryInterface
028189a0 6fe56992 d3d9!CBaseDevice::AddRef
028189a4 6fe56969 d3d9!CBaseDevice::Release
028189a8 6fe72587 d3d9!CBaseDevice::TestCooperativeLevel
028189ac 6ff0c6ad d3d9!CBaseDevice::GetAvailableTextureMem
028189b0 6ff49c8f d3d9!CD3DBase::EvictManagedResources
028189b4 6fe6b1c5 d3d9!CBaseDevice::GetDirect3D
028189b8 6fe56ff8 d3d9!CBaseDevice::GetDeviceCaps
028189bc 6fe8d144 d3d9!CBaseDevice::GetDisplayMode
028189c0 6fe7084e d3d9!CBaseDevice::GetCreationParameters
028189c4 6ff0bb74 d3d9!CBaseDevice::SetCursorProperties
028189c8 6ff0c04d d3d9!CBaseDevice::SetCursorPosition
028189cc 6fe8def0 d3d9!CBaseDevice::ShowCursor
028189d0 6fe6e9a0 d3d9!CBaseDevice::CreateAdditionalSwapChain
028189d4 6fe69ac7 d3d9!CBaseDevice::GetSwapChain
028189d8 6fe993ca d3d9!CBaseDevice::GetNumberOfSwapChains
028189dc 6feaf251 d3d9!CBaseDevice::Reset
028189e0 6fe9a064 d3d9!CBaseDevice::Present
028189e4 6feb1418 d3d9!CBaseDevice::GetBackBuffer
028189e8 6fe6bfe9 d3d9!CBaseDevice::GetRasterStatus
028189ec 6ff0c139 d3d9!CBaseDevice::SetDialogBoxMode
028189f0 6ff0c3bf d3d9!CBaseDevice::SetGammaRamp
028189f4 6ff0c4fd d3d9!CBaseDevice::GetGammaRamp
028189f8 6fe85ddb d3d9!CBaseDevice::CreateTexture
028189fc 6ff0ca68 d3d9!CBaseDevice::CreateVolumeTexture
02818a00 6feacb2d d3d9!CBaseDevice::CreateCubeTexture
02818a04 6fe72d69 d3d9!CBaseDevice::CreateVertexBuffer
02818a08 6fe732e6 d3d9!CBaseDevice::CreateIndexBuffer
02818a0c 6fea0127 d3d9!CBaseDevice::CreateRenderTarget
02818a10 6ff0cd88 d3d9!CBaseDevice::CreateDepthStencilSurface
02818a14 6ff0e0e0 d3d9!CBaseDevice::UpdateSurface
02818a18 6fe846ab d3d9!CBaseDevice::UpdateTexture
0:000> dps 0x0241ed7c
0241ed7c 6fe56f19 d3d9!CBaseDevice::QueryInterface
0241ed80 6fe56992 d3d9!CBaseDevice::AddRef
0241ed84 6fe56969 d3d9!CBaseDevice::Release
0241ed88 6fe72587 d3d9!CBaseDevice::TestCooperativeLevel
0241ed8c 6ff0c6ad d3d9!CBaseDevice::GetAvailableTextureMem
0241ed90 6ff49c8f d3d9!CD3DBase::EvictManagedResources
0241ed94 6fe6b1c5 d3d9!CBaseDevice::GetDirect3D
0241ed98 6fe56ff8 d3d9!CBaseDevice::GetDeviceCaps
0241ed9c 6fe8d144 d3d9!CBaseDevice::GetDisplayMode
0241eda0 6fe7084e d3d9!CBaseDevice::GetCreationParameters
0241eda4 6ff0bb74 d3d9!CBaseDevice::SetCursorProperties
0241eda8 6ff0c04d d3d9!CBaseDevice::SetCursorPosition
0241edac 6fe8def0 d3d9!CBaseDevice::ShowCursor
0241edb0 6fe6e9a0 d3d9!CBaseDevice::CreateAdditionalSwapChain
0241edb4 6fe69ac7 d3d9!CBaseDevice::GetSwapChain
0241edb8 6fe993ca d3d9!CBaseDevice::GetNumberOfSwapChains
0241edbc 6feaf251 d3d9!CBaseDevice::Reset
0241edc0 6fe9a064 d3d9!CBaseDevice::Present
0241edc4 6feb1418 d3d9!CBaseDevice::GetBackBuffer
0241edc8 6fe6bfe9 d3d9!CBaseDevice::GetRasterStatus
0241edcc 6ff0c139 d3d9!CBaseDevice::SetDialogBoxMode
0241edd0 6ff0c3bf d3d9!CBaseDevice::SetGammaRamp
0241edd4 6ff0c4fd d3d9!CBaseDevice::GetGammaRamp
0241edd8 6fe85ddb d3d9!CBaseDevice::CreateTexture
0241eddc 6ff0ca68 d3d9!CBaseDevice::CreateVolumeTexture
0241ede0 6feacb2d d3d9!CBaseDevice::CreateCubeTexture
0241ede4 6fe72d69 d3d9!CBaseDevice::CreateVertexBuffer
0241ede8 6fe732e6 d3d9!CBaseDevice::CreateIndexBuffer
0241edec 6fea0127 d3d9!CBaseDevice::CreateRenderTarget
0241edf0 6ff0cd88 d3d9!CBaseDevice::CreateDepthStencilSurface
0241edf4 6ff0e0e0 d3d9!CBaseDevice::UpdateSurface
0241edf8 6fe846ab d3d9!CBaseDevice::UpdateTexture

可以看到g_pD3dDevice的虚表地址是0x0281899c, pNewD3dDevice的虚表地址是0x0241ed7c,他们各自拥有自己虚表,尽管虚表里的内容是一样。如果你改变第一个对象的虚表内容,理论上不会影响第二个对象。  回复  更多评论
  
# re: HOOK技术的一些简单总结
2014-06-23 14:36 | allen
@Richard Wei

哦,之前理解错了。
应该说,我hook的不是虚表,而是hook的虚表里面函数指针指向的函数。  回复  更多评论
  
# re: HOOK技术的一些简单总结
2016-08-18 20:09 | 陈利敏
博主,我现在遇到一个问题,对于不同游戏,有的能Hook到createdevice函数,进入自定义的createdevice函数,但是有的游戏不行,不知道博主有没有遇到过,能否给予帮助,我的代码来源http://bbs.pediy.com/showthread.php?t=85368  回复  更多评论
  
# re: HOOK技术的一些简单总结
2016-08-19 12:23 | Richard Wei
@陈利敏
可以用先用API Monitor 或者WinDbg的API断点看看对方程序是如何工作的  回复  更多评论
  

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理