
hanniba911@yahoo.com.cn
引言:问题的提出 微软的office系列软件是目前使用最广泛的文字处理软件,工作中我们也会经常要去分析一些office,从中读 取一些相关的信息为数据取证工作服务。 最近我们就遇到这样一个情况:某个word文档里有一幅图片,想知道图片的编辑时间,作者,编辑软件,照 片拍摄情况等相关信息。我们知道如果是一个单一的jpg文件,在文件头会有一些相关的信息的,但是当一个jpg图 片被放入word中后,相关信息还在吗?如果在的话,这些信息又在哪里呢? 要知道问题的答案,我们不得不从word文档的文件格式说起: 一、基于流的普通文件和基于对象的复合文件 office系列软件所产生的文档是Microsoft Compound Document(微软复合文档)的一种,其文档格式与微 软复合文档的文档格式一致。(注:微软复合文档在WINDOWS操作系统中有着广泛的应用,比如WIN_XP操作系 统中的Thumbs.db文件也是一种微软复合文档)。 微软复合文档是结构化的储存文件,这种文件由微软的OLE Structured Storage API来创建和管理。 1. 文件里的文件系统("file system within a file") 先让我们考虑这样一个问题:一个文件中怎样才能即存储文本又存储图片同时又能存储其他如音频,视频信息 呢? 这个问题并不想它看上去那样没有水平:我们知道,一个文件,无论它是程序,文本,图片,还是其他的什 么,最终都要转换成二进制流存储在硬盘上的。我们打开一个文件时,计算机会自动去检查文件的扩展名或者文件 头部的一些特定的信息,以确定这个文件是什么文件,然后用相应的程序去解释文件中的数据,最后把结果显示在 屏幕上,这样计算机用户就感觉一个文件被“自动的”打开了。但是问题来了,一个文件总是要先被确定为某一类 型的文件,然后才能调用不同的程序去解释文件里的内容。但是一个文件怎么能又是文本型的又是图片型的(或者 其他什么组合的)呢? 应用程序又怎么知道该在什么时候去解释哪里的数据呢?? 让我们假设这样一个解决方案:如果要实现一个文档中既有文本又有图片的,那么我们建立两个文件,一个放 图片,另一个放文本,然后我们在建立一个文件,在第三个文件中我们存放前面两个放文本和图片的文件在硬盘上 的位置,并记录上文本被相关的应用程序解释后应该出现的位置,和图片被相关的应用程序解释后应该出现的位 置。当然文本和图片文件中的数据的解释还是要靠相应的应用程序,但是当我们打开第三个文件时,靠第三个文件 中记录的信息,系统就能调用不同的程序,解释前两个文件中的内容,并把解释出来的内容显示在屏幕正确的位置 上了。前两个数据文件内容的在屏幕上一叠加,我们就好像能看到一个既有文本又有图片的文件了(指文件三)。 好了,我们再进一步:把这三个文件放在一个文件中。但是这已经不是原来简单的单一文件了。为了表示区别,这 里原来的文件我们称之为 “stream”。 <---------------------------------------------------------------------> 文件一(stream1) 文件二(stream2) 文件三(stream3) 我们把三个文件依次排列在一个文件中,文件一中除了记录原来的信息外还记录了文件二、文件三在文件中的 起始位置和长度。这样,这个文件就实现了在一个文件中即存储文本又存储图片,同时又是在硬盘上只存在一个文 件,而不是上例中的三个文件。 再进一步,我们把stream1(就是原来的文件一)再分成n个文件,一个放作者等属性信息,一个放解释出来 的信息应该显示的位置,一个放stream2(就是原来的文件二)、stream3(就是原来的文件三)在文件中的起始 位置和长度。。。。。。 是的,这就是复合文档的雏形了,只是:这么多stream太乱了,管理使用都不是很方便!于是我们效仿硬盘 上的FAT文件系统,把各个stream再当成文件管理起来。 但是问题又出现了:正好这样的文件中还可能有另一个类似的文档B当作一般的文档链接进来原有的文档A (就像word中可以嵌入一个excel表),如果再要把文档B拆成M个小文件(现在你不妨称之为stream)放入A 中,那A中的stream就太多了,不利于管理。怎么办呢? 别忘了,我们现在已经把复合文件看成一个小小的文件系统了,文件系统里除了文件还有文件夹呢!如果有类 似的复合文件连进来,不妨把它作为一个文件夹(storage)放进来,对storage中的数据解释再按复合文件的规 则重复一遍。 嗯,好,这就是复合文件的结构模型了。 文档是以“红黑树”的数据结构组织的。每个文件都有且只有一个根目录,根目录下可以有若干个stream和 storage,stream中存放数据,而storage中存放其他stream和storage。 我们可以用SSFView.dll来看word文档的这些stream。当然,你也可以用VC自带的“DFVIEW.EXE”,但是 后者不具备数据导出功能。另外,DocFileView.exe程序也可以查看一个Word文档的Stream。 SSFiew.dll: http://www.codeproject.com/macro/ssfview.asp 二、stream的本质和结构 好吧,现在你已经知道stream就是office文档(准确的说的Mircosoft复合文档)的基本组成结构,你也已经 知道stream完成了一些文件的功能——记录信息,但是你还是不知道stream到底是什么? 让我们回顾“遥远的”win3.1x时代,当时OLE(对象的链接与嵌入技术)就作为动态数据交换的一种运送信 息的载体,而被引入win32系统中了。动态数据交换是“动态剪贴板”的一种,通过它各种应用程序可以向全局可 访问区域接受或发送数据。 实际上这些stream就是由上述的这些对象或对象流构成的,比如: data stream中是图片数据,WordDocument stream中是文本数据,SummaryInformation和 DocumentSummaryInformation stream中是摘要信息,等等。 你可以回想一下:为什么在一台没有安装office软件的windows的机器上,计算机能够读出office文档的属性 信息(可以通过在文件上点右键->属性->摘要->高级)。熟悉文件系统的朋友一定会发现:那些信息决不是文件 系统中所记录的信息!为什么? 那是因为在没安装office的windows系统上,操作系统仍然能够读取并分析出SummaryInformation和 DocumentSummaryInformation stream中的摘要信息其实是一种属性设置信息。而属性设置信息的格式是 windows自带的(即不是office专用的),因此不用安装office也能读取并显示出来。 关于属性设置信息的详细分析见: http://www.kyler.com/index.php?option=com_content&task=view&id=21&Itemid=47 1. 链表、链表、还是链表 当你把data stream导出以后你一定是很失望的,因为,虽然你已经知道了它是图片对象,但导出后的数据在 你看来还是一堆乱码,而且你手头的哪个程序都不能打开它了。不过别紧张,我们万里长征只差最后一步了。 经过简单的debug和测试我们发现一个含n个图片的Data stream的结构如下: Object1 Object2 Object3 。。。。。。 Object n 而每个Object的结构如下: 下一个Object的指针(如果是最后一个Object,则指向文件尾部) + 一些控制信息 + Jpg文件的二进制流 这是典型的链表结构。 好吧,现在你已经知道所有的秘密了,让我们回到最初的问题:当一个jpg图片被放入word中后,相关信息还 在吗?如果在的话,这些信息又在哪里呢? 答案是,当一个jpg图片被放入word中后,相关信息还在,信息就位于Data stream中各个Object的Jpg文件 的二进制流中,一点也没少。 这无疑是一个好消息:我们只要把jpg文件直接从各个Object中取出来,就是原来的各个Jpg文件了,然后我 们就可以用我们熟悉的分析jpg文件的工具去分析它,读取相关信息了。 附:从各个Object中取出Jpg文件的程序 好像还没人写过相应的程序(至少我没看到过,谁看到了告诉我一下啊),那我们就用VC写了一个实现代码吧, 很烂,就是一般实现一下,不嫌弃的话,可以参考一下:(我的程序里把data stream的导出结果硬性规定为 “text.ole”你如果想要写一个通用程序的话,可能要改进很多地方) 代码: /************************************************** 分解data stream中jpg文件的程序 先用文件中的指针把一个大的data stream分解成n个jpg 的object,然后从object中提取jpg图片. 本程序在windowsXP+SP2环境下,使用VC6.0编译通过 **************************************************/ #include #include using namespace std; int main(){ int Object_num=0; int Next=0,In_file_size,i; fstream In_file; In_file.open("text.ole",ios::in | ios::binary); In_file.seekg(0,ios::end); In_file_size=In_file.tellg(); // cout << "文件大小为:" << hex << In_file_size << endl; In_file.seekg(0,ios::beg); for (;;){ In_file.read((char *)&Next,4); Object_num++; // cout << hex << Next << endl; // cout << "现在已经发现" << Object_num << "个object。" << endl; In_file.seekg(((int)Next-4),ios::cur); if (In_file_size==In_file.tellg())break; } // cout << Object_num << endl; //先看一下有几个object In_file.seekg(0,ios::beg); //建立一个指针数组分割object int * point =new int [Object_num]; point[0]=0; //建立输出文件序列 fstream *out_files =new fstream [Object_num]; char (* out_files_name)[6]=new char [Object_num][6]; //准备输出文件文件名 char name_tmp[2]; char extend_name[5]=".jpg"; name_tmp[1]=''; for (i=0;i < Object_num;i++){ name_tmp[0]=(char)i+97; strcpy(out_files_name[i],name_tmp); strcat(out_files_name[i],extend_name); // cout << out_files_name[i] << endl; }//输出文件名准备完毕 for (i=0;i < Object_num;i++){ In_file.read((char *)&point[i],4); // cout << "第" << i << "个文件大小为:" << point[i] << endl; In_file.seekg(((int)point[i]-4),ios::cur); }//准备分割标志 In_file.seekg(0,ios::beg); int j,k; char tmp; bool jpg_switch=false;//设置开关 char Jpg_Magic[3]; Jpg_Magic[0]='xFF'; Jpg_Magic[1]='xD8'; Jpg_Magic[2]='xFF'; //定义了一个jpg头 char Test_head[3]; for (i=0;i < Object_num;i++){ // cout << "进入循环" << endl; out_files[i].open(out_files_name[i],ios::out | ios::binary); for (j=0;j < point[i];j++){ if(!jpg_switch){//未到jpg头就判断是否已到jpg文件头 In_file.read(Test_head,3); for (k=0;k<3;k++){//判断jpg头 if (Test_head[k]==Jpg_Magic[k]){ if (k==2){ jpg_switch=true; In_file.seekg(-1,ios::cur);//多退一个字节 } } else { break; } } In_file.seekg(-2,ios::cur); } else{ In_file.read(&tmp,1); out_files[i].write(&tmp,1); }//否则拷贝文件 // cout << In_file.tellg() << endl; } out_files[i].close(); jpg_switch=false; } cout << "完成!" << endl; In_file.close(); delete [] point; delete [] out_files; delete [] out_files_name; return 0; }