Reverse engineering malware: TrickBot (part 1 - packer)

In this post, I will show how to unpack, dump, and analyze a TrickBot sample. The goal is to show the reader the techniques used by malware analysts and why they are used, including wrong assumptions, mistakes, and everything that happens in real-life malware analysis, in contrast to the usual tutorials, where everything goes right (and if something goes wrong, the reader is left alone).

The requirements are, as usual, basic reverse engineering skills, a VM, and IDA.

Sample hash: 01e771dc6cf9572eac3d87120d7a7d1ff95fdc1499b668c7fde2919e0f685256


After opening the sample in IDA, we can quickly see that the sample does a bunch of boring initialization (deobfuscating API call names/strings, finding functions, etc):

boring initialization

Then, it proceeds to call GetProcessMemoryInfo:

anti-debug

A quick Google search yields this anti-debug technique, so if your debugger is caught, just invert CF on the corresponding ja instruction and move on.1

Looking forward, you can see that the sample tries to create a mutex, and if it doesn’t exist, it relaunches itself with -l as a command line parameter, and deletes itself. In the next run, the mutex already exists, so the other branch is taken, which is the path that ultimately leads to infection:

infection mutex stuff

However, you don’t really need to let it launch another process, attach to it, etc. Just modify the control flow so that it directly tries to infect you, to spare yourself a minute or two:

change EIP

As you can see, I selected the inverse branch, and used Ctrl + N to skip through the first step.

Now, the next function that is called is a bit scary:

scary function

You don’t need to read nor understand all of that - if you do, you’re accomplishing the author’s goal - to distract you from actual analysis. As you can see at the end of that GIF, there’s an API call: CreateProcessA, which suggests that the sample is going to do a RunPE or something like that. You don’t care – all you want is the final, unpacked, easy-to-analyze sample, and that’s what you’ll get.

To proceed, put a breakpoint on CreateProcessA (you can put it in CreateProcessInternalW if you’re unsure, as all CreateProcess friends ultimately go through there), and run:

exception

Oops. Something broke. Sometimes, malware uses SEH to obfuscate the control flow, but if you try to open Debugger > Debugger windows > SEH list, you get an error:

no SEH

This suggests that the SEH list is empty, and that the exception is legit (i.e. something broke). However, if you run the binary outside of a debugger, it infects you, so it must be us breaking something.

To find out, look at the faulting instruction:

.text:0040286D mov     esi, [ebp+arg_0]
.text:00402870 mov     dl, [esi+edx] ; esi = 0, edx = 0xA -> exception

esi is 0, but that’s obviously wrong - to find out what its original value should be, look at the instruction above: esi comes from arg_0. Cross-reference the function to find out where the first parameter comes from, and you end up with:

important parameter

important parameter function

As you can see, some global variable is zero. Cross-reference it to see where it gets set, and select the w item (i.e. xref that writes to the variable):

important variable - write

At first sight, it appears that the GlobalAlloc call is failing, but if you put a breakpoint and run the binary, it never hits, and you still get the exception, which means that the other branch is taken - which, in turn, means that GetFileSize is returning -1. But how can that be possible? The referenced file is the sample itself, why can it not get its own size?

To find the answer, put a breakpoint on GetFileSize, re-launch the binary, and check the handle passed to the function, to find out more about the file that’s causing problems:

exception

Snap! It doesn’t hit. If you look above, you’ll see:

  v151 = kernel32_CreateFileA(a1, 2147483648, 0, 0, 3, 0, 0);
  if ( v151 == -1 )
    return 0;

A-ha! That must be the part that’s returning. Putting a breakpoint on the call to CreateFileA, you see that’s what effectively happens, and that CreateFileA returns FFFFFFFF - but why? a1 is just the path to the sample itself, but CreateFileA can’t get a handle to it.

If you look at the CreateFile documentation, you’ll see that the third parameter is dwShareMode, and here it is 0, which means that the call is asking for full control over the file, without allowing sharing with other processes:

Prevents other processes from opening a file or device if they request delete, read, or write access.

However, our debugger has a handle to that file, and CreateFileA won’t close it - it’ll just complain that it can’t get that handle, and return -1. Our assumption is further confirmed by checking TIB.LastErrorValue:

TIB[0000071C]:7EFDD000 dd 20h                                  ; LastErrorValue

As per the documentation, 0x20 means ERROR_SHARING_VIOLATION - which confirms our previous assumptions. To fix it, the simplest way (imo) is to put a breakpoint on the CreateFileA call and change dwShareMode to FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE (all permissions), which is 1 | 2 | 4 -> 7:

it works

As you can see, eax now contains a valid HANDLE, and our CreateProcessA breapoint finally hits:

createprocess breakpoint

It is creating a process using CREATE_SUSPENDED, which smells a lot like RunPE / process hollowing / whatever you want to call it. Therefore, we risk it and put a breakpoint on ntdll_NtWriteVirtualMemory and ntdll_NtResumeThread, and run; the former would supposedly be used to write the PE, while the latter to execute the thread that’s going to run the written bytes. The breakpoint on ntdll_NtWriteVirtualMemory might seem unnecessary, but it is quite necessary, because if you manage to catch the non-memory-mapped PE file before it’s written, you can very easily dump it and further reversing gets much easier.

Going forward, after running the binary, the NtWriteVirtualMemory breakpoint will hit:

ntwritevirtualmemory

If you check the Buffer parameter, you see:

debug015:00365C80 db  4Dh ; M
debug015:00365C81 db  5Ah ; Z
debug015:00365C82 db  90h
debug015:00365C83 db    0

Bingo! Now, dump that and kill the current debugging session.

To be continued…


If you want to leave a comment, check out the reddit discussion.


  1. This “anti-debug” used to catch Cuckoo Sandbox as well (in my experience, at least). ↩︎