x64内核中的HOOK技术|拦截进程、拦截线程、拦截模块(思路)
一、为什么讲解HOOK技术?
在32系统下,例如我们要HOOK SSDT表,那么直接讲CR0的内存保护属性去掉,直接讲表的地址修改即可。
但是在64位系统下,不可以这样操作了。
第一是因为 SSDT表加密了。第二是 SSDT表你就算解密了,那么你的API的地址也会放的很远。SSDT表放不下4G以外的地址,所以也不能放。
而现在试想一下,杀毒软件也要在内核中做点手脚。如果不能HOOK了,它们该怎么发展那?
所以到了win7 (64位)系统下,微软被玩怕了。直接告诉你,你想HOOK那个API,我帮你做,你不用动手做,而且我的还稳定,不会蓝屏。所以x64下的HOOK都是很简单的。
比如 xxx杀毒软件要监视进程的创建,那么微软就说,我给你API调用,你提供回调。当创建的时候我就通知你,返回值也是你说了算。这样我们就可以使用它的API了,线程、模块都是这样的。
二丶进程监控以及拦截HOOK
API:
PsSetCreateProcessNotifyRoutineEx
函数原型
NTSTATUS
PsSetCreateProcessNotifyRoutineEx(
IN PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, //你提供的回调函数地址.
IN BOOLEAN Remove //是否删除回调. 如果为False则是注册回调,如果为TRUE则是删除回调.
);
这个函数的作用就是 当进程创建的时候会通知你。
参数一回调函数原型
VOID
CreateProcessNotifyEx(
__inout PEPROCESS Process, //进程的EPROCESS会提供给你.
__in HANDLE ProcessId, //进程ID会提供给你.
__in_opt PPS_CREATE_NOTIFY_INFO CreateInfo //进程信息的额外信息会提供给你. 注意参数是可操作的.也就是说可能为NULL
);
进程额外信息结构体
typedef struct _PS_CREATE_NOTIFY_INFO {
__in SIZE_T Size; //大小.
union {
__in ULONG Flags;
struct {
__in ULONG FileOpenNameAvailable : 1;
__in ULONG Reserved : 31;
};
};
__in HANDLE ParentProcessId; //父进程ID
__in CLIENT_ID CreatingThreadId;
__inout struct _FILE_OBJECT *FileObject; //文件对象
__in PCUNICODE_STRING ImageFileName; //进程路径
__in_opt PCUNICODE_STRING CommandLine; //命令行参数
__inout NTSTATUS CreationStatus; //返回状态.
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
我们如果对返回状态进行操作,那么如果你返回值给失败,那么进程就不会创建,也就实现了我们阻止进程的创建了。
内核 代码实际操作 阻止calc计算器的创建。
initHook函数代码.
void InstallHook()
{
NTSTATUS status; //定义NT状态
status = PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx, FALSE);//注册回调
if (NT_SUCCESS(status)) //判断状态是否成功.
dprintf("[Hello] PsSetCreateProcessNotifyRoutineEx CreateProcessNotifyEx=%pOK\r\n", CreateProcessNotifyEx);
else
dprintf("[Hello] PsSetCreateProcessNotifyRoutineEx status=%d\r\n", status);
}
回调函数代码
VOID CreateProcessNotifyEx( __inout PEPROCESS Process,
__in HANDLE ProcessId,
__in_opt PPS_CREATE_NOTIFY_INFO CreateInfo)
{
UNICODE_STRING ustrCalc; //定义内核函数字符串
RtlInitUnicodeString(&ustrCalc, L"\\??\\C:\\Windows\\system32\\calc.exe");//初始化计算器名字. 注意,这个要带上符号连接.
if (CreateInfo != NULL) //参数有可能为NULL
{
dprintf("[Hello] Create Porcess pid=%d ImageFileName=%wZ\r\n",
ProcessId,
CreateInfo->ImageFileName);
if (RtlCompareUnicodeString(CreateInfo->ImageFileName, &ustrCalc, TRUE) == 0) //比较字符串.如果成功
{
//阻止创建
CreateInfo->CreationStatus = STATUS_UNSUCCESSFUL; //标志给失败,则计算器不能运行.
}
}
else
dprintf("[Hello] Exit Porcess pid=%d\r\n", ProcessId);
}
回调卸载函数
PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyEx, TRUE); //卸载回调
代码图片:
其中UnInstallHook则是调用卸载回调的函数。
驱动入口点调用initHook即可。
拦截图片:
编译好我们的驱动.去x64加载,打开计算机则会被拦截。
原理:
如果想知道原理,那么就要看Wrk源码。而在Wrk中,并没有这个API,这个API是64位才有的。但是在32下有一个,有一个不带EX版本的,我们可以参考一下。
通过源码可以发现.是操作数组,且这个数组里面是我们填写的回调,操作系统会依次调用回调。
跟随数组查看:
跟随查看,发现是个定长数组,里面只有八项。但是在32位,没人会使用这个API,所以8项正好。但是在64位系统下,数组变为了64。
也就是说:我们可以通过找到数组首地址,进行HOOK里面的回调。那么我们的就会执行,当然我们也可以自己注册。
三、线程监控以及拦截HOOK
线程拦截和进程拦截相似。但是在64位下,微软提供的HOOK函数没有这么强大。并没有带有EX的,所以我们只能用原来的。
线程监控API
监控: PsSetCreateThreadNotifyRoutine
取消监控: PsRemoveCreateThreadNotifyRoutine
监控原型:
NTSTATUS
PsSetCreateThreadNotifyRoutine(
IN PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine //回调函数地址.当线程创建完毕,但还没运行的时候会调用你的回调.
);
卸载函数原型:
NTSTATUS
PsRemoveCreateThreadNotifyRoutine(
IN PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine //回调地址,你刚才注册的回调会取消.
);
本质也是在数组里面注册函数地址。
这两个函数只能看,但是不能进行操作。但是很多时候看看就已经可以了。
线程回调原型:
VOID
(*PCREATE_THREAD_NOTIFY_ROUTINE) (
IN HANDLE ProcessId, //进程ID
IN HANDLE ThreadId, //线程ID
IN BOOLEAN Create
);
可以看到:线程HOOK后,信息很少,就一个进程ID、一个线程ID,还有一个创建标志。没有什么了,现在我们要进行HOOK。
HOOK思路:
1. 通过进程ID,找到EPROCESS 对象
2. 通过线程ID,找到ETHREAD 对象
3. 通过EPROCESS对象,获得进程路径
4. 通过进程路径,找到对应进程名
5. 判断进程名是否相同
相同: 通过ETHREAD + 偏移,找到线程回调函数的地址. 并修改回调地址的内容为ret。
不相同: 不相同退出。
在上面HOOK思路中,第五不很重要.ETHREAD + 偏移,那么找到是那个成员?
那么我们要熟悉ETHREAD中那个成员重要
通过分析ETHREAD,可以得出:在偏移410的位置,存放这ring3下线程回调函数的地址,那么我们可以操作这个偏移,使其里面变为ret,变相了拦截了线程的创建。
实战代码:
initHook函数
void InstallHook()
{
NTSTATUS status;
status = PsSetCreateThreadNotifyRoutine(CreateThreadNotify); //注册线程回调
if (NT_SUCCESS(status))
dprintf("[Hello] PsSetCreateThreadNotifyRoutine CreateThreadNotify=%p\r\n", CreateProcessNotifyEx);
else
dprintf("[Hello] PsSetCreateThreadNotifyRoutine status=%d\r\n", status);
}
回调函数中的代码
VOID CreateThreadNotify(
IN HANDLE ProcessId,
IN HANDLE ThreadId,
IN BOOLEAN Create)
{
PEPROCESS Process = NULL;
PETHREAD Thread = NULL;
UCHAR *pszImageName = NULL;
NTSTATUS status;
UCHAR *pWin32Address = NULL;
status = PsLookupProcessByProcessId(ProcessId, &Process); //1.通过进程ID,获取EPROCESS
if (!NT_SUCCESS(status))
return;
status = PsLookupThreadByThreadId(ThreadId, &Thread); //2.通过线程ID,获取ETHREAD
pszImageName = PsGetProcessImageFileName(Process); //3.通过EPROCESS获取进程名
if (Create)
{
dprintf("[Hello] Create Thread pid=%d tid=%d ImageName=%s\r\n", ProcessId, ThreadId, pszImageName);
if (strstr(pszImageName, "calc") != NULL) //4.判断进程名是否是计算器
{
//KdBreakPoint();
//修改回调函数代码
pWin32Address = *(UCHAR**)((UCHAR*)Thread + 0x410); //5.是的话.找到回调函数地址.并改为C3
//KeAttachProcess();
if (MmIsAddressValid(pWin32Address))
{
KdBreakPoint();
//*pWin32Address = 0xC3; //修改为C3,但是注意,你修改的是否内存保护属性需要去掉.但是在64位下,不允许使用内联汇编了.不过你可以写二进制进行更改.或者汇编生成obj,驱动来使用
//这里直接手动更改为C3.
}
//KeUnstackDetachProcess();
}
}
else
dprintf("[Hello] Exit Thread pid=%d tid=%d\r\n", ProcessId, ThreadId);
if( Process )
ObDereferenceObject (Process); //引用计数--
if( Thread )
ObDereferenceObject (Thread);
}
PS:
上面通过进程ID获取EPROCESS等等的API,都是微软未公开的。但是导出了,我们只需要声明一下即可。但是既然是未公开那么就要看微软怎么使用,<br>微软使用之后,会对其引用计数--,所以我们也要这样做。
内存中都改为C3,则线程都不能创建了。
四丶模块拦截,以及阻止思路
模块拦截以及HOOK也是和上面一样,提供回调即可。
但模块是给的ImageBae,也就是模块基址。所以我们只需要解析PE找到OEP,把OEP代码改成ret即可。
API:
监控:
PsSetLoadImageNotifyRoutine
取消监控:
PsRemoveLoadImageNotifyRoutine
函数原型:
NTSTATUS
PsSetLoadImageNotifyRoutine(
IN PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine
);
回调函数原型:
VOID
(*PLOAD_IMAGE_NOTIFY_ROUTINE) (
IN PUNICODE_STRING FullImageName,
IN HANDLE ProcessId, // where image is mapped
IN PIMAGE_INFO ImageInfo
);
对于模块,并没有写实现代码。但是我觉着,思路很重要。仿照前面两个例子,自己扩展一下思路,自己实现也可以。
在这里先给大家拜个年。学习逆向方面的技术,第一是自己的兴趣;第二是自己的自学能力;第三就是找一个可靠的师兄。所谓前人栽树,后人乘凉。
本文由看雪论坛 张新琪 原创
转载请注明来自看雪社区
热门阅读
点击阅读原文/read,
更多干货等着你~