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
---------------
重定位源于代码中的地址操作,如果没有地址操作那么就不存在所谓的重定位了。让我们先
来分析一段代码。考虑如下代码:
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