Windows.Carving.Qbot

This artifact yara-scans memory or unpacked DLL samples for Qbot (or Qakbot) detections, decrypts and returns the botnet and C2 configurations.

You may select specific file paths or processes to be yara-scanned, or allow it to yara-scan all memory space.

Currently this artifact parses encrypted configurations from the Qbot variant introducted in April 2021. It will not parse the configurations from earlier variants.

NOTE: This content simply carves the configuration and does not unpack files on disk. That means pointing this artifact as a packed or obfuscated file will not obtain the expected results.

name: Windows.Carving.Qbot
author: "Eduardo Mattos - @eduardfir"
description: |
    This artifact yara-scans memory or unpacked DLL samples for Qbot (or Qakbot)
    detections, decrypts and returns the botnet and C2 configurations.
    
    You may select specific file paths or processes to be yara-scanned, or allow 
    it to yara-scan all memory space.
    
    Currently this artifact parses encrypted configurations from the Qbot variant
    introducted in April 2021. It will not parse the configurations from earlier 
    variants.
    
    NOTE:
    This content simply carves the configuration and does not unpack files on disk. 
    That means pointing this artifact as a packed or obfuscated file will not obtain the expected results.

type: CLIENT

reference:
  - https://blog.vincss.net/2021/03/re021-qakbot-dangerous-malware-has-been-around-for-more-than-a-decade.html
  - https://github.com/kevoreilly/CAPEv2/blob/master/modules/processing/parsers/mwcp/QakBot.py

parameters:
  - name: TargetFileGlob
    default:
  - name: PidRegex
    default: .
  - name: ProcessRegex
    default: .
  - name: DetectionYara
    default: |
        rule QakBot {
            meta:
                author = "kevoreilly"
                description = "QakBot Payload"
                cape_type = "QakBot Payload"
            strings:
                $crypto1 = {8B 5D 08 0F B6 C2 8A 16 0F B6 1C 18 88 55 13 0F B6 D2 03 CB 03 CA 81 E1 FF 00 00 80 79 08 49 81 C9 00 FF FF FF 41}
                $crypto2 = {5? 33 F? [0-9] 89 7? 24 ?? 89 7? 24 ?? 8? [1-3] 24 [1-4] C7 44 24 ?0 01 23 45 67 C7 44 24 ?4 89 AB CD EF C7 44 24 ?8 FE DC BA 98 C7 44 24 ?C 76 54 32 10 C7 44 24 ?0 F0 E1 D2 C3}
                $anti_sandbox1 = {8D 4? FC [0-1] E8 [4-7] E8 [4] 85 C0 7E (04|07) [4-7] 33 (C0|D2) 74 02 EB FA}
                $anti_sandbox2 = {8D 45 ?? 50 E8 [2] 00 00 59 68 [4] FF 15 [4] 89 45 ?? 83 7D ?? 0F 76 0C}
                $decrypt_config1 = {FF 37 83 C3 EC 53 8B 5D 0C 8D 43 14 50 6A 14 53 E8 ?? ?? ?? ?? 83 C4 14 85 C0 ?? 26 ?? ?? 86 20 02 00 00 66 85 C0 ?? ?? FF 37 FF 75 10 53}
                $decrypt_config2 = {8B 45 08 8B 88 24 04 00 00 51 8B 55 10 83 EA 14 52 8B 45 0C 83 C0 14 50 6A 14 8B 4D 0C 51 E8 6C 08 00 00}
                $decrypt_config3 = {6A 13 8B CE 8B C3 5A 8A 18 3A 19 75 05 40 41 4A 75 F5 0F B6 00 0F B6 09 2B C1 74 05 83 C8 FF EB 0E}
                $call_decrypt = {83 7D ?? 00 56 74 0B FF 75 10 8B F3 E8 [4] 59 8B 45 0C 83 F8 28 72 19 8B 55 08 8B 37 8D 48 EC 6A 14 8D 42 14 52 E8}
            condition:
                any of ($*)
        }
sources:
  - precondition:
      SELECT OS From info() where OS = 'windows'

    query: |
        -- find target files
        LET targetFiles = SELECT FullPath FROM glob(globs=TargetFileGlob) 
                
        -- find velociraptor process
        LET me <= SELECT Pid 
                  FROM pslist(pid=getpid())
        
        -- find all processes and add filters
        LET processes <= SELECT Name AS ProcessName, CommandLine, Pid
                        FROM pslist()
                        WHERE Name =~ ProcessRegex 
                            AND format(format="%d", args=Pid) =~ PidRegex
                            AND NOT Pid in me.Pid

        -- scan processes in scope with our DetectionYara
        LET processDetections <= SELECT * FROM foreach(row=processes,
                                query={
                                    SELECT * FROM if(condition=TargetFileGlob="",
                                        then={
                                            SELECT *, ProcessName, CommandLine, Pid, Rule AS YaraRule
                                            FROM proc_yara(pid=Pid, rules=DetectionYara)
                                        })
                                })

        -- scan files in scope with our DetectionYara
        LET fileDetections = SELECT * FROM foreach(row=targetFiles,
                                query={
                                    SELECT * FROM if(condition=TargetFileGlob,
                                        then={
                                            SELECT * FROM switch(
                                                a={ -- yara detection
                                                    SELECT FullPath, Rule AS YaraRule
                                                    FROM yara(files=FullPath, rules=DetectionYara)
                                                },
                                                b={ -- yara miss
                                                    SELECT FullPath, Null AS YaraRule 
                                                    FROM targetFiles
                                                })
                                        },
                                        else={ -- no yara detection run
                                            SELECT FullPath, 'N/A' AS YaraRule 
                                            FROM targetFiles
                                        })
                             })

        -- return the VAD region size from yara detections for later use                         
        LET regionDetections = SELECT * 
                                FROM foreach(row=processDetections,
                                    query={
                                        SELECT YaraRule, Pid, ProcessName, CommandLine, Address as QBOTBaseOffset, Size AS VADSize
                                        FROM vad(pid=Pid)
                                        WHERE Protection =~ "xrw"
                                })
        
        -- get data from the whole PE
        LET peData <= SELECT * FROM if(condition=TargetFileGlob,
                                        then={ -- query files
                                            SELECT YaraRule, FullPath,
                                                read_file(filename=FullPath) AS PEData
                                            FROM fileDetections
                                        },
                                        else={ -- query processes
                                            SELECT YaraRule, Pid, ProcessName, CommandLine,
                                                read_file(filename=str(str=Pid), accessor='process', offset=QBOTBaseOffset, length=VADSize) AS PEData
                                            FROM regionDetections
                                        })
                                        
        -- return .rsrc section info
        LET sectionInfo <= SELECT *,
                                parse_pe(file=PEData, accessor="data").Sections[3] AS Sectionrsrc
                           FROM peData
                           
        -- read the data from .rsrc sections
        LET sectionData <= SELECT *,
                                Sectionrsrc.RVA AS QBOTrsrcRVA,
                                read_file(filename=PEData, accessor="data", offset=Sectionrsrc.RVA, length=Sectionrsrc.Size) AS QBOTrsrcData
                           FROM sectionInfo

        -- parse the .rsrc sections to extract the rcdata resources containing the encrypted info
        LET parsedRdata = SELECT *,
                            parse_binary(filename=QBOTrsrcData, accessor="data", profile='''[
                                ["QbotRCData", 0, [
                                        ["__prefix", 0, "String", {"length": 104, "term":""}],
                                        ["Address3719", 104, "uint32", {"length": 4, "term":""}],
                                        ["Size3719", 108, "uint32", {"length": 4, "term":""}],
                                        ["__prefix2", 112, "String", {"length": 8, "term":""}],
                                        ["Address5812", 120, "uint32", {"length": 4, "term":""}],
                                        ["Size5812", 124, "uint32", {"length": 4, "term":""}], 
                                        ["Data3719", "x=> x.Address3719 - QBOTrsrcRVA", "String", {"length": "x=> x.Size3719", "term":""}],
                                        ["Data5812", "x=> x.Address5812 - QBOTrsrcRVA", "String", {"length": "x=> x.Size5812", "term":""}]
                                    ]
                                ]
                            ]''', struct="QbotRCData") AS RCDataSections
                          FROM sectionData  
                          
        -- rc4 decrypt the configurations        
        LET decryptedInfo <= SELECT *,
                                format(format="%x", args=crypto_rc4(key=unhex(string="43a5b0bdb1b829bb796e3dbad916f7185f6a6b1a"), string=RCDataSections.Data5812)) AS DecryptedBotInfo,
                                format(format="%x", args=crypto_rc4(key=unhex(string="43a5b0bdb1b829bb796e3dbad916f7185f6a6b1a"), string=RCDataSections.Data3719)) AS DecryptedC2Info
                             FROM parsedRdata
                             
        -- format the decrypted configurations
        SELECT * FROM if(condition=TargetFileGlob,
            then= {
                SELECT YaraRule, FullPath,
                    { SELECT unhex(string=BotId) AS Botnet, unhex(string=CampaignEpoch) AS Campaign FROM parse_records_with_regex(file=DecryptedBotInfo, accessor="data", regex='(?P<BotId>3130.+)0D0A(?P<CampaignEpoch>333D.+)0D0A')} AS BotInfo,
                    { SELECT ip(netaddr4_be=int(int="0x" + IPAdd)) AS IPAdress, int(int="0x" + Port) AS PortNum FROM parse_records_with_regex(file=DecryptedC2Info, accessor="data", regex='01(?P<IPAdd>........)(?P<Port>....)')} AS C2Info
                FROM decryptedInfo
            },
            else= {
                SELECT YaraRule, Pid, ProcessName, CommandLine,
                    { SELECT unhex(string=BotId) AS Botnet, unhex(string=CampaignEpoch) AS Campaign FROM parse_records_with_regex(file=DecryptedBotInfo, accessor="data", regex='(?P<BotId>3130.+)0D0A(?P<CampaignEpoch>333D.+)0D0A')} AS BotInfo,
                    { SELECT ip(netaddr4_be=int(int="0x" + IPAdd)) AS IPAdress, int(int="0x" + Port) AS PortNum FROM parse_records_with_regex(file=DecryptedC2Info, accessor="data", regex='01(?P<IPAdd>........)(?P<Port>....)')} AS C2Info
                FROM decryptedInfo
        })