看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路

Editor發表於2018-12-23

建安二十五年,操崩於洛陽,年六十六。遺令曰:“天下尚未安定,未得遵古也。葬畢,立疑冢七十二,亦虛亦實。智者得金玉珍寶。”——《三國演義》

這就是看雪CTF.TSRC 2018 團隊賽 第三題《 七十二疑冢》的名字出處。


看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


此題的難度就和《三國演義》中虛構的七十二疑冢一樣讓人迷惑。


今天中午12:00,本題攻擊時間畫上了句號。相比第一題(89人攻破)和第二題(67人攻破),《 七十二疑冢》只有 5 支戰隊將其攻破。


看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


最新賽況戰況一覽


本題,團隊 中午放題搬磚狗哭哭,以28206s的速度奪得第一!也順勢逆襲,成為第一名


看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


本題過後,攻擊團隊率先領先的Top10團隊為:


看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


五支黑馬戰隊:中午放題搬磚狗哭哭,雨落星沉,pizzatql,\',111new111,也是本次第三題的唯五攻破者。


其中雨落星辰是前兩次榜上的No.6,而剩下四位從未上過Top10!


可謂一道題改變了五個隊的命運啊!



第三題 點評


crownless:

七十二疑冢題目的主程式程式碼量很小,也沒有任何保護,因此可以使用反編譯器快速看清程式的邏輯。但是此題涉及到了線性反饋移位暫存器的相關知識,如果沒有相關知識的儲備及一定的數學功底,就很難上手本題。



第三題 出題團隊簡介


出題方:中婭之戒  

看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


初次見面請多關照!

期末考試纏身的密碼學方向在讀研(小)究(姑)生(娘)參見各位大佬(猛鞠躬)

//隊名叫中婭當然是想要個金身啦

//寫writeup的幾位可能都是神仙吧真的厲害QAQ

//其實題中還埋了幾個小彩蛋大佬們要不要找一找……?

//可不可以誇一下我的crackme小巧可愛……?可以嗎可以嗎可以嗎~


第三題 七十二疑冢 解題思路


本題解析由看雪論壇 Riatre 原創,據說曾在強網杯中一人吊打全場


看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


觀察


題目(又)給出了一個 32 位控制檯 Windows 應用程式C72.exe:

$ file C72.exe

C72.exe: PE32 executable (console) Intel 80386, for MS Windows


同樣沒有任何保護,執行一下:

N:\pediy\3>C72.exe

請輸入序列號!////當且僅當回顯“序列號輸入正確”時才算破解成功

----------------------------------------------------

序列號:12345678

Input invalid!

看起來也是一個經典的輸入 flag,輸出對錯的題目。



分析

這個程式分析起來難度比較小,其本身程式碼量很小,也沒有任何保護,無論是採用 IDA Pro 還是在偵錯程式中跟蹤,很快便可以看清程式邏輯,故這裡不再給出“如何閱讀反彙編”這部分的分析過程,也不在著重分析諸如輸入是怎麼編碼的之類的小細節問題,若有疑問可直接參閱後文的解題程式碼。


程式首先採用預定義的字串初始化了一個作為 key 使用的 buffer,接下來讀取輸入,並對其進行 hex decode,這裡的 hex decode 過程寫的不太魯棒(沒有判斷長度和字元範圍),但其採用了轉完之後再用 sprintf 重新格式化成大寫 hex 串並和原串比較的方法,保證了輸入一定是總計 18 個字元的大寫 hex 串。


將 decode 得到的 9 位元組記為 g_input。


程式接下來逐個考慮這 9 個位元組中的每一位(共 72 位),根據每一位利用另一張預定義的常量表(共72 * 16 位元組,不妨將其視作72行,每行16位元組的表)將原始的 key buffer 進行變換。變換的具體方法為,對於第 i 位,若其是 1,則將 key_buffer+i 開始的 16 位元組與預定義的常量表中的第 i 行進行異或。


接下來,程式摸出了一個 16 位元組的 buffer,並呼叫了一個函式對其進行“解密”,要求解密得到的最後兩位元組為 0,若符合則將解密得到的內容作為字串輸出。結合程式一開始輸出的提示,很顯然,這裡要求解密得到的是對應 序列號輸入正確 這七個漢字的 GBK 編碼加上兩位元組 00。


而這個“解密”函式也十分簡單:


unsigned char kMysteriousTable[256][16] = { /* 略 */ };

void DecryptResponse(unsigned char *data, unsigned char *key, unsigned int keylen)

{

for (unsigned int i = 0; i < keylen; i++)

{

unsigned int lsb = data[0] ^ key[i];

for (int j = 0; j < 15; j++) {

data[j] = data[j+1] ^ kMysteriousTable[lsb][j];

}

data[15] = 0 ^ kMysteriousTable[lsb][15];

}

}


其對輸入進行了 keylen 次操作,每次將資料的最低位元組混合上當前 key,用作索引查表,然後將資料移動 8 位,前方補 0 並異或上表中對應的 16 位元組。“移位”、“根據輸入決定異或上的值”、“異或”……敏銳的讀者可能在 10 句話之前就意識到了,這聽上去就是一個反饋移位暫存器。


其與教科書般的 FSR 的差別在於其一次做 8 位而非 1 位。


知道了其是一個 FSR 之後,我們首先驗證一下它是不是線性的,即對於一位元組中 8 位分別的反饋的組合等同於整體的反饋:


import pefile

import operator

def u128(x):

assert len(x) == 16

return reduce(operator.or_, [(ord(c) << (8 * i)) for i, c in enumerate(x)])

pe = pefile.PE('C72.exe')

kBytewiseLFSRTable = 0xDEC0

bwlfsr_table = [u128(pe.get_data(i, 16)) for i in xrange(kBytewiseLFSRTable, kBytewiseLFSRTable + 256 * 16, 16)]

for i in xrange(256):

val = reduce(operator.xor, [bwlfsr_table[1 << j] for j in xrange(8) if (i >> j) & 1], 0)

assert val == bwlfsr_table[i]

print 'OK'


程式輸出了 OK,同時我們發現:


assert bwlfsr_table[1] << 1 == bwlfsr_table[1 << 1]


也成立,因此看起來其就是一個單個的 Galois 模式的 LFSR 的一次處理一位元組的“加速處理”版,也就是說,若將上述 C 程式碼中的 data、key 都視作小端表示的整數,其完全等價於以下程式碼:


mask = 0x1000000001408008000000002000000 << 7 # toggle mask

for i in xrange(keylen * 8):

lsb = ((data & 1) ^ ((key >> i) & 1))

data >>= 1

if lsb:

data ^= mask


解決


知道了這一點之後,接下來就好辦了,線性反饋移位暫存器中的重點在於 線性,也就是說其輸出完全是輸入的線性組合,注意到輸入及 LFSR 的引數已知,key 混合進去和 LFSR 同樣是在 GF(2) 上進行的(說人話:都是異或),因此輸出可以表示為 key 的各位的線性表示,加上已知的預期輸出,可以得到一個線性方程組。


由於 LFSR 的長度是 128 位,這裡可以得到 128 個線性方程,而 key 的長度是 138 * 8 位,遠超過 128,因此可能的 key 會十分多,但由於 key 的生成方式所限,其中絕大多數都無法找到一個對應的 g_input。因此直接求解 key 再試圖反算回程式的輸入是不可行的。


注意到 key 生成的過程也是線性的,因此其實可以將 key 的每一位也表示成關於輸入的 72 位的線性表示,這樣只要解存在,其一定是唯一的,並且可以直接求出。


接下來就是愉快的寫程式碼時間了,由於最後 GF(2) 上的線性方程組打算直接採用 Sage 求解,不如就把整個程式碼用 Sage 寫吧。(當然也可以自己寫 Gauss Elimination,但是何必呢……)


import pefile

import operator

def u128(x):

assert len(x) == 16

return reduce(operator.or_, [(ord(c) << (8 * i)) for i, c in enumerate(x)])

def split_bits(x):

return [(x >> i) & 1 for i in xrange(128)]

pe = pefile.PE('C72.exe')

# Data offsets (RVA)

kInitialKey = 0xB9D0 # length = 138

kBytewiseLFSRTable = 0xDEC0

kKeyRowTobeMixed = 0xEEC0

kWinMsg = 0xBA7E # length = 14

LFSRInputInstr = 0x1281

# Sanity check

bwlfsr_table = [u128(pe.get_data(i, 16)) for i in xrange(kBytewiseLFSRTable, kBytewiseLFSRTable + 256 * 16, 16)]

for i in xrange(256):

val = reduce(operator.xor, [bwlfsr_table[1 << j] for j in xrange(8) if (i >> j) & 1], 0)

assert val == bwlfsr_table[i], hex(val) + ' ' + hex(bwlfsr_table[i]) + ' ' + str(i)

# Solve

key = map(ord, pe.get_string_at_rva(kInitialKey))

encmsg = u128(''.join([pe.get_data(i+3, 4) for i in xrange(LFSRInputInstr, LFSRInputInstr + 7 * 4, 7)]))

winmsg = u128(pe.get_data(kWinMsg, 14) + '\x00\x00')

mask = bwlfsr_table[1] << 7

bitspos = [i for i, x in enumerate(bin(mask)[2:][::-1]) if x == '1']

keyrows = [map(ord, pe.get_data(i, 16)) for i in xrange(kKeyRowTobeMixed, kKeyRowTobeMixed + 72 * 16, 16)]

class Symbol(object):

def __init__(self, *idx):

self.const_v = 0

self.vars = set(idx)

def __ixor__(self, other):

if isinstance(other, int):

self.const_v ^^= other

else:

assert isinstance(other, Symbol)

self.const_v ^^= other.const_v

self.vars.symmetric_difference_update(other.vars)

return self

@classmethod

def const(cls, v):

result = cls()

result.const_v = v

return result

inpbit = map(Symbol, xrange(72))

cur = map(Symbol.const, split_bits(encmsg))

# LFSR in Galois mode

for i in xrange(len(key)):

for j in xrange(8):

c = Symbol.const((key[i] >> j) & 1)

for k in xrange(16):

if i - k < 0: break

if i - k > 71: continue

if (keyrows[i-k][k] >> j) & 1:

c ^^= inpbit[i-k]

c ^^= cur[0]

nxt = cur[1:] + [Symbol.const(0)]

for pos in bitspos:

nxt[pos] ^^= c

cur = nxt

GF2 = GF(2)

lhs = matrix(GF2, len(cur), len(inpbit))

rhs = vector([GF2(a ^^ b.const_v) for a, b in zip(split_bits(winmsg), cur)])

for i, sym in enumerate(cur):

for b in sym.vars:

lhs[i, b] = 1

print hex(int(''.join(map(str, lhs.solve_right(rhs))), 2))[2:].rstrip('L').upper()

執行即可得到本題的“序列號”:


$ time sage solve.sage

E7DFE373BFF25B92B6

sage solve.sage0.81s user 0.13s system 99% cpu 0.944 total


Trivia


1、其實 Sage 自帶符號計算功能,用的好的話甚至不用像上面的程式碼一樣手動建出矩陣,可以直接 .solve(),但我不記得怎麼用了,又懶得看文件,所以……


2、出於好奇,事後去看了一下把這個問題丟進 SMT Solver (比如 Z3)能不能直接解出來。看起來至少 Z3 可以 simplify 出等價於手動建出來的表示的 sexpr,不過這種通用的 SMT Solver 並不是拿來算這種問題的,所以能不能跑出來大概看運氣(和一些 trick)吧。



合作伙伴

看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路


騰訊安全應急響應中心 

TSRC,騰訊安全的先頭兵,肩負騰訊公司安全漏洞、駭客入侵的發現和處理工作。這是個沒有硝煙的戰場,我們與兩萬多名安全專家並肩而行,捍衛全球億萬使用者的資訊、財產安全。一直以來,我們懷揣感恩之心,努力構建開放的TSRC交流平臺,回饋安全社群。未來,我們將繼續攜手安全行業精英,探索網際網路安全新方向,建設網際網路生態安全,共鑄“網際網路+”新時代。

看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路

看雪CTF.TSRC 2018 團隊賽 第三題 『七十二疑冢』 解題思路



轉載請註明:轉自看雪學院


看雪CTF.TSRC 2018 團隊賽 解題思路彙總: 

相關文章