0x00 - 前言
誒嘿,我又拖更了,
畢竟內心真的很不想講解Windows Internal,
以及PE病毒的基礎概念,
首先是因為這真的有難度,
需要有一定的背景知識,
而且你手中的病毒是具有實質的破壞性的。
其中,
所有的主題都會有延伸閱讀的資料,
或是一些cheat sheet可以參考,
而我都會把它們都放在私人公會中,
讓文章中只保留一些基本的資訊,
以防被白嫖或是被支那的內文農場網站爬走。
0x01 - 時代變了,大人!
繼之前的病毒開發系列之後,
我們也必須與時俱進,
把先前對組合語言以及Dos病毒架構的認知,
以更加現代化的語法(winapi)與系統框架(32位元)將其重新詮釋,
並把病毒移植到更新的平臺(win32)上。
由於 com檔本身無法在windows上執行,
因此,我們必須使用更加現代化的檔案格式,
也就是所謂的「Portable Exe(PE)」!
(一般俗稱為Exe檔)
以下是PE檔的檔頭(Header)結構:
好啦,我知道這一切逐漸變得越來越不親民,
但是如果不去了解、接受他,
那你肯定是無★法★進★步★的說~
然而,再教各位如何解析PE檔之前,
我們必須聊聊傳說中的Windows Internal,
系統改動以及Protected Mode的實裝,
而這些重大的改動,造就了Dos病毒的末路。
這邊我就不細說表格的內容了,
有需求的就自己慢慢看吧~(^ ^)
總結來說,
Protect Mode 的推出實裝了:
1. Windows Kernel:
系統被分為:
user space(0x00000000~0x7FFFFFFF)
kernel space(0x80000000~0xFFFFFFFF)
而他們各自被分配了2Gb。
其中使用者與執行的應用程式能接觸到的部分為user space;而Kernel space由Windows Kernel掌管,其中只有Kernel有權限接觸實體記憶體及系統資源。
2. Virtual Memory(MMU):
分離核心與使用者應用程式的關鍵。
3. Memory Paging:
MMU 把實體記憶體分成不同page,各自有4kb的實體記憶體大小。
由於這一切真的很複雜,
難到微軟甚至出了一本3000多頁的書,
叫做《Windows Internal Edition. 7》
專門用來講解Windows Internal的相關概念,
因此,我這邊就只講主要的影響:
1. 核心與應用程式分離
2. 只有核心有權限訪問實體的記憶體
3. 所有的應用程式都會被當成在作業系統上執行的唯一程式,並能夠使用全部的RAM(4Gb、32位元系統的極限),且能夠與全部的記憶體互動。舉個例子,如下圖所見,假設一臺電腦上正在同時執行兩個 notepad.exe,而且他們都要取得存放在記憶體位置0x1234的資料,然而他們卻透過同樣的記憶體位置取得了不一樣的資料。至於為什麼會有這種狀況呢?原因就是我先前提到的Memory Paging的技術,由於他們存在不同的Memory Page之中,因此對應回實體記憶體中的位置會是完全不同的。在這種狀況下,各個程式無法覆寫其他程式存放於記憶體中的資料,且能同時、無衝突的執行在系統上。
0x02 - 終末的Dos病毒
還記得我們以前玩的Dos病毒嗎?
病毒所有的功能,
都是建立在呼叫Dos Interupt之中。
基本上,當病毒程式在Dos中被執行後,
就直接Game Over,停都停不下來,
想調用什麼功能就調用什麼,
想幹誰就幹誰。
然而,
對於執行在Protect Mode中的程式而言,
想呼叫系統功能,你必須透過Win32 Api,
對Windows Kernel提出申請,
代替應用程式存取實體記憶體與系統資源,
而只要Kernel覺得你的請求不合適,
他就有權利拒絕你不合理的要求,
因此你無法跟在Dos中一樣為所欲為。
舉個例子,
假設電腦上同時執行和兩個notepad.exe,
其中Notepad_proc2呼叫了CreateFileW,
使用Win32 Api來開啟readme.txt。
而Notepad_proc1 呼叫了DeleteFile,
想要刪掉readme.txt。
在這種情況下,
由於CreateFileW建立的Filehandle未被關閉,
因此核心會認為兩個process互相衝突,
而拒絕DeleteFile Api的執行請求。
0x03 - 勇者死了!因為勇者掉進微軟這個王八蛋設計的 Win32 Api 裡!
為了讓各位親自體驗看看Win32 api,
首先,去官網下載masm32,
並在任意的Code editor打出以下程式碼,
把他存成 msgbox.asm。
編譯指令:
./ml.exe /c /coff msgbox.asm
./link.exe msgbox.obj
如果你的畫面彈出了一個小小的視窗,
並有著一個「OK」的按鈕的話,
恭喜你!寫出了你的第一個32位元程式!
我簡單講解一下,
裡面呼叫的MessageBox為Win32 api的功能:
而invoke的意義,則類似於call,
屬於Masm內建的macro。
相對於Call指令,
他多了可以在同一行程式碼中,
把參數傳遞到stack中的功能,
讓你的code增加可讀性。
至於各個Win32 Api所需的參數,
以及各自代表的意義,
都可以在微軟的網站上查到,
所以我就不解釋了。
當然,你也可以選擇不用invoke,
堅持用call來呼叫Win32 Api,
但你會需要手動把參數丟進stack中,
並且要以LIFO的規則傳入,
像這樣:
只是你的程式的可讀性會下降,
而且又會讓原始碼暴增5、6倍的大小。
不是不行,只是不太推薦。
體驗完Win32 Api的用法後,
請把剛剛編譯出的exe檔丟進PE Bear,
讓我們來仔細檢視一下檔頭的資訊:
第一張是PE Bear的介面,
第二張圖則是Exe檔的結構,
基本上只要對照一下兩張圖,
你就能看得懂各個區塊的意思,
因此我非常建議各位仔細研究一下這張圖。
接著,
讓我來教各位怎麼計算檔頭的資料位置,
先給各位看看公式:
「 VA = BaseOfImage + RVA 」
VA(Virtual Address):記憶體中的虛擬地址
BaseOfImage:載入記憶體的位置,無法預判,但理論上預設是0x400000。在執行時會被賦予一個數值,但在執行前可以視為0。
RVA(Relative Virtual Address):你就把他當成offset,去做累加就可以了。
舉個例子,
我現在如果想要到0x3c的e_ifanew,
取得 NT_Header 的相對位置的話,
就要到 BaseOfImage + 0x3c 的位置,
並提取記憶體中的數值。
以下圖的狀況而言,
該記憶體的位置中的便是”0xC0”。
而這代表著NT_Header位於:
BaseOfImage + 0xC0
依照這個計算邏輯,
到到達了NT_Header之後,
如果想要到達Image Optional Header的話,
位置則為:BaseOfImage + 0xC0 + 0x18
同理,
BaseOfImage 的真實數值 = [ BaseOfImage + 0xC0 + 0x18 + 0x1C ]。
只要依照表格上的RVA一區一區的進行累加,
你可以取得PE檔頭中的任何資訊。
如果你想確認自己的計算是否有問題,
你可以使用PE Bear中的資料進行比對,
來驗證提取的數值是否正確。