Windows所提供给R3环的API,实质就是对操作系统接口的封装,其实现部分都是在R0实现的。很多恶意程序会利用钩子来钩取这些API,从而达到截取内容,修改数据的意图。现在我们使用ollydbg对ReadProcessMemory进行跟踪分析,查看其在R3的实现。
测试 od 我们首先在od里面跟一下在ring3层ReadProcessMemory
的调用过程
首先在 exe 中 调用 kernel32.ReadProcessMemory
函数,我们可以看到这一部分主要是call dword ptr ds:[<&KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory
这一行代码比较关键,调用了kernel32.ReadProcessMemory
,继续往里面跟
1 2 3 4 5 6 7 8 9 10 11 01314E3 E 8B F4 mov esi,esp 01314E40 6 A 00 push 0x0 01314E42 6 A 04 push 0x4 01314E44 8 D45 DC lea eax,dword ptr ss:[ebp-0x24 ] 01314E47 50 push eax 01314E48 8B 4D C4 mov ecx,dword ptr ss:[ebp-0x3C ] 01314E4 B 8 D548D E8 lea edx,dword ptr ss:[ebp+ecx*4 -0x18 ] 01314E4 F 52 push edx 01314E50 6 A FF push -0x1 01314E52 FF15 64B 0310 call dword ptr ds:[<&KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory 01314E58 3B F4 cmp esi,esp
在 ReadProcessMemory
函数 中调用 jmp.&API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory>
函数,在kenel32.dll
中,mov edi,edi
是用于热补丁技术所保留的,这段代码仔细看其实除了jmp
什么也没干,继续跟jmp
1 2 3 4 5 7622 C1CE 8B FF mov edi,edi7622 C1D0 55 push ebp7622 C1D1 8B EC mov ebp,esp7622 C1D3 5 D pop ebp 7622 C1D4 E9 F45EFCFF jmp <jmp.&API-MS-Win-Core-Memory-L1-1 -0. ReadProcessMemory>
在 API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemo
中调用 KernelBase.ReadProcessMemory
函数,这里的调用链就是从kernel32.dll
到了kernelBase.dll
1 2 761F 20CD FF25 0 C191F7 jmp dword ptr ds:[<&API-MS-Win-Core-Memory-L1-1 -0. ReadProcessMemory>; KernelBase.ReadProcessMemory
在KernelBase.ReadProcessMemory
中 调用 <&ntdll.NtReadVirtualMemory>
函数,将ReadProcessMemory
中传入的参数再次入栈,调用ntdll.ZwReadVirtualMemory
函数,再往里面走
1 2 3 4 5 6 7 8 9 10 11 75 DA9A0A 8B FF mov edi,edi 75 DA9A0C 55 push ebp 75 DA9A0D 8B EC mov ebp,esp 75 DA9A0F 8 D45 14 lea eax,dword ptr ss:[ebp+0x14 ] 75 DA9A12 50 push eax 75 DA9A13 FF75 14 push dword ptr ss:[ebp+0x14 ] 75 DA9A16 FF75 10 push dword ptr ss:[ebp+0x10 ] 75 DA9A19 FF75 0 C push dword ptr ss:[ebp+0xC ] 75 DA9A1C FF75 08 push dword ptr ss:[ebp+0x8 ] 75 DA9A1F FF15 C411DA7 call dword ptr ds:[<&ntdll.NtReadVirtualMemory>] ; ntdll.ZwReadVirtualMemory
在 <&ntdll.NtReadVirtualMemory>
中调用 ntdll.KiFastSystemCall
函数,这里往eax里存放了一个编号,对应在内核中ReadProcessMemory
的实现,在 0x7FFE0300
处存放了一个函数指针,该函数指针决定了以什么方式进入0环(中断/快速调用)
1 2 3 77 A162F8 B8 15010000 mov eax,0x115 77 A162FD BA 0003F E7F mov edx,0x7FFE0300 77 A16302 FF12 call dword ptr ds:[edx] ; ntdll.KiFastSystemCall
在 ntdll.KiFastSystemCall
中 调用 sysenter
1 2 3 77 A170B0 8B D4 mov edx,esp 77 A170B2 0F 34 sysenter 77 A170B4 C3 retn
ida 其实在ida里面整个调用链会更加清晰,首先定位到ReadProcessMemory
可以发现,在调用NtReadVirtualMemory
之前会往参数里面压入5个值
再到Imports
模块继续跟NtProtectVirtualMemory
可以发现是调用了ntdll.dll
那么我们再到ntdll.dll
里面定位,因为这里我直接拿的win10的ntdll.dll
,在win10里面NtProtectVirtualMemory
和ZwProtectVirtualMemory
是同一个函数,可以看到这个地方首先也是将内核函数的编号给了eax,然后将函数指针存入edx,该函数指针决定了是以中断方式还是快速调用方式进入0环,然后再调用Wow64SystemServiceCall()
思路 虽然这里因为系统的原因最后调用的函数不同,但是实现的方法都是相同的。因为是在xp里面进行实验,这里就用od里面的调用进行分析实现
我们希望可以在自己的代码中直接使用 sysenter
,但经过编写发现其并没有提供这种指令。因此在sysenter
无法直接使用的情况下,只能去调用ntdll.KiFastSystemCall
函数
ntdll.KiFastSystemCall
函数需要借助ntdll.NtReadVirtualMemory
传递过来的参数,然后执行call指令。我们并不希望执行call指令执行,因为执行call指令意味着又上了一层。我们希望自己的代码中直接传递参数,并且直接调用调用ntdll.KiFastSystemCall
函数。因此我们需要模拟call指令,call指令的本质就是将返回地址入栈,并跳转。所以我们不需要跳转,只需要将返回地址入栈(四个字节 使用 sub esp,4
模拟)
我们内嵌汇编代码后,需要手动平衡栈,我们只需要分析esp改变了多少(push、pop以及直接对esp的计算)。经过分析共减少了24字节,所以代码最后应该有 add esp,0x18
来平衡栈
实现 代码如下
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 #include "stdafx.h" #include <Windows.h> void MyReadMemory (HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet) { _asm { lea eax, [ebp + 0x14 ] push eax push [ebp + 0x14 ] push [ebp + 0x10 ] push [ebp + 0xC ] push [ebp + 0x8 ] sub esp,4 mov eax, 0x115 mov edx, 0X7FFE0300 call dword ptr [edx] add esp, 0x18 } } int main () { HANDLE hProcess = 0 ; int t = 123 ; DWORD pBuffer; MyReadMemory ((HANDLE)-1 , (PVOID)&t, &pBuffer, sizeof (int ), 0 ); printf ("MyReadMemory : %x\n" , pBuffer); ReadProcessMemory ((HANDLE)-1 , &t, &pBuffer, sizeof (int ), 0 ); printf ("ReadProcessMemory : %x\n" , pBuffer); getchar (); return 0 ; }
实现效果如下,可以看到我们自己实现的函数跟调用ReadProcessMemory
输出的结果是相同的
拓展 再看下WriteProcessMemory
,还是调用了ntdll.dll
的NtProtectVirtualMemory
跟到NtProtectVirtualMemory
后发现跟ReadProcessMemory
的结构相同
那么也可以进行WriteProcessMemory
的重写
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 #include "stdafx.h" #include <windows.h> void MyWriteProcessMemory (HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten) { _asm { lea eax,[ebp + 0x18 ] push eax push [ebp + 0x14 ] push [ebp + 0x10 ] push [ebp + 0xC ] push [ebp + 0x8 ] sub esp,4 mov eax, 0x115 mov edx,0x7FFE0300 call dword ptr [edx] add esp,0x18 } } int main (int argc, char * argv[]) { char szBuffer[10 ] = "Drunkmars" ; char InBuffer[10 ] = {0 }; SIZE_T size = 0 ; WriteProcessMemory ((HANDLE)-1 ,InBuffer,szBuffer,sizeof (szBuffer)9 ,&size); printf ("WriteProcessMemory : %s\n" ,InBuffer); MyWriteProcessMemory ((HANDLE)-1 ,InBuffer,szBuffer,sizeof (szBuffer),&size); printf ("MyWriteProcessMemory : %s\n" ,InBuffer); return 0 ; }
也跟WriteProcessMemory
所打印出的效果相同
进阶 在前面我们是直接通过间接call 0x7FFE0300
这个地址,来实现进入ring0的效果,我们继续探究
_KUSER_SHARED_DATA 在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA
结构区域,用于 User 层和 Kernel 层共享某些数据,它们使用固定的地址值映射,_KUSER_SHARED_DATA
结构区域在 User 和 Kernel 层地址分别为:
User 层地址为:0x7ffe0000
Kernnel 层地址为:0xffdf0000
虽然指向的是同一个物理页,但在ring3层是只读的,在ring0层是可写的
在0x30偏移处SystemCall
存放的地址就是真正进入ring0的实现方法
我们跟进去看看,这里有两个函数,一个是KiFastSystemCall
即快速调用,一个是KiIntSystemCall
。因为在系统版本的原因,一些操作系统并不支持快速调用进ring0的指令,这时候就会使用到KiIntSystemCall
,即中断门的形式进入ring0
1 2 3 4 5 6 7 8 9 10 11 12 kd> u 0x7c92e4f0 ntdll!KiFastSystemCall: 7 c92e4f0 8b d4 mov edx,esp7 c92e4f2 0f 34 sysenterntdll!KiFastSystemCallRet: 7 c92e4f4 c3 ret7 c92e4f5 8 da42400000000 lea esp,[esp]7 c92e4fc 8 d642400 lea esp,[esp]ntdll!KiIntSystemCall: 7 c92e500 8 d542408 lea edx,[esp+8 ]7 c92e504 cd2e int 2 Eh7 c92e506 c3 ret
那么我们该如何判断当前系统是否支持快速调用呢?
当通过eax=1来执行cpuid指令时,处理器的特征信息被放在ecx和edx寄存器中,其中edx包含了一个SEP位(11位),该位指明了当前处理器是否支持sysenter/sysexit
指令,进入od使用cpuid
指令,这里为了方便查看寄存器的变化把eax置1,ecx和edx置0
执行命令后,这里的edx为BFEBFBFF
,拆完edx后,SEP位为1,证明支持sysenter/sysexit
,即调用ntdll.dll!KiFastSystemCall()
这个函数进入ring0
也可以在ida里面查看这两个函数
进0环需要更改CS、SS、ESP、EIP四个寄存器
CS的权限由3变为0 意味着需要新的CS
SS与CS的权限永远一致 需要新的SS
权限发生切换的时候,堆栈也一定会切换,需要新的ESP
进0环后代码的位置,需要EIP
首先看一下中断门,通过0x2E
的中断号最终进入了KiSystemService
这个内核模块
如果通过sysenter,即快速调用进入内核。中断门进0环,需要的CS、EIP在IDT表中,需要查内存(SS与ESP由TSS提供)
而CPU如果支持sysenter指令时,操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质是一样的
我们在三环执行的api无非是一个接口,真正执行的功能在内核实现,我们便可以直接重写三环api,直接sysenter进内核,这样可以规避所有三环hook。
API通过中断门进0环:
固定中断号为0x2E,CS/EIP由门描述符提供 ESP/SS由TSS提供,进入0环后执行的内核函数:NT!KiSystemService
API通过sysenter指令进0环:
CS/ESP/EIP由MSR寄存器提供(SS是算出来的),进入0环后执行的内核函数:NT!KiFastCallEntry
代码实现 因为这里_asm
不支持 sysenter
指令,可以用 _emit
代替,在模拟调用CALL [0x7FFE0300]
这条指令的时候需要填入调用函数的真实地址,否则会报错0xC0000005
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 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 #include "stdafx.h" #include <windows.h> BOOL __stdcall MyReadProcessMemory_IntGate (HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet) { LONG NtStatus; __asm { lea edx,hProcess; mov eax, 0xBA ; int 0x2E ; mov NtStatus, eax; } if (dwSizeRet != NULL ) { *dwSizeRet = dwSize; } if (NtStatus < 0 ) { return FALSE; } return TRUE; } BOOL __stdcall MyReadProcessMemory_sysenter (HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet) { LONG NtStatus; __asm { lea eax,[ebp + 0x18 ] push eax push [ebp + 0x14 ] push [ebp + 0x10 ] push [ebp + 0xC ] push [ebp + 0x8 ] sub esp, 4 ; mov eax, 0xBA ; push 0x004010EC ; mov edx, esp; _emit 0x0F ; _emit 0x34 ; NtReadVirtualMemoryReturn: add esp, 0xBA ; mov NtStatus, eax; } if (dwSizeRet != NULL ) { *dwSizeRet = dwSize; } if (NtStatus < 0 ) { return FALSE; } return TRUE; } BOOL __stdcall MyWriteProcessMemory_IntGate (HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten) { LONG NtStatus; _asm { lea edx,hProcess; mov eax, 0x115 ; int 0x2E ; mov NtStatus, eax; } if (lpNumberOfBytesWritten != NULL ) { *lpNumberOfBytesWritten = nSize; } if (NtStatus < 0 ) { return FALSE; } return TRUE; } BOOL __stdcall MyWriteProcessMemory_sysenter (HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten) { LONG NtStatus; _asm { lea eax,[ebp + 0x18 ] push eax push [ebp + 0x14 ] push [ebp + 0x10 ] push [ebp + 0xC ] push [ebp + 0x8 ] sub esp,4 mov eax, 0x115 push 0x004011F9 ; mov edx, esp; _emit 0x0F ; _emit 0x34 ; NtWriteVirtualMemoryReturn: add esp, 0x18 ; mov NtStatus, eax; } if (lpNumberOfBytesWritten != NULL ) { *lpNumberOfBytesWritten = nSize; } if (NtStatus < 0 ) { return FALSE; } return TRUE; } int main (int argc, char * argv[]) { char szBuffer[10 ] = "Drunkmars" ; char InBuffer[10 ] = {0 }; SIZE_T size = 0 ; HANDLE hProcess = 0 ; int t = 123 ; DWORD pBuffer, dwRead; ReadProcessMemory ((HANDLE)-1 , &t, &pBuffer, sizeof (int ), &dwRead); printf ("ReadProcessMemory : %x\n" , pBuffer); MyReadProcessMemory_IntGate ((HANDLE)-1 , &t, &pBuffer, sizeof (int ), &dwRead); printf ("MyReadProcessMemory_IntGate : %x\n" , pBuffer); MyReadProcessMemory_sysenter ((HANDLE)-1 , &t, &pBuffer, sizeof (int ), &dwRead); printf ("MyReadProcessMemory_sysenter : %x\n" , pBuffer); WriteProcessMemory ((HANDLE)-1 ,InBuffer,szBuffer,sizeof (szBuffer),&size); printf ("WriteProcessMemory : %s\n" ,InBuffer); MyWriteProcessMemory_IntGate ((HANDLE)-1 ,InBuffer,szBuffer,sizeof (szBuffer),&size); printf ("MyWriteProcessMemory_IntGate : %s\n" ,InBuffer); MyWriteProcessMemory_sysenter ((HANDLE)-1 ,InBuffer,szBuffer,sizeof (szBuffer),&size); printf ("MyWriteProcessMemory_sysenter : %s\n" ,InBuffer); getchar (); return 0 ; }
实现效果如下