用户层下API的逆向分析及重构

Windows所提供给R3环的API,实质就是对操作系统接口的封装,其实现部分都是在R0实现的。很多恶意程序会利用钩子来钩取这些API,从而达到截取内容,修改数据的意图。现在我们使用ollydbg对ReadProcessMemory进行跟踪分析,查看其在R3的实现。

0x00前言

Windows所提供给R3环的API,实质就是对操作系统接口的封装,其实现部分都是在R0实现的。很多恶意程序会利用钩子来钩取这些API,从而达到截取内容,修改数据的意图。现在我们使用ollydbg对ReadProcessMemory进行跟踪分析,查看其在R3的实现。

0x01测试

od

我们首先在od里面跟一下在ring3层ReadProcessMemory的调用过程

首先在 exe 中 调用 kernel32.ReadProcessMemory函数,我们可以看到这一部分主要是call dword ptr ds:[<&amp;KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory这一行代码比较关键,调用了kernel32.ReadProcessMemory,继续往里面跟

  1.   01314E3E 8BF4 mov esi,esp
  2.   01314E40 6A 00 push 0x0
  3.   01314E42 6A 04 push 0x4
  4.   01314E44 8D45 DC lea eax,dword ptr ss:[ebp-0x24]
  5.   01314E47 50 push eax
  6.   01314E48 8B4D C4 mov ecx,dword ptr ss:[ebp-0x3C]
  7.   01314E4B 8D548D E8 lea edx,dword ptr ss:[ebp+ecx*4-0x18]
  8.   01314E4F 52 push edx
  9.   01314E50 6A FF push -0x1
  10.   01314E52 FF15 64B0310 call dword ptr ds:[<&amp;KERNEL32.ReadProcessMemory>]; kernel32.ReadProcessMemory
  11.   01314E58 3BF4 cmp esi,esp

ReadProcessMemory函数 中调用 jmp.&amp;API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory> 函数,在kenel32.dll中,mov edi,edi 是用于热补丁技术所保留的,这段代码仔细看其实除了jmp什么也没干,继续跟jmp

  1. 7622C1CE 8BFF mov edi,edi
  2. 7622C1D0 55 push ebp
  3. 7622C1D1 8BEC mov ebp,esp
  4. 7622C1D3 5D pop ebp
  5. 7622C1D4 E9 F45EFCFF jmp <jmp.&amp;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. 761F20CD FF25 0C191F7
  2. jmp dword ptr ds:[<&amp;API-MS-Win-Core-Memory-L1-1-0.ReadProcessMemory>; KernelBase.ReadProcessMemory

KernelBase.ReadProcessMemory中 调用 <&amp;ntdll.NtReadVirtualMemory> 函数,将ReadProcessMemory中传入的参数再次入栈,调用ntdll.ZwReadVirtualMemory函数,再往里面走

  1.   75DA9A0A 8BFF mov edi,edi
  2.   75DA9A0C 55 push ebp
  3.   75DA9A0D 8BEC mov ebp,esp
  4.   75DA9A0F 8D45 14 lea eax,dword ptr ss:[ebp+0x14]
  5.   75DA9A12 50 push eax
  6.   75DA9A13 FF75 14 push dword ptr ss:[ebp+0x14]
  7.   75DA9A16 FF75 10 push dword ptr ss:[ebp+0x10]
  8.   75DA9A19 FF75 0C push dword ptr ss:[ebp+0xC]
  9.   75DA9A1C FF75 08 push dword ptr ss:[ebp+0x8]
  10.   75DA9A1F FF15 C411DA7
  11. call dword ptr ds:[<&amp;ntdll.NtReadVirtualMemory>] ; ntdll.ZwReadVirtualMemory

<&amp;ntdll.NtReadVirtualMemory> 中调用 ntdll.KiFastSystemCall 函数,这里往eax里存放了一个编号,对应在内核中ReadProcessMemory的实现,在 0x7FFE0300处存放了一个函数指针,该函数指针决定了以什么方式进入0环(中断/快速调用)

  1.   77A162F8 B8 15010000 mov eax,0x115 // 对应操作系统内核中某一函数的编号
  2.   77A162FD BA 0003FE7F mov edx,0x7FFE0300 // 该地方是一个函数,该函数决定了什么方式进零环
  3.   77A16302 FF12 call dword ptr ds:[edx] ; ntdll.KiFastSystemCall

ntdll.KiFastSystemCall 中 调用 sysenter

  1.   77A170B0 8BD4 mov edx,esp
  2.   77A170B2 0F34 sysenter
  3.   77A170B4 C3 retn

ida

其实在ida里面整个调用链会更加清晰,首先定位到ReadProcessMemory可以发现,在调用NtReadVirtualMemory之前会往参数里面压入5个值

再到Imports模块继续跟NtProtectVirtualMemory可以发现是调用了ntdll.dll

那么我们再到ntdll.dll里面定位,因为这里我直接拿的win10的ntdll.dll,在win10里面NtProtectVirtualMemoryZwProtectVirtualMemory是同一个函数,可以看到这个地方首先也是将内核函数的编号给了eax,然后将函数指针存入edx,该函数指针决定了是以中断方式还是快速调用方式进入0环,然后再调用Wow64SystemServiceCall()

0x02

虽然这里因为系统的原因最后调用的函数不同,但是实现的方法都是相同的。因为是在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 来平衡栈

0x03实现

代码如下

  1. // MyReadMemory.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <Windows.h>
  5. void MyReadMemory(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet)
  6. {
  7. _asm
  8. {
  9. lea eax, [ebp + 0x14]
  10. push eax //dwSizeRet
  11. push [ebp + 0x14] //dwSize
  12. push [ebp + 0x10] //pBuffer
  13. push [ebp + 0xC] //pAddr
  14. push [ebp + 0x8] //hProcess
  15. sub esp,4 //平衡 call NtReadProcessMemory 堆栈
  16. mov eax, 0x115
  17. mov edx, 0X7FFE0300
  18. call dword ptr [edx]
  19. add esp, 0x18
  20. }
  21. }
  22. int main()
  23. {
  24. HANDLE hProcess = 0;
  25. int t = 123;
  26. DWORD pBuffer;
  27. MyReadMemory((HANDLE)-1, (PVOID)&amp;t, &amp;pBuffer, sizeof(int), 0);
  28. printf("MyReadMemory : %x\n", pBuffer);
  29. ReadProcessMemory((HANDLE)-1, &amp;t, &amp;pBuffer, sizeof(int), 0);
  30. printf("ReadProcessMemory : %x\n", pBuffer);
  31. getchar();
  32. return 0;
  33. }

实现效果如下,可以看到我们自己实现的函数跟调用ReadProcessMemory输出的结果是相同的

0x04拓展

再看下WriteProcessMemory,还是调用了ntdll.dllNtProtectVirtualMemory

跟到NtProtectVirtualMemory后发现跟ReadProcessMemory的结构相同

那么也可以进行WriteProcessMemory的重写

  1. // MyWriteProcessMemory.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <windows.h>
  5. void MyWriteProcessMemory(HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten)
  6. {
  7. _asm
  8. {
  9. lea eax,[ebp + 0x18]
  10. push eax //lpNumberOfBytesWritten
  11. push [ebp + 0x14] //nSize
  12. push [ebp + 0x10] //lpBuffer
  13. push [ebp + 0xC] //lpBaseAddress
  14. push [ebp + 0x8] //hProcess
  15. sub esp,4 //平衡 call NtWriteProcessMemory 堆栈
  16. mov eax, 0x115
  17. mov edx,0x7FFE0300
  18. call dword ptr [edx]
  19. add esp,0x18
  20. }
  21. }
  22. int main(int argc, char* argv[])
  23. {
  24. char szBuffer[10] = "Drunkmars";
  25. char InBuffer[10] = {0};
  26. SIZE_T size = 0;
  27. WriteProcessMemory((HANDLE)-1,InBuffer,szBuffer,sizeof(szBuffer)9,&amp;size);
  28. printf("WriteProcessMemory : %s\n",InBuffer);
  29. MyWriteProcessMemory((HANDLE)-1,InBuffer,szBuffer,sizeof(szBuffer),&amp;size);
  30. printf("MyWriteProcessMemory : %s\n",InBuffer);
  31. return 0;
  32. }

也跟WriteProcessMemory所打印出的效果相同

0x05进阶

在前面我们是直接通过间接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. kd> u 0x7c92e4f0
  2. ntdll!KiFastSystemCall:
  3. 7c92e4f0 8bd4 mov edx,esp
  4. 7c92e4f2 0f34 sysenter
  5. ntdll!KiFastSystemCallRet:
  6. 7c92e4f4 c3 ret
  7. 7c92e4f5 8da42400000000 lea esp,[esp]
  8. 7c92e4fc 8d642400 lea esp,[esp]
  9. ntdll!KiIntSystemCall:
  10. 7c92e500 8d542408 lea edx,[esp+8]
  11. 7c92e504 cd2e int 2Eh
  12. 7c92e506 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

0x06代码实现

因为这里_asm不支持 sysenter指令,可以用 _emit 代替,在模拟调用CALL [0x7FFE0300]这条指令的时候需要填入调用函数的真实地址,否则会报错0xC0000005

  1. // sysenter.cpp : Defines the entry point for the console application.
  2. //
  3. #include "stdafx.h"
  4. #include <windows.h>
  5. BOOL __stdcall MyReadProcessMemory_IntGate(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet)
  6. {
  7. LONG NtStatus;
  8. __asm
  9. {
  10. // 直接模拟 KiIntSystemCall
  11. lea edx,hProcess; // 要求 edx 存储最后入栈的参数
  12. mov eax, 0xBA;
  13. int 0x2E;
  14. mov NtStatus, eax;
  15. }
  16. if (dwSizeRet != NULL)
  17. {
  18. *dwSizeRet = dwSize;
  19. }
  20. if (NtStatus < 0)
  21. {
  22. return FALSE;
  23. }
  24. return TRUE;
  25. }
  26. BOOL __stdcall MyReadProcessMemory_sysenter(HANDLE hProcess, PVOID pAddr, PVOID pBuffer, DWORD dwSize, DWORD *dwSizeRet)
  27. {
  28. LONG NtStatus;
  29. __asm
  30. {
  31. // 模拟 ReadProcessMemory
  32. lea eax,[ebp + 0x18]
  33. push eax //dwSizeRet
  34. push [ebp + 0x14] //dwSize
  35. push [ebp + 0x10] //pBuffer
  36. push [ebp + 0xC] //pAddr
  37. push [ebp + 0x8] //hProcess
  38. sub esp, 4; // 模拟 ReadProcessMemory 里的 CALL NtReadVirtualMemory
  39. // 模拟 NtReadVirtualMemory
  40. mov eax, 0xBA;
  41. push 0x004010EC; // 模拟 NtReadVirtualMemory 函数里的 CALL [0x7FFE0300]
  42. // 模拟 KiFastSystemCall
  43. mov edx, esp;
  44. _emit 0x0F; // sysenter
  45. _emit 0x34;
  46. NtReadVirtualMemoryReturn:
  47. add esp, 0xBA; // 模拟 NtReadVirtualMemory 返回到 ReadProcessMemory 时的 RETN 0x14
  48. mov NtStatus, eax;
  49. }
  50. if (dwSizeRet != NULL)
  51. {
  52. *dwSizeRet = dwSize;
  53. }
  54. // 错误检查
  55. if (NtStatus < 0)
  56. {
  57. return FALSE;
  58. }
  59. return TRUE;
  60. }
  61. BOOL __stdcall MyWriteProcessMemory_IntGate(HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten)
  62. {
  63. LONG NtStatus;
  64. _asm
  65. {
  66. lea edx,hProcess;
  67. mov eax, 0x115;
  68. int 0x2E;
  69. mov NtStatus, eax;
  70. }
  71. if (lpNumberOfBytesWritten != NULL)
  72. {
  73. *lpNumberOfBytesWritten = nSize;
  74. }
  75. if (NtStatus < 0)
  76. {
  77. return FALSE;
  78. }
  79. return TRUE;
  80. }
  81. BOOL __stdcall MyWriteProcessMemory_sysenter(HANDLE hProcess,LPVOID lpBaseAddress,LPVOID lpBuffer,DWORD nSize,LPDWORD lpNumberOfBytesWritten)
  82. {
  83. LONG NtStatus;
  84. _asm
  85. {
  86. lea eax,[ebp + 0x18]
  87. push eax //lpNumberOfBytesWritten
  88. push [ebp + 0x14] //nSize
  89. push [ebp + 0x10] //lpBuffer
  90. push [ebp + 0xC] //lpBaseAddress
  91. push [ebp + 0x8] //hProcess
  92. sub esp,4 //平衡 call NtWriteProcessMemory 堆栈
  93. mov eax, 0x115
  94. push 0x004011F9; // 模拟 NtWriteVirtualMemory 函数里的 CALL [0x7FFE0300]
  95. // 模拟 KiFastSystemCall
  96. mov edx, esp;
  97. _emit 0x0F; // sysenter
  98. _emit 0x34;
  99. NtWriteVirtualMemoryReturn:
  100. add esp, 0x18; // 模拟 NtWriteVirtualMemory 返回到 WriteProcessMemory 时的 RETN 0x14
  101. mov NtStatus, eax;
  102. }
  103. if (lpNumberOfBytesWritten != NULL)
  104. {
  105. *lpNumberOfBytesWritten = nSize;
  106. }
  107. if (NtStatus < 0)
  108. {
  109. return FALSE;
  110. }
  111. return TRUE;
  112. }
  113. int main(int argc, char* argv[])
  114. {
  115. char szBuffer[10] = "Drunkmars";
  116. char InBuffer[10] = {0};
  117. SIZE_T size = 0;
  118. HANDLE hProcess = 0;
  119. int t = 123;
  120. DWORD pBuffer, dwRead;
  121. ReadProcessMemory((HANDLE)-1, &amp;t, &amp;pBuffer, sizeof(int), &amp;dwRead);
  122. printf("ReadProcessMemory : %x\n", pBuffer);
  123. MyReadProcessMemory_IntGate((HANDLE)-1, &amp;t, &amp;pBuffer, sizeof(int), &amp;dwRead);
  124. printf("MyReadProcessMemory_IntGate : %x\n", pBuffer);
  125. MyReadProcessMemory_sysenter((HANDLE)-1, &amp;t, &amp;pBuffer, sizeof(int), &amp;dwRead);
  126. printf("MyReadProcessMemory_sysenter : %x\n", pBuffer);
  127. WriteProcessMemory((HANDLE)-1,InBuffer,szBuffer,sizeof(szBuffer),&amp;size);
  128. printf("WriteProcessMemory : %s\n",InBuffer);
  129. MyWriteProcessMemory_IntGate((HANDLE)-1,InBuffer,szBuffer,sizeof(szBuffer),&amp;size);
  130. printf("MyWriteProcessMemory_IntGate : %s\n",InBuffer);
  131. MyWriteProcessMemory_sysenter((HANDLE)-1,InBuffer,szBuffer,sizeof(szBuffer),&amp;size);
  132. printf("MyWriteProcessMemory_sysenter : %s\n",InBuffer);
  133. getchar();
  134. return 0;
  135. }

实现效果如下

  • 发表于 2022-02-28 09:45:59
  • 阅读 ( 6663 )
  • 分类:漏洞分析

0 条评论

szbuffer
szbuffer

30 篇文章

站长统计