Carberp Bootkit 源码解析
By boywhp@126.com
By boywhp@126.com
一、BootKit 安装程序生成
Carberp 主体代码位于 BKGen、BkSetup 和 FJ 文件夹主要是安装程序和文件拼接工具,
BkBuild 目录 bkbuild.bat 批处理完成具体的安装程序生成,如下:
附件 85368
1、用 bkgen 生成 vbr.com 这个就是实际的 Bootkit 功能代码
2、使用 fj 文件拼接 bksetup 安装程序,其配置文件是 bksetup.cfg,其配置大致如下:
附件 85369
BKInstall 安装程序 = BkSetup + vbr.com + payloader
为了防止被人分析以及特征码扫描,BkGen 会对生成 vbr.com 执行随机加密变形处理,不方便我们接
下来的调试分析,需要给 BkGen 的 bme.c[Block-mixing engine]打几个补丁。
补丁 1:关闭代码块乱序
附件 85370
补丁 2:关闭随机生成 jmp 指令
附件 85371
补丁 3:关闭随机 XOR 加密
附件 85372
二、BootKit 调试环境配置
这里我走了些坑爹的弯路,IDA+VMWARE,我调试的目标机是 windows 2003 x86,具体的配置见下
面官方教程:
https://www.hex-rays.com/products/ida/support/tutorials/debugging_gdb_linux_
vmware.pdf
这里尤其要注意的是 VMWARE 的 CPU 设置,一定不要设置成什么单处理器多核心啥的,否则 IDA
调试的时候会出现无法单步执行或者奇怪的跑飞现象,我因为这个原因都换成了 IDA+Bochs 调试,坑爹啊,
所以请务必注意下 VMWARE 的 CPU 设置。
附件 85373
三、BootKit 源码解析
(一)生成器整体结构
Carberp bookit 主体功能代码使用 masm 汇编编写,分为 vbs16.com 以及 vbr32.asm 其中 vbs16.com 运
行于实模式,vbr32.asm 代码运行于保护模式,其主代码块定义在 vbr32.asm。
附件 85374
BME 块混淆引擎通过读取该表,对代码块进行乱序,随机插入跳转指令以及 XOR 加密操作,第三列
指明了该块的标志,具体含义如下:
附件 85375
BME 使用常数映射完成生成器同生成代码的参数传递,variables.h 具体如下:
附件 85376
Carberp bookit 函数重定位实现(CALL EBP/BP 机制),Carberp 使用了一个类似 API 导入函数表的
机制,来完成运行时函数之间相互的调用,具体来说就是:
1、BME 引擎额外生成了一个函数偏移地址块附件 85377,该数据块记录了
各个函数块的相对 CorePtr 的地址。
附件 85378
附件 85379
2、Core16_start 核心重定位函数
附件 85380
附件 85381
var_offset_core 变量指向该函数偏移,bk初始化时将ebp/bp指向该函数地址,然后通过如下方式:附件 85382执行函数调用
Core16 函数会读取返回地址+1 的函数索引 ID,然后通过查函数偏移表得到函数地址并调用该函数,
该机制同时会混淆调试器对汇编代码的解析,调试的时候需要注意。
(二)入口点:
Carberp 实际上是感染 NTFS “$Boot” Sectors,该扇区位于分区的第二个扇区,安装程序会从第二
个扇区+0x20 开始搜索 jmp 指令,具体如下:
附件 85383
然后根据偏移地址,执行扇区数据替换为 vbr.com,该扇区最终被加载到 0xD200 偏移位置。
IDA 启动调试器,断点 0x7C00,F9 执行,第二次命中断点单步跟踪一会即可看到:
附件 85384
入口 0xD00:26A = 0xD26A,IDA 调试时,注意 ALT+S 设置为 16 位段 汇编模式
附件 85385
这里有一点比较有意思就是这段入口点的汇编代码和原始的 NTLDR 入口点汇编完全一致,除了那个
CALL near 地址不一样,我估计应该是增加欺骗性用的吧,对应源码如下:
附件 85386
最后 call bp;db F_Startup 会被 BME 引擎替换成一个 call near 指令,因为该入口块 B_NOEBP 标识
表明了此时 bp 尚未初始化,不能使用 call bp 调用方式,F_Startup 对应的正是 Startup16_start 的索引。
(二)Startup16_start
附件 85387
获取运行时基地址,使用最常见的 call /pop 定位代码位置。
BK 接下来需要分配内存,同时需要把自己的内存空间保留起来,以免内存被正常的启动代码覆盖掉,
附件 85388
附件 85389
BK 从 BIOS Data Area 可用内存空间最高地址保留了 3KB 用来存放 BK 自身代码
附件 85390
接下来 BK 将自身拷贝到该内存地址的同时完成 XOR 解码
附件 85391
最后初始化 bp->CORE pointer 、跳转到 var_block0_ofs 函数继续执行,var_block0_ofs 是代码块索引 0
相对于 CORE_PTR 的偏移地址,var_block0_ofs + var_offset_core = Main16_start【索引 0】。
附件 85392
(三)Main16_start
该函数主要完成原始 Loader 的解压、INT15 HOOK 以及 payload 加载。BK 安装程序将原始 Loader
压缩后保存在 BK 代码之后,PAYLOAD 代码保存在磁盘的 NTFS 保留扇区内,BK 代码通过 FileName 变
量获取 PAYLOAD 的位置信息,FileName 由 BkSetup 程序根据实际扇区位置写入【搜索 0x33333333 定位FileName】。
FileName 定义如下:
附件 85393
实际磁盘布局如下:
附件 85394
附件 85395
首先将 packed loader 向后挪动,以免解压时原始数据被破坏
附件 85396
附件 85397
0xD00:BE8 复制到 0xD00:206A,腾出 NTFS_LOADER_SIZE 空间,以便接下来解压 Loader。然后执
行解压,BK 使用了开源轻量级压缩库 aPLib 对原始 LOADER 进行压缩,网址如下:
http://ibsensoftware.com/products_aPLib.html
附件 85398
具体解压算法就不研究了,aPLib 本身也提供了 asm 版的解压实现,估计该 BK 的作者也就简单修改
了下。解压完成后,D00:26A 入口点就是原始代码数据了。
接下来调用 LoadData 函数把 PAYLOAD 数据加载到内存。
附件 85399
LoadData 根据 PAYLOAD 大小,申请 BDA 保留内存空间,然后使用 INT13 服务读磁盘,加载 PAYLOAD
到申请到的内存地址,PAYLOAD 内存地址同时保存到 FileName 数据结构中,以便以后使用。
最后执行 INT15 HOOK
附件 85400
GETPTR 宏获取指定函数 ID 的偏移地址,返回结果为 EAX,定义如下:
附件 85401
SALC 会被 BME 引擎重新解析为 MOV CL,XX,实际汇编如下:
附件 85402
(四)Handler15
该函数完成 INT15 的 HOOK,BK 需要处理额外处理下 BIOS INT 15,0xE820 内存检测,欺骗 windows
系统,以防自身保留的代码空间被操作系统意外使用了。
http://wiki.osdev.org/Detecting_Memory_(x86)
附件 85403
BK 首先测试 INT15 调用者的 cs,实际 NTLDR 会被加载到 cs = 0x2000 的位置,从而执行 H15_check_ldr
功能代码。
附件 85404
上述代码搜索 NTLDR 的特征码 0F-22-C0-87-DB,对应汇编为 MOV CR0, EAX / XCHG BX, BX,实际
对应位置如下:
附件 85405
这段代码设置 CR0 寄存器,开启 CPU 换页和保护模式,该函数实际为 EnableProtectPaging。
接下就是对 LDR 代码执行补丁操作了。
附件 85406
PatchLdr 首先保存原始特征码到 BK 的尾部空闲内存,然后将其替换为 9a CALL FAR JMP 到 PmInit。
附件 85407
现在 BK 已基本完成了前期的准备工作,他悄悄的在 EnableProtectPaging(开启换页和保护模式)关
键代码上种下了一个诅咒 PmInit。
(四)PmInit
一旦 Windows 系统准备好,开启换页的时候,BK 的 PmInit 函数就会得到执行,实际上,这个时间点
是在 OsLoader 完成内存子系统初始化后,第一次调用 Su 导出函数 HardwareCursor 完成后执行的。
附件 85408
Su 在 每 个 导 出 函 数 前 后 都 加 了 一 个 一 小 段 代 码 , 每 个 导 出 函 数 调 用 完 成 后 , 都 会 执 行 一 次
EnableProtectPaging。
PmInit 首先从 BK 尾部内存取出原始数据,恢复先前的 Hook 点。
附件 85409
由于 BK 使用了 DR 寄存器硬件断点功能,一旦断点命中,CPU 会执行 IDT INT1 处理,因此首先需
要对 IDT(中断分发表)INT1 执行 HOOK,使其指向 Int1Handler 函数。
附件 85410
HookIdt 中完成 INT1 HOOK,同时下了硬件断点 DR0 (IDT 读写 - 针对 NT6):
附件 85411
硬件断点 DR2(0x401000 执行 - 针对 NT5)
附件 85412
至此,BK 所有 16 位代码使命结束,函数返回后,操作系统正式进入换页保护工作模式。
(四)Int1Handler
附件 85413
BK 的 HOOK 函数开始都是 PUSH/MOV ESI, 12345678h,其中 12345678h 会在 HOOK 创建时写入,
使得 ESI 始终指向 BK 代码运行时线性基地址。
首先清掉上次下的硬件断点
附件 85414
重新设置 ebp 指向 CORE 函数,以便执行 CALL EBP 函数调用,汇编代码中夹了一个 REXW,这是
干啥的呢?
附件 85415
原来 BK 为了兼容 X86、X64,使用了 CPU REX 前缀,也就是说 64 位的话就相当于扩展了寄存器而
已,但是 32 位也能运行,只不过 REXW 相当于一个 DEC EAX 指令,这样就用不搞两套 BIN 了,而且代
码也可以通过这种方式,判断自身运行是 32 位还是 64 位环境,如下。
附件 85416
这里可能会有同学疑问,64 位就直接返回了,难道 BK 不支持 64 位?实际上不是的,应该是进入 64
位模式前,代码首先运行在 32 位保护模式,当然这只是我的猜想,也没有实际验证了【切换 64 位是通过
读写 MSR 寄存器完成的】。
执行 OS 入口点 HOOK:
附件 85417
Winload 函数负责 OS 入口点的 HOOK, NT5 X86 根据特征码 68 4B 23 00 00 PUSH 234B,搜索
OsLoader 中跳转到 OS 的入口点指令,Call [ebp+XXX],并调用 PatchCall 函数将该 Call 替换为 E8 Call Near
到 Hook 函数,NT5 OsLoader Call KernelEntryPoint 对应的反汇编如下:
附件 85418
这里 BK 会针对不同版本 Windows 进行的处理,可见这个 BK 兼容性应该还是不错的。
接下来,BK 最想干的事莫过于赶紧申请一块内存,毕竟第一次进入新环境,否则指不定啥时就没了,
OsLoader 通过 BlAllocateAlignedDescriptor 函数从内存子系统中申请内存。
附件 85419
我们看看 HookAlloc5 是怎么 Hook 内存分配的,还是特征码搜索 6a 00 6a 19 e8(PUSH 0;PUSH 19;
CALL xxx),具体代码如下:
附件 85420
IDA 定位特征码如下:
附件 85421
由此可以获取 BlAllocateAlignedDescriptor 函数的地址,然后执行 Inline Hook
附件 85422
注意这里的 add esi, 8 指令,因为我们同时 PatchCall 了两个函数,一个是 WinLoad 函数对 Os 入口点
调用了一次 PatchCall,然后是这次 PatchCall,BK 将原始函数的 Patched 字节保存到 BK 的内存尾部,这
里需要通过 esi 设定一个偏移量,否则会把上次保存的函数 Patched 字节冲掉了。
现在 BK 完成了 BlAllocateAlignedDescriptor 函数的 HOOK -> LoadFile5
OsLoader Call OsEntryPoint 的 HOOK -> TransferHook5。
(五)LoadFile5
附件 85423
该函数负责申请内存,将 payload 复制过去,然后在内存里面处理 PE 重定位。.
附件 85424
上述代码初始化 ebp 指向 CORE 函数,然后调用 RestoreCall 恢复 Inline Hook,其中 cl 寄存器指明堆
栈中多压入的参数个数,返回 ECX 寄存器 Call 原始函数指令的地址,具体细节请自行阅读。
然后从原始调用堆栈中复制一份到堆栈(COPY 代码大亮),执行原始函数调用。
附件 85425
然后测试调用是否成功,成功的话就说明内存子系统没问题了,可以搞点私活了,如下:
附件 85426
DecryptImage 函数从 FileName 里面取出内存地址(LoadData 函数完成读取扇区)以及 XOR 值,完
成内存解密,最后调用 BlAllocateAlignedDescriptor 函数申请 Windows 内存。
附件 85427
BuildImage 函数在内存中重建 PE 镜像,并处理重定位信息,具体细节就不细究了
附件 85428
(六)TransferHook5
该函数里面会获取到 Os 入口的地址 KiSystemStartup,然后再次进行了一次 Inline Hook,使得 Os 入口
执行 KernelHook5 函数,话说这里我感觉是不是多此一举啊?具体如下:
附件 85429
(七)KernelHook5
到这里内核终于正式启动起来了,我们的 Payload 也一切准备就绪了,该函数基本没干啥
附件 85430
统一了下 x86/x64 参数 1 -> ECX/RCX,然后直接调用 InitNt 函数。
(八)InitNt
附件 85431
根据 EDX 搜索内核内存基地址,需提供给 Payload 驱动解析内核导入表用,如下。
附件 85432
然后调用
附件 85433
(九)RegisterDriver
附件 85434
到这里基本上该做的都做完了,保存好现场,在堆栈中准备好参数块参数,然后调用内存镜像中的入
口点即可,这里要注意的是,此时 Payload 驱动只是被执行了,具体的后续工作还需要 Payload 驱动自行
完成,最重要的就是驱动导入表初始化。然后根据 BK 提供的内核信息,如下:
附件 85435
通过处理 NtLoaderBlock 信息,使得操作系统在加载 BOOT 驱动的时候再次执行 Payload 的驱动入口
点,相关代码都可以通过 C 代码完成,这里就不具体分析了。
注:本帖由看雪论坛志愿者PEstone 重新将PDF整理排版,若和原文有出入,以原作者附件为准
我是以Windows2003 X86为目标机调试跟踪的,具体64和Windows7啥的请自行分析
文章匆匆写完也没校对了,可能会有些错误,请自行略过
Carberp源码:
https://github.com/hzeroo/Carberp
PDF分析文档
附件 85236
我加了一些备注的汇编代码
附件 85237
Carberp PLOADER_PARAMETER_BLOCK
附件 85290