Windows系统程序设计之插入DLL和挂接API
【作者】北极星2003
【来源】看雪技术论坛(bbs.pediy.com) 
【时间】2006年6月12日
【说明】关于本文的所有信息都可以在附件中下载:InsertDLLAndHOOKAPI.rar

  首先简要介绍一下内容要点。
  在WINDOWS中,每个进程都有自己的私有地址空间,当用指针访问内存的时候只能访问到自己进程的地址空间,而无法进入其他进程的地址空间。而当自己的进程需要对其他进程进行一系列操作的时候,一个比较好的选择就是把这些操作封装在一个DLL中,然后把DLL插入到目标进程的私有地址空间中。这样,如果DLL中使用指针的话,就可以访问目标进程的地址空间。
  在程序中通常有两种方法来实现插入DLL:(1)使用远程线程(2)HOOK

  挂接API的目标是当系统调用API函数的时候能够执行我们的自定义函数,需要注意的是自定义函数的参数、返回类型、调用方式都必须和原API函数完全相同。这样我们就可以很便捷的取得或者修改参数来实现相应的目标,而无需考虑该API的内部实现。
  通常有两种方法可以实现挂接API:(1)修改IAT(2)API入口的覆盖。

  需要说明的是对知识点的简要概括,至于具体信息,无论在书本或者网络上都有很多,不再赘述。
  
  使用远程线程方法实现插入DLL可以参考我以前的文章《QQ盗号的核心技术(简单版)》http://bbs.pediy.com/showthread.php?threadid=14203

  这里主要讲解两个软件的制作。
(1)  IPPack——IP封包截获工具
原理:使用挂钩(HOOK)插入DLL,覆盖API入口地址挂接API
(2)  Watch IAT ——查看IAT信息
关于修改IAT来实现挂接API已经很普遍了,我也懒的再重复。这个工具软件是我很早以前写的,用来查看可执行文件的IAT信息,但有一个难点,由于IAT信息是在加载时候动态产生的,那么什么时候是最佳时机呢??
 
实例一 : IPPack —— IP封包截获工具
 
 
1、设计目标
    把基本的DLL插入技术和HOOK API技术相联系,应用到实际。另外,在该软件中还设计进程间通信、线程同步、DLL实现等多种Windows程序设计中的基本技术。(经常有朋友说这个软件不能截获数据包,这里需要说明的是这个软件仅仅是一个技术实例,并不是工具软件。对目标进程进行测试的时候,最好先用PE分析工具查看一下,确认导入表中有send,recv,sendto,recvfrom这四个函数。)

2、设计思路
  第一步:把DLL插入到目标进程地址空间
   

  第二步:挂接API,取得相关的参数信息并传送到IPPack.exe
      如果不熟悉管道通信机制,可以参考《Windows系统程序设计之进程间通信》
http://bbs.pediy.com/showthread.php...;threadid=26252  

3、难点
  插入DLL和挂接API的应用比较多,基本上没什么难度。
  
4、详细设计
(1)当启动IPPack.exe进行初始化时,启动命名管道线程服务

代码:
UINT ServerThread ( LPVOID lpParameter ) {   ……   while ( true )   {     ConnectNamedPipe ( CurPipeInst.hPipe, &OverLapStruct ) ;     WaitForSingleObject ( CurPipeInst.hEvent, INFINITE ) ;     if ( !GetOverlappedResult ( CurPipeInst.hPipe, &OverLapStruct, &dwByte, true ) )       break ;     ……     // 从管道中读取数据并显示到界面     ……     // 断开客户端的连接,以便等待下一客户的到来     DisconnectNamedPipe ( CurPipeInst.hPipe ) ;   }      return 0 ; }



(2)显示进程列表,需要提升本进程的权限

代码:
void CIPPackDlg::LookUpProcessPriviege () {   TOKEN_PRIVILEGES tkp;    HANDLE hToken;    if (!OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY,&hToken))     return ;    LookupPrivilegeValue(NULL,SE_DEBUG_NAME,&tkp.Privileges[0].Luid);  tkp.PrivilegeCount = 1;    tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;    AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, (PTOKEN_PRIVILEGES)NULL, 0);  }



(3)更新进程列表

代码:
void CSPDlg::UpdateProcess () {   DWORD  cbNeededProcess, cbNeededModule;   HMODULE  hMod ;   CString TempStr ;   EnumProcesses ( dwProcessId, sizeof(dwProcessId), &cbNeededProcess ) ;   for ( unsigned i = 0; i < ( cbNeededProcess/sizeof(DWORD) ); i++ )   {     HANDLE hProcess = OpenProcess( PROCESS_QUERY_INFORMATION |  \                                    PROCESS_VM_READ,  FALSE, dwProcessId[i] );     if ( hProcess != INVALID_HANDLE_VALUE )     {       EnumProcessModules( hProcess, &hMod, sizeof(hMod), &cbNeededModule ) ;       GetModuleBaseName ( hProcess, hMod, szProcessName[i].GetBuffer(512), 512 ) ;       TempStr.Format ( "%d", dwProcessId[i] ) ;       m_ListCon.InsertItem ( i, TempStr, 0 ) ;       m_ListCon.SetItemText ( i, 1, szProcessName[i] ) ;             CloseHandle ( hProcess ) ;     }   } }




(4)取得目标进程的主线程ID

代码:
dwTarProcessId    = dwProcessId[nCurIndex] ; szTarProcessName  = szProcessName[nCurIndex] ; HANDLE hThreadSnap = CreateToolhelp32Snapshot ( TH32CS_SNAPTHREAD, 0 ) ; if ( hThreadSnap == INVALID_HANDLE_VALUE )    return ;  THREADENTRY32 te32 = { sizeof(THREADENTRY32) } ; if ( Thread32First ( hThreadSnap, &te32 ) ) {   do{     if ( te32.th32OwnerProcessID == dwTarProcessId )     {       dwTarThreadId = te32.th32ThreadID ;        break ;     }   }while ( Thread32Next ( hThreadSnap, &te32 ) ) ; }



(5)封装HOOK API入口地址覆盖到CFEAHook类,FEAHook.h文件如下:

代码:
#ifndef _FEAHook_H #define _FEAHook_H class CFEAHook { public:   BOOL  isSuccess ;            // 标识是否成功   PSTR  pModuleName, pFunName ;      // 模块名、函数名   LPVOID  pOldFunEntry, pNewFunEntry ;  // 初始函数地址、HOOK后的函数地址   BYTE  bOldByte[5], bNewByte[5] ;    // 原始字节、目标字节 public:   CFEAHook () {  isSuccess  = false ;  }   ~CFEAHook() {  UnHook() ;    }   void Hook ( PSTR szModuleName, PSTR szFunName, FARPROC pFun )   {       HMODULE  hMod = GetModuleHandle ( szModuleName ) ;     if ( hMod != NULL )     {       isSuccess    = true ;       pModuleName    = szModuleName ;       pFunName    = szFunName ;       pNewFunEntry  = (LPVOID)pFun ;       pOldFunEntry  = (LPVOID)GetProcAddress ( hMod, pFunName ) ;       bNewByte[0]    = 0xE9 ;       *((PDWORD)(&(bNewByte[1])))  = (DWORD)pNewFunEntry - (DWORD)pOldFunEntry - 5 ;        DWORD   dwProtect, dwWriteByte, dwReadByte ;        VirtualProtect ( (LPVOID)pOldFunEntry, 5, PAGE_READWRITE, &dwProtect );       ReadProcessMemory  ( GetCurrentProcess(), (LPVOID)pOldFunEntry, bOldByte, 5, &dwReadByte ) ;           WriteProcessMemory  ( GetCurrentProcess(), (LPVOID)pOldFunEntry, bNewByte, 5, &dwWriteByte ) ;       VirtualProtect ( (LPVOID)pOldFunEntry, 5, dwProtect, NULL ) ;     }   }   void ReHook ()   {     DWORD  dwProtect, dwWriteByte ;     VirtualProtect ( pOldFunEntry, 5, PAGE_READWRITE, &dwProtect );     WriteProcessMemory ( GetCurrentProcess(), pOldFunEntry, bNewByte, 5, &dwWriteByte ) ;     VirtualProtect ( pOldFunEntry, 5, dwProtect, NULL ) ;   }   void UnHook ()   {     DWORD  dwProtect, dwWriteByte ;     VirtualProtect ( pOldFunEntry, 5, PAGE_READWRITE, &dwProtect );     WriteProcessMemory ( GetCurrentProcess(), pOldFunEntry, bOldByte, 5, &dwWriteByte ) ;     VirtualProtect ( pOldFunEntry, 5, dwProtect, NULL ) ;   } } ;


#endif

(6)HOOK四个函数

代码:
CFEAHook  HOOK_send ; CFEAHook  HOOK_recv ; CFEAHook  HOOK_sendto ; CFEAHook  HOOK_recvfrom ; HOOK_send.Hook    ( "wsock32.dll", "send",    (FARPROC)MY_send ) ; HOOK_recv.Hook    ( "wsock32.dll", "recv",    (FARPROC)MY_recv ) ; HOOK_sendto.Hook    ( "wsock32.dll", "sendto",    (FARPROC)MY_sendto ) ; HOOK_recvfrom.Hook  ("wsock32.dll",  "recvfrom",(FARPROC)MY_recvfrom ) ;



实例二:Watch IAT —— 查看IAT信息
  

1、  设计目标
查看可执行文件的IAT信息。这个软件虽然与修改IAT的HOOK API没有直接的联系,但我觉得非常有必要放到这个部分,至于必要性请看下文。


2、  存在的难点
由于IAT信息是在可执行文件初始化时经过PE加载器改写的,而不是原本的磁盘文件中的数据。问题就在这里,什么时候才是对IAT进行操作的最佳时机?PE加载器对IAT部分的改写是在什么时候完成的?如果仅仅创建挂起的进程,此时的加载器是否写入IAT信息?

3、问题的分析与设计
利用调试API实现对远程进程的创建、入口点的控制。
其实,可以通过先到达程序入口点,同时让程序挂起。这就可以利用调试API来实现,先创建目标进程(即可执行的目标文件,要查看IAT信息必须要让PE文件执行,从而才能使加载器改写功能的实现),该进程需带有DEBUG_ONLY_THIS_PROCES标志,这样该进程就具有可被本线程调试的权限,然后在主线程中建立一个调试循环体用于对目标进程执行流程的控制。当内核在完成进程装载和初始化之后会在被调试进程的上下文环境中自己调用一次int 3的(原本我在创建该进程的时候还带有CREATE_SUSPENDED标志,然后在入口点写入__int 3断点(0xcc),这样就等于做了重复工作,感谢drwch的指点),利用WaitForDebugEvent实现对目标进程EXCEPTION_DEBUG_EVENT异常的捕捉,这样程序就中断在程序入口点,同时可以读取所需的IAT信息。
这里还有一个关键之处在于目标进程的DEBUG_ONLY_THIS_PROCESS 
标志。该目标进程与常见它的母体有个令人比较讨厌的特性,由于目标进程是DEBUG子程式,如果母程式存在,子程式就不会结束。原本想利用DebugActiveProcessStop来实现子程式与母程式的分离,但该函数对于平台的要求比较特殊,最终决定用开辟线程的方式来解决这个问题。线程一旦结束,该子程式自然也就结束了。

3、  详细设计(关键代码如下)

代码:
DWORD WINAPI GetIATInfo ( LPVOID lpParameter ) {   ……   STARTUPINFO        stStartUp;   PROCESS_INFORMATION    stProcInfo;   GetStartupInfo ( &stStartUp ) ;   if( !CreateProcess(BaseInfo.szFileName,NULL,NULL,NULL,FALSE, \     DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&stStartUp,&stProcInfo) )      {     SetEvent ( hEvent ) ;     return dwErrorCode ;   }   else   {         DEBUG_EVENT devent ;     BOOL bIsContinue = true ;     while ( bIsContinue )     {       if ( WaitForDebugEvent( &devent, 100 ) )       {         switch ( devent.dwDebugEventCode )         {         case EXCEPTION_DEBUG_EVENT:           // 取IAT信息           for ( UINT uIATEntryIndex = 0; DllItemInfo[uIATEntryIndex].dwIATEntry; uIATEntryIndex++ )           {             DWORD dwIATEntry = DllItemInfo[uIATEntryIndex].dwIATEntry + BaseInfo.dwImageBase ;             DWORD dwIAT = 0, dwNumberOfByte = 0 ;             UINT count = 0 ;                          while ( ReadProcessMemory(stProcInfo.hProcess,(LPVOID)(dwIATEntry),&dwIAT,0X4,&dwNumberOfByte) )             {               if ( dwIAT )                {                 DllItemInfo[uIATEntryIndex].FunctionItemInfo[count].dwFunctionEntry = dwIAT ;                 DllItemInfo[uIATEntryIndex].FunctionItemInfo[count++].dwImageRVa = dwIATEntry-BaseInfo.dwImageBase;                 DllItemInfo[uIATEntryIndex].dwIATSize += 4 ;                 dwIAT = 0 ;                 dwIATEntry += 4 ;               }                 else                 break ;             }               }              bIsContinue = false ;           break ;         } //switch                  ContinueDebugEvent ( devent.dwProcessId, devent.dwThreadId, DBG_CONTINUE ) ;       } // if     } // while       } // else   SetEvent ( hEvent ) ;   return 0;  }


4、  心得体会
这个软件写的比较久了,回忆了下。加深对加载过程的了解,熟悉WIN32调试API。
【参考文献】
[1].Windows核心编程 Jeffrey Richter著
【版权声明】必须注明原创于看雪技术论坛(bbs.pediy.com) 及作者,并保持文章的完整性。