1. API的定义 Application Programming Interface,应用程序接口,简称API,主要是存放在系统目录C:\Windows\System32下的所有dll,目前大概存在上千个。在Windows上编程,必须要使用WindowsAPI,无法绕开。比如说C函数malloc底层调用的是WindowsAPI中的HeapAlloc。
Kernel32:内存管理、进程线程
User32:用户界面相关
GDI32:画图和显示文本相关
Ntdll:内核入口(大部分函数都需要进入0环实现功能,3环仅仅是一个接口;只有少部分函数可以完全在3环实现功能)
2. API的调用过程-3环部分 2.1. kernel32.dll!ReadProcessMemory 分析一下kernel32中的ReadProcessMemory函数
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 .text: 7C8021D0 .text: 7C8021D0 public ReadProcessMemory.text: 7C8021D0 ReadProcessMemory proc near .text: 7C8021D0 .text: 7C8021D0.text: 7C8021D0 ProcessHandle = dword ptr 8 .text: 7C8021D0 BaseAddress = dword ptr 0Ch .text: 7C8021D0 Buffer = dword ptr 10h .text: 7C8021D0 NumberOfBytesToRead= dword ptr 14h .text: 7C8021D0 lpNumberOfBytesRead= dword ptr 18h .text: 7C8021D0.text: 7C8021D0 mov edi , edi .text: 7C8021D2 push ebp .text: 7C8021D3 mov ebp , esp .text: 7C8021D5 lea eax , [ebp +NumberOfBytesToRead].text: 7C8021D8 push eax .text: 7C8021D9 push [ebp +NumberOfBytesToRead] .text: 7C8021DC push [ebp +Buffer] .text: 7C8021DF push [ebp +BaseAddress] .text: 7C8021E2 push [ebp +ProcessHandle] .text: 7C8021E5 call ds :NtReadVirtualMemory.text: 7C8021EB mov ecx , [ebp +lpNumberOfBytesRead].text: 7C8021EE test ecx , ecx .text: 7C8021F0 jnz short loc_7C8021FD.text: 7C8021F2.text: 7C8021F2 loc_7C8021F2: .text: 7C8021F2 test eax , eax .text: 7C8021F4 jl short loc_7C802204.text: 7C8021F6 xor eax , eax .text: 7C8021F8 inc eax .text: 7C8021F9.text: 7C8021F9 loc_7C8021F9: .text: 7C8021F9 pop ebp .text: 7C8021FA retn 14h .text: 7C8021FD .text: 7C8021FD.text: 7C8021FD loc_7C8021FD: .text: 7C8021FD mov edx , [ebp +NumberOfBytesToRead].text: 7C802200 mov [ecx ], edx .text: 7C802202 jmp short loc_7C8021F2.text: 7C802204 .text: 7C802204.text: 7C802204 loc_7C802204: .text: 7C802204 push eax .text: 7C802205 call SetError.text: 7C80220A xor eax , eax .text: 7C80220C jmp short loc_7C8021F9.text: 7C80220C ReadProcessMemory endp
可以看到代码仅仅是压入参数,然后call ds:NtReadVirtualMemory,之后设置了一下返回值,所以核心功能应该位于NtReadVirtualMemory中,该函数位于ntdll中
2.2. ntdll.dll!NtReadVirtualMemory 1 2 3 4 5 6 7 .text: 7C92D9E0 ZwReadVirtualMemory proc near .text: 7C92D9E0 .text: 7C92D9E0 mov eax , 0BAh .text: 7C92D9E5 mov edx , 7FFE0300h .text: 7C92D9EA call dword ptr [edx ] .text: 7C92D9EC retn 14h .text: 7C92D9EC ZwReadVirtualMemory endp
代码仅仅是设置了一下eax,然后call dword ptr [7FFE0300h],所以核心功能应该位于7FFE0300h地址中,该地址仍然位于ntdll中
2.3. 7FFE0300h地址 2.3.1. _KUSER_SHARED_DATA结构体 User层和Kernel层分别有一个该结构区域,用于User层和Kernel层分享某些数据,它们使用固定的地址值映射
User:0x7FFE0000
Kernel:0xFFDF0000
它们指向同一个物理页,但是在User层只读,在Kernel层可读可写。
2.3.2. SystemCall-进入0环的通道 7FFE0300h地址即_KUSER_SHARED_DATA+300h,为SystemCall,3环进入0环的函数的地址。通过eax=1来调用cpuid指令时,处理器的特征信息存放在ecx和edx寄存器中,其中edx包含了一个SEP位(11位),指明了当前CPU是否支持sysenter/sysexit指令(置1代表支持)。
不支持:ntdll.dll!KiIntSystemCall(),通过中断门进入0环
支持:ntdll.dll!KiFastSystemCall(),通过快速调用进入0环
操作系统会在判断当前CPU是否支持sysenter/sysexit指令后将对应的函数地址放置到_KUSER_SHARED_DATA+300h处。
2.3.3. 进入0环需要什么?
CS权限变化,需要新的CS
SS权限变化(和CS保持一致),需要新的SS
堆栈切换,需要新的ESP
指令切换,需要新的EIP
2.3.4. 通过中断门进入0环:ntdll.dll!KiIntSystemCall 1 2 3 4 5 6 7 8 .text: 7C92E500 KiIntSystemCall proc near .text: 7C92E500.text: 7C92E500 arg_4 = byte ptr 8 .text: 7C92E500.text: 7C92E500 lea edx , [esp +arg_4] .text: 7C92E504 int 2Eh .text: 7C92E506 retn .text: 7C92E506 KiIntSystemCall endp
API调用的统一中断门中断号为0x2E。需要的CS、EIP(EIP对应的0环函数为nt!KiSystemService)位于门描述符中,需要的SS、ESP位于TSS中。
2.3.5. 通过快速调用进入0环:ntdll.dll!KiFastSystemCall 1 2 3 4 .text: 7C92E4F0 KiFastSystemCall proc near .text: 7C92E4F0 mov edx , esp .text: 7C92E4F2 sysenter .text: 7C92E4F2 KiFastSystemCall endp
如果CPU支持sysenter指令,操作系统会在启动的时候提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,这就减去了中断门查找内存所消耗的大量时间,所以叫做快速调用。这里EIP对应的0环函数为nt!KiFastCallEntry。
2.3.6. MSR寄存器
MSR寄存器名称
偏移
IA32_SYSENTER_CS
174H
IA32_SYSENTER_ESP
175H
IA32_SYSENTER_EIP
176H
SS寄存器的值不在MSR寄存器中,通过计算得到,为CS寄存器的值+8。CPU会在启动的时候填充好MSR寄存器的值。
2.3.6.1. Windbg读写MSR寄存器 1 2 rdmsr 174 wrmsr 174 00000000 `00000008
2.3.6.2. 汇编指令读写MSR寄存器 1 2 3 4 mov ecx ,119h rdmsr or eax ,00200000h wrmsr
偏移值放置在ECX寄存器中,读写值存储在EDX:EAX寄存器中。
3. API的调用过程-0环部分 3.1. 内核模块文件
ntoskrnl.exe(10-10-12分页)
ntkrnlpa.exe(2-9-9-12分页)
单核和多核两种情况下,内核模块文件不同(名称相同、内部代码有差异)。
3.2. 相关结构体
_KTRAP_FRAME:用于保存进入0环时之前的一些寄存器等信息
_ETHREAD、_KTHREAD:线程、进程相关结构体
_KPCR:CPU控制区,保存CPU状态,每个CPU有一个该结构体
windbg命令dd KeNumberProcessors可以查看CPU数量
windbg命令dd KiProcessorBlock查看kpcr结构体地址,出现了几个有效地址就代表有几个CPU
3.3. 通过中断门进入0环之后:nt!KiSystemService 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 .text: 00407631 push 0 .text: 00407633 push ebp .text: 00407634 push ebx .text: 00407635 push esi .text: 00407636 push edi .text: 00407637 push fs .text: 00407639 mov ebx , 30h .text: 0040763E mov fs , ebx .text: 00407640 push dword ptr ds :0FFDFF000h .text: 00407640 .text: 00407640 .text: 00407646 mov dword ptr ds :0FFDFF000h , 0FFFFFFFFh .text: 00407650 mov esi , ds :0FFDFF124h .text: 00407656 push dword ptr [esi +140h ] .text: 0040765C sub esp , 48h .text: 0040765F mov ebx , [esp +68h +arg_0] .text: 00407663 and ebx , 1 .text: 00407666 mov [esi +140h ], bl .text: 0040766C mov ebp , esp .text: 0040766E mov ebx , [esi +134h ] .text: 00407674 mov [ebp +3Ch ], ebx .text: 00407677 mov [esi +134h ], ebp .text: 0040767D cld .text: 0040767E mov ebx , [ebp +60h ] .text: 00407681 mov edi , [ebp +68h ] .text: 00407684 mov [ebp +0Ch ], edx .text: 00407687 mov dword ptr [ebp +8 ], 0BADB0D00h .text: 0040768E mov [ebp +0 ], ebx .text: 00407691 mov [ebp +4 ], edi .text: 00407694 test byte ptr [esi +2Ch ], 0FFh .text: 00407694 .text: 00407694 .text: 00407698 jnz Dr_kss_a .text: 0040769E sti .text: 0040769F jmp loc_407781.text: 00407781 mov edi , eax .text: 00407783 shr edi , 8 .text: 00407786 and edi , 30h .text: 00407789 mov ecx , edi .text: 0040778B add edi , [esi +0E0h ] .text: 0040778B .text: 0040778B .text: 00407791 mov ebx , eax .text: 00407793 and eax , 0FFFh .text: 00407798 cmp eax , [edi +8 ] .text: 0040779B jnb _KiBBTUnexpectedRange .text: 004077A1 cmp ecx , 10h .text: 004077A4 jnz short loc_4077C0 .text: 004077A6 mov ecx , ds :0FFDFF018h .text: 004077A6 .text: 004077A6 .text: 004077A6 .text: 004077AC xor ebx , ebx .text: 004077AE or ebx , [ecx +0F70h ] .text: 004077AE .text: 004077B4 jz short loc_4077C0 .text: 004077B6 push edx .text: 004077B7 push eax .text: 004077B8 call ds :_KeGdiFlushUserBatch.text: 004077BE pop eax .text: 004077BF pop edx .text: 004077C0.text: 004077C0 loc_4077C0: .text: 004077C0 .text: 004077C0 inc dword ptr ds :0FFDFF638h .text: 004077C6 mov esi , edx .text: 004077C8 mov ebx , [edi +0Ch ] .text: 004077CB xor ecx , ecx .text: 004077CD mov cl , [eax +ebx ] .text: 004077D0 mov edi , [edi ] .text: 004077D2 mov ebx , [edi +eax *4 ] .text: 004077D5 sub esp , ecx .text: 004077D7 shr ecx , 2 .text: 004077DA mov edi , esp .text: 004077DC cmp esi , ds :_MmUserProbeAddress .text: 004077E2 jnb loc_407990 .text: 004077E8.text: 004077E8 loc_4077E8: .text: 004077E8 rep movsd .text: 004077EA call ebx .text: 004077EC.text: 004077EC loc_4077EC: .text: 004077EC mov esp , ebp .text: 004077EE.text: 004077EE loc_4077EE: .text: 004077EE .text: 004077EE mov ecx , ds :0FFDFF124h .text: 004077F4 mov edx , [ebp +3Ch ] .text: 004077F7 mov [ecx +134h ], edx
3.4. 通过快速调用进入0环之后:nt!KiFastCallEntry 详细信息参见Intel白皮书第二卷。
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 .text: 004076F0 mov ecx , 23h .text: 004076F5 push 30h .text: 004076F7 pop fs .text: 004076F9 mov ds , ecx .text: 004076FB mov es , ecx .text: 004076FD mov ecx , ds :0FFDFF040h .text: 00407703 mov esp , [ecx +4 ] .text: 00407706 push 23h .text: 00407708 push edx .text: 00407709 pushf .text: 0040770A.text: 0040770A loc_40770A: .text: 0040770A push 2 .text: 0040770C add edx , 8 .text: 0040770F popf .text: 00407710 or byte ptr [esp +1 ],2 .text: 00407715 push 1Bh .text: 00407717 push dword ptr ds :0FFDF0304h .text: 0040771D push 0 .text: 0040771F push ebp .text: 00407720 push ebx .text: 00407721 push esi .text: 00407722 push edi .text: 00407723 mov ebx , ds :0FFDFF01Ch .text: 00407729 push 3Bh .text: 0040772B mov esi , [ebx +124h ] .text: 00407731 push dword ptr [ebx ] .text: 00407733 mov dword ptr [ebx ], 0FFFFFFFFh .text: 00407739 mov ebp , [esi +18h ] .text: 0040773C push 1 .text: 0040773E sub esp , 48h .text: 00407741 sub ebp , 29Ch .text: 00407747 mov byte ptr [esi +140h ], 1 .text: 0040774E cmp ebp , esp .text: 00407750 jnz loc_4076C8.text: 00407756 and dword ptr [ebp +2Ch ], 0 .text: 0040775A test byte ptr [esi +2Ch ], 0FFh .text: 0040775E mov [esi +134h ], ebp
4. 实践:不通过WindowsAPI调用系统函数 4.1. 实现环境
操作系统:WinXP
编译器:VS2013
函数:WriteProcessMemory
4.2. 代码 4.2.1. 通过Call 7FFE0300 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 #include "stdio.h" #include "string.h" #include "windows.h" typedef NTSTATUS (NTAPI*pfnNtWriteVirtualMemory) ( HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG BufferLength, PULONG ReturnLength OPTIONAL ) ;pfnNtWriteVirtualMemory NtWriteVirtualMemory = (pfnNtWriteVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll" ), "NtWriteVirtualMemory" ); BOOL EnableDebugPrivilege () { HANDLE hToken; BOOL fOk = FALSE; if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken)) { TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1 ; LookupPrivilegeValue(NULL , SE_DEBUG_NAME, &tp.Privileges[0 ].Luid); tp.Privileges[0 ].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof (tp), NULL , NULL ); fOk = (GetLastError() == ERROR_SUCCESS); CloseHandle(hToken); } return fOk; } int main (void ) { EnableDebugPrivilege(); char strw[10 ]; char buffer[10 ] = "hello" ; HANDLE phandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); __asm{ push 0 push 0xA lea eax, buffer push eax lea eax, strw push eax push phandle push 0 ; 填补返回地址 mov eax, 0x115 ; NtWriteVirtualMemory in WinXP mov edx, 7F FE0300h call dword ptr[edx] add esp, 18 h ; 平衡堆栈 }; printf ("%s\n" , buffer); printf ("%s\n" , strw); return 0 ; }
4.2.2. 通过中断门 替换之前代码的asm部分即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __asm{ push 0 push 0xA lea eax ,buffer push eax lea eax , strw push eax push phandle push 0 mov eax , 0x115 push 0 lea edx ,[esp +8 ] int 2Eh add esp , 1Ch }
4.2.3. 通过快速调用 目前存在内联汇编无法编译的问题
4.3. NtProtectVirtualMemory 此代码所写内存本身为可写内存,如果需要写在原本只读的内存中,需要先调用NtProtectVirtualMemory修改内存页属性为可写,原理和上面相同
1 2 3 4 5 6 NTSYSAPI NTSTATUS NTAPI NtProtectVirtualMemory ( IN HANDLE ProcessHandle, IN OUT PVOID *BaseAddress, IN OUT PULONG NumberOfBytesToProtect, IN ULONG NewAccessProtection, OUT PULONG OldAccessProtection ) ;
以上为NtProtectVirtualMemory函数原型,参数分别为进程句柄、存有需要修改的页面地址的内存空间的指针、需要修改的字节大小、新的权限代码、存放旧的权限代码的内存空间指针。函数调用之后,2、3、5三个参数可能会被改变。2会向下对齐到1000的倍数地址(ex:7FFDF150->7FFDF000),3会向上对齐到1000的倍数(ex:3FF->1000),5会存放有旧的权限代码。
5. 系统服务表 5.1. 概念图
5.1.1. 系统服务表位置 系统服务表位于_KTHREAD + 0xE0
5.1.2. 系统服务表成员
ServiceTable:指针,指向函数地址表,表中每个成员占四个字节
Count:当前系统服务表被调用了多少次
ServiceLimit:当前系统服务表中函数数量
ArgmentTable:指针,指向函数参数表(存有函数地址表中对应函数参数的个数,以字节为单位),表中每个成员占一个字节
5.1.3. 两张系统服务表
内核文件(ntoskrl.exe、ntkrnlpa.exe)导出函数的一部分,为常用的系统服务
win32k.sys(user32.dll -> gdi32.dll -> win32k.sys)的导出函数的一部分,为与图形显示以及用户界面相关的系统服务
ntoskrl.exe(ntkrnlpa.exe)和win32k.sys其实就是Windows内核的两大构成部分。
5.2. 解析系统调用号->定位系统服务表中的函数 5.2.1. 系统调用号结构
比特位
31-13
12
11-0
结构
保留
表指示位
索引号
表指示位:1代表函数位于win32k.sys表中,0代表函数位于ntoskrl.exe表中
索引号:函数表的索引号
注意 :解析系统调用号的对应代码见上文3.3节
5.3. 修改系统服务表 可以通过SSDT(见第6节)来找到系统服务表并修改,注意使用2-9-9-12分页,使用10-10-12分页可能会导致蓝屏
6. 系统服务描述符表(SSDT,System Service Descriptor Table) SSDT中共有四个成员,每一个成员都是一个系统服务表,第三、四个成员Windows操作系统并未使用
6.1. 查看SSDT 内核文件(ntoskrnl.exe、ntkrnlpa.exe)导出了一个全局变量KeServiceDescriptorTable(直接声明即可使用),使用windbg命令dd KeServiceDescriptorTable可以查看SSDT,但是该命令只能看到第一个成员即ntoskrnl.exe表,查看第二个成员即win32k.sys表(未导出全局变量)需要通过windbg命令dd KeServiceDescriptorTableShadow