翻译作者:天魔降临
原始链接:http://www.ph4nt0m.org/bbs/showthread.php?s=&threadid=34318
PS:翻译作者话:
事先声明这片文章并非我的原创,你可以在这个地址
http://www.codeproject.com/system/inject2exe.asp
察看到原文,由于本人能力有限,可能有一些翻译错误请见谅(PS:感谢ROOTKIT上的朋友告诉我这片文章)
Download
PE Viewer
PE Maker - Step 1
PE Maker - Step 2 - Travel towards OEP
PE Maker - Step 3 - Support Import Table.
PE Maker - Step 4 - Support DLL and OCX
PE Maker - Step 5- Final work.
CALC.EXE - test file
0序文
也许你想要了解一个病毒,注入到程序内部并且感染他的方法,或者你对保护你特殊的PE文件的数据感兴趣。你能使用这篇文章的源代码构建你自定义的EXE BUILDER。如果用在好的方面,他能教你怎样保护或封装加密你的PE文件,但是同样如果你用在邪恶的方面,他能产生一个病毒。然而,我写这篇文章的目的是前者,所以,我不会为不道德的使用负责。
1预备知识
按照主题这篇文章不需要特殊的预备知识,如果你已经了解了DEBUGGER和文件结构,那么我建议你跳过2,3部分,这两部分是为毫无基础的人准备的。
2.PE文件的结构
规定PE文件的结构为WINDOWS OS提供了最好的方式去执行代码,并且储存一个程序运行所需要的基本数据。例如常量,变量等等。如图:
2.1MS-DOS的数据
MS-DOS数据由你的可执行文件调用DOS内部的一个函数完成。并且 the MS-DOS Stub program让他显示:
This program can not be run in MS-DOS mode" 或"This program can be run only in Windows mode的字样,或者当你试图在 MS-DOS 6.0中运行一个windows文件时也回显示类似的字样。MS-DOS数据最有意思的部分是"MZ"!
对于我,在 MS-DOS 数据中仅仅PE署名的偏移是重要的,借助于他我能找到WINDOWS NT数据的位置。
我建议你在看看上图。然后看看 文件中IMAGE_DOS_HEADER 的结构。
e_lfanew是一个与WINDOWS NT数据有关的偏移。在这我提供你一个显示EXE文件头信息的工具。
PE Viewer
Download source files - 132 Kb
2.2 The Windows NT data
正如前一节提及的e_lfanew储存者有关WINDOWS NT数据位置的信息,假设 pMem 指针与指向一个PE文件内存空间的起始点时,借助于下面的代码你既能找会DOS 头,也能找到WINDOWS NT 头
Code:
IMAGE_DOS_HEADER image_dos_header
;
IMAGE_NT_HEADERS image_nt_headers
;
PCHAR pMem
;
… memcpy
(&
image_dos_header
,
pMem
,
sizeof
(
IMAGE_DOS_HEADER
));
memcpy
(&
image_nt_headers
,
pMem
+
image_dos_header
.
e_lfanew
,
sizeof
(
IMAGE_NT_HEADERS
));
|
似乎找回头信息是很容易的事,我建议去看MSDN中关于IMAGE_NT_HEADERS 的说明。现在你应该很了解WINDOW NT结构了,他包含PE署名,the File Header,和the Optional Header。
下面是大多数环境中IMAGE_NT_HEADERS的结构:
PRE lang
=
c
++>
FileHeader
->
NumberOfSections OptionalHeader
->
AddressOfEntryPoint OptionalHeader
->
ImageBase OptionalHeader
->
SectionAlignment OptionalHeader
->
FileAlignment OptionalHeader
->
SizeOfImage OptionalHeader
->
DataDirectory
[
IMAGE_DIRECTORY_ENTRY_IMPORT
]->
VirtualAddress OptionalHeader
->
DataDirectory
[
IMAGE_DIRECTORY_ENTRY_IMPORT
]->
Size
PRE
>
|
你能清晰的观察到,这些值的目的,以及当为一个EXE文件分配虚拟内存时,他们的脚色。我要对PE数据目录进行一个简单的说明。当你通过Windows NT 信息对Optional header 进行观察时,你会发现在Optional Header的结尾处有十六个目录,再那你能找到连续的目录,包括一些与虚拟内存和其大小有关的信息。
Code:
#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_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
|
对于我们,如果你想观察相对虚拟内存地址和数据尺寸,下面的代码是足够的:
Code:
DWORD dwRVA
=
image_nt_headers
.
OptionalHeader
->
DataDirectory
[
IMAGE_DIRECTORY_ENTRY_RESOURCE
]->
VirtualAddress
;
DWORD dwSize
=
image_nt_headers
.
OptionalHeader
->
DataDirectory
[
IMAGE_DIRECTORY_ENTRY_RESOURCE
]->
Size
;
|
2.3 The Section Headers and Sections(节头和节)
现在我们观察PE文件怎样声明节的位置和尺寸。为了更好的理解节头(Section header)和节区的特征,我建议你去看看IMAGE_SECTION_HEADER在MSDN定义的结构,对于一个EXE packer的开发者VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData和Characteristics单元有着严格的规则。当开发一个exe packer,你应该非常了解他们。当你修改他们时,有许多你要注意,你要按OptionalHeader->SectionAlignment的顺序小心排列VirtualSize和VirtualAddress同样SizeOfRawData和PointerToRawData要按OptionalHeader->FileAlignment的顺序排列。否则你将损坏目标EXE文件,并且不能运行他们。至于Characteristics,更要格外小心,你要按IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_INITIALIZED_DATA的顺序建立一个节区。我更喜欢我的新节区在进程运行时有初始化这些数据的能力。当然引入表也要如此。此外,我还需要他能利用loader修改他自己。同样你要考虑好新的节区名,这样你能仅通过名字就知道他们的作用。如表二
表二
为了更好的理解节头和节区,请运行PE viewer。借助于他,你能了解一个文件映像中节头的应用。要想观察到虚拟内存,你应该用调试器载入一个PE文件。请记住无论什么时候在PE文件中删除或添加节区,注意调整PE文件的NumberOfSections( IMAGE_NT_HEADERS-> FileHeader->NumberOfSections)他说明了节区的数目。
3 DEBUGER、DISASSEMBLER和一些有用的工具
3.1 DEBUGER
要想变成一个PE工具的开发者,对BUG跟踪程序的使用经验是一个必备条件。此外,你还不得知道一些必备的汇编指令,对于我们INTEL公司的文档是最好的参考书,你可以到INTEL的网站获得他们。
IA-32 Intel Architecture Software Developer’s Manuals.
Intel Itanium Architecture Assembly Language Reference Guide.
The Intel Itanium Processor Developer Resource Guide.
对于PE文件的追踪,我认为SOFTICE是最好的调试工具。借助与内核模式我们可以进行进程追踪而无须应用API函数另外,我还要介绍在一个调试器,他在USER模式下使用有很好的效果,他利用API函数跟踪PE文件,并把自身进程依附在一个活跃的进程中。在WINDOWS内核的库中,微软提供了这些API函数,为了跟踪特定的进程,你要使用调试工具。一些重要的API函数包括 CreateThread(), CreateProcess(), OpenProcess(), DebugActiveProcess(), GetThreadContext(), SetThreadContext(), ContinueDebugEvent(), DebugBreak(), ReadProcessMemory(), WriteProcessMemory(), SuspendThread(), and ResumeThread().
3.1.2调试器的那些部分是重要的 (偶省掉了一些没用的部分)
前面我介绍了两个调试器,但是我并没有说明怎样使用他们。我建议你去看他们的HELP文档,但我还是想简要的说明一些重要的部分。
1。Registers viewer.
Code:
EAX ECX EDX EBX ESP EBP ESI EDI EIP o d t s z a p c
|
2。Disassembler or Code viewer
Code:
010119E0 PUSH EBP 010119E1 MOV EBP
,
ESP 010119E3 PUSH
-
1 010119E5 PUSH 01001570 010119EA PUSH 01011D60 010119EF MOV EAX
,
DWORD PTR FS
:[
0
]
010119F5 PUSH EAX 010119F6 MOV DWORD PTR FS
:[
0
],
ESP 010119FD ADD ESP
,-
68 01011A00 PUSH EBX 01011A01 PUSH ESI 01011A02 PUSH EDI 01011A03 MOV DWORD PTR SS
:[
EBP
-
18
],
ESP 01011A06 MOV DWORD PTR SS
:[
EBP
-
4
],
0
|
3.Memory watcher.
Code:
0023
:
01013000 00 00 00 00 00 00 00 00
-
00 00 00 00 00 00 00 00
................
0023
:
01013010 01 00 00 00 20 00 00 00
-
0A 00 00 00 0A 00 00 00
................
0023
:
01013020 20 00 00 00 00 00 00 00
-
53 63 69 43 61 6C 63 00
........
SciCalc
.
0023
:
01013030 00 00 00 00 00 00 00 00
-
62 61 63 6B 67 72 6F 75
........
backgrou 0023
:
01013040 6E 64 00 00 00 00 00 00
-
2E 00 00 00 00 00 00 00 nd
..............
|
4.Stack viewer.
Code:
0010
:
0007FFC4 4F 6D 81 7C 38 07 91 7C
-
FF FF FF FF 00 90 FD 7F Om
|
8 ‘
| .
0010
:
0007FFD4 ED A6 54 80 C8 FF 07 00
-
E8 B4 F5 81 FF FF FF FF T
.
0010
:
0007FFE4 F3 99 83 7C 58 6D 81 7C
-
00 00 00 00 00 00 00 00 Xm
|........
0010
:
0007FFF4 00 00 00 00 E0 19 01 01
-
00 00 00 00 00 00 00 00
.... ....
|
5.Command line, command buttons, or shortcut keys to follow the debugging process.
Code:
Command SoftICE OllyDbg Run F5 F9 Step Into F11 F7 Step Over F10 F8 Set
Break
Point F8 F2
|
3.2一些有用的工具
一个好的PE开发者要熟悉一些工具来节省他们的时间,所以我替大家选择了一些有用的工具,来分析可执行文件。
3.2.1 LordPE
LordPE仍然是获得PE文件信息并修改他们的首选工具。
3.3.2 PEiD
PEiD对于分析编译器,壳种类,入口点来说是一款很好的工具。到现在为止,他能侦测500种以上的PE文
件类型。
3.3.3 Resource Hacker
他能用来修改一些资源数据信息,如图标,菜单,版本等等。
3.3.4 WinHex
3.3.5 CFF Explorer
他支持PE32/64,PE重建包括CIL文件,换句话说就是.NET文件。他使用更方便,功能更强大。
4添加新的节区并改变EPO
我们已经完成了工作的第一步。所以我添加了一个新的节区,并重建了PE文件。在开始前我想让你借助OD熟悉PE头。用OD载入文件,选择View->Executable file,Special->PE header,你将看到类似图3的画面。现在在主菜单选择View->Memory,试着在内存映射窗口区别各个节区。
我想向你解释,我们怎样改变举例文件中的入口点偏移量(CALC.EXE)。首先,使用PE工具,找到入口点 0x00012475,并且映像基地址为 0x01000000,这个OEP是虚拟地址的相对地址,所以映像基地址来转变把他为虚拟地址。
Code:
Virtual_Address = Image_Base + Relative_Virtual_Address
DWORD OEP_RVA = image_nt_headers->OptionalHeader.AddressOfEntryPoint ; // OEP_RVA = 0x00012475 DWORD OEP_VA = image_nt_headers->OptionalHeader.ImageBase + OEP_RVA ; // OEP_VA = 0x01000000 + 0x00012475 = 0x01012475
|
PE Maker - 1
Download test PE file - 49.5 Kb
in loader.cpp文件中的DynLoader()函数用来保存新的节的数据
Code:
__stdcall void DynLoader() { _asm { //---------------------------------- DWORD_TYPE(DYN_LOADER_START_MAGIC) //---------------------------------- MOV EAX,01012475h // << Original OEP JMP EAX //---------------------------------- DWORD_TYPE(DYN_LOADER_END_MAGIC) //---------------------------------- } }
|
不幸的是这个源代码只能在sample test file. 中应用,我们应该借助于在新的节区保存前面的OEP的值,来完善他,并使用他到达真正的OEP。我会在在 Step 2 完善他
4.1找回并重建PE文件
为了使用新的PE文件,我写了一个简单的类库文件,来修复PE信息。
Code:
CPELibrary Class Step 1 ---------------------------------------------------------------- class CPELibrary { private: //----------------------------------------- PCHAR pMem; DWORD dwFileSize; //----------------------------------------- protected: //----------------------------------------- PIMAGE_DOS_HEADER image_dos_header; PCHAR pDosStub; DWORD dwDosStubSize, dwDosStubOffset; PIMAGE_NT_HEADERS image_nt_headers; PIMAGE_SECTION_HEADER image_section_header[MAX_SECTION_NUM]; PCHAR image_section[MAX_SECTION_NUM]; //----------------------------------------- protected: //----------------------------------------- DWORD PEAlign(DWORD dwTarNum,DWORD dwAlignTo); void AlignmentSections(); //----------------------------------------- DWORD Offset2RVA(DWORD dwRO); DWORD RVA2Offset(DWORD dwRVA); //----------------------------------------- PIMAGE_SECTION_HEADER ImageRVA2Section(DWORD dwRVA); PIMAGE_SECTION_HEADER ImageOffset2Section(DWORD dwRO); //----------------------------------------- DWORD ImageOffset2SectionNum(DWORD dwRVA); PIMAGE_SECTION_HEADER AddNewSection(char* szName,DWORD dwSize); //----------------------------------------- public: //----------------------------------------- CPELibrary(); ~CPELibrary(); //----------------------------------------- void OpenFile(char* FileName); void SaveFile(char* FileName); //----------------------------------------- };
|
借助于表1,image_dos_header, pDosStub, image_nt_headers, image_section_header [MAX_SECTION_NUM], 和image_section[MAX_SECTION_NUM] 的用法是很清晰的。我们使用OpenFile()和SaveFile()来重新得到并重建PE文件此外,一个重要的阶段是使用AddNewSection()来创造新的节区。
4.2为新的节区添加数据
在pecrypt.cpp中,我构建了另一个类,CPECryptor来包含新节区的数据。新节区的数据由loader.cpp文件中的DynLoader()函数构建。DynLoader Step 1.我们也使用CPECryptor类来填充其他资料。
Code:
CPECryptor Class Step 1 ---------------------------------------------------------------- class CPECryptor: public CPELibrary { private: //---------------------------------------- PCHAR pNewSection; //---------------------------------------- DWORD GetFunctionVA(void* FuncName); void* ReturnToBytePtr(void* FuncName, DWORD findstr); //---------------------------------------- protected: //---------------------------------------- public: //---------------------------------------- void CryptFile(int(__cdecl *callback) (unsigned int, unsigned int)); //---------------------------------------- }; //---------------------------------------------------------------- |
4.3一些关于构建新的PE文件的说明
用一下片断来排列虚拟地址和每个节区的虚拟大小
Code:
image_section_header[i]->VirtualAddress= PEAlign(image_section_header[i]->VirtualAddress, image_nt_headers->OptionalHeader.SectionAlignment);
image_section_header[i]->Misc.VirtualSize= PEAlign(image_section_header[i]->Misc.VirtualSize, image_nt_headers->OptionalHeader.SectionAlignment);
|
Align the PointerToRawData and the SizeOfRawData of each section by FileAlignment
Code:
image_section_header[i]->PointerToRawData = PEAlign(image_section_header[i]->PointerToRawData, image_nt_headers->OptionalHeader.FileAlignment);
image_section_header[i]->SizeOfRawData = PEAlign(image_section_header[i]->SizeOfRawData, image_nt_headers->OptionalHeader.FileAlignment);
|
Correct the SizeofImage by the virtual size and the virtual address of the last section
Code:
image_nt_headers->OptionalHeader.SizeOfImage = image_section_header[LastSection]->VirtualAddress + image_section_header[LastSection]->Misc.VirtualSize;
|
Set the Bound Import Directory header to zero, as this directory is not very important to execute a PE file:
Code:
image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]. VirtualAddress = 0; image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0;
|
4.4一些关于连接这个VC程序的解释
Set Linker->General->Enable Incremental Linking to No (/INCREMENTAL:NO).
你能了解增加连接和不增加连接之间的不同
为了获得DynLoader(), 的虚拟地址,再增加LINK中我们获得JMP pemaker.DynLoader 的虚拟地址。但是不使用LINK时用以下代码获得地址
Code:
DWORD dwVA= (DWORD) DynLoader;
|
This setting is more critical in the incremental link when you try to find the beginning and ending of the Loader,
DynLoader(), by CPECryptor::ReturnToBytePtr():(抱歉,这句话偶不知道如何才能翻译通顺
)
Code:
void* CPECryptor::ReturnToBytePtr(void* FuncName, DWORD findstr) { void* tmpd; __asm { mov eax, FuncName jmp df hjg: inc eax df: mov ebx, [eax] cmp ebx, findstr jnz hjg mov tmpd, eax } return tmpd; }
|
5.保存重要的数据和延伸原入口点现在,我们已经保存了原入口点,并映射了基地址以便于到达虚拟地址入口点。在DynLoader()结尾处我已经保存一个空白区来储存这些重要的数据。DynLoader Step 2.
PE Maker - Step 2Download source files - 58.3 Kb Code:
DynLoaderStep2>DynLoader Step 2
__stdcall void DynLoader() { _asm { //---------------------------------- DWORD_TYPE(DYN_LOADER_START_MAGIC) //---------------------------------- Main_0: PUSHAD // get base ebp CALL Main_1 Main_1: POP EBP SUB EBP,OFFSET Main_1 MOV EAX,DWORD PTR [EBP+_RO_dwImageBase] ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint] PUSH EAX RETN // >> JMP to Original OEP //---------------------------------- DWORD_TYPE(DYN_LOADER_START_DATA1) //----------------------------------<FONT color=red> _RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC) _RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC)</FONT> //---------------------------------- DWORD_TYPE(DYN_LOADER_END_MAGIC) //---------------------------------- } }
|
5.1恢复开始时寄存器间的关系
恢复他们间的关系是重要的,但在DynLoader Step 2的源代码中我们还没有做这件事。我们可以修改DynLoader() 函数来重建开始的关系
Code:
__stdcall void DynLoader() { _asm { //---------------------------------- DWORD_TYPE(DYN_LOADER_START_MAGIC) //---------------------------------- Main_0: <FONT color=red>PUSHAD// Save the registers context in stack</FONT> CALL Main_1 Main_1: POP EBP// Get Base EBP SUB EBP,OFFSET Main_1 MOV EAX,DWORD PTR [EBP+_RO_dwImageBase] ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint] MOV DWORD PTR [ESP+1Ch],EAX // pStack.Eax <- EAX <FONT color=red>POPAD // Restore the first registers context from stack</FONT> PUSH EAX XOR EAX, EAX RETN // >> JMP to Original OEP //---------------------------------- DWORD_TYPE(DYN_LOADER_START_DATA1) //---------------------------------- _RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC) _RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC) //---------------------------------- DWORD_TYPE(DYN_LOADER_END_MAGIC) //---------------------------------- } }
|
5.2恢复最初的堆栈
我们能利用设置在堆栈开始处的值加0x34到原入口点,来恢复原始的堆栈,但这不是很重要。然而在下面的代码中我用了一个简单的技巧来到达OEP以便于修改堆栈,你能利用OD来跟踪执行过程。
Code:
__stdcall void DynLoader() { _asm { //---------------------------------- DWORD_TYPE(DYN_LOADER_START_MAGIC) //---------------------------------- Main_0: PUSHAD // Save the registers context in stack CALL Main_1 Main_1: POP EBP SUB EBP,OFFSET Main_1 MOV EAX,DWORD PTR [EBP+_RO_dwImageBase] ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint] MOV DWORD PTR [ESP+54h],EAX // pStack.Eip <- EAX POPAD // Restore the first registers context from stack CALL _OEP_Jump DWORD_TYPE(0xCCCCCCCC) _OEP_Jump: PUSH EBP MOV EBP,ESP MOV EAX,DWORD PTR [ESP+3Ch] // EAX <- pStack.Eip MOV DWORD PTR [ESP+4h],EAX // _OEP_Jump RETURN pointer <- EAX XOR EAX,EAX LEAVE RETN //---------------------------------- DWORD_TYPE(DYN_LOADER_START_DATA1) //---------------------------------- _RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC) _RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC) //---------------------------------- DWORD_TYPE(DYN_LOADER_END_MAGIC) //---------------------------------- } }
|
5.3Approach OEP by Structured Exception Handling(借助于构造异常处理来接近OEP)
当程序执行了错误的代码就会抛出一个异常,在这种条件下,程序迅速跳转到一个叫做异常处理的功能中。
下面的文件包含了异常处理的调用,异常的抛出,以及其功能。
Code:
#include "stdafx.h" #include "windows.h"
void RAISE_AN_EXCEPTION() { _asm { INT 3 INT 3 INT 3 INT 3 } }
int _tmain(int argc, _TCHAR* argv[]) { __try { __try{ printf("1: Raise an Exception\n"); RAISE_AN_EXCEPTION(); } __finally { printf("2: In Finally\n"); } } __except( printf("3: In Filter\n"), EXCEPTION_EXECUTE_HANDLER ) { printf("4: In Exception Handler\n"); } return 0; }
|
;Code:
main() 00401000: PUSH EBP 00401001: MOV EBP,ESP 00401003: PUSH -1 00401005: PUSH 00407160 ; __try { ; the structured exception handler (SEH) installation 0040100A: PUSH _except_handler3 0040100F: MOV EAX,DWORD PTR FS:[0] 00401015: PUSH EAX 00401016: MOV DWORD PTR FS:[0],ESP 0040101D: SUB ESP,8 00401020: PUSH EBX 00401021: PUSH ESI 00401022: PUSH EDI 00401023: MOV DWORD PTR SS:[EBP-18],ESP ; __try { 00401026: XOR ESI,ESI 00401028: MOV DWORD PTR SS:[EBP-4],ESI 0040102B: MOV DWORD PTR SS:[EBP-4],1 00401032: PUSH OFFSET "1: Raise an Exception" 00401037: CALL printf 0040103C: ADD ESP,4 ; the raise a exception, INT 3 exception ; RAISE_AN_EXCEPTION() 0040103F: INT3 00401040: INT3 00401041: INT3 00401042: INT3 ; } __finally { 00401043: MOV DWORD PTR SS:[EBP-4],ESI 00401046: CALL 0040104D 0040104B: JMP 00401080 0040104D: PUSH OFFSET "2: In Finally" 00401052: CALL printf 00401057: ADD ESP,4 0040105A: RETN <FONT color=black>; } <FONT color=black>; } <FONT color=black>; __except( 0040105B: JMP 00401080 0040105D: PUSH OFFSET "3: In Filter" 00401062: CALL printf 00401067: ADD ESP,4 0040106A: MOV EAX,1 ; EXCEPTION_EXECUTE_HANDLER = 1 0040106F: RETN ; , EXCEPTION_EXECUTE_HANDLER ) ; } ; the exception handler funtion 00401070: MOV ESP,DWORD PTR SS:[EBP-18] 00401073: PUSH OFFSET "4: In Exception Handler" 00401078: CALL printf 0040107D: ADD ESP,4 ; } 00401080: MOV DWORD PTR SS:[EBP-4],-1 0040108C: XOR EAX,EAX ; restore previous SEH 0040108E: MOV ECX,DWORD PTR SS:[EBP-10] 00401091: MOV DWORD PTR FS:[0],ECX 00401098: POP EDI 00401099: POP ESI 0040109A: POP EBX 0040109B: MOV ESP,EBP 0040109D: POP EBP 0040109E: RETN
|
建立一个控制台工程,连接并执行先前的文件,观察结果。
Code:
1: Raise an Exception 3: In Filter 2: In Finally 4: In Exception Handler _
|
程序执行了一个异常的表达式printf("3: In Filter\n");,当异常发生,(在这个例子INT 3 异常,你也能用另外的异常),在OD中Debugging options->Exceptions你能看到不同类型异常的清单
5.3.1 Implement Exception Handler
我们希望建立一个异常处理来到达程序的OEP,借助前满的代码,你现在已经能区别SEH安装,异常抛出,异常表达式过滤,为了建立我们的异常接近(OEP),我们需要下面的代码
SEH installation
Code:
LEA EAX,[EBP+_except_handler1_OEP_Jump] PUSH EAX PUSH DWORD PTR FS:[0] MOV DWORD PTR FS:[0],ESP
|
异常抛出
Code:
异常表达式过滤
Code:
except_handler1_OEP_Jump: PUSH EBP MOV EBP,ESP ... MOV EAX, EXCEPTION_CONTINUE_SEARCH // EXCEPTION_CONTINUE_SEARCH = 0 LEAVE RETN</
|
所以我们利用ASM嵌套来到达OEP
Code:
__try // SEH installation { __asm { INT 3 // An Exception Raise } } __except( ..., EXCEPTION_CONTINUE_SEARCH ){} // Exception handler expression filter
|
ASM代码
Code:
;---------------------------------------------------- ; the structured exception handler (SEH) installation ; __try { LEA EAX,[EBP+_except_handler1_OEP_Jump] PUSH EAX PUSH DWORD PTR FS:[0] MOV DWORD PTR FS:[0],ESP <FONT color=green>; ---------------------------------------------------- ; the raise a INT 3 exception INT 3 INT 3 INT 3 INT 3 ; } ; __except( ... ; ---------------------------------------------------- ; exception handler expression filter _except_handler1_OEP_Jump: PUSH EBP MOV EBP,ESP ... MOV EAX, EXCEPTION_CONTINUE_SEARCH ; EXCEPTION_CONTINUE_SEARCH = 0 LEAVE RETN ;, EXCEPTION_CONTINUE_SEARCH ) { }
|
异常值__except(..., Value), 定义了这个异常如何被处理,他能由三个值分别是1,0,-1。为了理解他们请在MSDN Library中寻找有关
try-except 声明的描述。当我们把他设置为0时,就不会执行异常处理的功能,因此,借助于这个值,异常处理就会被忽略,并且线程继续执行其他的代码。
如何安装SEH就像你从前面的代码中看到的,SEH是借助于FS段寄存器还完成安装的。WIN32使用FS段寄存器作为主线程中数据块的指针。开始的0X1C字节包含Thread Information Block (TIB)的信息,此外,FS:[00h] 在与主线程的ExceptionList 有关(表三),在我们的代码中,我们已经将_except_handler1_OEP_Jump指针压入堆栈,并改变ExceptionList, FS:[00h]的值为堆栈起始的值,ESP线程信息块(TIB)
Code:
typedef struct _NT_TIB32 { DWORD ExceptionList; DWORD StackBase; DWORD StackLimit; DWORD SubSystemTib; union { DWORD FiberData; DWORD Version; }; DWORD ArbitraryUserPointer; DWORD Self; } NT_TIB32, *PNT_TIB32;
|
表三 FS段寄存器和线程信息块
5.3.2借助于调整线程间的关系获得OEP在这部分,我们将完成OEP的接近,并改变线程间的关系,忽略每个简单的异常处理。让线程在先前的OEP中继续执行代码。当产生异常时,进程间的关系就被保存在堆栈中,借助EXCEPTION_POINTERS,我们能访问ContextRecord的指针。ContextRecord拥有CONTEXT的数据结构(表四),这便是在执行时间中的线程间的关系。当我们使用 EXCEPTION_CONTINUE_SEARCH (0)来忽略异常时,指令指针间的关系也被设置到ContextRecord,以便程序可以返回先前的环境中去。因此,如果我们改变 Win32 Thread Context中的EIP,使其为开始的OEP,这便可以进入OEP。
表四
Code:
MOV EAX, ContextRecord MOV EDI, dwOEP ; EAX <- dwOEP MOV DWORD PTR DS:[EAX+0B8h], EDI ; pContext.Eip <- EAX
|
WIN32线程间关系的数据结构Code:
#define MAXIMUM_SUPPORTED_EXTENSION 512
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, *LPCONTEXT;
|
用下面的代码,我们就能使用异常处理程序来到达OEP
Code:
__stdcall void DynLoader() { _asm { //---------------------------------- DWORD_TYPE(DYN_LOADER_START_MAGIC) //---------------------------------- Main_0: PUSHAD // Save the registers context in stack CALL Main_1 Main_1: POP EBP SUB EBP,OFFSET Main_1 // Get Base EBP MOV EAX,DWORD PTR [EBP+_RO_dwImageBase] ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint] MOV DWORD PTR [ESP+10h],EAX // pStack.Ebx <- EAX LEA EAX,[EBP+_except_handler1_OEP_Jump] MOV DWORD PTR [ESP+1Ch],EAX // pStack.Eax <- EAX POPAD // Restore the first registers context from stack //---------------------------------------------------- // the structured exception handler (SEH) installation PUSH EAX XOR EAX, EAX PUSH DWORD PTR FS:[0] // NT_TIB32.ExceptionList MOV DWORD PTR FS:[0],ESP // NT_TIB32.ExceptionList <-ESP //---------------------------------------------------- // the raise a INT 3 exception DWORD_TYPE(0xCCCCCCCC) //-------------------------------------------------------- // -------- exception handler expression filter ---------- _except_handler1_OEP_Jump: PUSH EBP MOV EBP,ESP //------------------------------ MOV EAX,DWORD PTR SS:[EBP+010h] // PCONTEXT: pContext <- EAX //============================== PUSH EDI // restore original SEH MOV EDI,DWORD PTR DS:[EAX+0C4h] // pContext.Esp PUSH DWORD PTR DS:[EDI] POP DWORD PTR FS:[0] ADD DWORD PTR DS:[EAX+0C4h],8 // pContext.Esp //------------------------------ // set the Eip to the OEP MOV EDI,DWORD PTR DS:[EAX+0A4h] // EAX <- pContext.Ebx MOV DWORD PTR DS:[EAX+0B8h],EDI // pContext.Eip <- EAX //------------------------------ POP EDI //============================== MOV EAX, EXCEPTION_CONTINUE_SEARCH LEAVE RETN //---------------------------------- DWORD_TYPE(DYN_LOADER_START_DATA1) //---------------------------------- _RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC) _RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC) //---------------------------------- DWORD_TYPE(DYN_LOADER_END_MAGIC) //---------------------------------- } }
|
6.构建一个引入表,并重建开始的引入表我们有两种方法在应用程序中使用DLL文件:
.通过额外的支持使用库文件.在运行时间中使用DLL文件Code:
// DLL function signature typedef HGLOBAL (*importFunction_GlobalAlloc)(UINT, SIZE_T); ... importFunction_GlobalAlloc __GlobalAlloc;
// Load DLL file HINSTANCE hinstLib = LoadLibrary("Kernel32.dll"); if (hinstLib == NULL) { // Error - unable to load DLL }
// Get function pointer __GlobalAlloc = (importFunction_GlobalAlloc)GetProcAddress(hinstLib, "GlobalAlloc"); if (addNumbers == NULL) { // Error - unable to find DLL function }
FreeLibrary(hinstLib);
|
当你建立一个WINDOWS应用程序的工程,连接器至少包括kernel32.dll,没有kernel32.dll文件中的LoadLibrary()和GetProcAddress()函数我们就不能在运行时间中装载DLL文件,所依赖的信息被储存在引入表的节区内。如果利用Dependency Walker,我们将很容易的观察DLL模块,以及那些被引入PE文件的重要功能。
我打算建立我们自定义的引入表来管理我们的工程,我们必须修改原来的引入表,以便于运行程序中真正的代码。
PE Maker - Step 3Download source files - 65.4 Kb
6.1构件客户端引入表我建议你读一下6.4节
Microsoft Portable Executable and the Common Object File Format Specification 文挡,这节包含了重要的知识来让你了解引入表的性能。利用optional header 中的第二页数据,便可得到引入表数据。因此,你能用下面的代码获得他
Code:
DWORD dwVirtualAddress = image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; DWORD dwSize = image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
|
The VirtualAddress refers to structures by IMAGE_IMPORT_DESCRIPTOR. This structure contains the pointer to the imported DLL name and the relative virtual address of the first thunk(抱歉,THUNK我不知道该如何翻译,所以我无法理解这段的含义
)
Code:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; }; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD <FONT color=red>Name</FONT>; // the imported DLL name DWORD <FONT color=red>FirstThunk</FONT>; // the relative virtual address of the first thunk } IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
|
When a program is running, the Windows task manager sets the thunks by the virtual address of the function. The virtual address is found by the name of the function. At first, the thunks hold the relative virtual address of the function name, Table 5; during execution, they are fixed up by the virtual address of the functions, Table 6.
表五--在文件映像中的引入表表六--在虚拟内存中的引入表我想制作一个简单的引入表来引入Kernel32.dll中的LoadLibrary(),和 GetProcAddress() 函数,在运行时间中我要用这两个基本的API函数来覆盖其他的API函数。下面的ASM代码将展示实现他是多么的容易。
运行后.....6我已经准备了一个类库来制作每个引入表,这将借助于字符串。CITMaker类库在itmaker.h中,他将使用sz_IT_EXE_strings 来建立一个引入表,以及一个相对虚拟地址引入表。
Code:
static const char *sz_IT_EXE_strings[]= { "Kernel32.dll", "LoadLibraryA", "GetProcAddress", 0,, 0, };
|
下面我使用这个类库来建立引入表以支持DLL和OCX,所以这是一个常规库来表现所有可能的引入表。下个一步骤,我用下面的代码来完成。
6.2在运行时间内使用其他的API函数
在这次,我们使用LoadLibrary() 和GetProcAddress()来装载其他的DLL文件,找出其他的API函数的进程地址。
我想有一个完善的被引入函数表,类似于一个真正执行中的EXE文件,如果你看一下一个PE文件的内部,你将发现API的调用是API函数在虚拟地址间的间接跳转完成的。
JMP DWORD PTR [XXXXXXXX]
借助于这个性能我们可以很容易的扩展我们工程的其他部分 ,因此我们建立两个数据表,一个是API函数,另一个是JMP [XXXXXXXX]
Code:
#define __jmp_api byte_type(0xFF) byte_type(0x25) __asm { ... //---------------------------------------------------------------- _p_GetModuleHandle: dword_type(0xCCCCCCCC) _p_VirtualProtect: dword_type(0xCCCCCCCC) _p_GetModuleFileName: dword_type(0xCCCCCCCC) _p_CreateFile: dword_type(0xCCCCCCCC) _p_GlobalAlloc: dword_type(0xCCCCCCCC) //---------------------------------------------------------------- _jmp_GetModuleHandle: __jmp_api dword_type(0xCCCCCCCC) _jmp_VirtualProtect: __jmp_api dword_type(0xCCCCCCCC) _jmp_GetModuleFileName: __jmp_api dword_type(0xCCCCCCCC) _jmp_CreateFile: __jmp_api dword_type(0xCCCCCCCC) _jmp_GlobalAlloc: __jmp_api dword_type(0xCCCCCCCC) //---------------------------------------------------------------- ... }
|
下面的代码我们来安装自定义的引入表
6.3修复最初的引入表
In order to run the program again, we should fix up the thunks of the actual import table, otherwise we have a corrupted target PE file. Our code must correct all of the thunks the same as Table 5 to Table 6. Once more, LoadLibrary() and GetProcAddress() aid us in our effort to reach our intention.(8会翻译,抱歉)
7.支持DLL和OCX下面我将把DLL和OCX加入我们的PE工程中,如果我们注意到两次到达入口点的偏移地址,客户引入表,重新调整后的表的话,要支持他们是很容易的事。
PE Maker - Step 4Download source files - 68.6 Kb7.1 Twice OEP approachDLL或着OCX文件的入口点偏移最少要被主程序接触两次:
.构造[B]
当一个DLL文件被LoadLibrary()函数装载,或者通过调用 DllRegisterServer()函数使用LoadLibaray()和GetProcAddress() 函数对一个OCX文件注册,第一次到达OEP就完成了。
Code:
hinstDLL = LoadLibrary( "test1.dll" );
|
Code:
hinstOCX = LoadLibrary( "test1.ocx" ); _DllRegisterServer = GetProcAddress( hinstOCX, "DllRegisterServer" ); _DllRegisterServer(); // ocx register
|
[B].Destructor(破坏)当主程序使用FreeLibrary()函数释放库文件,就会第二次到达OEP
Code:
Code:
To perform this, I have employed a trick, that causes in the second time again, the instruction pointer (EIP) traveling owards the original OEP by the structured exception handler.(抱歉8会翻译)
我希望在前面的代码中你已经掌握了那个技巧,但是那还不够,我们还要考虑映像基址(ImageBase)的问题,当库文件被主程序载入不同映像基址我们应该写一些代码来找到真正的映像基址,记住他并在将来利用他。
借助于观察堆栈信息,这个代码找到真正的映像基址。通过使用真正的和形式上的映像基址映像基址,我们要修正程序内部所有的内存调用,不要对此担心,通过重新调整表的信息就可以轻松的完成。
7.2 对表进行重新调整为了更好的了解重置表(relocation table ),你应该看一看6.6节中
Microsoft Portable Executable and Common Object File Format Specification 文档的内容。重置表包含许多package来包含在虚拟内存映像中虚拟地址的相关信息。每一个package包含一个8bit的报头以显示虚拟地址基址和数据的数量。下面我用IMAGE_BASE_RELOCATION的数据结构来说明。
Code:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
|
表七表七 表现了重置表的主要概念。此外你能在OD中装载一个DLL或OCX文件来观察重置表,在内存映射窗口中便可观察到".reloc" 节区。使用我们工程中的下面代码,你能找到重置表的位置。
Code:
DWORD dwVirtualAddress = image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]. VirtualAddress; DWORD dwSize = image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
|
借助于OD的Long Hex viewer模式,我们可以看到,在这个例子中虚拟内存基址是0x1000,块大小是0x184。
Code:
008E1000 : 00001000 00000184 30163000 30403028 008E1010 : 30683054 308C3080 30AC309C 30D830CC 008E1020 : 30E030DC 30E830E4 30F030EC 310030F4 008E1030 : 3120310D 315F3150 31A431A0 31C031A8 008E1040 : 31D031CC 31F431EC 31FC31F8 32043200 008E1050 : 320C3208 32143210 324C322C 32583254 008E1060 : 3260325C 32683264 3270326C 32B03274
|
他重新安排了后面的虚拟地址:
Code:
0x1000 + 0x0000 = 0x1000 0x1000 + 0x0016 = 0x1016 0x1000 + 0x0028 = 0x1028 0x1000 + 0x0040 = 0x1040 0x1000 + 0x0054 = 0x1054
|
每个package使用其内部连续的4字节表格来完成表的重置.第一个字节与重置表的类型有关,下面的连续三个字节是那些必须用来纠正映像信息的虚拟地址基址和映像基址的偏移.
我所指的类型是什么?我所说的类型是以下一系列值的一个:
.IMAGE_REL_BASED_ABSOLUTE (0):不操作(NO effect)
.IMAGE_REL_BASED_HIGH (1):重新部署虚拟地址和偏移地址的高十六字节。
.IMAGE_REL_BASED_LOW (2):重新部署虚拟地址和偏移地址的低十六字节。
.IMAGE_REL_BASED_HIGHLOW (3): 重新部署虚拟地址和偏移地址。
在重新部署是都做了什么?通过重新部署,借助".reloc"节区packageS按照现在的映像基址,虚拟内存中的一些值被纠正。
Code:
delta_ImageBase = current_ImageBase - image_nt_headers->OptionalHeader.ImageBase
|
Code:
mem[ current_ImageBase + 0x1000 ] = mem[ current_ImageBase + 0x1000 ] + delta_ImageBase ; mem[ current_ImageBase + 0x1016 ] = mem[ current_ImageBase + 0x1016 ] + delta_ImageBase ; mem[ current_ImageBase + 0x1028 ] = mem[ current_ImageBase + 0x1028 ] + delta_ImageBase ; mem[ current_ImageBase + 0x1040 ] = mem[ current_ImageBase + 0x1040 ] + delta_ImageBase ; mem[ current_ImageBase + 0x1054 ] = mem[ current_ImageBase + 0x1054 ] + delta_ImageBase ; ...
|
I have employed the following code from Morphine packer to implement the relocation.(抱歉实在翻译不通顺)
7.3建立一个特殊的引入表为了支持 OLE-ActiveX控件注册,我们应该给予目标OCX和DLL文件一个特殊的入口表。因此,我用如下的字符串建立了一个引入表:
Code:
const char *sz_IT_OCX_strings[]= { "Kernel32.dll", "LoadLibraryA", "GetProcAddress", "GetModuleHandleA", 0, "User32.dll", "GetKeyboardType", "WindowFromPoint", 0, "AdvApi32.dll", "RegQueryValueExA", "RegSetValueExA", "StartServiceA", 0, "Oleaut32.dll", "SysFreeString", "CreateErrorInfo", "SafeArrayPtrOfIndex", 0, "Gdi32.dll", "UnrealizeObject", 0, "Ole32.dll", "CreateStreamOnHGlobal", "IsEqualGUID", 0, "ComCtl32.dll", "ImageList_SetIconSize", 0, 0, };
|
缺少这些API函数,库文件就不能装载,此外 DllregisterServer() and DllUregisterServer()也不会起作用。在CPECryptor::CryptFile中,在新的目标引入表创造时我对DLL和EXE文件做了区别。
Code:
if(( image_nt_headers->FileHeader.Characteristics & IMAGE_FILE_DLL ) == IMAGE_FILE_DLL ) { ImportTableMaker = new CITMaker( IMPORT_TABLE_OCX ); } else { ImportTableMaker = new CITMaker( IMPORT_TABLE_EXE );
}
|
8.保护线程局部存储使用线程局部存储(TLS),程序能执行一个线状的进程,这个性能通常被Borland使用:Delphi和C++ Builder。当你封装一个PE文件,你应该注意保持TLS清空(you should take care to keep clean the TLS),否则你的packer将不支持Borland Delphi and C++连接成EXE文件。请查阅6.7节的
Microsoft Portable Executable and Common Object File Format Specification 文档
Code:
typedef struct _IMAGE_TLS_DIRECTORY32 { DWORD StartAddressOfRawData; DWORD EndAddressOfRawData; DWORD AddressOfIndex; DWORD AddressOfCallBacks; DWORD SizeOfZeroFill; DWORD Characteristics; } IMAGE_TLS_DIRECTORY32, * PIMAGE_TLS_DIRECTORY32;
|
为了保证TLS目录的安全,我把他拷贝到装载者(loader)内部一个特殊的地方
Code:
... _tls_dwStartAddressOfRawData: dword_type(0xCCCCCCCC) _tls_dwEndAddressOfRawData: dword_type(0xCCCCCCCC) _tls_dwAddressOfIndex: dword_type(0xCCCCCCCC) _tls_dwAddressOfCallBacks: dword_type(0xCCCCCCCC) _tls_dwSizeOfZeroFill: dword_type(0xCCCCCCCC) _tls_dwCharacteristics: dword_type(0xCCCCCCCC) ...
|
It is necessary to correct the TLS directory entry in the Optional Header:(entry怕翻译错,这句就看英文把
)
Code:
if(image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]. VirtualAddress!=0) { memcpy(&pDataTable->image_tls_directory, image_tls_directory, sizeof(IMAGE_TLS_DIRECTORY32)); dwOffset=DWORD(pData1)-DWORD(pNewSection); dwOffset+=sizeof(t_DATA_1)-sizeof(IMAGE_TLS_DIRECTORY32); image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS]. VirtualAddress=dwVirtualAddress + dwOffset; }
|
9.注入你的代码(PS:我已等了很久~~~~~~~~~~)我已经放置了一个"Hello World!" 的代码
Code:
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) ; ...
|
PE Maker - Step 5Download source files - 71.7 Kb