建安二十五年,操崩於洛陽,年六十六。遺令曰:“天下尚未安定,未得遵古也。葬畢,立疑冢七十二,亦虛亦實。智者得金玉珍寶。”——《三國演義》
這就是看雪CTF.TSRC 2018 團隊賽 第三題《 七十二疑冢》的名字出處。
此題的難度就和《三國演義》中虛構的七十二疑冢一樣讓人迷惑。
今天中午12:00,本題攻擊時間畫上了句號。相比第一題(89人攻破)和第二題(67人攻破),《 七十二疑冢》只有 5 支戰隊將其攻破。
最新賽況戰況一覽
本題,團隊 中午放題搬磚狗哭哭,以28206s的速度奪得第一!也順勢逆襲,成為第一名
本題過後,攻擊團隊率先領先的Top10團隊為:
五支黑馬戰隊:中午放題搬磚狗哭哭,雨落星沉,pizzatql,\',111new111,也是本次第三題的唯五攻破者。
其中雨落星辰是前兩次榜上的No.6,而剩下四位從未上過Top10!
可謂一道題改變了五個隊的命運啊!
第三題 點評
crownless:
七十二疑冢題目的主程式程式碼量很小,也沒有任何保護,因此可以使用反編譯器快速看清程式的邏輯。但是此題涉及到了線性反饋移位暫存器的相關知識,如果沒有相關知識的儲備及一定的數學功底,就很難上手本題。
第三題 出題團隊簡介
出題方:中婭之戒
初次見面請多關照!
期末考試纏身的密碼學方向在讀研(小)究(姑)生(娘)參見各位大佬(猛鞠躬)
//隊名叫中婭當然是想要個金身啦
//寫writeup的幾位可能都是神仙吧真的厲害QAQ
//其實題中還埋了幾個小彩蛋大佬們要不要找一找……?
//可不可以誇一下我的crackme小巧可愛……?可以嗎可以嗎可以嗎~
第三題 七十二疑冢 解題思路
本題解析由看雪論壇 Riatre 原創,據說曾在強網杯中一人吊打全場
觀察
題目(又)給出了一個 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)吧。
合作伙伴
騰訊安全應急響應中心
TSRC,騰訊安全的先頭兵,肩負騰訊公司安全漏洞、駭客入侵的發現和處理工作。這是個沒有硝煙的戰場,我們與兩萬多名安全專家並肩而行,捍衛全球億萬使用者的資訊、財產安全。一直以來,我們懷揣感恩之心,努力構建開放的TSRC交流平臺,回饋安全社群。未來,我們將繼續攜手安全行業精英,探索網際網路安全新方向,建設網際網路生態安全,共鑄“網際網路+”新時代。
轉載請註明:轉自看雪學院
看雪CTF.TSRC 2018 團隊賽 解題思路彙總: