今天中午CTF 第二題結束~
大傢伙們對第二題,熱情高漲,光圍觀人數就已經達到8914人。
有很多小夥伴都徹夜不眠,只為攻破此題,第二天接著上班。
(年輕人果然身體好啊!)雖然成績很重要,但是也要注意身體哦!
第二題結束後,目前攻擊方排名如下:
目前第三題正在進行時,截止目前已有23人攻破,同志們繼續加油!
看雪評委netwind點評
此題一開始釋放了兩個“煙霧彈”,經過分析後可以判斷此處無解,分析發現程式裡有一段資料,這裡實為真實驗證流程的程式碼,真實的驗證流程需要透過溢位覆蓋返回地址跳轉過去,在清理掉花指令後分析出演算法解方程後便可求解。
第二題作者簡介
選取攻擊者birchfire的解析過程
1、初步分析,陷入迷茫
初步分析程式用全域性變數dword_41B034儲存了一個控制變數,控制變數初始值為2,當該值變為0時會輸出“You get it!”
而程式在獲取輸入之後呼叫了2個函式,這兩個函式在滿足一堆條件下分別會對dword_41B034做減1操作,看似2次減一之後dword_41b034就能變成0
再看條件,兩個函式的v0都用的輸入的前4位元組,v1都用的輸入的5到8位元組。
我們選出其中的2個方程
求差發現 12*(v1-v0)== 奇數,而輸入無論如何都是整數,所以等式左邊一直都是偶數,等式永遠不會成立。理論上證明這一組方程無整數解,不用去窮舉了。
2、發現玄機
發現函式在獲取輸入時存在棧溢位,輸入16個字元程式出現崩潰,返回地址被輸入的第13~16個位元組覆蓋,考慮直接ret到0x40102f位置,但優於規則限制輸入字元只能是數字和字母,該方案行不通;而用shellcode的話很難構造出只含有數字和字母的shellcode。考慮到驗證碼唯一性、字元限制等綜合條件,出題人的意圖可能是在棧溢位後ret到預先設定好的程式碼,ret的地址是可以透過輸入數字和字母字元構造出來的,用IDA瀏覽一下程式程式碼,不難發現一個看似符合條件的可疑塊00413131,0x41對應字元A,0x31對應字元1,而0x00剛好是字串結尾。由此猜測輸入共15個字元,最後3個字元是"11A"
3、抽絲剝繭,柳暗花明
用輸入11223344556611A作為輸入測試,發現程式並沒有崩潰,可見思路是正確的。
將輸入字元標記為汙點,使用汙點傳播提取出相關的指令如下圖,我們假定輸入的前4字元組成的int為a,5~8位元組組成的int為b,9~12位元組組成的int為c,標記出相關指令對應的表示式,推斷表示式需滿足的條件,構造滿足條件的一個輸入,讓程式往下執行,再得到下一個條件,以此類推。
得到3個3元一次方程組
方程2和方程3求差,得到2*c = 0xdceacc60 得到 c = 0x6e756630, 對應字元 "0fun"
4*(a-b) + a + c == 0xeaf917e2 …… 13*(a-b) + a + c == 0xe8f508c8 …… 23*(a-b) + a – c == 0x0c0a3c68 …… 3
方程1和方程2求差,得到(a-b) = 0x02040f1a得到 a = 0xe8f508c8 – c -3*(a-b) =0x7a7fa298-3*0x2040f1a = 0x7473754a, 對應字元“Just”
得到 b = a - 0x02040f1a = 0x726f6630,對應字元“0for”
最後串起來得到對應輸入字串:Just0for0fun11A
4、深入分析,窺探奧秘
解題過程的基本思想是用汙點傳播找出輸入影響到的指令及跳轉分支,根據驗證碼的唯一性去求分支的“取等”條件(往往也是隨機輸入對應的另外一條未走過的分支),構造出方程求解。但是到此為止還未弄清楚程式最後如何輸出"You
get it!"
為了剖析出題者的思路,我們Trace記錄下程式處理正確驗證碼的過程,在透過層層驗證之後,經過了一系列的條件跳轉,設計者透過大量的跳轉垃圾指令干擾分析,選出核心的16條指令如下所示:
第1條call指令長度5位元組,並將返回地址0x413835壓入棧0x12ff4c的位置,而第16條指令call的一個Printf函式,剛好使用棧0x12ff4c中的值作為引數指標,也就是說會輸出0x413835記憶體位置的字串。
第2條指令pop eax是為了獲得寫入資料的記憶體地址,為了維持棧平衡有了第3條指令push eax,效果是此時eax儲存的是要寫入”You get it!”的記憶體地址;
第7,9,11,12條指令透過將一些立即數異或運算得到字元”You get it!”的編碼,不直接使用待輸出字串是為了防止搜尋;
第8,10,13條指令將“You get it!”字串依次填入了棧0x12ff4c中指標指向的記憶體緩衝區0x413835
最後構造跳轉,去執行0x401044位置的Printf函式,輸出“You get it!“字串。
5、靈光閃現,另闢蹊徑
分析到此處不難想到,如果在棧溢位位置直接ret到0x413830位置的call指令是否也能輸出“You
get it!”,測試發現並不能,原因是“You get it!”字串是“解密”運算得到,並且“金鑰”剛好是輸入的前面幾個字元運算得到,如果不經過前面程式碼的鋪墊。
0x413950 xor eax, edx 中的edx值不會是0x20756f59,後面無法獲得“You get it!”字串,並且 0x413bbb jmp eax中的eax值也是根據輸入的a、b、c三個變數計算得到,沒有前面的程式碼鋪墊無法呼叫Printf函式,可見出題者在防多解方面也是下了功夫的。
另外考慮在ret到0x413131地址之前先ret到其它地方,構造rop鏈最後再ret到0x413131位置構造多解,此時還需保證esp-0x10位置的資料是滿足上述3個方程的a、b、c。構造方案如下圖所示,ret addr1是第一個需要構造的值,該位置剛好儲存折ret 0x10指令,溢位後的第一次ret之後eip指向了ret addr1,esp自動加4,指向了ret addr2;
之後執行addr1處的 ret 0x10 指令,eip指向了 ret addr2, esp自動加0x10指向了第三個需要跳轉的目標地址0x413131;然後執行 addr2 處的 ret 指令,eip指向0x413131位置。由於ret addr1和addr2不能含有0,而主程式模組地址高2位元組為0,因此搜尋ret \ ret 0x10 應該在kernel32.dll、ntdll.dll等動態庫中搜尋。
我這邊使用windows7進行了嘗試,測試時kernel32.dll剛好載入在0x76330000-0x76404000的地址空間,在kernell32.dll中搜尋到了大量ret和ret
0x10指令,當然ret 0x10 可以換成其它指令,但棧空間佈局需要相應更改,至少為ret 0x0c,底下才有a、b、c的儲存空間。另外ASLR等環境因素的影響,讀者去嘗試重現時可能載入的地址不一樣。
搜尋出的ret 0x10和ret指令分別選取0x76367931位置和0x76395659位置,也就是說構造的第一個返回地址為0x76367931,對應輸入字串ly6v,第二個返回地址為0x76395659,對應輸入字串為YV9v。
針對這個環境下我們可以構造出只包含數字和字母的另外一個輸入 1111222233331y6vYV9v1111Just0for0fun11A 使其輸出“You get it!”,其中下劃線字元可任意更改其它數字和字母。
但是這型別的驗證碼受到不同版本作業系統影響,kernel32.dll的不同會導致所需構造的驗證碼不一樣,需要根據版本差異重新搜尋指令的位置。另外ASLR也會有一定影響,kernel32.dll在系統啟動之後載入地址不會再發生變化,但系統重啟之後其基地址會重新隨機生成,驗證碼還得重新構造。因此理論上難以得到第二個穩定通用的只含有數字和字母的解。
最後給出題者點贊,題目構造猶如鬼斧神工!