打造XP下可运行的微型PE文件(292字节)

# 鬼仔:该文作者是《疯狂的程序员》的作者,现在《疯狂的程序员》已经写完了,还没来得及看。

作者:hitetoshi

前几天和朋友交流技术,提到手工打造微型PE文件,他说现在网上流传的大部分版本在XP SP3下都不能运行,于是心血来潮,拍着胸脯说:“你放心,忙完了帮你做一个。”
后来花了半天时间,终于打造出一个XP下可运行的微型PE,弹出一个对话框,292字节,当然这离极限也许还差得远,不过自己做了一次,还是有些心得,贴出来和大家分享一下。本文介绍的这个MiniPE可以在下载:http://download.csdn.net/source/774041

第一步 准备PE文件
先创建一个PE文件,为了尽可能地小,我们用汇编语言来编写。代码如下:

.386
.model flat,stdcall
option casemap:none

.data
byData db 90h

.code
start:

end
代码什么也没做,运行就报错(因为PE文件的EntryPoint实际上指向了不存在的区域),代码我们到后面再来填充它,这个PE文件只包含一个数据节。在Windows XP下,PE文件必须包含至少一个节,否则无法运行,这正是我们为什么要定义byData这个数据的原因。
为了让生成出的PE文件尽可能小,在链接的时候,我们使用/align:4这个选项,指定文件和节为4字节对齐(高版本的Microsoft增量链接器可能不支持4字节对齐,比如我测试的8.0版本,要求至少16字节对齐。所以使用这个选项,应该用低版本的链接器,我用的是MASM32V9自带的链接器,版本是5.12。)
这样生成出来的PE文件只有460字节,这是一个很好的开始,因为大部分极其简单的汇编程序生成出来都会在1.5K左右,如果是高级语言编写的,将会更高。文件的内容如下:
00000000h: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 ; MZ?…….. ..
00000010h: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ; ?……@…….
00000020h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000030h: 00 00 00 00 00 00 00 00 00 00 00 00 A8 00 00 00 ; …………?..
00000040h: 0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68 ; ..?.???L?Th
00000050h: 69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F ; is program canno
00000060h: 74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20 ; t be run in DOS
00000070h: 6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00 ; mode….$…….
00000080h: 5D 17 1D DB 19 76 73 88 19 76 73 88 19 76 73 88 ; ]..?vs?vs?vs?
00000090h: E5 56 61 88 18 76 73 88 52 69 63 68 19 76 73 88 ; 錠a?vs圧ich.vs?
000000a0h: 00 00 00 00 00 00 00 00 50 45 00 00 4C 01 01 00 ; ……..PE..L…
000000b0h: 77 B8 1A 49 00 00 00 00 00 00 00 00 E0 00 0F 01 ; w?I……..?..
000000c0h: 0B 01 05 0C 00 00 00 00 04 00 00 00 00 00 00 00 ; …………….
000000d0h: C8 01 00 00 C8 01 00 00 C8 01 00 00 00 00 40 00 ; ?..?..?….@.
000000e0h: 04 00 00 00 04 00 00 00 04 00 00 00 00 00 00 00 ; …………….
000000f0h: 04 00 00 00 00 00 00 00 CC 01 00 00 C8 01 00 00 ; ……..?..?..
00000100h: 00 00 00 00 02 00 00 00 00 00 10 00 00 10 00 00 ; …………….
00000110h: 00 00 10 00 00 10 00 00 00 00 00 00 10 00 00 00 ; …………….
00000120h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000130h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000140h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000150h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000160h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000170h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000180h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000190h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000001a0h: 2E 64 61 74 61 00 00 00 01 00 00 00 C8 01 00 00 ; .data…….?..
000001b0h: 04 00 00 00 C8 01 00 00 00 00 00 00 00 00 00 00 ; ….?……….
000001c0h: 00 00 00 00 40 00 00 C0 90 00 00 00 ; ….@..缾…

第二步 去掉数据节内容
看到文件的最后4字节,90 00 00 00,这正是我们定义的byData(链接器使用4字节对起,后面3字节填0),这当然不是我们需要的东西,我们定义byData,只是为了让链接器生成PE文件时能至少有一个节。所以我们先把它拿掉,在UltraEdit中直接删除最后4个字节,把000001a8处Section的Virtual Size改为0,这样,文件又少了4个字节。

第三步 去掉DOS Stub
我们的目标是在Windows XP下运行,DOS Stub自然是多余的,可是链接器并没有选项来去掉DOS Stub,只好手工来做这个工作,文件偏移0x3C处(IMAGE_DOS_HEADER的e_lfanew)指定了PE文件头位置,这里是0x000000A8,直接将文件偏移0x3C到0xA8间的数据删除,把后面的数据往前移,再将一些数据适当修正,比如e_lfanew修正为0x40。这里DOS Stub的大小为0xA8-0x40=0x68,拿掉它,我们的文件又小了104字节,内容如下:
00000000h: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 ; MZ?…….. ..
00000010h: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ; ?……@…….
00000020h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000030h: 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 00 ; …………@…
00000040h: 50 45 00 00 4C 01 01 00 77 B8 1A 49 00 00 00 00 ; PE..L…w?I….
00000050h: 00 00 00 00 E0 00 0F 01 0B 01 05 0C 00 00 00 00 ; ….?……….
00000060h: 09 00 00 00 00 00 00 00 60 01 00 00 60 01 00 00 ; ……..`…`…
00000070h: 60 01 00 00 00 00 40 00 04 00 00 00 04 00 00 00 ; `…..@………
00000080h: 04 00 00 00 00 00 00 00 04 00 00 00 00 00 00 00 ; …………….
00000090h: 64 01 00 00 60 01 00 00 00 00 00 00 02 00 00 00 ; d…`………..
000000a0h: 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00 ; …………….
000000b0h: 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000c0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000d0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000e0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000f0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000100h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000110h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000120h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000130h: 00 00 00 00 00 00 00 00 2E 64 61 74 61 00 00 00 ; ………data…
00000140h: 00 00 00 00 60 01 00 00 00 00 00 00 60 01 00 00 ; ….`…….`…
00000150h: 00 00 00 00 00 00 00 00 00 00 00 00 40 00 00 C0 ; …………@..

第四步 重叠DOS文件头和PE文件头
在Windows下,PE装载器只关心DOS文件头的e_magic和e_lfanew,有这么多无用的项目,何不把PE文件头往前挪挪,大家挤一挤,再誊点空间出来。当然,PE文件头的长度超过了DOS文件头,往前移动,肯定是会覆盖到e_lfanew的。e_lfanew是不能随便乱填的,怎么办?我们把PE文件头移动到文件偏移0x04的位置,再把e_lfanew修改为0x04,现在PE装载器可以正确从e_lfanew找到PE文件头的位置了,我们在来看看PE文件头,在PE文件头偏移0x3C-0x4=0x38的位置,刚好是IMAGE_OPETION_HEADER的SectionAlignment――节对齐值,刚刚好,我们的节对齐也是4,讲到这里,如果你链接PE文件时,用的对齐值不是4那么就得修改为4咯。
这一步也很简单,直接把刚才文件偏移0x40的数据拷贝到0x04处,这时候,我们的PE文件总大小为292字节:sizeof(IMAGE_NT_HEADERS)+sizeof(IMAGE_SECTION_HEADER)+4。文件内容如下:
00000000h: 4D 5A 90 00 50 45 00 00 4C 01 01 00 77 B8 1A 49 ; MZ?PE..L…w?I
00000010h: 00 00 00 00 00 00 00 00 E0 00 0F 01 0B 01 05 0C ; ……..?……
00000020h: 00 00 00 00 09 00 00 00 00 00 00 00 60 01 00 00 ; …………`…
00000030h: 60 01 00 00 60 01 00 00 00 00 40 00 04 00 00 00 ; `…`…..@…..
00000040h: 04 00 00 00 04 00 00 00 00 00 00 00 04 00 00 00 ; …………….
00000050h: 00 00 00 00 64 01 00 00 60 01 00 00 00 00 00 00 ; ….d…`…….
00000060h: 02 00 00 00 00 00 10 00 00 10 00 00 00 00 10 00 ; …………….
00000070h: 00 10 00 00 00 00 00 00 10 00 00 00 00 00 00 00 ; …………….
00000080h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
00000090h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000a0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000b0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000c0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000d0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000e0h: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; …………….
000000f0h: 00 00 00 00 00 00 00 00 00 00 00 00 2E 64 61 74 ; ………….dat
00000100h: 61 00 00 00 00 00 00 00 60 01 00 00 00 00 00 00 ; a…….`…….
00000110h: 60 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ; `……………
00000120h: 40 00 00 C0 ; @..

第五步 添加代码
这一步很有意思,我们的PE文件没有代码节,也没有导入表,怎么让他调用MessageBoxA呢?
第一个问题很好解决,PE文件头的EntryPoint不一定要指向代码节,例如很多加壳软件都会修改EntryPoint,让它指向自己的地址。极端一点,EntryPoint甚至可以指向PE文件的任意位置。我们来看一下,实际上对于PE文件头,PE装载器有很多字段也不会解释,这里把这些字段列出来,其中带“*”的字段表示值不能随便填,能随便填的值后面是其相对于我们这个PE文件的文件偏移:

IMAGE_FILE_HEADER STRUCT
Machine *
NumberOfSections *
TimeDateStamp
PointerToSymbolTable 0x0C
NumberOfSymbols 0x10
SizeOfOptionalHeader *
Characteristics *
IMAGE_FILE_HEADER ENDS
IMAGE_FILE_HEADER ENDS

IMAGE_OPTIONAL_HEADER32 STRUCT
Magic *
MajorLinkerVersion 0x1E
MinorLinkerVersion 0x1F
SizeOfCode 0x20
SizeOfInitializedData 0x24
SizeOfUninitializedData 0x28
AddressOfEntryPoint *
BaseOfCode 0x30
BaseOfData 0x34
ImageBase *
SectionAlignment *
FileAlignment *
MajorOperatingSystemVersion *
MinorOperatingSystemVersion *
MajorImageVersion *
MinorImageVersion *
MajorSubsystemVersion *
MinorSubsystemVersion *
Win32VersionValue *
SizeOfImage *
SizeOfHeaders *
CheckSum 0x5C
Subsystem *
DllCharacteristics *
SizeOfStackReserve *
SizeOfStackCommit *
SizeOfHeapReserve *
SizeOfHeapCommit *
LoaderFlags 0x74
NumberOfRvaAndSizes *
DataDirectory *
IMAGE_OPTIONAL_HEADER32 ENDS
另外对于DataDirectories,都指定了Virtual Address和Size,一个结构16字节,实际上只要我们把它的任意一个字段填0,另外一个字段就可以挪作它用了(TLS除外,两个字段必须都为0),有些DataDirectories甚至可以两个字段都任意填写,比如Exception Table,Seccrity Table,Base Relocation Table,Copyright Data Table,Global Ptr,再加上后面那个数据节的节表,除了VirtualSize,RAV/Offset和Pointer To Raw Data外,其他的都能随意填充。这样算下来,只要我们的代码足够小,还是有足够的空间容纳的。
现在让我们来写代码。
一个没有导入表的程序,如何才能动态装载User32.dll并调用其中的MessageBoxA呢?我们来看看下面的代码:
.386
.model flat,stdcall
option casemap:none

include windows.inc

.data
szUser32 db ‘user32.dll’,0
szMsg db ‘Hello World!’,0
szTitle db ‘MiniPE’,0

.code
GetApi proc hModule,nIndex

mov esi,hModule
mov edx,nIndex
add esi,[(IMAGE_DOS_HEADER ptr [esi]).e_lfanew]
assume esi:ptr IMAGE_NT_HEADERS
mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
add esi,eax
assume esi:ptr IMAGE_EXPORT_DIRECTORY

sub edx,[esi].nBase
rol edx,2
mov edi,[esi].AddressOfFunctions
add edi,eax
add edi,edx
mov edx,dword ptr [edi]
add eax,edx
ret
GetApi endp

WinMain proc
local hModule

assume fs:nothing
mov eax,fs:[30h]
mov eax,[eax+0Ch]
mov eax,[eax+0Ch]
mov eax,[eax]
mov eax,[eax]
mov eax,[eax+18h]
mov hModule,eax
invoke GetApi,hModule,245h
.if eax
push offset szUser32
call eax
.if eax
mov hModule,eax
invoke GetApi,hModule,1DDh
.if eax
push MB_OK
push offset szTitle
push offset szMsg
push 0
call eax
.endif
.endif
.endif
ret
WinMain endp

end WinMain
上面的代码并没有显示调用任何API,但是在Windows XP下,你尝试编译并运行它,你会发现他竟然能弹出一个对话框来。看看代码的入口:
首先加载fs:30h到eax,如果你研究过SHE的,一定对fs段寄存器很熟悉。在基于NT的操作系统中,fs寄存器用于访问线程本地信息(TEB)。我们来看看TEB到0x30处的定义:
0x00 NtTib :_NT_TIB
0x1C EnvironmentPointer :Ptr32 Void
0x20 ClientId :_CLIENT_ID
0x28 ActiveRpcHandle :Ptr32 Void
0x2C ThreadLocalStoragePointer :Ptr32 Void
0x30 ProcessEnvironmentBlock :Ptr32 _PEB
很显然,这里是通过TEB来访问进程环境块(PEB),后面的代码又将PEB的0x0C的数据加载到eax,我们来看PEB到0x0C的定义:
0x00 InheritedAddressSpace :UChar
0x01 ReadImageFileExecOptions :UChar
0x02 BeginDebugged :UChar
0x03 SpareBool :UChar
0x04 Mutant :Ptr32 Void
0x08 ImageBaseAddress :Ptr32 Void
0x0C Ldr :Ptr _PEB_LDR_DATA
可见,偏移0x0C出是_PEB_LDR_DATA,加载程序信息,后面的代码又访问了这个结构0x0C处的数据,我们再来看看_PEB_LDR_DATA到0x0C的定义:
0x00 Length :Uint4B
0x04 Initialized :UChar
0x08 SsHandle :Ptr32 Void
0x0C InLoadOrderModuleList :_LIST_ENTRY
实际上_PEB_LDR_DATA+0x0C处的InLoadOrderModuleList正是LDR_DATA_TABLE_ENTRY结构,让我们来看看它:
0x00 InLoadOrderLinks :_LIST_ENTRY
0x08 InMemoryOrderLinks :_LIST_ENTRY
0x10 InInitializationOrderLinks :_LIST_ENTRY
0x18 DllBase :Ptr32 Void
0x1C EntryPoint :Ptr32 Voi
……
我们的程序首先读取了其偏移0处的数据,它正是_LIST_ENTRY的Flink字段――前向链接,mov eax,[eax]这段代码直接跳过这个结构,把下一个_PEB_LDR_DATA结构加载到eax,重复两次,在读0x18处的内容――模块基地址。这是为什么呢?因为在Windows XP下,任何进程都必须含有至少3个模块:自身、ntdll.dll、kernel32.dll,其加载顺序也是自身、ntdll.dll、kernel32.dll。我们跳过前两个模块,把第三个模块kernel32.dll的基地址加载到eax寄存器。
上面的结构看上去很复杂,要得到这些结构的详细信息,可以在WindDbg中使用“DT ModuleName!*”命令来列出模块所有名字列表,然后再用“DT ModuleName!StructName”来列出结构的详细定义。
得到了kernel32.dll的基地址,后面的代码就很容易理解了,通过导出序号0x245从kernel32.dll的导出表中查找导出函数(0x245对应的正是LoadLibraryA),得到LoadLibraryA的地址再通过调用它来加载user32.dll,再用同样的方法用序号0x1DD从user32.dll导出表中查找MessageBoxA的地址。当然,这里我们也可以使用函数名从模块中查找导出函数,不过为了尽量把代码写得短小,这里用了序号。罗云彬的《Windows环境下32位汇编语言程序设计》中对这两种方法都作了说明,有兴趣可以参考一下。
解决了无导入表调用MessageBoxA的问题,我们还要想办法把我们的代码优化得尽可能短,再回忆我们前面讲的,我们要利用PE文件头的空余字段,这些字段看上去没有一块足够大的连续空间可以容纳我们的整个代码,所以我们还希望我们的代码每条指令的机器码尽量短,以便我们可以通过插入JZ/JNZ这样的指令来灵活地利用空余的空间。在高级语言中,有__falstcall这样的调用约定,我们借鉴一下,通过寄存器来传递调用函数的参数,最终优化出来,我的代码如下:
.386
.model flat,stdcall
option casemap:none

include windows.inc

.code

WinMain proc
assume fs:nothing
mov ebx,offset GetApi
mov eax,fs:[30h]
mov eax,[eax+0Ch]
mov eax,[eax+0Ch]
mov eax,[eax]
mov eax,[eax]
mov eax,[eax+18h]
mov edx,245h
call ebx
push 00400000h
call eax
mov edx,1DDh
call ebx
push MB_OK
push 00400000h
push 00400000h
push 0
call eax
ret
WinMain endp

GetApi proc
mov esi,eax
add esi,[(IMAGE_DOS_HEADER ptr [esi]).e_lfanew]
assume esi:ptr IMAGE_NT_HEADERS
mov esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
add esi,eax
assume esi:ptr IMAGE_EXPORT_DIRECTORY
sub edx,[esi].nBase
rol edx,2
mov edi,[esi].AddressOfFunctions
add edi,eax
add edi,edx
mov edx,dword ptr [edi]
add eax,edx
ret
GetApi endp

end WinMain
在上面的代码中,对于GetApi的调用,我们使用eax来传递hModule,因为无论是通过访问TEB还是LoadLibraryA,模块地址都已经在eax中了,无须再用其他指令调整,edx来传递导出序号。上面的被调函数并没有保护寄存器,是因为我们的代码可以完全掌握所有的寄存器,确保关键寄存器不会被被调用函数覆盖。代码中有三个push 00400000h――将字符串地址压栈,可以肯定,我们最终会把user32.dll,Hello!,MiniPE这些字符串找个合适的空隙存放起来,地址暂时不能确定,先空起来,最后再填。代码编译出来:
004001E0 >/$ BB 1E024000 MOV EBX,ASMTest.0040021E
004001E5 |. 64:A1 3000000>MOV EAX,DWORD PTR FS:[30]
004001EB |. 8B40 0C MOV EAX,DWORD PTR DS:[EAX+C]
004001EE |. 8B40 0C MOV EAX,DWORD PTR DS:[EAX+C]
004001F1 |. 8B00 MOV EAX,DWORD PTR DS:[EAX]
004001F3 |. 8B00 MOV EAX,DWORD PTR DS:[EAX]
004001F5 |. 8B40 18 MOV EAX,DWORD PTR DS:[EAX+18]
004001F8 |. BA 45020000 MOV EDX,245
004001FD |. FFD3 CALL EBX
004001FF |. 68 02004000 PUSH ASMTest.00400002
00400204 |. FFD0 CALL EAX
00400206 |. BA DD010000 MOV EDX,1DD
0040020B |. FFD3 CALL EBX
0040020D |. 6A 00 PUSH 0
0040020F |. 68 1A004000 PUSH ASMTest.0040001A
00400214 |. 68 0D004000 PUSH ASMTest.0040000D
00400219 |. 6A 00 PUSH 0
0040021B |. FFD0 CALL EAX
0040021D \. C3 RETN
0040021E . 8BF0 MOV ESI,EAX
00400220 . 0376 3C ADD ESI,DWORD PTR DS:[ESI+3C]
00400223 . 8B76 78 MOV ESI,DWORD PTR DS:[ESI+78]
00400226 . 03F0 ADD ESI,EAX
00400228 . 2B56 10 SUB EDX,DWORD PTR DS:[ESI+10]
0040022B . C1C2 02 ROL EDX,2
0040022E . 8B7E 1C MOV EDI,DWORD PTR DS:[ESI+1C]
00400231 . 03F8 ADD EDI,EAX
00400233 . 03FA ADD EDI,EDX
00400235 . 8B17 MOV EDX,DWORD PTR DS:[EDI]
00400237 . 03C2 ADD EAX,EDX
00400239 . C3 RETN
有90字节,看样子空隙足矣。

主函数的代码规模比较大,尽量找个连续的位置,看一下,文件偏移0x94这个位置比较合适(DataDirectories的Exception Table Address处),把刚才编译好的Shellcode拷贝进去。到文件偏移0xB0这个位置,注意,不能填了,那么在偏移0xAC这个位置我们安排两条指令来跳过它,直接跳到0xB4,JE 004000B4 JNE 004000B4,Shellcode刚好4字节。这样按顺序往下填写,遇到需要跳过的地方就在它前面四字节位置用JE/JNE,到最后,从0x11E还空了些字段出来。
然后是GetApi这个函数,虽然才28字节。可是难阿,连连续28字节的位置都找不出来了。
起始地址我们放在文件偏移0x0C处(PE文件头的Time/Date Stamp字段),和前面一样,遇到不能覆盖的位置就在它前面四字节用JE/JNE跳过。但是代码填到文件偏移0x25处为难了,0x2A是不能覆盖的,那是PE文件头的EntryPoint阿,看看这里的指令,ADD ESI,EAX SUB EDX,DWORD PTR [ESI+10h],两条算术指令,那么我们就用JNZ来跳过。我们可以肯定地知道,这两条算术指令的运算结果不可能是0,否则,我们的程序也就出错了。依次类推,后面遇到为难的而刚好又是算术指令的地方就用JNZ来跳过,才两字节阿,又节省了宝贵的资源。这样跳来跳去,到文件偏移0x8C的地方,GetApi的代码也填完了。
再回头来看看主函数的第一条代码,将GetApi的地址存入ebx,现在我们确定了GetApi的位置,那么就把0040000C填进去吧。然后是三个字符串,分别是“user32”(全名应该是“user32.dll”,但实在找不出位置了,就用“user32”吧),“Hello!”,“MiniPE”。刚才说了,在文件偏移0x11E处还有空位呢,“user32”就填这里吧。“Hello!”放在文件偏移0xD8处,“MiniPE”放在文件偏移0xE0处。最后别忘了修正
004001FF |. 68 02004000 PUSH ASMTest.00400002
0040020F |. 68 1A004000 PUSH ASMTest.0040001A
00400214 |. 68 0D004000 PUSH ASMTest.0040000D
这三条指令后面字符串和PE文件头EntryPoint的地址,EntryPoint修正为0x00000094。

到此为止,292字节的微型PE打造成功了,最终的文件内容如下:

00000000h: 4D 5A 90 00 50 45 00 00 4C 01 01 00 8B F0 03 76 ; MZ?PE..L…嬸.v
00000010h: 3C 8B 76 78 74 0D 75 0B E0 00 0F 01 0B 01 03 F0 ; <媣xt.u.?…..?
00000020h: 2B 56 10 03 F0 2B 56 10 75 06 00 00 94 00 00 00 ; +V..?V.u…?..
00000030h: C1 C2 02 8B 7E 1C 75 3C 00 00 40 00 04 00 00 00 ; 谅.媬.u<..@…..
00000040h: 04 00 00 00 04 00 00 00 00 00 00 00 04 00 00 00 ; …………….
00000050h: 00 00 00 00 64 01 00 00 60 01 00 00 00 00 00 00 ; ….d…`…….
00000060h: 02 00 00 00 00 00 10 00 00 10 00 00 00 00 10 00 ; …………….
00000070h: 00 10 00 00 03 F8 75 08 10 00 00 00 00 00 00 00 ; …..鴘………
00000080h: 03 FA 75 04 00 00 00 00 8B 17 03 C2 C3 00 00 00 ; .鷘…..?.旅…
00000090h: 00 00 00 00 BB 0C 00 40 00 64 A1 30 00 00 00 8B ; …[email protected]?…?
000000a0h: 40 0C 8B 40 0C 8B 00 8B 00 8B 40 18 74 06 75 04 ; @.婡.??婡.t.u.
000000b0h: 00 00 00 00 BA 45 02 00 00 FF D3 68 1E 01 40 00 ; ….篍… 觝..@.
000000c0h: 74 2E 75 2C 00 00 00 00 00 00 00 00 00 00 00 00 ; t.u,…………
000000d0h: 00 00 00 00 00 00 00 00 48 65 6C 6C 6F 21 00 00 ; ……..Hello!..
000000e0h: 4D 69 6E 69 50 45 00 00 00 00 00 00 00 00 00 00 ; MiniPE……….
000000f0h: FF D0 BA DD 01 00 00 FF D3 6A 00 68 E0 00 40 00 ; 泻?.. 觠.h?@.
00000100h: 74 12 75 10 00 00 00 00 94 00 00 00 00 00 00 00 ; t.u…..?……
00000110h: 94 00 00 00 68 D8 00 40 00 6A 00 FF D0 C3 75 73 ; [email protected]. 忻us
00000120h: 65 72 33 32 ; er32

我想前面讲的JE/JNE,JNZ这些跳转把你搞糊涂了吧,那么用OD调试一下,一步一步跟下程序运行流程,虽然OD调试的时候会报告“32为可执行文件格式错误”和“模块minipe有空的代码段”,但这都不影响程序的正常运行。

第六步 补充
这个MiniPE只能够在Windows XP下运行,因为没有导入表,在Windows 2K下无法运行。
后来朋友发给我一个别人做的MiniPE,才133字节,神了!粗看了一下,他简化了后面的数据目录,把导入表也进行了重叠,连MessageBoxA的字符串都放不下了,就用“MessageBoxA”,牛人就是牛人阿,继续学习中。
顺便做做广告,《疯狂的程序员》大约会在下下周出版啦,谢谢大家的支持,大家可以在这里看一看:
http://www.china-pub.com/301678

参考资料:
1.一块钱三毛的《手工构造一个超微型的 PE 文件》。
2.Eldad Eilam的《Reversing:Secrets of Reverse Engineering》。

(转载文章请保持完整性并注明出处)

相关日志

抢楼还有机会... 抢座Rss 2.0或者 Trackback

发表评论