1、R3进入R0
WindowsXp前R3进入R0都是依靠中断门(0x2E)进行提权,这种提权方式较为复杂,需要压入SS、CS、EIP、ESP等等一系列复杂操作。因此Xp后引入快速调用(FastCall)。
x86使用的是sysenter/sysreturn,x64是syscall/sysexit。
以APIReadProcessMemory为例,使用CE跳转到该函数,然后手动跟踪。
可以看到调用过程如下:
Text1
| kernel32.ReadProcessMemory->kernelbase.ReadProcessMemory->ntdll.ZwReadVirtualMemory
|
“Window7有一项变化,即对Kernel32.dll做了重构,引入KernelBase.dll,把原本实现在Kernel32.dll中的逻辑移到Kernelbase.dll中,Kernel32只保留了接口,这样修改后,负责用户空间开发的团队只需要使用稳定版本的Kernel32.dll,不需要频繁更新,负责内核空间的团队如果对底层做修改,一般只需要修改Kernelbase.dll,不需要更新Kernel32.dll,两个团队之间的相互牵制大大减少。” –《软件调试-卷二:Windows平台调试(上)》
其中ZwReadVirtualMemory中只通过一条call [edx]即可完成函数调用,实际上edx指向的地址为_KUSER_SHARED_DATA中的成员SystemCall,该成员保存着快速调用函数的入口。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| struct _KUSER_SHARED_DATA { ULONG TickCountLowDeprecated; ULONG TickCountMultiplier; volatile struct _KSYSTEM_TIME InterruptTime; volatile struct _KSYSTEM_TIME SystemTime; volatile struct _KSYSTEM_TIME TimeZoneBias; USHORT ImageNumberLow; USHORT ImageNumberHigh; WCHAR NtSystemRoot[260]; ULONG MaxStackTraceDepth; ULONG CryptoExponent; ULONG TimeZoneId; ULONG LargePageMinimum; ULONG Reserved2[7]; enum _NT_PRODUCT_TYPE NtProductType; UCHAR ProductTypeIsValid; ULONG NtMajorVersion; ULONG NtMinorVersion; UCHAR ProcessorFeatures[64]; ULONG Reserved1; ULONG Reserved3; volatile ULONG TimeSlip; enum _ALTERNATIVE_ARCHITECTURE_TYPE AlternativeArchitecture; ULONG AltArchitecturePad[1]; union _LARGE_INTEGER SystemExpirationDate; ULONG SuiteMask; UCHAR KdDebuggerEnabled; UCHAR NXSupportPolicy; volatile ULONG ActiveConsoleId; volatile ULONG DismountCount; ULONG ComPlusPackage; ULONG LastSystemRITEventTickCount; ULONG NumberOfPhysicalPages; UCHAR SafeBootMode; union { UCHAR TscQpcData; struct { UCHAR TscQpcEnabled:1; UCHAR TscQpcSpareFlag:1; UCHAR TscQpcShift:6; }; }; UCHAR TscQpcPad[2]; union { ULONG SharedDataFlags; struct { ULONG DbgErrorPortPresent:1; ULONG DbgElevationEnabled:1; ULONG DbgVirtEnabled:1; ULONG DbgInstallerDetectEnabled:1; ULONG DbgSystemDllRelocated:1; ULONG DbgDynProcessorEnabled:1; ULONG DbgSEHValidationEnabled:1; ULONG SpareBits:25; }; }; ULONG DataFlagsPad[1]; ULONGLONG TestRetInstruction; ULONG SystemCall; ULONG SystemCallReturn; ULONGLONG SystemCallPad[3]; union { volatile struct _KSYSTEM_TIME TickCount; volatile ULONGLONG TickCountQuad; ULONG ReservedTickCountOverlay[3]; }; ULONG TickCountPad[1]; ULONG Cookie; ULONG CookiePad[1]; LONGLONG ConsoleSessionForegroundProcessId; ULONG Wow64SharedInformation[16]; USHORT UserModeGlobalLogger[16]; ULONG ImageFileExecutionOptions; ULONG LangGenerationCount; ULONGLONG Reserved5; volatile ULONGLONG InterruptTimeBias; volatile ULONGLONG TscQpcBias; volatile ULONG ActiveProcessorCount; volatile USHORT ActiveGroupCount; USHORT Reserved4; volatile ULONG AitSamplingValue; volatile ULONG AppCompatFlag; ULONGLONG SystemDllNativeRelocation; ULONG SystemDllWowRelocation; ULONG XStatePad[1]; struct _XSTATE_CONFIGURATION XState; };
|
_KUSER_SHARED_DATA是R0和R3共享的一段内存,其中R3只有读权限,R0有读写权限。将edx的值减掉0x300可得到地址头。在R0下地址为fffe0000。随便附加一个R3进程,然后查看7ffe0000。
使用Windbg查看一下该处函数。
这里没有显示符号,到ce看一下。
实际上叫做KiFastSystemCall。其中sysenter指令执行时会跳转到MSR[176]指向的函数地址。快速调用之所以比中断门提权快是因为,快速调用进入内核的时,CS、SS、EIP、ESP均来自MSR寄存器,CPU直接读取后即可进入提权操作,并且快速调用中会自己保护上下文环境。中断门需要自己压参数,和保护上下文,性能上不如直接读取CPU寄存器快。
| 索引 |
说明 |
| 174H |
CS |
| 175H |
ESP |
| 176H |
EIP |
SS=CS的值+8,因为SS的描述符是紧随着CS的描述符。
使用windbg查看176位置的函数。
由于没有加载出符号,但实际上这个函数是KiFastCallEntry
1.2 分析KiFastCallEntry
IDA中会看到还有一个KiFastCallEntry2,这是因为不是所有API都是过这个KiFastCallEntry,那个2是为了兼容其他框架。
这里需要了解两个结构体,KPCR和KTrap_Frame
- fs:[30]:在3环时,该处指向的是PEB结构,0环下指向_KPCR结构。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| struct _KPCR { union { struct _NT_TIB NtTib; struct { struct _EXCEPTION_REGISTRATION_RECORD* Used_ExceptionList; VOID* Used_StackBase; VOID* Spare2; VOID* TssCopy; ULONG ContextSwitches; ULONG SetMemberCopy; VOID* Used_Self; }; }; struct _KPCR* SelfPcr; struct _KPRCB* Prcb; UCHAR Irql; ULONG IRR; ULONG IrrActive; ULONG IDR; VOID* KdVersionBlock; struct _KIDTENTRY* IDT; struct _KGDTENTRY* GDT; struct _KTSS* TSS; USHORT MajorVersion; USHORT MinorVersion; ULONG SetMember; ULONG StallScaleFactor; UCHAR SpareUnused; UCHAR Number; UCHAR Spare0; UCHAR SecondLevelCacheAssociativity; ULONG VdmAlert; ULONG KernelReserved[14]; ULONG SecondLevelCacheSize; ULONG HalReserved[16]; ULONG InterruptMode; UCHAR Spare1; ULONG KernelReserved2[17]; struct _KPRCB PrcbData; };
|
- KTrap_Frame:栈帧,由于R3切换到R0,环境产生改变,因此需要保存。这个结构就是用来保存R3切换到R0的环境。
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| struct _KTRAP_FRAME { ULONG DbgEbp; ULONG DbgEip; ULONG DbgArgMark; ULONG DbgArgPointer; USHORT TempSegCs; UCHAR Logging; UCHAR Reserved; ULONG TempEsp; ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7; ULONG SegGs; ULONG SegEs; ULONG SegDs; ULONG Edx; ULONG Ecx; ULONG Eax; ULONG PreviousPreviousMode; struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList; ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ULONG ErrCode; ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; ULONG HardwareSegSs; ULONG V86Es; ULONG V86Ds; ULONG V86Fs; ULONG V86Gs; };
|
2、系统服务描述表
由上边分析可知,最后调用的函数来自于一个ServiceTable,这个叫做服务表,一共有两张,一张叫做SystemServiceDecriptionTable,另一张叫做SystemServiceDecriptionTableShadow其中后边这张只有带有UI的程序才会存在。
1 2 3 4 5 6 7 8 9 10 11 12
| typedef struct _KSERVICE_TABLKSERVICE_TABLE_DESCRIPTORE_DESCRIPTOR { PULONG_PTR FuncPoint; PULONG Count; PULONG Limit; PUCHAR ArgsPoint; }KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR; #define NUMBER_SERVICE_TABLES 2 KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable[NUMBER_SERVICE_TABLES] KSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTableShadow[NUMBER_SERVICE_TABLES]
|
Windows中提供了一个导出的全局变量SystemServicesDescriptorTable,也叫SSDT。这个全局变量存储了系统服务表的地址,但仅存储了第一张表的地址。
在windbg中输入命令 dd KeServiceDescriptorTable查看该全局变量。
一张表共0x10个字节,也解释微软在上边通过服务号计算表的算法为什么利用0x10算。内核中还有一个全局变量称为SSDT Shadow,该变量未导出,但可以查看所有的系统服务表。在windbg中输入命令 dd KeServiceDescriptorTableShadow,也叫SSSDT。查看该全局变量。可以看到所有的系统服务表。
2.1 分析SSSDT初始化
2.2 服务号
小技巧:服务号>0x1000是SSSDT,小于0x1000是SSDT。
3、R0回R3
检查ETW日志是否需要记录。并为Kthread->TrapFrame成员赋值。可用于恢复旧TrapFrame或蓝屏时进行堆栈回溯。
随后判断是否虚拟8086模式和之前的特权级。并相应的执行性能统计
如果有APC需要处理则处理APC。
恢复异常链表及调试寄存器(如果为调试模式的话)。
进行一万个感觉没卵用的判断
最后恢复各种寄存器,并以iret方式返回3环。
4、HOOK SSDT
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #include<ntifs.h>
typedef struct _KSERVICE_TABLKSERVICE_TABLE_DESCRIPTORE_DESCRIPTOR { PULONG_PTR FuncPoint; PULONG Count; PULONG Limit; PUCHAR ArgsPoint; }KSERVICE_TABLE_DESCRIPTOR, *PKSERVICE_TABLE_DESCRIPTOR;
EXTERN_C PKSERVICE_TABLE_DESCRIPTOR KeServiceDescriptorTable;
typedef NTSTATUS(NTAPI *fnNtOpenProcess)(_Out_ PHANDLE ProcessHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, _In_opt_ PCLIENT_ID ClientId);
fnNtOpenProcess lpOpenProcess = NULL;
PVOID HookSSDT(ULONG serviceIndex, PVOID hkFunc) { ULONG func_offset = serviceIndex & 0xFFF; PULONG_PTR func_pointer = &((KeServiceDescriptorTable->FuncPoint)[func_offset]); PHYSICAL_ADDRESS tmp = MmGetPhysicalAddress(func_pointer); PULONG_PTR mapAddr = MmMapIoSpace(tmp, PAGE_SIZE, MmCached); if (mapAddr > 0) { PVOID origin = (KeServiceDescriptorTable->FuncPoint)[func_offset]; *mapAddr = hkFunc; return origin; } return NULL; }
VOID unloadDrv(PDRIVER_OBJECT pDrv) { HookSSDT(0xBE, lpOpenProcess); }
NTSTATUS NTAPI MyNtOpenProcess(_Out_ PHANDLE ProcessHandle, _In_ ACCESS_MASK DesiredAccess, _In_ POBJECT_ATTRIBUTES ObjectAttributes, _In_opt_ PCLIENT_ID ClientId) { PEPROCESS eproc = NULL; NTSTATUS status = PsLookupProcessByProcessId(ClientId->UniqueProcess, &eproc); if (NT_SUCCESS(status)) { PUNICODE_STRING imageName = NULL; status = SeLocateProcessImageName(eproc, &imageName); if (NT_SUCCESS(status)) { UNICODE_STRING target = {0}; RtlInitUnicodeString(&target, L"\\Device\\HarddiskVolume2\\内核工具包\\Loader\\InstDrv.exe"); if (!RtlCompareUnicodeString(imageName, &target, TRUE)) { DbgPrintEx(77, 0, "hook \r\n"); return STATUS_ACCESS_VIOLATION; } } }
return lpOpenProcess(ProcessHandle, DesiredAccess, ObjectAttributes, ClientId); }
NTSTATUS DriverEntry(PDRIVER_OBJECT pDrv, PUNICODE_STRING pReg) { pDrv->DriverUnload = unloadDrv;
lpOpenProcess = HookSSDT(0xBE, MyNtOpenProcess); if (lpOpenProcess>0) DbgPrintEx(77, 0, "ok\r\n"); else DbgPrintEx(77, 0, "no\r\n"); return STATUS_SUCCESS; }
|
通过HOOK OpenProcess函数判断如果打开的进程为InstDrv则直接返回失败,效果:CE对进程InstDrv无图标。
打开进程失败。
驱动卸载后,一切正常。
5、PreviousMode理解
假如R3向驱动发送了一个信息号,让驱动执行函数NtProtectVirtualMemory,此时R0的PreviousMode为1,即为三环。
打开WRK查看函数NtProtectVirtualMemory流程
可以看到如果PreviousMode不是KernelMode,则会判断当前传进来的参数是否可读,否则会跑出一个异常。这里的坑就是,假设通过驱动修改某个进程地址的属性,那么此时BaseAddress属于R3的内存范围,因此这里的判断就会出错,进入异常。
因此需要在调用的时候修改PreviousMode。
调用完毕后再修改回来。