標籤:編譯器 圖片 文法 com 流程 慢慢 規則 跳轉 對齊
遊戲通常會包含各種各樣的功能,如戰鬥系統、UI渲染、經濟系統、生產系統等,每個系統又包含各式各樣子功能,如傷害判定、施法、使用道具、角色移動、玩家之間交易等等。這些遊戲功能在代碼實現中往往少不了條件判斷(如傷害判定)、迴圈(遍曆物品列表,播放遊戲動畫)等。
在逆向過程中如果可以從組合語言識別出對應的文法結構,在分析過程中將彙編代碼轉換為C語言文法結構,可以協助對程式執行流程的理解。
下面分別介紹最常見的邏輯文法結構:
a) if...else
b) switch...case
c) for、while
註:文中使用的反組譯碼工具為IDA
一、 if...else
彙編代碼:
if...else結構比較固定,通常包含cmp指令、jcc指令以及滿足條件後執行的指令塊。
if...else結構可以串聯,串聯後的if...else有明顯的代碼塊邊界,逆向工具通常可以將代碼塊標識出來(圖中虛線)。
二、 switch...case
- 一個簡單switch...case
彙編代碼:
顯示了switch...case基本的結構:a) 跳轉運算式;b) 分支代碼;c) 跳轉表
a) 跳轉運算式
其中loc_401235代碼塊對應switch...case中default分支。
當nGameEvent > 4時,跳轉到loc_401235代碼塊,即default分支。
當nGameEvent <= 4時,根據跳轉運算式進行跳轉:
jmp ds:off_40123C[nGameEvent*4]
其中off_40123C為跳轉表地址,跳轉表中每一項代表一個32位地址(4個位元組),當nGameEvent為0按第一項地址跳轉,當nGameEvent為1按第二項地址跳轉,依次類推。
b) 分支代碼
各個分支的處理邏輯都在這裡,範例程式碼中僅僅簡單的調用對應函數。
(PS:這裡用jmp而不用call是編譯器最佳化的結果)
c) 跳轉表
跳轉表實際是一個地址數組,存放了每個跳轉分支的地址(32位絕對位址),當nGameEvent為0時,跳轉運算式讀取數組中第一項資料(0x0040121C),即
.text:0040121C E9 8F FF FF+ jmp [email protected]@YAXXZ
調用DoLogin函數。
(PS: 實際運行時,由於隨機化基址,從調試器看到的跳轉表內容可能與靜態分析時不同,這是重定位引起的,關於重定位的原理可以參考相關文檔,這裡不再詳述)
- 不連續的switch...case
上面的樣本中case的值是連續的,因此跳轉表比較規則。在實際使用中可能會遇到不規則的case值,如:
彙編代碼:
上面的代碼有兩個特點:
- 最小case值非0
中最小case值為3,為了不浪費跳轉資料表空間,編譯器會將索引值減去3保證最小的case值對應跳轉表中的第一項。
2.case值不連續
編譯器會在跳轉表間隔中插入default跳轉,保證邏輯正確。(以空間換取時間)
3.雙重跳轉表
彙編代碼:
相對於前一個樣本,此處case值間隔更大。如果按照之前的方法,跳轉表的大小需要(110-30 + 1)* 4 = 324位元組,佔用記憶體空間大。
編譯器為了節省空間的,使用了雙重跳轉:跳轉表、間接跳轉表。其中跳轉表與之前介紹的跳轉表一致,而間接跳轉表儲存的不是分支地址,而是索引值,指向跳轉表中的索引。
跳轉表:
間接跳轉表:
在進入switch...case時,先算根據間接跳轉表獲得索引號,再根據索引號尋找跳轉表,擷取實際分支地址。
使用雙重跳轉表後,實際佔用空間:5*4 +(110 – 30 + 1)= 101位元組,大大減少空間佔用。
- swtich...case退化
當case值間隔過大,使用跳轉表、雙重跳轉表消耗的空間太大,編譯器會將switch...case退化為if...else,如:
彙編代碼:
這裡沒有跳轉表結構,只剩下cmp/jcc指令,可見編譯器已經將swtich...case轉換為等價的if...else。但在轉換過程中,編譯還是做了力所能及的最佳化:通過二叉尋找法加快跳轉分支的尋找。
- 嵌套switch...case
彙編代碼:
可以看出嵌套的switch...case結構在彙編代碼上是相對獨立的,外層和內層switch結構有各自的跳轉表。
外層跳轉表:
記憶體跳轉表(雙重跳轉表):
根據跳轉表中的地址項,也可以清楚的區分外層和內層的跳轉分支。
三、 迴圈語句
a) for迴圈
彙編代碼:
其中nop dword ptr[eax+00h] 為指令對齊,沒有實際意義。迴圈的彙編實現為:
b) while迴圈
彙編代碼:
其中nop dword ptr[eax+eax+00h] 為指令對齊,沒有實際意義。迴圈的彙編實現為:
從上面可以看出,for和while結構的彙編實現幾乎一摸一樣,僅僅是使用的寄存器有些區別。實際逆向過程中將迴圈映射為for或者while結構都是可以的。同時還可以看出,迴圈有個明顯的特徵:往回跳轉(向地址小的方向跳轉),大部分情況下遇到往回跳轉的指令就是迴圈,極少數如編譯器代碼結構最佳化產生的往回跳轉不是迴圈除外。
四、 總結
文法結構對應的彙編代碼與編譯器有很大關係,同一份原始碼不同編譯器產生的彙編代碼結構不一樣;即使是同一個編譯器,不同的編譯選項產生的彙編代碼結構也不盡相同。需要在逆向過程中慢慢熟悉編譯器的特性。
*轉載請註明來自遊戲安全實驗室(GSLAB.QQ.COM)
C語言與組合語言對照分析