2023/06/06:
搬运一个我自己在司马看雪论坛发过的帖子
2018/11/08:
1.0
本贴只针对win7 x64系统下的 32位D3D9程序,局限性太大,仅作为抛砖引玉 。
下面进入正题。
基本流程如下:
1:在内核中 hook相关shadowSSDT或SSDT函数
2:在目标进程申请内存,写入用于D3D9绘制的shellcode
3:利用APC的力量回到 用户模式,执行用于D3D9绘制的 shellcode。
1.1
首先,如果我们要做一个hook D3D9的绘制。
自然是要编写dll,注入到目标进程,
hook D3D9虚函数,获取到D3D9设备,执行绘制代码。
时至今日,这种技术已经是基本操作了。
经历各大游戏厂商(代{过}{滤}理商)以及外挂作者们之间的斗争,
这种注入以及hook的方式已然不可取。
那么是否有一种相对隐蔽的方式来完成这项工作呢?
答案是有的。(废话,没有你写这贴干嘛【滑稽】)
从我们能想到的根本问题入手的话…
在应用层 hook D3D9虚函数以及直接注入dll会拉闸,
既然如此,我们在内核层做hook,代码全都以shellcode的形式
在目标进程里跑起来。
1.2
在win7 x64下, 通过调试一个网上下载的D3D9示例Demo
仔细跟踪其 D3D9的Present函数,我们发现 Present 最终会调用
USER32.HungWindowFromGhostWindow+20处的代码
反汇编如下:
call fs:[000000C0]其实是call Wow64子系统的调用
经过Wow64子系统的包装,最终进入内核。
mov eax, 000012CF这句汇编代码
其中 000012CF代表的是 shadowSSDT 的索引值
了解过 SSDT&shadowsSSDT的朋友都知道
第一个4k页面指向的是SSDT函数
第二个4k页面指向的是shadowSSDT函数
另外两个页面未使用(这是题外话。)
00000012CF在第二个页面上,所以这个索引指向shadowSSDT函数。
我们减去 0x1000 就是它的真实索引值 0x02CF,十进制就是 719。
我们打开PChunter这款非常有用的软件, 我们可以看到索引719指向的
shadowSSDT函数名 NtUserHwndQueryRedirectionInfo
接下来我们就hook这个函数并写好过滤
一般来说,这种shadowSSDT函数,只传进来一个参数(rcx), 保险起见我们写四个。
示例过滤函数如下:(自备shadowSSDT hook代码,本贴不会放完整代码,防止伸手)
But, 光是这样是不够的, 我们还要在这里获取到D3D9的设备。
这就给我们出了一个难题,一般在应用层都是hook取设备的。
经过查资料,我们想到了一个好办法,利用栈回溯,找到最初调用
Present函数的地方, 并把第一个参数(也就是设备)取出来。
好在windows x86下的栈回溯是基于EBP来回溯的,原理相对简单,
我们可以手动编写一下。
1.3
应用层在通过syscall进入内核之前,会把当前的TrapFrame保存
起来。在内核中当前_KTHREAD成员 TrapFrame 就是应用层保存的
TrapFrame 指针。
在win7 x64下,其结构偏移为 0x1D8。
至此,我们可以顺利的获取应用层最后保存的RBP了。
接下来写栈回溯,具体原理我就不多做描述
大家可以自行百度了解一下相关知识
示例代码如下:
笔者这里偷了个懒,直接判断call 地址是否小于 0x00500000
以此确定 当前帧就是call present的。
当然,获取到设备的同时,得以确认,当前线程就是从
Present函数一直call 进内核的。下面就可以放心的执行绘制操作了。
2.0
在内核中给当前进程申请内存,最方便的莫过于ZwAllocateVirtualMemory
申请内存成功之后,写入shell code。
比如直接call D3D9的 Clear函数,画个方块。
示例如下:
要注意的是,不要一直不停的申请内存,我想各位看官应该不会这么做[滑稽]。
3.0
做完如上工作,接下来就可以想办法回到用户模式,执行shellcode了。
回到用户模式的方法有好几种,比如KeUserModeCallBack, APC。
其中 KeUserModeCallBack 需要在应用层中写好代{过}{滤}理分发,并且如果目标进程是
wow64进程,还需要多写一层代{过}{滤}理函数,着实麻烦。
所以我们采用相对简单的 APC,利用APC机制回到用户模式。
代码如下:
注意这里的细节,如果目标进程是 wow64进程,起始地址需要 / -4。
然后就是常规的初始化APC,插入队列。
由于我们是回到当前进程的应用层,所以初始化APC时,
KeInitializeApc第二个参数填 PsGetCurrentThread()。
最后利用KeTestAlertThread直接交付触发队列里的APC Routine。
3.1
放上效果图如下:
3.2
But,出现了新的问题
经过笔者测试,有些系统并不走 NtUserHwndQueryRedirectionInfo
而是另外的ShadowSSDT函数,不过我们依然有办法解决这种小问题。
在出问题的机器上,一通调试分析后,找到如下代码
图片可能有点模糊
命中断点位置的代码是cmp [esi+760],ebx
其中 esi+760 = 0xC2D4118
然而地址 0xC2D4118的值为0,
所以代码会 je D3D9.DLL+7C47
抛开这里往下看
如果没有 je D3D9.DLL+7C47
代码会走进这里
一路跟进去, Go Go Go
再跟
最终看到了熟悉的代码, 是 NtUserHwndQueryRedirectionInfo
3.3
然而直接走到 D3D9.DLL+7C47 肯定不会经过 NtUserHwndQueryRedirectionInfo
最简单的解决方式肯定是直接把 je D3D9.DLL+7C47 nop
测试结果如下
很明显
改了之后加载驱动可以直接绘制上这个 基佬色 的方块
然而还原之后
绘制效果消失了
而且在 USER32.HungWindowFromGhostWindow+20处
下断点是不会命中的(这不是废话吗)
4.0
梳理流程
判断函数主要判断 [esi+760] 是否为0
如果为0,直接走 D3D9.DLL+7C47,否则走 NtUserHwndQueryRedirectionInfo
这里我有必要提一下,不要想着锁定 [esi+760] 的值,这样做会导致D3D无限初始化Surface
虽然进了 NtUserHwndQueryRedirectionInfo,但是会一直在这里死循环,不再进行任何绘制。
而且修改代码段的内存来强行让他走 NtUserHwndQueryRedirectionInfo 恐怕也不是各位看官想要的结果.
4.1
摆在面前的有两条路
1: 在创建窗口->创建D3D设备 这个流程中仔细分析创建需要的参数,找出其原因.
2: 在这个绘制流程中,进入判断函数前 改掉 [esi+760] 的值,进入 NtUserHwndQueryRedirectionInfo 之后再改回来
由于时间有限,我姑且只讲一下第二种方式。
笔者重新理了一下执行流程
如图所示,我们只需要在 D3DKMTPresent 这个函数进入内核之后
改变其 [esp+760] 的值为1
在进入判断代码时,就会顺理成章的进入 NtUserHwndQueryRedirectionInfo
然后我们在 NtUserHwndQueryRedirectionInfo里把 [esp+760] 的值改成 0
这样走到后面的检测代码中,就不会无限获取D3D9主Surface导致死循环。
还有个很重要的问题 esp+760 如何获取,这个比较简单了,经过笔者指针扫描+瞎猜
这个值可以通过如下方式获得。
最后一级偏移为760,所得地址便是 esp+760 ,所得值便是 [esp+760]。
结语:
来自未来的时间(2023/06/06):
以后有空再对这类型内容做研究,届时依然会更新
你以为这就完了?
事实上这个所谓的[esp+760] 来自于Win7的DWM Aero效果开关
如果是开启的状态下,则不需要我们再多做任何额外的Hook操作
你以为这就完了?
推荐一个有意思的瓜
吃瓜这里
嗯,这次真完了