【前期信息】

    山东某公司,企业规模也不算小,但不是IT公司,所以未做很好的数据存储规划。公司的重要数据通过WINDOWS网络共享放在一台PC上,同时也连接打印机,有很多人员直接拷贝数据文件到这台PC上打印。

    前几天,忽然F盘的所有文件均无法打开,表现为:

        1、文件名称,时间,路径完全正确,磁盘占用空间也正确。

        2、所有的jpg图片文件打开都提示:“windows照片查看器无法打开此图片,因为照片查看器不支持此文件格式,或者您没有照片查看器的最新更新”

        3、所有的doc打开时都提示:"请选择使文档可读的编码",选择任何一个编码后文件都是错误的。

        4、所有的docx打开时都提示:"无法打开文件,因为内容有错误"

        5、所有的xls打开时都提示:“您尝试打开的文件的格式与文件扩展名指定的格式不一致,打开文件前请验证文件没有损坏且来源可信”

        6、所有的xlsx打开时都提示:"您无法打开文件,因为文件格式或文件扩展名无效,请确定文件未损坏,并且文件扩展名与文件的格式匹配"

        7、所有的PDF文档打开时均提示:“打开文档时发生错误,文档已损坏且无法修复”

        8、其他所有类型文件均无法正常打开。(键入上述错误提示的目的在于网友可以根据错误提示搜索到本文,没有稿费,不凑字数) 

    用户首先通过网上下载的一些数据恢复软件进行恢复,但没任何结果。之后先后送修给当地几家数据恢复,均告之无法恢复,或需要送至北京处理,用户一想,反正是要送北京的,数据也非常重要(事后用户告诉我们,这些数据文件如果恢复不了,损失至少在几十万元),于是,就向北京的数据恢复公司进行咨询。

     我们的分析和初检结论大致是这样的:

        1、确定存储是否RAID。得到的结论是只有一块500G单盘,排除可能的RAID旧盘同步后出现这种故障的可能。

        2、确定硬盘是否有物理故障,比如是否有异响,是否访问缓慢,是否提示IO错误等。得到的结论是除了F盘,其他分区数据完全正常,硬盘在其他数据恢复公司检测也无物理故障。排除因物理故障(如硬盘固件缺陷表错误,坏道等)导致的类似故障。

        3、确定是否采用加密。得到的结论是无启用过任何加密。排除一些加密系统丢失加密链后直接访问密文导致的类似故障。

        4、确定是否采用第三方软件做过分区大小调整、合并。得到的结论是没有。排除因PQ(Norton PartitionMagic)、DiskGenius, Acronis Disk Director Suite等软件调整、合并分区出错导致的类似故障。

        5、确定是否操作系统故障。得到的结论是重装系统杀毒后依然。排除因病毒挟持所有可执行文件或加入文件系统拦截层导致的类似故障。

        6、无其他异常操作。于是推断故障可能因病毒或***导致。

    因之前遇到过多起类似案例,通常这种破坏并不复杂,而且可逆(一些不道德的小***会按此敲诈用户)。与用户沟通后,用户将硬盘送至北亚数据恢复中心。

【确定方案】     

    将硬盘连接至安全的操作环境中(不加载盘符,不自动写数据,保证完全只读),发现文件系统底层上完全正常,但数据区全部错误。以一个PDF文件为例,在WINHEX中打开时如下图:

    一个正常的PDF文件,二进制结构一定是以0x46445025(即ASCII的“%PDF”)做为开头标志。这个文件的开头以0x71736712开始。两者比较,显然是一种异或转换,通过计算,两者相差(异或)0x37。观察本PDF文件的尾部,发现同样做了篡改。

    于是,在WINHEX中选中文件所有内容,对选中块以0x37做字节异或(xor):

    保存出来后,打开,文件正常。

    接下来对其他文件做分析,发现篡改的算法均是全部文件对某个值xor,但此值不确定,按字节概率计算,应该有256种可能,加上文件数量及类型众多,显然不能手动进行修正。需要分析其xor加数的生成规律。

    分析过程如下:

        1、推断是否与路径相关:在同一路径下打开不同的文件分析篡改的异或加数,发现不尽相同,排除。

        2、推断是否与文件名称相关:查找所有文件,按名称排序,找到相同文件名称但大小不同的文件,打开后分析篡改的异或加数,发现不相同,排除。

        3、推断是否与类型相关:找到同一类型的几个不同文件,分析篡改的异或加数,发现不相同,排除。

        4、推断是否与存储的物理位置相关:在WINHEX中按不同文件起始位置进行分析篡改的异或加数,未发现相关性,排除。

        5、推断是否与文件头部相关:查找头部相同的文件(有同一文件的不同更新,头部是相同的),进行分析,也排除。

        6、推断尾部相关的可能性不大。(当然如果后面分析仍无法得到规律,则需返回此项再做验证)

        7、推断是否与文件创建时间相关:分别查找相同创建时间、相同访问时间、相同最后一次访问时间的2个文件,进行分析,发现与此无关,排除。

        8、推断是否与大小相关:简单验证后,未举出反例推翻,但需要完全证明与大小相关,同时要得到算法,需要有足够多的样本。

对是否与大小相关的验证

    首先通过命令方式打印所有文件的大小,WINDOWS很不擅长此操作,改用LINUX处理:    

 

find ./  |xargs ls -ld 2>/dev/null|awk '{printf($5"\t\t"$9"\n");}' >../list.txt 

    之后用excel打开此列表文件,如下图:

     因篡改的异或加数只有一个字节,故推断,如果与大小相关,极有可能是对文件大小mod 256后关系对应,于是在excel中计算所有文件大小值 的mod 256,如下图:

    对mod 256的值进行排序,excel可能可以直接实现,不过,至少可以复制整列,再以数字方式粘贴:

    排序后如下图:

    对相同mod 256的文件进行篡改验证,未发现不符合规律者,基本断定篡改值与文件大小mod 256的值存在完全映射关系。

    对所有可能做抽样分析后,得到篡改异或加数的生成规律:

    至此,篡改算法得到,同时修正算法也自然就容易多了。

【解决方案】   

通过VS2010下编写程序解决,修复程序源码如下:

 

 
  1. //北亚数据恢复中心,张宇,www.datahf.net 
  2. //文件夹遍历算法,来源于互联网,仅做了简单修正。 
  3. #include "stdafx.h" 
  4. #include <iostream> 
  5. #include "windows.h" 
  6. #include <string.h> 
  7. using namespace std; 
  8.  
  9. #define BUFFSIZE (256*1024) 
  10. byte buff[BUFFSIZE]; 
  11. bool xor_file(LPCTSTR swfile) 
  12.     LARGE_INTEGER liSize; 
  13.     HANDLE hFile; 
  14.     hFile = CreateFile(swfile, 
  15.         GENERIC_READ|GENERIC_WRITE,  
  16.         FILE_SHARE_READ|FILE_SHARE_WRITE, 
  17.         NULL,  
  18.         OPEN_EXISTING,  
  19.         FILE_FLAG_SEQUENTIAL_SCAN, 
  20.         NULL); 
  21.     if(hFile == INVALID_HANDLE_VALUE) 
  22.     { 
  23.         _tprintf(_T("%s canot open!\n"),swfile); 
  24.         return false
  25.     } 
  26.     GetFileSizeEx(hFile,&liSize); 
  27.     int t = liSize.QuadPart % (long long)256; 
  28.     int xor_value = 0; 
  29.  
  30.     if( (t>=0) && (t<9)) 
  31.         xor_value += (t+0x37); 
  32.  
  33.     else if((t>=9) && (t<73)) 
  34.         xor_value += (t-9+0xC0); 
  35.  
  36.     else if((t>=73) && (t<137)) 
  37.         xor_value += (t-73+0x80); 
  38.  
  39.     else if((t>=137) && (t<201)) 
  40.         xor_value += (t-137+0x40); 
  41.  
  42.     else 
  43.         xor_value += (t-201+0x00); 
  44.  
  45.     DWORD nb= liSize.QuadPart / (long long) BUFFSIZE; 
  46.     DWORD tb= liSize.QuadPart % (long long) BUFFSIZE; 
  47.     DWORD bRead; 
  48.     DWORD bWrite; 
  49.     DWORD i; 
  50.     for(i=0;i<nb;i++) 
  51.     { 
  52.         ReadFile(hFile,buff,BUFFSIZE,&bRead,NULL); 
  53.         if(bRead != BUFFSIZE) 
  54.             _tprintf(_T("%s canot read,Pos:%I64d!\n"),swfile,i*(long long)BUFFSIZE); 
  55.         for(int ii=0;ii<BUFFSIZE;ii++) 
  56.             buff[ii] ^= xor_value; 
  57.         SetFilePointer(hFile,-BUFFSIZE,NULL,FILE_CURRENT); 
  58.         WriteFile(hFile,buff,BUFFSIZE,&bWrite,NULL); 
  59.         if(bWrite != BUFFSIZE) 
  60.             _tprintf(_T("%s canot write,Pos:%I64d!\n"),swfile,i*(long long)BUFFSIZE); 
  61.     } 
  62.  
  63.     { 
  64.         ReadFile(hFile,buff,tb,&bRead,NULL); 
  65.         if(tb != bRead) 
  66.             _tprintf(_T("%s canot read,Pos:%I64d!\n"),swfile,i*(long long)BUFFSIZE); 
  67.         for(int ii=0;ii<tb;ii++) 
  68.             buff[ii] ^= xor_value; 
  69.         SetFilePointer(hFile,-tb,NULL,FILE_CURRENT); 
  70.         WriteFile(hFile,buff,tb,&bWrite,NULL); 
  71.         if(tb != bWrite) 
  72.             _tprintf(_T("%s canot write,Pos:%I64d!\n"),swfile,i*(long long)BUFFSIZE); 
  73.     } 
  74.     CloseHandle(hFile); 
  75.     return true
  76. void TraverseDirectory(TCHAR Dir[MAX_PATH]); 
  77. bool select_file(LPCTSTR Dir,WIN32_FIND_DATA &FindFileData) 
  78.     if((FindFileData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)!=0&&_tcscmp(FindFileData.cFileName,L".")==0||_tcscmp(FindFileData.cFileName,L"..")==0)        //判断是文件夹&&表示为"."||表示为"." 
  79.     { 
  80.         return false
  81.     } 
  82.     TCHAR DirAdd[MAX_PATH]; 
  83.     StringCchCopy(DirAdd,MAX_PATH,Dir); 
  84.     StringCchCat(DirAdd,MAX_PATH,TEXT("\\")); 
  85.     StringCchCat(DirAdd,MAX_PATH,FindFileData.cFileName);       //拼接得到此文件夹的完整路径 
  86.  
  87.     if((FindFileData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)!=0)      //判断如果是文件夹 
  88.     { 
  89.         TraverseDirectory(DirAdd);                                  //实现递归调用 
  90.     } 
  91.     if((FindFileData.dwFileAttributes&FILE_ATTRIBUTE_DIRECTORY)==0)    //如果不是文件夹 
  92.     { 
  93.         _tprintf (_T("%s\n"), DirAdd); 
  94.         xor_file(DirAdd); 
  95.     } 
  96.     return true
  97. //传入要遍历的文件夹路径,并遍历相应文件夹 
  98. void TraverseDirectory(TCHAR Dir[MAX_PATH])     
  99.     WIN32_FIND_DATA FindFileData; 
  100.     HANDLE hFind=INVALID_HANDLE_VALUE; 
  101.     TCHAR DirSpec[MAX_PATH];                  //定义要遍历的文件夹的目录 
  102.     DWORD dwError; 
  103.     StringCchCopy(DirSpec,MAX_PATH,Dir); 
  104.     StringCchCat(DirSpec,MAX_PATH,TEXT("\\*"));   //定义要遍历的文件夹的完整路径\* 
  105.  
  106.     hFind=FindFirstFile(DirSpec,&FindFileData);          //找到文件夹中的第一个文件 
  107.  
  108.     if(hFind==INVALID_HANDLE_VALUE)                               //如果hFind句柄创建失败,输出错误信息 
  109.     { 
  110.         FindClose(hFind);  
  111.         return;   
  112.     } 
  113.     else  
  114.     { 
  115.         select_file(Dir,FindFileData); 
  116.         while(FindNextFile(hFind,&FindFileData)!=0)                            //当文件或者文件夹存在时 
  117.         { 
  118.             select_file(Dir,FindFileData); 
  119.         } 
  120.         FindClose(hFind); 
  121.     } 
  122.  
  123. int _tmain( int argc, TCHAR *argv[] ) 
  124.     locale loc( "chs" );                //支持中文输出,否则wchar可能无法输出值为中文的变量 
  125.     cout.imbue( loc ); 
  126.     if( argc != 2 ) 
  127.     { 
  128.         _tprintf(_T("Usage: %s [workdir]\n"), argv[0]); 
  129.         return -1; 
  130.     } 
  131.     _tprintf (_T("work dir is %s\n"), argv[1]); 
  132.  
  133.     TraverseDirectory(argv[1]);  
  134.  
  135.     return 0; 

【验证】

    程序运行完成后,对文件进行抽检,无报错,为进一步确定可靠性,查找所有JPG文件,显示缩略图,无异常。

查找所有doc文件,显示作者,标题(这两个信息是通过内容部分得到的),未发现异常(只是OS盗版的痕迹挺重,呵呵),至此,确定算法正确。数据恢复完成。