gemesa@home:~$

Reversing the QBot interceptor

Table of contents

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)
}