看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析

Editor發表於2021-11-25

看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


看雪·眾安 2021 KCTF秋季賽的第四題《偶遇棋痴》已於今天中午12點截止答題,經統計,本題圍觀人數多達1721人,共計3支戰隊成功破解。


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


恭喜辣雞戰隊用時108677秒拿下“一血”,接下來和我一起來看看該賽題的設計思路和相關解析吧~



專家點評


下面由看雪專家:kkHAIKE給大家帶來第四道賽題的點評。


kkHAIKE點評:題主立題新穎,讓 lambda演算 這類學術從此又有了新的使用領域,不足的地方是,希望移動端題能夠加入相應的對抗就更好了。



出題團隊簡介


第四題《偶遇棋痴》出題方 Syclover戰隊:


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


戰隊成員如下:


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析



賽題設計思路


參賽題目:神秘的數學(Android)


電腦科學領域,尤其是程式語言,經常傾向於使用一種特定的演算:Lambda演算(Lambda Calculus)。這種演算也廣泛地被邏輯學家用於學習計算和離散數學的結構的本質。現在流行的大部分程式語言中都可以找到 Lambda 的影子,例如 Python、Java,它還是函數語言程式設計的理論基礎。Lambda 演算具有圖靈完備的性質,因此可以用 Lambda 演算實現任何程式。

 

本題設計思路是將驗證邏輯使用 Lambda 演算實現,選手需要學習 Lambda 演算基礎知識,例如 Lambda 演算如何實現數值計算、邏輯計算、currying、α-conversion、Beta Reduction、Church Boolean、Church Numberals等知識才能得心應手的解出本題。

 

在題目設計過程中,提前對 Lambda 表示式進行預編譯,並透過 Boost 的序列化元件對預編譯表示式序列化,正式題目對錶達式反序列化後得到 Lambda 語法樹,選手需要透過各種除錯技巧從記憶體中分離 Lambda 語法樹。

 

選手輸入驗證碼後,程式對選手輸入的資料進行編譯得到對應的 Lambda 表示式,並插入到預編譯的語法樹上,以便進行資料演算。

 

最後,根據 Lambda 演算結果,判斷選手輸入是否正確。


題目 lambda 編譯指令碼

import stringfrom pwn import *def L_TRUE():    return "(\\x \\y x)" def L_FALSE():    return "(\\x \\y y)" def L_ZERO():    return "(\\f \\x x)" def L_SUCC():    return "(\\n \\f \\x n f (f x))" def L_NUM(n):    s = L_ZERO()    for _ in range(n):        s = "("+ L_SUCC() + " " + s + ")"    return s def L_FLAG_NODE(i):    return ("(\z %d )" % i) def L_SUB():    return "(\\m\\n n " + L_PRE() + " m)" def L_PRE():    return "(\\n\\f\\x " + L_CDR() + " (n (" + L_PREFN() + " f) (" + L_CONS() + " x x)))" def L_CDR():    return "(\\p p " + L_FALSE() + ")" def L_CONS():    return "(\\x \\y \\f f x y)" def L_PREFN():    return "(\\f \\p " + L_CONS() +" (f (" + L_CAR() + " p)) (" +  L_CAR() + " p))" def  L_CAR():    return "(\\p p " + L_TRUE() + ")" def L_ADD():    return "(\\m \\n \\f \\x m f (n f x))" def L_IF():    return "(\\p \\x \\y p x y)" def L_NOT():    return "(\\p \\a \\b p b a)" def L_XOR(): # only works for boolean value    return "(\\a \\b a (" + L_NOT() + " " + "b" + ") b)" def L_MULT():    return "(\\m \\n \\f m (n f))" def H_ADD(l, r):    return "(" + L_ADD() + " " + l + " " + r + ")" def H_MULT(l, r):    return "(" + " ".join([L_MULT(), l , r]) + ")" def H_SUB(l, r):    return "(" + " ".join([L_SUB(), l , r]) + ")" def H_XOR(l, r):    return "(" + " ".join([L_XOR(), l , r]) + ")" def H_ABSDIFF(l, r):    return H_ADD(H_SUB(l, r), H_SUB(r, l))  import randomdef challenge(flag, seed):    random.seed(seed)    sub_eq = []    flag_seq = []    for c in bytes(flag):        flag_seq.append(c & 0xf)        flag_seq.append(c >> 0x4)     s = L_NUM(0)    for i in range(len(flag_seq)):        l1 = random.randint(1, 10)        d2 = random.randint(1, 10)        target = flag_seq[i] * l1 + d2        print((l1, d2, target), ",")        sub_eq.append(H_ABSDIFF(H_ADD(H_MULT(L_FLAG_NODE(i), L_NUM(l1)), L_NUM(d2)), L_NUM(target)))    for exp_i in range(len(sub_eq)):        s = H_ADD(s, sub_eq[exp_i])    return s #print()def compile_data(text):    p = process(["lambda/lambda", 'xxxx'])    p.sendline(text)    return p.recvall().decode('utf-8') def data2array(name, data):    return "char " + name +"[] = {" + ",".join([hex(c) for c in data.encode("ASCII")]) + ", 0x0};"   def gen_text_arr(t):    return ("char * arr[] = {" + ",".join(['"%s"' % x for x in t]) + "};").replace("\\","\\\\") ts = [L_NUM(i) for i in range(16)]ts.append(challenge(b"pediy{Lambda6}", 2))rr = gen_text_arr(ts)open("lambda/text_chall.h", "w").write(rr)input("step2")l = []for i in range(16):    r = open("tests/enc/data_" + str(i) + ".txt", "r").read()    l.append(data2array("num_%d" % i, r))open("lambda/nums.h", "w").write("\n".join(l))open("lambda/chall.h", "w").write(data2array("chall", open("tests/enc/data_16.txt", "r").read()))


破解思路:

1、從記憶體提取 Lambda 表示式(反序列化之後)。


2、對 Lambda 表示式按規則進行化簡。

a. 模式匹配數值表示式(1,2,...,n)
b. 模式匹配加法、減法、乘法、比較等運算
c. 化簡比較表示式
其中,比較運算比較難以理解,因為 lambda 中數值比較需要用兩個 sub 實現,而不是一個 sub,即 a - b == 0 && b - a == 0 才能說明 a,b 相等。


3、編寫解題指令碼。


題目答案(解題指令碼):

data = [(1, 2, 2) ,(2, 6, 20) ,(3, 5, 20) ,(5, 10, 40) ,(4, 10, 26) ,(1, 10, 16) ,(3, 7, 34) ,(7, 9, 51) ,(6, 9, 63) ,(8, 9, 65) ,(5, 1, 56) ,(1, 6, 13) ,(8, 6, 102) ,(7, 7, 35) ,(9, 3, 12) ,(9, 3, 57) ,(4, 4, 56) ,(1, 3, 9) ,(6, 3, 15) ,(3, 9, 27) ,(9, 6, 42) ,(9, 9, 63) ,(3, 8, 11) ,(7, 9, 51) ,(6, 10, 46) ,(6, 6, 24) ,(8, 3, 107) ,(7, 8, 57) ,]flag_l = []flag = ''for d in data:    flag_l.append((d[2] - d[1]) // d[0])for i in range(len(flag_l) // 2):    flag += chr((flag_l[i * 2 + 1] << 4 )| flag_l[i * 2])print(flag)


編譯版本flag:pediy{Lambda6}



賽題解析(方式1)


本賽題解析由看雪論壇kkHAIKE給出:


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


初看過程:

1、jni 直接呼叫 stringFromJNI 方法判斷輸入

2、將輸入的 14個 字元,按 4bit 拆成 28個 作為 ipt

3、透過 boost::text_oarchive 反序列一個字串到 t

4、執行方法一(0x39910),方法二(0x39614)

5、將最後得出的結果和一個 固定結果fxx 比較


推測結構:


1、剛開始根據方法一/方法二能推出大概的結構是:

// 名稱可從 vptr 的 typeinfo 中看到struct term {    int type    union {        char ch;        term *other;    }    term *next;    int idx;}


2、後根據 ·0x3B5EA· 處的 puts("Running out of free variables.");

3、找到原始專案 https://github.com/mpu/lambda;

4、發現作者在原 term 基礎上增加了idx成員;

5、遂根據原始碼退出方法二為 eval_deep。


序列化分析


1、下載 boost 原始碼,推出 0x39736 處呼叫 0x3ADB4 方法,應該像是 ar >> (term*)t;

2、推出該方法應該是 load_pointer_type<term>::invoke;

3、因為該結構是鏈式結構,所以檢視 該方法 的引用 0x3B018,為 serialize 函式;

4、PS:我是透過找vptr找到的,感覺上面這樣推簡單就這樣說了。


lambda 演算


學廢這兩篇就能做題了:

https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97#lambda%E6%BC%94%E7%AE%97%E8%88%87%E7%B7%A8%E7%A8%8B%E8%AA%9E%E8%A8%80


https://zh.wikipedia.org/wiki/%E9%82%B1%E5%A5%87%E6%95%B0


方法一細節

1、作者引入了一個特殊的 抽象化(繫結z);

2、他會從輸入的 ipt,使用 該抽象化 的 idx(之前新增的成員),選擇一個索引;

3、從內建 16個term 選擇一個替換掉該抽象化。


列印

1、因為作者增加了一個z抽象化,原來的parse_dump_term會崩潰所以要做修改。

voidparse_dump_term(const struct term * t, FILE * stream){    const struct term * pterm = t;    int nparen = 0;    for (;;)        switch (pterm->type) {        case Tlam: {            // CHANGE            if (pterm->data.lam.var == 'z') {                fprintf(stream, "[x%d]", pterm->idx);                goto Close_paren;            }            fprintf(stream, "Lam (%c, ", pterm->data.lam.var);            pterm = pterm->data.lam.body;            nparen++;            continue;


序列化原始碼

template<class Archive>void serialize(Archive& ar, term &t, const unsigned int version) {    ar& t.type;    switch (t.type) {    case Tlam:        ar& t.data.lam.var;        ar& t.idx;        if (t.data.lam.var != 'z') {            ar& t.data.lam.body;        }        break;    case Tapp:        ar& t.data.app.left;        ar& t.data.app.right;        break;    case Tvar:        ar& t.data.var;        break;    }}term* parse_term_in(std::istream &in) {    boost::archive::text_iarchive ar(in);    term *ret = nullptr;    ar >> ret;    return ret;}term* parse_term_s(const char* s) {    std::istringstream ss(s);    return parse_term_in(ss);}


華點

1、此時 dump 出的表示式非常巨大 190 KB,找不到下手的地方

2、轉變思路,找到作者提供的另外 16個term,都 dump 出來

3、找到兩個特點:

  • 一個比一個長;

  • 除第一個是抽象化之外,其他的都是應用(app)


4、此時回到那個 lambda 演算庫,所謂的演算,是指將應用套用並化簡(至抽象化或變數)

5、將他們全部演算,發現剛好對應 邱奇數0-15自然數

6、使用 記事本替換大法,將原始dump的自然數全部替換

7、根據 邱奇數 頁面,還可以替換,PLUS(ADD),SUCC(INC),MULT

8、最後還剩一個雙目運算比較長,隨便代入幾個自然數,得出是 減法(SUB)

9、這個減法有個特點,必須滿足,a-b>=0,要不結果是 0,這個後面會考。


求解

1、此時應該可以手推出結果了,無奈函式太長,我還是選擇求解器求解;

2、將替換後的結果弄成 python 的樣子,PP是結合雙目運算;


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


3、本想直接去 python + w3 執行,發現會因為函式層級太多無法解析;

4、故寫了一個求解指令碼。

def _ADD(a, b):    return a + bdef _MULT(a, b):    return a * bdef _bind(f, a):    return lambda b: f(a,b)def ADD(a):    return _bind(_ADD, a)def MULT(a):    return _bind(_MULT, a)def INC(a):    return a+1from z3 import *x = IntVector('x', 28)s = Solver()def _SUB(a, b):    # 一定要加,不加無解    s.add(a>=b)    return a - bdef SUB(a):    return _bind(_SUB, a)with open("ctf4.txt", "rt") as f:    data = f.read()def call(p):    if p[0] == "P":        p = p[3:]        a, p = call(p)        p = p[2:]        b, p = call(p)        p = p[1:]        return a(b), p    elif p[0] == "A":        p = p[4:]        a, p = call(p)        p = p[1:]        return ADD(a), p    elif p[0] == "S":        p = p[4:]        a, p = call(p)        p = p[1:]        return SUB(a), p    elif p[0] == "I":        p = p[4:]        a, p = call(p)        p = p[1:]        return INC(a), p    elif p[0] == "M":        p = p[5:]        a, p = call(p)        p = p[1:]        return MULT(a), p    elif p[0] == "x":        p = p[1:]        c = 0        while p[c]>='0' and p[c]<='9':            c+=1        r = int(p[:c])        p = p[c:]        return x[r], p    else:        c = 0        while p[c]>='0' and p[c]<='9':            c+=1        r = int(p[:c])        p = p[c:]        return r, prr, _ = call(data)s.add(rr==0)s.check()print(s.model())



賽題解析(方式2)


本題的另一解決,獲得者雖然沒看懂題,但還是解出來了,這也是逆向分析的魅力所在。


本賽題解析由看雪論壇xym給出:


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


老實說一看到是安卓題我還是挺怵的,因為演算法一般都是arm型別的so檔案,而我一直都沒有一個合適的arm除錯環境。


不管怎麼樣,先上手用jadx檢視一下吧。


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


果然,介面直接匯入了kctf2021這個動態庫並宣告瞭stringFromJNI這個native函式,並且直接用它對輸入進行了判斷。


用IDA開啟libkctf2021.so,定位到目標函式。

看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析
看來關鍵演算法就在sub_39998這個函式里了。在ida裡看到這個函式這麼清晰明白的顯示出來,我的第一感覺是哪裡又藏著hook會修改這個函式,於是先簡單檢查了一下,確定so沒有預載入可疑的函式。


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


關鍵函式邏輯很清楚,當輸入字串長度為14時,初始化一個表(這個表對我解題並沒有用,下面略過不說),然後將輸入每4bit轉成一個dword,變成一個長為28的陣列。


然後呼叫sub_39658處理一個固定字串,根據字串的首行搜尋可以得知是這是boost的一個序列化。為了弄清楚反序列化後的物件是什麼東西,我這裡有兩個選擇:1是下載boost開發庫開發實驗一下,2是找個東西模擬執行一下。


因為unidbg比boost更快下載完,因此我最終模擬執行了這個反序列化函式並列印了物件的記憶體。


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


序列化後的物件v13繼續使用sub_39910進行處理。

看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析
首次看這個函式並沒有看懂,只能推斷他應該是一個很多層的遞迴處理,並且會根據輸入去反序列化其他16個序列化物件中的一個。
看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析
結合sub_39614函式可以分析出v13這個物件應該是一個比較大的類似二叉樹一樣的結構,具有非常多的節點,但是每個節點非常簡單,只佔16個位元組記憶體。這裡發現了一個比較複雜的函式sub_39570(但是這個函式同樣對我解題並沒有用,下面略過不說……因為我最後都沒看懂這個函式)。


最後看一下本題check的邏輯:結尾雖然有一堆的判斷跳轉,實際歸納起來就是判斷最終的v13是不是隻有3個特定節點了。


因此梳理一下這道題的思路:作者定製了一棵非常大的二叉樹,然後用sub_39910把拆為4bit後的28個輸入轉為16棵小樹中的一顆並插入到二叉樹值為122的對應節點中,形成一棵不帶122節點的樹。然後使用sub_39614對這個樹的所有節點進行刪減,如果最後只剩3個節點就算成功(3個節點應該是這道題理論上能刪減到的最少節點吧)。


那麼這道題的破題之處就在於,因為使用者輸入選擇的分支必然是散佈到整棵樹的不同部位,那對其中某分支能獨自佔據的最大分支來說,其刪減結果其實都與其他輸入無關。因此使用者的每個輸入相對其他輸入很可能都是獨立的,整個大題可以拆分成為28道相同的小題,而每道小題的解合起來就是整個大題的解。因此我們的目標轉換為了在保持27個輸入不變時,求剩下那個輸入能使最終節點數最少的值。


因為unidbg在執行UXTB16指令時會報錯,因此模擬執行的辦法在這裡就中斷了。


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析


幸好整個程式可以轉為等效的C程式碼,透過查詢arm彙編指令集,把clz和UXTB16轉為相應的函式或程式碼,實現了每個輸入的暴力破解。實際程式碼中分成兩次,第一次是所有的低4bit,第二次是所有的高4bit(因為可見字元最高位為0,所以其實只有3bit),實際使用的指標也不是節點數最少,而是二叉樹層數最少(從結果看應該是等效的)。

看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析



往期解析

1、看雪·眾安 2021 KCTF 秋季賽 | 第二題設計思路及解析


2、看雪·眾安 2021 KCTF 秋季賽 | 第三題設計思路及解析



看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析

看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析

第五題《拔刀相向》正在火熱進行中,

還在等什麼,快來參賽吧!


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析

- End -


看雪·眾安 2021 KCTF 秋季賽 | 第四題設計思路及解析

公眾號ID:ikanxue

官方微博:看雪安全

商務合作:wsc@kanxue.com

相關文章