复合文档格式研究之07-破解VBA

李懿 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章