Overview

Sample

dca16a0e7bdc4968f1988c2d38db133a0e742edf702c923b4f4a3c2f3bdaacf5 Malware Bazaar

This malware name is crazy, we will refer to it as "ramen noodls" for simplicity.

References

Stage 1

The first stage is a C++ loader that is used to decrypt, load, and execute the 2nd stage shellcode. The program flow of the loader is difficult to follow due to the C++ and some nested structures and callbacks. It is not clear if this was intentional or note.

The shellcode appears to be located in what appears to be a giant Base64 encoded string (not confirmed). This first stage may have alos implmented some PAGE_GUARD exception handling progrem flow redirection for anti-analysis (not confrimed). Dispite this it is trivial to execute the stage until it jumps to the decrypted loaded shellcode (as long as the PAGE_GUARD exceptions are passed to the process).

Stage 2

For analysis we just dumped the entire memory region that contained the second stage shellcode. The region is based at 0x00760000 but the shellcode entry is at 0x00780A68. The memory region dump is available on Malshare d4f37699c4b283418d1c73416436826e95858cf07f3c29e6af76e91db98e0fc0.

The first task of the shellcode is to walk the PEB to locate Kernel32.

PEB Walk _LDR_DATA_TABLE_ENTRY and Shifted Pointers in IDA

The process of "walking the PEB" to access the modules loaded in a process as been a shellcode meme since the beginning of shellcode. It's everywhere, and we all mostly understand how it works.

The issue comes when we try to represent this cleanly in IDA's pseudocode view... accessing a _LDR_DATA_TABLE_ENTRY via its LIST_ENTRY in the _PEB_LDR_DATA structure creates a very messy IDB. Instead of the _LDR_DATA_TABLE_ENTRY members we see unhelpful offsets relative to the Flink and Blink member of the LIST_ENTRY.

The reason this happens is that the InMemoryOrderModuleList is defined as a LIST_ENTRY in the _PEB_LDR_DATA. The LIST_ENTRY struct simply describes a linked list as follows.

typedef struct _LIST_ENTRY {
   struct _LIST_ENTRY *Flink;
   struct _LIST_ENTRY *Blink;
}

The problem comes from where that struct is actually located in the _LDR_DATA_TABLE_ENTRY...

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks; //<---- Offset into the struct!
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
}

Because the LIST_ENTRY is offset into the _LDR_DATA_TABLE_ENTRY it means that when we attempt to cast a pointer to a LIST_ENTRY as a _LDR_DATA_TABLE_ENTRY we are off by some amount (in this case 2 * PVOID = 8 bytes). The following diagram attempts to explain the issue.

One hack might be to create our own custom struct that starts at the offset, but this would be madness when dealing with more than a few types... instead we have... shifted pointers!

IDA Shifted Pointers

IDA has a simple concept (with some insane syntax) to deal with this issue called a shifted pointer. When assigning a type to LIST_ENTRY the shifted pointer syntax can be used to tell IDA that it is actually a pointer inside a larger struct with an offset.

_LIST_ENTRY *__shifted(_LDR_DATA_TABLE_ENTRY,8) pListEntry

References

Thanks

Thanks to everyone who helped me figure this out for once and for all!

Analysis

This stage only serves one purpose; unpack and execute the final stage. The unpacking algorithm is currently unknown!

Thanks Mishap

Full analysis of Stage 2 (with unpacker)

Emulation Attempt

Instead of analyzing this intermediate stage we are going to try and emulate passed it.

from dumpulator import Dumpulator

dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip

print(hex(shellcode_start))


shell_base = 0x00810000
shell_end = 0x0082CFF5
0x82cc90
dp.start(shellcode_start, end=shell_end)
commit(0x84a000[0x1d000], PAGE_READWRITE)
commit(0x21d0000[0x1d000], PAGE_EXECUTE_READWRITE)
stage3_start = dp.regs.ebx

print(hex(stage3_start))
0x21d607f
shell_page = dp.memory.find_region(stage3_start)
print(shell_page.size)
print(shell_page.start)
print(f"Shellcode entrypoint: {hex(stage3_start - shell_page.start)}")
print(f"Shellcode base: {hex(shell_page.start)}")
shell_page_data = dp.read(shell_page.start, shell_page.size)
open('/tmp/dump_stage3.bin','wb').write(shell_page_data)
118784
35454976
Shellcode entrypoint: 0x607f
Shellcode base: 0x21d0000
118784

We don't yet have a way to add exception hooks to dumpulator so if we want to do this without knowing the address of the end of stage 2 then we need to modify the dumpulator source code.

Update! @mishap has a public implementation of a quick and dirty approach to modifying dumpulator for a generic approach to this Dirty exception hook

print(hex(dp.read_long(dp.regs.esp)))
print(hex(dp.read_long(dp.regs.esp-4)))
0x21d0000
0x19fefc
dp.memory.find_region(0x19fefc)
MemoryRegion(0x1a0000, 0x4000, PAGE_READONLY, MemoryType.MEM_MAPPED, None)
dp.exports.get(0x7656f530)
'kernel32.dll:LocalFree'
hex(dp.read_long(0x21d0000))
'0x14c5352'
dp.read(0x13BAC, 10)
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
dp.start(dp.regs.eip, end=0x021D5D88 )
dp.read(0x13BAC, 100)
bytearray(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')
dp.start(dp.regs.eip, end=0x021D6098)
dp.modules.find(dp.regs.eax)
Module(0x76550000, 0xf0000, 'C:\\Windows\\System32\\kernel32.dll')

Stage 3

We used some simple emulation to extract the Stage3 shellcode. This stage is easily identifiable because it has many plaintext strings!

dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip

print(hex(shellcode_start))


shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
print(f"EIP: {hex(dp.regs.eip)}")
dp.start(dp.regs.eip, end=0x021D5D88)
dp.regs.esi
print(hex(dp.regs.esi))
dp.read(dp.regs.esi, 100)
0x21e3bac
bytearray(b'k\x00e\x00r\x00n\x00e\x00l\x003\x002\x00.\x00d\x00l\x00l\x00\x00\x00\x00\x00ZwQueryInformationProcess\x00\x00\x00ntdll.dll\x00\x00\x00KiUserExceptionDispatcher\x00\x00\x00,\x00\x00\x00')

Rename IAT Hashes in IDA

### apis = {dict with hashdb enum of all imports}

api_nums = dict((v,k) for k,v in apis.items())

ptr = 0x0001B114

while ptr < 0x0001BC9E:
    api_hash = ida_bytes.get_dword(ptr)
    api_name = api_nums.get(api_hash,'')
    if api_name != '':
        print("ptr_"+api_name)
        idc.set_name(ptr, "ptr_"+api_name, 0x800) #SN_FORCE
        ptr += 4
    else:
        ptr += 1

Config Extraction

We know that Stage3 has an encrypted config with the URL used to download the malware payload. We are going to attempt to use some emulation to extract this.

from dumpulator import Dumpulator

dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip

print(hex(shellcode_start))


shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
print(f"EIP: {hex(dp.regs.eip)}")
0x82cc90
commit(0x84a000[0x1d000], PAGE_READWRITE)
commit(0x21d0000[0x1d000], PAGE_EXECUTE_READWRITE)
EIP: 0x82cff5
hex(dp.read_long(dp.regs.esp+4))
'0x19ff54'
init_data_offset_0 = dp.read_long(0x19ff54)
dp.read_long(init_data_offset_0)
25521
hex(dp.read_long(0x19ff54+4))
'0x426008'
config_data = dp.read(0x426008, 152)
print(config_data.hex())
a8c49801f17b5f46e0aa0d585c5448b97ce811dbd8d04f9d0988296fb4a6216a90ba17a4f14b8a5e47e4e0f9515f48984522f533ea749bc5306fbd398237cb0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
key = bytes.fromhex('52abdf06b6b13ac0da2d22dc6cd2be6c201769e012b5e6ec0eab4c14734aed51')
out = bytes.fromhex('215248590e000000ef4456b59328427fba4377a1d192aa92687474703a2f2f3131362e3230322e31382e3133322f626c6f622f71336b36746b2e7869386f008e8a1f771e17b45183cfda277723418924c7936eb3cb4ef3541c60510f1dd542c28d53aa06cf6cf4e16e3ebcbc8c4c702a9a31f99e44e93688702587843fe3834d0ca21df074ae3aabfe9b480a09fbd2df9b8ae88b40ad5ce6')
out
b"!RHY\x0e\x00\x00\x00\xefDV\xb5\x93(B\x7f\xbaCw\xa1\xd1\x92\xaa\x92http://116.202.18.132/blob/q3k6tk.xi8o\x00\x8e\x8a\x1fw\x1e\x17\xb4Q\x83\xcf\xda'w#A\x89$\xc7\x93n\xb3\xcbN\xf3T\x1c`Q\x0f\x1d\xd5B\xc2\x8dS\xaa\x06\xcfl\xf4\xe1n>\xbc\xbc\x8cLp*\x9a1\xf9\x9eD\xe96\x88p%\x87\x84?\xe3\x83M\x0c\xa2\x1d\xf0t\xae:\xab\xfe\x9bH\n\t\xfb\xd2\xdf\x9b\x8a\xe8\x8b@\xad\\\xe6"
len('\xefDV\xb5\x93(B\x7f\xbaCw\xa1\xd1\x92\xaa\x92')
16
c2 = out[24:].split(b'\x00')[0].decode('utf-8')
c2
'http://116.202.18.132/blob/q3k6tk.xi8o'

Generic Config Extraction

  • Emulate Stage2
    • Extract Stage3 shellcode
    • Extract the enc config buffer as arg2 passed to the shellcode
  • Use regex to locate hard coded decryption key in Stage3
  • Decrypt!
from dumpulator import Dumpulator

dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip

print(hex(shellcode_start))


shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)

# Extract stage3
stage3_base = dp.read_long(dp.regs.esp)
stage3_size = dp.memory.query(stage3_base).region_size
stage3_data = dp.read(stage3_base, stage3_size)

# Extract encrypted config
ptr_config_data = dp.read_long(0x19ff54+4)
config_data = dp.read(ptr_config_data, 152)
0x82cc90
commit(0x84a000[0x1d000], PAGE_READWRITE)
commit(0x21d0000[0x1d000], PAGE_EXECUTE_READWRITE)
import re
import struct 
from  malduck import rc4

# 6A 20                                   push    20h ; ' '       ; a3
# 8D 85 F8 FE FF FF                       lea     eax, [ebp+arg_rc4_key_stream]
# 68 A4 2C 01 00                          push    offset g_key    ; a2
# 8B 77 04                                mov     esi, [edi+4]
# 50                                      push    eax             ; a1
# E8 03 E6 FF FF                          call    mw_rc4_ksa
# 56                                      push    esi             ; arg_out_buff
# 56                                      push    esi             ; arg_in_buff
# 8D 85 F8 FE FF FF                       lea     eax, [ebp+arg_rc4_key_stream]
# 68 98 00 00 00                          push    98h             ; arg_size


key_egg = rb'\x6A\x20(?:(?!\x6A\x20).)*?\x98\x00\x00\x00'
key_candidates = []
for match in re.finditer(key_egg, stage3_data):
    data_target = match.group()
    key_address = struct.unpack('<I', data_target.split(b'\x68')[1][:4])[0]
    # print(hex(key_address))
    key_offset = key_address
    key_data = stage3_data[key_offset:key_offset+32]
    key_candidates.append(key_data)
    
assert len(key_candidates) != 0

# Test each key and look for decrypted magic byte 
magic_bytes = b'!RHY'

config_out = None

for key in key_candidates:
    out = rc4(key, config_data)
    #print(out)
    if out[:4] == magic_bytes:
        config_out = out
        break
        
assert config_out != None

c2 = config_out[24:].split(b'\x00')[0].decode('utf-8')

print(f'C2: {c2}')
C2: http://116.202.18.132/blob/q3k6tk.xi8o

Hat tip to @printup for this as an alternative to our regex

index: int = len(data)
    while True:
        end_index: int = data.rfind(end_token, 0, index)
        if end_index == -1:
            break
        begin_index: int = data.rfind(begin_token, 0, end_index)
        if begin_index == -1:
            break

        print(f"Found a match: {data[begin_index:end_index + len(end_token)]}")
        index = begin_index