Over the past two years, rarely did a worm get as much attention that Conficker (aka Downadup) is getting now. Its last variant, the infamous W32/Conficker.C, which surfaced in early March and is set to time-bomb on April 1, is literally all over the media. Of course, its features are well known and documented and some papers (such as SRI's excellent analysis and a blog post from Sourcefire) even give interesting insights on the reverse engineering process. Indeed, while understanding the behavior of the malware is important to most people, learning how to understand it is even more important to some. Does the fable of the fisherman who gives the hungry man a fishing rod rather than a fish sound familiar?
That is the purpose of this post. While not delving into the depths of reverse engineering Conficker, it aims at providing a few tips to whomever may want to participate in the community efforts, for a better understanding of the infamous worm variants. And the best part is that part of these tips apply to other malware pieces.
The first thing to note is that Conficker is encrypted/compressed by a custom "run-time packer", which is a very common strategy to prevent static analysis. Indeed, should you load a copy of the worm in a disassembler, all you'll see is the assembly code of the said run-time packer, and a bunch of compressed data.
The first thing to do, therefore, is to unpack it, to reveal the actual assembly code of the worm.
The following gives some insights that may be useful to achieve this, using OllyDbg and IDAPro.
_Note: It is assumed that doing this in an isolated and safe lab machine is required to avoid possible infection of internal networks. _
2. Loading the malware into the debugger.
The Conficker worm is in fact a DLL file, sometimes obfuscated by a first layer of UPX run-time encryption/compression. Thus it's a good idea to give it a first pass of unpacking with the appropriate UPX version, before loading it into the debugger.
We all have our own methods for debugging DLLs, and my personal choice is to modify the DLL bit flag to turn it into an EXE to the eyes of the debugger. Among other PE editors, CFF Explorer from ntcore is a tool that allows to do that.
Now open the file in OllyDbg and 'Step into' the DllMain() function.
3. Choosing the unpacking method
Run time packers are commonly hard to trace due to many anti-debugging tricks being employed by the malware authors. Yet all of them will certainly undergo some stages before jumping to the actual malicious code. We can roughly divide the unpacking process flow into the following simplified flowchart:
Conficker follows roughly this sequence, decrypting byte by byte and saving the data in non-contiguous location, before reconstructing it again to copy somewhere in memory. That said, there are really two methods to unpack the actual malicious code:
Either you reverse-engineer the unpacking algorithm, re-implement it in a scripting language, and run the script on the packed file (long and tedious)
Or you let the run-time packer do the unpacking job for you, and catch a break right before the execution flow is passed to the actual malicious code
The latter method is the one we'll explore here; while it is quicker, it has a drawback: it implies defeating the anti-debugging tricks scattered over the unpacking code, which are precisely meant to prevent the code to run inside a debugger.
4. The Good, the bad and the branch
Surprisingly, defeating the anti-debugging tricks in Conficker's run-time packer code is not that difficult, once you've noticed something: Throughout the code, "decision-making" code blocks are frequently present at the end of its basic blocks. A "decision-making" code bit looks like this:
call sub_100014A0 ; Unknown function test eax, eax jz loc_1000128E ; branch 1 jmp @loc_10001698 ; branch 2
This is very easy to formulate in English: If the "unknown function" returns 0, then go to branch 1, otherwise go to branch 2. The question therefore is: what does the "unknown function" do? Examples of such functions called in "decision-making" code bits can be seen below:
A specialist eye would quickly spot the RDTSC instructions and the nuisance API calls, very typical of debugger detection strategies implemented by malware authors. Consequently, the "unknown functions" mentioned above really are debugger detectors, returning 0 if the code runs in a debugger. We can thus label the "decision-making" code blocks as such:
call sub_100014A0 ; debugger detector function test eax, eax jz loc_1000128E ; bad branch jmp @loc_10001698 ; good branch
Where "good branch" leads to the following of the unpacking process (until the next decision-making code block), and "bad branch" leads to the exit door (after more or less deceptive circumvolutions).
Therefore, all we need to do to get the unpacking code to run properly is to set breakpoints over each decision-making code block, and force the good branch (either by manually setting the EIP right to it, or by mingling with the registers), hopping over the bad branches in rhythm.
Tracing this malware thus reminds me about Tinikling (an indigenous Filipino dance), where dancers need a coordinated jumping to avoid being hit by the bamboo poles by two other people tapping and sliding the bamboos.
Now we know how to fence over the anti-debugging traps, the last remaining question is: when do I know I am done with the unpacking process? Again, there are various methods for that; the one we will use here consists in identifying when the code flow reaches the "Resolve APIs" phase in the unpacking sequence.
As a matter of fact, to access the services provided by the Operating System, modern compiled code typically resorts to _function pointer tables _(aka IAT in the Windows executable format): the addresses of imported API functions are stored in a table with a static location, and API calls in the code are done via an indirection through this table. It makes the _relocation _process easier: when a dll exporting API functions is dynamically loaded in the process memory space, the Windows loader only needs to update the function pointer table with the imported functions addresses, rather than fixing all the calls to the said API functions throughout the code.
As a matter of fact, once unpacking is complete and the actual malicious code reconstructed, a run-time packer will play the role of the Windows loader, and engage in a relocation process on the Malware's function pointer table. In other (simple) words: it loads the needed dynamic libraries, and writes the addresses of the API functions used by the Malware in its function table. To obtain such addresses, the run-time packer will typically call the Windows API function GetProcAddress().
You have probably already inferred that a breakpoint astutely set on this function will be reached in the "Resolve APIs" phase... At this point, the malware code is fully reconstructed and stands, bare, in memory. Save it, like shown below:
Although not necessary for further analysis in IDA, at this point, it is a good idea to turn this data file into a working Win32 executable file, by reconstructing the proper PE headers, for a "clean" result. Various tools may help in that (eg: ShellCode2Exe )
Once this is done, you're set to load your brand new, unpacked worm copy into IDA and engage in a new battle: understanding the malware actual features. This is left as an... exercise for the reader.
Guillaume Lovet contributed to this report