12.1 透過命令行參數確定 main 函數#
本章程序 TEST_REVERSER.exe,從這個練習中學習靜態逆向和調試的新知識。
首先看下程序架構,運行 ide 查看。
從上圖可知,此程序 32 位架構,通過 vc++ 2015 編譯。
第二步,嘗試運行程序,提示輸入用戶名和密碼,隨意輸入用戶名密碼,顯示 bad reverser。
第三步 打開 ida,加載目標程序
轉到關鍵部位的一個方法就是搜索字符串。搜索一些命令行參數,如 argc、argv 等,由於是 c++ 編寫的程序,函數原型如下:
int main(int argc, char *argv[])
從 name 中搜索 arg 等參數,ctrl + F 調用處搜索框。
雙擊上圖中的_p_argc_,內容如下:
按 X 鍵搜索引用
上圖中程序調用了_p_argc_
,_p_argv
函數,然後傳值給 main 函數。
雙擊進入 main 函數。
main 函數的三個參數
main 函數的引用(有符號)
12.2 main 函數堆棧分析#
在任意函數參數或局部變量上雙擊,轉向靜態堆棧視圖。
從上圖中,我們可以知道,最下方的首先是函數參數,它們總是在返回地址 return address (r) 的下方,因為在調用函數之前參數首先通過 push 指令傳入堆棧,然後再傳入返回地址。
再往上是調用 main 函數的上一级函數的 ebp 值。
上圖中,main 函數執行的第一條指令 push ebp
時會將它保存到堆棧上,然後將 esp 的值傳給 ebp,將 ebp 作為下方的函數參數和上方局部變量引用的基址,最後 sub esp,94h,0x94 移動 esp 為局部變量和緩存區創造空間,在這個程序中移動的距離是 0x94,編譯器根據源代碼來計算局部變量佔用的空間。
esp 的值指向局部變量的上方,ebp 指向基址,基址的上方是局部變量,下面是返回地址和函數參數,如下圖所示。
所以在以 ebp 為基址的函數中,一旦上一層函數的 ebp 值通過push ebp
保存到堆棧之後,esp 的值被傳給 ebp。00000000 作為一個水準線,上方的地址為負(-),下方的地址為正(+)。
上圖中,var_4 的相對地址是 - 00000004,如果將 ebp 的值作為基準,var_4 的實際地址是 ebp-4。
在反匯編視圖中,右鍵單擊使用 var_4 的任何一處可以驗證上述內容。
var_4 上方有一片空白區沒有變量,有可能是一個緩存區。
把視圖向上方移動一下。就可以看到空白區上方的第一個變量 Buf,如下圖。
右鍵單擊選擇 ARRAY,彈出如下窗口,可以知道數組由 120 個 1 字節元素組成,因此數組大小為 120。
函數堆棧視圖
上圖中顯示了 ebp 基址,當指向 mov ebp,esp 指令後,esp 再減去 0x94,最後指向局部變量區域頂部,如下圖所示,執行 sub esp,0x94 之後,esp 的值。
上圖中,左側的 00000094 代表的 esp=ebp-0x94,在函數內部調用其他函數時,esp 還會再往上移動,在 main 函數內部知道退出 main 函數,還是對 - 0x94 之前的局部變量進行操作。
12.3 main 函數局部變量#
接下來從靜態堆棧視圖對局部變量進行逆向分析,main 函數的參數是已知的。
局部變量
上圖中,程序讀取某個值並且和 ebp 上的值進行異或,運算之後保存到 var_4 中,作用是防止堆棧溢出。
雙擊sub_4011B0
進入,可以看到sub_401040
函數。
sub_401040 函數內部由 printf 函數,由此判斷這個函數用來打印字符的。
之後,size 變量賦值為 8,從引用可以看到有兩處引用,只是讀取了內容,而未修改
接下來有個 gets_s 函數,gets_s 函數會限制用戶輸入,上圖顯示最大輸入 8 個字符,通過 push eax 傳參,然後 lea 獲取變量 buf 也就是緩存區的地址。
如果用戶輸入少於 8 個字符就直接按回車,函數也會中斷輸入然後返回。所以在 Buf 緩存區最多有 8 個字符。
然後程序再通過PUSH EDX
將緩存區地址傳給strlen()
這個 API 函數作為參數,strlen()
獲取 Buf 中的字符串長度,再把結果保存到 var_90 變量中。
12.4 循環和代碼塊編組#
上圖中,藍色箭頭指向的往回跳轉可能是一個循環,而且var_84
變量開始作為這個循環的計數器。在0x4019f5
處有一個條件跳轉,滿足條件則結束循環。計數器從 0 開始累加,直到大於或等於var_90
變量時,循環結束。
計數器加 1
計數器變量的值傳入 EAX,EAX 加 1 之後再回傳給計數器變量。
上圖中,程序從EBP+EDX+BUF
處取出BUFFER
的第一個字節。EBP+BUF
和儲環計數器相加,計數器開始時是 0,每循環一次加 1,讀取下個字節。這樣循環把BUFFER
每一個字節的十六進制數加到var_ 88(初始值0)
變量上。
這個循環的內容是字符相加。
都標成同一種顏色,拖動最下面的代碼塊,使它們靠得更近。
可以對這三個區塊分組,通過按住 ctrl + 鼠標單擊選項卡上方,依次選中每一個區塊,上方顏色變成青色。
然後右鍵,組合結點。
最終效果
想看具體內容,需要右鍵取消組合結點
12.5 註冊算法分析#
繼續循環內容下面的代碼分析
上圖中,要求用戶輸入用戶名和密碼,下面的 sub_4011b0 是 printf 函數,然後調用了 gets_s 函數,用戶名和密碼使用同一緩存區 Buf 和最大字符限制 Size。
因為程序已經計算出用戶名每個字符相加的和,不再使用用戶名字符串,所以密碼可以使用同一緩存區。
接下來,調用了 atoi 函數將輸入的內容轉換成 10 進制,並且保存到變量 var_94 中,這個也就是密碼變量。
然後程序通過 push edx 將 var_94 密碼變量傳入,通過 push eax 傳入變量 var_ 88,傳入的這兩個變量作為 0x401010 函數的參數。
進入 0x401010 函數內部,可以發現兩個參數,arg_4 應該是密碼變量,因為密碼變量首先傳入了堆棧中,上面的 arg_0 參數就是 var_88 變量的值。
那麼 0x401010 函數是如何使用這兩個參數的呢?
在進行 cmp 比較之前,程序將密碼變量 arg_4 傳給 eax,在執行 shl eax,1。
shl 將 eax 中的 bit 向左偏移,右側低位用 0 填充,作為特例,shl reg,1 相當於乘以 2。
所以程序將密碼變量乘以 2,再和 arg_0 進行比較。
通過 python 的 ord 函數計算 ascii 對應的十進制值。
如果將 pepe 作為用戶名,那麼 pepe 字符的和如下:
結果為 0x1aa,輸入的密碼乘以 2 之後和 0x1aa 比較,所以正確的密碼應該是乘以 2 之後等於 0x1aa,那麼結果如下:
結果是 213。
此時可以打開程序,輸入用戶名 pepe,密碼 213。
顯示成功信息。
通過上圖可以知道,當這兩個值不相等時轉向紅色代碼塊,並返回 0,如果相等則轉向綠色代碼塊,返回值為 1。
返回值的作用是什麼呢?
通過上圖可以知道,返回值傳給了 var_7D 變量
通過上圖可知,如果返回值為 0,則跳轉到 bad reverser,如果是 1,則跳轉到 good reverser。
這章主要講了關於如何逆向分析繞過註冊,主要內容包括函數堆棧、局部變量、註冊算法分析等,