6、重定位的奥秘 
--------------- 
重定位源于代码中的地址操作,如果没有地址操作那么就不存在所谓的重定位了。让我们先 
来分析一段代码。考虑如下代码: 
format  PE GUI 4.0 
mov     esi,pszText 
ret 
pszText     db      '#$%*(*)@#$%',0 
打开softice,看看我们代码被编译为: 
001B:00401000   BE06104000          MOV     ESI,00401006 
001B:00401005   C3                  RET 
001B:00401006   ... 
可见,pszText的地址是在编译时计算好的。我们的病毒代码如果要插入到宿主体内,那么这 
个地址就不正确了。我们必须使我们的这个地址是在运行时计算出来的。这就是病毒中经典 
的重定位问题。考虑如下代码: 
format  PE GUI 4.0 
        call    delta 
    delta: 
        pop     ebp 
        sub     ebp,delta 
        lea     esi,dword [ebp+pszText] 
        ret 
pszText     db      '#$%*(*)@#$%',0 
我们再来看看这次我们的代码被翻译成了什么样 :P 
001B:00401000   E800000000          CALL    00401005 
001B:00401005   5D                  POP     EBP 
001B:00401006   81ED05104000        SUB     00401005 
001B:0040100C   8DB513104000        LEA     ESI,[EBP+00401013] 
001B:00401012   C3                  RET 
001B:00401013   ... 
我们首先用call/pop指令得到了delta在内存中的实际地址(为什么要用这样一个call/pop 
结构呢?我们看到这个call被翻译成E8 00 00 00 00,后面的00000000为相对地址,所以这 
个指令被翻译成mov 00401005。因为后面是一个相对地址,所以当这段代码被插入到宿主中 
后这个call依然可以得到正确的地址),在这个程序中是00401005。然后得到delta的偏移 
地址(offset),这个地址也是00401005,但我们从指令的机器码中看到这个地址是个绝对 
地址。我们用这个实际的地址减去这个绝对的偏移地址就得到了这个程序段对于插入前原程 
序段的偏移量。这是什么意思呢,上面的程序其实根本不需要重定位,让我们来考虑这样一 
个情况: 
假设上面的代码被插入到了宿主中。假设插入的地址为00501000(取这个地址是为了计算方 
便 :P),这时通过call/pop得到delta的地址为00501005。但delta的offset是在编译时计算 
的绝对地址,所以仍为00401005。这两个值相减就得到了这个程序段相对于原程序段的偏移 
量00100000。这就意味着我们所有地址操作都要加上这个偏移才能调整到正确的地址。这就 
是代码的自身重定位。 
当然这种重定位还可以写成别的形式,比如: 
        call 
    shit: 
        ... 
    delta: 
        pop     ebp 
        sub     ebp,shit 
        ... 
等等... 这些就留给读者自己去分析吧。 

7、SEH 
------ 
我们都知道,在x86系列中,保护模式下的异常处理是CPU通过在IDT中查询相应的异常处理 
例程来完成的。Win32中,系统利用SEH(Structured Exception Handling,结构化异常处 
理)来实现对IDT内异常的处理。同时,SEH还被用来处理用户自定义异常。 
可能读者对SEH这个词不很熟悉,但对于下边的程序大家也许都不会感到陌生: 
#pragma warning (disable: 4723) 
#include <windows.h> 
#include <iostream> 
using namespace std; 
int main (int argc, char *argv[]) 

    __try 
    { 
        int a=0,b=456; 
        a=b/a; 
    } 
    __except (GetExceptionCode () == EXCEPTION_INT_DIVIDE_BY_ZERO ? 
              EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) 
    { 
        cout<<"产生除0异常\n"<<endl; 
    } 
    return 0; 

这里的__try / __except用到的就是SEH。下面我们来看一下SEH的工作原理。在Win32的线 
程中,FS总是指向一个叫做TIB(Thread Information Block,线程信息块)的结构,在NT 
系统中这个结构为TEB(Thread Environment Block,线程环境块)。我们不需要清楚整个 
结构,我们只需要知道这个结构的第一个双字是指向EXCEPTION_REGISTRATION结构的指针。 
; 这是FASM对结构的定义,熟悉一下 :P 
struc EXCEPTION_REGISTRATION 

    .prev       dd      ? 
    .handler    dd      ? 

prev字段指向下一个ER结构。handler指向异常处理例程。这是一个典型的链表结构。每当 
有异常发生时,SEH机制被激活。然后SEH通过TIB/TEB找到ER链,并搜寻合适的异常处理例 
程。 
下面我们看一个简单的程序,这个程序演示了怎样利用SEH来除错。 
format  PE GUI 4.0 
entry   __start 

section '.text' code    readable executable 
    __start: 
        xor     eax,eax 
        xchg    [eax],eax 
        ret 
运行程序,发现产生了异常,下面我们把上面的代码前面加上这两句: 
        push    dword [fs:0] 
        mov     [fs:0],esp 
再次运行程序,怎么样?程序正常退出了。打开SOFTICE并加载该程序进行调试。查看ESP指 
向的地址: 
: d esp2 
0023:0006FFC4 C7 14 E6 77 .. 
可知程序RET后的返回地址为77e614c7h,所以查看这个地址处的代码: 
: u 77e614c7 
001B:77E614C7 PUSH EAX 
001B:77E614C8 CALL Kernel32! ExitThread 
可见,程序被加载到内存后栈顶的双字指向ExitThread,我们的程序就是简单地把这个函数 
当做了异常处理例程。这样当有异常发生是程序便退出了,没有了那个讨厌的异常对话框。 
当然,我们利用SEH的目的并不是简单地让程序在发生错误时直接退出。多数教程在将SEH时 
都会举除0错误并用SEH除错的例子。这样的例子太多了,google上可以搜到很多,所以这里 
我就不做无用功了 :P 下面的例子演示了一个利用SEH解密的例子: 
format  PE GUI 4.0 
entry   __start 


; code section... 

section '.text' code    readable writeable executable 
    _decript: 
            mov     ecx,encripted_size              ; decript 
            mov     esi,encripted 
            mov     edi,esi 
        decript: 
            lodsb 
            xor     al,15h 
            stosb 
            loop    decript 
            mov     eax,[esp+0ch]                   ; context 
            mov     dword [eax+0b8h],encripted 
            xor     eax,eax         ; ExceptionContinueExecution 
            ret 

    __start: 
            lea     eax,[esp-8]                     ; setup seh frame 
            xchg    eax,[fs:0] 
            push    _decript 
            push    eax 
            mov     ecx,encripted_size              ; encript 
            mov     esi,encripted 
            mov     edi,esi 
        encript: 
            lodsb 
            xor     al,15h 
            stosb 
            loop    encript 
            int     3                               ; start decription 
        encripted: 
            xor     eax,eax                         ; simply show a message box 
            push    eax 
            call    push_caption 
            db      'SEH',0 
        push_caption: 
            call    push_text 
            db      'A simple SEH test :P',0 
        push_text: 
            push    eax 
            call    [MessageBox] 
        encripted_size    =   $-encripted 
            ret 


; import section... 

section '.idata' import data    readable 
    ; image import descriptor 
    dd      0,0,0,RVA usr_dll,RVA usr_thunk 
    dd      0,0,0,0,0 
    ; dll name 
    usr_dll     db      'user32.dll',0 
    ; image thunk data 
    usr_thunk: 
        MessageBox      dd      RVA __imp_MessageBox 
                        dd      0 
    ; image import by name 
    __imp_MessageBox    dw      0 
                        db      'MessageBoxA',0 
程序分为三个部分:建立自定义异常处理例程、加密代码、利用SEH解密。下面我们对这三个 
部分分别进行分析。 
程序首先在堆栈上腾出一个ER空间(lea),然后然后使FS:0指向它。之后填充这个ER结构, 
prev字段填为之前的FS:[0],handler字段为自定义的异常处理例程_decript。这样我们就 
完成了SEH的修改。 
下面是代码的加密,这段代码在后面的章节会讲到。这里是简单地把被加密代码的每个字节 
与一个特定的值(程序中是15h)相异或(再次异或即解密),这就是最简单的加密手段。 
之后我们用int 3引发一个异常,这时我们的_decript被激活,我们使用与加密完全相同的 
代码解密。到这里,我们还是在复习前面的知识 :P 后面的代码有点费解了,没关系,让我 
们来慢慢理解 :P 
我们先来看看SEH要求的异常处理例程回调函数原形: 
VOID WINAPI (*_STRUCTURED_EXCEPTION_HANDLER) ( 
    PEXCEPTION_RECORD pExceptionRecord, 
    PEXCEPTION_REGISTRATION pSEH, 
    PCONTEXT pContext, 
    PEXCEPTION_RECORD pDispatcherContext 
); 
我们先来看一下EXCEPTION_RECORD结构: 
typedef struct _EXCEPTION_RECORD { 
    DWORD ExceptionCode; 
    DWORD ExceptionFlags; 
    struct _EXCEPTION_RECORD *ExceptionRecord; 
    PVOID ExceptionAddress; 
    DWORD NumberParameters; 
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; 
} EXCEPTION_RECORD, *PEXCEPTION_RECORD; 
ExceptionCode字段定义了产生异常的原因,下面是WinNT.h中对异常的部分定义: 
... 
#define STATUS_GUARD_PAGE_VIOLATION      ((DWORD   )0x80000001L) 
#define STATUS_DATATYPE_MISALIGNMENT     ((DWORD   )0x80000002L) 
#define STATUS_BREAKPOINT                ((DWORD   )0x80000003L) 
#define STATUS_SINGLE_STEP               ((DWORD   )0x80000004L) 
#define STATUS_ACCESS_VIOLATION          ((DWORD   )0xC0000005L) 
#define STATUS_IN_PAGE_ERROR             ((DWORD   )0xC0000006L) 
#define STATUS_INVALID_HANDLE            ((DWORD   )0xC0000008L) 
#define STATUS_NO_MEMORY                 ((DWORD   )0xC0000017L) 
#define STATUS_ILLEGAL_INSTRUCTION       ((DWORD   )0xC000001DL) 
... 
我们并不太关心这个结构的其他字段。下面我们需要理解的是CONTEXT结构。我们知道Win- 
dows为线程循环地分配时间片,当一个线程被挂起后,为了以后它还可以恢复运行,系统必 
须保存其线程环境。对一个线程来说,其环境就是各个寄存器的值,只要寄存器的值不变其 
线程环境就没有变。所以只需要把这个线程的寄存器状态保存下来就可以了。Windows用一个 
CONTEXT结构来保存这些寄存器的状态。下面是WinNT.h中对CONTEXT的定义: 
typedef struct _CONTEXT { 
    DWORD ContextFlags; 
    DWORD   Dr0; 
    DWORD   Dr1; 
    DWORD   Dr2; 
    DWORD   Dr3; 
    DWORD   Dr6; 
    DWORD   Dr7; 
    FLOATING_SAVE_AREA FloatSave; 
    DWORD   SegGs; 
    DWORD   SegFs; 
    DWORD   SegEs; 
    DWORD   SegDs; 
    DWORD   Edi; 
    DWORD   Esi; 
    DWORD   Ebx; 
    DWORD   Edx; 
    DWORD   Ecx; 
    DWORD   Eax; 
    DWORD   Ebp; 
    DWORD   Eip; 
    DWORD   SegCs; 
    DWORD   EFlags; 
    DWORD   Esp; 
    DWORD   SegSs; 
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; 
} CONTEXT, *PCONTEXT; 
最后我们再来说一下这个异常处理过程的返回值,这个返回值决定了程序下一步的执行情 
况,很多人在刚刚接触SEH的时候总是忽略这个返回值,导致程序不能得到正确的结果,我 
就犯过这样的错误 :P 
SEH异常处理例程的返回值有4种定义: 
ExceptionContinueExecution(=0):返回后系统把线程环境设置为CONTEXT的状态后继续 
执行。 
ExceptionContinueSearch(=1):表示这个异常处理例程拒绝处理这个异常,系统会根据 
ER的prev字段搜索下一个异常处理例程并调用它。 
ExceptionNestedException(=2):表示发生了嵌套异常,即异常处理例程中发生了新的异 
常。 
ExceptionCollidedUnwind(=3):发生了嵌套的展开。 
现在我们再回过头来看我们刚才的代码,程序中首先通过mov eax,[esp+0ch]得到CONTEXT结 
构,然后通过mov dword [eax+0b8h],encripted把encripted的地址写到CONTEXT的Eip字段 
中。这样,当这个异常处理以ExceptionContinueExecution返回时程序就会执行Eip处开始 
的代码了。而异常处理中的代码是很难动态跟踪的,我们可以利用SEH的这个特点骗过一些 
杀毒软件 :P 
好了,又结束了一节 :D 我们距离真正编写一个病毒已经不远了,让我们来看看我们还需要 
什么 :P
posted on 2008-10-13 22:47  一个人的天空@  阅读(430)  评论(0编辑  收藏  举报