x86系统调用

1、R3进入R0

WindowsXp前R3进入R0都是依靠中断门(0x2E)进行提权,这种提权方式较为复杂,需要压入SS、CS、EIP、ESP等等一系列复杂操作。因此Xp后引入快速调用(FastCall)

x86使用的是sysenter/sysreturn,x64是syscall/sysexit。

以APIReadProcessMemory为例,使用CE跳转到该函数,然后手动跟踪。

可以看到调用过程如下:

Text
1
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
//0x5f0 bytes (sizeof)
struct _KUSER_SHARED_DATA
{
ULONG TickCountLowDeprecated; //0x0
ULONG TickCountMultiplier; //0x4
volatile struct _KSYSTEM_TIME InterruptTime; //0x8
volatile struct _KSYSTEM_TIME SystemTime; //0x14
volatile struct _KSYSTEM_TIME TimeZoneBias; //0x20
USHORT ImageNumberLow; //0x2c
USHORT ImageNumberHigh; //0x2e
WCHAR NtSystemRoot[260]; //0x30
ULONG MaxStackTraceDepth; //0x238
ULONG CryptoExponent; //0x23c
ULONG TimeZoneId; //0x240
ULONG LargePageMinimum; //0x244
ULONG Reserved2[7]; //0x248
enum _NT_PRODUCT_TYPE NtProductType; //0x264
UCHAR ProductTypeIsValid; //0x268
ULONG NtMajorVersion; //0x26c
ULONG NtMinorVersion; //0x270
UCHAR ProcessorFeatures[64]; //0x274
ULONG Reserved1; //0x2b4
ULONG Reserved3; //0x2b8
volatile ULONG TimeSlip; //0x2bc
enum _ALTERNATIVE_ARCHITECTURE_TYPE AlternativeArchitecture; //0x2c0
ULONG AltArchitecturePad[1]; //0x2c4
union _LARGE_INTEGER SystemExpirationDate; //0x2c8
ULONG SuiteMask; //0x2d0
UCHAR KdDebuggerEnabled; //0x2d4
UCHAR NXSupportPolicy; //0x2d5
volatile ULONG ActiveConsoleId; //0x2d8
volatile ULONG DismountCount; //0x2dc
ULONG ComPlusPackage; //0x2e0
ULONG LastSystemRITEventTickCount; //0x2e4
ULONG NumberOfPhysicalPages; //0x2e8
UCHAR SafeBootMode; //0x2ec
union
{
UCHAR TscQpcData; //0x2ed
struct
{
UCHAR TscQpcEnabled:1; //0x2ed
UCHAR TscQpcSpareFlag:1; //0x2ed
UCHAR TscQpcShift:6; //0x2ed
};
};
UCHAR TscQpcPad[2]; //0x2ee
union
{
ULONG SharedDataFlags; //0x2f0
struct
{
ULONG DbgErrorPortPresent:1; //0x2f0
ULONG DbgElevationEnabled:1; //0x2f0
ULONG DbgVirtEnabled:1; //0x2f0
ULONG DbgInstallerDetectEnabled:1; //0x2f0
ULONG DbgSystemDllRelocated:1; //0x2f0
ULONG DbgDynProcessorEnabled:1; //0x2f0
ULONG DbgSEHValidationEnabled:1; //0x2f0
ULONG SpareBits:25; //0x2f0
};
};
ULONG DataFlagsPad[1]; //0x2f4
ULONGLONG TestRetInstruction; //0x2f8
ULONG SystemCall; //0x300
ULONG SystemCallReturn; //0x304
ULONGLONG SystemCallPad[3]; //0x308
union
{
volatile struct _KSYSTEM_TIME TickCount; //0x320
volatile ULONGLONG TickCountQuad; //0x320
ULONG ReservedTickCountOverlay[3]; //0x320
};
ULONG TickCountPad[1]; //0x32c
ULONG Cookie; //0x330
ULONG CookiePad[1]; //0x334
LONGLONG ConsoleSessionForegroundProcessId; //0x338
ULONG Wow64SharedInformation[16]; //0x340
USHORT UserModeGlobalLogger[16]; //0x380
ULONG ImageFileExecutionOptions; //0x3a0
ULONG LangGenerationCount; //0x3a4
ULONGLONG Reserved5; //0x3a8
volatile ULONGLONG InterruptTimeBias; //0x3b0
volatile ULONGLONG TscQpcBias; //0x3b8
volatile ULONG ActiveProcessorCount; //0x3c0
volatile USHORT ActiveGroupCount; //0x3c4
USHORT Reserved4; //0x3c6
volatile ULONG AitSamplingValue; //0x3c8
volatile ULONG AppCompatFlag; //0x3cc
ULONGLONG SystemDllNativeRelocation; //0x3d0
ULONG SystemDllWowRelocation; //0x3d8
ULONG XStatePad[1]; //0x3dc
struct _XSTATE_CONFIGURATION XState; //0x3e0
};

_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是为了兼容其他框架。


这里需要了解两个结构体,KPCRKTrap_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
//0x3748 bytes (sizeof)
struct _KPCR
{
union
{
struct _NT_TIB NtTib; //0x0
struct
{
struct _EXCEPTION_REGISTRATION_RECORD* Used_ExceptionList; //0x0
VOID* Used_StackBase; //0x4
VOID* Spare2; //0x8
VOID* TssCopy; //0xc
ULONG ContextSwitches; //0x10
ULONG SetMemberCopy; //0x14
VOID* Used_Self; //0x18
};
};
struct _KPCR* SelfPcr; //0x1c
struct _KPRCB* Prcb; //0x20
UCHAR Irql; //0x24
ULONG IRR; //0x28
ULONG IrrActive; //0x2c
ULONG IDR; //0x30
VOID* KdVersionBlock; //0x34
struct _KIDTENTRY* IDT; //0x38
struct _KGDTENTRY* GDT; //0x3c
struct _KTSS* TSS; //0x40
USHORT MajorVersion; //0x44
USHORT MinorVersion; //0x46
ULONG SetMember; //0x48
ULONG StallScaleFactor; //0x4c
UCHAR SpareUnused; //0x50
UCHAR Number; //0x51
UCHAR Spare0; //0x52
UCHAR SecondLevelCacheAssociativity; //0x53
ULONG VdmAlert; //0x54
ULONG KernelReserved[14]; //0x58
ULONG SecondLevelCacheSize; //0x90
ULONG HalReserved[16]; //0x94
ULONG InterruptMode; //0xd4
UCHAR Spare1; //0xd8
ULONG KernelReserved2[17]; //0xdc
struct _KPRCB PrcbData; //0x120
};
  • 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
//0x8c bytes (sizeof)
struct _KTRAP_FRAME
{
ULONG DbgEbp; //0x0
ULONG DbgEip; //0x4
ULONG DbgArgMark; //0x8
ULONG DbgArgPointer; //0xc
USHORT TempSegCs; //0x10
UCHAR Logging; //0x12
UCHAR Reserved; //0x13
ULONG TempEsp; //0x14
ULONG Dr0; //0x18
ULONG Dr1; //0x1c
ULONG Dr2; //0x20
ULONG Dr3; //0x24
ULONG Dr6; //0x28
ULONG Dr7; //0x2c
ULONG SegGs; //0x30
ULONG SegEs; //0x34
ULONG SegDs; //0x38
ULONG Edx; //0x3c
ULONG Ecx; //0x40
ULONG Eax; //0x44
ULONG PreviousPreviousMode; //0x48-----R0用
struct _EXCEPTION_REGISTRATION_RECORD* ExceptionList; //0x4c
ULONG SegFs; //0x50
ULONG Edi; //0x54
ULONG Esi; //0x58
ULONG Ebx; //0x5c
ULONG Ebp; //0x60
ULONG ErrCode; //0x64
ULONG Eip; //0x68
ULONG SegCs; //0x6c
ULONG EFlags; //0x70-----R3用
ULONG HardwareEsp; //0x74
ULONG HardwareSegSs; //0x78
ULONG V86Es; //0x7c 虚拟8086模式下,保护模式下不用
ULONG V86Ds; //0x80
ULONG V86Fs; //0x84
ULONG V86Gs; //0x88
};

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;//hook函数
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。

调用完毕后再修改回来。