李懿 Excel学习 2016-07-05
微信扫一扫
关注该公众号
7、破解VBA
终于还是走到这一步了,再学了之前那么多烧脑的理论知识后,接下去继续烧脑。不过如果能将之前的内容全部都理解的话,到这里已经几乎没有什么难度了,唯一剩下的只有一点补充知识了。为了测试效果,本章我们使用了一个加密的Excel文件,其代码部分如下:
图30 VBA测试文件代码
如果你够勤快,最后可以做成这样一个程序
图31 VBA代码浏览器
为了破解,我们还需要官方文档一份,下载地址如下,可惜是英文的。
https://msdn.microsoft.com/en-us/library/office/cc313094(v=office.12).aspx
那就开始吧。
7.1 VBA文件结构
在官方文档19页,有这样一幅图,画出了所有VBA涉及的Storage和Stream
图32 VBA文件结构
看过上一章我们从文件中读取的Directory
Entry的朋友们一定对这些还有些印象。
图33 Directory Entry示例
这些不是都是属于VBA的内容么?下面我们挑一些重要的:
dir Stream:这是VBA中最重要的一部分,它记录了VBA项目的各种属性、模块的属性等。我们重点中的重点也就是要把这个Stream读出来。
Module Stream:存放了各个模块的代码。
Project Stream:存放了VBA项目的属性,我们设置的VBA查看密码也存在这里。
其他的内容感兴趣的也可以看看,反正我看了,觉得都没啥用。
7.2 读取Stream
先别急着去读取dir,细心的读者一定发现我们还遗漏了什么?没错,那就是如果把Stream分解出来。没错,用Directory中的信息和FAT表,Directory中记录了Stream的Sector起始ID以及Stream的大小,FAT表中记录了Sector的读取顺序,用它们就可以轻松地读取Stream了。
聪明的你一定发现MiniFAT貌似从来没用过。哈哈,还记得最开始在Header中读取的Mini Stream Cutoff Size,其实当时写的有些问题,这个值其实是Mini Stream和正常Stream的分界线。这个值始终是4096。当Stream大于或等于这个大小时,Stream存放在普通的扇区中,而当Stream小于这个数值时,就存放在Mini Stream中了。那么问题来了,Mini Stream存放在哪里?我们翻开Directory Entry看看
图34 Root Entry
看到了啥?我们之前说了Storage相当于文件夹,Stream相当于文件,显然Storage是没有大小的。而这个Root Entry中的SectorStartID和StreamSize其实表示的是Mini Stream所在的Sector的ID,以及Mini Stream总共的大小。
Mini Sector也是连续存放在一般的扇区中,其大小在Header中也有记录,为64个字节。根据FAT和起始扇区的ID,我们可以把所有的Mini Sector扇区的起始地址记录下来。于是乎,自定义一个类型:
Public Type CFBMINIFAT
NextID As Long
StartPos As Long
End Type
其中记录了Next
ID,也就是MINIFAT记录的信息,然后用以下程序来获取所有的MINI
Sector的信息
Sub ReadMiniSector(FS As Integer, _
FAT() As Long, _
MiniFat() As CFBMINIFAT, _
CurPos As Long, _
SectorID As Long, SectorSize As Long, MiniSectorSize As Long)
Dim i As Long
Dim CurOffset As Long
'若SectorID为结束ID,则停止读取
If SectorID = SECTORTYPE.ENDOFCHAIN Or SectorID = SECTORTYPE.FREESECT Then
Exit Sub
End If
'定位
CurOffset = GetFileOffset(SectorID, SectorSize)
For i = 0 To SectorSize / MiniSectorSize - 1
'当前位置加1
CurPos = CurPos + 1
'记录起始位置的地址
MiniFat(CurPos).StartPos = CurOffset
'偏移至下一个地址
CurOffset = CurOffset + MiniSectorSize
Next i
'读取下一个
ReadMiniSector FS, FAT(), MiniFat(), CurPos, FAT(SectorID), SectorSize,MiniSectorSize
End Sub
看看,都是同一个套路。然后再写一下获取普通Stream以及获取Mini
Stream的程序。反正也差不多,就举个例子。
Sub ExtractNormalStream(FS As Integer, _
FAT() As Long, _
Result() As Byte, _
CurPos As Long,TotalLength As Long, _
SectorID As Long,SectorSize As Long)
Dim i As Long
Dim Data As Byte
'若SectorID为结束ID,则停止读取
If SectorID = SECTORTYPE.ENDOFCHAIN Or SectorID = SECTORTYPE.FREESECT Then
Exit Sub
End If
'定位
Seek FS, GetFileOffset(SectorID, SectorSize)
For i = 0 To SectorSize - 1
'当前位置加1
CurPos = CurPos + 1
'读取数据
Get FS, , Data
Result(CurPos) = Data
'若已经读完,则退出
If CurPos = TotalLength Then
Exit Sub
End If
Next i
'读取下一个
ExtractNormalStream FS, FAT(), Result(), CurPos, TotalLength,FAT(SectorID), SectorSize
End Sub
然后可以获取你想要的Stream了。
7.3 解压缩dir
当我们迫不及待地读完dir Stream的时候,然后与文档对比一下,却发现怎么也对不上。注意到文档中有一句
The entire stream MUST be compressed as specified in Compression (section 2.4.1).
翻译过来就是整个Stream都必须以2.4.1中所述的方法进行压缩。然后翻到2.4.1。发现这个压缩技术成为run length encoding,翻译过来叫行程编码。感兴趣可以去搜索一下这个编码的算法,以后我有空也会发布文章解说,反正都说这是个非常简单的算法。
不过我也不打算在这里多说,有一个称为RtlDecompressBuffer的API可以直接用,可以将我们的dir解压缩。不过有个陷阱就是,dir的第一个字节不要加进去解压缩,否则你永远的不到想要的结果。
Private Declare Function RtlDecompressBuffer Lib "NTDLL" _
(ByVal flags As Integer, _
ByVal BuffUnCompressed As Long, _
ByVal UnCompSize As Long, _
ByVal BuffCompressed As Long, _
ByVal CompBuffSize As Long, _
OutputSize As Long) As Long
Public Sub Decompress(Origin() As Byte, Output() As Byte)
Dim Result() As Byte
Dim ResultSize As Long
Dim i As Long
Dim Origin2() As Byte
'声明结果输租
ReDim Result(UBound(Origin) * 100)
'原始数组中去除第一个字节
ReDim Origin2(UBound(Origin) - 1)
For i = 1 To UBound(Origin)
Origin2(i - 1) = Origin(i)
Next i
'解压
RtlDecompressBuffer 2, VarPtr(Result(0)), UBound(Result) + 1,VarPtr(Origin2(0)), UBound(Origin2) + 1, ResultSize
ReDim Output(ResultSize - 1)
For i = 0 To ResultSize - 1
Output(i) = Result(i)
Next i
End Sub
接着你就可以按照文档的格式来“阅读”dir了。
7.4 dir简介
dir 中的数据和我们之前遇到的都不太一样。早些时候,基本上都是固定长度的记录,而dir中的记录都是变长的,而且记录数量不确定,不过这也难不到我们。因为每个记录都有自己独特的ID,通过判断ID,然后编写不同的方法去读取即可。dir中有3种记录,分别是:
-
InformationRecord :项目信息记录
-
ReferencesRecord :引用信息
-
ModulesRecord:模块信息
其中,每个记录中又由若干个不同的记录组成。当把ModulesRecord中的MODULEOFFSET记录的TextOffset读取出来的时候,就可以在Directory中去读取相应的模块Stream,并获取其中的代码了。可悲的VBA,其实是把所有的代码都明码存放的,怪不得一直听人说VBA代码一点都不安全。
至于去除VBA的查看密码,你可以在Project Stream中找到ProjectPassword这么一条,其实很容易就去掉了(以前有个VBA写的去除工程密码的程序就是这么干的)。
至于具体的代码,我就不发了。主要也是不想给伸手党提供什么福利,我想说,请尊重原作者。而对于读完我所有文章并且实践的朋友们,相信后面的也不是什么难事。当你使用自己写的程序成功读取了VBA代码之后,那么我相信其他的VBA代码对你来说根本就不是什么难事了。
7.5 Excel 2007以上格式的文件
有人说了,这个复合文档是2003以下的版本的事,现在都用2007以上版本了,有啥用?我会说对于VBA工程文件而言,其实都是一样的,曾经有个知名的专家还跟我争论这个事,我也懒得扯了。
众所周知,Excel 到了2007以上的版本是一种叫做Open XML的格式。这个格式的特点就是把一堆的XML文件用一个Zip压缩包打包在一起。我们就把一个启用宏的工作簿解压一下。你会在xl文件夹的下面找到这样一个可疑的文件。
图35 Excel 以上版本存放VBA项目的文件
这个称为vbaProject.bin的文件就是存放VBA项目的文件,它是一个复合文档格式,所以读取方法就是以上所有的内容了。
7.6 总结
一路到此,终于把复合文档解说完了,我也有种如释重负的感觉。其实这篇文章早在去年就写了一半了,后来一直很忙就没有继续下去。其实写这篇的目的也是看不惯网络各种泛滥的错误的解释文章。当然,我的水平有限,很多地方还不完善,也不正确,还请各位多多包涵和指教。最后,把微软公布的所有Office文件格式的相关文档奉献给大家,感兴趣的可以使用它们去读取Excel、Word、PPT的内容。
Office File Formats Technical Documents:
https://msdn.microsoft.com/zh-cn/library/office/cc313105(v=office.14).aspx