小工具:释放TrueCrypt加密卷占用空间

有不少好电影(大雾)想加密存放,但是心疼硬盘肿么办,加密盘没有填满的空间都浪费了肿么办!

TrueCrypt支持创建文件大小动态增长的加密盘,其原理是基于NTFS稀疏文件,建立时为0字节大小,有数据写入时才真正往硬盘写文件。

但是这样还不够。如果删除了加密盘中的文件,已经分配的空间并不会自动释放。VHD倒是支持先进行碎片整理,然后压缩以释放浪费的空间,但这样显然就和加密无缘了。

能想到的方法很简单,读取加密盘的空间分配信息,然后将加密盘文件对应的区域释放掉。查阅MSDN发现可以对NTFS稀疏文件使用FSCTL_SET_ZERO_DATA将指定区域设为稀疏并释放占用。

简单写了一段小程序,风格很烂(懒),目前只支持标准512扇区磁盘、FAT32分区。NTFS处理太复杂,需要处理MFT、$Bitmap可能的分块,对于开启NTFS压缩的还需要考虑忽略零碎的簇,写起来起码要1000行。

#include <Windows.h>
#include <stdio.h>
#include <string.h>

void error(char* str)
{
	printf(str);
	printf("按回车键退出。");
	getchar(); getchar();
	exit(-1);
}

void main()
{
	printf("TrueCrypt稀疏文件释放工具v0.1 by gmsj0001\n");
	char buf[64];
	printf("输入加密卷挂载盘符,带冒号:");
	scanf("%s", buf);
	char file[64];
	sprintf(file, "\\\\.\\%s", buf);
	//打开分区,读取FAT表
	HANDLE hDevice = CreateFile(file, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	if (hDevice == INVALID_HANDLE_VALUE)
		error("分区打开失败!");
	char boot[512];
	DWORD dwRead;
	ReadFile(hDevice, boot, 512, &dwRead, NULL);
	if (memcmp(&boot[0x52], "FAT32   ", 8))
		error("不是FAT32分区!");
	int fat_addr = *(short*)&boot[0xe] * 512;
	int fat_size = *(int*)&boot[0x24] * 512;
	int clu_num = (*(int*)&boot[0x20] - *(short*)&boot[0xe] - *(char*)&boot[0x10] * *(int*)&boot[0x24]) / *(char*)&boot[0xd];
	int data_addr = (*(short*)&boot[0xe] + *(char*)&boot[0x10] * *(int*)&boot[0x24]) * 512;
	int clu_size = *(char*)&boot[0xd] * 512;
	int* fat = (int*)malloc(fat_size);
	printf("读取FAT表。。。\n");
	SetFilePointer(hDevice, fat_addr, 0, FILE_BEGIN);	//操作磁盘设备必须是扇区对齐
	ReadFile(hDevice, fat, fat_size, &dwRead, NULL);
	CloseHandle(hDevice);
	//打开文件
	printf("请卸载加密卷,然后输入加密卷文件名:");
	scanf("%s", file);
	hDevice = CreateFile(file, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	if (hDevice == INVALID_HANDLE_VALUE)
		error("打开文件失败!");
	printf("正在释放空间,请祈祷本软件没有bug。。。\n");
	FILE_ZERO_DATA_INFORMATION zero;
	int i;
	for (i = 0; i < clu_num; ++i)
	{
		if (fat[i + 2] == 0)
		{
			zero.FileOffset.QuadPart = 0x20000 + data_addr + (LONGLONG)i * clu_size;	//0x20000为TrueCrypt文件头的长度
			for (; i < clu_num; ++i)
				if (fat[i + 2] != 0)
					break;
			zero.BeyondFinalZero.QuadPart = 0x20000 + data_addr + (LONGLONG)i * clu_size;
			DeviceIoControl(hDevice, FSCTL_SET_ZERO_DATA, &zero, sizeof(zero), NULL, 0, &dwRead, 0);
		}
	}
	CloseHandle(hDevice);
	free(fat);
	printf("搞定,按回车键退出。");
	getchar(); getchar();
}

暂不提供二进制程序,因为我不完全保证此工具是完全bug free的,万一被转载到天涯海角出了问题我扶不起。

使用效果:

以后建立加密盘可以大胆地开空间(当然FAT32不能超过32G),再也不用担心填满后删除造成空间浪费了。

手动编辑$BadClus标记坏扇区

高能警告:本文系给文件系统动手术,意在折腾与学习,实际操作中请务必小心谨慎,最好备份重要文件以及要编辑的扇区,进行每一次编辑操作前请务必确保自己已经了解这一步的原理,否则请在分分钟把整个分区弄丢后执行chkdsk修复的过程中默念100遍“不作死就不会死”。

友情提醒:本文仅记录笔者个人案例,仅说明$BadClus文件相关,不介绍基础知识,请先了解硬盘分区、NTFS文件系统、MFT表等相关知识。

修复文件与定位坏扇区位置

这几天打算调整分区对齐,因为没对齐的分区若要对齐必须全部数据平移而不能借助无损分区工具(所有的簇全部是没对齐的必须全部平移,分区工具只能按簇的单位滑动才能尽量少地移动数据),所以只能考虑在对齐的位置新建分区,将全部数据拷进来。

往移动硬盘拷数据时有一集动画片复制失败,使用WinHex拷贝选区到新文件写入到80%的时候报告错误,新文件不完整。查看新文件的大小在源文件中找到出错位置附近,跳过报告错误的位置将剩余部分手动复制到新文件中,出错的位置粘贴0字节补齐,这样即可得到基本修复的文件(当然没办法完整播放了,但是可以跳过那几秒)。之前用HDDREG修复过硬盘上的一些坏扇区,这次打算尝试通过NTFS的坏簇列表屏蔽。

用WinHex打开分区,找到出错的文件右键-位置-列出的簇,在簇窗口中右键-缩短连续的簇列表,手动计算之前编辑文件时出错的大概位置对应到哪个簇,并定位到这块位置,此时WinHex访问出错,并且应当能报告无法访问扇区的准确扇区编号,将扇区号记录下来,加上分区的起始扇区,即得到硬盘坏扇区的LBA。因为我是要重新分区,所以待重新分区后再用这个LBA计算出新分区的簇号,如果不是重新分区,直接记录簇号即可。

MarkBadClusTool是一个很好的作品,可根据LBA将坏簇填写到$BadClus里,但笔者使用时出现了一些问题,比如填写扇区扩展的数值填0的话程序报告无效数值,填小数目比如1的话程序最后执行完后压根没往$BadClus里写东西,估计是算法有缺陷。笔者硬盘的坏扇区不是机械故障,很小规模的,并且不会自己扩散,所以没必要把周边的好扇区一并屏蔽了。另外就是这个工具每次都要走两遍MFT,作用在于如果坏簇上已经存在文件,将尝试将该文件移走,而笔者已经手动把那个文件弄走并且修复了,默认坏簇现在处于free空间,仅需要把它添加到$BadClus这个文件里即可。因为MarkBadClusTool有提供源代码,遂决定自己折腾一下。

data runs、稀疏文件与NTFS压缩、$BadClus:$Bad研究笔记

对于硬盘上的数据,MFT中以data runs的结构存储文件的簇指针。简单地讲,就是一堆起始簇号-大小pairs的有序集合,这样就能表示一个文件了。pair使用可变长度整数记录,每个pair首字节高4位表示“起始簇号相对于上一块的delta”这个整数的长度,低4位表示“块大小”这个整数的长度。语言说起来比较绕,建议查阅已有文章,有图解的一目了然。

$BadClus的$Bad属性记录坏簇这个知识很容易就在网上找到,但是如果直接按上面的data runs填入坏簇的位置信息,写入后会发现分区丢了,说明“打开的姿势不对”。查阅MarkBadClusTool的代码,发现程序使用DataRunsToSpaceDataRuns“将常规Dataruns数据转换为用来描述空间信息的dataruns”。这句注释写的很不明所以,走读代码后发现这段代码是将坏簇的data runs两端与之间填入正常区域的长度。

原理上讲同为data runs是不应该有多种写法的,查阅了很多资料才得知真正含义:$BadClus:$Bad文件并不是一个将坏簇连接起来的文件,而是一个大小为整个分区的NTFS稀疏文件,其中正常的位置是稀疏文件的0,坏簇的地方指向磁盘上的坏簇。而稀疏簇的表示是将“起始簇号”设为-1,“起始簇号delta”设为0,表现在data runs里就是一个字节的整数长度接上正常块的长度数据。这一点MarkBadClusTool虽然执行的结果对,但写法与注释其实并不正确(那么真正的0簇开始的数据因该怎么写,答案是1x xx 00,见$Boot中的记录)。

顺便聊一下压缩。NTFS压缩是完全基于稀疏文件机制的。data runs只有真正簇(delta_lcn不为0)与稀疏簇(delta_lcn为0)之分,并不在乎存放的数据是压缩的还是未压缩的。数据是否压缩通过64K块是否在末尾有稀疏簇来判断。如果一个64K块尝试压缩后体积并没有减少4K以上,那么该数据依然会以未压缩形式存放,如果减小4K以上,那么就在末尾插入几个稀疏簇补齐到64K。

NTFS的文件属性查看压缩文件对于压缩后没有减小体积的其“占用空间”显示为文件大小,但是从上面的算法来看,有一定的几率是压缩后比未压缩大一个簇的。可实验建立一个刚好为4K的随机数据文件(无法被压缩),NTFS会将其补齐到64K然后尝试压缩,结果自然要比4K多几字节,这样就占用了2个簇的实际数据。

基于稀疏文件的NTFS压缩在对付迅雷下载刚开始创建文件时的卡死有奇效(迅雷脑残不在程序里指定创建稀疏文件。。。)

案例

下面以笔者的案例介绍一下手动修改的过程。

本次要处理的坏扇区为754546051到754546055连续的5个扇区,位于当前D盘,D盘起始扇区为83886080(40G整),持续400G。簇总数为104857599(最后一个簇为保留扇区,最后一个扇区为PBR的备份)。

754546051-754546055对应到D盘逻辑扇区为670659971-670659975,除以8得到簇号为83832496,刚好被包含在一个簇里。

构造整个分区大小的稀疏文件:第0到83832495共83832496个簇的0、第83832496簇开始长度为1的坏簇、第83832497到104857598共21025102个簇的0。写出来为:

04  B0 2E FF 04    41  01  B0 2E FF 04    04  4E D1 40 01    00

定位到$BadClus:$Bad的data runs位置($BadClus+0x168),填写上面的内容,然后将0x124位置的属性长度改为修改后长度,8字节对齐(本例改为0x60),在0x180位置填入MFT记录结尾的FF FF FF FF,在0x18位置填入修改后的MFT记录长度,8字节对齐(本例改为0x188)。然后找到$Bitmap文件,计算第83832496/8=10479062余0字节对应的扇区位置,将该字节第0位置1表示空间已分配。

然后即可写入磁盘。WinHex旧版本不支持DeviceIoControl卸载分区,换用新版本即可。

参考文献

1、MarkBadClusTool及其源代码:http://bbs.pediy.com/showthread.php?t=162539
2、上述源码中layout_ntfs.h中Attribute compression一节的注释
3、$BadClus:$Bad是稀疏文件:http://0cch.net/ntfsdoc/files/badclus.html
4、data run:http://blog.csdn.net/begges/article/details/6699236
5、《数据恢复技术深度揭秘》相关章节

手动编辑启动扇区制作DOS启动盘

为什么要做DOS启动盘?

目前在MS平台下需要涉及到开机引导的工作,如各种的PE系统、U盘工具箱、Win7的OEMBIOS激活等,基本上全部是使用grub4dos作为第一引导,即启动分区中有一个grldr文件。

这个时代需要进DOS操作的应用目前并不多,大多数人都习惯了PE。然而除了个人偏好以外,还是有部分软件需要在DOS下运行,如以前提到的HDD Regenerator。在grub4dos中进DOS并不难,找来io.sys与command.com然后在grub里打chainloader /io.sys就成了。

“老师,能不能给力点?”好的,我们知道grub4dos正如其名字,原本是个在DOS版的grub,名叫grub.exe,开机引导的grldr大概是之后才添加的。那么能不能在U盘或移动硬盘上只装DOS的启动,再在DOS下以一个程序的身份运行grub.exe,DOS–>grub–>chainloader /bootmgr或者DOS–>grub–>kernel /vmlinuz是不是显得NB些?

于是笔者就开始折腾,然后发现这道题还不是那么容易解决——这个问题实际上是如何在高版本系统下搞出一个带Win98引导记录的FAT32硬盘分区。笔者试了几款常用的软件,如USBoot、HPUSBFW,发现都不能如愿,遂只好自己解决。USBoot的HDD模式会把U盘缩水到2G,说明他可能用的是FAT12分区;HPUSBFW不支持选择单个分区,大概会把我500G的移动硬盘清理干净来着。以下内容需要读者了解系统启动的基本知识,诸如MBR、DBR、分区表等笔者不再详细复述。

在DOS/Win9X下,可以用format C: /s命令格式化一个FAT32分区并复制相关引导文件。格式化时分区的DBR将被填写DOS/Win9X的引导代码,即从IO.SYS文件启动。但是在高版本的Windows中,虽然也可以在我的电脑中右键格式化分区,但DBR会被填写另外的代码,对于NT5系统是从NTDLL启动,NT6系统从BOOTMGR启动。NT系统中格式化时也有一个“创建MS-DOS启动盘”选项,但这个选项仅在格式化软盘时有效,格式化硬盘时这个选项是灰色的,而且软盘分区为FAT12格式,其DBR引导代码也与硬盘分区不同。

笔者首次采用的方法是将U盘或移动硬盘映射为vmdk虚拟硬盘,然后使用虚拟机启动Win98的启动盘后format。这样需要注意一个问题,即Win7系统出于安全考虑,直接读写磁盘时会写保护有分区的空间,如需操作写保护的空间,必须使用DeviceIoControl锁定分区,显然,我们拿不到虚拟机软件的代码,所以需要想点办法。笔者想出的办法是用WinHex将移动硬盘MBR扇区结尾的55AA去掉使分区表失效,拔掉重插后Windows便不能加载分区,再用WinHex将其改回来,然后挂到虚拟机上用Win98启动盘进DOS格式化,效果很好。注意必要的时候还要用fdisk将分区设为活动,以及fdisk /mbr填写默认的MBR代码。Win98(DOS7.1)启动盘的镜像可以在此处下载。

“老师,能不能再给力点?”好的,上面的办法有点复杂,要用上虚拟机,还要用改分区表的危险办法避免写保护。我们既然知道MBR–>DBR–>IO.SYS的启动顺序,何不试试直接用WinHex在相应的地方填写引导代码?

笔者找到一篇神文章,详细地dump并分析了启动盘的引导记录。请猛击此处阅读(什么?不懂鸟语?那还学个蛋计算机,洗洗睡吧)。文章分析指出,Win98的DBR代码存在于分区的第一扇区和第三扇区。文章并不能方便地直接把十六进制代码拷出来,不过我们可以在format.com里寻找。

WinHex打开format.com,查找MSWIN4.1,可以查到两条记录,可以看到第一条记录之后不远写着FAT12,第二条是FAT32。找到FAT32的这段扇区数据复制5A-1FD范围的内容(即format.com 39C4-3B67),粘贴到U盘分区首个扇区的相应位置,再往后找到第三扇区的引导代码(即format.com 3D6A-3EEF),粘贴到U盘分区第三扇区的相应位置。U盘MBR也要初始化并设置分区标志为活动分区(80),Partition Manager之类的软件都可以做。

这样即免去了格式化分区而做到了刷写引导代码。最后拷贝io.sys、command.com两个文件,如果需要还可以拷贝himem.sys等文件,即做好了DOS启动盘,将grub.exe拷进来并编辑menu.lst即可进一步启动bootmgr、linux等其他系统。

哦对了,开机时会闪过Win98的蓝天白云,瞬间穿越的赶脚,有助提升逼格。如果不喜欢或者觉得很傻,可以创建0字节的logo.sys,或自建256色位图设计自己的开机画面,高端点亦可直接修改io.sys。

 

删除Win7镜像中ei.cfg以达到选择操作系统的目的(偏执的完美主义版)

这篇文章讲的并不是有关删除ei.cfg这个文件就能选择安装的操作系统这一知识——delete键在哪里这种事情按说是不用教的。

通常情况下,我使用U盘安装系统,也就是把sources目录拷贝到U盘中,至于ei.cfg文件,直接按delete键就ok了。

但是在某些情况下必须用光盘——给Mac装系统的时候。去年某飞临走时需要把他Mac上的win7换成家庭高级版(出国装个盗版的旗舰版win7太高调)。那么需要在光盘镜像里删掉这个文件,这就稍微有点麻烦——我们需要用ultraiso之类的镜像编辑软件把这个文件删除,然后保存一个新镜像刻盘。嗯,广大群众都是这么做的。

但是作为一个处女座程序员。。。。。。。。我表示不能忍受为了删一个文件就要重新往硬盘里写3个G的文件!!!!!!!!!!并且这样出来的文件已经“不再是msdn原始档”,说不定ultraiso往里面留下点什么呢。(事实上,某飞也不能忍受,他最后宁愿从msdn上重新下个原版的家庭高级版镜像。)

能不能直接对ISO文件做个小patch来达到这一目的?当年我们修改试用版的VS2008和VS2010就是直接把新的序列号替换进去的。嗯,可以试一试。。

于是用虚拟光驱加载ISO,用WinHex打开光盘,找到sources目录表,往下翻到ei.cfg,记下在整个磁盘上的偏移。卸载虚拟光驱,winhex打开iso文件,找到这里,把ei.cfg这个文件名改掉一个字符。满怀信心地重新加载,结果发现sources目录只剩下200M,最后一个文件名叫du.dll,看来加载到这里的时候哪里坏掉了。

果然不能投机取巧,下面是技术探讨的正文。

win7-1

以中文x86带sp1版win7镜像为例。如图选中的一段为一条文件记录。为什么这样断句呢,因为目录表的起始扇区也是以00 01 02 00开始的,经常做结构探索的应该有这种直觉的断句方法。

查阅UDF与ECMA 167白皮书,可以找到这个数据的前16字节为Descriptor Tag结构,如下(ECMA 167 3/7.2)。

RBP Length Name Contents
0 2 Tag Identifier Uint16 (1/7.1.3)
2 2 Descriptor Version Uint16 (1/7.1.3)
4 1 Tag Checksum Uint8 (1/7.1.1)
5 1 Reserved #00 byte
6 2 Tag Serial Number Uint16 (1/7.1.3)
8 2 Descriptor CRC Uint16 (1/7.1.3)
10 2 Descriptor CRC Length Uint16 (1/7.1.3)
12 4 Tag Location Uint32 (1/7.1.5)

在这里Tag Identifier等于0x0101即257,查阅ECMA 167 4/7.2.1可知对应的是File Identifier Descriptor结构,翻到ECMA 167 4/14.4章节可找到该结构,如下。

RBP Length Name Contents
0 16 Descriptor Tag tag (4/7.2)(Tag=257)
16 2 File Version Number Uint16 (1/7.1.3)
18 1 File Characteristics Uint8 (1/7.1.1)
19 1 Length of File Identifier (=L_FI) Uint8 (1/7.1.1)
20 16 ICB long_ad (4/14.14.2)
36 2 Length of Implementation Use (=L_IU) Uint16 (1/7.1.3)
38 L_IU Implementation Use bytes
[L_IU+38] L_FI File Identifier d-characters (1/7.2)
[L_FI+L_IU+38] * Padding bytes

该章节中对File Characteristics描述有当该字段设置第2个bit位时代表该文件被删除。太好了,将这个位置的数据改为4,而不需要动下面的文件名了。

刚刚我们改文件名失败的原因相信在上面的结构描述中已经能找到答案:Descriptor Tag结构中有对数据的CRC校验,所以我们刚才直接改文件名就相当于破坏了光盘的结构。经测验,这里的CRC16采用的是XModem算法。http://www.lammertbies.nl/comm/info/crc-calculation.html这个地址可以在线计算各种算法的CRC。我们将修改后的上图0x9d418到0x9d43b共0x24个字节重新计算CRC,结果为0x942e,将这个结果填入0x9d410处。

最后需要计算Tag中除了偏移4以外的15个字节的checksum填入偏移4处,这个用Windows计算器就能算好,改为F4。

这样就改好了。如下图,一共修改了4个字节。

win7-2

保存加载虚拟光驱,成功。

 

参考文献:

Universal Disk Format Specification Revision 2.60  http://www.osta.org/specs/

Standard ECMA-167  http://www.ecma-international.org/publications/standards/Ecma-167.htm