日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【CTF大赛】第五届XMan选拔赛 ezCM Writeup

發布時間:2025/3/21 编程问答 18 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【CTF大赛】第五届XMan选拔赛 ezCM Writeup 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

ezCM

直至比賽結束,這道題目都是 0 解題,一方面是因為比賽時間較短,另一方面還是因為這道題目較難,考察了不常見的橢圓曲線算法(ECC),大大增加了對做題者的要求。

題目信息


題目是使用 Golang 來編寫的一個 CrackMe 程序,程序內符號沒有被去除,所以這篇文章就不會講解如何恢復 Golang 程序符號,另外 IDA Pro 7.6 已經支持 Golang 程序分析,打開就可以直接恢復被去除的符號信息。

題目要求打開一個 KeyFile ,并且通過讀取其文件的內容來注冊程序,我們要做的就是通過分析程序驗證方式來編寫一個 KeyFile,使其可以通過程序注冊驗證,最終拿到 flag 數據。

前置知識

由于是 Golang 的題目,在一些數據結構和調用約定上和大多數語言都不一樣,所以一定不能過于的依賴偽代碼,在調試過程中最好能夠多關注匯編代碼,這樣在逆向過程中會快速掌握到核心。這部分內容參考學習了 panda0s – Golang underlying data representaion ,本來是不想把這部分內容放在這篇文章中的,但是由于關聯性過大,所以不得不拿來飽滿文章內容。
函數調用

在函數調用的過程中,無論是調用參數還是返回值都是通過棧來傳遞。

其傳參的特征是

  • 參數傳遞順序是從右往左傳遞,而且不使用像是 push pop 這樣的操作棧的指令,而是直接對棧上的內容進行修改。
  • 參數傳遞一般都是借助一個寄存器中轉,例如
    rax、rcx,先把數據原來的儲存位置的數據賦值到這個寄存器上,然后再把這個寄存器的內容賦值給棧上數據,并且如果數據是 0x10
    size 的結構體,就會借助 xmm 寄存器中轉來加速。
  • 其返回值的特征是

  • 返回值的位置緊貼著在最后一個參數的地址之后。以上圖為例,最后一個參數的地址是 rsp + 0x250 – 0x248 + 0x8 =
    rsp + 0x10,所以這里的返回值的地址就是在 rsp + 0x250 – 0x238 = rsp +
    0x18,有多個返回值的情況也是類似。
  • 方法調用

    在上圖中,嚴格意義上并不是一次函數調用,而是一次方法調用。他是對 MyMainWindow 這個對象下的 Academy 方法進行了調用,這個傳入的參數就是這個對象的指針,像是 this 一樣。這個對象的指針就相對于函數調用的第一個參數。

    String 字符串

    String 結構

    struct String{char * strPtr;int64 size; }

    所以 Golang 程序在傳遞字符串的時候,同時也會在后續跟一個參數,這個參數指的就是字符串的長度。同時由于這樣的機制,使得字符串的內容在內存中分布不需要截止符’\x00’

    Slice 切片

    在其他語言中(例如 python),Slice 是一種切片的操作,切片之后可以返回一個新的數據對象,但是 Golang 中的 Slice 不僅僅是一種切片的操作,更像是一種靈活的數據結構。

    了解 Slice 結構后,在 IDA 中修改對應的變量類型,可以大大加快分析速度。

    Slice 結構

    struct slice {dq Pointer;dq Length;dq Capacity; }

    Pointer:指向 Slice 底層數組的元素開始位置的指針

    Length:Slice 的當前長度

    Capacity:Slice 底層數組的最大長度,超過此長度會自動擴展
    初始化 Slice

    my_slice := make([]int, 35)

    這表示先聲明一個長度為 5、數據類型為 int 的底層數組,然后從這個底層數組中從前向后取 3 個元素作為 slice 的結構(length = 3,cap = 5)

    make 最底層調用 runtime_makeslice 分配空間,這個函數返回的是指向內部數組的指針
    訪問 Slice

    org_len := slice1[name_size + 1]


    在訪問 Slice 中元素時,會檢測是否越界如果越界則調用 runtime_panicIndex

    append / copy

    當 Length 已經等于 Capacity 的時候,再使用 append 給 slice 追加元素,會調用 runtime_growslice 進行擴容。

    在代碼中的表現是在 append / copy 的時候會檢測,slice.len + 1 與 slice1.cap 的大小關系

    如圖在把 slice 轉換為字符串的過程前,由于將要 copy slice,所以會對傳參 len 進行檢測。

    切片截取

    myvar := slice1[a:b]

    myvar 是新一個新的切片結構
    dataPtr = &slice1.dataPtr[1],相當于給了一個底層數組的指針
    len = b – a
    cap = slice1.cap – a

    尋找關鍵函數

    對于這種有界面的程序,和常規的只有一個控制臺的題目不同的是,題目的關鍵信息不是直接存在于 main 函數中,所以我們首先要做的就是定位到題目的關鍵位置。

    而在這道題里,我們的突破口就是在沒有選擇文件的情況下,點擊“Register”就會彈出的信息框“Cannot open target file.”

    我們可以利用 IDA 的 Shift + F12 熱鍵調出 Strings 窗口來查找“Cannot open target file.”字符串

    搜索字符串后,我們再雙擊進入,在前面自動生成的名稱處按下 X 熱鍵查找交叉引用

    對于每一處引用我們都前去查看,最終找到了關鍵的代碼位置(截圖僅截取部分代碼)

    我們接下來對函數內容進行分步驟的解析

    KeyFile 格式解析

    特征格式

    這部分內容雖然偽代碼看起來混亂,但是大概可以猜測是開頭和結尾的特征字符

    ---BEGIN CERT--- xxx ----END CERT----

    通過這兩個特征讀取出關鍵的秘鑰信息 xxx,然后傳入到后續函數,這樣的標記格式在其他地方也很常見,所以這里不著重分析。
    解密核心數據

    在后續函數中的對秘鑰核心數據做了一個解碼

    這里上述代碼中可以看出,程序先通過一個 base64 解密對中間部分內容進行解碼,然后以兩個字節為一個單位進行解密,對第一字節異或 0xAA 得到數據,并且對第二字節異或 0x55,與第一字節的內容進行比對,如果不同則直接退出。

    數據結構格式

    解密后的數據是如何在存放的,分別又代表著那些信息?想要知道這些就要分析接下來所做的代碼。

    代碼中由于對切片做了很多索引操作,所以有各種各樣的越界檢測,我們拋開這部分代碼來看,就可以看出 Username 和 Organization 的儲存結構 —— 第一個字節存放字符串長度,后續跟字符數據。

    根據前置知識中切片的相關知識,這里調用 math_big_nat_setBytes 的切片內容我們可以大致還原,主要就是根據切片的 len 和 cap 來確定,

    切片左邊的值:由來源切片的 cap 減去的內容

    切片右邊的值:由來源切片左邊的值 + 新切片的 len

    所以 main_a 和 main_b 的內容來源于新的切片內容分別是

    a[org_len + name_len + 2:org_len + name_len + 2 + 8] a[org_len + name_len + 10:org_len + name_len + 10 + 8]

    這部分內容可以結合上面的偽代碼結合得出,也就是 Username 和 Organization 后的 8 字節是 main_a 的內容,再后 8 字節是 main_b 的內容,最后 4 字節是 main_expire 的內容。

    這里需要注意的是,main_a 和 main_b 的內容都是以字節的形式直接轉換為大數類型,而 main_expire 是以 WORD 的形式讀取(使用 2 字節),這兩種讀取方式的字節序不同。
    數據表

    綜合上面所說的,可以得出以下表格來記錄 KeyFile 文件內加密數據格式

    驗證邏輯

    約束條件

    了解了程序如何解析 KeyFile 后,接下來才是本文最關鍵的地方,也就是程序的驗證方法。

    首先會把 main_a 和 main_b 的內容相加,然后與 13417336609348053335(0xba33f48ee008e957)進行比對,如果相同則進入后續的判定,這就是對 a 和 b 之間關系的一個約束。

    初始化大數

    接下來又對三個大數進行了初始化,我把這幾個常量去 Google 搜索了一下,發現 main_p 的值是在 GF§ 上的橢圓曲線中常用的一種取值,這對于我們了解接下來的代碼的大致內容有所幫助。

    這樣根據常量來猜測程序意義的方法也是常用的,這里就是借助了橢圓曲線中常見的 p。

    驗證代碼

    最終的檢測代碼就是判斷題目 public_key 是否在橢圓曲線(y^2 = x^3 + ax + b)上,其中 a 和 b 是用戶可控的值,我們現在有一個在橢圓曲線上的點 生成元 G ,那么我們就可以根據這個 G 點的值和 a + b 的約束來反推 a 和 b 的值。

    (gy^2) % p = (gx^3 + a * gx + b) % p (gy^2 - gx^3) % p = (a * gx + b) % p (gy^2 - gx^3 - (a + b)) % p = (a * (gx - 1)) % p (a * gx + b) % p - ((a + b) % p) = (a * (gx - 1)) % p ((a * gx + b) % p - ((a + b) * inv(gx - 1)) % p = a % p

    推導過程只是我粗淺的理解,所以可能不是很規范,但是表明了如何推出 a 的值,有了 a 的值后,我們直接相減就可以計算得到 b 的值。
    注冊機編寫
    通過上述的邏輯和整理,我們可以快速的編寫出一個 Keygen,我的代碼如下

    import base64 import gmpy2 from Crypto.Util.number import *def calc_ab():gx = 0xf20553f3b02d1cad6aa8f895cc331a84b78f9bded26ecd9170662d3251d8d8a2gy = 0xa5c2e0fca8853a37f651726d719dd734421d0e01adf23c12c921e9060bc4c832p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fsum = 0xba33f48ee008e957 # a + b# y^2 = x^3 + ax + bp1 = (gy * gy) % p # y^2p2 = (gx * gx * gx) % p # x^3p3 = (p1 - p2) % p # ax + bp4 = (p3 - sum) % p # ax + b - (a + b) = a * (x - 1)inv = gmpy2.invert(gx - 1, p)a = (p4 * inv) % pb = (sum - a) % preturn a, bdef generate(Username, Organization, ExpireTime):if len(Username) >= 16:return ""if len(Organization) >= 16:return ""if ExpireTime > 0xffff:return ""reg_info = ""reg_info += chr(len(Username)) + Usernamereg_info += chr(len(Organization)) + Organizationa, b = calc_ab()reg_info += long_to_bytes(a).rjust(8, '\x00') + long_to_bytes(b).rjust(8, '\x00')reg_info += long_to_bytes(ExpireTime).rjust(2, '\x00')[::-1]en_info = ''for i in reg_info:en_info += chr(ord(i) ^ 0xAA) + chr(ord(i) ^ 0x55)en_info = base64.b64encode(en_info)return '---BEGIN CERT---\n' + en_info + "\n----END CERT----"with open("C:\\reg.crt", "w") as f:f.write(generate('wjh', 'org', 8888))

    接下來把 KeyFile 拖入程序,點擊 Register,即可成功通過驗證得到 Flag

    可以發現 flag 的值其實就是 main_a 和 main_b 的 hex 編碼后的值,這樣可以保證 flag 的唯一性。

    總結

    在這之前其實也遇到過一些 Golang 的題目,但是因為 Golang 難以分析,所以這些題目的核心算法相對來說都比較簡單,都是一些比較簡單的邏輯問題。這道題雖然分析過程看似簡單輕松,但是實際上我對其內涵的原理和程序的用法進行了深入的研究,消耗了大量的時間和精力。雖然本題最終展現的并不是一個 ECC 難題,但是在逆向分析的過程中,我也學習到了一些 ECC 的內涵和代碼實現。希望各位師傅可以借此題來開篇學習 Golang 逆向和 ECC 算法。


    最后
    我整理了相關的資料,有需要的朋友可以私我哦!!!

    【資料】

    總結

    以上是生活随笔為你收集整理的【CTF大赛】第五届XMan选拔赛 ezCM Writeup的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。