飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 914|回复: 8

[病毒分析] [翻译]使用 Malcat 编写 Qakbot 5.0 配置提取器

[复制链接]
  • TA的每日心情
    开心
    2019-3-15 11:00
  • 签到天数: 262 天

    [LV.8]以坛为家I

    发表于 2024-2-17 16:50:55 | 显示全部楼层 |阅读模式
    本帖最后由 梦幻的彼岸 于 2024-2-18 10:56 编辑

    翻译:梦幻的彼岸
    原文地址:https://malcat.fr/blog/writing-a ... ractor-with-malcat/
    样本:
    73472cfc52f2732b933e385ef80b4541191c45c995ce5c42844484c33c9867a3.msi (Bazaar, VT)
    传染链:
    MSI installer -> Backdoored DLL -> PE loader -> Qakbot
    分析所使用的工具
    难度
    中级水平
    简介
    在过去的 15 年里,人们对 Qakbot 进行了大量研究,它在恶意软件领域扮演着重要角色。在2023 年 8 月被成功清除后,它最近又受到了关注,因为在 2023 年 12 月左右又发现了一个新的变种。
    但在起死回生后,该 RAT 也切换到了新版本:5.0。遗憾的是,现有的 Qakbot 配置提取器停止工作了(据我所知),这表明恶意软件代码发生了非同小可的变化。这相对来说比较恼人:配置提取器对于僵尸网络跟踪和事件响应非常有用。但与其抱怨,不如让我们启动 Malcat,看看能否自己编写一个配置提取器!

    第一阶段: MSI installer
    感谢 Malware Bazaar,我们很容易就找到了 Qakbot 的最新样本。这个样本碰巧是一个 MSI 安装程序。MSI 安装程序经常被恶意软件作者滥用来打包他们的恶意程序。因此,让我们在 Malcat 中加载该文件,看看有什么发现。在摘要视图中,我们首先看到的是一个 "Acrobat "安装程序。
    图 1:安装程序

    当然在分析 MSI 安装程序时,首先要看的是CustomAction表,它在一定程度上驱动着安装过程。幸运的是,Malcat full & pro 可以在反编译视图中显示所有 MSI 表格的内容。只需按F4并向下滚动到CustomAction表(表按字母顺序排序)。LaunchFile项尤其有趣,其中确实有一个运行名为viewer.exe的程序,命令行非常可疑:
    1
    2
    3
    4
    5
    6
    7
    {
        "Action": "LaunchFile",
        "Type": 2,
        "Source": "viewer.exe",
        "Target": "/HideWindow rundll32 [APPDIR]\\MicrosoftOffice15\\ClientX64\\[ProductName].dll,CfGetPlatformInfo",
        "ExtendedType": null
    }

    我们首先来看看这个viewer.exe。根据我的经验,MSI 安装程序中有两种类型的文件:

    • 安装过程中需要的文件:图片、插件、工具等。这些文件存储在二进制数据库中。Malcat 会在虚拟文件系统选项卡中将其列为Binary.<文件名>。
    • 永久安装到磁盘的文件:这些文件存储在 CAB 存档中,如本安装程序中的disk1.cab文件。

    我们的文件viewer.exe似乎属于第一种类型,我们只需在 "虚拟文件系统"选项卡中双击Binary.viewer.exe即可打开它。快速威胁情报哈希值查询(Ctrl+I或摘要视图中的 "检查情报"按钮)提示我们,该文件可能是一个简单的第三方启动器:
    图 2:viewer.exe

    下一个疑点是目标属性中引用的 DLL。我们没有 DLL 的名称,但幸运的是,在disk1.cab 文件中有一个名为dll_1的 DLL 文件。要打开它,只需双击disk1.cab,然后双击dll_1。我们现在正面临感染的第二阶段。

    第二阶段: Antimalw.dll
    文件dll_1是一个 922KB 的 PE DLL,sha256 为a59707803f3d94ed9cb429929c832e9b74ce56071a1c2086949b389539788d8a(VirusshareVT),名为antimalw.dll(版本信息)或antimalware_provider64.dll(导出名称)。该文件立即引起我们的怀疑:

    • 它声称是 Bitdefender 的 AMSI 提供商,即 Bitdefender 杀毒软件的脚本扫描组件。antimalw.dll包含 Bitdefender 原始 DLL 的部分内容,但显然不是。
    • 它的数据目录显示它是用证书签名的,但证书的位置已被.rsrc部分覆盖。
    • 它有一个名为ЬГнЦИРИ的大型高熵资源
    • 其入口函数为空
    • 它只有一个导出函数CfGetPlatformInfo,但似乎被混淆了

    看起来,恶意软件的作者获取了 Bitdefender 的antimalware_provider64.dll,并用恶意代码对其进行了回溯/覆盖。
    图 3:一个可疑的 DLL

    既然我们已经确认文件是恶意的,那就言归正传吧。面对打包的恶意软件,我采取的第一步是一个我称之为 Where is the poop, Robin的过程。你看,没有什么魔法:恶意软件必须将其有效载荷存储在某个地方(当然,除非它们是下载工具)。因此,与其盲目深入代码或将二进制文件提交到缓慢的沙盒中,最好的办法往往是先找到加密的有效载荷。找到隐藏的有效载荷可以让你立即解密,或者在最坏的情况下,为你提供开始逆向工程的有用指针。
    大型高熵资源 ЬГнЦИРИ似乎是我们开始搜索的好对象。在十六进制视图中滚动浏览其字节,我们可以在文件末尾附近看到一个重复模式。这通常暗示着某种旋转密钥加密机制。由于文件末尾为零的可能性很大,而且我们知道恶意软件作者喜欢使用 XOR 加密,因此我们只需尝试使用密钥"HU03!Mm!?qYHCTnaEX<\0"(注意末尾的空字节)解除 XOR 加密即可。顺便提一下,在导出函数CfGetPlatformInfo 中,该字符串作为堆栈字符串出现,这一点令人鼓舞:
    图 4:对资源进行解密

    事实上,我们已经成功解密了资源。XOR 加密万岁!

    第三阶段: PE loader
    我们现在面对的是一个看起来像 180KB x64 shellcode 的文件(sha2568c7401218e6da9533d4e97849ad6c528b231c1b9cdcf43d1788757c3862dc2d4)。现在有两种方法。最简单的方法就是模拟 shellcode,具体步骤如下:

    • 将架构强制为 x64
    • 选择 shellcode 的第一个字节并在此定义新函数
    • 使用 Malcat 的模拟器脚本试试运气,例如运行脚本emulation/Speakeasy (shellcode)

    另一方面,Malcat 从 180 KB 的外壳代码中雕刻出了一个 170KB 的纯文本 PE 文件。因此,让我们采取简单的方法,只需双击雕刻好的 PE 文件,即可进入下一阶段:

    图 5:shellcode 及其嵌入的 PE 文件
    第四阶段: the Qakbot DLL
    下一阶段是名为cldapi.dll 的 170KB PE dll,其 sha256 为af6a9b7e7aefeb903c76417ed2b8399b73657440ad5f8b48a25cfe5e97ff868f(Virusshare,VT)。我们面对的是感染链的最后阶段:一个编译于 2024-01-29 的Qakbot恶意软件,因此很可能是新的 5.0 版本之一!


    我们如何确定这是最终的恶意软件?通常,我倾向于使用 Malpedia 的 Yara 规则进行确认,但遗憾的是,他们的Yara 规则似乎并不涵盖新版 Qakbot。但如果我们将cldapi.dll样本与 2023 年 3 月的 Qakbot 版本(例如这个版本)进行比较,就会发现即使某些字符串被更改或加密,但大多数字符串仍然存在:

    图 6:字符串与 2023 年 3 月 Qakbot 样本的比较

    除了 Qakbot 属性外,我们还可以看到 DLL 稍微被混淆了:

    • API 地址在运行时通过哈希值动态解析(哈希值已加密)
    • 大多数字符串都已加密
    • 这里或那里有一些垃圾代码岛

    虽然在我们的案例中,API 混淆并不是什么大问题,但如果我们要编写配置提取器,字符串加密可能会有问题。这将是我们的第一项任务:定位并解密 Qakbot 的字符串。

    解密字符串查找第一个加密字符串数组
    虽然 Qakbot 并不是一个巨大的恶意软件,但要逆转超过 120KB 的代码总是很繁琐。由于我们要找的是相当精确的东西,即加密数据块,因此我们将再次把重点放在数据上,而不是深入代码。更确切地说,我们将尝试找到任意数据部分的所有数据缓冲区,它们是:

    • 相对较大,例如超过 64 字节
    • 具有高熵
    • 有传入代码引用

    为方便搜索,请确保您已启用传入参考高亮显示


    Malcat 偶然发现了一些已知的常量数组,如嵌入式 Zlib 库使用的预计算表,这为我们节省了一些时间,因为我们对这些缓冲区并不感兴趣。从地址0x180028150 开始,我们可以看到一些候选缓冲区。前三个缓冲区看起来很有希望(为了清楚起见,我们给它们起了名字并用颜色标出):

    图 7:加密缓冲区Candidates

    这三个缓冲区都被同一个函数sub_180002ab8引用,我们将其重命名为decrypt_string_1。这个函数看起来就像典型的字符串解密函数:如下所示,它有许多输入引用,每次调用都有一个不同的硬编码参数。这个参数很有可能是一个字符串索引:
    1
    2
    3
    4
    5
    void decrypt_string_1(xunknown4 string_index)
    {
        decrypt_aes_plus_xor(ENCRYPTED_STRINGS_1, 0x5ad, AES_ENCRYPTED_XOR_KEY, 0xd0, AES_PASSWORD, 0x63, string_index);
        return;
    }

    图 8:第一个字符串解密函数的上下文内容

    函数decrypt_string_1非常简单:它调用一个名为decrypt_aes_plus_xor的辅助函数,并将加密后的三个缓冲区作为参数。其反编译代码(F4)如下:

    每个变量的值如下:

    [td]
    名称地址字节大小描述
    decrypt_strings_1
    0x180002ab8
    0x3f
    Decryption function for the first encrypted strings array
    STRINGS_1
    0x1800282a0
    0x5ad
    First encrypted strings array
    AES_ENCRYPTED_XOR_KEY
    0x1800281c0
    0xd0
    The XOR key used to decrypt the string array, but AES256-CBC encrypted
    AES_PASSWORD
    0x180028150
    0x63
    The password used to derive the AES256 key for AES_ENCRYPTED_XOR_KEY
    decrypt_aes_plus_xor
    0x18000dc2c
    0x1de
    The function that decrypts the string array and selects the string
    aes_encrypt_decrypt_iv_prefix
    0x180011504
    0x3f7
    A function called by decrypt_aes_plus_xor that decrypts or encrypts an arbitrary data buffer using AES256 in CBC mode
    解密字符串
    要获得decrypt_aes_plus_xor函数的功能,需要进行一些逆向工程。由于代码相对较短,你可以静态地完成它,不过会遇到一些问题,因为 API 是动态解析的。使用调试器跟踪函数是更明智的选择。总之,最后的工作相对简单,字符串解密例程看起来就像这样:

    图 9:如何解密字符串

    好消息是,我们已经在 Malcat 中获得了所需的所有材料!事实上,Malcat 已经有了一个名为CryptDeriveKey 的数据转换。实际上,我们根本不需要它:在这种特定配置下,CryptDeriveKey只是计算密码的SHA256哈希值,并直接将其用作密钥。至于CryptDecrypt:它在 CBC 模式下执行简单的 AES 256 解密,我们也有一个用于此的转换。


    注意:Advapi32.dll 加密函数默认添加/删除填充,因此请务必在转换窗口中勾选 "unpad"。


    因此,只需使用 Malcat 转换,我们就能在几秒钟内手动解密字符串,如下图所示:
    图 10:利用 Malcat 变换解密 th 字符串

    结果如下:

    SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileListProgramDatanetstat -nao%s "$%s = \"%s\"; & $%s"net localgrouppowershell.exeroute print"%s\system32\schtasks.exe" /Create /ST %02u:%02u /RU "NT AUTHORITY\SYSTEM" /SC ONCE /tr "%s" /Z /ET %02u:%02u /tn %sComponent_08ERROR: GetModuleFileNameW() failed with error: ERROR_INSUFFICIENT_BUFFERnet viewipconfig /allSelf checkT2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI94Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBVStart screenshot%s.%uadrclient.dllnet shareqwinsta\System32\WindowsPowerShell\v1.0\powershell.exeat.exe %u:%u "%s" /ISelf test FAILED!!!Component_07whoami /all /c ping.exe -n 6 127.0.0.1 &  type "%s\System32\calc.exe" > "%s"error res='%s' err=%d len=%unltest /domain_trusts /all_trusts.lnkcmdschtasks.exe /Create /RU "NT AUTHORITY\SYSTEM" /SC ONSTART /TN %u /TR "%s" /NP /F%s \"$%s = \\\"%s\\\\; & $%s\"ERROR: GetModuleFileNameW() failed with error: %uschtasks.exe /Delete /F /TN %uarp -aSelf check ok!cmd.exe /c set%s %04x.%u %04x.%u res: %s seh_test: %u consts_test: %d vmdetected: %d createprocess: %dMicrosoftpowershell.exe -encodedCommand %SSELF_TEST_1microsoft.com,google.com,kernel.org,www.wikipedia.org,oracle.com,verisign.com,broadcom.com,yahoo.com,xfinity.com,irs.gov,linkedin.comc:\ProgramDatanslookup -querytype=ALL -timeout=12 _ldap._tcp.dc._msdcs.%s%u;%u;%u;powershell.exe -encodedCommand runas/teorema505Self test OK.ProfileImagePathp%08x
    遗憾的是,除了 CNC http 端点 (/teorema505),这里既没有 CNC 地址列表,也没有任何有价值的配置数据。因此,我们必须深入挖掘。
    对第二个字符串数组进行解密
    这个二进制文件中还有第二个加密字符串数组。这个数组的重要性较低,解密方法与第一个数组完全相同。唯一的区别是使用的 XOR 密钥和 AES 密码不同。如果你感兴趣,下面是 Qakbot 示例中与第二个数组相关的变量位置:

    名称地址字节大小描述
    decrypt_strings_2
    0x18000de90
    0x3f
    Decryption function for the second encrypted strings array
    STRINGS_2
    0x1800297a0
    0x1836
    Second encrypted strings array
    AES_ENCRYPTED_XOR_KEY_2
    0x18002afe0
    0xa0
    The XOR key used to decrypt the string array, but AES256-CBC encrypted
    AES_PASSWORD_2
    0x180029700
    0x9f
    The password used to derive the AES256 key for AES_ENCRYPTED_XOR_KEY_2

    如果使用与第一个数组相同的程序,就可以得到下面的字符串列表:请参见 pastebin

    配置
    确定配置位置

    解密字符串固然是件好事,但我们的目标是获取 Qakbot 的配置,或者至少是它的命令与控制 (CNC) 服务器列表。我们将遵循流程,从数据分析入手。在上一章介绍的解密字符串列表中,有两个字符串看起来有点不寻常:

    • 字符串数组中偏移量0x182处的第 14 个字符串: T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9
    • 字符串数组中偏移量0x1a9处的第 15 个字符串: 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV

    我们在上一章中看到,字符串解密函数decrypt_strings_1的第一个参数是要解密的字符串的索引,即它相对于加密字符串数组起始位置的位置。因此,如果我们想知道这两个字符串是如何使用的,只需在代码中查找它们的偏移量即可。让我们关注第一个字符串:

    图 11:查找对字符串 0x182 的引用

    我们很快就得到了两个候选函数:一个是位于偏移量0x18000622c的函数,我们将其称为decrypt_CNC,另一个是位于偏移量0x18000345c的函数,我们将其称为decrypt_params。第二个好消息是,除了0x182字符串之外,这两个函数都引用了一个高熵缓冲区(分别名为CNC_LIST和PARAMS)。这些函数和变量的地址如下:

    名称地址字节大小描述
    decrypt_CNC
    0x18000622c
    0x2cc
    Decryption function for Qakbot's CNC
    CNC_LIST
    0x180028852
    0x51
    Encrypted CNC list
    decrypt_params
    0x18000345c
    0x76
    Decryption function for Qakbot's campaign information
    PARAMS
    0x180029022
    0x51
    Encrypted campaign informations
    aes_decrypt_and_check_sha256
    0x180015d14
    0x105
    Function to decrypt both encrypted blob

    最后还有一个好消息:这两个函数最终都会调用我们的好帮手aes_encrypt_decrypt_iv_prefix。我们在逆转字符串解密过程时已经发现了这个函数:它能解密以 16 字节 IV 为前缀的 AES256-CBC 加密缓冲区。
    图 12:计算机解密功能候选方案
    解密 CNC 列表
    如果我们深入研究一下函数,特别是aes_decrypt_and_check_sha256 中的内容,就会发现加密后的 blob CNC_LIST和PARAMS有一个特殊的结构:

    • 它们的前缀是大小(16 位 int)
    • 然后是一个字节的 Blob 标识符
    • 然后,我们就得到了已知的 AES 加密 blob:
      • 16 个字节的初始化向量 (IV)
      • 实际的 AES256-CBC 加密内容

    blob 格式如下图所示:

    图 13:Cnc 列表加密 blob

    要解密 blob,我们将使用与字符串相同的程序:

    • 计算密码"T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9"的 SHA256 值(7085d1138cbac863a9b4f1bf85a4d413804ef3a3ec52729fa15747a6ee320325)
    • 选择 0x40 字节的 AES 加密数据
    • 在CBC 模式下使用 Malcat 的转换AES 解密,将 IV 设置为加密数据前的 16 个字节,将密钥设置为 sha256 哈希值
    • 不要忘记检查unpad

    解密CNC_LIST blob 后,我们看到的是一个相对简单的二进制结构。在函数decrypt_CNC中稍加反转,我们就能迅速了解解密所需的一切信息。解密后的 blob 以 sha256 校验和开头,然后是一个(IP、端口)对列表。详情如下:
    图 14:CNC 列表已解密

    就是这样!我们得到了 3 个 CnC 地址:

    • 31.210.173.10:443 (VT)
    • 185.156.172.62:443 (VT)
    • 185.113.8.123:443 (VT)

    现在,让我们看看第二个缓冲区PARAMS 为我们提供了哪些信息。

    解密 campagn 信息
    第二个引用的 BlobPARAMS是以完全相同的方式用相同的密码("T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9 "的 sha256)加密的。如果重复使用相同的解密过程,最后应该会得到类似这样的结果:

    图 15:已解密的活动信息

    我们可以得到三个参数:

    • id 10的参数似乎是活动 ID (tchk08)
    • id 3的参数似乎是一个时间戳,很可能是编译时间
    • 不知道参数40的用途

    有了这最后一条信息,我们就可以停止搜索 Qakbot 配置数据了。

    编写脚本编写思路
    你可能已经注意到了,最后解密的过程有点繁琐。在本章中,我们将通过在 Malcat 中编写一个 python 配置提取器来自动完成这一过程。事实上,Malcat 具有强大的 python 绑定功能,这些功能都有详尽的文档说明。在脚本中,你可以通过某种 Python 方式访问完整的分析对象。


    注:如果您拥有 Malcat 的完整版或专业版,也可在headless mode下从命令行运行脚本

    该脚本的作用是重新执行我们手动执行的所有步骤:
    • 收集 .data 部分中所有有趣的引用缓冲区
    • 查看这些缓冲区是否以大小为前缀。如果没有,则尝试通过查看引用函数代码中使用的常量来推断其大小。
    • 然后解密所有内容:
      • 对于字符串数组: 尝试所有可能的三元组排列(strings_array、xor_key_encrypted、aes_password)来解密字符串,并保留有效的排列。
      • 用于提取配置: 使用在第一个字符串数组中找到的任何高熵字符串作为 AES 密码,并尝试解密 CNC IP 和活动信息。保留有效的信息(我们可以用 sha256 进行双重检查)

    数组的排序在不同的样本之间会发生变化。我本可以使用代码签名来更容易地找到字符串解密函数,但代码可能会改变,而代码签名在重新编译时并不那么稳健。另一方面,分析数据则更稳健一些,我希望脚本能工作一段时间。
    既然这篇博文已经够长了,我就把下面相对完备的代码留给大家吧。对你来说,唯一有点陌生的概念可能就是Malcat 的地址空间及其a2p 函数等。但除了这个小细节外,其他的应该都很容易理解。
    [AppleScript] 纯文本查看 复制代码
    """
    name: Qakbot 5.0
    category: config extractors
    author: malcat
    
    Decrypt strings and extract CnC informations from a (plain-text) Qakbot 5.0 sample
    """
    
    import malcat
    import struct
    import itertools
    import hashlib
    import json
    import datetime
    import re
    import math
    import collections
    
    from transforms.binary import CircularXor
    from transforms.block import AesDecrypt
    
    
    
    ############################ utility functions
    
    def decrypt_aes_iv_prefix(data:bytes, aes_password: bytes):
        key = hashlib.sha256(aes_password).digest()
        iv = data[0:16]
        data = data[16:]
        return AesDecrypt().run(data, mode="cbc", iv=iv, key=key, unpad=True)
    
    
    def get_all_referencing_functions(a:malcat.Analysis, address:int):
        res = []
        for incoming_ref_type, incoming_ref_address in a.xref[address]:
            fn = a.fns.find(incoming_ref_address)
            if fn is not None:
                res.append(fn)
        return set(res)
    
    
    
    def entropy(data:str, base=2):
        if len(data) <= 1:
            return 0
        counts = collections.Counter()
        for d in data:
            counts[d] += 1
        ent = 0
        probs = [float(c) / len(data) for c in counts.values()]
        for p in probs:
            if p > 0.:
                ent -= p * math.log(p, base)
        return ent
    
    
    ############################ interesting buffer heuristics
    
    def enumerate_interesting_buffers(a:malcat.Analysis, section_name:str, prefixed_buffer:bool = False):
        section = a.map[section_name]
    
        # get all incoming xref in the section: denotates the start of a buffer
        data_xrefs = [x.address for x in a.xref[section.start:section.end]]
    
        for i in range(1, len(data_xrefs) - 1): # let's assume the first and last xrefs will never be interesting
            prev, cur, next = data_xrefs[i-1:i+2]
            prev_off = a.a2p(prev)
            cur_off = a.a2p(cur)
            next_off = a.a2p(next)
    
            if prefixed_buffer and cur - prev == 2:
                # is it a size-prefixed buffer ? (i.e. there is a referenced word 2 bytes before)
                size, = struct.unpack("<H", a.file[prev_off:cur_off])
                yield cur, size
            elif not prefixed_buffer:
                # we'll look for all immediate constants in referencing functions and see which one could be a size
                for fn in get_all_referencing_functions(a, cur):
                    for basic_block in fn:
                        if not basic_block.code:
                            continue
                        for instruction in basic_block:
                            for operand in instruction:
                                if operand.value and operand.value > 0x10 and cur + operand.value <= next and next - (cur + operand.value) < 0x20:
                                    yield cur, operand.value
    
    
    ############################ strings decryption
    
    
    def get_potential_strings_triples(a:malcat.Analysis):
        # Here we will look for 3 buffers referenced from the same function: 
        # one is the strings, one the xor key, one the aes password
    
        function_to_refs = {}
        done = set()
    
        # group all interesting buffers by referencing functions
        for address, size in enumerate_interesting_buffers(a, ".data", prefixed_buffer=False):
            if size < 0x20:
                continue
            # find all reference coming from functions
            for fn in get_all_referencing_functions(a, address):
                function_to_refs.setdefault(fn.address, []).append((address, size))
    
        # now try to find a function referencing 3 interesting buffers
        for fn_address, by_function in function_to_refs.items():
            if len(by_function) < 3:
                # there should be at least 3 references to candidate buffers inside one function
                continue
            # we don't know which is one is the data, xor key or aes password: try all permutations of triples
            for candidate_triple in itertools.permutations(by_function, r=3):
                if not candidate_triple in done:
                    done.add(candidate_triple)
                    yield candidate_triple
    
    def get_strings_arrays(a:malcat.Analysis):
        res = []
        # tries to decrypt all string arrays candidates
        for strings, xor, aes_password in get_potential_strings_triples(a):
    
            print(f"Trying strings=({a.ppa(strings[0])}, {hex(strings[1])}), xor=({a.ppa(xor[0])}, {hex(xor[1])}), aes_password=({a.ppa(aes_password[0])}, {hex(aes_password[1])}) ... ", end="")
    
            try:
                # decrypt XOR key using AES
                xor_address, xor_size = xor
                xor_offset = a.a2p(xor_address)
                xor_buffer = a.file[xor_offset: xor_offset + xor_size]
    
                aes_address, aes_size = aes_password
                aes_offset = a.a2p(aes_address)
                aes_buffer = a.file[aes_offset: aes_offset + aes_size]
    
                xor_key = decrypt_aes_iv_prefix(xor_buffer, aes_buffer)
    
                # decrypt strings using XOR key         
                strings_address, strings_size = strings
                strings_offset = a.a2p(strings_address)
                strings_buffer = a.file[strings_offset: strings_offset + strings_size]
    
                strings_decrypted = CircularXor().run(strings_buffer, key=xor_key).decode("utf8")
                all_strings = strings_decrypted.split("\x00")
    
                res.append(all_strings)
                print(f"Found {len(all_strings)} strings !")
    
            except BaseException as e:
                print(f"{e} :(")
    
        return res
    
    
    
    ############################ config extraction
    
    def qakbot_config_extraction(a:malcat.Analysis):
        print("Running heuristic to find string arrays ...")
        config_password = None
        strings_1 = []
    
        # find string arrays
        for string_array in get_strings_arrays(a):
            print(f"\nFound one string array of {len(string_array)} strings:")
            print("\n".join(string_array))
            if "ipconfig /all" in string_array:
                strings_1 = string_array
            print()
    
        ips = []
        options = {}
        config_passwords = []
    
        # try to find endpoint
        for s in strings_1:
            if re.match(r"^/[a-zA-Z0-9_%?=&-]{2,16}$", s):
                options["http_endpoint"] = s
                break
    
        # try to find password candidates: high-entropy, good length, not a lot of space or backslaches
        for s in strings_1:
            if len(s) > 30 and len(s) < 60 and entropy(s) > 4 and s.count(" ") < 2 and s.count("\\") < 2:
                config_passwords.append(s)
        print(f"Found {len(config_passwords)} password candidates: {', '.join(config_passwords)}")
    
        # ok now try to look for prefixed buffers:
        for address, size in enumerate_interesting_buffers(a, ".data", prefixed_buffer=True):
    
            # and try to decrypt using our password candidates
            for config_password in config_passwords:
                print(f"Trying config decryption for {a.ppa(address)}, {hex(size)}) with password {config_password} ... ", end="")
                try:
                    offset = a.a2p(address)
                    buffer = a.file[offset:offset+size]
    
                    # AES decrypt the buffer (skip blob identifer)
                    decrypted = decrypt_aes_iv_prefix(buffer[1:], config_password.encode("ascii"))
    
                    # verify checksum
                    checksum = decrypted[:32]
                    data = decrypted[32:]
                    if hashlib.sha256(data).digest() != checksum:
                        raise ValueError("Invalid blob checksum")
    
                    # looks like campaign info?
                    if data.count(b"=") >= 2:
                        data = data.decode("ascii").replace("\r", "")
                        d = dict([x.split("=") for x in data.split("\n") if x.strip()])
                        print(f"Found config dictionnary with  {len(d)} entries!")
                        for k, v in d.items():
                            if k == "10":
                                k = "campaign_id"
                            elif k == "3":
                                k = "date"
                                v = datetime.datetime.fromtimestamp(int(v)).isoformat()
                            options[k] = v
    
                    # looks like campaign IPs list?
                    elif data.startswith(b"\x01"):
                        for i in range(0, len(data), 8):
                            type, ip, port,_ = struct.unpack_from(">B4sHB", data, i)
                            if type != 1:
                                raise ValueError(f"Unknown CNC format {type}")
                            ip = ".".join(map(str, struct.unpack("BBBB", ip)))
                            ips.append((ip, port))
                        print ("Found IPs !")
    
                    else:
                        print("Unknwon config data")
    
                except Exception as e:
                    print(f"{e} :(")
    
        return {
            "cncs": ips,
            "options": options,
        }
    
    ################################ MAIN
    
    if __name__ == "__main__":
    
        config = qakbot_config_extraction(analysis)
    
        print("\nQAKBOT_CONFIG = ", end="")
        print(json.dumps(config, indent=4))
    结果对照分析样本
    当运行最后阶段cldapi.dll 时,脚本将输出类似下面的内容:
    [AppleScript] 纯文本查看 复制代码
    Running heuristic to find string arrays ...
    Trying strings=(0x180028150 (.data:150), 0x63), xor=(0x180028150 (.data:150), 0x63), aes_password=(0x180028150 (.data:150), 0x58) ... Data must be padded to 16 byte boundary in CBC mode :(
    Trying strings=(0x180028150 (.data:150), 0x63), xor=(0x180028150 (.data:150), 0x63), aes_password=(0x180028150 (.data:150), 0x60) ... Data must be padded to 16 byte boundary in CBC mode :(
    Trying strings=(0x1800297a0 (.data:17a0), 0x1836), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x180029700 (.data:1700), 0x9f) ... Found 185 strings !
    Trying strings=(0x18002afe0 (.data:2fe0), 0xa0), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x1800297a0 (.data:17a0), 0x1836) ... Padding is incorrect. :(
    ...
    Trying strings=(0x18002afe0 (.data:2fe0), 0xa0), xor=(0x18002afe0 (.data:2fe0), 0xa0), aes_password=(0x18002afe0 (.data:2fe0), 0x9f) ... Padding is incorrect. :(
    Trying strings=(0x18002b190 (.data:3190), 0x9c0), xor=(0x18002b190 (.data:3190), 0x9c0), aes_password=(0x18002b190 (.data:3190), 0x9c0) ... Padding is incorrect. :(
    
    Found one string array of 52 strings:
    SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList
    ProgramData
    netstat -nao
    %s "$%s = \"%s\"; & $%s"
    ...
    
    Found one string array of 185 strings:
    %SystemRoot%\SysWOW64\xwizard.exe
    .dat
    kernelbase.dll
    WBJ_IGNORE
    mpr.dll
    ...
    
    Found 2 password candidates: T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9, 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV
    
    Trying config decryption for 0x180028852 (.data:852), 0x51) with password T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9 ... Found IPs !
    Trying config decryption for 0x180028852 (.data:852), 0x51) with password 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV ... Padding is incorrect. :(
    Trying config decryption for 0x180029022 (.data:1022), 0x51) with password T2X!wWMVH1UkMHD7SBdbgfgXrNBd(5dmRNbBI9 ... Found config dictionnary with 3 entries!
    Trying config decryption for 0x180029022 (.data:1022), 0x51) with password 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV ... Padding is incorrect. :(
    
    QAKBOT_CONFIG = {
        "cncs": [
            [
                "31.210.173.10",
                443
            ],
            [
                "185.156.172.62",
                443
            ],
            [
                "185.113.8.123",
                443
            ]
        ],
        "options": {
            "http_endpoint": "/teorema505",
            "campaign_id": "tchk08",
            "40": "1",
            "date": "2024-01-31T15:22:34"
        }
    }

    它很有效!

    与另一个样本对比
    但是,该提取脚本对其他样本也有效吗?让我们用在 Malpedia 上找到的另一个去除保护的 Qakbot 样本试试:
    [AppleScript] 纯文本查看 复制代码
    Running heuristic to find string arrays ...
    Trying strings=(0x140028150 (.data:150), 0x80), xor=(0x140028150 (.data:150), 0x80), aes_password=(0x1400281e0 (.data:1e0), 0x94) ... 'utf-8' codec can't decode byte 0xad in position 0: invalid start byte :(
    Trying strings=(0x140028150 (.data:150), 0x80), xor=(0x140028150 (.data:150), 0x80), aes_password=(0x140028280 (.data:280), 0x5b5) ... Padding is incorrect. :(
    Trying strings=(0x140028150 (.data:150), 0x80), xor=(0x1400281e0 (.data:1e0), 0x94), aes_password=(0x140028150 (.data:150), 0x80) ... Data must be padded to 16 byte boundary in CBC mode :(
    Trying strings=(0x140028150 (.data:150), 0x80), xor=(0x1400281e0 (.data:1e0), 0x94), aes_password=(0x1400281e0 (.data:1e0), 0x94) ... Data must be padded to 16 byte boundary in CBC mode :(
    Trying strings=(0x140029620 (.data:1620), 0x1825), xor=(0x1400294c0 (.data:14c0), 0xc0), aes_password=(0x140029590 (.data:1590), 0x87) ... Found 185 strings !
    ...
    Trying strings=(0x14002b220 (.data:3220), 0x9c0), xor=(0x14002b220 (.data:3220), 0x9c0), aes_password=(0x14002b220 (.data:3220), 0x9c0) ... unsupported operand type(s) for +: 'NoneType' and 'int' :(
    
    Found one string array of 52 strings:
    Component_08
    Self test FAILED!!!
    route print
    whoami /all
    ...
    
    Found one string array of 185 strings:
    kernelbase.dll
    mcshield.exe
    wmic process call create 'expand "%S" "%S"'
    SOFTWARE\Microsoft\Windows Defender\Exclusions\Paths
    %ProgramFiles%\Internet Explorer\iexplore.exe
    %SystemRoot%\SysWOW64\xwizard.exe
    ...
    
    Found 2 password candidates: 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV, ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#ewZn@@@Tu
    
    Trying config decryption for 0x140028842 (.data:842), 0x61) with password 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV ... Padding is incorrect. :(
    Trying config decryption for 0x140028842 (.data:842), 0x61) with password ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#ewZn@@@Tu ... Found IPs !
    Trying config decryption for 0x140029012 (.data:1012), 0x51) with password 4Lm7DW&yMF*ELN4D8oNp0CtKUf*C2LAstORIBV ... PKCS#7 padding is incorrect. :(
    Trying config decryption for 0x140029012 (.data:1012), 0x51) with password ArpBUw9Lb9ndqXhFTfBst9YHotv92LB7BKvK#ewZn@@@Tu ... Found config dictionnary with 2 entries!
    
    QAKBOT_CONFIG = {
        "cncs": [
            [
                "146.70.158.28",
                6882
            ],
            [
                "116.202.110.87",
                443
            ],
            [
                "77.73.39.175",
                32103
            ],
            [
                "185.156.172.62",
                443
            ],
            [
                "185.117.90.142",
                6882
            ]
        ],
        "options": {
            "http_endpoint": "/teorema505",
            "campaign_id": "bmw01",
            "date": "2024-01-26T12:25:33"
        }
    }

    它也能正常工作!请注意,两个字符串数组中的字符串在不同样本中的排序是不同的。

    结论
    在这篇博文中,我们学习了如何利用 Malcat 的文件解析器和数据转换来解压多层 MSI 安装程序,直至最终的 Qakbot 样本。坚持纯静态分析,并着重强调数据分析,我们看到了如何解密 Qakbot 的字符串数组并解码其命令和控制配置。最后,通过使用 Malcat 的 python 绑定,我们编写了一个功能完备的静态配置提取器。该提取器脚本不使用任何代码签名,也不使用任何硬编码值,因此有望在未来的更改中保持稳定。
    希望大家喜欢这次的解包/脚本编写过程。希望 Qakbot 配置提取器对您今后的分析工作有所帮助。和往常一样,欢迎与我们分享您的意见或建议!

    评分

    参与人数 1威望 +1 飘云币 +1 收起 理由
    黑色夜心情 + 1 + 1 PYG有你更精彩!

    查看全部评分

    PYG19周年生日快乐!

    该用户从未签到

    发表于 2024-2-17 19:16:37 | 显示全部楼层
    楼主威武,谢谢
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    无聊
    6 小时前
  • 签到天数: 444 天

    [LV.9]以坛为家II

    发表于 2024-2-17 21:18:56 | 显示全部楼层
    楼主威武,谢谢
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    3 小时前
  • 签到天数: 1110 天

    [LV.10]以坛为家III

    发表于 2024-2-18 00:18:02 | 显示全部楼层
    请教版主:如何下载 Malcat Professional v0.9.5?
    谢谢!

    注:非 https://malcat.fr/latest/malcat_win64_lite.zip
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    4 小时前
  • 签到天数: 1597 天

    [LV.Master]伴坛终老

    发表于 2024-2-18 01:39:10 | 显示全部楼层
    感谢分享,学习了!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    昨天 12:16
  • 签到天数: 218 天

    [LV.7]常住居民III

    发表于 2024-2-18 10:56:16 | 显示全部楼层
    很棒的软件!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    昨天 12:16
  • 签到天数: 218 天

    [LV.7]常住居民III

    发表于 2024-2-19 11:18:59 | 显示全部楼层
    大佬好厉害,膜拜!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表