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):
Then, it proceeds to call GetProcessMemoryInfo:
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
-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:
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:
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:
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
friends ultimately go through there), and run:
Oops. Something broke. Sometimes, malware uses SEH to obfuscate the control flow, but if you try to open
Debugger windows >
SEH list, you get an error:
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
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:
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):
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
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:
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
FFFFFFFF - but why?
a1 is just the path to the sample itself, but
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[0000071C]:7EFDD000 dd 20h ; LastErrorValue
As per the documentation,
ERROR_SHARING_VIOLATION - which confirms our previous assumptions. To fix it, the simplest way (imo) is to put a breakpoint on
CreateFileA call and change
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE (all permissions), which
1 | 2 | 4 ->
As you can see,
eax now contains a valid
HANDLE, and our
CreateProcessA breapoint finally hits:
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_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:
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.
This “anti-debug” used to catch Cuckoo Sandbox as well (in my experience, at least). ↩︎