我见过安服小伙汁在安全应急响应时,担心现场分析遗漏,在授权的情况下,在失陷的主机上安装vmware converter并把硬盘转为虚拟磁盘带走分析,可能是硬盘不方便拆也可能是不方便关机。类似的工具还有微软的disk2vhd或winhex将硬盘转为虚拟磁盘文件。这些方式我都了解过。在这个安服场景下这些工具都有些不足,例如要安装个软件,要么就是全扇区备份太耗时,要么就是转后的格式不方便vmware来引导仿真启动。
为了写一个更贴切这个安服场景的工具,我通过逆向分析 Disk2VHD 的工作机制和相关代码,最终实现一个类似工具的过程。这既是一段技术上的探索,也是一场编程能力的挑战。
先放代码仓库链接 github.com/threatexpert/disk2vmdk
首先,编程之前有几个主要问题要解决:
VBoxManage是开源虚拟机项目virtualbox中的一个工具,主要特点是可以接受从stdin管道输入原始磁盘数据,然后按照指定格式生成磁盘镜像。从安装包中获取编译好的工具来调用是比较方便的,比起去研究和编译virtualbox庞大的源代码项目方便太多了,比如希望输入原始磁盘数据后生成VMDK到h:\test.vmdk,原始磁盘大小假设是1024209543168字节,那么使用该工具+参数如下就可以了。
Virtualbox最后的32位安装包版本是5.2.44,二进制程序可以运行在winxp至win11,通用性很好,只需从安装包中提取VBoxManage.exe、msvcp100.dll、msvcr100.dll、VBoxDDU.dll、VBoxRT.dll总共5个文件,开发者就可以方便的实现把原始镜像数据转换VMDK、VHD或VDI几种常见的虚拟磁盘镜像格式。
如果不想调用VBoxManage进程管道输入,还可以考虑直接动态调用VBoxDDU.dll的4个导出接口VDCreate、VDCreateBase、VDWrite、VDClose就可以实现,非常便捷。函数原型可以从Virtualbox开源中获得。更快捷的方式是可以从我的源码中找到VDCreate相关的源码文件VBoxSimple,
这是已经从Virtualbox源码中裁剪出来的相关代码,接口是不是很简单很方便。
对于备份来说,主要好处是比较节省存储空间,不像DD原始镜像,VMDK、VHD、VDI这几种格式都支持镜像文件按需增长,且具有类似压缩的效果,比如原磁盘有大量磁盘空间是全零数据,这些空间在转为VMDK时并不需要占用镜像文件的大小。
对于安全分析员来说,备份包含系统分区的虚拟磁盘镜像可以很方便作为虚拟机磁盘加载启动,然后展开一些分析工作。
对于备份磁盘数据来说,也是非常方便的,特别是现在7z软件可以解析虚拟磁盘文件。我们就好像把整个磁盘数据打包成一个文件,从底层拷贝磁盘数据是直接跳过文件权限问题的,后续可以直接用7z右键打开直接浏览这个虚拟磁盘文件里的所有文件。
操作系统将整个磁盘设备抽象成了一个文件,在Windows下是可以用API CreateFile打开、ReadFile读取的方式去读取所有扇区的,跟读取普通文件的方式类似。操作系统还进一步根据磁盘的分区信息,把每个分区挂载并抽象成一个文件。这样开发者就能很方便的根据要备份的磁盘或分区进行备份。
\\.\PhysicalDriveX 这是整个磁盘的文件名格式,X是序号,可在磁盘管理了解。
\\?\Volume{GUID} 这是分区的文件名格式。
磁盘设备可以通过QueryDosDevice以第一个参数为NULL来获取所有设备列表,再筛选设备名开头是PhysicalDrive来得到系统挂载的所有磁盘设备,然后就可以使用CreateFile打开相应设备,得到设备的句柄,有了句柄后主要就是通过DeviceIoControl和设备驱动通信了,常见通信控制代码例如IOCTL_DISK_GET_DRIVE_GEOMETRY_EX可查询磁盘大小信息、IOCTL_DISK_GET_DRIVE_LAYOUT_EX可查询分区信息等等,在网上搜索这些控制代码可以找到很多参考代码。
这个功能可以说是最简单的,比如磁盘设备名是\.\PhysicalDrive0,把它当普通文件用从头读取,直到ReadFile遇到结尾,最好还是先获得磁盘的大小,根据总大小来读取,使用的缓冲区应该是扇区大小(512字节)的倍数。注意你可能不能用普通文件的方式用SetFilePointer 调整文件指针到末尾,然后根据文件指针位置来获得磁盘的原始大小,而是应该用DeviceIoControl结合参数IOCTL_DISK_GET_DRIVE_GEOMETRY_EX来查询磁盘大小信息。
首先把磁盘的划分按“分区”和“非分区”区别对待,“非分区”的区域比如分区表、没有识别的分区空间等等都认为是已使用的扇区,“分区”区域的使用情况是由相关文件系统决定的,然而文件系统不管是NTFS还是FAT32,都可以通过DeviceIoControl结合参数FSCTL_GET_VOLUME_BITMAP向分区设备查询已经分配使用的空间位图。其实这也是disk2vhd的原理,在一开始并不清楚disk2vhd备份速度比winhex克隆磁盘快很多是什么原因,通过Procmon和静态反编译分析等方式对disk2vhd进行研究后才有所了解,原理disk2vhd并不是全扇区备份。
首先是使用Procmon过滤出disk2vhd的所有行为,发现备份数据过程有很多ReadFile是从分区设备读取数据,仔细分析每个ReadFile的详细发现,偏移不是有规律递增,会“智能”的跳进,似乎会跳过一些未使用的空间,往前追溯到CreateFile附近发现,有两个DeviceIoControl查询行为很可疑,分别是FSCTL_GET_VOLUME_BITMAP(0x9006F)和FSCTL_FILESYSTEM_GET_STATISTICS(0x90060),通过搜索网上资料了解,这两个控制代码的作用应该就是获得磁盘在使用的扇区的关键作用,然后用反编译工具对disk2vhd 2.01 (MD5 AD3E0AB4C552584FDDCD1DAC4388A0A9)研究了一番,找到“某函数”相关代码验证了disk2vhd的原理,disk2vhd的流程是,先得到BITMAP信息,然后还尝试把根目录的页文件\Pagefile.sys和休眠文件Hiberfil.sys的相关偏移也在BITMAP中置0,再判断分区如果是FAT则需要计算首个簇(cluster)的偏移,最后拷贝分区的时候就可以根据BITMAP信息中的非0位“智能”的跳进。
对逆向分析感兴趣的小伙伴,通过定位DeviceIoControl的引用函数1400055C0,查看参数有0x9006F或0x90060的函数应该可以很容易找到“某函数”的地址,该函数前部分还有一些代码是获取簇(cluster)的大小,以及计算装载整个分区的Bitmap需要多大内存。簇是什么呢?还记得格式化磁盘的时候界面有个“分配单元大小”的选项吧,这个就等于一个簇的大小,比如通常NTFS一个簇是4096,等于是8个扇区(512字节)组成一个簇。
用FSCTL_GET_VOLUME_BITMAP查询到的BITMAP信息结构体如下,开发者根据其中的Buffer进行解析。
Bitmap是以cluster为单位描述一块磁盘空间是否有被使用,用1个位的1或0描述是否被使用。一个cluster的大小通常是格式化时的“分配单元大小”决定的,所以Buffer中每个字节可以描述8个cluster单元的磁盘空间的使用状态。
假设StartingLcn 为0时,Buffer的第一字节最低位描述第0个cluster的状态,然后按顺序去分析所有cluster状态。第0个cluster的偏移在NTFS下就是分区起始位置,这样按cluster单元大小可直接算出每个cluster在分区的绝对逻辑偏移,而FAT的首个cluster不是在分区起始位置,需要从分区的起始512个字节的内容(BootSector)去分析首个cluster的偏移,感兴趣的同学可以去分析样本中的func_1400056B0,看他如何解析的,可以把代码直接抠出来用。
注意Bitlocker是以分区来加密的,不是整个磁盘。如果用户已经解锁了加密分区,通过\?\Volume{XXXX}方打开设备,读取的数据就是解密后的数据。如果从\.\PhysicalDriveX设备读取整个磁盘,这相当于绕过了Bitlocker的驱动模块,那么磁盘中Bitlocker分区相应的位置读取到的会是加密的数据。
通过了解掌握一种虚拟磁盘的组件的接口、windows的磁盘管理接口,以及分区Bitmap的一些知识,咱就可以开发类似disk2vhd这种物理磁盘转虚拟磁盘的功能了。另外远程备份的方式则只是多开发层网络通信,详细可以见源代码。
VBoxManage.exe convertfromraw stdin
"h:\\test.vmdk"
-
-
format
VMDK
1024209543168
VBoxManage.exe convertfromraw stdin
"h:\\test.vmdk"
-
-
format
VMDK
1024209543168
class
CVBoxSimple {
void
*
_pDisk;
public:
CVBoxSimple();
virtual ~CVBoxSimple();
static
bool
InitLib();
bool
CreateImage(const char
*
format
, const char
*
dst_file, uint64_t cbFile);
/
/
UTF8
bool
CreateVMDK(const char
*
dst_file, uint64_t cbFile);
bool
Write(uint64_t offFile, const void
*
pvBuf, size_t size);
bool
Close();
};
class
CVBoxSimple {
void
*
_pDisk;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!