windows API
Application Programming Interface,简称 API 函数。
Windows有多少个API?
主要是存放在 C:\WINDOWS\system32下面所有的dll
几个重要的DLL:
- Kernel32.dll:最核心的功能模块,比如管理内存、进程和线程相关的函数等.
- User32.dll:是Windows用户界面相关应用程序接口,如创建窗口和发送消息等.
- GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数.比如要显示一个程序窗口,就调用了其中的函数来画这个窗口.
- Ntdll.dll:大多数API都会通过这个DLL进入内核(0环).
分析ReadProcessMemory
该API在Kernel32.dll中导出。
通过IDA分析可以看到,在函数中又调用了另一个导入的函数。
在kernel32.dll的导入函数中,准确的说在IAT表中,可以找到是用的哪个dll提供的函数。
可以看到是ntdll.dll,那么我们继续跟踪ntdll.dll。
在ntdll中,可以看到先传入给eax一个值,这个值实际上是个索引号。然后再call了0x7FFE0300这个值。
这个地址可以看到他是写死的,那么就意味着任何一个exe加载这个dll,都会去call这个位置,这个位置是所有进程共享的。实际上这里是一个结构体(_KUSER_SHARED_DATA),call的是其中一个成员。
kd> dt _KUSER_SHARED_DATA 0x7ffe0000
并且这个KUSER_SHARED_DATA结构体是共享的:在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA 结构区域,用于 User 层和 Kernel 层共享某些数据。
_它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:
- User 层地址为:0x7ffe0000
- Kernel 层地址为:0xffdf0000
虽然指向的是同一个物理页,但在User 层是只读的,在Kernel层是可写的。
可以再windbg通过指令查看:
kd> !vtop 0a5c03c0 7ffe0000
X86VtoP: Virt 7ffe0000, pagedir a5c03c0
X86VtoP: PAE PDPE a5c03c8 - 0000000010dca001
X86VtoP: PAE PDE 10dcaff8 - 0000000010e3e067
X86VtoP: PAE PTE 10e3ef00 - 8000000000041025
X86VtoP: PAE Mapped phys 41000
Virtual address 7ffe0000 translates to physical address 41000.
物理地址为41000
PTE的属性最后是5,即为0101,R/W位为0,则属性为可写。
同样的查看kernel层:
kd> !vtop 0a5c03c0 ffdf0000
X86VtoP: Virt ffdf0000, pagedir a5c03c0
X86VtoP: PAE PDPE a5c03d8 - 0000000011448001
X86VtoP: PAE PDE 11448ff0 - 000000000038f163
X86VtoP: PAE PTE 38ff80 - 0000000000041163
X86VtoP: PAE Mapped phys 41000
Virtual address ffdf0000 translates to physical address 41000.
指向的是同一个物理页。
PTE的属性最后是3,即为0011,R/W位为1,可读可写。
这就意味着在三环,我们无法通过hook这个地址来hook所有进程的执行流,但当然是拦不住我们的。
在我们了解了KUSER_SHARED_DATA结构体后,就可以知道call的实际上是Systemcall的地址,通过反汇编查看
kd> u 0x7c92e4f0
ntdll!KiFastSystemCall:
7c92e4f0 8bd4 mov edx,esp
7c92e4f2 0f34 sysenter
ntdll!KiFastSystemCallRet:
7c92e4f4 c3 ret
7c92e4f5 8da42400000000 lea esp,[esp]
7c92e4fc 8d642400 lea esp,[esp]
ntdll!KiIntSystemCall:
7c92e500 8d542408 lea edx,[esp+8]
7c92e504 cd2e int 2Eh
7c92e506 c3 ret
通过sysenter指令(快速调用)进入0环。操作系统会在系统启动的时候在KUSER_SHARED_DATA结构体的+300的位置,写入一个函数,这个函数就是KiFastSystemCall或者KiIntSystemCall。
当通过eax=1来执行cpuid指令时,处理器的特征信息被放在ecx和edx寄存器中,其中edx包含了一个SEP位(11位),该位指明了当前处理器知否支持sysenter/sysexit指令。
支持:ntdll.dll!KiFastSystemCall()
不支持:ntdll.dll!KiIntSystemCall()。该方式通过中断门进入0环。
kd> u KiIntSystemCall
ntdll!KiIntSystemCall:
7c92e500 8d542408 lea edx,[esp+8]
7c92e504 cd2e int 2Eh
7c92e506 c3 ret
7c92e507 90 nop
ntdll!RtlRaiseException:
7c92e508 55 push ebp
7c92e509 8bec mov ebp,esp
7c92e50b 9c pushfd
7c92e50c 81ecd0020000 sub esp,2D0h
因为我们比较了解中断门,我们先看看中断门是怎么进入0环的。
kd> dq 8003f400 + 0x2e*8
8003f570 8053ee00`0008e481 80548e00`00081780
8003f580 80538e00`0008db40 80538e00`0008db4a
8003f590 80538e00`0008db54 80538e00`0008db5e
8003f5a0 80538e00`0008db68 80538e00`0008db72
8003f5b0 80538e00`0008db7c 806d8e00`00082728
8003f5c0 80538e00`0008db90 80538e00`0008db9a
8003f5d0 80538e00`0008dba4 80538e00`0008dbae
8003f5e0 80538e00`0008dbb8 806d8e00`00083b70
该描述符对应的eip是8053e481,反汇编查看。
这里就已经进入到内核模块,函数为KiSystemService。
从r3到r0必然是需要提权的,替换的寄存器有:CS,EIP,SS,ESP(SS与ESP由TSS提供)。这是通过中断门的方式。
如果通过sysenter,即快速调用进入内核。
操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质是一样的!
MSR寄存器存储了很多值,微软只公布了一小部分。
我们可以通过RDMSR/WRMST来进行读写(操作系统使用WRMST写该寄存器):
kd> rdmsr 174 //查看CS
kd> rdmsr 175 //查看ESP
kd> rdmsr 176 //查看EIP
SS是被写死的,算出来的。cs=8 --》ss=0x10
所以一个是通过内存获取(IDT TSS),一个是通过寄存器获取。本质上没有区别,只是效率上的区别。
总结
我们在三环执行的api无非是一个接口,真正执行的功能在内核实现,我们便可以直接重写三环api,直接sysenter进内核,这样可以规避所有三环hook。
API通过中断门进0环:
- 固定中断号为0x2E
- CS/EIP由门描述符提供 ESP/SS由TSS提供
- 进入0环后执行的内核函数:NT!KiSystemService
API通过sysenter指令进0环:
1) CS/ESP/EIP由MSR寄存器提供(SS是算出来的)
2) 进入0环后执行的内核函数:NT!KiFastCallEntry
实验
自己实现直接通过sysenter 和 int 2e直接进入0环
这种方式可以绕过所有的三环钩子
sysenter
#include "StdAfx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
BOOL __stdcall My_ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
#define EXIT_ERROR(x) \
do \
{ \
cout << "error in line " << __LINE__ << endl; \
printf("errcode = %d\n", GetLastError()); \
cout << x; \
system("pause"); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
int pid = 0;
cout << "请输入要读取的进程的PID:";
cin >> pid;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
EXIT_ERROR("hProcess == NULL!");
WORD t;
DWORD dwSizeRead;
ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
&t, sizeof WORD, &dwSizeRead);
cout << hex << t << " " << dwSizeRead << endl;
getchar();
system("pause");
My_ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
&t, sizeof WORD, &dwSizeRead);
cout << hex << t << " " << dwSizeRead;
getchar();
system("pause");
return 0;
}
BOOL __stdcall My_ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead)
{
DWORD NtStatus;
__asm {
lea eax, [nSize]
push eax
push nSize // nsize值入栈
push lpBuffer // buffer入栈
push lpBaseAddress // lpBaseAddress入栈
push hProcess // hProcess入栈
sub esp, 0x04; // 模拟 ReadProcessMemory 里的 CALL NtReadVirtualMemory
mov eax, 0BAh // NtReadVirtualMemory
push 0x00401EBC //NtReadRet的地址
// kifastcall
mov edx,esp
_emit 0x0f
_emit 0x34 // 没有sysenter,要硬编码0x0f34
NtReadRet:
add esp, 0x18; // 模拟 NtReadVirtualMemory 返回到 ReadProcessMemory 时的 RETN 0x14
mov NtStatus, eax;
}
printf("nsize = %d, status = %d\n", nSize, NtStatus);
return 0;
}
int 2E
#include "StdAfx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
BOOL __stdcall My_ReadProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
#define EXIT_ERROR(x) \
do \
{ \
cout << "error in line " << __LINE__ << endl; \
printf("errcode = %d\n", GetLastError()); \
cout << x; \
system("pause"); \
exit(EXIT_FAILURE); \
} while (0)
int main()
{
int pid = 0;
cout << "请输入要读取的进程的PID:";
cin >> pid;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hProcess == NULL)
EXIT_ERROR("hProcess == NULL!");
WORD t;
DWORD dwSizeRead;
ReadProcessMemory(hProcess, (LPCVOID)0x00400000,
&t, sizeof WORD, &dwSizeRead);
cout << hex << t << " " << dwSizeRead << endl;
getchar();
system("pause");
My_ReadProcessMemory_INT(hProcess, (LPCVOID)0x00400000,
&t, sizeof WORD, &dwSizeRead);
cout << hex << t << " " << dwSizeRead;
getchar();
system("pause");
return 0;
}
BOOL WINAPI My_ReadProcessMemory_INT(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead)
{
DWORD NtStatus;
__asm
{
mov eax, 0xBA;
lea edx, hProcess // edx里面存储最后入栈的参数
int 2Eh
mov NtStatus, eax
}
*lpNumberOfBytesRead = nSize;
if (NtStatus) return NtStatus;
else return 0;
}