PE header and number of RVAs

Started by
5 comments, last by Jan Wassenberg 18 years ago
With some of the talk on this forum the past few days regarding PE headers, I got a renewed interest again in some of the stuff I'd been working on with hacking them apart and such. So I dug out some of my old tools and decided to try to create the smallest PE header I could. :) Ideally, I'd like to use it as a stub for decompression for a 4k demo. I know most current methods actually use a 16-bit DOS .com stub to decompress then launch a Win32 EXE, but that just seems silly to me. In any case, without too much trouble I was able to get a simple "Hello World!" to print out on the console in a whopping 448 bytes. The only problem with this tiny EXE is that it only works under Windows 2000. XP complains that it is not a valid executable... *sigh* The largest part of the PE header that wastes the most space is the RVA table. In the IMAGE_OPTIONAL_HEADER struct a field called NumberOfRvaAndSizes is supposed to specify the number of these RVA entries. Since my EXE only needs to import external symbols, I figured I could set this to 2 and save around 100 bytes or so, since I wouldn't need to store the unnecessary RVA entries. It seems that this is where I broke compatibility with 2000 and XP, even though Microsoft's own MSDN page says "The number of directories is not fixed. Check the NumberOfRvaAndSizes member before looking for a specific directory." in the Remarks section. In case anyone is interested, a zip is available here which contains three EXEs, each a subsequent step after the other that I used to trim down the PE header:
    tiny.exe  (608 bytes): Minimal MZ header, works fine on both 2K and XP
    tiny2.exe (480 bytes): Chopped RVA table to only 2 entries, 2K only
    tiny3.exe (448 bytes): Combined .code and .data sections into one, 2K only
So, does anyone know if the NumberOfRvaAndSizes is absolutely _required_ to be set to 16? Or maybe there's something I overlooked and didn't set properly? I've tried to use the various tools out there for checking EXEs -- PE Explorer has no problem with it, but Microsoft's PEDUMP crashes (seems to be coded to expect 16 entries -- although it really looks like a bug). Thanks for any help anyone can provide!
Advertisement
*bump* I know I can't be the only one around here that does this kind of stuff...

Anyone? :)
One last *bump*. Still no replies after this, and I'll just have to deal with 16 RVA entries... :(
Quote:I know I can't be the only one around here that does this kind of stuff

Correct :D

I've written a 1500 byte demo (3d chess game with demo playback; similar screenshot) that also messed with the PE header.

It declared 2 data dirs and indeed worked under Win2k but (after dusting off) no longer on WinXP SP2. The error returned is "app failed to initialize (0xC0000005)", which comes up twice. No wonder - IIRC it flouted the PE spec and was the bare minimum that Win2k would deign to give a PID.

Looks like you'll have to expand to 16 entries, then. But fine and good they worked on the loader since Win2k, because I remember BSODs when raising an exception in code beyond OptionalHeader.SizeOfRawData.

Quote:I know most current methods actually use a 16-bit DOS .com stub to decompress then launch a Win32 EXE, but that just seems silly to me.

Why silly? .com packers abound (IIRC I used APACK) and the dropper itself only adds 27 bytes with a little cheating:
; exe dropper; Jan Wassenberg 2003; free mem	mov		ah, 0x49	int		0x21; fopen	mov		ah, 0x3c	pop		cx				; cx = 0 (attrib)	mov		dx, 0x100+exe			; filename == "MZ."	int		0x21; fwrite	xchg		ax, bx	mov		ah, 0x40	dec		cx				; 64 kb	int		0x21; fclose	mov		ah, 0x3e	int		0x21; exec	mov		ax, 0x4b00	int		0x21exe:incbin "w.exe"

(the EXE data can be misused as filename: just stash ".\0" right after the initial MZ to get dropped filename="MZ")

Quote:In any case, without too much trouble I was able to get a simple "Hello World!" to print out on the console in a whopping 448 bytes.

Not bad, but can be significantly reduced yet. While drunk^H^H^H^H^Hinspired, I figured out the MZ header and PE optional header can be interleaved! Unfortunately I never drew up a diagram of the headers side by side, but the critical insight is: only signature and e_lfanew fields are needed in the MZ header; the former can be skipped by starting PE header at RVA 4 and the latter falls over OptionalHeader.SectionAlignment, for which it is a valid value.

Wacky header follows: (note: indented comments indicate data embedded into apparently unused header fields)
		db	'MZ'				; e_magic		db	'.',0				;   dropped file name		dd	'PE'				; Signature		dw	0x014c				; Machine = x86		dw	1				; NumberOfSections		dd	0,0,0		dw	0x0070				; SizeOfOptionalHeader		dw	0x010f				; Characteristics = 32 bit exe		dw	0x010b				; Magic		db	'glu32.dll',0			;   Name (RVA 30)		dd	0		dd	rva(start)			; AddressOfEntryPoint		dd	0,0		dd	0x400000			; ImageBase		dd	4				; SectionAlignment, e_lfanew		dd	4				; FileAlignment		dd	0,0		dw	4				; MajorSubsystemVersion		db	'EDIT',0			;   wnd class name (RVA 78)		db	0		dd	0x10000				; SizeOfImage (virtual)		dd	0xa4				; SizeOfHeaders		dd	0		dw	2				; Subsystem = WIN32_GUI		dw	0		dd	0x400000			; SizeOfStackReserve		dd	0x400000			; SizeOfStackCommit		dd	30				;   Name (RVA 108)		dd	rva(imp_tbl)			;   FirstThunk		dd	0		dd	2				; NumberOfRvaAndSizes (# data dirs)		dd	0				; Export VirtualAddress		dd	0				; Export Size, End Import		dd	96				; Import VirtualAddress		dd	40				; Import Size		dd	0,0		dd	0				; VirtualSize		dd	0				; VirtualAddress		dd	0x10000				; SizeOfRawData				FIXME?		dd	0				; PointerToRawData


This craziness yields a 160 byte executable :)

--
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3
While this is most likely a fun exercize, working at the edges of the spec like this might cause incompatibilities in the future - it even happened in the case abive. Just one of the reasons PE encryptors and compressors will never be used by me...
Quote:Original post by Jan Wassenberg
Quote:I know most current methods actually use a 16-bit DOS .com stub to decompress then launch a Win32 EXE, but that just seems silly to me.

Why silly? .com packers abound (IIRC I used APACK) and the dropper itself only adds 27 bytes with a little cheating:
*** Source Snippet Removed ***
(the EXE data can be misused as filename: just stash ".\0" right after the initial MZ to get dropped filename="MZ")

I don't like the idea of writing an EXE to disk at all. While I agree it works for size, it's a bit unclean... hence the reason I wanted to try to shrink the PE header down as much as possible, while still trying to maintain some sort of compatibility.

Quote:Not bad, but can be significantly reduced yet. While drunk^H^H^H^H^Hinspired, I figured out the MZ header and PE optional header can be interleaved!

I had considered that myself too, but then decided against it since I already had some incompatibility issues between 2k and XP. Plus, I wasn't sure if a section alignment of 4 was actually valid or not. :)

Oh well, since it seems that XP requires 16 RVA entires, I probably won't bother with messing with the header too much more.

Thanks for the help, Jan!

Quote:Original post by Anonymous Poster
While this is most likely a fun exercize, working at the edges of the spec like this might cause incompatibilities in the future - it even happened in the case abive. Just one of the reasons PE encryptors and compressors will never be used by me...

It's fun, definitely. :)

Although you should note that what I'm trying to do is much lower level than what "PE encryptors and compressors" do. AFAIK, no EXE packer in their right mind would try to muck with the PE header itself as much as we have.
Quote:I don't like the idea of writing an EXE to disk at all. While I agree it works for size, it's a bit unclean... hence the reason I wanted to try to shrink the PE header down as much as possible, while still trying to maintain some sort of compatibility.

I see where you're coming from. Droppers also usually do not delete the file afterwards, which isn't nice.
In my case, I was gunning for absolute minimum size and such considerations had little weight :)

Quote:I had considered that myself too, but then decided against it since I already had some incompatibility issues between 2k and XP.

Yep, grr. Worst one I stumbled over is that they removed DllInitialize from opengl32.dll, so anyone who linked by ordinal (which is very tempting, given the many functions that must be imported) got screwed over.

Quote:Plus, I wasn't sure if a section alignment of 4 was actually valid or not. :)

It is on NT systems, but not on 9x.

Quote:Oh well, since it seems that XP requires 16 RVA entires, I probably won't bother with messing with the header too much more.

Aww. Interleaving might still be viable (even if it crashes several debuggers), and you could try stashing code/data inside those directories.

Quote:Thanks for the help, Jan!

Glad to :)
E8 17 00 42 CE DC D2 DC E4 EA C4 40 CA DA C2 D8 CC 40 CA D0 E8 40E0 CA CA 96 5B B0 16 50 D7 D4 02 B2 02 86 E2 CD 21 58 48 79 F2 C3

This topic is closed to new replies.

Advertisement