Jump to content
M2G

Using DLL Injection to Automatically Unpack Citadel

Recommended Posts

In this post, I will present DLL injection by means of automatically unpacking Citadel. But first, the most important question:

WHAT IS DLL INJECTION AND REASONS FOR INJECTING A DLL

DLL injection is one way of executing code in the context of another process. There are other techniques to execute code in another process, but essentially this is the easiest way to do it. As a DLL brings nifty features like automatic relocation of code good testability, you don't have to reinvent the wheel and do everything on your own.

But why should you want to injecting a DLL into a foreign process? There are lots of reasons to inject a DLL. As you are within the process address space, you have full control over the process. You can read and write arbitrary memory locations, set hooks etc. with unsurpassed performance. You could basically do the same with a debugger, but it is way more convenient to do it in an injected DLL.

Some showcases are:

  • creation of cheats, trainers
  • extracting passwords and encryption keys
  • unpacking packed/encrypted executables

To me, especially the opportunity of unpacking and decrypting malware is very interesting. During my day job, I had to unpack a bunch of Citadel binaries in order to prepare them for static analysis. Basically, most Citadel samples are packed by the same packer or family of packers. In the following, I will shortly summarize how it works.

THE CITADEL PACKER

In order to evade anti-virus detection, the authors of the Citadel packer (and other commercial malware) have devised an interesting unpacking procedure. Roughly, it can be summarized in the following stages:

First, the unpacker stub does some inconspicuously looking stuff in order to thwart AV detection. The code is slightly obfuscated, but not as strong as to raise suspicion. Actually, the code that is being executed decrypts parts of the executable and jumps to it by self-modifying code. In the snippet below, you see how exactly the code is modified. The first instruction of the function that is supposedly called is changed to a jump to the newly decrypted code.

mov     [ebp+var_1], 0F6h 

mov al, [ebp+var_1]

mov ecx, ptr_to_function

xor al, 0A1h

sub al, 6Eh

mov [ecx], al ; =0xE9

mov ecx, ptr_to_function

...

mov [ecx+1], eax ; delta to decrypted code

...

call eax

As you can see (after doing some math), an unconditional near jmp is inserted right at the beginning of the function to be called. Hence, by calling a supposedly normal function, the decrypted code is executed.

The decrypted stub allocates some memory and copies the whole executable to that memory. Them it does some relocation (as the base address has changed) and executes the entry point of executable. In the following code excerpt, you can see the generic calculation of the entry point:

mov     edx, [ebp+newImageBase]

mov ecx, [edx+3Ch] ; e_lfanew

add ecx, edx ; get PE header

...

mov ebx, [ecx+28h] ; get AddressOfEntryPoint

add ebx, edx ; add imageBase

...

mov [ebp+vaOfEntryPoint], ebx

...

mov ebx, [ebp+vaOfEntryPoint]

...

call ebx

Here, the next stage begins. At first glance it seems the same code is executed twice, but naturally, there's a deviation in control flow.

For example, the the packer authors had to make sure that the encrypted code doesn't get decrypted twice. For that, they declared a global variable which in this sample initially holds the value 0x6E6C82B7. So upon first execution, the variable alreadyDecrypted is set to zero.

mov     eax, alreadyDecrypted

cmp eax, 6E6C82B7h

jnz dontInitialize

...

mov alreadyDecrypted, 0

dontInitialize:

...

In the decryption function, that variable is checked for zero, as you can see/calculate in the following snippet:

mov     [ebp+const_0DF2EF03], 0DF2EF03h

mov edi, 75683572h

mov esi, 789ADA71h

mov eax, [ebp+const_0DF2EF03]

mov ecx, alreadyDecrypted

xor eax, edi

sub eax, esi

cmp eax, ecx ; eax = 0

jnz dontDecrypt

Once more, you see the obfuscation employed by the packer.

Then, a lengthy function is executed that takes care of the actual unpacking process. It comprises the following steps:

  • gather chunks of the packed program from the executable memory space
  • BASE64-decode it
  • decompress it
  • write it section by section to the original executable's memory space, effectively overwriting all of the original code
  • fix imports etc.

After that, the OEP (original entry point) is called.

The image below depicts a typical OEP of Citadel. Note that after a call to some initialization function, the first API function it calls is SetErrorMode.

citadel_oep.gif

Weaknesses

What are possible points to attack the unpacking process? Basically, you can grab the unpacked binary at two points:

  • first, when it is completely unpacked on the heap, but not yet written to the original executable's image space, and
  • second, once Citadel has reached its OEP.

The second option is the most common and generic one when unpacking binaries, so I will explain that one. Naturally, you can write a static unpacker and perhaps one of my future posts will deal with that.

One of the largest weaknesses are the memory allocations and setting the appropriate access rights. As a matter of fact, in order to write to the original executable's memory, the Citadel unpacker grants RWE access to the whole image space. Hence, it has no problems accessing and executing all data and code contained in it.

If you set a breakpoint on VirtualProtect, you will see what I mean. There are very distinct calls to this function and the one setting the appropriate rights to the whole image space really sticks out.

After a little research, I found two articles dealing with the unpacking process of the Citadel packer (here and here), but both seem not aware that the technique presented in the following is really easily implemented.

Once you have reached the VirtualProtect call that changes the access rights to RWE, you can change the flags to RW-only, hence execution of the unpacked binary will not be possible. So, once the unpacker tries to jump to the OEP, an exception will be raised due to missing execution rights.

So, now that we know the correct location where to break the packer, how to unpack Citadel automatically?

Here DLL injection enters the scene. The basic idea is very simple:

  • start the Citadel binary in suspended state
  • inject a DLL
  • this DLL sets a hook on VirtualProtect, changing RWE to RW at the correct place as backup, a hook on SetErrorMode is set. Hence, when encountering unknown packers, the binary won't be executed for too long.
  • resume the process

Some other things have to be taken care of, like correctly dumping the process and rebuilding imports, but these are out of the scope of this article. If you encounter them yourself and don't know how to handle them, just ask me ;-)

It seems not too easy to find a decent DLL injector. Especially, one that injects a DLL before the program starts (if there is one around, please tell me). As I could not find an injector that is capable of injecting right at program start, I coded my own. You can find it at my GitHub page. It uses code from Jan Newger, so kudos to him. I'm particularly fond of using test-driven development employing the googletest framework

Conclusion

The presented technique works very well against the Citadel unpacker (sorry, I don't know any other name for that unpacker). So far, I've encountered about 50 samples and almost all can be unpacked using this technique. Furthermore, all unpackers that overwrite the original executable's image space can be unpacked by this technique. In future posts, I will evaluate this technique against other packers.

Fun fact: it seems Brian Krebs coded Citadel after all:

citadel_krebs.gif

If you supply the command line option -z to a Citadel sample, you will see the following message box:

citadel_krebs_msgbox.gif

Sursa

  • Upvote 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...