複合文檔格式研究之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

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