# Matanbuchus Triage Notes **research.openanalysis.net/matanbuchus/loader/yara/triage/dumpulator/emulation/2022/06/19/matanbuchus-** triage.html OALABS Research June 19, 2022 ## Overview #### Matanbuchus is a malware downloader that has been observed as the final loading stage in multiple phishing campaigns. There are two components, a Matanbuchus loader intented to load the Matanbuchus downloader. Once the downloader is executed it makes an HTTP request to the C2 with some victim info, and downloads the final payload. According to DCSO CyTec... Matanbuchus is the name given to a Malware-as-a-Service sold on Russian-speaking cybercriminal forums. Starting at a rental price of $2,500, the malware consists of an obfuscated two-stage loader which has been deployed in conjunction with Qakbot and Cobalt Strike payloads. ----- ### References ## Analysis #### The DLL has an export called HackCheck that uses OutputDebugStringA to print ``` start dll HackCheck The sample uses API hashing with FNV1a hash algo to resolve API calls ### Yara Rule #### This yara rule is very brittle and needs lots of testing/refining ``` ----- ``` rule matanbuchus { meta: description = "Identifies matanbuchus" strings: // recursive fnv1 hash // 69 C2 93 01 00 01 imul eax, edx, 1000193h // 50 push eax // B9 01 00 00 00 mov ecx, 1 // C1 E1 00 shl ecx, 0 $x1 = { 69 c2 93 01 00 01 50 b9 01 00 00 00 c1 e1 00 } // string decryption // 0F BE 1C 01 movsx ebx, byte ptr [ecx+eax] // 33 DE xor ebx, esi // 6A 00 push 0 // 6A 01 push 1 $x2 = { 0f be 1c 01 33 de 6a 00 6a 01 } condition: all of ($x*) } #### Yara Rule Revised ``` ----- ``` rule matanbuchus { meta: description = "Identifies matanbuchus" strings: // fnv1 hash // 69 C2 93 01 00 01 imul eax, edx, 1000193h // // 69 C0 93 01 00 01 imul eax, 1000193h $x1 = { 69 ?? 93 01 00 01 } //pe loader $x2 = { B8 4D 5A 00 00 } $x3 = { 81 38 50 45 00 00 } //InternetCloseHandle $x5 = { 66 E9 DD 4D } //InternetOpenUrlA $x6 = { BC 8B CF F4 } //InternetCheckConnectionA $x7 = { 3F 82 58 52 } //InternetReadFile $x8 = { 66 E9 DD 4D } condition: all of ($x*) } ### String Decryption #### Simple Decryption Some strings are built as stack strings then copied into a buffer and returned from a function. The returned buffer is then decrypted directly using a simple XOR decryption routine where the first byte is the key. import struct data = b'jl8|tt8Py{s[p}{s' key = struct.pack('(?:\xC6\x85.{5}){0,})(?P(?:\xC6\x45..){1,})' for m in re.finditer(string_egg, file_data): match_data = m.group(0) #stack_strings.append({"data":match_data.replace(b'\xC6\x45',b'')[1::2], "match":match_data}) stack_strings.append({"data":m['a'][6::7] + m['b'][3::4], "match":match_data}) keys = [] key_byte_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x6a(.)' for m in re.finditer(key_byte_len_egg, file_data): keys.append( {'key':m.group(2) + m.group(1), 'length':ord(m.group(3))}) key_dw_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x68(....)' ``` ----- ``` for m in re.finditer(key_dw_len_egg, file_data): keys.append( {'key':m.group(2) + m.group(1), 'length':struct.unpack('(?:\xC6\x85.{5}){0,})(?P(?:\xC6\x45..){3,})' for m in re.finditer(string_egg, file_data): match_data = m.group(0) #stack_strings.append({"data":match_data.replace(b'\xC6\x45',b'')[1::2], "match":match_data}) stack_strings.append({"data":m['a'][6::7] + m['b'][3::4], "match":match_data}) keys = [] key_byte_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x6a(.)' for m in re.finditer(key_byte_len_egg, file_data): keys.append( {'key':m.group(2) + m.group(1), 'length':ord(m.group(3))}) key_dw_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x68(....)' for m in re.finditer(key_dw_len_egg, file_data): keys.append( {'key':m.group(2) + m.group(1), 'length':struct.unpack(' 4: print(ss) except: continue ``` ----- ``` Failed to read module data C:\Users\IEUser\Desktop\DLLLoader32_82D6.exe "C:\Users\IEUser\Desktop\DLLLoader32_82D6.exe" e03ed Uninstall 3fe11 Running exe Starting the exe with parameters Run CMD in memory Run PS in memory Running dll in memory #3 (DllInstall(Unstall)) Running dll in memory #3 (DllInstall(Install)) Regsvr32 & Execute MemLoadDllMain || MemLoadExe zNETjp 5deb9c Run EXE with admin rights TAMfm RunDll32 & Execute Crypt update & Bots upgrade Running dll in memory #2 (DllRegisterServer) tbesqn #### Different Sample From Same Family This is clearly a different sample based on the decrypted strings, but it seems to be part of the matanbuchus family... maybe this is a payload instead of a loader? The sample matches analysis from this blog: Introduction of a PE file extractor for various situations> We need to figure out why these samples are so different... ## Taking a Closer Look At Obfuscated Samples #### Obfuscated sample b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e - does match yara (2021-11-12 11:47:44 UTC) Rony bd68ecd681b844232f050c21c1ea914590351ef64e889d8ef37ea63bd9e2a2ec doesn't match yara (2022-06-14 10:30:32 UTC) These samples appear to be the same (they are likely the "payload" portion of Matanbuchus) but the earlier sample uses obfuscated string encryption, while the newer sample uses the simpler stack based string encryption. ### Fixing our Yara Rule to Match the Stack Based String Encryption Payload (as well as loaders) ``` ----- #### This was a simple fix! The payload does not contain the murmur hash code, once we removed that the yara rule matched all samples. ### Let's Take a Look At the Obfusacated Strings #### We know that the one sample we have that is a "payload" will likely have some of the same strings as the obfuscated sample so let's pull these our first as a reference. ----- ``` b Content Length: \x00 b'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe \x00' b'collectiontelemetrysystem.com\x00' b'DllRegisterServer\x00' b'097f5m\x00' b'Running dll in memory #3 (DllInstall(Unstall))\x00' b'runas\x00' b'.exe\x00' b'timeout /t 3 && move /Y \x00' b'Running dll in memory #3 (DllInstall(Install))\x00' b'.exe\x00' b'.exe\x00' b'TiC7\x00' b'.nls\x00' b'Run PS in memory\x00' b'Admin\x00' b'%LOCALAPPDATA%\\\x00' b'DllInstall\x00' b'cmd.exe /c \x00' b'collectiontelemetrysystem.com\x00' b'Not in domain\x00' b'regsvr32.exe \x00' b'%PROCESSOR_REVISION%\\\x00' b'%APPDATA%\\\x00' b'41.4.0\x00' b'8QN04\x00' b'64 Bit\x00' b'8S2x\x00' b'Starting the exe with parameters\x00' b'C:\\Windows\\System32\\cmd.exe /c \x00' b'cmd.exe /c \x00' b'High start exe\x00' b'Running exe\x00' b'User-Agent: \x00' b'%PROCESSOR_REVISION%\\\x00' b'Content-Type: application/x-www-form-urlencoded\x00' b'%APPDATA%\\\x00' b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\x00' b'\\explorer.exe\x00' b'MemLoadDllMain || MemLoadExe\x00' b'Run CMD in memory\x00' b'3CEk\x00' b'3m7x\x00' b'.nls\x00' b'%APPDATA%\\\x00' b'NCST \x00' b'BCha\x00' b"/c ECHO 'You must restart the program to resolve a critical error!' && start \x00" b'7eriel\x00' b'%APPDATA%\\\x00' b'.nls\x00' b'%PROCESSOR_REVISION%\\\x00' ``` ----- ``` b \\ z{VIS\rA6\rb\x00 b'%LOCALAPPDATA%\\\x00' b'IsWow64Process\x00' b'rundll32.exe \x00' b'%PROCESSOR_REVISION%\\\x00' b'kernel32\x00' b'%PROCESSOR_REVISION%\\\x00' b'.nls\x00' b'User\x00' b'%PROCESSOR_REVISION%\\\x00' b'.nls\x00' b'\r\n\r\n\x00' b'\rXOGONSEzBER%\x00' b'%PROCESSOR_REVISION%\\\x00' b'\nost: \x00' b'MemLoadShellCode\x00' b'Q6X6\x00' b'timeout /t 3 && del \x00' b'cmd.exe\x00' b'%02X-%02X-%02X-%02X-%02X-%02X\x00' b'Running dll in memory #2 (DllRegisterServer)\x00' b'Crypt update & Bots upgrade\x00' b'timeout /t 3 && del \x00' b'3fe11\x00' b'%PROCESSOR_ARCHITECTURE%\x00' b'%PROCESSOR_ARCHITECTURE%\x00' b'NSeyDX\x00' b'%APPDATA%\\\x00' b'4nes\x00' b'jpofxs\x00' b'Not in domain\x00' b'DllInstall\x00' b' && rd /s /q \x00' b' && regsvr32.exe -e "\x00' b'32 Bit\x00' b'%USERDOMAIN%\x00' ``` ----- ``` from dumpulator import Dumpulator, syscall from dumpulator.native import * import pefile @syscall def ZwQueryVolumeInformationFile(dp: Dumpulator, FileHandle: HANDLE, IoStatusBlock: P(IO_STATUS_BLOCK), FsInformation: PVOID, Length: ULONG, FsInformationClass: FSINFOCLASS ): return STATUS_SUCCESS dp = Dumpulator("/tmp/b9b.dmp", quiet=True) dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp)) functs = [0x74040976,0x740409A4,0x740409BA,0x74040998,0x7404098C,0x7403F910,0x7403F91C,0x7403F9 functs = [0x7403FC98] for fn in functs: dp.call(fn,[]) data_start = 0x7407E000 data_end = 0x74080228 for ptr in range(data_start,data_end,4): try: ss = dp.read_str(dp.read_ptr(ptr)) if len(ss) >= 4: print(ss) except: continue print("\n\n\n ===\n") print(dp.read_str(dp.read_ptr(0x7407FDFC ))) ``` ----- ``` Failed to read module data C:\Users\IEUser\Desktop\DLLLoader32_82D6.exe "C:\Users\IEUser\Desktop\DLLLoader32_82D6.exe" Q6X6 zkC7 rJqU e03ed 3CEk Uninstall 3fe11 Running exe au5o Starting the exe with parameters 3m7x Run CMD in memory Run PS in memory Running dll in memory #3 (DllInstall(Unstall)) Running dll in memory #3 (DllInstall(Install)) Regsvr32 & Execute MemLoadDllMain || MemLoadExe b2tb hszA zNETjp 5deb9c DS2x Run EXE with admin rights TAMfm RunDll32 & Execute wgjv Crypt update & Bots upgrade f1da nX8y Running dll in memory #2 (DllRegisterServer) tbesqn === Running exe ``` ----- ``` FILE_PATH '/tmp/samples/b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e.bin' file_data = open(FILE_PATH,'rb').read() def xor_decrypt(data, key): out = [] for i in range(len(data)): out.append(data[i] ^ key[i%len(key)]) return bytes(out) def is_ascii(data): return re.match(B"^[\s!-~]+\0*$", data) is not None # .text:74049A7D C6 45 B8 0D mov byte ptr [ebp-48h], 13 # .text:74049A81 C7 45 E0 6E 60 69 23 mov dword ptr [ebp-20h], 2369606Eh # .text:74049A88 C7 45 E4 68 75 68 2D mov dword ptr [ebp-1Ch], 2D687568h # .text:74049A8F C7 45 E8 22 6E 2D 00 mov dword ptr [ebp-18h], 2D6E22h # .text:74049A96 C7 45 EC DA 4E 1E 00 mov dword ptr [ebp-14h], 1E4EDAh test_data = unhex('895db4c645b80dc745e06e606923c745e46875682dc745e8226e2d00c745ecda4e1e0052') stack_strings = [] string_egg = rb'(?P(?:\xC6\x45..){1})(?P(?:\xC7\x45.....){2,})' for m in re.finditer(string_egg, file_data): match_data = m.group(0) #print(tohex(match_data)) key = m['a'][3] raw_data = m['b'] str_data = b'' for i in range(0,len(raw_data),7): str_data += raw_data[i:i+7][-4:] print(xor_decrypt(str_data, bytes([key]))) ``` ----- ``` b Shell32.dllR b'Matanbuc' b' HTTP/1.' b'User-Agent: ' b'DllInsta' b'DllInsta' b'cmd.exe /c \x05\xdfK\x1b\x05' b' && rd /s /q' b'cmd.exe /c \r\xd7C\x13\r' # .text:7404406B C7 45 AC 71 5D 48 5D mov dword ptr [ebp-54h], 5D485D71h # .text:74044072 C7 45 B0 52 5E 49 5F mov dword ptr [ebp-50h], 5F495E52h # .text:74044079 C7 45 B4 54 49 4F 0A mov dword ptr [ebp-4Ch], 0A4F4954h # .text:74044080 66 C7 45 B8 0A 0A mov word ptr [ebp-48h], 0A0Ah # .text:74048CF5 C6 45 B8 31 mov byte ptr [ebp-48h], 31h ; '1' # .text:74048CF9 C7 45 E0 75 5D 5D 78 mov dword ptr [ebp-20h], 785D5D75h # .text:74048D00 C7 45 E4 5F 42 45 50 mov dword ptr [ebp-1Ch], 5045425Fh # .text:74048D07 66 C7 45 E8 5D 5D mov word ptr [ebp-18h], 5D5Dh # .text:74048D0D 88 5D EA mov [ebp-16h], bl # .text:74048D10 C7 45 EC DA 4E 1E 00 mov dword ptr [ebp-14h], 1E4EDAh # .text:740454A9 30 14 08 xor [eax+ecx], dl # .text:740454AC 41 inc ecx # .text:740454AD 83 F9 1D cmp ecx, 1Dh File "", line 18 80 b0 d8 ff 07 74 a8 40 83 f8 06 ^ SyntaxError: invalid syntax ``` ----- ``` from dumpulator import Dumpulator, syscall from dumpulator.native import * import pefile @syscall def ZwQueryVolumeInformationFile(dp: Dumpulator, FileHandle: HANDLE, IoStatusBlock: P(IO_STATUS_BLOCK), FsInformation: PVOID, Length: ULONG, FsInformationClass: FSINFOCLASS ): return STATUS_SUCCESS dp = Dumpulator("/tmp/b9b.dmp", quiet=True) dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp)) start_addr = 0x74044390 end_addr = 0x740443C1 dp.start(start_addr, end=end_addr) dp.read(dp.regs.ebx, 14) Failed to read module data bytearray(b'193.56.146.60\\') ### String Decryption Recap #### With these older samples, there are 3 different string decryption methods used Stack strings build with DWORDs that are decrypted using a single byte XOR, the byte is the first byte pushed onto the stack string (this is the same method used for small strings in the new samples) Global strings that are generated using constructors which have some light obfuscation. To deal with these we simply emulate all of the constructors and scrape the global strings from the .data section of the PE. The third and most complex method relies on two calls to functions used to build the encrypted string and then a simple single-byte XOR to decrypt the string. Complex Third String Decryption Method ``` ----- ``` c6 45 c4 53 57 6a 08 740458C7 .text:740436A5 BE DA 4E 1E 00 mov esi, 1E4EDAh .text:740436AA 89 9D 28 FF FF FF mov [ebp+var_D8], ebx .text:740436B0 89 9D 2C FF FF FF mov [ebp+var_D4], ebx BE DA ?? ?? ?? 89 ?? ?? ?? ?? ?? 89 .text:740480FF 89 5D B0 mov [ebp-50h], ebx .text:74048102 8B D3 mov edx, ebx .text:74048104 89 5D B4 mov [ebp-4Ch], ebx .text:74048107 89 5D B8 mov [ebp-48h], ebx .text:7404810A C6 45 BC 1A mov byte ptr [ebp-44h], 1Ah 89 ?? ?? 8b ?? 89 ?? ?? 89 ?? ?? c6 45 .text:740431A2 72 F7 jb short loc_7404319B .text:740431A4 88 59 0C mov [ecx+12], bl .text:740438E2 72 F7 jb short loc_740438DB .text:740438E4 88 59 0B mov [ecx+0Bh], bl ``` ----- ``` from dumpulator import Dumpulator, syscall from dumpulator.native import * import pefile @syscall def ZwQueryVolumeInformationFile(dp: Dumpulator, FileHandle: HANDLE, IoStatusBlock: P(IO_STATUS_BLOCK), FsInformation: PVOID, Length: ULONG, FsInformationClass: FSINFOCLASS ): return STATUS_SUCCESS def emulate(start_addr, end_addr, ret_reg, str_len): dp = Dumpulator("/tmp/b9b.dmp", quiet=True) dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp)) dp.start(start_addr, end=end_addr) return dp.read(dp.regs.__getitem__(ret_reg), str_len) pe = pefile.PE(data=file_data) em_egg = rb'\xBE\xDA...\x89.....\x89.+?(?=\x72\xf7\x88)' for m in re.finditer(em_egg, file_data): start_offset = m.start() end_offset = m.end() + 2 start_addr = pe.get_rva_from_offset(start_offset) + 0x74030000 end_addr = pe.get_rva_from_offset(end_offset) + 0x74030000 str_len = file_data[end_offset + 2] if file_data[end_offset + 1] == 0x5f: reg_name = 'edi' elif file_data[end_offset + 1] == 0x59: reg_name = 'ecx' print(f"Testing: {hex(start_addr)}") try: print(emulate(start_addr, end_addr, reg_name, str_len )) except: print("fail") continue ``` ----- ``` Testing: 0x74042f48 Failed to read module data bytearray(b'IPHLPAPI.DLL') Testing: 0x740430ce Failed to read module data bytearray(b'IPHLPAPI.DLL') Testing: 0x740436a5 Failed to read module data bytearray(b'\xfb\xcc\xf5\xfc\xd5i\xd2\t\xf7nz') Testing: 0x74043814 Failed to read module data bytearray(b'\x98\xe7\xe1\x13-\x0fo\xc0hUY') Testing: 0x74043991 Failed to read module data fail ``` -----