【转载】分析重装系统也无法清除的鬼影病毒
声明:本文由【MS509 Team】成员expsky原创,仅用于技术交流分享,禁止将相关技术应用到不当途径。
整理电脑的时候找到自已以前分析的一个鬼影病毒的资料,当时兼容市面上主要的windows系统(XP, win7,包含x86和x64系统)样本来自国外,有不少亮点,当时花了不少时间把所有原理分析出来并重新用汇编和C++实现了出来。以前的一些资料简单整理了下,分享出来,也给自己以前的工作留个记录。
该木马样本通过感染MBR达到早于系统得到执行。整个样本以非文件形式存在,直接写人磁盘扇区。包括感染的MBR以及保存在磁盘末尾非文件系统的payload。
木马执行流程从MBR开始,通过hook和注册回调函数的机制,将所有的木马程序在windows启动过程中逐级得到执行, windows启动完毕后最终将下载者dll注入到指定的系统进程中,下载者代码循环执行而告终。
木马亮点有:
绕过windows的PatchGuard保护
兼容XP, win7(x86,x64)(bootkit型底层木马兼容不同系统是挺有挑战的)
自保护功能:感染木马后通过WinHex等工具查看到的MBR是正常的,而且无法修改被感染的MBR(不是简单的修改失败,而是会呈现MBR被修改成功的假象)
从内核态向用户态的指定系统进程注入下载者DLL(根据系统是32位还是64位,会注入相应的32位和64位版本的DLL)
启动过程中关闭指定进程
木马工作原理
1)木马 植入程序运行后感染MBR 和磁盘末尾未分区部分(无文件,直接写入磁盘扇区)
感染的数据主要包括6个部分:
感染的MBR;
启动部分代码;
x86驱动代码;x64驱动代码;
x86下载者DLL;x64下载者DLL
(前两部分未压缩,其他4部分进行了aPLib压缩)
2)重启电脑后感染的MBR接管执行:
a) 加载末尾20个扇区的前18个扇区(未压缩数据)到常规内存并执行
b) 加载第19个扇区(原始MBR)到0x7C00
c) 加载第20个扇区(配置信息:驱动、下载者代码的大小、payload所在扇区位置数据)到常规内存
d) 调用Int15扫描常规内存,保存ARD结构体用于后续常规内存的分页扫描(查找ntoskrnl.exe映像)
e) hook IVT表中int13向量(用于监控后续磁盘读操作)
f) 跳转到0x7C00控制权转交原始MBR,系统继续启动
ARD: AddressRange Descriptor Structure
+0:BaseAddrLow:基地址的低32位
+4:BaseAddrHigh:基地址的高32位
+8:LengthLow:长度(字节)的低32位
+12:LengthHigh:长度(字节)的高32位
+16:Type:这个地址范围的地址类型(1:AddressRangeMemory;2:?AddressRangeReserved;Other:Undefined)
3)Int13 钩子监视系统启动过程的读磁盘操作,当读取到kdcom.dll 时接管执行(绕过PatchGuard的时机)
a)int13钩子根据读取文件的前0×200字节的校验值和PE特征码识别kdcom.dll,根据kdcom.dll PE的mechine字段判断系统为32位还是64位(32位和64位部分hook函数不同,整体流程一致)
b)根据前面保存的ARD结构体扫描常规内存,找到ntoskrnl.exe基地址(根据文件hash值),再根据ntoskrnl PE导出表hook函数IoCreateDriver(根据函数名hash值,64位系统hook MmMapIoSapce)
c)控制权继续转交系统
4)IoCreateDriver钩子函数接管执行
a)先恢复IoCreateDriver钩子
b)同前机制,根据ntoskrnl.exe基地址和PE导出表找到函数PsSetLoadImageNotifyRoutine
c)调用PsSetLoadImageNotifyRoutine,注册回调函数
d)控制权继续转交系统
5)系统加载模块时,上步PsSetLoadImageNotifyRoutine注册的回调函数接管执行
a)如前机制,得到NtReadFile,NtClose,NtOpenFile,ExAllocatePool函数指针
b)申请8K内存,再打开Device\Harddisk0\Patition0,将驱动部分压缩数据读入其中
c)申请0×3600字节内存,解压驱动数据到其中
d)释放8K压缩数据内存,转入驱动代码部分执行
6)驱动部分压缩数据解压后的执行
a)根据实际加载的内存地址,对驱动部分的常量进行重定位
b)InstallProcessNotifyCallback注册回调函数,回调函数监控当创建的进程如包含在屏蔽进程列表中时,直接return
c)PsSetLoadImageNotifyRoutine注册回调函数,当创建进程包含在注入进程列表中时注入下载者DLL
d)Hook NtReadFile, NtWriteFile, 当尝试读写感染的MBR或磁盘末尾未分区处,都返回正常值,隐藏被感染迹象。同样写入数据到此部分也会做相应保护,不会真正写入,但会呈现已被写入的假象
内核向用户态进程注入代码原理
通过KeInitializeApc、KeInsertQueueApc(未文档化函数)插入内核APC回调函数、ExQueueWorkItem插入WorkItem回调函数,附加用户进程,将注入代码复制到用户进程内存空间,最后通过一个用户APC指向注入代码,并异步得到执行。
详细流程如下:
1)PsSetLoadImageNotifyRoutine注册回调函数
2)任意进程加载模块时,上步注册的回调函数得到执行
调用FsRtlIsNameInExpression,判断加载的是kernel32.dll,是则继续执行,否则返回。(The FsRtlIsNameInExpressionroutine determines whether a Unicode string matches the specified pattern.)
FsRtlAllocatePool申请0×34字节内核非分页内存,用于后续APC对象及回调参数。
将kernel32.dll基址存入0×34字节的最后一个DWORD中(用于APC回调函数入参)
调用KeGetCurrentThread得到当前线程对象(用于APC初始化的入参)
调用KeInitializeApc、KeInsertQueueApc(未文档化函数)插入内核APC回调函数(执行后续流程)
3)上步插入的内核APC函数异步执行
调用ExFreePool释放APC结构体内存
FsRtlAllocatePool申请0×40字节非分页内核内存(用于后续EVENT对象、WORKITEM对象及其参数)
调用KeGetCurrentThread、PsGetCurrentProcess、PsGetCurrentThreadProcessId得到进程对象,线程对象,PID(存入上面申请的内存中,作为参数传给ExQueueWorkItem插入的回调函数)
调用KeInitializeEvent,用于同步,等待WorkItem执行完毕
调用ExQueueWorkItem插入后续流程的回调函数
调用KeWaitForMutexObject等待上面的WorkItem回调函数执行完毕
调用ExFreePool释放上面申请的0×40字节内核内存
4)上步ExQueueWorkItem插入的回调函数执行
调用PsLookupProcessByProcessId得到EPROCESS
调用PsGetProcessImageFileName通过EPROCESS得到进程文件名
计算进程文件名的hash值与想要注入代码的进程列表做对比(包含在内继续执行,否则返回)
调用KeStackAttachProcess将当前线程附加到目标进程用户态地址空间
配置入参数ClientId, ObjectAttributes后,调用ZwOpenProcess打开目标进程
调用ZwAllocateVirtualMemory申请目标进程用户态内存
mov指令将内核中的注入代码拷贝到上步申请的用户态进程空间
调用FsRtlAllocatePool申请0×30字节内核非分页内存用于APC对象
调用KeInitializeApc,KeInsertQueueApc插入用户APC(注入代码在用户态进程执行)
调用ZwClose,KeUnstackDetachProcess,ObDereferenceObject释放资源
末尾调用KeSetEvent设置事件,用于同步,通知WorkItem执行完毕
5)上步插入的用户APC异步得到执行(注入的代码在目标进程执行)
至此,完成了内核态向目标用户进程注入代码并得到执行
自保护原理
木马自保护功能:当木马正常工作后,用WinHex等磁盘查看工具是看不到MBR及磁盘末尾被感染的迹象的,并且用工具对MBR或磁盘末尾进行修改后,呈现被改动的假象,但实际并未修改成功。木马代码依然存在在磁盘相应位置。
通过hook NtReadFile和NtWriteFile两个函数实现的自保护功能
当写数据到MBR或磁盘末尾处时,写入的数据存入内存而不真正写入磁盘,当读取磁盘MBR或末尾时,不真实读取,而用之前暂存的数据代替,呈现数据被写入的假象,实现自我保护
内存0×20000(128k)
0×0 | 磁盘末尾最后一个扇区数据(配置信息) |
---|---|
0×200 | 原始MBR |
0×400 | 保存被修改过的MBR(假MBR) |
0×600 | 感染后的MBR |
0×800 | 磁盘末尾0xFC00字节(126个扇区,63K) |
0×10400 0×20000 | 保存被修改后的磁盘末尾0xFC00字节数据(126个扇区,63K),假的磁盘末尾数据 |
(1)NtWriteFile钩子函数
a)确认写入的数据是否在MBR处或磁盘末尾0xFC00处
b)当向MBR写入数据时:将写入的数据保存到上表内存中0×400处(假MBR),以及更新0×200(原始MBR)和0×600(染后后MBR)的末尾0x4C的数据信息(只更新MBR中分区表等数据信息,而不改变MBR中的代码);修改磁盘MBR处后0x4C的分区表等数据,修改磁盘倒数第2个扇区(备份的原始MBR)的后0x4C数据部分
c)当向磁盘末尾0xFC00写入数据时:计算写入数据与末尾0xFC00的交叉情况,保存写入的数据到0×10400处(假的磁盘末尾数据)
(2)NtReadFile钩子函数
a)确认读取的数据是否在MBR处或磁盘末尾0xFC00处
b)当读取MBR时,用上表中0×400处的假MBR代替
c)当读取磁盘末尾时,用上表中0×10400处假数据代
结语
这是几年前逆向分析的基于MBR的bootkit型样本(来自国外),运行相当稳定(也尝试过改造来从BIOS启动,但主板芯片的差异,以及植入BIOS的程序兼容性很难保证,最终稳定性不好)该样本当时未在国内传播,逆向老外的样本过程中发现各种细节处理的非常到位,师夷长技以自强 。^_^
*本文原创作者: expsky ,本文属FreeBuf原创奖励计划,未经许可禁止转载