My automated unpacking script (which really needs a sensible name!) is a few years old now, so I was interested to see how it would go with some malware that was developed after it was. That is, I wanted to answer the question ‘is my script still useful?’. It turns out it is still useful, and this post is the first of a few posts that aim to demonstrate why.
Being keen to build my automated unpacking script up to be an automated dynamic analysis tool, and having recently pulled a CryptoLocker variant from someone’s PC, I decided to investigate whether my script can be used to analyse CryptoLocker and if so, how.
The setup that I used to do this analysis is pretty much as described in My Malware Analysis Setup. I’ll quickly describe the gist of it, and differences from it, here.
A Linux (UNIX is my native environment) host using VirtualBox to run a Windows guest virtual machine.
I’ve created a shared folder which, for obvious reasons, is empty apart from the sample and my unpack.py script. With a Linux host, using a shared folder lets me examine unpack.py‘s output files in my native UNIX environment, rather than having to find (or write) non-native Windows commands/applications to achieve the same thing.
I’m using a bridge configured (brctl(8)) on the host which VirtualBox is bridging the guest network interface to.
Since I don’t want the malware sample to be able to communicate with its command-and-control (C&C) server (at least not yet), forwarding (routing) is disabled on the host. I could also use iptables(8) to protect the host and local networks.
I have Wireshark sniffing (monitoring) this bridge interface so that it will see all traffic that the guest sends and receives. It shouldn’t receive much because it can’t get anywhere, but it will see any DNS requests that the malware sample spits out.
The virtual guest
Windows XP virtual machines with the network adapter shared with one of the host’s adapters (as discussed above). I’d rather use Windows 7, but for some reason my unpack.py script misses a few things when running under Windows 7 — I need to investigate why.
Wireshark(1) running on the host, sniffing the network interface bridged to the guest.
tshark(1) on the host. This will be used to extract information such as DNS names and syslog messages from the Wireshark packet capture.
Eventlog-to-syslog installed on the Windows XP guest, and at least process creation and process termination auditing enabled in the Windows Local Security Policy.
Python 2.7, WinAppDbg 1.5, unpack.py. This software is what will be doing most of the work for us.
Analysing the sample
We’ll start Wireshark sniffing the guest’s traffic. I don’t particularly want this sample to communicate with the C&C server (at least not yet), so the host has routing disabled, but Wireshark will still see the guest attempting to connect to the Internet (as long as the host’s interface has an IP address which is specified as the default gateway/router in the guest. Also make sure that the guest has a DNS server configured — it doesn’t have to be reachable, just configured to be either the host, or an IP address not on the guest’s local network).
Let’s get on with it then. E: is my shared drive.
e: c:\Python27\python.exe e:\unpack.py filename.exe > inf.log 2> inf-stderr.log
The original executable popped a Window up with a PDF icon and a title but no content. A packet capture also shows DNS queries for weird (random-looking string of characters) hosts in a weird looking domain.
Let’s start with the basics and see what processes ran as a result of us doing that. We’ll do this by looking at the Windows event log events, although rather than trying to fight with Event Viewer’s GUI to do that, we’ll snarf them from the network capture after Eventlog-to-Syslog has conveniently spat them out over the network for us. We’re only interested in process created/exited messages at the moment, so we’ll use grep(1) to limit the output to only those events.
$ tshark -T fields -e syslog.msg -r dump.pcap 'udp.port == 514' |grep "Security: 59: " Mar 5 20:50:16 HOSTNAME Security: 592: HOSTNAME\username: A new process has been created: New Process ID: 3604 Image File Name: C:\Python27\python.exe Creator Process ID: 3460 User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:50:16 HOSTNAME Security: 592: HOSTNAME\username: A new process has been created: New Process ID: 3612 Image File Name: \Device\VBoxMiniRdr\vboxsrv\cuckoo1\filename.exe Creator Process ID: 3604 User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 592: HOSTNAME\username: A new process has been created: New Process ID: 1300 Image File Name: \Device\VBoxMiniRdr\vboxsrv\cuckoo1\filename.exe Creator Process ID: 3612 User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 592: HOSTNAME\username: A new process has been created: New Process ID: 1888 Image File Name: C:\WINDOWS\explorer.exe Creator Process ID: 1300 User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 593: HOSTNAME\username: A process has exited: Process ID: 1300 Image File Name: \Device\VBoxMiniRdr\vboxsrv\cuckoo1\filename.exe User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 592: HOSTNAME\username: A new process has been created: New Process ID: 1776 Image File Name: C:\WINDOWS\system32\vssadmin.exe Creator Process ID: 1888 User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 593: HOSTNAME\username: A process has exited: Process ID: 1776 Image File Name: C:\WINDOWS\system32\vssadmin.exe User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 593: HOSTNAME\username: A process has exited: Process ID: 3612 Image File Name: \Device\VBoxMiniRdr\vboxsrv\cuckoo1\filename.exe User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7) Mar 5 20:53:27 HOSTNAME Security: 593: HOSTNAME\username: A process has exited: Process ID: 3604 Image File Name: C:\Python27\python.exe User Name: username Domain: HOSTNAME Logon ID: (0x0,0xB5C7)
Interesting. Looking at the New Process ID and Creator Process ID fields we can see that python.exe (running unpack.py) starts filename.exe (pid 3612 — the malware sample). The filename.exe process (pid 3612) then creates another filename.exe process (pid 1300). This is the behaviour that we saw in the log data from unpack.py and if we look back at that, we see that the process ids match:
[*] <3612:3616> 0xa21b90: CreateProcess("","filename.exe",0x4): 1 (0xa4, 0xa8, <1300:1376>)
Smashing — it’s always good when various pieces of log data corroborate each other. However, then it gets interesting. The event log data shows that our naughty little filename.exe process (pid 1300) starts an explorer.exe process (pid 1888), before exiting. Then, the new (rogue) explorer.exe (pid 1888) starts vssadmin.exe (pid 1776), and then vssadmin.exe, the first filename_96522.exe process (pid 3612), and then python.exe (running unpack.py) all exit, leaving the new explorer.exe process (pid 1888) running. That doesn’t look dodgy at all!
We can create a process tree:
python unpack.py (pid 3604) filename.exe (pid 3612) filename.exe (pid 1300) explorer.exe (pid 1888) vssadmin.exe (pid 1776)
If you’re wondering what vssadmin.exe is, it is the admin command for the Volume Shadow copy Service. Volume shadow copies are used to create a read only point in time snapshot of a file system which can then be backed up (they help with backups because it eliminates the problem where some of an application’s files may be modified between the backup process starting and finishing backing up the files for that particular application).
From Microsoft’s TechNet (How Volume Shadow Copy Service Works: Data Recovery):
The Volume Shadow Copy Service provides the backup infrastructure for the Microsoft Windows XP and Microsoft Windows Server 2003 operating systems, as well as a mechanism for creating consistent point-in-time copies of data known as shadow copies.
The key being that volume shadow copies can contain copies of files that our little ransomware sample is about to encrypt, so it is possible that vssadmin.exe is used by the rogue explorer.exe process in an attempt to delete any volume shadow copies. We don’t have any command line information available, so we can’t tell for certain.
Now let’s see what unpack.py was able to tell us about the malware sample’s behaviour.
[*] Started at 2016-03-05 20:50:16 [*] Starting filename.exe [*] Starting debug loop [*] <3612:3616> Create process event for pid 3612 (C:\vboxsrv\cuckoo1\filename.exe) [-] command line: ... [*] <3612:3616> 0x5ad7a0e2: IsDebuggerPresent(): 0x1 [-] Returning 0
Right. So unpack.py starts the executable file (the malware sample) and notices that it calls IsDebuggerPresent(), and that IsDebuggerPresent() is about to return 0x1, so it modifies the eax register so that the caller (our sample) thinks that IsDebuggerPresent() returned 0x0.
[*] <3612:3616> 0x7c809af9: VirtualAllocEx(0xffffffff,0x0,0x28a5 (10405),0x1000,0x040) = 0xa20000 [-] Request for EXECUTEable memory [*] <3612:3616> 0x4046af: VirtualAlloc(0x0,0x28a5 (10405),0x1000,0x040) = 0xa20000 [*] VirtualAlloc()d memory address 0xa20000 written from 0x40aa3f (infringement_96522!0xaa3f): mov [esi], bl [-] Enabling tracing 0x40aa0a: mov ebx, [ebp+0xc] 0x40aa0d: mov ecx, eax 0x40aa0f: neg edx 0x40aa11: mov eax, edx 0x40aa13: lea edx, [ecx-0x205] 0x40aa19: test edx, edx 0x40aa1b: jz infringement_96522!0xaa22 0x40aa1d: add eax, 0x57f 0x40aa22: mov bl, [ebx+esi] 0x40aa25: lea edx, [ecx+eax-0x22c] 0x40aa2c: add ecx, edx 0x40aa2e: add eax, 0x147 0x40aa33: test ecx, ecx 0x40aa35: jz infringement_96522!0xaa3d 0x40aa37: add edx, 0xc4 0x40aa3d: mov ecx, edx 0x40aa3f: mov [esi], bl 0x40aa41: sub ecx, eax 0x40aa43: jz infringement_96522!0xaa4c 0x40aa45: lea edx, [edx+edx-0x24c] 0x40aa4c: mov ebx, [ebp+0x14] 0x40aa4f: inc esi 0x40aa50: dec ebx 0x40aa51: mov ecx, edx 0x40aa53: mov [ebp+0x14], ebx 0x40aa56: jnz infringement_96522!0xaa0a [E] Reached tracing limit of 250000 instructions [D] Single-step instruction limit reached -- stopping tracing
This block of log data shows that unpack.py detected the sample requesting a block of executable memory and then writing to it. It traced execution and found the loop containing the instruction (at address 0x40aa3f) that was writing to the executable memory — this is likely the unpacking loop.
[*] VirtualAlloc()d memory address 0xa220a6 written from 0x40aac6 (infringement_96522!0xaac6): mov [edx+edi], cl [*] VirtualAlloc()d memory address 0xa228a3 written from 0x40aae0 (infringement_96522!0xaae0): mov [edx], bl
It detects another two instructions writing to the executable memory.
[*] Found unpacked entry point at 0xa20000 called from 0x4057de (jmp dword [ebp-0x4]) (after executing 227733 instructions) [-] Unpacking loop at 0x40aa0a - 0x40aa56 [-] Dumping 10405 bytes of memory range 0xa20000 - 0xa228a4
unpack.py has detected execution of the unpacked code in the executable memory block at address 0xa20000. The entry point was at 0xa20000, and execution jumped there from the jmp dword [ebp-0x4] instruction at address 0x4057de. Now that control has passed to the executable memory block, unpack.py assumes that it has been completely unpacked and dumps the memory block to disk. We’ll have a look at this in a moment.
[*] <3612:3616> 0x7c809af9: VirtualAllocEx(0xffffffff,0x0,0x8a000 (565248),0x1000,0x004) = 0xad0000 [*] <3612:3616> 0xa2126a: VirtualAlloc(0x0,0x8a000 (565248),0x1000,0x004) = 0xad0000 [*] <3612:3616> 0x7c809af9: VirtualAllocEx(0xffffffff,0x0,0x17600 (95744),0x1000,0x004) = 0xb60000 [*] <3612:3616> 0xa21624: VirtualAlloc(0x0,0x17600 (95744),0x1000,0x004) = 0xb60000 [-] Dumping 75081 bytes of compressed memory at 0xb44bb0 to filename.exe.memblk0xb44bb0.comp [*] <3612:3616> 0xa2170e: RtlDecompressBuffer(0x2,0xb60000,0x17600,0xb44bb0,0x12549,0x12f774): 0 [-] Dumping 95744 bytes of decompressed memory at 0xb60000 to filename.exe.memblk0xb60000.decomp
Okay. More memory allocations with VirtualAlloc() (VirtualAlloc() calls VirtualAllocEx() with its first argument being 0xffffffff — that is why we see VirtualAllocEx(0xffffffff,…) followed immediately by VirtualAlloc(…) in unpack.py‘s log output — VirtualAllocEx() returns (remember that these are post-call hooks), and then the corresponding call to VirtualAlloc() returns).
Also notice that the address of one of those allocated memory blocks — the one at address 0xb60000 — is later passed to RtlDecompressBuffer(). This tells us that the malware sample is uncompressing data from address 0xb44bb0 (the fourth argument passed to RtlDecompressBuffer()) to the newly allocated memory at address 0xb60000. unpack.py dumps both the compressed data and the uncompressed data to disk.
[*] <3612:3616> 0xa21b90: CreateProcess("","filename.exe",0x4): 1 (0xa4, 0xa8, <1300:1376>) [-] CREATE_SUSPENDED. Hooking ResumeThread() (1) [*] <3612:3616> 0xa21d38: VirtualAllocEx(0xa4,0x400000,0x1c000 (114688),0x3000,0x040) = 0x400000 [-] Request for EXECUTEable memory [*] <3612:3616> 0xa21e04: WriteProcessMemory(0xa4,0x400000,0xb60000,0x400,0x0): 1 [-] Dumping 1024 bytes of memory at 3612:0xb60000 written to 1300:0x400000 to filename.exe.memblk0x400000-1300.wpm [*] <3612:3616> 0xa21fd5: WriteProcessMemory(0xa4,0x401000,0xb60400,0x8a00,0x0): 1 [-] Dumping 35328 bytes of memory at 3612:0xb60400 written to 1300:0x401000 to filename.exe.memblk0x401000-1300.wpm [*] <3612:3616> 0xa21fd5: WriteProcessMemory(0xa4,0x40a000,0xb68e00,0xc400,0x0): 1 [-] Dumping 50176 bytes of memory at 3612:0xb68e00 written to 1300:0x40a000 to filename.exe.memblk0x40a000-1300.wpm [*] <3612:3616> 0xa21fd5: WriteProcessMemory(0xa4,0x417000,0xb75200,0x1000,0x0): 1 [-] Dumping 4096 bytes of memory at 3612:0xb75200 written to 1300:0x417000 to filename.exe.memblk0x417000-1300.wpm [*] <3612:3616> 0xa21fd5: WriteProcessMemory(0xa4,0x419000,0xb76200,0x200,0x0): 1 [-] Dumping 512 bytes of memory at 3612:0xb76200 written to 1300:0x419000 to filename.exe.memblk0x419000-1300.wpm [*] <3612:3616> 0xa21fd5: WriteProcessMemory(0xa4,0x41a000,0xb76400,0x1200,0x0): 1 [-] Dumping 4608 bytes of memory at 3612:0xb76400 written to 1300:0x41a000 to filename.exe.memblk0x41a000-1300.wpm [*] <3612:3616> 0xa2218e: WriteProcessMemory(0xa4,0x7ffd7008,0x12fac4,0x4,0x0): 1 [-] Dumping 4 bytes of memory at 3612:0x12fac4 written to 1300:0x7ffd7008 to filename.exe.memblk0x7ffd7008-1300.wpm [*] <3612:3616> 0x7c83290f: ResumeThread(0xa8) [-] New suspended process (pid 1300) resumed
Recognise that behaviour? It looks like process hollowing. The sample has created another process (CreateProcess()), using its own .exe file (the second argument), in a suspended state (the third argument, 0x4, corresponds to CREATE_SUSPENDED). Since .exe files are usually loaded at a base address of 0x400000, I suspect that the subsequent VirtualAllocEx() call is to resize the block of memory at which the new process has been loaded.
Next, notice that there are a number of WriteProcessMemory() calls that write to the newly created process (process handle — the first argument — of 0xa4, which we can see from the values returned by the CreateProcess() call). These appear to be writing the various sections of a PE file. I’ll show you how to confirm this in a moment. Also notice that the source memory address for the WriteProcessMemory() calls is the newly allocated memory to which RtlDecompressBuffer() was just used to uncompress data to (0xb60000).
[*] Unhandled exception at 0x73e6e0b3: EXCEPTION_ACCESS_VIOLATION [*] Unhandled exception at 0x0: EXCEPTION_ACCESS_VIOLATION ... [ lots more EXCEPTION_ACCESS_VIOLATION messages at 0x0 ] ... [*] <3612:3616> Exit process event for C:\vboxsrv\cuckoo1\filename.exe: 0xc0000005 [*] Terminating [D] Number of created processes: 1
Things then start to go downhill a tad, evidenced by unpack.py logging numerous unhandled EXCEPTION_ACCESS_VIOLATION exceptions at address 0x0, before the process exits with an exit status of 0xc0000005 (access violation).
This looks like a good time to do a quick recap of what we’ve found out so far.
- Our malware sample requested executable memory at the instruction before 0x4046af (that address is the return address from VirtualAlloc()). The newly allocated memory is at address 0xa20000.
- A loop between addresses 0x40aa0a and 0x40aa56 unpacks code to the newly allocated executable memory block. The instruction (within the loop) that writes to the memory block is the mov [esi], bl instruction at address 0x40aa3f.
- A mov [edx+edi], cl instruction at address 0x40aac6, and a mov [edx], bl instruction at address 0x40aae0 modify two addresses (0xa220a6 and 0xa228a3, respectively) within the new memory block.
- A jmp dword [ebp-0x4] instruction at address 0x4057de jumps to the unpacked code at address 0xa20000 (the start of the allocated block of executable memory).
- The sample requests some more, non-executable, memory blocks, and decompresses 0x12549 bytes of COMPRESSION_FORMAT_LZNT1 (0x2) compressed data to the 0x17600 byte block of memory at 0xb60000 (one of the newly allocated blocks of memory).
- It then creates a new process using its own .exe file, replaces it with blocks of data from the newly decompressed data at address 0xb60000, and starts the new process running the replacement code.
- The sample then does something which causes a whole load of EXCEPTION_ACCESS_VIOLATION exceptions. I suspect that this is some sort of anti-reversing behaviour that will need to be investigated by some other means (more than likely with an interactive debugger). When I ran the sample through Cuckoo, it was suggesting that there was some unhooking going on, which I suspect may be what is causing this.
I’m keen to find out what is causing it and see if I can then get unpack.py to detect it, log it, and withstand it.
So that was fun — there’s still more that we can do though. Remember I mentioned coming back to the decompressed memory written to disk, and coming back to the memory blocks that were written to the new filename.exe process (pid 1300)? Let’s look at them now.
Since the new filename.exe process (pid 1300) was being overwritten with blocks of the uncompressed data in the memory block at 0xb60000, we’d expect to see the uncompressed memory to look something like a PE file:
$ file filename.exe.memblk0xb60000.decomp filename.exe.memblk0xb60000.decomp: PE32 executable (GUI) Intel 80386, for MS Windows
which it does. Now let’s test our theory that the WriteProcessMemory() calls were writing each of the decompressed PE file sections to the new suspended process by having a look at the sections in the decompressed PE file:
$ objdump -h filename.exe.memblk0xb60000.decomp filename.exe.memblk0xb60000.decomp: file format pei-i386 Sections: Idx Name Size VMA LMA File off Algn 0 .text 0000892a 00401000 00401000 00000400 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 1 .rdata 0000c2f4 0040a000 0040a000 00008e00 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .data 00001000 00417000 00417000 00015200 2**2 CONTENTS, ALLOC, LOAD, DATA 3 .rsrc 000001b4 00419000 00419000 00016200 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .reloc 000010a0 0041a000 0041a000 00016400 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA
Now, if you have a look at those values and compare them to the values in the WriteProcessMemory() calls, you’ll notice that they match nicely. The destination addresses in the calls match the VMA (Virtual Memory Address) of each of the sections in the PE file, and the memory block offset of the source address, and the number of bytes to copy, match the file offsets in the section headers (with the number of bytes to copy matching the difference between consecutive sections’ file offsets).
For instance, the first WriteProcessMemory() call writes 0x400 bytes from 0xb60000 to 0x400000. This is the PE header before the start of the first section. The first section, .text., starts at file offset 0x400.
The second WriteProcessMemory() call writes 0x8a00 bytes from 0xb60400 to 0x401000. 0x8a00, when added to the .text section’s file offset of 0x400 gives 0x8e00 — the file offset of the second section. 0xb60400 is 0x400 bytes from the start of the allocated memory block at 0xb60000, and 0x400 is the file offset of the .text section. 0x401000 is the VMA — memory load address — of the .text section.
So, if the WriteProcessMemory() calls are just writing each of the sections from the decompressed PE file in to their designated memory addresses within the new process, and unpack.py has just written each of these blocks of data to a disk file, then we should be able to join all of these blocks of data together and get the PE file:
# Concatenate all of the memory blocks except for the four bytes written to address 0x7ffd7008 # as that looks like an address on the stack, and not a PE file section. $ cat filename.exe.memblk0x4*.wpm > decompressedbinary.exe $ file decompressedbinary.exe decompressedbinary.exe: PE32 executable (GUI) Intel 80386, for MS Windows $ md5sum -b filename.exe.memblk0xb60000.decomp decompressedbinary.exe 425d639fe38dee72e671ed83000d66c2 *filename.exe.memblk0xb60000.decomp 425d639fe38dee72e671ed83000d66c2 *decompressedbinary.exe
… and look at that — the blocks of memory written to the new process, when joined together in memory address order, not only form a Windows PE file, but form the Windows PE file that was decompressed to address 0xb60000. This is good as it confirms what we suspected was happening based on the log data from unpack.py.
Hold on a moment though — if we go back to those WriteProcessMemory() calls, we see that one of them is not like the others. The last call doesn’t write to a 0x4xxxxx address but instead writes to the address 0x7ffd7008. It also writes data from address 0x12fac4, rather than from the decompressed data at 0xb60000, and it only writes four bytes. Let’s have a look at them:
$ od -x filename.exe.memblk0x7ffd7008-1300.wpm 0000000 0000 0040 0000004
Looks familiar — that value, when byte-swapped, is 0x00400000, which looks a lot like the address of our new PE header. I don’t know what this is, and haven’t tried to find out yet. I’m assuming that it is something to do with running the new process, and telling the operating system that the new process’ PE header is now at address 0x400000 (just in case the Windows loader didn’t load it there as part of the CreateProcess() call). That 0x7ffd7008 address didn’t correspond to a memory block when I loaded the malware sample into OllyDbg, but then I didn’t start running it. It would be nice to know what it is achieving by writing 0x00400000 to that address, but it isn’t necessary to see what our malware sample is doing, so we’ll leave it for the time being.
I have a lot more analysis work to write about, but in the interest of shorter blog posts, I’ll break it up into a few posts.
So, we’ve seen this mischievous CryptoLocker malware sample unpack some code and run it. We saw that the unpacked code then created another process, using its own .exe file. This new process was created in a suspended state and then overwritten with sections of a decompressed PE file.
It would be handy to know where that compressed PE file came from, but all we have to go on is the fact that it was decompressed from address 0xb44bb0, yet we don’t see any memory allocated there as the result of a VirtualAlloc() call.
The suspended, and now overwritten, process is then resumed. We also know from the Windows event log entries, that this new process starts explorer.exe and convinces it to go over to the dark side. We know this because it is C:\WINDOWS\explorer.exe that is started, which is the standard Windows version of explorer.exe (assuming that it hasn’t been modified at some point), yet we see it run vssadmin.exe.
We also know from the network capture that the sample keeps spitting out DNS queries for a random-looking host in a random-looking domain.
If you found this interesting, or even if you were just reading it to pass the time on your trip in to work (hopefully not if you were doing the driving though), then join me for the next part where we’ll analyse the decompressed PE file and see what it gets up to.