main 函數及其參數#
第 17 章中,通過遠程調試的方式對程序進行了脫殼,此處實驗的程序來自第 17 章脫殼後的程序。這裡直接用作者給的程序,而不加載自己脫殼的程序。
此章對上述程序進行分析並編寫註冊機。
複習下程序,32 位架構。
打開 ida 加載脫殼後的程序,首先查看字符串。
此程序運行後,在命令行輸出Pone un User
,英文是Enter a user
。
單擊Pone un User
,跳轉到程序具體位置。
按 “X” 鍵查看字符串引用
點擊 ok,進入引用具體位置
上圖中,在以 ebp 為基址的函數中,首先執行push ebp
指令將上一層函數的 ebp 值保存到棧上,然後執行mov ebp,esp
,將 ebp 作為計算函數本身的局部變量、參數、緩存區的基準。
之後再執行 sub esp,94h 指令從 ebp 的基址開始獲取 0x94 個字節用於存儲局部變量和緩存區
對函數任意變量或者參數雙擊,ida 會顯示函數的靜態棧視圖,下圖點擊 var_4 的顯示內容,靜態棧視圖
上圖中,該函數有兩個參數,也就是函數的默認參數 argc 和 argv,在函數調用之前通過 push 指令傳到了棧上,並且顯示在返回地址(r)的下方。(注:書中這裡講解一開始沒有參數,後面將 sub_201070 重命名為 main 函數後才顯示了這兩個參數)
這兩個參數雖然顯示了,但是未被引用,所以也可以當成無參
security cookie 等局部變量#
回到靜態棧視圖,S 表示STO RED EBP
,是通過push ebp
指令保存的上一層函數的 ebp,再上方就是局部變量空間,一般都有一個var_4
變量來保護棧,防止緩衝區溢出。
var_4
有兩處引用,第一處是函數的起點,用於在棧上存儲一個安全口令security cookie
。
這是一個隨機數,在函數剛運行時,跟 ebp 進行異或運算後,結果保存到var_4
中
上圖中是另一處引用,var_4
的值傳給了 ecx,然後在與 ebp 進行異或運算,恢復原始值,然後再調用另一個函數檢查其值
點擊@__security_check_cookie@4
,進入到這個函數內部,如下圖
上圖中,如果值一切正常,則返回,如果 ecx 中不是_security_cookie
的值,將會終止進程,而不是執行返回,這只會發生在內存溢出將 var_4 覆蓋的時候
按 N 鍵,現在將var_4
重命名為COOKIE
下面的函數命名為check_cookie
修改後最終如下:
回到函數起始點處,有兩個變量還不清楚作用,一個是var_90
的初始值為0
,另一個是size
,初始值為8
,如下圖
如下圖,查看var_7d
的引用,在某個函數被調用後,var_7d
的值被傳入了 AL 寄存器,然後將值傳給了 EDX 寄存器,檢查其值是否為 0,確定輸出 Good reverser 或者 Bad reverser,所以這是一個單字節的變量,重命名 SUCCESS_FLAG,也就是最終計算的 flag 值。
按 N 重命名對var_7d
進行重命名
將左側註冊成功的代碼塊改成綠色,右側註冊失敗改成黃色,上圖中,只要修改 JZ 這個指令,也就能實現成功註冊。
用戶名和密碼處理#
上圖中,有另一個變量var_90
,初始值為 0,程序將 BUF 中的每個字節讀取出來,傳入 edx(在 0x231109 指令處),並和var_90
相加,第一次循環中加的是 0,結果再保存到var_90
,那麼之後的循環中 edx 加的是之前所有字節的和,將var_90
重命名為SUMMARY
。如下圖
上圖中var_84
是循環的計數器,計數器加 1,只循環前 4 個字節,var_84
大於等於 4 會跳出循環。將其命名為COUNTADDR
。
為了讓這個循環更好的顯示,按住 ctrl 鍵,在這三個循環上單擊,右鍵選擇 group nodes
最終顯示如下
如果想取消編組,則右鍵選擇 unhide group 或者點擊圖標
循環之後,獲取密碼
程序使用 Buf 存放獲取的密碼,因為程序已經保存了用戶名前 4 個字節的和。
上圖中,程序接著調用strlen()
函數獲取密碼的長度,如果長度小於 4,那麼就退出程序,如果不小於 4,那麼程序進入右邊綠色代碼塊。
右邊綠色代碼塊中,使用atoi
函數將密碼字符串轉成一個十六進制數,這個函數類似 python 中的hex()
然後十六進制密碼和0x1234
進行異或運算,然後保存到 edx 變量中。
上圖中,將密碼與0x1234
異或的結果及用戶名前 4 個字節的和傳入sub_231010
函數中進行比較,根據比較的結果決定輸出結果。
sub_231010
函數的參數中,arg_4 參數是首先傳到棧上的,可以對這兩個參數進行重命名。
右鍵單擊set type
,ida 會根據參數識別函數原型。
函數聲明如下圖所示
ida 自動註釋與傳入參數一致
在sub_231010
內部,在兩個參數比較之前,密碼變量傳入了 eax,執行了 shl eax,1,相當於乘以 2
最後進行比較,如果這兩個數相等,程序轉向綠色代碼並將 1 傳入 al,跳出循環,然後傳入SUCCESS_FLAG
,最終決定是否註冊成功。
算法總結#
程序首先將用戶名的前 4 個字節相加
密碼轉換為 16 進制後和 0x1234 進行異或運算,結果再乘以 2
下面構建一個基於用戶名的公式,註冊機也是基於這一點,根據用戶名計算密碼
x = password (16進制數)
(x ^ 0x1234)*2 = SUCCESS_FLAG
那麼x ^ 0x1234 = (SUCCESS_FLAG/2)
由於異或運算可逆。
A ^ B = C
A = B ^ C
那麼x = (SUCCESS_FLAG/2) ^ 0x1234
使用 python 編寫註冊機#
假如用戶輸入 “pepe”,長度小於 8 字節,那麼字節之和就可以按如下方式計算
sum = 0
user='pepe'
length=len(user)
for i in range(length):
sum+=ord(user[i])
print(hex(sum))
那麼 pepe 前 4 個字節之和就是:
sum = 0
user='pepe'
for i in range(4):
sum+=ord(user[i])
print(hex(sum))
接下來編寫一個適用於任何合法用戶名的註冊機
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
if(length>=4):
print(hex(sum))
使用 input () 函數獲取命令行輸入,但是現在上述代碼適用於任何合法的賬戶
根據之前總結的公式x = (SUCCESS_FLAG/2) ^ 0x1234
,將用戶名的計算結果除以 2,在和 0x1234 進行異或運算,找到十六進制的密碼
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
print(user)
if(length>=4):
print("success_flag",hex(sum))
password = (sum//2)^0x1234
print("password:",password)
目前註冊機已經完成,密碼也從 16 進制轉成了 10 進制,python 的默認輸出就是 10 進制
上述代碼當輸入用戶名字符數達到 8 個字符時程序會崩潰,因為字符串最後還有一個終止符 null,連同終止符在內不能超過 8 個。當然輸入 7 個字符是沒問題的。
最後還有一個問題就是如果 4 個字符的和是個奇數。因為在比較之前密碼是乘以 2 的,結果永遠是一個偶數,所以這種情況就沒有解。
加個檢查流程
sum = 0
user=input("input user name:")
length=len(user)
for i in range(4):
sum+=ord(user[i])
print(sum)
print(user)
if(sum%2==0):
print("偶數")
if(length>=4):
print("success_flag",hex(sum))
password = (sum//2)^0x1234
print("password:",password)
else:
print("奇數")
檢查用戶名字節加和 sum 除以 2 的餘數,如果不等於 0,sum 是奇數,對應的密碼是無解的。
奇數的情況
偶數的情況
這樣,註冊機就完成了