开发背景
我见过安服小伙汁在安全应急响应时,担心现场分析遗漏,在授权的情况下,在失陷的主机上安装vmware converter并把正在运行操作系统的硬盘转为虚拟磁盘带走分析,可能是硬盘不方便拆也可能是不方便关机。类似的工具还有微软的disk2vhd或winhex将硬盘转为虚拟磁盘文件。这些方式我都了解过。在这个安服场景下这些工具都有些不足,例如要安装个软件,要么就是全扇区备份太耗时,要么就是转后的格式不方便vmware来引导仿真启动。
为了写一个更贴切这个安服场景的工具,我通过逆向分析 Disk2VHD 的工作机制和相关代码,最终实现一个类似工具的过程。这既是一段技术上的探索,也是一场编程能力的挑战。
已开发的工具名学微软的disk2VHD,这里叫disk2vmdk,其实也支持转存为VHD(HyperV)、VDI(virtualBox),源码仓库和工具下载链接见底部。
首先,编程之前有几个主要问题要解决:
- 如何编程生成虚拟磁盘格式
- 如何只备份已使用磁盘空间
- 如何备份bitlocker系统盘解密后的数据
如何编程生成虚拟磁盘格式
1. VBoxManage是一个很方便编程调用接口
VBoxManage是开源虚拟机项目virtualbox中的一个工具,主要特点是可以接受从stdin管道输入原始磁盘数据,然后按照指定格式生成磁盘镜像。从安装包中获取编译好的工具来调用是比较方便的,比起去研究和编译virtualbox庞大的源代码项目方便太多了,比如希望输入原始磁盘数据后生成VMDK到h:\test.vmdk,原始磁盘大小假设是1024209543168字节,那么使用该工具+参数如下就可以了。
VBoxManage.exe convertfromraw stdin "h:\\test.vmdk" --format 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,
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();
};
这是已经从Virtualbox源码中裁剪出来的相关代码,接口是不是很简单很方便。
2. 虚拟磁盘的好处
-
对于备份来说,主要好处是比较节省存储空间,不像DD原始镜像,VMDK、VHD、VDI这几种格式都支持镜像文件按需增长,且具有类似压缩的效果,比如原磁盘有大量磁盘空间是全零数据,这些空间在转为VMDK时并不需要占用镜像文件的大小。
-
对于安全分析员来说,备份包含系统分区的虚拟磁盘镜像可以很方便作为虚拟机磁盘加载启动,然后展开一些分析工作。
-
对于备份磁盘数据来说,也是非常方便的,特别是现在7z软件可以解析虚拟磁盘文件。我们就好像把整个磁盘数据打包成一个文件,从底层拷贝磁盘数据是直接跳过文件权限问题的,后续可以直接用7z右键打开直接浏览这个虚拟磁盘文件里的所有文件。
如何只备份已使用磁盘空间
操作系统将整个磁盘设备抽象成了一个文件,在Windows下是可以用API CreateFile打开、ReadFile读取的方式去读取所有扇区的,跟读取普通文件的方式类似。操作系统还进一步根据磁盘的分区信息,把每个分区挂载并抽象成一个文件。这样开发者就能很方便的根据要备份的磁盘或分区进行备份。
1. 先了解访问磁盘设备文件名的格式
\\.\PhysicalDriveX 这是整个磁盘的文件名格式,X是序号,可在磁盘管理了解。
\\?\Volume{GUID} 这是分区的文件名格式。
磁盘设备可以通过QueryDosDevice以第一个参数为NULL来获取所有设备列表,再筛选设备名开头是PhysicalDrive来得到系统挂载的所有磁盘设备,然后就可以使用CreateFile打开相应设备,得到设备的句柄,有了句柄后主要就是通过DeviceIoControl和设备驱动通信了,常见通信控制代码例如IOCTL_DISK_GET_DRIVE_GEOMETRY_EX可查询磁盘大小信息、IOCTL_DISK_GET_DRIVE_LAYOUT_EX可查询分区信息等等,在网上搜索这些控制代码可以找到很多参考代码。
2. 如何备份磁盘所有扇区
这个功能可以说是最简单的,比如磁盘设备名是\.\PhysicalDrive0,把它当普通文件用从头读取,直到ReadFile遇到结尾,最好还是先获得磁盘的大小,根据总大小来读取,使用的缓冲区应该是扇区大小(512字节)的倍数。注意你可能不能用普通文件的方式用SetFilePointer 调整文件指针到末尾,然后根据文件指针位置来获得磁盘的原始大小,而是应该用DeviceIoControl结合参数IOCTL_DISK_GET_DRIVE_GEOMETRY_EX来查询磁盘大小信息。
3. 如何仅备份磁盘已使用的空间
首先把磁盘的划分按“分区”和“非分区”区别对待,“非分区”的区域比如分区表、没有识别的分区空间等等都认为是已使用的扇区,“分区”区域的使用情况是由相关文件系统决定的,然而文件系统不管是NTFS还是FAT32,都可以通过DeviceIoControl结合参数FSCTL_GET_VOLUME_BITMAP向分区设备查询已经分配使用的空间位图。其实这也是disk2vhd的原理,在一开始并不清楚disk2vhd备份速度比winhex克隆磁盘快很多是什么原因,通过Procmon和静态反编译分析等方式对disk2vhd进行研究后才有所了解,原理disk2vhd并不是全扇区备份。
3.1. 简述逆向分析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字节)组成一个簇。
3.2. 解析Bitmap获得已分配使用的扇区
用FSCTL_GET_VOLUME_BITMAP查询到的BITMAP信息结构体如下,开发者根据其中的Buffer进行解析。
struct {
LARGE_INTEGER StartingLcn;
LARGE_INTEGER BitmapSize;
BYTE Buffer[1];
} VOLUME_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,看他如何解析的,可以把代码直接抠出来用。
4. 如何备份Bitlocker分区解锁后的数据
注意Bitlocker是以分区来加密的,不是整个磁盘。如果用户已经解锁了加密分区,通过\?\Volume{XXXX}方打开设备,读取的数据就是解密后的数据。如果从\.\PhysicalDriveX设备读取整个磁盘,这相当于绕过了Bitlocker的驱动模块,那么磁盘中Bitlocker分区相应的位置读取到的会是加密的数据。
工具介绍
- 适应系统winxp-win11
- 类似winhex针对挂载中的磁盘进行全盘镜像,可直接生成为vmdk格式,也可选dd格式、VHD格式和VDI格式。
- 镜像磁盘时可选某个分区排除,例如只取C盘和引导分区做镜像,生成镜像仍然可以虚拟机仿真启动,也节省时间。
- 可选择只备份磁盘分区中已使用的磁盘空间的数据,节省时间,注意这将无法在镜像上进行数据恢复方面的取证分析。
- 支持通过网络制作远程的磁盘镜像
文件
disk2vmdk.exe是制作镜像的主要程序,启动它开始制作本地或远程磁盘镜像。vbox32和vbox64是disk2vmdk生成虚拟磁盘文件功能的动态依赖库。disk2vmdk.exe运行后界面如下图。
d2vagent是制作远程磁盘镜像时,需要复制到远程主机运行的程序,根据远程主机系统是32位
或64位,仅需要复制一个相应的d2vagent文件就可以,运行后界面如下图。配置密码后启动监听TCP端口。本地用disk2vmdk的远程模式和它通讯,在disk2vmdk的磁盘列表中右键,点“远程磁盘”,输入远程主机的IP端口和密码,接下来的操作就与本地模式无差别了。
操作
双击需要镜像的硬盘,会弹出硬盘的分区信息。如果有的分区的数据不想备份就把最前面的勾勾去掉。下图案例表示仅备份引导分区和C盘分区的所有扇区,准备生成vmdk格式的磁盘镜像。如果你不准备在生成的磁盘镜像上进行数据恢复方面的取证分析,还可以把C盘的选项“全部空间”的勾去掉,更节省时间,因为将只对C盘已使用的扇区进行备份并转换成vmdk。
VSS选项表示备份数据前对磁盘分区创建快照(还原点),然后拷贝整个快照,备份数据期间数据改动不会影响镜像内容。配置完成后点确定。
点击开始按钮开始针对C盘创建镜像的任务,左下角显示任务进度
在保存路径下能看到vmdk镜像文件和json配置信息文件,备份完成后,我们可以创建一个虚拟机,来使用这个虚拟磁盘文件当硬盘并启动虚拟机。具体步骤这里就不详细展开了,但值得注意,生成的镜像文件添加了只读属性,这是为了避免意外篡改制作好的镜像文件,例如直接用vmware虚拟机启动磁盘镜像等操作是会导致镜像文件内容产生变动的。如果需要用虚拟机启动该磁盘镜像,需要在启动虚拟机前给虚拟机拍摄快照,否则由于磁盘是只读的会导致虚拟机无法开机,报“权限不足,无法访问文件”、“打不开磁盘”、“找不到磁盘文件”等错误。
总结
通过了解掌握一种虚拟磁盘的组件的接口、windows的磁盘管理接口,以及分区Bitmap的一些知识,咱就可以开发类似disk2vhd这种物理磁盘转虚拟磁盘的功能了。另外远程备份的方式则只是多开发层网络通信,详细可以见源代码。