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