目录
<1>.将编译好的文件拖入DBG / OD 分析(定位MAIN函数找到API调用位置)
<1>.ReadProcessMemory分析(R3功能实现分析)
<2>._KUSER_SHARED_DATA.SystemCall
WindowsAPI
-
API(Application Programming Interface),我们调用时只需提供正确的参数以及接收返回值就可以判断API执行是否成功或者通过GetLastError获得错误原因.
-
大部分API在R3都是处理各种校验,真正执行功能都是在R0(并不是所有的API都是在R0处理).
-
系统中几个核心DLL(Kernel32.dll,User32.dll,GDI32.dll,Ntdll.dll(大多数API通过此DLL进入内核)).
-
通过API ReadProcessMemory / OpenProcess 分析函数从R3进入R0过程,进入R0如何处理原有寄存器数据,传递参数,找到对应内核函数并调用,以及从R0返回R3过程.
-
涉及知识点(_KUSER_SHARED_DATA,_KTRAP_FRAME,_KPCR,_KPRCB,_KTHREAD,KiFastSystemCall→KiFastCallEntry,KiIntSystemCall→KiSystemService,SSDT)下文详解.
-
代码示例(重写R3API,SSDTHOOK,内核重载).
R3API调用分析
代码示例:
#include <stdio.h>
#include <Windows.h>
int main()
{
//随便选择一个进程
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 2252);
//0x400000大部分情况下为ImageBase
DWORD dwData = 0;
ReadProcessMemory(hProcess, (PVOID)0x400000, &dwData, 4, NULL);
return 0;
}
<1>.将编译好的文件拖入DBG / OD 分析(定位MAIN函数找到API调用位置)
<2>.OpenProcess执行流程分析
OpenProcess执行流程:进程模块内CALLAPI(OpenProcess) -> kernel32.dll(OpenProcess) -> kernelBase.dll(OpenProcess) -> ntdll.dll(ZwOpenProcess) -> ntdll.dll中执行会进入R0后文详解.
<3>.ReadProcessMemory执行流程分析
ReadProcessMemory执行流程:进程模块内CALLAPI(ReadProcessMemory) -> kernel32.dll(ReadProcessMemory) -> kernelBase.dll(ReadProcessMemory) -> ntdll.dll(ZwReadVirtualMemory) -> ntdll.dll中执行会进入R0后文详解.
R3API功能实现分析
<1>.ReadProcessMemory分析(R3功能实现分析)
1).通过IDA导入KernelBase.dll,查询ReadProcessMemory函数,如下图:
分析得出ReadProcessMemory函数并未做任何处理而是调用ntdll.dll中NtReadVirtualMemory
2).通过IDA导入Ntdll.dll,查询NtReadVirtualMemory函数,如下图:
后文详解此处...
<2>.OpenProcess分析(R3功能实现分析)
1).通过IDA导入KernelBase.dll,查询OpenProcess函数,如下图:
分析得出OpenProcess函数并未做任何功能实现,而是在原有参数基础上填充内核需要结构体信息后调用NtOpenProcess
2).通过IDA导入Ntdll.dll,查询NtOpenProcess函数,如下图:
这两个函数最终都执行到ntdll.dll中并且除了eax值不相同其余都一样.
edx = 7FFE0300h
call [edx]
这里只需要分析edx指向地址7FFE0300h中的值即可.
这里我们需要了解一个结构体_KUSER_SHARED_DATA
_KUSER_SHARED_DATA
_KUSER_SHARED_DATA结构如下:
kd> dt _KUSER_SHARED_DATA
nt!_KUSER_SHARED_DATA
+0x000 TickCountLowDeprecated : Uint4B
+0x004 TickCountMultiplier : Uint4B
+0x008 InterruptTime : _KSYSTEM_TIME
+0x014 SystemTime : _KSYSTEM_TIME
+0x020 TimeZoneBias : _KSYSTEM_TIME
+0x02c ImageNumberLow : Uint2B
+0x02e ImageNumberHigh : Uint2B
+0x030 NtSystemRoot : [260] Wchar //系统目录
+0x238 MaxStackTraceDepth : Uint4B
+0x23c CryptoExponent : Uint4B
+0x240 TimeZoneId : Uint4B
+0x244 LargePageMinimum : Uint4B
+0x248 Reserved2 : [7] Uint4B
+0x264 NtProductType : _NT_PRODUCT_TYPE
+0x268 ProductTypeIsValid : UChar
+0x26c NtMajorVersion : Uint4B
+0x270 NtMinorVersion : Uint4B
+0x274 ProcessorFeatures : [64] UChar
+0x2b4 Reserved1 : Uint4B
+0x2b8 Reserved3 : Uint4B
+0x2bc TimeSlip : Uint4B
+0x2c0 AlternativeArchitecture : _ALTERNATIVE_ARCHITECTURE_TYPE
+0x2c4 AltArchitecturePad : [1] Uint4B
+0x2c8 SystemExpirationDate : _LARGE_INTEGER
+0x2d0 SuiteMask : Uint4B
+0x2d4 KdDebuggerEnabled : UChar
+0x2d5 NXSupportPolicy : UChar
+0x2d8 ActiveConsoleId : Uint4B
+0x2dc DismountCount : Uint4B
+0x2e0 ComPlusPackage : Uint4B
+0x2e4 LastSystemRITEventTickCount : Uint4B
+0x2e8 NumberOfPhysicalPages : Uint4B
+0x2ec SafeBootMode : UChar
+0x2ed TscQpcData : UChar
+0x2ed TscQpcEnabled : Pos 0, 1 Bit
+0x2ed TscQpcSpareFlag : Pos 1, 1 Bit
+0x2ed TscQpcShift : Pos 2, 6 Bits
+0x2ee TscQpcPad : [2] UChar
+0x2f0 SharedDataFlags : Uint4B
+0x2f0 DbgErrorPortPresent : Pos 0, 1 Bit
+0x2f0 DbgElevationEnabled : Pos 1, 1 Bit
+0x2f0 DbgVirtEnabled : Pos 2, 1 Bit
+0x2f0 DbgInstallerDetectEnabled : Pos 3, 1 Bit
+0x2f0 DbgSystemDllRelocated : Pos 4, 1 Bit
+0x2f0 DbgDynProcessorEnabled : Pos 5, 1 Bit
+0x2f0 DbgSEHValidationEnabled : Pos 6, 1 Bit
+0x2f0 SpareBits : Pos 7, 25 Bits
+0x2f4 DataFlagsPad : [1] Uint4B
+0x2f8 TestRetInstruction : Uint8B
+0x300 SystemCall : Uint4B //系统调用
+0x304 SystemCallReturn : Uint4B //调用返回
+0x308 SystemCallPad : [3] Uint8B
+0x320 TickCount : _KSYSTEM_TIME
+0x320 TickCountQuad : Uint8B
+0x320 ReservedTickCountOverlay : [3] Uint4B
+0x32c TickCountPad : [1] Uint4B
+0x330 Cookie : Uint4B
+0x334 CookiePad : [1] Uint4B
+0x338 ConsoleSessionForegroundProcessId : Int8B
+0x340 Wow64SharedInformation : [16] Uint4B
+0x380 UserModeGlobalLogger : [16] Uint2B
+0x3a0 ImageFileExecutionOptions : Uint4B
+0x3a4 LangGenerationCount : Uint4B
+0x3a8 Reserved5 : Uint8B
+0x3b0 InterruptTimeBias : Uint8B
+0x3b8 TscQpcBias : Uint8B
+0x3c0 ActiveProcessorCount : Uint4B
+0x3c4 ActiveGroupCount : Uint2B
+0x3c6 Reserved4 : Uint2B
+0x3c8 AitSamplingValue : Uint4B
+0x3cc AppCompatFlag : Uint4B
+0x3d0 SystemDllNativeRelocation : Uint8B
+0x3d8 SystemDllWowRelocation : Uint4B
+0x3dc XStatePad : [1] Uint4B
+0x3e0 XState : _XSTATE_CONFIGURATION
<1>._KUSER_SHARED_DATA
1.用户层和内核层分别定义了一个_KUSER_SHARED_DATA结构体,用于在用户层和内核层共享数据,其大小为4KB(测试环境Win7 x86 这块结构系统默认用了0x5ff,意味着结构体 + 0x600 ~ 0xFFF可以构建自己的共享数据).
2. 页的知识可以知道共享数据是用户层和内核层_KUSER_SHARED_DATA结构体对应线性地址指向同一个物理页,但在用户层中这块内存是只读的,内核层中是可读可写的.
3. 用户层和内核层使用固定的地址映射_KUSER_SHARED_DATA结构体,地址如下表所示:
内核起始地址 | 内核结束地址 | 用户起始地址 | 用户结束地址 | |
---|---|---|---|---|
x86 | 0xFFDF0000 | 0xFFDF0FFF | 0x7FFE0000 | 0x7FFE0FFF |
x64 | 0xFFFFF780`00000000 | 0xFFFFF780`00000FFF | 0x7FFE0000 | 0x7FFE0FFF |
4._KUSER_SHARED_DATA共享论证.
测试环境Win7 x86
1).Windbg输入指令 !process 0 0 找一个进程附加
2).Windbg输入指令 .process /i xxxxxxxx
此时Windbg处于Dbgview进程空间中.
3).Windbg输入指令 !pte 用户层以及内核层_KUSER_SHARED_DATA结构体对应线性地址
4).修改用户层结构数据查看内核层对应数据
<2>._KUSER_SHARED_DATA.SystemCall
(Windbg输入指令 dt _KUSER_SHARED_DATA)
nt!_KUSER_SHARED_DATA
+0x300 SystemCall : Uint4B //系统调用
+0x304 SystemCallReturn : Uint4B //调用返回
R3API如果通过 MOV EDX, 7FFE0300h; CALL DWORD PTR [edx];方式进R0,实际上相当于调用_KUSER_SHARED_DATA.SystemCall中的存储的值.
_KUSER_SHARED_DATA.SystemCall中存储的值决定了函数通过什么方式进R0.(操作系统通过检查当前CPU是否支持快速调用来填充这个值,支持函数地址为KiFastSystemCall快速调用,不支持函数地址为KiIntSystemCall中断调用).
CPU是否支持快速调用?
当EAX = 1 执行CPUID指令 如果EDX第11位(SEP) = 1 说明支持快速调用,否则为中断调用,即_KUSER_SHARED_DATA.SystemCall中存储的值.
至此已经了解到R3进入R0两种方式,接下来分析中断调用,快速调用如何进入R0.
代码示例:
#include <stdio.h>
#include <windows.h>
int main()
{
DWORD dwEAX = 0;
DWORD dwECX = 0;
DWORD dwEDX = 0;
__asm
{
xor eax, eax
mov eax, 1
CPUID
mov dwEAX, eax
mov dwECX, eax
mov dwEDX, edx
}
printf("EAX 0x%08x \n",dwEAX);
printf("ECX 0x%08x \n",dwECX);
printf("EDX 0x%08x \n",dwEDX);
printf("EDX 11(BIT) [%d] \n", (dwEDX & 0x800) >> 11);
system("pause");
return 0;
}
intel白皮书介绍如下:
系统调用
<1>.中断调用KiIntSystemCall
固定中断号为: 2Eh 通过解析如下图:
<2>.快速调用KiFastSystemCall
如果CPU支持sysenter(快速调用)指令,操作系统会提前将CS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值写入相关寄存器(没有查询内存过程速度更快).
MSR | 地址 |
---|---|
IA32_SYSENTER_CS | 174H |
IA32_SYSENTER_ESP | 175H |
IA32_SYSENTER_EIP | 176H |
intel白皮书对SYSENTER介绍如下:
代码示例(特权指令需要在R0下运行)
#include <ntifs.h>
NTSTATUS DriverUnload(PDRIVER_OBJECT pDriver)
{
DbgPrint("Driver Exit \r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
DbgPrint("Driver Load \r\n");
pDriver->DriverUnload = DriverUnload;
DbgBreakPoint();
ULONGLONG uData = 0;
__asm
{
mov ecx, 0x174 //相当于msr寄存器index
rdmsr
mov dword ptr [uData], eax //eax存储数据低32位
mov dword ptr [uData + 4],edx //edx存储数据高32位
}
DbgPrint("MSR[174] -> [0x%llx] \r\n", uData);
return STATUS_SUCCESS;
}
<3>.中断调用快速调用区别
快速调用 | 中断调用 | |
---|---|---|
R3执行API | KiFastSystemCall | KiIntSystemCall |
8bd4 mov edx,esp //三环栈顶 系统调用号在EAX 0f34 sysenter |
8d542408 lea edx,[esp+8] //参数指针 系统调用号在EAX cd2e int 2Eh c3 ret |
|
提权方式(段的机制R3进入R0相当于CPL发生改变 ) | 如果CPU支持sysenter指令,操作系统会提前将CS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值写入相关寄存器(没有查询内存过程速度更快).将特权级切换到R0,如果EFLAG.VM被置位,则清除该标志位. | int 2Eh对应段描述符为83e4ee00`00083fee中断门描述符,其中加载代码段选择子为0x0008 对应段描述符为00cf9(1001)b00`0000ffff DPL = 0执行成功后CPL = 0.且因权限切换会向堆栈压入SS,ESP,EFLAG,CS,EIP. |
权限发生改变意味着CS,SS,ESP,EIP的切换 | 查询MSR寄存器 CS = rdmsr 174 SS = CS + 8(数值上) ESP = rdmsr 175 EIP = rdmsr 176 |
ESP,SS由TSS提供. CS由中断门描述符中低4字节高16位提供. EIP由中断门描述符中高4字节高16位与低4字节低16位组成. |
进入R0执行API | KiFastCallEntry(Windbg输入 rdmsr 176获取) | KiSystemService(Windbg输入!IDT 2E获取) |
至此已经知道R3API在ntdll.dll中进入R0的两种方法.
在分析对应内核函数前需要了解两个结构体_KTRAP_FRAME,_KPCR.
_KTRAP_FRAME
Windbg输入dt _KTRAP_FRAME指令:
_KTRAP_FRAME结构如下:
nt!_KTRAP_FRAME //类似于R3 -> CONTEXT
+0x000 DbgEbp : Uint4B
+0x004 DbgEip : Uint4B
+0x008 DbgArgMark : Uint4B
+0x00c DbgArgPointer : Uint4B
+0x010 TempSegCs : Uint2B
+0x012 Logging : UChar
+0x013 Reserved : UChar
+0x014 TempEsp : Uint4B
+0x018 Dr0 : Uint4B
+0x01c Dr1 : Uint4B
+0x020 Dr2 : Uint4B
+0x024 Dr3 : Uint4B
+0x028 Dr6 : Uint4B
+0x02c Dr7 : Uint4B
+0x030 SegGs : Uint4B
+0x034 SegEs : Uint4B
+0x038 SegDs : Uint4B
+0x03c Edx : Uint4B
+0x040 Ecx : Uint4B
+0x044 Eax : Uint4B
+0x048 PreviousPreviousMode : Uint4B
+0x04c ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x050 SegFs : Uint4B
+0x054 Edi : Uint4B
+0x058 Esi : Uint4B
+0x05c Ebx : Uint4B
+0x060 Ebp : Uint4B
+0x064 ErrCode : Uint4B //如果是发生错误导致其他中断触发时,这里会有ErrCode,中断调用进内核函数KiSystemService这里push 0.
+0x068 Eip : Uint4B
+0x06c SegCs : Uint4B
+0x070 EFlags : Uint4B
+0x074 HardwareEsp : Uint4B
+0x078 HardwareSegSs : Uint4B
+0x07c V86Es : Uint4B //0x07c ~ 0x088位置为虚拟8086模式下使用,函数进入R0时栈顶默认指向_KTRAP_FRAME.V86Es
+0x080 V86Ds : Uint4B
+0x084 V86Fs : Uint4B
+0x088 V86Gs : Uint4B
0x07c ~ 0x088位置为虚拟8086模式下使用.
中断调用进入R0时栈顶(ESP由TSS提供(每个线程进入R0时ESP都由TSS.ESP0提供,以及TSS里存储的ESP0一直是当前线程进入R0时对应ESP0,线程切换时会更新TSS里存储的ESP0))默认指向_KTRAP_FRAME.V86Es,中断门执行权限发生切换时会向堆栈压入SS,ESP,EFLAG,CS,RETADDR(EIP),由此得知当执行函数KiIntSystemCall进入R0函数KiSystemService时此时ESP指向_KTRAP_FRAME.Eip.(下文分析).
快速调用进入R0时堆栈是由MSR[175]提供的,KiFastCallEntry函数执行时首先会修改FS指向_KPCR结构,通过_KPCR -> _TSS定位到当前线程ESP0,并切换新的堆栈.此时ESP指向_KTRAP_FRAME.V86Ds.(下文分析).
_KPCR
一个核一个_KPCR(Processor Control Region CPU控制块)记录当前CPU核对应各种状态以及上下文环境.
查看CPU数量
kd> dd KeNumberProcessors
83fb796c 00000001 //一个核心
查看KPCR
kd> dd KiProcessorBlock //几个核对应几个KPCR
83fb78c0 83f78d20 00000000 //减去120(kpcr的大小)
kd> dt _KPCR 83f78d20-120 //就是kpcr的地址
nt!_KPCR
+0x000 NtTib : _NT_TIB
nt!_NT_TIB
+0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 StackBase : Ptr32 Void
+0x008 StackLimit : Ptr32 Void
+0x00c SubSystemTib : Ptr32 Void
+0x010 FiberData : Ptr32 Void
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32 Void
+0x018 Self : Ptr32 _NT_TIB //结构体指针 指向自己
+0x000 Used_ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Used_StackBase : Ptr32 Void
+0x008 Spare2 : Ptr32 Void
+0x00c TssCopy : Ptr32 Void
+0x010 ContextSwitches : Uint4B
+0x014 SetMemberCopy : Uint4B
+0x018 Used_Self : Ptr32 Void
+0x01c SelfPcr : Ptr32 _KPCR
+0x020 Prcb : Ptr32 _KPRCB
+0x024 Irql : UChar
+0x028 IRR : Uint4B
+0x02c IrrActive : Uint4B
+0x030 IDR : Uint4B
+0x034 KdVersionBlock : Ptr32 Void
+0x038 IDT : Ptr32 _KIDTENTRY
+0x03c GDT : Ptr32 _KGDTENTRY
+0x040 TSS : Ptr32 _KTSS
+0x044 MajorVersion : Uint2B
+0x046 MinorVersion : Uint2B
+0x048 SetMember : Uint4B
+0x04c StallScaleFactor : Uint4B
+0x050 SpareUnused : UChar
+0x051 Number : UChar
+0x052 Spare0 : UChar
+0x053 SecondLevelCacheAssociativity : UChar
+0x054 VdmAlert : Uint4B
+0x058 KernelReserved : [14] Uint4B
+0x090 SecondLevelCacheSize : Uint4B
+0x094 HalReserved : [16] Uint4B
+0x0d4 InterruptMode : Uint4B
+0x0d8 Spare1 : UChar
+0x0dc KernelReserved2 : [17] Uint4B
+0x120 PrcbData : _KPRCB
KiSystemService(函数分析)
KiIntSystemCall(R3) -> KiSystemService(R0)
设置环境
KiSystemService设置环境后跳转KiFastCallEntry(详情见下文)
KiFastCallEntry(函数分析)
KiFastSystemCall(R3) -> KiFastCallEntry(R0)
设置环境
寻找内核函数地址,拷贝参数
此时需要了解一个结构SystemServiceTable
结构如下:
定位SystemServiceTable
_KTHREAD -> ServiceTable
系统服务表有两张:
1.ntoskrnl.exe导出的常用系统服务.
2.Win32k.sys导出的与图形显示和用户界面相关的系统服务(只有GDI相关线程访问对应系统服务表才会有值).
系统服务表结构如下:
typedef struct _KSERVICE_TABLE_DESCRIPTOR
{
KSYSTEM_SERVICE_TABLE ntoskrnl; // 内核函数
KSYSTEM_SERVICE_TABLE win32k; // win32k.sys 函数
KSYSTEM_SERVICE_TABLE unUsed1; // 未使用
KSYSTEM_SERVICE_TABLE unUsed2; // 未使用
} KSERVICE_TABLE_DESCRIPTOR, * PKSERVICE_TABLE_DESCRIPTOR;
typedef struct _KSYSTEM_SERVICE_TABLE
{
PULONG ServiceTableBase; // 函数地址表基址
PULONG ServiceCounterTableBase;// 函数被调用的次数
ULONG NumberOfService; // 函数个数
PULONG ParamTableBase; // 函数参数表基址
} KSYSTEM_SERVICE_TABLE, * PKSYSTEM_SERVICE_TABLE;
ServiceTable指向函数地址表每个成员大小为4字节,存储函数地址.
ServiceLimit存储函数地址表的成员个数.
ArgumentTable 函数参数表每个成员大小为1字节,存储函数参数个数(存储值 / 4 = 参数个数).
在快速调用和中断调用R3函数执行时,EAX存储了系统服务号.
通过第12位确定是哪张表.
通过低12位确定在函数地址表中的索引值以及函数参数表的索引值.
System Services Descriptor Table系统服务描述符表,为导出结构KeServiceDescriptorTable(代码中只需声明即可直接使用).
查找ReadProcessMemory(测试环境系统服务号为115h)对应内核函数地址以及参数
Windbg查看SSDT
dd KeServiceDescriptorTable
继续函数分析
至此完成了初始化内核环境以及参数拷贝,函数调用.
函数返回
涉及到APC,此部分会更新到APC处,大致流程为执行完毕后首先判断当前IRQL等级(不为0跳转处理蓝屏),然后判断是否为虚拟8086模式,接着判断有没有APC需要处理等.最后通过iretd返回.
涉及到的结构体如下图:
Win7 x86系统调用全过程...