EPROCESS结构体:在内核中表示一个进程
二叉树,存储进程在内核层申请的虚拟内存信息
(x86 EPROCESS+0x11c) (x64 EPROCESS+0x7d8)指向VadRoot(VAD树)
可以看到两种内存:Private(私有内存)、 Mapped(映射内存)
通过VirtualAlloc、VirtualAllocEx等申请(malloc、new是在用户层申请虚拟内存,所以不是)
独享物理页,可读可写可执行的 Private 会被杀软重点关注
通过 CreateFileMapping 等映射
可能与其他进程共享物理页,可以防止多个进程访问DLL时死锁
操作系统通过 VirtualAlloc 预先分配好的一大块内存
malloc和new的调用链: malloc -> _nh_malloc_dbg -> _heap_alloc_dbg -> _heap_alloc_base -> HeapAlloc new -> _nh_malloc -> _nh_malloc_dbg -> _heap_alloc_dbg -> _heap_alloc_base -> HeapAlloc
HeapAlloc的作用就是在堆里分一些出来
用户层地址0-7开头,内核层8-f
用户层(x86 0x7ffe0000
可读)和内核层(x86 0xffdf0000
可读可写)分别有一个KUSER_SHARED_DATA
结构体(用于共享数据,指向同一物理页)
操作系统在系统启动时在 KUSER_SHARED_DATA+0x300
写入 KiFastSystemCall
或 KiIntSystemCall
快速调用:sysenter/sysexit
(返回用户层),ntdll
中的 KiFastSystemCall
函数
中断门(老版本):int 0x2E
,ntdll 中的 KiIntSystemCall 函数
通过汇编判断支持的方式:
; 将CPU的信息存入寄存器
mov eax,0x1
cpuid
;
之后edx的SEP位(从右向左从0开始数第11位)为1则支持快速调用,为0则支持中断门
进入内核后,CS由3变为0(0环权限)、SS也改变权限、换栈,然后找SSDT表(系统服务描述符表,KSERVICE_TABLE_DESCRIPTOR
结构体,存储真正函数实现的地址)
两个SSDT表(结构相同):
KeServiceDescriptorTable
主要处理 kernel32,在ntoskrnl.exe是导出的
KeServiceDescriptorTableShadow
主要处理 user32、gdi32,没有被Windows导出
双机调试看结构:
kd> dd KeServiceDescriptorTable
80553fa0 80502b8c 00000000 0000011c 80503000
...
一行是一个KSYSTEM_SERVICE_TABLE
结构体
参数1指向当前内核函数地址(位于所有内核函数地址的数组中)
参数2指向当前内核函数使用次数的地址(位于所有内核函数使用次数的数组中)
参数3代表所有内核函数个数(284)
参数4代表当前内核函数参数个数(0x18/0x4=0x6)
kd> db 80503000
80503000 18 20 2c 2c 40 2c 40 44-0c 08 18 18 08 04 04 0c
...
用Ollydbg找函数在SSDT表中的调用号
底部Command(bp 函数名),下断点
直到跟进Call ntdll,看到调用号(0x7A)ntdll.ZwOpenProcess mov eax,0x7A
偏移到函数
kd> dd 80502b8c + 7A*4
80502d74 805c2296 805e49fc 805e4660 805a0722
...
kd> u 805c2296
nt!NtOpenProcess:
805c2296 68c4000000 push 0C4h
805c229b 68a8aa4d80 push offset nt!FsRtlLegalAnsiCharacterArray+0x2008 (804daaa8)
805c22a0 e86b6cf7ff call nt!wctomb+0x45 (80538f10)
805c22a5 33f6 xor esi,esi
805c22a7 8975d4 mov dword ptr [ebp-2Ch],esi
805c22aa 33c0 xor eax,eax
805c22ac 8d7dd8 lea edi,[ebp-28h]
805c22af ab stos dword ptr es:[edi]
3.驱动(x86)
驱动程序(.sys):让内核兼容不同硬件,在加载时会在注册表注册服务
每个进程有4GB内存空间:低2GB是独立的,高2GB位于内核层是公共的
DRIVER_OBJECT结构体代表驱动程序(一种内核模块)DRIVER_OBJECT+0x10 为 DriverSize(驱动大小)
DRIVER_OBJECT+0x1c 为 DriverName(驱动名称)
DRIVER_OBJECT+0x14 为 DriverSection(指向LDR_DATA_TABLE_ENTRY结构体)
LDR_DATA_TABLE_ENTRY+0x0 为 InLoadOrderLinks结构体(LIST_ENTRY双向链表结构体,可以遍历内核模块)
LDR_DATA_TABLE_ENTRY+0x18 为 DllBase(当前模块首地址)
LDR_DATA_TABLE_ENTRY+0x20 为 SizeOfImage(当前模块大小)
LDR_DATA_TABLE_ENTRY+0x24 为 FullDllName(驱动文件完整路径)
遍历模块输出信息,例:
#include <ntddk.h>typedef struct _LDR_DATA_TABLE_ENTRY {
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
UINT32 SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
UINT32 Flags;
UINT16 LoadCount;
UINT16 TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
UINT32 CheckSum;
UINT32 TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;
VOID DriverUnload(PDRIVER_OBJECT driver) { // 卸载驱动时调用
DbgPrint("Unload\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING reg_path) { // 加载驱动时调用
DbgPrint("驱动地址:%x\n注册表路径:%wZ", driver, reg_path);
PLDR_DATA_TABLE_ENTRY phead = (PLDR_DATA_TABLE_ENTRY)(driver->DriverSection); // 头节点
PLDR_DATA_TABLE_ENTRY pcur = phead; // 当前节点
do {
pcur = (PLDR_DATA_TABLE_ENTRY)(pcur->InLoadOrderLinks.Flink);
DbgPrint("DllBase:%p,SizeOfImage:%08X,BaseDllName:%wZ\n", pcur->DllBase, pcur->SizeOfImage, &(pcur->BaseDllName));
} while (phead != pcur);
driver->DriverUnload = DriverUnload; // 指定卸载函数
return STATUS_SUCCESS;
}
1.x86:用jmp进行hook
2.x64:在0环通过回调函数,挂驱动(.sys)来实现监控
系统会随机对内存和快照进行比对,导致用jmp进行hook可能触发PG(Patch Guard)导致蓝屏
微软提供了CmRegisterCallback
回调函数来监控注册表操作
本文为免杀三期学员笔记:https://www.cnblogs.com/LeiyNeKo/articles/17201218.html
课程链接如下