基于 RTF specification v1.7 的 RTF 文件解析及 OLE 对象提取(使用 Python 开发)(基于云计算的电子商务采购技术)

网友投稿 289 2022-08-24


基于 RTF specification v1.7 的 RTF 文件解析及 OLE 对象提取(使用 Python 开发)(基于云计算的电子商务采购技术)

0x01 Office RTF 文件介绍

0x02 根据 RTF Specification Version 1.7 分析 RTF 文件

注:(1)组和控制字是解析 RTF 文件的十分重要的部分,起着框架和支撑作用,在指定算法时需要尤其小心,不然会导致后面的解析出现意想不到的 Bug(2)控制字、控制符、未格式化文本和组是排列组合或者顺序排列的关系,在没有专门说明的情况下不存在附属关系,所以解析时需要注意 (3)对于文档中的控制字在 RTF 文件规范中基本上都会有详细的解释

0x03 制定解析算法

解析 RTF 文件之前所必须要考虑的就是算法问题,好的算法能够精确对 RTF 语法进行分割和识别。

a) 组平衡算法

在解析正常的 RTF 文件时组一定是平衡的,举个例子:​​{ { 1 } { { 2 } } }​​​,最外面的组嵌套着两个小组,第一个组包含文本字符 1,第二个组里面又包含一个组并且这个组包含文本字符 2。虽然说嵌套关系比较复杂,但是组的​​{​​​ 和​​}​​ 始终是一样多的,因为始终需要完成包裹。当然了,这个是正常的文件,谁也不敢保证损坏的 RTF 文档当中的组也是平衡的,所以需要组平衡算法来判断组是否有损坏,代码如下图所示:

# 组平衡判断 def balance(data, seek): local_1 = 0; for i in data: if(i == '{'): local_1 += 1; if(i == '}'): local_1 -= 1; local_1 += seek; if(local_1 == 0): return True; return False;

算法很简单,循环遍历传入的数据 data,假如碰到​​{​​​ 就将 local_1 变量减去 1;同样的假如碰到​​}​​ 就将 local_1 变量加上 1,最后通过判断 local_1 是否为 0 来判断 data 中的组是否平衡。算法中的 seek 起着控制作用,一般情况下传入的是 0。

注:相比于正常的文件,其实损坏的文件也可以进行强制的解析,主要是通过算法修复损坏的组或者直接将损坏的组丢弃掉。由于组修复超出了文章的讨论范围,所以不在多述

b) 控制字和组分离算法

在组平衡的前提下可以开展组解析,组解析算法的目的就是将组分离开并储存在变量当中,由于组与组是嵌套关系所以解析时需要确定解析等级,比如​​{ { 1 } { 2 { 3 } } { 4 } }​​​,在一级解析的情况下为​​{ { 1 } { 2 { 3 } } { 4 } }​​​,二级解析为​​{ { 1 } { 2 { 3 } } { 4 } },{ 1 },{ 2 { 3 } },{ 4 }​​​,三级解析为​​{ { 1 } { 2 { 3 } } { 4 } },{ 1 },{ 2 { 3 } },{ 3 },{ 4 }​​,解析算法如下:

# 核心函数, 将数据解析, 得出控制字和组 def rtfAnalysis(data): # 对数据进行组平衡判断 if(balance(data, 0) == False): print("[-] 解析出错, RTF 组格式损坏"); exit(0); # - 组控制字解析算法: # 根据 RTF 1.7 文档规范以及解析的功能, 对 RTF 数据中的 \ 、 \\ 、 { 、 } 字符对文件进行分割, 达到分离 # 控制字和组的目的, 之后使用 python 独有的切片操作, 将分割后的字符储存在集合中, 最后返回集合, 达到分离 # 并解析 RTF 数据的目的, 本算法可能在效率上不及压入弹出算法 local_1 = 0; local_2 = 0; local_3 = 0; listgroup1 = []; listgroup2 = []; for i in data: if(i == '\\' and local_3 == 0): if(data[local_1 + 1] != '*' and data[local_1 - 1] != '*' ): listgroup1.append(i); listgroup2.append(local_1); elif(data[local_1 + 1] == '*'): listgroup1.append(i); listgroup2.append(local_1); if(i == '{' or i == '}'): local_3 = 1; if(i == '{'): if(local_2 == 0): listgroup1.append(i); listgroup2.append(local_1); local_2 += 1; if(i == '}'): local_2 -= 1; if(local_2 == 0): listgroup1.append(i); listgroup2.append(local_1); local_3 = 0; local_1 += 1; local_1 = 0; local_2 = 0; local_3 = 0; listgroup3 = {}; for list1 in listgroup1: if(local_1 < len(listgroup2) - 1): if(list1 == '\\'): listgroup3[listgroup2[local_1]] = data[listgroup2[local_1]:listgroup2[local_1 + 1]].strip(); elif(list1 == '{'): listgroup3[listgroup2[local_1]] = data[listgroup2[local_1]:listgroup2[local_1 + 1] + 1]; else: if(list1 != '}'): listgroup3[listgroup2[local_1]] = data[listgroup2[local_1]:len(data) + 1].strip(); local_1 += 1; return listgroup3;

上述算法首先使用第一个循环对 data 数据进行判断,如果遇到​​}​​​​{​​​​\​​​ 字符,就将它们的位置放入​​listgroup1​​​ 数组。之后使用第二个循环对​​listgroup1​​​ 数组进行遍历,利用 python 特有的切片操作分离出完整的控制字、控制符和组,并且将结果放入​​listgroup3​​​ 中。最后返回​​listgroup3​​。

注:需要注意的是在第一个循环中的控制字判断的时候需要注意引用控制字,也就是 ​​\*\​​​。同样的在进行组操作的时候需要判断哪一个是组开头,并且哪一个是组结尾,这里使用了 ​​local2​​​ 和 ​​local3​​ 两个变量帮助进行判断。在第二个循环当中的切片操作需要注意切片时不能大于整个 data 数据的大小,否则会发生数组越界异常

c) 组嵌套和控制字查找算法

组嵌套算法和组分离算法不同,组嵌套算法主要是为了识别组与组之间的嵌套关系的,比如分析一个 OLE 嵌入式对象,该对象可能在文件的任何位置(可能和 \rtf 控制字在同一级,也可能在文档区的某个组段落之中),这时就需要确定 OLE 对象所在的组是什么样的嵌套关系。控制字查找算法借鉴了移动窗口算法,使用移动窗口进行查找。代码如下:

# 查找特定的组, 记录其组嵌套关系, 并返回完整的组 def searchGroup(data, findStr): WSize = len(findStr); WSPosition = []; for i in range(len(data)+1): if(i + WSize == len(data) - 3): break; CWData = data[i:i + WSize]; if(CWData == findStr): WSPosition.append(i); local_1 = 0; local_2 = 0; dict1 = {}; for m in WSPosition: grouplist = []; for n in data[0:m]: if(n == '{'): grouplist.append(local_1); if(n == '}'): grouplist.pop(); local_1 += 1; dict1[m] = grouplist; local_1 = 0; return dict1;

上述代码当中的第一个循环用于查找特定的控制字,比如​​/object​​​ 对象控制字,首先计算出对象的大小​​WSize​​​,之后使用​​data[i:i + WSize]​​​ 切片操作将 data 数据中的​​/object​​​ 查找出来,并且将它的位置储存在​​WSPosition​​​ 中。而第二个循环用于确定组嵌套关系,使用的模拟堆栈压入和弹出操作,比如​​{ { } { {\object} } }​​​ 这个例子,​​\object​​​ 被包裹了两个组,因为上面的查找控制字算法得出了控制字的位置,所以进行切片操作,切片完成之后就会变成这个样子​​{ { } { {​​​,之后进行循环遍历操作,如果遇到​​{​​​ 就将其位置压入堆栈,如果遇到​​}​​​ 就将最近压入的​​{​​ 的位置弹出来,所以就可以成功的避开完整的组,同时也得出了外面包裹的组的头位置,最后将这些信息储存在 dict1 中并返回。

注: 需要注意的是,第一个循环的滑动窗口不能越出 data 数据的最大大小,不然会引发数组越界异常

0x04 制定脚本功能

既然解析算法都已经定下来了,那么下面就需要考虑功能性的问题了。RTF 文档中的解析主要是对控制字进行解析,而各个控制字的解释在 RTF 文档格式规范中基本都有所提及,只是格式各不一样。考虑到本脚本主要是简单分析 RTF 文件格式和提取其中的 OLE 对象(如果有的话),所以不必将文档中的所有的控制字都给解析出来。基于此目的得出这样几个功能:(1) -l : 列出所有组和控制字符 (2) -i : 对文件进行基本解析 (3) -o : 解析文件中的 ole 对象。

a) 列出所有组和控制字符

基于控制字和组分离算法,将所有的控制字和组打印出来,并根据传入的参数制定打印的深度,也就是循环解析嵌套组的深度,目前支持 1 和 2 两个等级的深度。

b) 对文件进行基本解析

基本解析包括对 RTF 文件头和文档区头的基本解析,解析出诸如 RTF 版本信息、字符集、Unicode RTF 版本、创建时间、作者信息以及打印出各个控制字。

c) 解析文件中的 ole 对象

基于控制字查找算法,查找出本文档是否含有 OLE 嵌入式对象格式,如果含有的话,提炼出完整的 OLE 对象,并且打印出对象的基本信息和输入输出流。

0x05 编写脚本

a) 使用 printAllobj() 函数打印所有的组和控制字符

代码如下所示,根据解析的深度使用 rtfAnalysis 函数获取组和控制字的集合,并依次将它们打印出来。

# 根据解析的结果打印组和控制字def printAllobj(data, level): # 该函数主要为了打印出二级和三级列表下的组或控制字, 完成对 RTF 文件的基本解析 print(" [+] 开始解析: "); print(" [*] 注: \">\" 代表一级组列表下的或控制字 ; \">>\" 代表二级列表下的组或控制字 ; \">>>\" 代表三级列表下的组或控制字"); print(" ") group = rtfAnalysis(data); local_1 = 0; local_2 = 0; local_3 = 0; for i in group: local_1 += 1; if(group[i][0] == '{'): print(" >>>", group[i][0:30], "......", group[i][-30:]); listgroup = rtfAnalysis(group[i][1:-1]); for m in listgroup: local_2 += 1; if(listgroup[m][0] == '{'): print(" >>", listgroup[m][0:30], "......", listgroup[m][-30:]); if(level == "2"): listgroup1 = rtfAnalysis(listgroup[m][1:-1]); for n in listgroup1: local_3 += 1; if(listgroup1[n][0] == '{'): print(" >", listgroup1[n][0:30], "......", listgroup1[n][-30:]); else: if(listgroup1[n][1] == '\''): pass else: print(" >", listgroup1[n]); else: if(listgroup[m][1] == '\''): pass else: print(" >>", listgroup[m]); else: if(group[i][1] == '\''): pass else: print(" >>>", group[i]); print("\n [+] 解析完毕"); print(" [+] 结果:"); print(" [>] 一级对象或控制字总个数:", local_1, "|", "二级对象或控制字总个数:", local_2, "|", "三级对象或控制字总个数:", local_3); print(" [>] RTF 对象格式完好, 不存在缺失状况\n"); print(" [+] 如需要更详细的了解某些控制字和组, 可以参照 RTF 规范文档");

b) 使用 printInfo() 函数对文档进行基本解析

代码如下所示:

# 打印 RTF 文件基本信息def printInfo(data): group1 = rtfAnalysis(data); if(group1[0][0:5] != "{\\rtf"): return False; group2 = rtfAnalysis(group1[0][1:-1]); group1 == {}; for i in group2: if(group2[i][0] == '{'): break; group1[i] = group2[i]; RTF_characterSet = {"\\ansi":"ANSI (默认)", "\\mac":"Apple Macintosh", "\\pc":"IBM PC code page 437", "\\pca":"IBM PC code page 850"}; RTF_deffont = {"\\stshfdbch":"远东字符", "\\stshfloch":"ASCII字符", "\\stshfhich":"High-ANSI字符", "\\stshfbi":"Complex Scripts (BiDi)字符"}; RTF_group = {"\\fonttbl":"字体表", "\\filetbl":"文件表", "\\colortbl":"颜色表", "\\stylesheet":"样式表", "\\listtable":"编目表", " \\*\\rsidtbl":"RSID", "\\*\\generator":"生成器"}; # 开始解析 RTF 头部信息 print(" > 开始解析:\n\n # 文件头部信息:\n"); # 解析 RTF 版本 list1 = []; for i in group1: list1.append(group1[i]); rtfN = list1[0]; rtfVersion = rtfN[rtfN.find("\\rtf") + len("\\rtf"):]; print(" [+] RTF 版本号:", rtfVersion); # 解析字符集 local_1 = 0; for m in list1: for n in RTF_characterSet: if(m == n): print(" [+] 字符集:", n[1:]); local_1 = 1; if(local_1 == 0): print(" [+] RTF 文件缺少字符集"); # 解析默认 Unicode RTF for m in list1: if(m.find("\\ansicpg") != -1): print(" [+] Unicode RTF(\\ansicpg):", m[m.find("\\ansicpg") + len("\\ansicpg"):]); # 解析控制字 deffont local_1 = 0; for m in list1: for n in RTF_deffont: if(m.find(n) != -1): print(" [*]", n[1:], ":", m[m.find(n) + len(n):]); local_1 = 1; if(local_1 == 0): print(" [+] RTF 文件缺少 deffont 控制字"); # 解析其他文件头部中存在的可被解析的组对象 print("\n [+] RTF 文件头部被解析出的其他对象: \n"); for m in group2: for n in RTF_group: if(group2[m].find(n) != -1): print(" [+]", n,"(", RTF_group[n], ")"); print(" "); # 开始解析 RTF 文档区信息

c) 使用 printOLE() 函数解析 RTF 文件的 OLE 对象

代码如下所示:

# 解析 RTF 文件的 OLE 对象def printOLE(data): olestr = "\\object"; if(balance(data, 0) == False): print("[-] RTF 文件中的组不平衡, 文件可能损坏, 但不影响分析"); dict1 = searchGroup(data, olestr); # 开始解析 RTF 文件中的 OLE 对象 print("# 开始解析 RTF 文件中的 OLE 对象...\n"); # 打印 RTF 文件中的对象个数 count = 0; for i in dict1: count += 1; print(" [+] RTF 文件中的 OLE 对象个数:", count); # 根据对象的首位置取出完整的对象 count = 1; count1 = 0; ole = {}; for m in dict1: olestart = dict1[m][-1]; olend = olestart; for n in data[olestart:]: if(n == '{' or n == '}'): if(n == '{'): count += 1; if(n == '}'): count -= 1; if(count == 1): break; olend += 1; ole[m] = data[olestart:olend + 1]; # 打印对象的嵌套关系和基本信息 obj_objtype = {"\\objemb":"OLE嵌入对象类型", "\\objlink":"OLE链接对象类型", "\\objautlink":"OLE自动链接对象类型", "\\objsub":"Macintosh版管理签署对象类型", "\\objpub":"Macintosh版管理发布对象类型", "\\objicemb":"MS Word for Macintosh可安装命令(IC)嵌入对象类型", "\\objhtml":"超文本标记语言(HTML)控件对象类型", "\\objocx":"OLE控件类型"}; obj_objmod = {"\\linkself":"该对象为同一文档中的另一部分", "\\objlock":"锁定对象的所有更新操作", "\\objupdate":"强制对象在显示之前更新", "\\*\\objclass":"表示该对象类的文本参数", "\\objname":"表示该对象名称的文本参数", "\\objtime":"列出对象最后更新的时间"}; obj_objinfo = {"\\objhN":"N是以缇表示的对象的原始高度, 假定该对象具有图形表示特性", "\\objwN":"N是以缇表示的对象的原始宽度, 假定该对象具有图形表示特性", "\\objsetsize":"强制对象服务器将对象尺寸设置为客户端给出的尺寸", "\\objalignN":"N是以缇表示的应该对齐制表位的对象的左缩进距离, 用于正确放置公式编辑器方程", "\\objtransyN":"是以缇表示的对象应该参考于基线垂直移动的距离,用于正确放置数学方程式", "\\objcroptN":"N是以缇表示的顶端裁剪值", "\\objcropbN":"N是以缇表示的底端裁剪值", "\\objcroplN":"N是以缇表示的左裁剪值", "\\objcroprN":"N是以缇表示的右裁剪值", "\\objscalexN":"N是水平缩放百分比", "\\objscaleyN":"N是垂直缩放百分比"}; obj_objdata = {"\\objdata":"该子引用包含了特定格式表示的对象数据,OLE对象采用OLESaveToStream结构,这是一个引用控制字", "\\objalias":"该子引用包含了Macintosh编辑管理器发行对象的别名记录,这是一个引用控制字", "\\objsect":"该子引用包含了Macintosh编辑管理器发行对象的域记录,这是一个引用控制字"}; obj_objres = {"\\rsltrtf":"如果可能, 强制结果为RTF", "\\rsltpict":"如果可能, 强制结果为一个Windows图元文件或者MacPict图片格式", "\\rsltbmp":"如果可能, 强制结果为一个位图", "\\rslttxt":"如果可能, 强制结果为纯文本", "\\rslthtml":"如果可能, 强制结果为HTML", "\\rsltmerge":"无论获取任何新的结果均使用当前结果格式", "\\result":"结果目标在\\object目标引用中可选"}; count = 1; count2 = 1; for k in dict1: print("\n ======================================================================================================================= \n"); print(" [*] 开始解析第", count, "个对象 (对象处于文件的位置:", k,") : "); print(" [1] 对象的嵌套关系:"); print(" [!] 一级对象嵌套二级对象, 二级对象嵌套三级对象, 以此类推"); for i in dict1[k]: print(" ", count2, "级对象 ", data[i:i + 40]); count2 += 1; print("\n [+] 当前 OLE 对象处于", count2 - 1, "级嵌套中"); count2 = 0; # 打印对象基本信息 print("\n [*] 对象的基本信息:"); CWord = rtfAnalysis(ole[k][1:-1]); for m in CWord: # 解析对象类型 for n in obj_objtype: if(CWord[m] == n): print(" [+] 对象类型:", n, "-", obj_objtype[n]); # 解析对象信息 for n in obj_objmod: if(CWord[m].find(n) != -1): if(CWord[m].find("\\*\\") != -1): print("\n [!] 对象信息:", n, CWord[m].split()[1][:-1], "-", obj_objmod[n]); else: print("\n [!] 对象信息:", n, "-", obj_objmod[n]); # 解析对象尺寸、位置、裁剪与缩放 for n in obj_objinfo: if(n.find("N") != -1): ki = n[0:-1]; seek = CWord[m].find(ki); if(seek != -1): if(n.find("N") != -1): print(" [+] 对象尺寸、位置、裁剪与缩放:", n, " N =", CWord[m][len(n):], "(", obj_objinfo[n], ")"); else: print(" [+] 对象尺寸、位置、裁剪与缩放:", n, CWord[m][len(n):], "-", obj_objinfo[n]); # 对象数据 for n in obj_objdata: if(CWord[m].find(n) != -1 and CWord[m][0] == "{"): print(" [!] 对象数据:", n, "-", obj_objdata[n]); # 对象结果 for n in obj_objres: if(CWord[m].find(n) != -1): print(" [!] 对象结果:", n, "-", obj_objres[n]); # 打印对象数据 length = 0; print("\n [*] 对象输入数据(\\objdata):"); for i in CWord: if(CWord[i].find("\\objdata") != -1): length = len(CWord[i]); print(" ", CWord[i][0:400], "......", CWord[i][-400:]); print("\n [*] 数据大小: ", length, " 文件中的位置: ", i); length = 0; print("\n [*] 对象输出数据(\\result):"); for i in CWord: if(CWord[i].find("\\result") != -1): length = len(CWord[i]); print(" ", CWord[i][0:400], "......", CWord[i][-400:]); print("\n [*] 数据大小: ", length, " 文件中的位置: ", i); count += 1;

0x05 脚本测试

a) 打印所有组和控制字

b) 对文件进行基本解析

c) 解析文件中的 ole 对象

其实脚本测试的时候不一定使用正常的 RTF 文档进行解析,也可以使用各种畸形的文件进行测试

python 脚本共享地址(百度云):​​文件规范 v1.7:​​https://pan.baidu.com/s/11w-xGVoguP-jtpJ6ocpwSA(提取码:9xar)​​测试文档:​​https://pan.baidu.com/s/1KfEwC5UPmQCLtxJ8wzSlvg(提取码:gz84)​​

本文到此结束,如有错误,欢迎指正


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Python中的异常处理(python中的异常处理有哪些)
下一篇:Java synchronized同步方法详解
相关文章

 发表评论

暂时没有评论,来抢沙发吧~