Reversing the QBot interceptor
Table of contents
- Table of contents
- Introduction
- Executive summary
- Hashes
- Architecture
- Hooking framework
- Traffic interception
- String obfuscation
Introduction
This analysis covers a QBot traffic interceptor malware sample using Ghidra. The primary goals are to identify the hooking framework, document all traffic interception subroutines and identify string obfuscation methods. Detection rules are provided as YARA and capa.
Executive summary
This sample is a QBot man-in-the-browser injector DLL that comes packed with two encrypted payloads (32-bit and 64-bit). It resolves all its API calls dynamically, looks through processes and injects the right payload. Once the payload is injected, it drops inline hooks across different areas: WinInet (HTTP interception), Chrome’s BoringSSL, Firefox’s NSS/NSPR stack, keystroke and clipboard grabbing, DNS redirection and connection-level functions.
Hashes
Injector DLL:
$ mv 7fc3f5e06bbaad459af71d3c0d28c51b7802546984f886f14a1b12a779fff6f8 qbot.dll
$ md5sum < qbot.dll
a59a669a40ecdfdf9d3ae0c3f2b2db34 -
$ sha1sum < qbot.dll
577522512506487c63a372bbda77be966c23cbd1 -
$ sha256sum < qbot.dll
7fc3f5e06bbaad459af71d3c0d28c51b7802546984f886f14a1b12a779fff6f8 -
Payload (32-bit):
$ md5sum < payload_32bit.dll
6e393f179b71d6ac848668e26c2cd236 -
$ sha1sum < payload_32bit.dll
50cf91ee5ff2c723ce18745cff0c0f5648e3a5e5 -
$ sha256sum < payload_32bit.dll
4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089 -
Payload (64-bit):
$ md5sum < payload_64bit.dll
c537b90fdaaf77d1d8d5502b5f1f8041 -
$ sha1sum < payload_64bit.dll
ecf5afa76e9e41174e31546450ba59391025ac0b -
$ sha256sum < payload_64bit.dll
9155b45cc143122bf57cad69c8b84cccdea63a45e18745df0f6076cacc316399 -
Architecture
| Component | Location | Role |
|---|---|---|
| Injector DLL | Sample itself | API resolution, process enumeration, payload injection. |
| 32-bit payload | RES_DATA_1 (XOR-encrypted) |
Hooking engine + all interception. |
| 64-bit payload | RES_DATA_2 (XOR-encrypted) |
Same, for 64-bit targets. |
Hooking framework
The inline hooking engine lives in the payloads. Both architectures use the same design but there are some minor differences (e.g. because of the different pointer size). These differences are not discussed here. Because of time limitations, only the 32-bit variant is discussed.
Hook installation: FUN_10002902_install_hook loops through 5 encrypted descriptor tables containing DLL/API name pairs. It takes 2 args: number of entries in the table and the address of the table. Each table is an array of the following struct:
struct hook_entry {
DWORD dll_blob_offset; // 0x00 encrypted string offset --> DLL name
DWORD api_blob_offset; // 0x04 encrypted string offset --> API name
PVOID hook_function; // 0x08 function to redirect to
PVOID *original_slot; // 0x0C where to save original pointer
BYTE pad[5]; // 0x10
}; // 21 bytes
Hook tables (table entries are added as comment for simplicity):
void FUN_10002a0f_install_zwresumethread_hook(void)
{
/* ntdll.dll/ZwResumeThread */
FUN_10002902_install_hooks(1,0x100249d4);
return;
}
void FUN_100029f5_install_core_hooks(void)
{
/* ntdll.dll/LdrLoadDll
ws2_32.dll/WSAConnect
ws2_32.dll/connect
ws2_32.dll/WSASend
ws2_32.dll/send
dnsapi.dll/DnsQuery_A
dnsapi.dll/DnsQuery_W
dnsapi.dll/Query_Main
user32.dll/TranslateMessage
user32.dll/GetClipboardData */
FUN_10002902_install_hooks(10,0x100249f0);
return;
}
void FUN_10002a02_install_wininet_hooks(void)
{
/* HttpSendRequestA
HttpSendRequestW
InternetReadFile
InternetReadFileExA
InternetCloseHandle
InternetQueryDataAvailable
HttpOpenRequestA
HttpOpenRequestW
HttpSendRequestExW
InternetWriteFile */
FUN_10002902_install_hooks(10,0x10024b20);
return;
}
void FUN_10002cf3_install_firefox_ssl_hooks(void)
{
int iVar1;
iVar1 = FUN_1000a1f0();
if (iVar1 != 0) {
/* nss3/PR_OpenTCPSocket
nss3/PR_Read
nss3/PR_Write
nss3/PR_Close
nspr4/PR_OpenTCPSocket
nspr4/PR_Read
nspr4/PR_Write
nspr4/PR_Close */
FUN_10002902_install_hooks(8,0x10024bf8);
return;
}
return;
}
void FUN_10002a1c_install_message_loop_hooks(void)
{
/* GetMessageA
GetMessageW
PeekMessageA
PeekMessageW */
FUN_10002902_install_hooks(4,0x10024ac8);
return;
}
It also hooks Chrome functions but since Chrome has its own TLS stack (BoringSSL), the payload handles this with a signature-based approach in FUN_10002c8c_install_chrome_hooks. The signature table (DAT_10024d38_chrome_sig_table) is available in FUN_10001000_return_chrome_sig_table and consists of 20 entries. Each entry contains a byte pattern for a specific Chrome version’s BoringSSL implementation.
YARA
Note: the rules are available here as well.
$ yara qbot_hooks.yar ../tmp/payload_32bit.dll
qbot_install_hooks ../tmp/payload_32bit.dll
qbot_install_chrome_hooks ../tmp/payload_32bit.dll
qbot_chrome_boringssl_signature_table ../tmp/payload_32bit.dll
qbot_hook_table_firefox_ssl ../tmp/payload_32bit.dll
qbot_hook_table_wininet ../tmp/payload_32bit.dll
qbot_hook_table_core_0 ../tmp/payload_32bit.dll
qbot_hook_table_core_1 ../tmp/payload_32bit.dll
qbot_hook_table_messageloop ../tmp/payload_32bit.dll
/*
YARA rules for QBot MitB hooks.
Target: 7fc3f5e06bbaad459af71d3c0d28c51b7802546984f886f14a1b12a779fff6f8
https://www.virustotal.com/gui/file/7fc3f5e06bbaad459af71d3c0d28c51b7802546984f886f14a1b12a779fff6f8/details
*/
rule qbot_install_hooks
{
meta:
description = "Iterates encrypted hook table, resolves APIs via GetModuleHandleA/GetProcAddress, installs inline hooks."
family = "QBot"
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_10002902_install_hooks"
strings:
/*
10002920 ff 15 48 CALL dword ptr [->KERNEL32.DLL::GetModuleHandleA] = 00023870
80 01 10
10002926 8b f8 MOV EDI,param_1
...
10002945 ff 15 44 CALL dword ptr [->KERNEL32.DLL::GetProcAddress] = 0002385e
80 01 10
*/
$resolve_pair = {
FF 15 [4]
8B F8
[1-16]
FF 15
}
/*
LAB_1000295e XREF[3]: 1000291d(j), 10002932(j),
10002941(j)
1000295e 83 c6 15 ADD ESI,0x15
10002961 4b DEC EBX
10002962 75 ad JNZ LAB_10002911
*/
$loop_stride = {
83 C6 15
4B
75 ??
}
/*
10002930 85 ff TEST EDI,EDI
10002932 74 2a JZ LAB_1000295e
10002934 8b 46 04 MOV param_1,dword ptr [ESI + 0x4]
*/
$null_gate_module = {
85 FF
74 ??
8B 46 04
}
/*
LAB_10002911 XREF[1]: 10002962(j)
10002911 8b 06 MOV param_1,dword ptr [ESI]
10002913 e8 ad 1e CALL FUN_100147c5_decrypt_string undefined * FUN_100147c5_decrypt
01 00
10002918 89 45 fc MOV dword ptr [EBP + local_8],param_1
1000291b 85 c0 TEST param_1,param_1
1000291d 74 3f JZ LAB_1000295e
*/
$struct_access = {
8B 06
E8 [4]
[2-8]
85 C0
74 ??
}
condition:
uint16(0) == 0x5A4D and
$resolve_pair and
$loop_stride and
2 of ($null_gate_module, $struct_access)
}
rule qbot_install_chrome_hooks
{
meta:
description = "Parses Chrome PE header, scans .text section for signature matches, installs inline hooks."
family = "QBot"
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_10002a7b_install_chrome_hooks"
strings:
$dot_text = ".text" ascii
/*
10002aca 81 3f 50 CMP dword ptr [EDI],0x4550
45 00 00
*/
$pe_sig_check = {
81 3F 50 45 00 00
}
/*
LAB_10002b02 XREF[1]: 10002c7e(j)
10002b02 68 3c 82 PUSH s_.text_1001823c = ".text"
01 10
10002b07 ff 75 f4 PUSH dword ptr [EBP + local_10]
10002b0a ff 15 38 CALL dword ptr [->KERNEL32.DLL::lstrcmpA] = 00023832
80 01 10
10002b10 85 c0 TEST EAX,EAX
*/
$strcmp_text = {
68 [4]
FF 75 ??
FF 15 [4]
85 C0
}
/*
10002bb9 81 c6 0c ADD ESI,0x30c
03 00 00
*/
$sig_stride = {
81 C6 0C 03 00 00
}
/*
10002b2f 81 c6 02 ADD ESI,0x102
01 00 00
*/
$sig_offset = {
81 C6 02 01 00 00
}
/*
10002ac5 8b 78 3c MOV EDI,dword ptr [EAX + 0x3c]
10002ac8 03 f8 ADD EDI,EAX
*/
$elfanew = {
8B 78 3C
03 F8
}
/*
10002c37 e8 f2 00 CALL FUN_10002d2e undefined4 FUN_10002d2e(void * p
00 00
10002c3c 85 c0 TEST EAX,EAX
10002c3e 78 44 JS LAB_10002c84
*/
$hook_and_check = {
E8 [4]
85 C0
78 ??
}
condition:
uint16(0) == 0x5A4D and
$dot_text and
$pe_sig_check and
$elfanew and
3 of ($strcmp_text, $sig_stride, $sig_offset, $hook_and_check)
}
rule qbot_chrome_boringssl_signature_table
{
meta:
description = "QBot Chrome BoringSSL inline hook signature table (20 version-specific byte patterns for locating SSL/TLS functions in chrome.dll .text section)."
family = "QBot"
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "DAT_10024d38_chrome_sig_table, FUN_10002c8c_install_chrome_hooks_w"
note = "The sig table is referenced in the hook install function, so the function can be located by following the sig table references."
strings:
/*
DAT_10024d38_chrome_sig_table XREF[1]: FUN_10001000_return_chrome_sig_t
10024d38 90 ?? 90h
10024d39 00 ?? 00h
10024d3a 55 ?? 55h U
10024d3b 89 ?? 89h
...
*/
$chrome_sig_entry0_a = {
90 00 // len = 0x90
55 89 e5 53 57 56 83 ec 08 a1 // prologue
aa aa aa aa // wildcard
8b 7d 08 31 e8 89 45 f0
8b 47 18
c7 80 9c 00 00 00 00 00 00 00
}
/*
10024e3a 60 ?? 60h `
10024e3b 00 ?? 00h
10024e3c 55 ?? 55h U
10024e3d 89 ?? 89h
...
*/
$chrome_sig_entry0_b = {
60 00 // len = 0x60
55 89 e5 57 56 8b 7d 08 // prologue
83 7f 50 00
74 20
68 31 04 00 00
68 aa aa aa aa // push <wildcard>
6a 42
6a 00
6a 10
}
/*
10024f3c 5e ?? 5Eh ^
10024f3d 00 ?? 00h
10024f3e 55 ?? 55h U
10024f3f 89 ?? 89h
...
*/
$chrome_sig_entry0_c = {
5e 00 // len = 0x5e
55 89 e5 53 56 8b 75 0c 31 db // prologue
85 f6
0f aa aa aa aa aa // conditional jump <wildcard>
}
/*
10025044 8d ?? 8Dh
10025045 00 ?? 00h
10025046 55 ?? 55h U
10025047 89 ?? 89h
...
*/
$chrome_sig_entry1_a = {
8d 00 // len = 0x8d
55 89 e5 53 57 56 50 // prologue
8b 45 0c 8b 75 08
c6 00 00
8b 46 18
83 b8 90 00 00 00 00
}
// Add further entries here if necessary.
condition:
2 of ($chrome_sig_entry*)
}
rule qbot_hook_table_firefox_ssl
{
meta:
description = "QBot Firefox SSL hook table (PR_Read, PR_Write, PR_Close, PR_OpenTCPSocket via nss3.dll and nspr4.dll)."
family = "QBot"
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_10002cf3_install_firefox_ssl_hooks"
note = "The hook table is referenced in the hook install function, so the function can be located by following the hook table references."
strings:
/*
nss3.dll
DAT_10024bf8_firefox_ssl_hooks XREF[2]: FUN_100026dc:10002891(*),
FUN_10002cf3_install_firefox_ssl
10024bf8 f5 1c 00 00 ddw 1CF5h
PR_OpenTCPSocket
10024bfc 2e 09 00 00 ddw 92Eh
10024c00 ef a6 00 10 ddw 1000A6EFh
10024c04 f4 8a 02 10 ddw 10028AF4h
10024c08 00 00 00 ??[5]
00 00
*/
$firefox_nss3 = {
f5 1c 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
f5 1c 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
f5 1c 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
f5 1c 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
/*
nspr4.dll
10024c4c e4 05 00 00 ddw 5E4h
PR_OpenTCPSocket
10024c50 2e 09 00 00 ddw 92Eh
10024c54 ef a6 00 10 ddw 1000A6EFh
10024c58 f4 8a 02 10 ddw 10028AF4h
10024c5c 00 00 00 ??[5]
00 00
*/
$firefox_nspr4 = {
e4 05 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
e4 05 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
e4 05 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
e4 05 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
condition:
$firefox_nss3 and $firefox_nspr4
}
rule qbot_hook_table_wininet
{
meta:
description = "QBot WinInet hook table (10 entries hooking HttpSendRequest, InternetReadFile, HttpOpenRequest, etc.)."
family = "QBot"
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_10002a02_install_wininet_hooks"
note = "The hook table is referenced in the hook install function, so the function can be located by following the hook table references."
strings:
/*
wininet.dll
DAT_10024b20_wininet_hooks XREF[3]: FUN_100026dc:10002812(*),
FUN_10002a02_install_wininet_hoo
FUN_10002d56_init_hook_slots:100
10024b20 c5 21 00 00 ddw 21C5h
HttpSendRequestA
10024b24 d7 24 00 00 ddw 24D7h
10024b28 af df 00 10 ddw 1000DFAFh
10024b2c 9c aa 02 10 ddw 1002AA9Ch
10024b30 00 00 00 ??[5]
00 00
*/
$wininet_table = {
c5 21 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
c5 21 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
c5 21 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
condition:
$wininet_table
}
rule qbot_hook_table_core_0
{
meta:
description = "QBot core_0 hook table (WSAConnect, connect, WSASend, send). "
family = "QBot"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_100029f5_install_core_hooks"
note = "The hook table is referenced in the hook install function, so the function can be located by following the hook table references."
strings:
/*
10024a05 6a 1d 00 00 ddw 1D6Ah
WSAConnect
10024a09 08 06 00 00 ddw 608h
10024a0d 1f f0 00 10 ddw 1000F01Fh
10024a11 48 b4 02 10 ddw 1002B448h
10024a15 00 00 00 ??[5]
00 00
*/
$ws2_table = {
6a 1d 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
6a 1d 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
6a 1d 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
6a 1d 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
condition:
$ws2_table
}
rule qbot_hook_table_core_1
{
meta:
description = "QBot core_1 hook table (DnsQuery_A, DnsQuery_W, Query_Main)."
family = "QBot"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_100029f5_install_core_hooks"
note = "The hook table is referenced in the hook install function, so the function can be located by following the hook table references."
strings:
/*
10024a59 23 09 00 00 ddw 923h
DnsQuery_A
10024a5d 1c 33 00 00 ddw 331Ch
10024a61 91 25 00 10 ddw 10002591h
10024a65 68 8a 02 10 ddw 10028A68h
10024a69 00 00 00 ??[5]
00 00
*/
$dns_table = {
23 09 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
23 09 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
23 09 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
condition:
$dns_table
}
rule qbot_hook_table_messageloop
{
meta:
description = "QBot message loop hook table (GetMessageA/W, PeekMessageA/W)."
family = "QBot"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_10002a1c_install_message_loop_hooks"
note = "The hook table is referenced in the hook install function, so the function can be located by following the hook table references."
strings:
/*
user32.dll
DAT_10024ac8_message_loop_hooks XREF[2]: FUN_10002a1c_install_message_loo
FUN_10002d56_init_hook_slots:100
10024ac8 8e 33 00 00 ddw 338Eh
GetMessageA
10024acc 0e 27 00 00 ddw 270Eh
10024ad0 57 8f 00 10 ddw 10008F57h
10024ad4 e4 8a 02 10 ddw 10028AE4h
10024ad8 00 00 00 ??[5]
00 00
*/
$msgloop_table = {
8e 33 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
8e 33 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
8e 33 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
8e 33 00 00 ?? ?? 00 00 ?? ?? 00 10 ?? ?? 02 10 00 00 00 00 00
}
condition:
$msgloop_table
}
capa
Note: the rules are available here as well.
$ capa -r rules/ ../tmp/payload_32bit.dll
┌───────────┬──────────────────────────────────────────────────────────────────┐
│ md5 │ 6e393f179b71d6ac848668e26c2cd236 │
│ sha1 │ 50cf91ee5ff2c723ce18745cff0c0f5648e3a5e5 │
│ sha256 │ 4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089 │
│ analysis │ static │
│ os │ windows │
│ format │ pe │
│ arch │ i386 │
│ path │ /home/gemesa/git-repos/group-ib/tmp/payload_32bit.dll │
└───────────┴──────────────────────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ATT&CK Tactic ┃ ATT&CK Technique ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ COLLECTION │ Man in the Browser [T1185] │
│ DEFENSE EVASION │ Obfuscated Files or Information [T1027] │
│ EXECUTION │ Native API [T1106] │
└───────────────────────────┴──────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ MBC Objective ┃ MBC Behavior ┃
┡━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ ANTI-STATIC ANALYSIS │ Obfuscated Files or Information::Encryption-Standard │
│ │ Algorithm [B0001.005] │
│ COLLECTION │ API Hooking [F0015] │
└──────────────────────┴───────────────────────────────────────────────────────┘
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Capability ┃ Namespace ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ install API hooks using encrypted function │ persistence/hooking │
│ references │ │
│ install hooks in Chrome browser via PE section │ persistence/hooking/browser │
│ scanning │ │
└────────────────────────────────────────────────┴─────────────────────────────┘
rule:
meta:
name: install API hooks using encrypted function references
namespace: persistence/hooking
authors:
- gemesa@protonmail.com
scopes:
static: function
dynamic: unsupported
att&ck:
- Execution::Native API [T1106]
- Defense Evasion::Obfuscated Files or Information [T1027]
- Collection::Man in the Browser [T1185]
mbc:
- Collection::API Hooking [F0015]
- Anti-Static Analysis::Obfuscated Files or Information::Encryption-Standard Algorithm [B0001.005]
description: >
Iterates over an array of encrypted entries, decrypts module and function
names at runtime, resolves them with GetModuleHandleA/GetProcAddress,
then installs inline hooks. Decrypted strings are wiped after use to
hinder forensic analysis.
features:
- and:
- api: kernel32.GetModuleHandleA
- api: kernel32.GetProcAddress
- basic block:
- and:
- api: kernel32.GetModuleHandleA
- api: kernel32.GetProcAddress
- characteristic: loop
- characteristic: calls to
rule:
meta:
name: install hooks in Chrome browser via PE section scanning
namespace: persistence/hooking/browser
authors:
- gemesa@protonmail.com
scopes:
static: function
dynamic: unsupported
att&ck:
- Collection::Man in the Browser [T1185]
mbc:
- Collection::API Hooking [F0015]
description: >
Parses the PE header of a loaded Chrome module, locates the .text
section, searches for known code signatures from an internal table,
and patches matching addresses to install inline hooks.
features:
- and:
- api: kernel32.lstrcmpA
- string: ".text"
- number: 0x4550
- number: 0x30C
- number: 0x102
Traffic interception
The traffic interception related subroutines can be collected following the hook functions detailed in the previous chapter. Revisiting the following structure, we know that the hook function is the 3. member in the struct (1-indexed).
struct hook_entry {
DWORD dll_blob_offset; // 0x00 encrypted string offset --> DLL name
DWORD api_blob_offset; // 0x04 encrypted string offset --> API name
PVOID hook_function; // 0x08 function to redirect to
PVOID *original_slot; // 0x0C where to save original pointer
BYTE pad[5]; // 0x10
}; // 21 bytes
Based on this, we can gather the functions from the hook descriptor blocks (see an example below). The traffic interception functions are a subset of these hook functions (+ possibly other functions called by these).
DAT_100249f0_core_hooks XREF[3]: FUN_100026dc:100027f9(*),
FUN_100029f5_install_core_hooks:
FUN_10002d56_init_hook_slots:100
100249f0 2e 07 00 00 ddw 72Eh
LdrLoadDll
100249f4 e4 36 00 00 ddw 36E4h
100249f8 dc 26 00 10 ddw 100026DCh
100249fc 78 8a 02 10 ddw 10028A78h
10024a00 00 00 00 ??[5]
00 00
10024a05 6a 1d 00 00 ddw 1D6Ah
WSAConnect
10024a09 08 06 00 00 ddw 608h
10024a0d 1f f0 00 10 ddw 1000F01Fh
10024a11 48 b4 02 10 ddw 1002B448h
10024a15 00 00 00 ??[5]
00 00
10024a1a 6a 1d 00 00 ddw 1D6Ah
connect
10024a1e 62 19 00 00 ddw 1962h
10024a22 9c f0 00 10 ddw 1000F09Ch
10024a26 54 d5 02 10 ddw 1002D554h
10024a2a 00 00 00 ??[5]
00 00
10024a2f 6a 1d 00 00 ddw 1D6Ah
WSASend
10024a33 e1 28 00 00 ddw 28E1h
10024a37 0d f1 00 10 ddw 1000F10Dh
10024a3b 44 b4 02 10 ddw 1002B444h
10024a3f 00 00 00 ??[5]
00 00
10024a44 6a 1d 00 00 ddw 1D6Ah
send
10024a48 78 36 00 00 ddw 3678h
10024a4c 90 f1 00 10 ddw 1000F190h
10024a50 4c b4 02 10 ddw 1002B44Ch
10024a54 00 00 00 ??[5]
00 00
10024a59 23 09 00 00 ddw 923h
DnsQuery_A
10024a5d 1c 33 00 00 ddw 331Ch
10024a61 91 25 00 10 ddw 10002591h
10024a65 68 8a 02 10 ddw 10028A68h
10024a69 00 00 00 ??[5]
00 00
10024a6e 23 09 00 00 ddw 923h
DnsQuery_W
10024a72 03 27 00 00 ddw 2703h
10024a76 e8 25 00 10 ddw 100025E8h
10024a7a 70 8a 02 10 ddw 10028A70h
10024a7e 00 00 00 ??[5]
00 00
10024a83 23 09 00 00 ddw 923h
Query_Main
10024a87 5f 04 00 00 ddw 45Fh
10024a8b 7d 26 00 10 ddw 1000267Dh
10024a8f 6c 8a 02 10 ddw 10028A6Ch
10024a93 00 00 00 ??[5]
00 00
10024a98 8e 33 00 00 ddw 338Eh
TranslateMessage
10024a9c 60 31 00 00 ddw 3160h
10024aa0 a1 8e 00 10 ddw 10008EA1h
10024aa4 b4 8a 02 10 ddw 10028AB4h
10024aa8 00 00 00 ??[5]
00 00
10024aad 8e 33 00 00 ddw 338Eh
GetClipboardData
10024ab1 fa 06 00 00 ddw 6FAh
10024ab5 d0 23 00 10 ddw 100023D0h
10024ab9 64 8a 02 10 ddw 10028A64h
Here are the hook functions from the previous chapter that appear in the hook descriptor blocks:
WinInet HTTP interception:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| HttpSendRequestA | 0x1000DFAF |
wininet_hooks |
| HttpSendRequestW | 0x1000E009 |
wininet_hooks |
| InternetReadFile | 0x1000E3B9 |
wininet_hooks |
| InternetReadFileExA | 0x1000E4A8 |
wininet_hooks |
| InternetCloseHandle | 0x1000D969 |
wininet_hooks |
| InternetQueryDataAvailable | 0x1000D8CB |
wininet_hooks |
| HttpOpenRequestA | 0x1000E745 |
wininet_hooks |
| HttpOpenRequestW | 0x1000E7AA |
wininet_hooks |
| HttpSendRequestExW | 0x1000E974 |
wininet_hooks |
| InternetWriteFile | 0x1000E9BD |
wininet_hooks |
Winsock connection interception:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| WSAConnect | 0x1000F01F |
core_hooks |
| connect | 0x1000F09C |
core_hooks |
| WSASend | 0x1000F10D |
core_hooks |
| send | 0x1000F190 |
core_hooks |
DNS redirection:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| DnsQuery_A | 0x10002591 |
core_hooks |
| DnsQuery_W | 0x100025E8 |
core_hooks |
| Query_Main | 0x1000267D |
core_hooks |
Firefox NSS/NSPR interception:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| PR_OpenTCPSocket | 0x1000A6EF |
firefox_ssl_hooks |
| PR_Read | 0x1000AD91 |
firefox_ssl_hooks |
| PR_Write | 0x1000AB38 |
firefox_ssl_hooks |
| PR_Close | 0x1000A70E |
firefox_ssl_hooks |
Keystroke and clipboard capture:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| TranslateMessage | 0x10008EA1 |
core_hooks |
| GetClipboardData | 0x100023D0 |
core_hooks |
GetMessage/PeekMessage hooks:
| Hooked API | Hook function | Descriptor table |
|---|---|---|
| GetMessageA | 0x10008F57 |
message_loop_hooks |
| GetMessageW | 0x10008F95 |
message_loop_hooks |
| PeekMessageA | 0x10008FD3 |
message_loop_hooks |
| PeekMessageW | 0x10009014 |
message_loop_hooks |
Chrome BoringSSL hooks are not in descriptors. They are installed via PE .text section signature scanning.
String obfuscation
FUN_100147c5_decrypt_string
Decryption function in the payload. Used for all DLL names, API names, configuration data. Decrypted strings freed via FUN_10014863 after use.
It is a rolling XOR decryptor with a 64-byte key. DAT_100244c0 is the 64-byte static XOR key and DAT_1002d55c is the encrypted blob (up to 0x36F4 bytes). Every encrypted string in the payload is just identified by its integer offset into the blob. The callers pass constants like 0x1D29, 0x338E, 0x1EC, etc.
undefined * FUN_100147c5_decrypt_string(int param_1)
{
byte *pbVar1;
byte *pbVar2;
undefined *puVar3;
uint uVar4;
int iVar5;
uint local_8;
local_8 = 0;
uVar4 = param_1;
if ((DAT_1002d55c != 0) || (DAT_1002d55c = FUN_10013e0d(), DAT_1002d55c != 0)) {
for (; uVar4 < 0x36f4; uVar4 = uVar4 + 1) {
if ((&DAT_100244c0)[uVar4 & 0x3f] == *(char *)(DAT_1002d55c + uVar4)) {
local_8 = uVar4 - param_1;
break;
}
}
puVar3 = (undefined *)FUN_10013db6_heapalloc(local_8 + 1);
if (puVar3 != (undefined *)0x0) {
uVar4 = 0;
if (local_8 == 0) {
return puVar3;
}
iVar5 = DAT_1002d55c + param_1;
do {
pbVar1 = puVar3 + uVar4;
pbVar2 = (byte *)(iVar5 + uVar4);
uVar4 = uVar4 + 1;
*pbVar1 = (&DAT_100244c0)[(uint)(pbVar1 + (param_1 - (int)puVar3)) & 0x3f] ^ *pbVar2;
} while (uVar4 < local_8);
return puVar3;
}
}
return &DAT_1002d5c4;
}
FUN_10014863_wipe_string
Decrypted strings are zeroed and freed after use.
YARA
Note: the rules are available here as well.
$ yara qbot_string_decryptor.yar ../tmp/payload_32bit.dll
qbot_string_decryptor ../tmp/payload_32bit.dll
/*
YARA rules for QBot string decryptor.
Target: 4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089
https://www.virustotal.com/gui/file/4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089/details
*/
rule qbot_string_decryptor
{
meta:
description = "QBot rolling XOR string decryptor. Resolves DLL names, API names and config strings."
author = "gemesa@protonmail.com"
hash = "4229f9f1c316c207783f484ba2bc518074180f87ddb01e47328a4f6f23f97089"
reference = "FUN_100147c5_decrypt_string"
strings:
// Length-finding loop. Scan blob for null terminator.
/*
LAB_10014805 XREF[1]: 10014818(j)
10014805 8b d1 MOV EDX,ECX
10014807 83 e2 3f AND EDX,0x3f
1001480a 8a 92 c0 MOV DL,byte ptr [EDX + DAT_100244c0] = 5Ah Z
44 02 10
10014810 32 14 08 XOR DL,byte ptr [param_1 + ECX*0x1]
10014813 74 07 JZ LAB_1001481c
10014815 41 INC ECX
10014816 3b ce CMP ECX,ESI
10014818 72 eb JC LAB_10014805
*/
$null_scan = {
8B D1
83 E2 3F
8A 92 ?? ?? ?? ??
32 14 08
74 07
41
3B CE
72 EB
}
// Decrypt loop. XOR each byte with rolling key.
/*
LAB_10014844 XREF[1]: 1001485c(j)
10014844 8d 0c 10 LEA ECX,[param_1 + EDX*0x1]
10014847 8d 1c 0f LEA EBX,[EDI + ECX*0x1]
1001484a 83 e3 3f AND EBX,0x3f
1001484d 8a 9b c0 MOV BL,byte ptr [EBX + DAT_100244c0] = 5Ah Z
44 02 10
10014853 32 1c 16 XOR BL,byte ptr [ESI + EDX*0x1]
10014856 42 INC EDX
10014857 88 19 MOV byte ptr [ECX],BL
*/
$decrypt_loop = {
8D 0C 10
8D 1C 0F
83 E3 3F
8A 9B ?? ?? ?? ??
32 1C 16
42
88 19
}
// Blob size constant 0x36F4 (max encrypted blob length).
/*
LAB_100147fa XREF[2]: 100147d8(j), 100147f1(j)
100147fa be f4 36 MOV ESI,0x36f4
00 00
*/
$blob_size = { BE F4 36 00 00 }
condition:
($null_scan and $decrypt_loop) or
($null_scan and $blob_size)
}