【CTF大赛】第五届XMan选拔赛 ezCM Writeup
ezCM
直至比賽結(jié)束,這道題目都是 0 解題,一方面是因?yàn)楸荣悤r(shí)間較短,另一方面還是因?yàn)檫@道題目較難,考察了不常見的橢圓曲線算法(ECC),大大增加了對(duì)做題者的要求。
題目信息
題目是使用 Golang 來編寫的一個(gè) CrackMe 程序,程序內(nèi)符號(hào)沒有被去除,所以這篇文章就不會(huì)講解如何恢復(fù) Golang 程序符號(hào),另外 IDA Pro 7.6 已經(jīng)支持 Golang 程序分析,打開就可以直接恢復(fù)被去除的符號(hào)信息。
題目要求打開一個(gè) KeyFile ,并且通過讀取其文件的內(nèi)容來注冊(cè)程序,我們要做的就是通過分析程序驗(yàn)證方式來編寫一個(gè) KeyFile,使其可以通過程序注冊(cè)驗(yàn)證,最終拿到 flag 數(shù)據(jù)。
前置知識(shí)
由于是 Golang 的題目,在一些數(shù)據(jù)結(jié)構(gòu)和調(diào)用約定上和大多數(shù)語(yǔ)言都不一樣,所以一定不能過于的依賴偽代碼,在調(diào)試過程中最好能夠多關(guān)注匯編代碼,這樣在逆向過程中會(huì)快速掌握到核心。這部分內(nèi)容參考學(xué)習(xí)了 panda0s – Golang underlying data representaion ,本來是不想把這部分內(nèi)容放在這篇文章中的,但是由于關(guān)聯(lián)性過大,所以不得不拿來飽滿文章內(nèi)容。
函數(shù)調(diào)用
在函數(shù)調(diào)用的過程中,無論是調(diào)用參數(shù)還是返回值都是通過棧來傳遞。
其傳參的特征是
rax、rcx,先把數(shù)據(jù)原來的儲(chǔ)存位置的數(shù)據(jù)賦值到這個(gè)寄存器上,然后再把這個(gè)寄存器的內(nèi)容賦值給棧上數(shù)據(jù),并且如果數(shù)據(jù)是 0x10
size 的結(jié)構(gòu)體,就會(huì)借助 xmm 寄存器中轉(zhuǎn)來加速。
其返回值的特征是
rsp + 0x10,所以這里的返回值的地址就是在 rsp + 0x250 – 0x238 = rsp +
0x18,有多個(gè)返回值的情況也是類似。
方法調(diào)用
在上圖中,嚴(yán)格意義上并不是一次函數(shù)調(diào)用,而是一次方法調(diào)用。他是對(duì) MyMainWindow 這個(gè)對(duì)象下的 Academy 方法進(jìn)行了調(diào)用,這個(gè)傳入的參數(shù)就是這個(gè)對(duì)象的指針,像是 this 一樣。這個(gè)對(duì)象的指針就相對(duì)于函數(shù)調(diào)用的第一個(gè)參數(shù)。
String 字符串
String 結(jié)構(gòu)
所以 Golang 程序在傳遞字符串的時(shí)候,同時(shí)也會(huì)在后續(xù)跟一個(gè)參數(shù),這個(gè)參數(shù)指的就是字符串的長(zhǎng)度。同時(shí)由于這樣的機(jī)制,使得字符串的內(nèi)容在內(nèi)存中分布不需要截止符’\x00’
Slice 切片
在其他語(yǔ)言中(例如 python),Slice 是一種切片的操作,切片之后可以返回一個(gè)新的數(shù)據(jù)對(duì)象,但是 Golang 中的 Slice 不僅僅是一種切片的操作,更像是一種靈活的數(shù)據(jù)結(jié)構(gòu)。
了解 Slice 結(jié)構(gòu)后,在 IDA 中修改對(duì)應(yīng)的變量類型,可以大大加快分析速度。
Slice 結(jié)構(gòu)
struct slice {dq Pointer;dq Length;dq Capacity; }Pointer:指向 Slice 底層數(shù)組的元素開始位置的指針
Length:Slice 的當(dāng)前長(zhǎng)度
Capacity:Slice 底層數(shù)組的最大長(zhǎng)度,超過此長(zhǎng)度會(huì)自動(dòng)擴(kuò)展
初始化 Slice
這表示先聲明一個(gè)長(zhǎng)度為 5、數(shù)據(jù)類型為 int 的底層數(shù)組,然后從這個(gè)底層數(shù)組中從前向后取 3 個(gè)元素作為 slice 的結(jié)構(gòu)(length = 3,cap = 5)
make 最底層調(diào)用 runtime_makeslice 分配空間,這個(gè)函數(shù)返回的是指向內(nèi)部數(shù)組的指針
訪問 Slice
在訪問 Slice 中元素時(shí),會(huì)檢測(cè)是否越界如果越界則調(diào)用 runtime_panicIndex
append / copy
當(dāng) Length 已經(jīng)等于 Capacity 的時(shí)候,再使用 append 給 slice 追加元素,會(huì)調(diào)用 runtime_growslice 進(jìn)行擴(kuò)容。
在代碼中的表現(xiàn)是在 append / copy 的時(shí)候會(huì)檢測(cè),slice.len + 1 與 slice1.cap 的大小關(guān)系
如圖在把 slice 轉(zhuǎn)換為字符串的過程前,由于將要 copy slice,所以會(huì)對(duì)傳參 len 進(jìn)行檢測(cè)。
切片截取
myvar := slice1[a:b]myvar 是新一個(gè)新的切片結(jié)構(gòu)
dataPtr = &slice1.dataPtr[1],相當(dāng)于給了一個(gè)底層數(shù)組的指針
len = b – a
cap = slice1.cap – a
尋找關(guān)鍵函數(shù)
對(duì)于這種有界面的程序,和常規(guī)的只有一個(gè)控制臺(tái)的題目不同的是,題目的關(guān)鍵信息不是直接存在于 main 函數(shù)中,所以我們首先要做的就是定位到題目的關(guān)鍵位置。
而在這道題里,我們的突破口就是在沒有選擇文件的情況下,點(diǎn)擊“Register”就會(huì)彈出的信息框“Cannot open target file.”
我們可以利用 IDA 的 Shift + F12 熱鍵調(diào)出 Strings 窗口來查找“Cannot open target file.”字符串
搜索字符串后,我們?cè)匐p擊進(jìn)入,在前面自動(dòng)生成的名稱處按下 X 熱鍵查找交叉引用
對(duì)于每一處引用我們都前去查看,最終找到了關(guān)鍵的代碼位置(截圖僅截取部分代碼)
我們接下來對(duì)函數(shù)內(nèi)容進(jìn)行分步驟的解析
KeyFile 格式解析
特征格式
這部分內(nèi)容雖然偽代碼看起來混亂,但是大概可以猜測(cè)是開頭和結(jié)尾的特征字符
通過這兩個(gè)特征讀取出關(guān)鍵的秘鑰信息 xxx,然后傳入到后續(xù)函數(shù),這樣的標(biāo)記格式在其他地方也很常見,所以這里不著重分析。
解密核心數(shù)據(jù)
在后續(xù)函數(shù)中的對(duì)秘鑰核心數(shù)據(jù)做了一個(gè)解碼
這里上述代碼中可以看出,程序先通過一個(gè) base64 解密對(duì)中間部分內(nèi)容進(jìn)行解碼,然后以兩個(gè)字節(jié)為一個(gè)單位進(jìn)行解密,對(duì)第一字節(jié)異或 0xAA 得到數(shù)據(jù),并且對(duì)第二字節(jié)異或 0x55,與第一字節(jié)的內(nèi)容進(jìn)行比對(duì),如果不同則直接退出。
數(shù)據(jù)結(jié)構(gòu)格式
解密后的數(shù)據(jù)是如何在存放的,分別又代表著那些信息?想要知道這些就要分析接下來所做的代碼。
代碼中由于對(duì)切片做了很多索引操作,所以有各種各樣的越界檢測(cè),我們拋開這部分代碼來看,就可以看出 Username 和 Organization 的儲(chǔ)存結(jié)構(gòu) —— 第一個(gè)字節(jié)存放字符串長(zhǎng)度,后續(xù)跟字符數(shù)據(jù)。
根據(jù)前置知識(shí)中切片的相關(guān)知識(shí),這里調(diào)用 math_big_nat_setBytes 的切片內(nèi)容我們可以大致還原,主要就是根據(jù)切片的 len 和 cap 來確定,
切片左邊的值:由來源切片的 cap 減去的內(nèi)容
切片右邊的值:由來源切片左邊的值 + 新切片的 len
所以 main_a 和 main_b 的內(nèi)容來源于新的切片內(nèi)容分別是
a[org_len + name_len + 2:org_len + name_len + 2 + 8] a[org_len + name_len + 10:org_len + name_len + 10 + 8]這部分內(nèi)容可以結(jié)合上面的偽代碼結(jié)合得出,也就是 Username 和 Organization 后的 8 字節(jié)是 main_a 的內(nèi)容,再后 8 字節(jié)是 main_b 的內(nèi)容,最后 4 字節(jié)是 main_expire 的內(nèi)容。
這里需要注意的是,main_a 和 main_b 的內(nèi)容都是以字節(jié)的形式直接轉(zhuǎn)換為大數(shù)類型,而 main_expire 是以 WORD 的形式讀取(使用 2 字節(jié)),這兩種讀取方式的字節(jié)序不同。
數(shù)據(jù)表
綜合上面所說的,可以得出以下表格來記錄 KeyFile 文件內(nèi)加密數(shù)據(jù)格式
驗(yàn)證邏輯
約束條件
了解了程序如何解析 KeyFile 后,接下來才是本文最關(guān)鍵的地方,也就是程序的驗(yàn)證方法。
首先會(huì)把 main_a 和 main_b 的內(nèi)容相加,然后與 13417336609348053335(0xba33f48ee008e957)進(jìn)行比對(duì),如果相同則進(jìn)入后續(xù)的判定,這就是對(duì) a 和 b 之間關(guān)系的一個(gè)約束。
初始化大數(shù)
接下來又對(duì)三個(gè)大數(shù)進(jìn)行了初始化,我把這幾個(gè)常量去 Google 搜索了一下,發(fā)現(xiàn) main_p 的值是在 GF§ 上的橢圓曲線中常用的一種取值,這對(duì)于我們了解接下來的代碼的大致內(nèi)容有所幫助。
這樣根據(jù)常量來猜測(cè)程序意義的方法也是常用的,這里就是借助了橢圓曲線中常見的 p。
驗(yàn)證代碼
最終的檢測(cè)代碼就是判斷題目 public_key 是否在橢圓曲線(y^2 = x^3 + ax + b)上,其中 a 和 b 是用戶可控的值,我們現(xiàn)在有一個(gè)在橢圓曲線上的點(diǎn) 生成元 G ,那么我們就可以根據(jù)這個(gè) G 點(diǎn)的值和 a + b 的約束來反推 a 和 b 的值。
推導(dǎo)過程只是我粗淺的理解,所以可能不是很規(guī)范,但是表明了如何推出 a 的值,有了 a 的值后,我們直接相減就可以計(jì)算得到 b 的值。
注冊(cè)機(jī)編寫
通過上述的邏輯和整理,我們可以快速的編寫出一個(gè) Keygen,我的代碼如下
接下來把 KeyFile 拖入程序,點(diǎn)擊 Register,即可成功通過驗(yàn)證得到 Flag
可以發(fā)現(xiàn) flag 的值其實(shí)就是 main_a 和 main_b 的 hex 編碼后的值,這樣可以保證 flag 的唯一性。
總結(jié)
在這之前其實(shí)也遇到過一些 Golang 的題目,但是因?yàn)?Golang 難以分析,所以這些題目的核心算法相對(duì)來說都比較簡(jiǎn)單,都是一些比較簡(jiǎn)單的邏輯問題。這道題雖然分析過程看似簡(jiǎn)單輕松,但是實(shí)際上我對(duì)其內(nèi)涵的原理和程序的用法進(jìn)行了深入的研究,消耗了大量的時(shí)間和精力。雖然本題最終展現(xiàn)的并不是一個(gè) ECC 難題,但是在逆向分析的過程中,我也學(xué)習(xí)到了一些 ECC 的內(nèi)涵和代碼實(shí)現(xiàn)。希望各位師傅可以借此題來開篇學(xué)習(xí) Golang 逆向和 ECC 算法。
最后
我整理了相關(guān)的資料,有需要的朋友可以私我哦!!!
【資料】
總結(jié)
以上是生活随笔為你收集整理的【CTF大赛】第五届XMan选拔赛 ezCM Writeup的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 你们应该听说过”w8ay“这个ID吧!一
- 下一篇: 【域渗透】教你怎么利用DCSync导出域