0%

使用SSDTHOOK实现进程保护(2)

在XP下的HOOK方式

在XP下可以使用的HOOK方式,包含 SSDTHOOK INLINEHOOK OBJECTHOOK 等,
其中 SSDTHOOK OBJECTHOOK 相对于 INLINEHOOK 更稳定,但是也更容易被移除。

SSDTHOOK的原理

SSDT全称 System Services Descriptor Table 中文叫 系统服务描述符表 它是一个地址数组,
保存着由R3切换到R0时,所用函数的索引地址。在XP中导出了一个描述SSDT的结构体

1
2
3
4
5
6
typedef struct _SSDT_TABLE {
PULONG ServiceTableBase; // 基址
PULONG ServiceCounterTableBase;
ULONG NumberOfServices; // 数量
PUCHAR ParamTableBase;
} SSDT_TABLE, *PSSDT_TABLE;

我们在使用时,需要先声明一下这个导出的结构体变量

1
NTSYSAPI SSDT_TABLE KeServiceDescriptorTable;

那么如何查找某函数对应的索引呢,比如 ZwTerminateProcess 这个函数,我们先看一下反汇编

1
2
3
4
5
6
7
8
0: kd> u ZwTerminateProcess
nt!ZwTerminateProcess:
80502140 b801010000 mov eax,101h
80502145 8d542404 lea edx,[esp+4]
80502149 9c pushfd
8050214a 6a08 push 8
8050214c e800030400 call nt!KiSystemService (80542451)
80502151 c20800 ret 8

这个函数的功能就是,把 0x101 索引放入到 eax 中,然后通过 KiSystemService 访问索引的函数,
索引 0x101 处存储的就是 NtTerminateProcess 函数,这个函数才是真实的 退出进程 功能函数。
可以看到 0x101 正是 ZwTerminateProcess 函数 起始地址+1字节 的位置,所以可以这样定义一个宏

1
2
// 根据ZW函数查找NT函数的ID
#define SERVICE_ID(Service) (*(PULONG)((ULONG)(Service) + 1))

同时我们也可以借助于一些ARK工具来验证这个索引是否正确,如下为PCHunter显示的信息

函数索引

而我们HOOK函数 NtTerminateProcess 就是用我们自己的函数地址,替换 0x101 索引处的地址,
这样系统调用 NtTerminateProcess 函数时就会调用我们的函数,当我们检测完放行时,还得调用原始
函数,执行正常功能,如果我们不放行,就可以直接返回 STATUS_ACCESS_DENIED 进行拦截操作。

1
2
3
4
NTSTATUS NtTerminateProcess(
_In_opt_ HANDLE ProcessHandle,
_In_ NTSTATUS ExitStatus
);

如下为HOOK函数,以及相关调用代码

1
2
3
4
5
6
7
void SSDTHook(IN PULONG FuncAddr, IN PVOID ProxyAddr, OUT PULONG OrigianlAddr)
{
WPOff(); // 关闭内存写保护
*OrigianlAddr = *(PULONG)FuncAddr; // 保存原函数地址
*(PULONG)FuncAddr = (ULONG)ProxyAddr; // 替换为自定义函数
WPOn(); // 打开内存写保护
}
1
2
3
4
5
6
ULONG OriginalNtTerminateProcess = 0; // 原函数地址
// 开始Hook函数
SSDTHook(
&KeServiceDescriptorTable.ServiceTableBase[SERVICE_ID(ZwTerminateProcess)],
ProxyNtTerminateProcess,
&OriginalNtTerminateProcess);

由于我们需要写系统关键处的内存,所以就需要先关闭写保护,如下为相关汇编代码

1
2
3
4
5
6
7
8
9
10
11
// 关闭R0级写保护
void WPOff()
{
__asm
{
cli
mov eax, cr0
and eax, not 10000h
mov cr0, eax
}
}
1
2
3
4
5
6
7
8
9
10
11
// 恢复R0级写保护
void WPOn()
{
__asm
{
mov eax, cr0
or eax, 10000h
mov cr0, eax
sti
}
}

最后是我们自定义函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef NTSTATUS(*PNTP)(HANDLE, NTSTATUS); // NtTerminateProcess

NTSTATUS ProxyNtTerminateProcess(HANDLE ProcessHandle, NTSTATUS ExitStatus)
{
NTSTATUS Status = STATUS_SUCCESS;
do
{
// 检查参数
if (ProcessHandle == NULL) break;
if (ProcessHandle == (HANDLE)-1) break;
// 本次操作对应的进程
PsGetCurrentProcessId();
// 使用ZwQueryInformationProcess查询进程名
if (!NT_SUCCESS(Status))
return STATUS_ACCESS_DENIED; // 拦截操作
} while (0);
// 调用原始函数
if (OriginalNtTerminateProcess != 0)
return ((PNTP)OriginalNtTerminateProcess)(ProcessHandle, ExitStatus);
return STATUS_ACCESS_DENIED;
}

OBJECTHOOK的原理

所谓的OBJECTHOOK就是对进程内核对象结构体中,对应的几个进程相关操作函数地址进行替换,
一般情况下我们都是替换OpenProcedure函数的地址,这个函数在打开进程句柄时会被调用。

如何找到这些信息?在不同的操作系统中,内核对象结构体是不同的,所以需要我们搭建VMWARE调试
环境,通过WINDBG来实际查看目标系统的信息,这里以 Windows Server 2003 x64 系统为例进行说明。

操作系统中的所有进程内核对象,是一个链表结构,链表头部是一个全局变量,名称为 PsProcessType

1
extern POBJECT_TYPE *PsProcessType;

这个全局变量已经被导出,我们可以使用 dq 指令来查看64位内存地址

1
2
3
4
5
1: kd> dq PsProcessType
fffff800`011d1fb8 fffffadf`e7a6da00 fffffadf`e7a6d6c0
fffff800`011d1fc8 fffffa80`00002ba0 00000000`00000002
fffff800`011d1fd8 00000000`00000000 00000000`00000000
fffff800`011d1fe8 00000000`00000000 00000000`00000000

因为它是一个 OBJECT_TYPE 结构体类型指针,我们查看对应结构体信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1: kd> dt _OBJECT_TYPE fffffadf`e7a6da00
ACPI!_OBJECT_TYPE
+0x000 Mutex : _ERESOURCE
+0x068 TypeList : _LIST_ENTRY [ 0xfffffadf`e7a6da68 - 0xfffffadf`e7a6da68 ]
+0x078 Name : _UNICODE_STRING "Process"
+0x088 DefaultObject : (null)
+0x090 Index : 5
+0x094 TotalNumberOfObjects : 0x16
+0x098 TotalNumberOfHandles : 0x70
+0x09c HighWaterNumberOfObjects : 0x18
+0x0a0 HighWaterNumberOfHandles : 0x70
+0x0a8 TypeInfo : _OBJECT_TYPE_INITIALIZER
+0x118 Key : 0x636f7250
+0x120 ObjectLocks : [4] _ERESOURCE

其中 TypeList 成员,是一个链表头,可以通过遍历这个链表,来找到所有的进程对象。
然后查看 TypeInfo 成员,对应结构体为 OBJECT_TYPE_INITIALIZER 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1: kd> dx -r1 (*((ACPI!_OBJECT_TYPE_INITIALIZER *)0xfffffadfe7a6daa8))
(*((ACPI!_OBJECT_TYPE_INITIALIZER *)0xfffffadfe7a6daa8)) [Type: _OBJECT_TYPE_INITIALIZER]
[+0x000] Length : 0x70 [Type: unsigned short]
[+0x002] UseDefaultObject : 0x0 [Type: unsigned char]
[+0x003] CaseInsensitive : 0x0 [Type: unsigned char]
[+0x004] InvalidAttributes : 0xb0 [Type: unsigned long]
[+0x008] GenericMapping [Type: _GENERIC_MAPPING]
[+0x018] ValidAccessMask : 0x1f0fff [Type: unsigned long]
[+0x01c] SecurityRequired : 0x1 [Type: unsigned char]
[+0x01d] MaintainHandleCount : 0x0 [Type: unsigned char]
[+0x01e] MaintainTypeList : 0x0 [Type: unsigned char]
[+0x020] PoolType : NonPagedPool (0) [Type: _POOL_TYPE]
[+0x024] DefaultPagedPoolCharge : 0x1000 [Type: unsigned long]
[+0x028] DefaultNonPagedPoolCharge : 0x438 [Type: unsigned long]
[+0x030] DumpProcedure : 0x0 [Type: void (__cdecl*)(void *,_OBJECT_DUMP_CONTROL *)]
[+0x038] OpenProcedure : 0x0 [Type: long (__cdecl*)(_OB_OPEN_REASON,_EPROCESS *,void *,unsigned long,unsigned long)]
[+0x040] CloseProcedure : 0x0 [Type: void (__cdecl*)(_EPROCESS *,void *,unsigned long,unsigned __int64,unsigned __int64)]
[+0x048] DeleteProcedure : 0xfffff800012875b0 [Type: void (__cdecl*)(void *)]
[+0x050] ParseProcedure : 0x0 [Type: long (__cdecl*)(void *,void *,_ACCESS_STATE *,char,unsigned long,_UNICODE_STRING *,_UNICODE_STRING *,void *,_SECURITY_QUALITY_OF_SERVICE *,void * *)]
[+0x058] SecurityProcedure : 0xfffff800012884f0 [Type: long (__cdecl*)(void *,_SECURITY_OPERATION_CODE,unsigned long *,void *,unsigned long *,void * *,_POOL_TYPE,_GENERIC_MAPPING *,char)]
[+0x060] QueryNameProcedure : 0x0 [Type: long (__cdecl*)(void *,unsigned char,_OBJECT_NAME_INFORMATION *,unsigned long,unsigned long *,char)]
[+0x068] OkayToCloseProcedure : 0x0 [Type: unsigned char (__cdecl*)(_EPROCESS *,void *,void *,char)]

到这里可以看到 OpenProcedure 函数指针目前内容为 0x0,对应的函数类型为

1
long (__cdecl*)(_OB_OPEN_REASON,_EPROCESS *,void *,unsigned long,unsigned long)

经过计算,可以得出 OpenProcedure 指针的地址,相对于 OBJECT_TYPE 类型偏移为 0xE0 ,也就是说
((ULONGLONG)*PsProcessType + 0xE0) 表示当前 OpenProcedure 指针的位置,通过替换该指针的内容,
就可以实现OBJECTHOOK操作,如下为处理函数的相关定义和内容

1
2
3
4
5
6
7
8
9
typedef enum _OB_OPEN_REASON {
ObCreateHandle = 0x0,
ObOpenHandle = 0x1,
ObDuplicateHandle = 0x2,
ObInheritHandle = 0x3,
ObMaxOpenReason = 0x4
} OB_OPEN_REASON, *POB_OPEN_REASON;
// 函数类型定义
typedef NTSTATUS(*OPENPROCEDURE)(OB_OPEN_REASON, PEPROCESS, PVOID, ULONG, ULONG);
1
2
3
4
5
6
7
ULONGLONG OldOpenProcedure = 0; // 原函数地址
// 开始HOOK函数
void SetObjectHook()
{
OldOpenProcedure = *(ULONGLONG*)((ULONGLONG)*PsProcessType + 0xE0);
*(ULONGLONG*)((ULONGLONG)*PsProcessType + 0xE0) = (ULONGLONG)MyOpenProcedure;
}
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
NTSTATUS MyOpenProcedure(OB_OPEN_REASON Reason, PEPROCESS Process, PVOID Object, ULONG arg4, ULONG arg5)
{
BOOLEAN bDeny = FALSE;
UNREFERENCED_PARAMETER(arg4);
UNREFERENCED_PARAMETER(arg5);
do
{
// 判断进程是不是系统进程
if (PsGetProcessId(Process) == (HANDLE)4) break;
if (PsGetProcessId(Object) == (HANDLE)4) break;
// 创建时EPROCESS全路径是空的
if (Reason == ObCreateHandle) break;
// 判断目标进程是不是当前进程自己
if ((PVOID)Object == (PVOID)Process) break;
// 因为使用ZwQueryInformationProcess会触发重入
// 所以这里直接查找在EPROCESS结构体中对应的进程路径
(PUNICODE_STRING*)((PUCHAR)Process + 0x318);
// 拦截打开进程的操作
if (bDeny) return STATUS_ACCESS_DENIED;
} while (0);
// 因为原地址为0x0所以不再调用原地址
// if (OldOpenProcedure != 0)
// return ((OPENPROCEDURE)OldOpenProcedure)(Reason, Process, Object, arg4, arg5);
return STATUS_SUCCESS;
}

注意,不同的系统中 OpenProcedure指针位置 以及 函数类型 有可能是不同的,不能照搬使用。