WindowsX86内核16.API调用流程和HookAPI
WindowsAPI调用流程[*]不管调那个函数都可以,只要能进内核就行了,但是一般是不会调Ntdll里的,因为Ntdll里的参数要求和3环是不一样的
[*]user32.dll里都是UI相关的API,Kernel.dll里是核心的功能,这两个是常用的,可以选择一个来分析
分析 ReadProcessMemoryIDA打开Kernel.dll,找到ReadProcessMemory
push ebp
mov ebp, esp
lea eax,
push eax ; NumberOfBytesRead
push ; NumberOfBytesToRead
push ; Buffer
push ; BaseAddress
push ; ProcessHandle
call ds:NtReadVirtualMemory; 发现它转换完参数就调了Nt这个导出函数然后点击导出表,搜索这个函数,发现它是由ntdll.dll导入的,也就是这里啥也没干,全部交给ntdll了
IDA打开ntdll.dll,搜索NtReadVirtualMemory函数,在导出表搜索
其实老版本用的都是这个,新版本都改成zw开头的了也就是ZwReadVirtualMemory,但是在IDA导出表搜索的时候发现它其实两个函数指向的地址是同一个地址;为了兼容老版本.所以就分析NtReadVirtualMemory就行了
ZwReadVirtualMemory proc near
mov eax, 0BAh ;186 API编号
mov edx, 7FFE0300h ;全局变量
call edx
retn 14h
ZwReadVirtualMemory endp
这个全局变量要么通过KiIntSystemCall,要么通过KiFastSystemCall 或者直接在WinDbg里跟函数也行, 也就是rdmsr 176得到地址,然后跟这个函数.在WinDbg里发现它最终调到内核里的KiFastCallEntry了,所以就分析这个就行了
分析KiFastCallEntry_KiFastCallEntry proc near
var_B= byte ptr -0Bh
mov ecx, 23h ; '#'
push 30h ; '0' 把fs改成内核的,这样就可以访问kpcrb了
pop fs ; fs本来指向的是3环的teb,改完就能访问kpcrb了
mov ds, ecx ;
mov es, ecx ; ds和es改成23
mov ecx, large fs:40; 40就是kpcr里的TSS : +0x040 TSS:Ptr32 _KTSS
mov esp, ; +4就是TSS里的Esp0: +0x004 Esp0:Uint4B;也就是 ESP=ESP0切换栈
push 23h ; '#'
push edx ;保存环境
pushf ;下面有个加法,怕影响标志寄存器,所以这里就pushf了
loc_40770A:
push 2
;这里+8就说明它在3环就没有+8,栈顶是EBP+返回值,真正的参数就在+8的位置
add edx, 8; edx是3环的栈,
popf
or , 2
push 1Bh
push dword ptr ds:0FFDF0304h
push 0
push ebp
push ebx
push esi
push edi ;保存环境
;因为汇编里不能用 lea eax,fs,所以它就把首地址放到1C的位置,它就可以mov获取了
mov ebx, large fs:1Ch; 这里是拿结构体首地址 +0x01c SelfPcr : Ptr32 _KPCR
push 3Bh ;
;+0x120 PrcbData: _KPRCB;然后在+4,就是KPRCB里的
;+0x004 CurrentThread : Ptr32_KTHREAD了
mov esi, ; 获取当前线程CurrentThread
push dword ptr
mov dword ptr , 0FFFFFFFFh; 然后这里取内容得到了异常链表,再把它赋值为-1
;+0x018 InitialStack : Ptr32 Void
mov ebp, ; 当前线程结构体+18的位置,也就是拿栈
push 1
sub esp, 48h
sub ebp, 29Ch
;+0x140 PreviousMode : Char
mov byte ptr , 1; 这是获取之前的模式,记一下是从3环调的还是从0环调的
cmp ebp, esp
jnz loc_4076C8
and dword ptr , 0
test byte ptr , 0FFh
mov , ebp
jnz Dr_FastCallDrSave
loc_40776A:
mov ebx,
mov edi,
mov , edx
mov dword ptr , 0BADB0D00h
mov , ebx
mov , edi
sti;这里的代码都不是很重要
loc_407781:
mov edi, eax ;eax给edi
shr edi, 8; 右移8位
and edi, 30h;再and30
;1111 1111 1111 1111右移八位=>1111 1111
;再&30 也就是 1100=> 1111 11 00 0000 0000 => 11 00 0000 0000
;&30就是取出11这两位,正常情况下 +0000 就是SSDT;要不就是10了+10就是ShadowSSDT
mov ecx, edi
;KTHREAD 里的+0x0e0 ServiceTable: Ptr32 Void;这就是SSDT表,
;也就是这个表它是从ethread拿的,说明它在创建线程的时候就要把表地址放到这里了
;但是这个地址还要加上edi,从这里可以说明这个表有两个.两个表就通过上面的and结果来区分
;一个叫SSDT这个表放NTdll的所有函数,另外一个叫ShadowSSDT(这里放的都是UI的API)
add edi, ;做了一个加法 esi此时是ethread+e0 就是ServiceTable
;如果调的是UI的API它就拿服务表的首地址再+16,否则就+0,+8的位置刚好是数量
mov ebx, eax
and eax, 0FFFh ;与掉低12位,说明API编号只有12位有效,高位都丢弃了
cmp eax, ;然后和edi+8比较,说明edi+8放的是函数的数量
jnb _KiBBTUnexpectedRange
cmp ecx, 10h
jnz short loc_4077C0
mov ecx, ds:0FFDFF018h
xor ebx, ebx;这里是检查
loc_4077AE:
or ebx,
jz short loc_4077C0
push edx
push eax
call ds:_KeGdiFlushUserBatch
pop eax
pop edx;这里是检查
loc_4077C0:
inc dword ptr ds:0FFDFF638h
mov esi, edx
mov ebx, ;从第12的位置取出参数
xor ecx, ecx ;ecx清0
mov cl, ;然后直接拿出来用了
mov edi, ;edi是函数表
mov ebx, ;查出函数地址
sub esp, ecx ;抬栈
shr ecx, 2 ;除4
mov edi, esp
cmp esi, ds:_MmUserProbeAddress ;检查栈顶是不是有效的
jnb loc_407990
loc_4077E8:
rep movsd ;开始拷贝参数
call ebx ;然后就直接调用了
loc_4077EC:
mov esp, ebp
loc_4077EE:
;调完就返回了
mov ecx, ds:0FFDFF124h
mov edx,
mov , edx
_KiFastCallEntry endp ;这个函数其实没有完,
它会走到_KiServiceExit里,然后在那里恢复环境,在返回
[*]UI的内核实现代码不在ntoskrnl.exe里, 它是重新封装了一个模块叫Win32k.sys这里放UI函数的实现代码
[*]win32k不是所有进程都有的.因为Windows上也有没有界面的软件,所以它做了两个表SSDT和ShadowSSDT
[*]
[*]也就是说当一个进程是控制台的时候它就只用SSDT表;
[*]当它是一个Win32应用程序的时候这两个表都存在
[*]获取win32k模块 可以遍历所有进程,找一个带窗口的进程,最直接的就是找桌面进程或者登录界面;explorer 切换到这个进程 .process /p /i 822e7020
然后再lm 就可以看到有一个win32k了:bf800000 bf9c2800 win32k (deferred)
切换到这个进程后查看它的eprocess里面的ethread表,它就会有两个
[*]
[*]查看SSDT表
[*]在WinDbg里查看KiFastCallEntry函数,然后设置一个断点,断下来就查看edi这个地址的值 可以断在判断成功的地址比如: 804df791 8bd8 mov ebx,eax;这个地址就可以 然后就查看edi就行了,edi指向的地址是一个结构体 此时eip = 8055b1e0 kd> dd 8055b1e0 8055b1e0 804e36a8 00000000 0000011c 80511088 上面分析已经知道了+8的地址是函数数量,也就是0000011c 第一个地址指向的是一个函数指针表;可以使用dds 804e36a8,它就会把每个成员符号解析一下. kd> dds 804e36a8 804e36a8 80590df5 nt!NtAcceptConnectPort 804e36ac 8057a0f1 nt!NtAccessCheck .......省略 可以这里放的确实是所有API的实现 第四个成员也是一个表,放的是参数的大小,可以通过 db 80511088查看
[*]如果eip+16的位置有表,就说明它是个Win32的程序,如果没有表,就是控制台程序. 而它+16的地址就是8055b1f0,正好有数据 8055b1f0 bf999b80 00000000 0000029b bf99a890 和上面查看的流程一样 kd> dds bf999b80 bf999b80 bf935f7e win32k!NtGdiAbortDoc bf999b84 bf947b29 win32k!NtGdiAbortPath ;这里确实是Win32k的API
[*]
[*]不在函数里下断点的查看流程
[*]直接遍历所有进程,然后随便找一个进程,打开它的ETHREAD 也可以先看eprocess;直接dt _eprocess 864ac578;然后就可以在结构体里找到线程了, 或者直接kd>!process 864ac578 7 ;这样就直接可以看到它的线程了 然后随便找一个线程 kd> dt _kthread 8223b648 就能找到它的服务表了,在e0的位置+0x0e0 ServiceTable: 0x8055b220 Void 然后dd查看它,kd>dd 0x8055b220 kd> dd 0x8055b220A ReadVirtual: 8055b220 not properly sign extended 8055b220 804e36a8 00000000 0000011c 80511088 8055b230 00000000 00000000 00000000 00000000 可以看到它是一个控制台程序,没有第二个表
[*]HookAPI
[*]
[*]HookApi就有很多方案了
[*]
[*]hook msr
[*]inline hook KiFastCallEntry;在这个函数里找一个位置改jmp,可以在call ebx这里改jmp,因为这里环境准备好了,Erie参数也在栈顶,而且判断 ebx就知道他要调哪个函数了;有时候hook可能并不只为了拦截,如果是行为监控的话,就可以call ebx之后改jmp,只为了获取它调了什么,打个日志
[*]hook修改SSDT表,或者ShadowSSDT表 改SSDT或者ShadowSSDT表是最稳定的,改表兼容性更好,只是麻烦一点.
[*]这三种HOOK也有缺点,比如内核里调NT是拦截不到的,因为改的是表,而它不走表,除非它调zw
[*]
[*]做Hook首先要搞清楚的是如何拿到这两个表SSDT和ShadowSSDT
[*]这两个表是全局变量,可以看一下WRK,看它是怎么做的 表在ethread里,所以应该看kthread; 看它在kthread什么时候填的 kthread里有个服务表,就是ServiceTable,可以参考引用,看谁填了这个表 这样就找到了这两个表 DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; DECLSPEC_CACHEALIGN KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow; 因为它是两个全局变量,所以它正好就是隔16 如果是控制台,它就把KeServiceDescriptorTable的地址给ethread 如果是Win32的就是给完一个,再+16给第二个
[*]它的结构体就是 typedef struct _KSERVICE_TABLE_DESCRIPTOR { PULONG_PTR Base; PULONG Count; ULONG Limit; PUCHAR Number; } KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
[*]要拿到这两个表的话很简单,在老版本里这个表KeServiceDescriptorTable它直接导出了 所以要拿到这个表的地址直接extern就行了 extern PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable; 而KeServiceDescriptorTableShadow表没有导出,但是它在内存中是连续的,所以直接+16就是它了 有了这两个表的地址就可以hook了
[*]但是从Win7开始它就不导出了这个表了,Win7拿的话就得从kthread里拿了 从kthread里拿得话要先搞定偏移,第二个问题就是不一定能拿到Shadow表.因为ethread里不一定有 所以从etherad里拿也是不完整.但是可以强制切进程到桌面,然后再去拿这个表就可以了,所以难点就是偏移如何解决,要兼容所有版本的话写switch也可以,但是太麻烦了,其实可以从KiFastCallEntry里拿;因为它里面肯定会用这个偏移的也就是 add edi, 这行代码,那么就可以扫特征码了 这个函数地址是很好拿的,可以通过msr拿,就是rdmsr 176,拿到地址后就扫特征码,拿出偏移. 拿这个表的时候要把进程切到一个Win32的程序上,这样的话这两个表都能拿到了 注意要拿ethread就得先拿eprocess,遍历进程的话又有偏移问题了,不过这个遍历进程线程得话它有未公开函数来用;可以用PsLookupthreadByThread();给个线程ID就返回ethread,要拿线程ID还是不难的. 而Ps这些函数都是导出的,所以拿地址就很方便
[*]拿线程的话拿谁的线程ID也很关键,都是拿Win32程序的,没有的话也可以自己写一个.
[*]获取表地址代码示例typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG_PTR Base;
PULONG Count;
ULONG Limit;
PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
//SSDT表
PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
//ShadowSSDT表
PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;
//声明函数
NTSTATUS __stdcall PsLookupThreadByThreadId(
__in HANDLE ThreadId,
__deref_out PETHREAD* Thread
);
/*驱动卸载函数 clean_up*/
VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
DbgPrint(" Unload! DriverObject:%p\n", DriverObject);
}
/*1.驱动入口函数*/
NTSTATUS DriverEntry(
__in struct _DRIVER_OBJECT* DriverObject,
__in PUNICODE_STRINGRegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint(" DriverEntry DriverObject:%p\n", DriverObject);
DriverObject->DriverUnload = Unload;
PETHREAD Thread = NULL;
//通过线程ID获取EThread;284是桌面的一个线程ID
NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
DbgPrint(" Thread:%p\n", Thread);
if (!NT_SUCCESS(Status)) {
return STATUS_SUCCESS;
}
//成功了以后就可以开始拿表了 就是ethread + 偏移
KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
//Shadow表的话就拿指针+1就可以了
KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;
DbgPrint(" KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
//Shadow表直接+1就行了,因为它是结构体指针
DbgPrint(" KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);
//ethread成功获取了的话对象的引用计数要--
if (Thread) {
ObfDereferenceObject(Thread);
}
return STATUS_SUCCESS;
}
[*]
[*]接下来就可以HookAPI了
[*]可以选择Hook:OpenProcess,模拟读写进程内存做保护的情况,因为HookAPi的话太多了,读啊写啊的,但是不管
[*]读还是写都要先打开获取句柄,但是不知道OpenProcess的API编号是多少,可以通过Ntdll导出函数来查看
[*]Ntdll里有个ZwOpenProcess,它里面就写了API编号,是7Ah.得到API编号后就可以对它进行Hook了
[*]
[*]Hook的话要知道它的参数,可以通过WRK查一下就知道了
[*]这样就可以自己写一个伪造的了,然后把SSDT对应的表项改为自己的就可以了
[*]
[*]代码示例#include <ntddk.h>
typedef struct _KSERVICE_TABLE_DESCRIPTOR {
PULONG_PTR Base;
PULONG Count;
ULONG Limit;
PUCHAR Number;
} KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
//SSDT表
PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
//ShadowSSDT表
PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorShadowTable;
//声明函数
NTSTATUS __stdcall PsLookupThreadByThreadId(
__in HANDLE ThreadId,
__deref_out PETHREAD* Thread
);
//要保存一下旧的OpenProcess函数
NTSTATUS (*g_OldNtOpenProcess)(
__out PHANDLE ProcessHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__in_opt PCLIENT_ID ClientId);
LONG g_RefCount = 0;//引用计数
//自己伪造的OpenProcess函数
NTSTATUS FakeNtOpenProcess(
__out PHANDLE ProcessHandle, //句柄
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,//路径在这里
__in_opt PCLIENT_ID ClientId //进程ID在这里写了
)
{
InterlockedIncrement(&g_RefCount);//引用计数++,为了避免多线程同步问题,用延迟锁来++
//拦截的话可以不返回失败,返回成功,让对方找不到bug
//判定一下参数
if (ClientId != NULL) {
//当前ID不等于指定的线程ID就说明不是自己操作
if (PsGetCurrentProcessId() != (HANDLE)1984) {
//并且这个参数PID也等于指定的线程ID
if (ClientId->UniqueProcess == (HANDLE)1984) {
//这就说明是别的进程在操作了
returnSTATUS_INVALID_BUFFER_SIZE; //给它随便返回一个错误码
}
}
//这样就对指定进程做了个保护,就是只要不是自己操作,就返回错误码,是自己操作就正常调旧的
DbgPrint("FakeNtOpenProcess UniqueProcess %d\n", ClientId->UniqueProcess);
}
else {
//打印一下日志就行了
DbgPrint("FakeNtOpenProcess %wZ\n", ObjectAttributes->ObjectName);
}
//做监控的话就记录一下,然后给它调旧的就行了
NTSTATUS Status= g_OldNtOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId);;
InterlockedDecrement(&g_RefCount);//返回之前--
return Status;
};
//开启写保护
void EnableWP() {
//获取CR0寄存器.
ULONG_PTR cr0 = __readcr0();
//把第16位置为1
cr0 |= 0x10000;
//在写回去
__writecr0(cr0);
}
//关闭写保护
void DisableWP() {
//获取CR0寄存器.
ULONG_PTR cr0 = __readcr0();
//把第16位清0
cr0 &= ~0x10000;
//在写回去
__writecr0(cr0);
}
/*驱动卸载函数 clean_up*/
VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
{
//卸载的时候把他还原回去
DisableWP();
//再把它改掉
KeServiceDescriptorTable->Base = (ULONG_PTR)g_OldNtOpenProcess;
EnableWP();
DbgPrint(" Unload! DriverObject:%p\n", DriverObject);
while (g_RefCount != 0); //引用计数不等于0就不卸载
}
/*1.驱动入口函数*/
NTSTATUS DriverEntry(
__in struct _DRIVER_OBJECT* DriverObject,
__in PUNICODE_STRINGRegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
DbgPrint(" DriverEntry DriverObject:%p\n", DriverObject);
DriverObject->DriverUnload = Unload;
//PETHREAD Thread = NULL;
////通过线程ID获取EThread
//NTSTATUS Status = PsLookupThreadByThreadId((HANDLE)284,&Thread);
//DbgPrint(" Thread:%p\n", Thread);
//if (!NT_SUCCESS(Status)) {
// return STATUS_SUCCESS;
//}
//因为没用到Shadow表,所以可以拿自己的
PETHREAD Thread = PsGetCurrentThread();
//成功了以后就可以开始拿表了 就是ethread + 偏移
KeServiceDescriptorTable = *(PKSERVICE_TABLE_DESCRIPTOR*)((char*)Thread + 0xe0);
//Shadow表的话就拿指针+1就可以了
KeServiceDescriptorShadowTable = KeServiceDescriptorTable + 1;
DbgPrint(" KeServiceDescriptorTable:%p\n", KeServiceDescriptorTable);
//Shadow表直接+1就行了,因为它是结构体指针
DbgPrint(" KeServiceDescriptorShadowTable:%p\n", KeServiceDescriptorShadowTable);
//修改SSDT表第7A项,就是OpenProcess先保存一下,要强转成函数指针
g_OldNtOpenProcess =(NTSTATUS(*)(__out PHANDLE ,__in ACCESS_MASK ,
__in POBJECT_ATTRIBUTES ,__in_opt PCLIENT_ID))
KeServiceDescriptorTable->Base;
//因为SSDT表的内存地址是不可写的,所以要关闭写保护.然后在修改
DisableWP();
//再把它改掉
KeServiceDescriptorTable->Base = (ULONG_PTR)FakeNtOpenProcess;
EnableWP();
//ethread成功获取了的话对象的引用计数要--
//if (Thread) {
// ObfDereferenceObject(Thread);
//}
return STATUS_SUCCESS;
}
//这段代码在卸载的时候还是有问题的,考虑不够.这些都是在做保护,如果病毒作者他HOOK了API的话,ARK工具就需要检测了 可以先遍历SSDT表,要知道那个API被HOOK了的话,就要判断地址了 只要判断API地址是不是在Nt范围内,如果在NtKernel范围内就没有被HOOK,因为要Hook的话肯定要把自己的函数地址写在这里,所以它的地址肯定不是在主模块. 但是它完全可以在NtKernel里找一个空的地址,然后jmp过去,这样的话ARK就检测不出来了 更深层次的检测应该是对比文件,就是重载内核.重载内核可以得到一个干净的SSDT表,然后和它做对比. 说白了检测APIHook就是扫描这个SSDT表. 检测出来就要恢复了.● ARK工具遍历SSDT表显示函数名的方法 不同版本表是不一样的,所以显示出函数名是个难点 1.最老的办法就是写死,写一个数组,什么版本用什么数组. 2.遍历Ntdll的导出表.Ntdll里肯定有个函数进内核,而且它也写了函数编号,所以就可以遍历它的导出表,找到每 个函数对应的编号,然后编号和SSDT表的下标对比,这样就知道函数名称了,但是这种方法有缺点,就是不全,比如 SSDT表里的有些函数它是非公开的,那么在Ntdll里就没有导出.这样的话拿函数名就不全了 3.解析PDB文件,根据系统版本自动从Windows服务器把PDB下载下来,只下载NtKernel和Win32k的PDB 下载下来以后就可以解析PDB里的符号信息,把RVA传递进去就能查出它的函数名了 微软提供了API可以自动下载,下载完还需要解析,微软也做好库了,在VS里有个完整的工具能解析所有符号. 路径就是Microsoft Visual Studio\2019\Community\DIA SDK\Samples 它有个示例程序DIA2Dump,直接打开就可以查看了 要运行的话需要把dll注册一下,要不然就自己加载dll● 绕过ARK工具检测是否被Hook的方法 1备份原SSDT表 2目标进程的ETHREAD.ServiceTable = 备份的SSDT表 也就是改表的地址,不改表项了. ● ZwQuerySystemInformation可以获取所有的系统信息....不管是进程,线程,模块啥都能获取到
页:
[1]