逆向分析之路-從運算碼開始-05


15.3.3 x86指令格式

在這小節得先提到一個有點困難的資訊,稱之為x86指令格式。在Intel® 64 and IA-32 Architectures Software Developer’s Manual官方這份文件中有提到一個指令「最多」由這六個項目所組成的,其中只有運算碼是必須的,其他都是視情況而定,如下圖所示:

如非真的必要,我們盡量少提起複雜的部份,如果一時還不明白也沒關係,之後再回過頭來看就清楚點了。

1.指令前置碼

主要用來輔助說明運算碼,分為4組,每組用1個位元編碼,每組在指令中最多指定1個前置碼,4組的順序可以任意排放。簡單的說,就是你可以如67 66 3E 加上運算碼如8B之類的組合,這在後面我們會舉個這樣的例子說明。


2.運算碼(Opcode)

因為搭配的運算元類型不同,所以佔的長度也不同,他有分長度是1 byte、2 bytes與3 bytes,當運算碼不是1 byte時,就會用0FH當運算碼開頭來加以區分,且由Mod R/M中資訊來擴充表示運算碼。但這個運算碼也有一些特例,會讓你以為是指令前置碼,這個有概念即可。

3.運算元類型(Mod R/M)及比例變址(SIB)

許多指令的記憶體運算元需要利用到Mod R/M位元來找位址。這裡的R就是R暫存器,M就是記憶體的意思,而Mod與R/M組合,共有32種用來表示8個暫存器與24種定址模式。這其中的定址模式中,有可能又要扯到參考後面的比例變址(SIB)來輔助,這是由三個部分組成:比例係數、索引暫存器及基底暫存器,藉由下面公式計算SIB的值:

Base + Index * Scale

而Reg/Opcode可以用來表示某個暫存器,或者拿來當作額外的3 bits運算碼。接下來有三張非常關鍵的圖表,說明怎麼使用Mod R/M和SIB來查出運算碼:

舉個例子來說明怎麼看這幾張圖表,第一張的圖表用在對16位元的定址查詢用,第二張是用在對32位元的定址,第三張在查詢某些需要用到SIB的位址的定址。當得到一個Mod R/M的值如CC,那麼我們找到CC對應的行和列,可以發現,它對應如下:

 

列是 ESP/SP/AH/MM4/XMM4
行是 CL/CX/ECX/MM1/XMM1/1/001

 

那麼就將範圍限定在這幾個暫存器上了,當然,至於它最後要選哪個暫存器,就由opcode來決定。

Mod R/M表其實是個運算元類型表,他原始的編碼是定址模式Mod(如11),加上暫存器或運算碼,又稱Reg/Opcode(如CL就是001),最後加上R/M(如ESP就是100)。

同樣的,對於需要用到SIB表的指令,如上例CC,會發現它對應如下,意即表示SIB最後的值為[ESP]+ECX*8。

列是 [ECX*8]
行是 ESP
SIB定址是 [ESP] + ECX*8 = [ESP + ECX * 8]

最後再補充個定址模式,就講完了該會的前置作業,對於後面我們的分析會很有幫助。

簡單舉兩個例子來說明這小節在告訴我們什麼,第一個例子講解在16位元定址的情況,因為先前提到的內嵌組合語言的方式不允許我們在32位元情況下去直接對16位元暫存器位址存取,所以我們使用本書一開始的組合語言寫法。

範例一:

  1. 首先打開Visual Studio,建立一個Visual C++ Win32主控台應用程式,我們命名為opcode,往後只要是用組合語言寫法,我們都用這個名稱重複修改執行

 

  1. 選擇主控台應用程式,留意要勾選空專案,以免他幫我們自動建立了C++程式。

  1. 在方案下方按滑鼠右鍵,依下圖設定,這很基本的過程,本書都一直在用,要很熟練。

  1. 勾選以masm來幫我們組建。

  1. 使用C++檔來建立一個組合語言的程式,如main,副檔名要自己修改為 .asm。

  1. 在方案下方設定屬性,進入點是 main,因為我們習慣組合語言用 main包裝程式碼。

 

  1. 我們寫了段程式碼,要留意三個地方。

 

第一,因為我們主要用了ds:[bp+si+5]這種16位元的定址模式,得使用small,不能像之前使用flat,否則會出錯。這段程式碼只是造出來為了講解編碼,直接執行會有違規存取的狀況。

第二,除了mov r/m 這指令外,下方還有一個lock,那是為了方便一次講解兩個例子而寫在一起。請設定好中斷點後按下F11功能鍵執行。

第三,就是被反組譯出來的運算碼是不是跟我們預期的樣子。特別是會發現,只要是有用括號[ ]包起來的資料,也就是跟記憶體位址有關的存取,都會被編譯成word ptr或byte ptr形式。這代表著,其實我們在程式碼時沒寫,但實際上有被隱藏了省略的程式語法。

我們把上圖放大來解釋,第一式,由於第一運算元ax他是屬於在32位元環境針對16位元的暫存器存取,因此要用到指令前置碼運算元長度覆蓋66H,而後面的第二運算元也是一樣,要用到位址長度覆蓋67H,但同時他又用到了ds,也就是指向DATA節區,要用到節區覆蓋3EH,而這三個前置碼的先後順序是由編譯器來決定的,順序是可變的。所以,前置碼會放 67 66 3E。

至於mov的運算碼是什麼呢,得看後面的運算元是什麼型態。很明顯是屬於16位元暫存器(r16)與記憶體(m16),所以opcode會是8B,見下圖:

最後的[bp+si+5],由於裡面並沒有用到比例變址(SIB),也就是裡面沒有用到乘法,所以不需查SIB圖表,只要查看運算元類型(Mod R/M)16位元定址圖表即可。所以得出42

 

因此這條程式碼會被翻譯成 67 66 3E 8B 42 05其中05就是[bp+si+5]中的5

 是不是也不會太難?那試試看自己解釋那條lock add [eax], cx的機械碼怎麼來的。

範例二:

第二個例子我們就回到這章一開始熟悉的在C++中使用內嵌組合語言的寫法。上個範例使用了mov ax, ds:[bp+si+5]是不允許在內嵌中使用的最主要原因在無法支援MODEL small的假指令,但雷同的這個mov eax, ds:[esi+ebp]+5總可以了吧!原因在完全符合下圖格式:

那開始分析吧!

  1. 32位元定址所產生的運算碼相對簡單,第一運算元eax他是屬於在32或64位元環境針對32位元的暫存器存取,沒問題。但後面的第二運算元用到了ds,也就是指向DATA節區,要用到節區覆蓋3EH,所以前置碼只放 3E。
  2. 至於mov的運算碼是什麼呢,得看後面的運算元是什麼型態。很明顯是屬於32位元暫存器(r32)與記憶體(m32),所以opcode會是8B。
  3. 最後的[esi+ebp]+5,不論加5是否寫在括號裡面,但他就等於[esi]+[ebp*1]+5,這時我們查看運算元類型(Mod R/M)32位元定址圖表有沒有這種類型:

所以運算元類型(Mod R/M)得出44。特別留意只要是[–][–]形式就是記憶體定址,需查SIB

  1. 因為他也隱含了乘法1,要用到比例變址(SIB)了,所以得出2E。

 

注意,這個圖表是先查有比例乘法的列ebp,再查行的esi,最後加5,很自然就是編譯成 05。

因此這條程式碼會被翻譯成 3E 8B 44 2E 05,分析完畢。

 然而,結束了嗎?各位有想過,原有[esi+ebp]+5如果是寫成[ebp+esi]+5,看起來程式碼是相同,但從上面這樣分析下來,應該是完全不同。這留給各位當作業囉!結果會是:3E 8B 44 35 05,因為主角換成了[esi*1],而[ebp]不在裡面,只能歸類在[*]。

 

 

發表留言

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料