截取系统 API 调用

截取系统 API 调用
在很多情况下,为了测试代码或扩展操作系统的功能,软件开发人员或测试人员必须截取系统函数调用。有一些软件包能够提供该功能,如微软公司的 Detours* 库,或 OK Thinking Software 的 Syringe*。但是从另一个角度而言,开发人员可能希望不需借助第三方软件,自己就能实现该功能。

本文描述了函数截取的几种不同方式,并详细介绍了无需使用商业软件包,也不需受 GNU*(通用公共许可证)许可的约束,就能够实现该功能的一种通用方法。本文所有材料由英特尔公司开发,或根据 MSDN* 样本代码修改而来。

截取系统函数调用的两项基本技术
大部分截取任意函数调用的方法都是准备一个 DLL,用来替代将被截取的目标函数,然后将 DLL 注入至目标进程;在与目标进程连接的基础上, DLL 将自己与目标函数相连。这种技术之所以适合此任务,是因为在大多数情况下我们无法获得目标应用程序的源代码,而这种技术只需相对简单地编写一个包含代换函数的 DLL,就可将其与软件的其它部分分离开来。

两种截取方法已经过研究和分析。Syringe 通过修改函数输入条目( thunking 表)运行。而Detours 库则直接修改目标函数( 在目标进程空间内),并无条件地跳转至代换函数。此外,它还提供能够调用原始函数的 trampoline 函数。

Detours 技术之所以采用后一种方法,是因为在许多情况下,Syringe 无法找到 thunk,并且它不能提供 trampoline功能来调用原始函数。在这两种方法下,注入 DLL 的工作方式相同。

截取系统函数调用的全部工作流程如下所示:

DLL 注入 — 首先,主软件打开目标进程,并使其加载包含代换函数的 DLL

目标函数修改 — 当 DLL 连接至进程时,它在目标进程空间内修改目标函数,从而直接跳转至 DLL 中的代换函数。Trampoline 函数能够随意调用原始函数。

目标函数截取 — 当调用目标函数时,它直接跳转至 DLL 中的代换函数。如果开发人员希望调用原始的功能,则他或她就可以调用 trampoline 函数。
DLL 注入
本节内容完全以 MSDN 文章“ 定制调试诊断工具和实用程序 — 摆脱 DLL“地狱”   (DLL Hell)*”为基础,该文章还包括可下载的源代码。在本文附录中可获得 Inject.cppInject.h 。已对它们进行了定制以便于集成——仅需将其包括在项目中然后调用 InjectLib 即可。使目标进程加载 DLL 的算法按如下步骤工作:

通过调用 OpenProcess 打开目标进程。

通过调用 VirtualAllocEx 在目标进程中分配内存。利用 WriteProcessMemory 将要被注入的 DLL 名称写入分配的内存。

通过调用 GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW") 来获取 LoadLibrary 的地址;

调用 CreateRemoteThread,指定 LoadLibrary 的入口点,并将 DLL (第 2 步中) 的名称作为其自变量。目标进程将加载 DLL

利用 VirtualFreeEx 释放分配的内存。已不再需要该内存。


Inject.cpp 融合了包括稳固的安全特性等大量其它功能,但上述步骤已足够阐明其核心概念。

目标函数修改
目标函数修改为自我修改代码,尽管在将 jmp 注入进程内存的过程中存在一些缺陷,但它在  MSDN*  上具有完善的文件证明。为避免混淆,本节列出了几乎全部的样本代码。

目标函数修改的两个主要方面为代换函数和 trampoline 函数。下面的代码片断为截取 GetSystemPowerStatus API 的 DLL 示例:
这段代码为连接所做的第一件事就是调用 InterceptAPI。需要使用包含目标函数的模块名称、目标函数的名称以及代换函数的地址。 GetSystemPowerStatus 位于 kernel32.dll 中。其它基本的 Win32* API,如 MessageBoxPeekMessage,都能够在 user32.dll 中获得。MSDN 指定每个 API 所属的模块;未来的增强版中,将自动为给定的 API 找到正确的模块。

InterceptAPI 将目标函数的前五个字节覆盖为无条件跳转( opcode 0xE9),后面为四个字节的带符号整数(向代换函数的位移)。位移从下一个指令开始;因此需要使用 pbReplaced - (pbTargetCode +4)。进行该代码操作时,需要注意以下两点:

将区域覆盖的保护模式改为 VirtualProtect。否则,将发生非法访问错误。

必须使用 FlushInstructionCache 来支持指令已存在于高速缓存中的情况。否则,即使内存中的指令已经有所变化,旧代码仍将在高速缓存中运行。

现在,当调用 GetSystemPowerStatus 函数时,它跳转至代换函数,然后直接返回调用方,成功截取调用。
Trampoline 函数
在很多情况下,代换函数除使用自身代码外,还需调用原始目标函数,这样就能够扩展 API 的功能,而不是替换整个 API。Trampoline 函数可以提供该功能。Trampoline 函数的原理如下所示:

编写一个具有相同声明的哑元函数(dummy function),将作为 trampoline 使用。确保哑元函数的长度超过 10 个字节。

在覆盖目标函数的前五个字节之前,将它们复制到 trampoline 函数的起始处。

利用无条件跳转,将trampoline的第六个字节覆盖为目标函数的第六个字节。

与之前一样覆盖目标函数。

当从代换函数或其它地方调用 trampoline 函数时,它执行复制出的原始代码的前五个字节,然后跳转至实际原始代码的第六个字节。控制返回至 trampoline 的调用方。当完成其它任务时,控制返回至 API 的控制方。

可能存在一种复杂的情况,即原始代码的第六个字节可能是先前指令的一部分。在这种情况下,函数会覆盖部分先前指令,然后崩溃。在 GetSystemPowerStatus 的情况中,前五个字节后的新指令开始于第七个字节。因此,对于这种工作机制,需要将六个字节复制到 trampoline,并且代码必须相应地调整这个偏移量。

代码需要复制的字节数取决于 API。查看原始目标代码( 利用调试器或反汇编器)并计算需要复制的字节数是非常必要的。未来的增强版将自动检测正确的偏移量。假设我们已经知道正确的偏移量,下面的代码则显示出可建立 trampoline 函数的可扩展 InterceptAPI 函数:

 1  BOOL InterceptAPI(HMODULE hLocalModule,  const   char *  c_szDllName,  const   char *  c_szApiName,
 2          DWORD dwReplaced, DWORD dwTrampoline,  int  offset)
 3  {
 4       int  i;
 5      DWORD dwOldProtect;
 6          DWORD dwAddressToIntercept  =  (DWORD)GetProcAddress(
 7              GetModuleHandle(( char * )c_szDllName), ( char * )c_szApiName);
 8 
 9      BYTE  * pbTargetCode  =  (BYTE  * ) dwAddressToIntercept;
10      BYTE  * pbReplaced  =  (BYTE  * ) dwReplaced;
11      BYTE  * pbTrampoline  =  (BYTE  * ) dwTrampoline;
12 
13       //  Change the protection of the trampoline region
14       //  so that we can overwrite the first 5 + offset bytes.
15      VirtualProtect(( void   * ) dwTrampoline,  5 + offset, PAGE_WRITECOPY,  & dwOldProtect);
16       for  (i = 0 ;i < offset;i ++ )
17           * pbTrampoline ++   =   * pbTargetCode ++ ;
18      pbTargetCode  =  (BYTE  * ) dwAddressToIntercept;
19 
20       //  Insert unconditional jump in the trampoline.
21       * pbTrampoline ++   =   0xE9 ;         //  jump rel32
22       * ((signed  int   * )(pbTrampoline))  =  (pbTargetCode + offset)  -  (pbTrampoline  +   4 );
23      VirtualProtect(( void   * ) dwTrampoline,  5 + offset, PAGE_EXECUTE,  & dwOldProtect);
24      
25       //  Overwrite the first 5 bytes of the target function
26      VirtualProtect(( void   * ) dwAddressToIntercept,  5 , PAGE_WRITECOPY,  & dwOldProtect);
27       * pbTargetCode ++   =   0xE9 ;         //  jump rel32
28       * ((signed  int   * )(pbTargetCode))  =  pbReplaced  -  (pbTargetCode  + 4 );
29      VirtualProtect(( void   * ) dwAddressToIntercept,  5 , PAGE_EXECUTE,  & dwOldProtect);
30      
31       //  Flush the instruction cache to make sure 
32       //  the modified code is executed.
33      FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
34       return  TRUE;
35  }
36 

结论
本文描述了截取系统函数调用的一种通用方法,同时还提供了 trampoline 函数,从而保留了原始功能。本文仅对方法进行简要描述,并未对完整的软件包作出说明,因此如下一些细节并没有实现:

自动检测包含目标 API 的模块。

自动检测 trampoline 函数的偏移量。

删除代换函数,并注入 DLL。(到目前为止,清空代换函数的唯一方法是关闭应用程序。

然而,对于开发人员而言,无需依赖第三方软件包,执行截取任意系统函数调用的软件,本文中涉及的技术、说明及源代码已经足够。

  1  #include  " stdafx.h "
  2  #include  " Inject.h "
  3 
  4  #include  < tchar.h >
  5  #include  < malloc.h >      //  For alloca
  6  #include  < pi.h >
  7 
  8  #ifdef UNICODE
  9  #define  InjectLib InjectLibW
 10  #else
 11  #define  InjectLib InjectLibA
 12  #endif     //  !UNICODE
 13 
 14  BOOL AdjustDacl(HANDLE h, DWORD DesiredAccess)
 15  {
 16       //  the WORLD Sid is trivial to form programmatically (S-1-1-0)
 17      SID world  =  { SID_REVISION,  1 , SECURITY_WORLD_SID_AUTHORITY,  0  };
 18      
 19      EXPLICIT_ACCESS ea  =
 20      {
 21          DesiredAccess,
 22              SET_ACCESS,
 23              NO_INHERITANCE,
 24          {
 25               0 , NO_MULTIPLE_TRUSTEE,
 26                  TRUSTEE_IS_SID,
 27                  TRUSTEE_IS_USER,
 28                  reinterpret_cast < LPTSTR > ( & world)
 29          }
 30      };
 31      ACL *  pdacl  =   0 ;
 32      DWORD err  =  SetEntriesInAcl( 1 & ea,  0 & pdacl);
 33       if  (err  ==  ERROR_SUCCESS)
 34      {
 35          err  =  SetSecurityInfo(h, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,  0 0 , pdacl,  0 );
 36          LocalFree(pdacl);
 37           return (err  ==  ERROR_SUCCESS);
 38      }
 39       else
 40           return (FALSE);
 41  }
 42 
 43  //  Useful helper function for enabling a single privilege
 44  BOOL EnableTokenPrivilege(HANDLE htok, LPCTSTR szPrivilege, TOKEN_PRIVILEGES &  tpOld)
 45  {
 46      TOKEN_PRIVILEGES tp;
 47      tp.PrivilegeCount  =   1 ;
 48      tp.Privileges[ 0 ].Attributes  =  SE_PRIVILEGE_ENABLED;
 49       if  (LookupPrivilegeValue( 0 , szPrivilege,  & tp.Privileges[ 0 ].Luid))
 50      {
 51           //  htok must have been opened with the following permissions:
 52           //  TOKEN_QUERY (to get the old priv setting)
 53           //  TOKEN_ADJUST_PRIVILEGES (to adjust the priv)
 54          DWORD cbOld  =   sizeof  tpOld;
 55           if  (AdjustTokenPrivileges(htok, FALSE,  & tp, cbOld,  & tpOld,  & cbOld))
 56               //  Note that AdjustTokenPrivileges may succeed, and yet
 57               //  some privileges weren't actually adjusted.
 58               //  You've got to check GetLastError() to be sure!
 59               return (ERROR_NOT_ALL_ASSIGNED  !=  GetLastError());
 60           else
 61               return (FALSE);
 62      }
 63       else
 64           return (FALSE);
 65  }
 66 
 67 
 68  //  Corresponding restoration helper function
 69  BOOL RestoreTokenPrivilege(HANDLE htok,  const  TOKEN_PRIVILEGES &  tpOld)
 70  {
 71       return (AdjustTokenPrivileges(htok, FALSE, const_cast < TOKEN_PRIVILEGES *> ( & tpOld),  0 0 0 ));
 72  }
 73 
 74  HANDLE GetProcessHandleWithEnoughRights(DWORD PID, DWORD AccessRights)
 75  {
 76      HANDLE hProcess  =  ::OpenProcess(AccessRights, FALSE, PID);
 77       if  (hProcess  ==  NULL)
 78      {
 79          HANDLE hpWriteDAC  =  OpenProcess(WRITE_DAC, FALSE, PID);
 80           if  (hpWriteDAC  ==  NULL)
 81          {
 82               //  hmm, we don't have permissions to modify the DACL
 83               //  time to take ownership
 84              HANDLE htok;
 85               if  ( ! OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY  |  TOKEN_ADJUST_PRIVILEGES,  & htok))
 86                   return (FALSE);
 87              
 88              TOKEN_PRIVILEGES tpOld;
 89               if  (EnableTokenPrivilege(htok, SE_TAKE_OWNERSHIP_NAME, tpOld))
 90              {
 91                   //  SeTakeOwnershipPrivilege allows us to open objects with
 92                   //  WRITE_OWNER, but that's about it, so we'll update the owner,
 93                   //  and dup the handle so we can get WRITE_DAC permissions.
 94                  HANDLE hpWriteOwner  =  OpenProcess(WRITE_OWNER, FALSE, PID);
 95                   if  (hpWriteOwner  !=  NULL)
 96                  {
 97                      BYTE buf[ 512 ];  //  this should always be big enough
 98                      DWORD cb  =   sizeof  buf;
 99                       if  (GetTokenInformation(htok, TokenUser, buf, cb,  & cb))
100                      {
101                          DWORD err  =  
102                              SetSecurityInfo( 
103                              hpWriteOwner, 
104                              SE_KERNEL_OBJECT,
105                              OWNER_SECURITY_INFORMATION,
106                              reinterpret_cast < TOKEN_USER *> (buf) -> User.Sid,
107                               0 0 0  
108                              );
109                           if  (err  ==  ERROR_SUCCESS)
110                          {
111                               //  now that we're the owner, we've implicitly got WRITE_DAC
112                               //  permissions, so ask the system to reevaluate our request,
113                               //  giving us a handle with WRITE_DAC permissions
114                               if  (
115                                   ! DuplicateHandle( 
116                                  GetCurrentProcess(), 
117                                  hpWriteOwner,
118                                  GetCurrentProcess(), 
119                                   & hpWriteDAC,
120                                  WRITE_DAC, FALSE,  0  
121                                  ) 
122                                  )
123                                  hpWriteDAC  =  NULL;
124                          }
125                      }
126                      
127                       //  don't forget to close handle
128                      ::CloseHandle(hpWriteOwner);
129                  }
130                  
131                   //  not truly necessary in this app,
132                   //  but included for completeness
133                  RestoreTokenPrivilege(htok, tpOld);
134              }
135              
136               //  don't forget to close the token handle
137              ::CloseHandle(htok);
138          }
139          
140           if  (hpWriteDAC)
141          {
142               //  we've now got a handle that allows us WRITE_DAC permission
143              AdjustDacl(hpWriteDAC, AccessRights);
144              
145               //  now that we've granted ourselves permission to access 
146               //  the process, ask the system to reevaluate our request,
147               //  giving us a handle with right permissions
148               if  (
149                   ! DuplicateHandle( 
150                  GetCurrentProcess(), 
151                  hpWriteDAC,
152                  GetCurrentProcess(), 
153                   & hProcess,
154                  AccessRights, 
155                  FALSE, 
156                   0  
157                  ) 
158                  )
159                  hProcess  =  NULL;
160              
161              CloseHandle(hpWriteDAC);
162          }
163      }
164      
165       return (hProcess);
166  }
167 
168  BOOL WINAPI InjectLibW(DWORD dwProcessId, PCWSTR pszLibFile) 
169  {
170      BOOL fOk  =  FALSE;  //  Assume that the function fails
171      HANDLE hProcess  =  NULL, hThread  =  NULL;
172      PWSTR pszLibFileRemote  =  NULL;
173      
174       //  Get a handle for the target process.
175      hProcess  =  
176          GetProcessHandleWithEnoughRights(
177          dwProcessId,
178          PROCESS_QUERY_INFORMATION  |     //  Required by Alpha
179          PROCESS_CREATE_THREAD      |     //  For CreateRemoteThread
180          PROCESS_VM_OPERATION       |     //  For VirtualAllocEx/VirtualFreeEx
181          PROCESS_VM_WRITE               //  For WriteProcessMemory
182          );
183       if  (hProcess  ==  NULL)
184           return (FALSE);
185      
186       //  Calculate the number of bytes needed for the DLL's pathname
187       int  cch  =   1   +  lstrlenW(pszLibFile);
188       int  cb   =  cch  *   sizeof (WCHAR);
189      
190       //  Allocate space in the remote process for the pathname
191      pszLibFileRemote  =  
192          (PWSTR) VirtualAllocEx(hProcess, NULL, cb, MEM_COMMIT, PAGE_READWRITE);
193      
194       if  (pszLibFileRemote  !=  NULL)
195      {
196           //  Copy the DLL's pathname to the remote process's address space
197           if  (WriteProcessMemory(hProcess, pszLibFileRemote, 
198              (PVOID) pszLibFile, cb, NULL))
199          {
200               //  Get the real address of LoadLibraryW in Kernel32.dll
201              PTHREAD_START_ROUTINE pfnThreadRtn  =  (PTHREAD_START_ROUTINE)
202                  GetProcAddress(GetModuleHandle(TEXT( " Kernel32 " )),  " LoadLibraryW " );
203               if  (pfnThreadRtn  !=  NULL)
204              {
205                   //  Create a remote thread that calls LoadLibraryW(DLLPathname)
206                  hThread  =  CreateRemoteThread(hProcess, NULL,  0
207                      pfnThreadRtn, pszLibFileRemote,  0 , NULL);
208                   if  (hThread  !=  NULL)
209                  {
210                       //  Wait for the remote thread to terminate
211                      WaitForSingleObject(hThread, INFINITE);
212                      
213                      fOk  =  TRUE;  //  Everything executed successfully
214                      
215                      CloseHandle(hThread);
216                  }
217              }
218          }
219           //  Free the remote memory that contained the DLL's pathname
220          VirtualFreeEx(hProcess, pszLibFileRemote,  0 , MEM_RELEASE);
221      }
222      
223      CloseHandle(hProcess);
224      
225       return (fOk);
226  }
227 
228 
229  BOOL WINAPI InjectLibA(DWORD dwProcessId, PCSTR pszLibFile) {
230      
231       //  Allocate a (stack) buffer for the Unicode version of the pathname
232      PWSTR pszLibFileW  =  (PWSTR) 
233          _alloca((lstrlenA(pszLibFile)  +   1 *   sizeof (WCHAR));
234      
235       //  Convert the ANSI pathname to its Unicode equivalent
236      wsprintfW(pszLibFileW, L " %S " , pszLibFile);
237      
238       //  Call the Unicode version of the function to actually do the work.
239       return (InjectLibW(dwProcessId, pszLibFileW));
240  }
241 

你可能感兴趣的:(截取系统 API 调用)