Zero2Automated - Custom Sample

David S January 29, 2024 Updated: January 29, 2024 #Reverse Engineering #Write-up #Malware #Windows

My write-up for the first custom sample of the Zero2Automated Advanced Malware Analysis course.

Notes

I'm using Binary Ninja and radare2 for disassembling and debugging the sample.

Stage 1

Let's start by opening the main_bin.exe sample in Binja and take a look at the entrypoint.

The entrypoint

Well, only one subroutine to look at, let's call it init...

sub_401711

After C runtime initialization, another sub occurs: sub_404f7 Seems like this calls functions from an array of pointers. let's rename the variables. call_array_of_fns Let's see what pointers are called.

pointers After some digging, it seems that these are also routines for the initialization of the runtime. Guess we have to look again at init.

sub_401711 A sub with no arguments? Quite suspicious.

sub_401400 That is definitly some interesting sub! Seems to be using API obfuscation. It loads libraries dynamically using LoadLibaryA and then uses GetProcAddress for retrieving the address of a subroutine by its name. sub_401300 is called every time when a new string is used. This seems to be the decryption routine. Let's take a look.

String decryption routine. The __builtin_strncpy at 0x0401350 and _strchr at 0x401392 is a big hint for some char-wise substitution. We need to take a closer look at the inner logic of the do-while loop.

rot13 The do-while calculates the address the last char from the hardcoded "abcdef[...]" lookup table which is copied by the __builtin_strncpy into var_4c. Hence, ecx_1 = ecx_2 - &var_4c is the length of the lookup table. The outer loop iterates over the string argument of the decryption routine and stores the current character's position inside the lookup table into edx_2. Now comes the interesting part: the if-else statement. When the character's position plus 0xd is smaller than the lookup table's size, edx_4 is the sum. BUT if not, edx_4 is the character's position MINUS the lookup table's size plus 0xd. This if-else statement is equal to new_pos = (char_pos + 0xd) % lut_size. What is 0xd in decimal? 13! Could the decryption routine be a simple rot13? Let's try it out with one of its arguments!

rot13_dec

Bingo. Let's give this routine a meaningful name (rot13_dec) and use BinaryNinja's power to find every call and decrypt the argument in-place.

in_place_dec After the string decryption, we can see what is going on in in the sub. After renaming the variables: decrypt_resource It is clear that this sub seems to be the main one which firstly loads a resource using LoadResource, allocates memory with a size which is given in the header of the resource and then calls another sub with these as as arguments.

The called routine looks like this. decrypt_routine I don't bother longer with it and continue.

At the bottom of our current routine is another one.

rpi_hollowing

rpi_hollowing_unanno This seems to be doing some process hollowing. Let's annotate it.

rpi_hollowing_anno It gets its own filename by calling GetModuleFileNameA and then spawns the executable with CreateProcessA again. Then, it allocates memory (VirtualAllocEx) in the context of the new process for storing the payload in it. Seems like it does address relocation from 0x4011f3 to 0x401230, then overwrites a register in the thread context with the new entrypoint, sets the new context and then resumes the thread.

We now know how to get the payload and can jump to a VM and run the sample. Let's set a breakpoint at the sub which spawns the thread, since it's only argument is the decrypted payload. We can then dump the payload.

radare2_main_bin

radare2_main_bin rcx hold the first argument, let's see if its a PE by printing it in hex....

As you can see, the first two bytes mark it as a PE, we can even see the DOS stub. Let's dump the whole memory where the payload lays in. radare2_main_bin

That's it, we can now inspect the payload, let's jump to stage 2.

Stage 2

Let's look again at the entrypoint. stage2_entrypoint And jump the to the only function in this sub.

stage2_main_in_crt_init This does again C runtime initialization, hence let's directly jump to the sub sub_401ea0, which is called inside the initialization. It seems to be the main procedure. hash_sub At the beginning, the sub gets its own application path and then trims it to the basename. So the name of the executable. Let's annotate it. main_basename_anno Then, another sub is called with the basename and the length as arguments. The return value is compared with a hardcoded constant. This may be a value returned by an hash function. Let's inspect the sub.

hash_sub_2 Here, registers are XORed quite often with the constant 0xedb88320. A quick Google-Search reveals that this constant is a reversed representation of a generator polynomial for the CRC32 checksum calculation. This sub calculates a CRC32 checksum (let's call it hash) for a given string! hash_string Looking at the cross-references of the hash_string sub, I came to the following sub, which is used many times in the binary. get_fn_by_hash This one seems to load a library by its name using LoadLibraryA and a hardcoded array fo strings. Let's take a look on the array. get_fn_by_hash_libnames These are all standard windows libraries. The first argument of the sub is the offset inside the array, and so the library name. Then, it tries to find the function with the name for which the crc32 hash value is equal to the second argument of the sub. I've annotate the sub and call it for now get_fn_by_hash. get_fn_by_hash_anno Looking at the cross-references, the binary seems to use this sub every time, when it wants to use the Windows API. It seems like, this is the main API hashing method used here. get_fn_by_hash_calls I've decided to grab all function names of the libraries used by get_fn_by_hash and calculate thier CRC32-checksums to create a lookup-table. Then, I've used Binja's API to comment each call of get_fn_by_hash with the the function name which matches the supplied hash.

import binascii

hashes={binascii.crc32(x.encode()):x for x in [*wininet_fns, *ntdll_fns, *kernel32_fns]}

def comment_get_fn_by_hash():
    f=bv.get_functions_by_name("get_fn_by_hash")[0]

    call_pos=list(f.caller_sites)

    for call in call_pos:
        call_is_varinit=type(call.hlil) == binaryninja.highlevelil.HighLevelILVarInit

        tbs_hash=None
        if call_is_varinit:
            tbs_hash=call.hlil.operands[1].operands[1][1].constant
        else:
            tbs_hash=call.hlil.operands[1][1].constant

        if (name:=hashes.get(tbs_hash)) is not None:
            print(f"{name} at {call.address:x}")
            bv.set_comment_at(call.address, name)
            if call_is_varinit:
                call.hlil.operands[0].name=name
        else:
            print(f"{tbs_hash:x} not found")

After running the script: comment_get_fn_by_hash isdebuugerpresenet One can see, that the second if-statement conists of some anti-analysis checks. BUT the sub sub_401dc0 is called in both statement bodies:

same_call_sub_401dc0 Let's inspect this sub next:

decrypt_url Some function names could not be resolved, but InternetReadFile indicates that some payload/config fetching is going on here. The memcpy at 0x401e40 is very interesting. Looking at the do-while loop below, the string is decrypted using simple nibble-wise rotation and XORing with 0xc5. Taking a look at the cross-references to __builtin_memcpy: other_memcpy One can see, that there is another sub which has an encrypted string: decrypt_svhost We can easly write a decryption routine for both of them:

import sys

def decrypt(s, key):
 s_dec=""
 for c in s:
  if c == 0: break
  c=((c&0xf)<<4)|((c&0xf0)>>4)

  c^=key
  s_dec+=(chr(c))
 return s_dec

url= [
    0xda, 0x1b, 0x1b, 0x5b, 0x6b, 0xff, 0xae, 0xae, 0x5b, 0x4a, 0x6b, 0x1b,
    0x0a, 0x7a, 0xca, 0xba, 0xbe, 0x6a, 0xaa, 0x8a, 0xae, 0x7b, 0x4a, 0x2b,
    0xae, 0x8a, 0x98, 0x0a, 0x8a, 0xcf, 0x18, 0x28, 0xea, 0x00
]

string = [
    0x1e, 0x89, 0xef, 0x5f, 0xbc, 0xcc, 0x6c, 0xdc, 0x5d, 0x1d, 0xef, 0x1f,
    0xbd, 0x1d, 0x6d, 0x7c, 0xfc, 0x19, 0x09, 0xef, 0x1d, 0x4d, 0x1c, 0xac,
    0xdc, 0x1d, 0x6d, 0xc8, 0x7c, 0xad, 0x7c, 0x00
]

print("url: %s" % (decrypt(url, 0xc5),))
print("string: %s" % (decrypt(string, 0xa2),))

Which outputs:

url: https://pastebin.com/raw/mLem9DGk
string: C:\Windows\System32\svchost.exe

The content of the pastbin link https://pastebin.com/raw/mLem9DGk is just another url to an large image: https://i.ibb.co/KsfqHym/PNG-02-Copy.png. I guess it contains a hidden payload. Let's now annotate the current sub. fetch_and_run_payload The sub fetch_url at 0x401e76 seems to just to fetch the contents of an url: fetch_url Let's continue by inspecting the next sub at 0x401e7d (I named it fetch_and_run_payload2). fetch_and_run_payload2 This sub seems to firstly create a directory named cruloader in a temporary directory retrieved by GetTempPath and then writes the image from the pastebin URL into this directory using the name output.jpg (found out during dynamic analysis). fetch_and_run_payload2_middle We can jump to a VM and set at breakpoint at the CreateFileW at 0x401503 and dump the string on stack. fetch_and_run_payload2_path The saved image looks like this. fetch_and_run_payload2_pic Next, it seems like the payload extracted and decrypted from the image. I don't want to bother with this and jump directly to the end of the sub. fetch_and_run_payload2_bottom Let's take a look at sub_401d50. sub_401d50 This seems to just initialize some global mem with function pointers of functions for creating processes, and manipulating their memory. Looks like preparation for process hollowing. create_svchost Jumping to sub_401ca0, we can see that is uses the same xor-encryption as explained above. I've already annotated it. Seems like this sub spawns svchost.exe in suspended state (definetly process-hollowing) and then returns the handle to it. We now come to the last sub of fetch_and_run_payload2. run_payload That one does the actual process hollowing, it seems to parse the pe header of the payload, writes the payload into memory owned by the process and relocates addresses. At the end, it resumes the process: ResumeThread At this point, I am quite confident that we can set a breakpoint at the call of this sub and dump the payload, since it is decrypted before: payload_breakpoint Additionally, we need to patch the conditional jump in the main since we don't know the correct basename for the hash: patch_if First, lets set a breakpoint to the je of the if-statement which checks if the binary has the correct basename and overwrite it to jump to the fetch_and_run_payload sub: overwritten_je And then put a breakpoint at the call of run_payload which does the process hollowing and gets the decrypted payload as an argument: breakpoint

breakpoint_top_bot

ebx stores the pointer to the third argument, which is the pointer to the payload, let's inspect it. breakpoint_ebx Bingo again! This is the payload from the Image, let's dump it and continue to stage 3:

stage3_dump

Stage 3 - Final Payload

Let's open the dumped stage 3 payload in Binary Ninja and take a look at the entrypoint. stage3_start One sub; jump to it. stage3_main It is C runtime initialization again, let's jump to the main. stage3_uhoh Well, that was quite short! This seems to be the final payload and the end of the chain.

THANK YOU FOR READING!