Friday, December 29, 2006

Malware With a Twist

For the last couple days I've been playing with a sample of "Big Yellow", worm that exploits a vulnerability in Symantec Antivirus to spread (yes, SAV actually listens on a network port--2967--to receive commands from some sort of central server. I was horrified too). I got interested in it after a warning was posted on the Internet Storm Center.

Since the warning helpfully included the MD5 of the executable (f538d2c73c7bc7ad084deb8429bd41ef), I just went over to Offensive Computing and grabbed a copy for myself. Feel free to do the same in the discussion that follows, but be safe--don't do analysis on a machine connected to a network, and take some steps like renaming the executable to something that won't run when you double-click it.

To start with, a lot of basic tools barf when run against the file. Dumpbin and pedump both crash, and OllyDBG complains that the file is not a valid 32-bit executable (though it goes ahead and runs it). On the UNIX side, objdump can't recognize the file format, and "file" will only admit that it appears to be a DOS executable (when in fact it's a Win32 app). It turns out that the Windows executable loader is a lot more forgiving than most tools. Let's look inside the file itself to see what the heck is going on...

Here are the first few bytes of the file:

00000000 4D 5A 4B 45 52 4E 45 4C 33 32 2E 44 4C 4C 00 00 MZKERNEL32.DLL..
00000010 50 45 00 00 4C 01 03 00 BE B0 11 40 00 AD 50 FF PE..L......@..P.
00000020 76 34 EB 7C 48 01 0E 01 0B 01 4C 6F 61 64 4C 69 v4.|H.....LoadLi
00000030 62 72 61 72 79 41 00 00 18 10 00 00 10 00 00 00 braryA..........

Now this already looks pretty weird. We have the normal "MZ", indicating a DOS executable, but then, where we'd expect to find fields that describe the size of the DOS stub (the tiny DOS program that begins every Windows executable--this allows Windows programs to, at the very least, spit out an error message when some misguided fool tries to run them in DOS), there's a human-readable ASCII string.

But it turns out that this doesn't matter! Windows doesn't even bother to look at the DOS stub or ensure its validity when it loads an executable. It just checks the first two bytes to ensure that they're "MZ", and then goes to offset 0x0000003c, where the offset to the PE header is stored.

Going down to offset 0x3c gives us our next surprise--the offset is 0x10, which is inside the DOS header! Again, Windows is OK with this, because it doesn't examine the DOS header at all. The only part that might cause problems is the offset we just saw--at 0x3c, this value sits inside the PE header, it would be a bit of a coincidence if 0x00000010 just happened to be valid at that point in the PE header, right?

Well, once again, we find that in the quest for faster executable load times, the Windows PE loader once again cuts corners, and ignores many of the fields in the PE header as well. Solar Eclipse's excellent article "Tiny PE" describes exactly which ones are ignored, and it turns out that our PE offset at 0x3c falls inside an unused field in the PE "Optional" header (which is, in fact, mandatory). Here's the relevant portion in our PE file:

00000010 50 45 00 00 ; "PE\0\0" -- Win32 executable magic
00000014 4C 01 ; Machine (Intel 386)
00000016 03 00 ; NumberOfSections (3)
00000030 62 72 61 72 ; SizeOfInitializedData (UNUSED)
00000034 79 41 00 00 ; SizeOfUninitializedData (UNUSED)
00000038 18 10 00 00 ; AddressOfEntryPoint -- also the offset to
; the PE header!
0000003C 10 00 00 00 ; BaseOfCode (UNUSED)

The only other really important fields in the PE header, for our purposes, are the AddressOfEntryPoint (0x00001018, found at offset 0x00000038) and ImageBase (0x00400000, found at offset 0x00000044). (That's actually a lie. But it will ruin the surprise if I tell you the other field that we should care about, so just hang around and see.) The former tells us where execution will begin once the executable is loaded into memory, and the latter says where in memory the executable will be mapped in.

Let's keep going. After the Optional Header, we find the all-important section table. This lists the sizes, locations, and characteristics of the various chunks of the raw data file that will be mapped into memory, and also says where in memory to place them. For the sake of brevity, I'm only going to deal with the first section for now:

00000170 50 53 FF D5 AB EB E7 C3 ; Section name (UNUSED)
00000178 00 90 00 00 ; VirtualSize
0000017C 00 10 00 00 ; VirtualAddress
00000180 F0 01 00 00 ; SizeOfRawData
00000184 10 00 00 00 ; PointerToRawData
00000188 00 A0 40 00 ; PointerToRelocations (UNUSED)
0000018C 2B CC 40 00 ; PointerToLinenumbers (UNUSED)
00000190 66 00 ; NumberOfRelocations (UNUSED)
00000192 00 00 ; NumberOfLinenumbers (UNUSED)
00000194 60 00 00 E0 ; Characteristics

The VirtualSize says how much space the section takes up in memory (0x9000 bytes), the VirtualAddress gives the offset in memory where the section should go (0x1000), PointerToRawData lists precisely where in the actual file the section is (offset 0x10 in the file), and SizeOfRawData gives the size of the section's data in the file (0x01F0 bytes).

So now let's pretend that, in the depths of our ignorance, we're going to execute this thing. We dutifully start with our memory at 0x00400000, and then map 0x1F0 bytes of data from the file starting at offset 0x10, placing it in memory starting at 0x00401000 (that is, ImageBase + VirtualAddress). Then we map the other sections into their proper places, hop over to 0x00401018 (ImageBase + AddressOfEntryPoint) and start executing. In the file, we can find the instructions that will be executed at (0x00401018 - ImageBase) - VirtualAddress + PointerToRawData = 0x00000028.

A disassembly of the code at that point gives us this:

00000028: or (%ecx),%eax
0000002A: dec %esp
0000002B: outsb %esi,%edx
0000002C: popad
0000002D: dec %esp
0000002F: imul 0x72(%edx),%esp
[basically nonsense follows]

So that doesn't look right at all. What happened? As it turns out, there actually is one more field in the PE header that's crucial to understanding what's going on here--FileAlignment, which occurs at offset 0x0000004C in our file, and has a value of 0x200. This value gives a guarantee that any section in the file will be found at an offset that is a multiple of 0x200. But as you may recall, our PointerToRawData was 0x10--not a multiple of 0x200, no matter how hard it tries. Windows doesn't complain about this, but silently rounds it down to the nearest multiple of 0x200, which happens to be 0x00.

Armed with this new information, let's try disassembling again, using 0x00 as the offset of the section in our file (so the code is now found at 0x00000018 rather than 0x00000028):

00000018: mov 0x4011b0,%esi
0000001D: lodsd
0000001E: push %eax
0000001F: push 0x34(%esi)
00000022: jmp 0x7c

Now this looks a lot more sensible--we're loading a string into memory, executing a normal looking jump, etc. From here we can begin to actually analyze this code. Keep in mind that the actual code that this worm runs is almost certainly compressed or encrypted, and we haven't even started to tackle that!

But here is where we'll stop for now. Interestingly, it seems that even the formidable IDA Pro has only recently gained the ability to figure out this kind of trickery. Attempting to disassemble the code using IDA 4.0 (which is available as freeware, amazingly enough) gives the bogus disassembly that we initially came up with. IDA 5.0, however, is smart enough to warn that the section is misaligned, and starts disassembling at the correct point.

Hopefully I'll have time to put more time into analyzing this, and I'll be sure to post updates as I go. If you need to know what the bugger does right now, I heartily recommend eEye's writeup.