我正在尝试完成仅使用程序集制作的PE文件,它应该在控制台中显示一条消息。我想以这样的方式组织它,以后我可以轻松添加更多内容(知道在哪里添加代码,数据,导入的函数)。
我现在创建了4个部分,包括代码,数据,未启动的数据和导入的元素。我现阶段的主要问题是:
首先,我将在下面显示我的所有代码。为了节省时间并使其更容易阅读,不会添加一些真正无关紧要的内容 这是NASM代码
; Constants (use '$' as prefix)
$SECTION_ALIGNMENT equ 4096 ; Each section is aligned to 4096 in memory
$FILE_ALIGNMENT equ 512 ; Each section is aligned to 512 on disk
$PREFERRED_ADDRESS equ 4194304 ; Preffered address for EXE is 4 MB
$TOTAL_PE_SECTIONS equ 4 ; Code, Data, Bss and IData
; Image size = headers aligned to section alignment + sections size aligned
; to next multiple of section alignment, everything aligned, too
$IMAGE_SIZE equ $SECTION_ALIGNMENT + (HEADERS_SIZE/$SECTION_ALIGNMENT) + \
$TOTAL_PE_SECTIONS * $SECTION_ALIGNMENT
; Will help us align some of the values to the next specified multiple
%define Round(Number, Multiple) Multiple+(Number/Multiple)
section .header progbits vstart=0
; This is the MZ header
DOS_HEADER:
db "MZ" ; MZ signature
; ...
; Here we have all the members of the DOS header, in 4 paragraphs
; ...
db PE_HEADER ; The last one is pointing to the PE header
DOS_STUB:
; ...
; A small DOS program to display a simple message in DOS (64 bytes)
; ...
; This is the PE header
PE_HEADER:
db "PE", 0, 0 ; PE signature
dw 0x014C ; Platform Intel I386
dw $TOTAL_PE_SECTIONS
dd 1371668450 ; Creation timestamp
dd 0 ; No symbols table
dd 0 ; No symbols
dw SECTIONS_TABLE - OPT_HEADER ; Optional header size
dw 0x0002|0x0004|0x0008|0x0100|0x0200 ; Characteristics
; Optional header
OPT_HEADER:
dw 0x010B ; Signature
db 0 ; Linker version
db 0 ; Minor linker version
dd CODE_SIZE
dd DATA_SIZE ; Initialized data size
dd BSS_SIZE ; Uninitiated data size
dd CODE ; Entry point
dd CODE ; Code RVA
dd DATA ; Data RVA
dd $PREFERRED_ADDRESS ; Preferred address in memory
dd $SECTION_ALIGNMENT
dd $FILE_ALIGNMENT
dw 4 ; OS version
dw 0 ; Minor OS version
dw 0 ; Image version
dw 0 ; Minor image version
dw 3 ; Subsystem version
dw 10 ; Minor subsystem version
dd 0 ; WIN32 version
dd $IMAGE_SIZE ; Image size calculated above
dd Round(HEADERS_SIZE, $SECTION_ALIGNMENT) ; Headers size
dd 0 ; Checksum
dw 3 ; System interface CUI
dw 0 ; DLL characteristics
dd 4096 ; Reserved stack
dd 4096 ; Still not ??
dd 65536 ; sure about ??
dd 0 ; these ??
dd 0
dd 2 ; Data directory entries
dd 0 ; Export table pointer
dd 0 ; Export table size
dd I_TABLE ; Import table pointer
dd I_TABLE_S ; Size of import table
dq 0 ; Reserved
SECTIONS_TABLE:
CODE_SECTION_HEADER:
db ".code", 0, 0, 0
dd Round(CODE_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd CODE
dd Round(CODE_SIZE, $FILE_ALIGNMENT) ; Size on disk
dd Round(0, $FILE_ALIGNMENT) ; Real start address
dd 0
dd 0
dw 0
dw 0
dd 0x00000020|0x20000000|0x40000000|0x80000000
DATA_SECTION_HEADER:
db ".data", 0, 0, 0
dd Round(DATA_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd $SECTION_ALIGNMENT * 2
dd Round(DATA_SIZE, $FILE_ALIGNMENT) ; Size on disk
dd Round(0, $FILE_ALIGNMENT) ; Real start address
dd 0
dd 0
dw 0
dw 0
dd 0x00000040|0x40000000|0x80000000
BSS_SECTION_HEADER:
db ".bss", 0, 0, 0, 0
dd Round(BSS_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd $SECTION_ALIGNMENT * 3
dd 0
dd 0
dd 0
dd 0
dw 0
dw 0
dd 0x00000080|0x40000000|0x80000000
IDATA_SECTION_HEADER:
db ".idata", 0, 0
dd Round(IDATA_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd $SECTION_ALIGNMENT * 4
dd 0
dd Round(0, $FILE_ALIGNMENT) ; Real start address
dd 0
dd 0
dw 0
dw 0
dd 0x00000040|0x40000000|0x80000000
HEADERS_SIZE equ $$ - DOS_HEADER
align 512 ; Align to 512 bytes in memory
section .scode vstart=$SECTION_ALIGNMENT align=16
use32
CODE:
push -11
call dword [$PREFERRED_ADDRESS + F_GetStdHandle]
push 0
push 0x402000
push 6
push $PREFERRED_ADDRESS + hello
push eax
call dword [$PREFERRED_ADDRESS + F_WriteConsole]
push -1
call dword [$PREFERRED_ADDRESS + F_Sleep]
ret
CODE_SIZE equ $$ - CODE
section .sdata vstart=$SECTION_ALIGNMENT*2 progbits align=4
DATA:
hello: db 'Hello!'
DATA_SIZE equ $$ - DATA
section .sbss vstart=$SECTION_ALIGNMENT*3 align=4
BSS:
dd 5
BSS_SIZE equ $$ - BSS
section .sidata vstart=$SECTION_ALIGNMENT*4 align=4
IDATA:
F_Sleep: dd I_Sleep
F_WriteConsole: dd I_WriteConsole
F_GetStdHandle: dd I_GetStdHandle
dd 0
I_TABLE:
.originalfthk dd 0
.timedate dd 0
.forwarder dd 0
.name dd kernel32
.firstthunk dd IDATA
I_TABLE_S equ $$ - I_TABLE
times 20 db 0
kernel32: db 'kernel32.dll', 0
I_Sleep:
dw 0
db 'Sleep', 0
align 2
I_WriteConsole:
dw 0
db 'WriteConsoleA', 0
align 2
I_GetStdHandle:
dw 0
db 'GetStdHandle', 0
IDATA_SIZE equ $$ - IDATA
这里的主要问题是可执行文件崩溃,因为代码部分的指针是错误的。我正在讨论来自.sdata
的指向hello消息的指针以及来自.sidata
部分的指向导入函数的指针。如果我将hello变量和.sidata
的整个内容都复制到.scode
(下面是ret
)它可以工作,但是只要我将每个东西复制到它应该是正确的部分中,exe打破了。
所以,看起来地址错误估计。从这里开始,部分标题或其他地方可能存在错误的值。你觉得怎么样?
更新
在实施以下更改后,我现在遇到了一个问题。只要.data
部分小于512字节,一切正常。一旦超过该值,我就会收到“奇怪的无效win32应用程序”错误。
所以,这里我有2个由 PEInfo 导出的HTML文件。第一个包含工作文件的信息(.data
部分小于512字节):
Working EXE PEInfo
当.data
部分包含超过512个字节时,第二个包含损坏的EXE的信息:Corrupt EXE PEInfo
也许有人能发现事故的不同和原因。
答案 0 :(得分:4)
我现在有机会详细查看代码并实际让它运行。所以这里是我发现的所有问题。
首先,你的尺寸计算似乎都没有用。你做这样的事情:
CODE_SIZE equ $$ - CODE
但是你尝试在定义它之前引用CODE_SIZE
,所以它只是评估为零。
我的解决方案是添加结束标签,例如CODE_END:
,无论你通常在哪里执行其中一项计算。然后在代码的最开始,在使用这些值之前,将大小计算为每个块的结束标签和起始标签之间的差异。
HEADERS_SIZE equ HEADERS_END - DOS_HEADER
CODE_SIZE equ CODE_END - CODE
DATA_SIZE equ DATA_END - DATA
IDATA_SIZE equ IDATA_END - IDATA
I_TABLE_SIZE equ I_TABLE_END - I_TABLE
下一个大问题是你的Round
宏,它看起来像这样:
%define Round(Number, Multiple) Multiple+(Number/Multiple)
我不确定你认为自己在做什么,但这更像是你需要的东西:
%define Round(Number, Multiple) (Number+Multiple-1)/Multiple*Multiple
您希望确保 Number 是 Multiple 的倍数,因此是除数乘法序列。您还需要将Multiple-1
添加到原始数字以强制它向上舍入。
下一个大问题是RVA计算,或缺乏。文件结构中有很多地方需要您将偏移量指定为相对虚拟地址(RVA),这是内存中的相对偏移量。当您按原样获取标签的值时,它会在磁盘上为您提供偏移量。
对于剖面偏移,您基本上需要将该偏移除以文件对齐,然后将其乘以剖面对齐。此外,代码块将在一个部分对齐偏移处加载,因此应该相对于代码块计算所有内容,然后在结果中添加一个部分对齐。
%define RVA(BaseAddress) (BaseAddress - CODE)/$FILE_ALIGNMENT*$SECTION_ALIGNMENT+$SECTION_ALIGNMENT
现在适用于区域边界上的地址。对于其他任何事情,您需要计算相对于其基本地址的内部偏移量,然后将其添加到该部分的RVA。
%define RVA(Address,BaseAddress) RVA(BaseAddress)+(Address-BaseAddress)
这些计算假设各个部分已与$FILE_ALIGNMENT
值对齐,但实际情况并非如此。您在代码部分之前有一个align
,如下所示:
align 512 ; Align to 512 bytes in memory
但是你需要在每个部分之前以及文件末尾的部分之前。我还建议使用$FILE_ALIGNMENT
常量,否则就没有意义了。
align $FILE_ALIGNMENT ; Align to 512 bytes in memory
除此之外,您还需要摆脱所有section
声明。例如,需要删除所有这些行。
section .header progbits vstart=0
section .scode vstart=$SECTION_ALIGNMENT align=16
section .sdata vstart=$SECTION_ALIGNMENT*2 progbits align=4
section .sbss vstart=$SECTION_ALIGNMENT*3 align=4
section .sidata vstart=$SECTION_ALIGNMENT*4 align=4
由于您手动构建整个文件格式,它们没有用处,它们阻止您使用跨越边界的标签进行偏移计算(我们几乎无处不在)。
现在我们已经正确对齐了所有内容并且我们有两个RVA宏,我们可以开始修复需要使用RVAs的代码的各个部分。
首先在可选标题中,我们有代码RVA,数据RVA和入口点。此外,当我们在那里时,我相信应该将各种大小值指定为节对齐的多个。
dd Round(CODE_SIZE, $SECTION_ALIGNMENT)
dd Round(DATA_SIZE, $SECTION_ALIGNMENT) ; Initialized data size
dd Round(BSS_SIZE, $SECTION_ALIGNMENT) ; Uninitiated data size
dd RVA(CODE) ; Entry point
dd RVA(CODE) ; Code RVA
dd RVA(DATA) ; Data RVA
同样在可选标题中,当我认为它应该四舍五入到文件对齐时,您将标题大小四舍五入到节对齐。
dd Round(HEADERS_SIZE, $FILE_ALIGNMENT) ; Headers size
这是其中一项实际上没有任何区别的事情 - 代码将以任何方式工作 - 但我仍然认为这是错误的,应该予以纠正。
同样,正如我在第一个答案中指出的那样,即使您不使用全部16个条目,数据目录表的大小也应始终设置为16。如果你不这样做似乎确实有效,但我建议你再做一次。
dd 16 ; Data directory entries
dd 0 ; Export table pointer
dd 0 ; Export table size
dd RVA(I_TABLE,IDATA) ; Import table pointer
dd I_TABLE_SIZE ; Size of import table
times 14 dq 0 ; Space the other 14 entries
另请注意,I_TABLE偏移量已更新为使用相对于IDATA部分的RVA。
接下来在您的部分表中,您的所有偏移都是错误的。例如,代码段标题的开头应如下所示:
db ".code", 0, 0, 0
dd Round(CODE_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd RVA(CODE) ; Start address in memory
dd Round(CODE_SIZE, $FILE_ALIGNMENT) ; Size on disk
dd CODE ; Start address on disk
同样对于数据部分:
db ".data", 0, 0, 0
dd Round(DATA_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd RVA(DATA) ; Start address in memory
dd Round(DATA_SIZE, $FILE_ALIGNMENT) ; Size on disk
dd DATA ; Start address on disk
和idata部分:
db ".idata", 0, 0
dd Round(IDATA_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd RVA(IDATA) ; Start address in memory
dd Round(IDATA_SIZE, $FILE_ALIGNMENT) ; Size on disk
dd IDATA ; Start address on disk
bss部分略有不同。 bss部分的重点是磁盘上不占用空间,但它确实占用了内存空间。这意味着您实际上不能包含bss数据的任何数据定义。所以这段代码必须:
BSS:
dd 5
但这意味着磁盘上的部分不会与内存中的部分匹配。为了简化RVA计算,我建议的解决方法是将bss部分作为文件中的最后一部分。当它的大小从磁盘上的0扩展到内存中不会影响任何其他偏移的任何内容时。
所以我会在名为IMAGE_END:
的文件末尾添加一个标签,然后像这样定义bss部分:
db ".bss", 0, 0, 0, 0
dd Round(BSS_SIZE, $SECTION_ALIGNMENT) ; Size in memory
dd RVA(IMAGE_END) ; Start address in memory
dd 0 ; Size on disk
dd 0 ; Start address on disk
请注意,此部分必须位于sections表中的idata部分之后,因为地址需要按升序排列。
如果您不再在代码中使用bss部分,您可能想知道BSS_SIZE
值的来源。我担心你必须手动定义该值。您还必须手动为该部分中的任何变量的偏移量定义常量。正如我之前所说,你不能使用数据定义,因为我们不希望它占用磁盘上的任何空间。
接下来我们进入导入表。你为此使用的布局有些奇怪,但这似乎不是一个问题,所以我会按原样离开。您确实需要更新所有地址以使用RVAs。
首先是IAT:
F_Sleep: dd RVA(I_Sleep,IDATA)
F_WriteConsole: dd RVA(I_WriteConsole,IDATA)
F_GetStdHandle: dd RVA(I_GetStdHandle,IDATA)
然后是导入描述符:
.originalfthk dd 0
.timedate dd 0
.forwarder dd 0
.name dd RVA(kernel32,IDATA)
.firstthunk dd RVA(IDATA,IDATA)
我还应该提到你在这个描述符之后立即设置I_TABLE_S
变量,如果你还记得,我说你应该用结束标签替换这些大小的计算。但是,在这种情况下,描述符表的大小也应该包括最终的零条目。因此,放置该结束标签的正确位置不在此处,而是在times 20 db 0
填充之后。
times 20 db 0
I_TABLE_END:
这是另外一件我觉得不太重要的事情,但我还是建议修理。
此外,当您从一个DLL导入时,此布局很好,但是当您需要更多时,您将需要更多描述符和更多IAT部分。因此,我建议在每个IAT之前添加标签,例如在这种情况下类似于kernel32_iat
。然后你将你的第一个thunk初始化为。
.firstthunk dd RVA(kernel32_iat,IDATA)
最后,我想处理$IMAGE_SIZE
计算。您使用的计算假定每个部分的大小固定。但鉴于我们在文件末尾有一个IMAGE_END
标签和一个RVA宏,我们可以轻松地将精确的图像大小计算为RVA(IMAGE_END)
。
但是,这并没有考虑到bss部分,它会使图像加载到内存后变大。所以图像大小的正确定义应该是:
$IMAGE_SIZE equ RVA(IMAGE_END) + Round(BSS_SIZE,$SECTION_ALIGNMENT)
请注意,这应该在文件开头附近定义 - 在任何地方使用之前,但在RVA
宏之后,BSS_SIZE
已定义。