PE文件导入表的代码注入

PE文件导入表的代码注入
 
         试想一下,如果通过修改导入表,能把PE格式文件中的函数入口点,重定向到自己的程序中来,是不是很酷!这样,在自己在程序中,可以过滤掉对某些函数的调用,或者,设置自己的处理程序,Professional Portable Executable (PE) Protector也就是这样做的。另外,某些rootkit也使用了此方法把恶意代码嵌入到正常程序中。在逆向工程的概念里,均称为API重定向技术,让我们一起进入这个神奇的世界吧。
 
 
         1、导入表简介
         PE文件由MS-DOS头、NT头、节头、节映像组成,如图1所示。MS-DOS头在从DOS至现今Windows的所有微软可执行文件中都存在;NT头的概念抽象自Unix系统的可执行与链接文件格式(ELF)。实际上,PE格式可以说是Linux可执行与链接格式(ELF)的兄弟,PE格式头由PE签名、通用对象文件格式(COFF)头、PE最优化头、节头组成。
 
 
图1:PE文件格式结构
 
 
         NT头的定义可在Visual C++的<winnt.h>头文件中找到,也可使用DbgHelp.dll中的ImageNtHeader()函数得到相关信息,另外,还可以使用DOS头来获取NT头,因为DOS头的最后位置e_lfanew,代表了NT头的偏移量。
 
 
typedef struct _IMAGE_NT_HEADERS {
   DWORD Signature;
   IMAGE_FILE_HEADER FileHeader;
   IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
 
 
         在PE最优化头中,有一些数据目录指明了当前进程虚拟内存中,主信息表的相对位置及大小,这些表可保存资源、导入、导出、重定位、调试、线程本地存储及COM运行时的有关信息。没有导入表的PE可执行文件是不可能存在的,这张表包含了DLL名及函数名,这些都是程序通过虚拟地址调用它们时所必不可少的;控制台可执行文件中没有资源表,然而,对图形用户界面的Windows可执行文件来说,资源表却是至关重要的部分;当某个动态链接库导出了它的函数,此时就需要导出表了,在OLE Active-X容器中也同样;而.NET虚拟机缺少了COM+运行时头则不能被执行。PE格式的详细说明见表1:
 
 
数据目录
0 Export Table(导出表)
1 Import Table(导入表)
2 Resource Table(资源表)
3 Exception Table(异常表)
4 Certificate File(凭证文件)
5 Relocation Table(重定位表)
6 Debug Data(调试数据)
7 Architecture Data(架构数据)
8 Global Ptr(全局指针)
9 Thread Local Storage Table(线程本地存储表)
10 Load Config Table(加载配置表)
11 Bound Import Table(边界导入表)
12 Import Address Table(导入地址表)
13 Delay Import Descriptor(延误导入描述符)
14 COM+ Runtime Header(COM+运行时头)
15 Reserved(保留)
    表1:数据目录
 
 
// <winnt.h>
 
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES     16
 
//可选头格式
 
typedef struct _IMAGE_OPTIONAL_HEADER {
 
   ...
 
   IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
 
 
//目录项
#define IMAGE_DIRECTORY_ENTRY_EXPORT      0 //导出目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT      1 //导入目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE    2 //资源目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 //基重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG       6 //调试目录
#define IMAGE_DIRECTORY_ENTRY_TLS         9 //TLS目录
 
 
         可通过简单的几行代码获得导入表的位置及大小,知道了导入表的位置后,就可知道DLL名及函数名了,这将在后面进行讨论。
 
 
PIMAGE_NT_HEADERS pimage_nt_headers = ImageNtHeader(pImageBase);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
   DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
 
PIMAGE_DOS_HEADER pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
PIMAGE_NT_HEADERS pimage_nt_headers = (PIMAGE_NT_HEADERS)
   (pImageBase + pimage_dos_header->e_lfanew);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
   DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
 
 
         2、导入描述符概览
         通过导入表的导入目录项,可获知文件映像内部导入表的位置,且对每个导入的DLL、导入描述符,都有一个相应的容器,其包含了首个thunk(转换程序)地址、原始首个thunk的地址,还有指向DLL名的指针。FirstThunk指向首个thunk的位置,这些thunk在程序运行时,由Windows的PE加载器初始化,如图5所示。OriginalFirstThunk指向这些thunk的第一个存储位置,对每个函数而言,这也是提供Hint(提示)数据地址及函数名数据之处,见图4。在本例中,OriginalFirstThunk不存在,而FirstThunk则指向了提示数据及函数名数据位置之处,见图3。
         IMAGE_IMPORT_DESCRIPTOR结构代表了导入描述符,以下是其定义:
 
 
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
  DWORD    OriginalFirstThunk;
   DWORD   TimeDateStamp;
   DWORD   ForwarderChain;
   DWORD   Name;
   DWORD   FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
 
 
         OriginalFirstThunk指向第一个thunk(IMAGE_THUNK_DATA),thunk中保存了提示数据地址及函数名。
         TimeDateStamp包含了绑定的时间日期戳,如果它为0,表示在导入的DLL没有任何绑定。在将来,会设为0xFFFFFFFF以表明有绑定。
         ForwarderChain指向API的第一个转发链,设为0xFFFFFFFF表示没有转发。
         Name指明了DLL名的相对虚拟地址。
         FirstThunk包含了由IMAGE_THUNK_DATA定义的首个thunk数组的虚拟地址,而thunk由加载器用函数虚拟地址初始化。如果OrignalFirstThunk不存在,它指向了第一个thunk、提示(Hint)thunk及函数名。
 
 
typedef struct _IMAGE_IMPORT_BY_NAME {
   WORD    Hint;
   BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
 
typedef struct _IMAGE_THUNK_DATA {
   union {
      PDWORD    Function;
      PIMAGE_IMPORT_BY_NAME AddressOfData;
   } u1;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;
 
 
图2:导入表一览
 
 
图3:带有Orignal First Thunk的导入表视图
 
 
         这两张导入表(图2、3)清楚地说明了带有及不带Orignal First Thunk时的区别。
 
 
图4:被PE加载器覆写后的导入表
 
 
         可用Dependency Walker,见图5,来查看导入表的所有信息,另外,还有一个小工具Import Table viewer,见图6,也可用来查看此类信息。
 
 
图5:Dependency Walker——Visual Studio自带的工具
 
 
图6:Import Table viewer
 
 
         另外,也可利用下面这段代码,在自己的程序中显示导入DLL及导入函数(只适用于控制台模式的程序)。
 
 
PCHAR        pThunk;
PCHAR        pHintName;
DWORD        dwAPIaddress;
PCHAR        pDllName;
PCHAR        pAPIName;
//----------------------------------------
DWORD dwImportDirectory= RVA2Offset(pImageBase, pimage_nt_headers->
   OptionalHeader.
   DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
//----------------------------------------
PIMAGE_IMPORT_DESCRIPTOR pimage_import_descriptor=
   (PIMAGE_IMPORT_DESCRIPTOR) (pImageBase+dwImportDirectory);
//----------------------------------------
while(pimage_import_descriptor->Name!=0)
{
   pThunk= pImageBase+pimage_import_descriptor->FirstThunk;
   pHintName= pImageBase;
   if(pimage_import_descriptor->OriginalFirstThunk!=0)
   {
        pHintName+= RVA2Offset(pImageBase,
           pimage_import_descriptor->OriginalFirstThunk);
   }
   else
   {
      pHintName+= RVA2Offset(pImageBase, pimage_import_descriptor->FirstThunk);
   }
   pDllName= pImageBase + RVA2Offset(pImageBase,pimage_import_descriptor->Name);
   printf(" DLL Name: %s First Thunk: 0x%x", pDllName,
          pimage_import_descriptor->FirstThunk);
 
   PIMAGE_THUNK_DATA pimage_thunk_data= (PIMAGE_THUNK_DATA) pHintName;
   while(pimage_thunk_data->u1.AddressOfData!=0)
   {
      dwAPIaddress= pimage_thunk_data->u1.AddressOfData;
      if((dwAPIaddress&0x80000000)==0x80000000)
      {
         dwAPIaddress&= 0x7FFFFFFF;
         printf("Proccess: 0x%x", dwAPIaddress);
      }
      else
      {
         pAPIName= pImageBase+RVA2Offset(pImageBase, dwAPIaddress)+2;
         printf("Proccess: %s", pAPIName);
      }
      pThunk+= 4;
      pHintName+= 4;
      pimage_thunk_data++;
   }
   pimage_import_descriptor++;
}
 
 
         3、API重定向技术
         在了解了导入表的所有基础知识后,现在就要来看看重定向方法了,其算法非常简单,在当前进程的虚拟内存内部创建一个额外的虚拟空间,并生成相应指令通过JMP重定向到原始函数位置。在这里,可使用绝对跳转或相对跳转,在使用绝对跳转时要非常小心,不能像图7中那么简单地进行,应首先把虚拟地址存到EAX中,接着由JMP EAX跳转。
 
 
图7:绝对跳转指令的API重定向
 
 
         当然了,也可以使用相对跳转,下面的代码就是这样做的,但要清楚,不能对所有DLL模块都进行API重定向,比如,拿“计算器”CALC.EXE来说,MSVCRT.DLL的某些thunk在运行时初始化期间,是从CALC.EXE代码节内部访问的,因此,重定向的话则不能正常工作。
 
 
_it_fixup_1:
   push ebp
   mov ebp,esp
   add esp,-14h
   push PAGE_READWRITE
   push MEM_COMMIT
   push 01D000h
   push 0
   call _jmp_VirtualAlloc
   //NewITaddress=VirtualAlloc(NULL, 0x01D000,
   //                          MEM_COMMIT, PAGE_READWRITE);
   mov [ebp-04h],eax
   mov ebx,[ebp+0ch]
   test ebx,ebx
   jz _it_fixup_1_end
   mov esi,[ebp+08h]
   add ebx,esi                        // dwImageBase + dwImportVirtualAddress
 
_it_fixup_1_get_lib_address_loop:
      mov eax,[ebx+0ch]               // image_import_descriptor.Name
      test eax,eax
      jz _it_fixup_1_end
 
      mov ecx,[ebx+10h]               // image_import_descriptor.FirstThunk
      add ecx,esi
      mov [ebp-08h],ecx               // dwThunk
      mov ecx,[ebx]                   // image_import_descriptor.Characteristics
      test ecx,ecx
      jnz _it_fixup_1_table
      mov ecx,[ebx+10h]
 
_it_fixup_1_table:
      add ecx,esi
      mov [ebp-0ch],ecx               // dwHintName
      add eax,esi                     // image_import_descriptor.Name
                                    // + dwImageBase = ModuleName
      push eax                        // lpLibFileName
      mov [ebp-10h],eax
      call _jmp_LoadLibrary             // LoadLibrary(lpLibFileName);
 
      test eax,eax
      jz _it_fixup_1_end
      mov edi,eax
 
_it_fixup_1_get_proc_address_loop:
         mov ecx,[ebp-0ch]            // dwHintName
         mov edx,[ecx]                // image_thunk_data.Ordinal
         test edx,edx
         jz _it_fixup_1_next_module
         test edx,080000000h          //是否按顺序导入
         jz _it_fixup_1_by_name
         and edx,07FFFFFFFh          //取得顺序
         jmp _it_fixup_1_get_addr
 
_it_fixup_1_by_name:
         add edx,esi                  // image_thunk_data.Ordinal +
                                    // dwImageBase = OrdinalName
         inc edx
         inc edx                      // OrdinalName.Name
_it_fixup_1_get_addr:
         push edx                     // lpProcName
         push edi                     // hModule
         call _jmp_GetProcAddress      // GetProcAddress(hModule,
                                    //                      lpProcName);
         mov [ebp-14h],eax            //_p_dwAPIaddress
         //=========================================================
         //            重定向引擎
         push edi
         push esi
         push ebx
 
         mov ebx,[ebp-10h]
         push ebx
         push ebx
         call _char_upper
 
         mov esi,[ebp-10h]
         mov edi,[ebp+010h]
 
_it_fixup_1_check_dll_redirected:
            push edi
            call __strlen
            add esp, 4
 
            mov ebx,eax
            mov ecx,eax
             push edi
            push esi
            repe cmps
            jz _it_fixup_1_do_normal_it_0
            pop esi
            pop edi
            add edi,ebx
         cmp byte ptr [edi],0
         jnz _it_fixup_1_check_dll_redirected
            mov ecx,[ebp-08h]
            mov eax,[ebp-014h]
            mov [ecx],eax
            jmp _it_fixup_1_do_normal_it_1
 
_it_fixup_1_do_normal_it_0:
            pop esi
            pop edi
            mov edi,[ebp-04h]
            mov byte ptr [edi], 0e9h    // JMP指令
            mov eax,[ebp-14h]
            sub eax, edi
            sub eax, 05h
            mov [edi+1],eax             //相对JMP值
            mov word ptr [edi+05], 0c08bh
            mov ecx,[ebp-08h]
            mov [ecx],edi               // -> Thunk
            add dword ptr [ebp-04h],07h
 
_it_fixup_1_do_normal_it_1:
         pop ebx
         pop esi
         pop edi
        //===================================================
         add dword ptr [ebp-08h],004h    // dwThunk => next dwThunk
         add dword ptr [ebp-0ch],004h   // dwHintName =>
                                         // next dwHintName
      jmp _it_fixup_1_get_proc_address_loop
 
_it_fixup_1_next_module:
      add ebx,014h              // sizeof(IMAGE_IMPORT_DESCRIPTOR)
   jmp _it_fixup_1_get_lib_address_loop
 
_it_fixup_1_end:
   mov esp,ebp
   pop ebp
   ret 0ch
 
 
         说点题外话,千万不要认为这样就可以绕过Professional EXE Protector了,这个软件中有一套自己的x86指令生成引擎用于创建重定向代码,有时,这个引擎还带有一个扰乱变形引擎,以使它复杂到难以跟踪分析。
 
 
         在工作原理上,上述代码依照了下列算法:
 
1、 创建一个单独的空间以存储由VirtualAlloc()生成的指令。
2、 通过LoadLibrary()和GerProcAddress()找到函数的虚拟地址。
3、 检查DLL名是否匹配有效DLL列表。在本例中,识别出KERNEL32.DLL、USER32.DLL、GDI32.DLL、ADVAPI32.DLL、SHELL32.DLL为可重定向的有效DLL名。
4、 如果DLL名有效,转到重定向部分;否则,用原始函数虚拟地址初始化thunk。
5、 为重定向API,生成JMP(0xE9)指令并计算函数的相对地址以确定相对跳转。
6、 把生成的指令存储在单独的空间中,并把thunk引用为这些指令的首地址。
7、 对其他函数及DLL重复以上步骤。
 
 
如果对CALC.EXE执行了以上步骤,并以OllyDbg进行跟踪,将会看到类似以下的代码:
 
 
008E0000 - E9 E6F8177C     JMP SHELL32.ShellAboutW
008E0005     8BC0           MOV EAX,EAX
008E0007 - E9 0F764F77     JMP ADVAPI32.RegOpenKeyExA
008E000C     8BC0           MOV EAX,EAX
008E000E - E9 70784F77     JMP ADVAPI32.RegQueryValueExA
008E0013     8BC0           MOV EAX,EAX
008E0015 - E9 D66B4F77     JMP ADVAPI32.RegCloseKey
008E001A     8BC0           MOV EAX,EAX
008E001C - E9 08B5F27B     JMP kernel32.GetModuleHandleA
008E0021     8BC0           MOV EAX,EAX
008E0023 - E9 4F1DF27B     JMP kernel32.LoadLibraryA
008E0028     8BC0           MOV EAX,EAX
008E002A - E9 F9ABF27B     JMP kernel32.GetProcAddress
008E002F     8BC0           MOV EAX,EAX
008E0031 - E9 1AE4F77B     JMP kernel32.LocalCompact
008E0036     8BC0           MOV EAX,EAX
008E0038 - E9 F0FEF27B     JMP kernel32.GlobalAlloc
008E003D     8BC0           MOV EAX,EAX
008E003F - E9 EBFDF27B     JMP kernel32.GlobalFree
008E0044     8BC0           MOV EAX,EAX
008E0046 - E9 7E25F37B     JMP kernel32.GlobalReAlloc
008E004B     8BC0           MOV EAX,EAX
008E004D - E9 07A8F27B     JMP kernel32.lstrcmpW
008E0052     8BC0           MOV EAX,EAX
 
 
         而绝对跳转时的代码如下:
 
 
008E0000 - B8 EBF8A57C     MOV EAX,7CA5F8EBh
 //SHELL32.ShellAboutW的地址
008E0005     FFE0           JMP EAX
 
 
         下面,就要用重定向技术来改变某个API的功能了,在本例中,将把CALC.EXE的ShellAbout()对话框重定向到“Hello World!”消息框,且只需对前述代码作稍许改动:
 
 
...
   //==============================================================
   push edi
   push esi
   push ebx
 
   mov ebx,[ebp-10h]
   push ebx
   push ebx
   call _char_upper
 
   mov esi,[ebp-10h]
   mov edi,[ebp+010h]        // [ebp+_p_szShell32]
 
_it_fixup_1_check_dll_redirected:
      push edi
      call __strlen
      add esp, 4
 
      mov ebx,eax
      mov ecx,eax
      push edi
      push esi
      repe cmps             // byte ptr [edi], byte ptr [esi]
      jz _it_fixup_1_check_func_name
      jmp _it_fixup_1_no_check_func_name
 
_it_fixup_1_check_func_name:
      mov edi,[ebp+014h]    // [ebp+_p_szShellAbout]
      push edi
      call __strlen
      add esp, 4
      mov ecx,eax
      mov esi,[ebp-18h]
      mov edi,[ebp+014h]    // [ebp+_p_szShellAbout]
      repe cmps //byte ptr [edi], byte ptr [esi]
      jz _it_fixup_1_do_normal_it_0
 
_it_fixup_1_no_check_func_name:
      pop esi
      pop edi
      add edi,ebx
   cmp byte ptr [edi],0
   jnz _it_fixup_1_check_dll_redirected
   mov ecx,[ebp-08h]
   mov eax,[ebp-014h]
   mov [ecx],eax
   jmp _it_fixup_1_do_normal_it_1
 
_it_fixup_1_do_normal_it_0:
      pop esi
      pop edi
      mov ecx,[ebp-08h]
      mov edi,[ebp+18h]
      mov [ecx],edi    //把新函数的地址存到thunk中
 
_it_fixup_1_do_normal_it_1:
   pop ebx
   pop esi
   pop edi
   //==============================================================
   ...
 
 
         先检查DLL是否为“Shell32.dll”,再检查函数名是否为“ShellAboutW”,如果这两个条件为true,把ShellAbout()的thunk重定向到新的函数。
         新的函数只是一个简单的消息框:
 
 
_ShellAbout_NewCode:
_local_0:
   pushad    //把寄存器上下文保存在堆栈中
   call _local_1
_local_1:
   pop ebp
   sub ebp,offset _local_1 //得到基ebp
   push MB_OK | MB_ICONINFORMATION
   lea eax,[ebp+_p_szCaption]
   push eax
   lea eax,[ebp+_p_szText]
   push eax
   push NULL
   call _jmp_MessageBox
   // MessageBox(NULL, szText, szCaption, MB_OK | MB_ICONINFORMATION) ;
   popad    //从堆栈中恢复第一个寄存器上下文
   ret 10h
 
 
         当想要用一个新的函数取代某个API时,应考虑以下两个重点部分:
 
         一是不要因丢失堆栈点而破坏了堆栈内存(Stack memory),因此,最终必需以ADD ESP,xxx或RET xxx恢复原始堆栈点。
         二是通过PUSHAD、POPAD,尽量保证除了EAX之外的大多数线程寄存器的安全。
 
 
         大家可以看到,代码中也使用了PUSHAD及POPAD回收线程寄存器,对本例来说,ShellAbout()有4个DWORD成员,因此当返回时,堆栈点增长了0x10。
         重定向ShellAbout()之后,点击计算器(CALC.EXE)“帮助”菜单中的“关于计算器”,就可看到结果了。
 
 
图8:“关于计算器”被重定向到一个消息框
 
 
         4、保护防止逆向工程
         要使用复杂的API重定向技术来重新构造一个导入表简直是难上加难,有时,像Import REConstructor这样的工具(图10)在重建导入表时也会被搞糊涂,尤其是当重定向是通过多态代码映像来实现时。在逆向工程界,Import REConstructor是一个非常有名的工具,它会挂起目标进程以捕捉导入表信息。如果是像简单的JMP这样实现的重定向,使用此工具当然可以被重建,不过,如果加密了函数名并在内存中与多态变形代码进行绑定,它就不能得到正确的导入表了。Native Security Engine 6就是这样的一个打包器,它有一个x86代码生成器外加一个扰乱变形引擎,两者都有助于实现一个复杂的重定向结构。
 
 
图9:Import REConstructor
 
 
         图10中列明了导入表保护的主要策略,它们中有些重定向至虚拟的Win32库,例如,分别有Kernel32、User32、AdvApi32的虚拟库,使用自己的库可防止被他人破解或安装自己的虚拟机。
 
 
图10:导入表保护
 
 
         利用此方法,也可切断对外界的访问,如MoleBox就是这样,它过滤了FindFirstFile()及FindNextFile()以把TEXT及JPEG文件合并在压缩文件内部,当程序找到硬盘上的一个文件时,它将会被重定向到内存中。
 
 
         5、导入表的运行时注入
         在这一节,最重要的问题是,怎样注入到运行时进程的导入表中呢,其实可通过重写内存数据及从外部重定向导入表来实现。
         (1)、WindowFromPoint()可获取特定点的窗口句柄,GetWindowThreadProcessId()可有助于了解此窗口句柄的进程ID及线程ID。
 
POINT point;
HWND hWindowUnderTheMouse = WindowFromPoint(point);
 
DWORD     dwProcessId;
DWORD     dwThreadId;
dwThreadId=GetWindowThreadProcessId(hSeekedWindow, &dwProcessId);
 
 
         (2)、进程及线程句柄可由OpenProcess()及OpenThread()获得。
 
HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE,
                               dwProcessId );
HANDLE hThread = OpenThread( THREAD_ALL_ACCESS, FALSE,
                              dwThreadId);
 
 
         (3)、要操纵进程内存,先要挂起主线程来冻结进程。
 
SuspendThread(hThread);
 
 
         (4)、线程环境块(Thread Environment Block TEB)的位置可由FS:[18]获得,而使用GetThreadContext()与GetThreadSelectorEntry()则可以知道FS段的基址值。
 
CONTEXT         Context;
LDT_ENTRY       SelEntry;
 
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread,&Context);
 
//计算FS的基址
GetThreadSelectorEntry(hThread, Context.SegFs, &SelEntry);
DWORD dwFSBase = ( SelEntry.HighWord.Bits.BaseHi << 24) |
                 (SelEntry.HighWord.Bits.BaseMid << 16) |
                  SelEntry.BaseLow;
 
 
         (5)、线程环境块可在目标进程虚拟内存内部读取获得,线程与进程环境块的具体形式,请见图11。
 
PTEB pteb = new TEB;
PPEB ppeb = new PEB;
DWORD        dwBytes;
 
ReadProcessMemory( hProcess, (LPCVOID)dwFSBase, pteb,
                   sizeof(TEB), &dwBytes);
ReadProcessMemory( hProcess, (LPCVOID)pteb->Peb, ppeb,
                   sizeof(PEB), &dwBytes);
 
 
图11:线程环境块(Thread Environment Block)及进程环境块(Process Environment Block)
 
 
         (6)、当前进程内存的PE映像的映像基址可由进程环境块中信息获得。
 
DWORD dwImageBase = (DWORD)ppeb->ImageBaseAddress;
 
 
         (7)、ReadProcessMemory()可用于读取PE文件的整个映像。
 
PIMAGE_DOS_HEADER pimage_dos_header = new IMAGE_DOS_HEADER;
PIMAGE_NT_HEADERS pimage_nt_headers = new IMAGE_NT_HEADERS;
 
ReadProcessMemory( hProcess,
                  (LPCVOID)dwImageBase,
                   pimage_dos_header,
                   sizeof(IMAGE_DOS_HEADER),
                  &dwBytes);
ReadProcessMemory( hProcess,
                  (LPCVOID)(dwImageBase+pimage_dos_header->
                            e_lfanew),
                   pimage_nt_headers, sizeof(IMAGE_NT_HEADERS),
                  &dwBytes);
 
PCHAR pMem = (PCHAR)GlobalAlloc(
                   GMEM_FIXED | GMEM_ZEROINIT,
                   pimage_nt_headers->
                   OptionalHeader.SizeOfImage);
 
ReadProcessMemory( hProcess,
                  (LPCVOID)(dwImageBase),
                   pMem,
                   pimage_nt_headers->
                   OptionalHeader.SizeOfImage,
                   &dwBytes);
 
 
         (8)、查找DLL名及thunk值确定目标以便对它进行重定向,在本例中,DLL名是Shell32.dll,而thunk是ShellAbout()的虚拟地址。
 
HMODULE hModule = LoadLibrary("Shell32.dll");
DWORD dwShellAbout= (DWORD)GetProcAddress(hModule, "ShellAboutW");
 
DWORD dwRedirectMem = (DWORD)VirtualAllocEx(
                   hProcess,
                   NULL,
                   0x01D000,
                   MEM_COMMIT,
                   PAGE_EXECUTE_READWRITE);
 
RedirectAPI(pMem, dwShellAbout, dwRedirectMem);
 
...
 
int RedirectAPI(PCHAR pMem, DWORD API_voffset,
                DWORD NEW_voffset)
{
   PCHAR     pThunk;
   PCHAR     pHintName;
   DWORD     dwAPIaddress;
   PCHAR     pDllName;
   DWORD     dwImportDirectory;
 
   DWORD     dwAPI;
 
   PCHAR pImageBase = pMem;
   //----------------------------------------
   PIMAGE_IMPORT_DESCRIPTOR    pimage_import_descriptor;
   PIMAGE_THUNK_DATA           pimage_thunk_data;
   //----------------------------------------
   PIMAGE_DOS_HEADER pimage_dos_header;
   PIMAGE_NT_HEADERS pimage_nt_headers;
   pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
   pimage_nt_headers = (PIMAGE_NT_HEADERS)
                       (pImageBase+pimage_dos_header-> e_lfanew);
   //----------------------------------------
   dwImportDirectory=pimage_nt_headers->OptionalHeader
               .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
               .VirtualAddress;
   if(dwImportDirectory==0)
   {
        return -1;
   }
   //----------------------------------------
   pimage_import_descriptor=(PIMAGE_IMPORT_DESCRIPTOR)
                            (pImageBase+dwImportDirectory);
   //----------------------------------------
   while(pimage_import_descriptor->Name!=0)
   {
      pThunk=pImageBase+pimage_import_descriptor->
         FirstThunk;
      pHintName=pImageBase;
      if(pimage_import_descriptor->OriginalFirstThunk!=0)
      {
         pHintName+=pimage_import_descriptor->
            OriginalFirstThunk;
      }
      else
      {
         pHintName+=pimage_import_descriptor->FirstThunk;
      }
      pDllName=pImageBase+pimage_import_descriptor->Name;
 
      StrUpper(pDllName);
      if(strcmp(pDllName,"SHELL32.DLL")==0)
      {
         pimage_thunk_data=PIMAGE_THUNK_DATA(pHintName);
         while(pimage_thunk_data->u1.AddressOfData!=0)
         {
            //----------------------------------------
            memcpy(&dwAPI, pThunk, 4);
            if(dwAPI==API_voffset)
            {
               memcpy(pThunk, &NEW_voffset, 4);
               return 0;
            }
            //----------------------------------------
            pThunk+=4;
            pHintName+=4;
            pimage_thunk_data++;
         }
      }
      pimage_import_descriptor++;
   }
   //----------------------------------------
   return -1;
}
 
 
         (9)、重定向中的额外内存由VirtualProtectEx()分配,在此只是生成代码,并把它写到新的空间中。
 
DWORD dwRedirectMem = (DWORD)VirtualAllocEx(
                      hProcess,
                      NULL,
                      0x01D000,
                      MEM_COMMIT,
                      PAGE_EXECUTE_READWRITE);
 
...
 
PCHAR pLdr;
DWORD Ldr_rsize;
GetLdrCode(pLdr, Ldr_rsize);
 
WriteProcessMemory( hProcess,
                   (LPVOID)(dwRedirectMem),
                   pLdr,
                   Ldr_rsize,
                   &dwBytes);
 
 
         (10)、额外的内存中是加载代码,它显示一个消息框。
 
void GetLdrCode(PCHAR &pLdr, DWORD &rsize)
{
   HMODULE     hModule;
   DWORD       dwMessageBox;
 
   PCHAR       ch_temp;
   DWORD       dwCodeSize;
   ch_temp=(PCHAR)DWORD(ReturnToBytePtr(DynLoader,
      DYN_LOADER_START_MAGIC))+4;
   dwCodeSize=DWORD(ReturnToBytePtr(DynLoader,
      DYN_LOADER_END_MAGIC))-DWORD(ch_temp);
   rsize= dwCodeSize;
   pLdr = (PCHAR)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, dwCodeSize);
   memcpy(pLdr, ch_temp, dwCodeSize);
 
   ch_temp=(PCHAR)ReturnToBytePtr(pLdr,
      DYN_LOADER_START_DATA1);
 
   hModule = LoadLibrary("User32.dll");
   dwMessageBox= (DWORD)GetProcAddress(hModule, "MessageBoxA");
   memcpy(ch_temp+4, &dwMessageBox, 4);
}
 
   ...
_ShellAbout_NewCode:
_local_0:
   pushad    //在堆栈中保存寄存器上下文
   call _local_1
_local_1:
   pop ebp
   sub ebp,offset _local_1    //取得ebp基址
   push MB_OK | MB_ICONINFORMATION
   lea eax,[ebp+_p_szCaption]
   push eax
   lea eax,[ebp+_p_szText]
   push eax
   push NULL
   mov eax, [ebp+_p_MessageBox]
   call eax
   // MessageBox(NULL, szText, szCaption,
   /             MB_OK | MB_ICONINFORMATION) ;
   popad    //从堆栈中恢复第一个寄存器上下文
   ret 10h
   ...
 
 
         (11)、在修改之后,可执行映像就写到内存中了,记得在写之前,不要忘了把内存设为完全访问。
 
VirtualProtectEx( hProcess,
                 (LPVOID)(dwImageBase),
                   pimage_nt_headers->
                  OptionalHeader.SizeOfImage,
                  PAGE_EXECUTE_READWRITE,
                  &OldProtect);
 
WriteProcessMemory( hProcess,
                   (LPVOID)(dwImageBase),
                    pMem,
                    pimage_nt_headers->
                    OptionalHeader.SizeOfImage,
                    &dwBytes);
 
         VirtualProtectEx()设置页访问为PAGE_EXECUTE_READWRITE保护类型,当使用WriteProcessMemory()且可执行页中有PAGE_EXECUTE时,必须有PAGE_READWRITE访问权限。
 
 
         (12)、现在,可以释放前面挂起的进程了,点击“关于”菜单,将会看到如图12所示的界面。
 
ResumeThread(hThread);
 
 
图12:ShellAbout() thunk的运行时注入
 
 
         6、对程序的挂钩
         利用此方法,可挂钩所有Windows控件并过滤API,也就是通常所说的用户级rootkit,下面就来看一下对Yahoo Messenger的挂钩是怎样实现的。
 
         (1)、使用FindWindow()通过类名取得Yahoo Messenger的句柄。
 
HWND hWnd = FindWindow("YahooBuddyMain", NULL);
 
         (2)、用前面描述的方法实现对进程的注入。
 
         (3)、在GetDlgItemText()的导入thunk上进行注入,以过滤它的参数。
 
UINT GetDlgItemText( HWND hDlg,
                     int nIDDlgItem,
                     LPTSTR lpString,
                     int nMaxCount);
 
         (4)、比较对话框ID、nIDDlgItem,以确定当前哪个项目在使用,找到之后,对原始GetDlgItemText()进行挂钩。
 
CHAR pYahooID[127];
CHAR pPassword[127];
 
switch(nIDDlgItem)
{
case 211:     // Yahoo ID
   //
   GetDlgItemText(hDlg, nIDDlgItem, pYahooID, 127);
   // ...
   //
   GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
   break;
 
case 212:     //密码
   //
   GetDlgItemText(hDlg, nIDDlgItem, pPassword, 127);
   // ...
   //
   GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
   break;
 
default:
   //
   GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
}
 
 
图13:对Yahoo Messenger的挂钩
 
 
         7、结论
         导入表是Windows可执行文件中非常重要的部分,相关知识可助于理解程序在运行时,是怎样请求某个API的。你也可重定向导入表到当前进程的另一个可执行内存中,通过自己的PE加载器防止别人对程序进行逆向工程,或者仅是挂钩API函数;也可从外部在运行时挂起某个进程从而修改它的导入表。更进一步来说,在理解了这个概念之后,还可以创建自己的虚拟机,在Windows或Liunx内部的一个单独环境中,运行Windows可执行文件,也就是说,甚至可以不需要Windows就能运行Windows EXE文件了。

猜你喜欢

转载自blog.csdn.net/bobopeng/article/details/38735835