截取系统 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  }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值