看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路

Editor發表於2018-12-23

有一片魔法森林,森林裡的每一棵樹都不一樣;每一棵樹與別的樹之間都有一扇魔法門,穿過一扇門就會到達另一棵樹;魔法門是雙面的,順時針穿過和逆時針穿過,會到達兩個不同的目的地;只有按照正確的路線穿過這些魔法門才能走出這片魔法森林。


森林裡有一隻獨角獸 她有兩件法寶:天書 和 金蛋;


天書裡記錄著《魔法森林》設計思路,能幫你找到出路,但是,能開啟天書的就只有那枚金蛋;


可惜的是,獨角獸弄丟了那枚金蛋,你只有找到金蛋,交給獨角獸,她才能帶你走出這片魔法森林......


看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


第七題《魔法森林》在今天(12月15日)中午12:00 攻擊結束,意味著本次KFCT已經走完了前半階段。


魔法森林充滿魔幻讓人眩暈,以至於本題在48小時內只有一個團隊將其攻破,闖出森林的團團迷霧!


他就是中午放題搬磚狗哭哭! 以 165202s(約45.8小時)衝出森林迷霧!


看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


最新賽況戰況一覽


攻擊方在第七題之後的最新排名情況如下:(Top 10)

看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


本題結束後,攻擊方Top 10 排名無異。


中午放題搬磚狗哭哭 在接下來的比賽中,還能穩坐第一嗎?


又會再次出現排名大調整的景象麼?



第七題 點評


crownless:

魔法森林這道題的設計遵循了“安全 普適 高效 最簡”的原則,而且準備了一份“劇情介紹”來增加題目的趣味性。原理上,此題涉及了群論的相關知識,具有一定的難度。



第七題 出題團隊簡介


出題團隊: GRAFFINDOR  


隊員:leafpad逆向萌新,喜歡數學,正在科銳努力學習中。


隊長:佛系程式girl,膜拜各位大佬而入看雪神壇…觀戰期間,倍感人生艱難…


看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路



第七題 設計思路


由看雪論壇Chikey 原創


看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


大家好!我們又見面了!這一次,我奉上的作品是:密室逃脫系列之二——魔法森林 。


既然說是系列,顯然與前一作品是有關係的。我們先來理一下它們之間的關聯和區別:


(1)首先,它們的設計目標是一致的,都是為了驗證我的執念到底是否可行(在上一期有提到過 這裡不再贅述);


(2)它們都遵循著相同的設計原則:安全、普適、高效、最簡。所謂安全, 指的是希望能在看雪CTF中存活,攻擊方不禁手,不假設攻擊者有知識盲區;所謂普適,指保護方法不限定硬體平臺,不限定作業系統,不限定程式語言(但在實際比賽中 我只能選擇一種來實現;所謂高效,指不使用攻擊難度與防禦代價呈線性關係的防護手段(期望是指數關係);所謂最簡,指不做錦上添花的事情,每個操作和每個資料都有存在的必要性, 一旦去掉某個保護都會對安全性形成致命威脅 ;


(3)趣味性,雖然這不是比賽必須的,但是我每次都準備了一份“劇情介紹”希望參與者有個更好的心情來幹這道題(不是我看不起對手故意放水)。



從設計上講,這兩次的作品都使用了“主盾”+“副盾”的結構。主盾的作用是抵抗最核心的攻擊,而副盾的作用可以理解為是主盾的放大器。類似結構在對稱密碼演算法設計中已經得到了充分的驗證,可以認為是一種最優結構。然而,魔法森林並非密室逃脫的簡單升級,這兩件作品在使用主副盾結構時是有本質區別的: 


(1)鑑於密室逃脫在副盾上出現了2處嚴重bug,導致不到6小時就被攻破 。這次的魔法森林更換了副盾(天書,也就是本文),避免了上次的缺陷;


(2)迄今為止,密室逃脫的主盾還沒有被發現受到了有效的攻擊(可能是因為副盾出現了嚴重bug導致防禦系統崩潰 所以就沒人搞主盾了),但是我自己發現了主盾上的一個缺陷(leakage) 於是這次的魔法森林升級了主盾(魔法門)。形象地說,從橡木盾升級為銅盾,一次鑄造成型。更小,更重,沒有拼接痕跡。 但和上次的橡木盾一樣,都刷了防水層,能夠抵抗攻擊者對主盾的直接侵蝕 ;


(3)增加了第三面護盾——隱身盾(金蛋)。它的作用是保護副盾,它能讓副盾隱身,如果攻擊者找不到副盾,那麼主盾將免受攻擊(嚴格意義上講,它違背了最簡原則,因為即使沒有它,本題也應該安全,所以此盾的難度故意設得很低,大致相當於簽到題,但卻增加了“蛋生雞,雞生蛋”的樂趣話題)。


如果你是在比賽期間看到了此文,那麼恭喜你,你已經找到了副盾,但是, 找到了副盾,並不等於攻破了此題,真正的挑戰,才剛開始~!


Good Luck!



第七題 魔法森林 解題思路


本題解析由看雪論壇Riatre 原創。


看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


觀察


題目(又³)給出了一個 32 位控制檯應用程式,無任何保護。


從讀取輸入的方式來看,非常的看場雪了。



分析


程式明面上的邏輯比較簡單,可參考如下程式碼。


#include <stdio.h>

#include <string.h>

#include <stdlib.h>

#include <stdarg.h>

// 0042E924     kForestEntry

// 00431058     kForestExit

// 0042E914     kLastKey

// 0041EB10     kQGBinOpTable

// 0041E710     kCRC32Table

#include "const_value.inc"

unsigned char *g_control_dword;

char g_hexserial[36];

unsigned char g_serial[16];

// 00401040

void FillTable(unsigned char *input, unsigned int seed, unsigned char *output) {

unsigned char seed_u8[4];

*(unsigned int*)(seed_u8) = seed;

int sum = 0;

for (int i = 0; i < 1958; i++) {

sum = (sum + seed_u8[i & 3]) % 65025;

output[i] = input[i] ^ kQGBinOpTableFlat[sum];

}

}

// inline @ 0040112F, 00401172

unsigned int CRC32(void* _data, int len) {

unsigned char* data = (unsigned char*)_data;

unsigned int crc = 0x5A1C600E;

for (int i = 0; i < len; i++) {

crc = kCRC32Table[(unsigned char)(crc ^ data[i])] ^ (crc >> 8);

}

return crc;

}

// 004010D0

void GenerateControlDword() {

unsigned char* buf0 = (unsigned char *)malloc(0x7A6u);

unsigned char* buf1 = (unsigned char *)malloc(0x7A6u);

g_control_dword = buf1;

if ( !buf0 || !buf1 ) {

printf("\nWrong");

return;

}

memset(buf0, 0, 0x7A6u);

memset(buf1, 0, 0x7A6u);

unsigned int inp_crc = CRC32(g_hexserial, strlen(g_hexserial));

FillTable(kTableInput, inp_crc | 0xA4A4A4A4, buf0);

FillTable(buf0, CRC32(buf0, 0x7A6), buf1);

free(buf0);

}

// 004011E0

void CheckSerialNumber() {

unsigned char buffie[432];

GenerateControlDword();

memset(buffie, 0, sizeof(buffie));

memcpy(buffie, kForestEntry, 16);

for (int block = 1; block < 27; block++) {

unsigned char* cur_block = buffie + block * 16;

unsigned char* last_block = cur_block - 16;

for (int i = 0; i < 16; i++) {

unsigned int i0 = g_control_dword[(block * 16 - 16 + i) * 4];

unsigned int i1 = g_control_dword[(block * 16 - 16 + i) * 4 + 1];

unsigned int i2 = g_control_dword[(block * 16 - 16 + i) * 4 + 2];

unsigned int i3 = g_control_dword[(block * 16 - 16 + i) * 4 + 3];

unsigned int as_be_dword = i3 + ((i2 + ((i1 + (i0 << 8)) << 8)) << 8);

unsigned char cur_byte = last_block[0];

for (int j = 1; j < 16; j++) {

if ( as_be_dword & 1 ) {

cur_byte = kQGBinOpTable[last_block[j]][cur_byte];

} else {

cur_byte = kQGBinOpTable[cur_byte][last_block[j]];

}

as_be_dword >>= 1;

}

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

if ( as_be_dword & 1 ) {

cur_byte = kQGBinOpTable[g_serial[j]][cur_byte];

} else {

cur_byte = kQGBinOpTable[cur_byte][g_serial[j]];

}

as_be_dword >>= 1;

}

cur_block[i] = cur_byte;

}

}

if (memcmp(kForestExit, buffie + 416, 16) != 0) {

printf("\nError! SerialNo!\n\n");

} else {

for (int i = 0; i < 16; i++) {

buffie[208 + i] ^= kLastKey[i];

}

buffie[224] = 0;

printf("\n%s\n", &buffie[208]);

}

free(g_control_dword);

g_control_dword = 0;

}

// 00401490

int ReadSerialNumber() {

char buf[36];

printf("\nPlease input your serial:");

scanf("%33s", g_hexserial);

for (int i = 0; i < 16; i++) {

int res = 0;

if (g_hexserial[i * 2] <= 57) {

res += (g_hexserial[i * 2] - 48) * 16;

} else {

res += (g_hexserial[i * 2] - 55) * 16;

}

if (g_hexserial[i * 2 + 1] <= 57) {

res += g_hexserial[i * 2 + 1] - 48;

} else {

res += g_hexserial[i * 2 + 1] - 55;

}

g_serial[i] = res;

}

sprintf(

buf,

"%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",

g_serial[0], g_serial[1], g_serial[2], g_serial[3],

g_serial[4], g_serial[5], g_serial[6], g_serial[7],

g_serial[8], g_serial[9], g_serial[10], g_serial[11],

g_serial[12], g_serial[13], g_serial[14], g_serial[15]);

return strcmp(g_hexserial, buf) != 0;

}

// 004015E0

int main(int argc, const char **argv) {

if ( ReadSerialNumber() ) {

printf("\nWrong Input!");

system("PAUSE");

return -1;

} else {

CheckSerialNumber();

getchar();

getchar();

return 0;

}

return 0;

}


<!-- TODO(riatre): 這段擴寫一下講清楚一點,順便描述一下輸入是怎麼構造出來的 -->


後面的計算是在 hexdecode 過的輸入上做的,而後面的計算過程又受一個用 hexdecode 之前的輸入上算出來的某種 CRC or 上 0xA4A4A4A4 生成的 table (後文稱作 ControlDword)有關。這大概就是描述中的“雞生蛋,蛋生雞”的部分吧。簡單觀察後面的計算,不妨設其在ControlDword 確定的情況下解唯一。這裡可以控制用來造出這個行為的變數看起來只有kTableInput。看起來作者是先確定了 ControlDword 和後面的解之後再構造的前面的部分。根據之前接觸到的“看場雪”所出的題目來看,作者是個十分喜歡自hi的人,不妨認為ControlDword 就是描述中提到的“天書”,並且其至少包含一段有意義的文字。


注意到 0xA4A4A4A4 包含 12 位,因此實際有效的輸入只有 20 bit,不妨直接列舉一遍,按照原程式中的 GenerateControlDword 的邏輯算出輸出,然後找裡面有 “森林” 兩個字的。結果可以得到如下文字(GBK 編碼,共 1958 位元組):


看雪CTF從入門到存活(二)主盾與副盾


大家好! 我們又見面了! 這一次 我奉上的作品是:密室逃脫系列之二----魔法森林


既然說是系列 顯然與前一作品是有關係的 我們先來理一下它們之間的關聯和區別


(1)首先 它們的設計目標是一致的 都是為了驗證我的執念到底是否可行(在上一期有提到過 這裡不再贅述)


(2)它們都遵循著相同的設計原則:安全 普適 高效 最簡


所謂安全 指的是希望能在看雪CTF中存活?攻擊方不禁手 不假設攻擊者有知識盲區


所謂普適 指保護方法不限定硬體平臺 不限定作業系統 不限定程式語言(但在實際比賽中 我只能選擇一種來實現)


所謂高效 指不使用攻擊難度與防禦代價呈線性關係的防護手段(期望是指數關係)


所謂最簡 指不做錦上添花的事情 每個操作和每個資料都有存在的必要性 一旦去掉某個保護都會對安全性形成致命威脅


(3)趣味性 雖然這不是比賽必須的 但是我每次都準備了一份‘劇情介紹’ 希望參與者有個更好的心情來幹這道題(不是我看不起對手故意放水)


從設計上講 這兩次的作品 都使用了‘主盾’+‘副盾’的結構


主盾的作用是抵抗最核心的攻擊 而副盾的作用可以理解為是主盾的放大器


類似結構在對稱密碼演算法設計中 已經得到了充分的驗證 可以認為是一種最優結構


然而 魔法森林並非密室逃脫的簡單升級


這兩件作品在使用主副盾結構時 是有本質區別的


(1)鑑於密室逃脫在副盾上出現了2處嚴重bug 導致不到6小時就被攻破 這次的魔法森林更換了副盾(天書,也就是本文) 避免了上次的缺陷


(2)迄今為止 密室逃脫的主盾還沒有被發現受到了有效的攻擊(可能是因為副盾出現了嚴重bug導致防禦系統崩潰 所以就沒人搞主盾了)但是我自己發現了主盾上的一個缺陷(leakage) 於是這次的魔法森林升級了主盾(魔法門)


形象地說 從橡木盾升級為銅盾 一次鑄造成型 更小 更重 沒有拼接痕跡 但和上次的橡木盾一樣 都刷了防水層 能夠抵抗攻擊者對主盾的直接侵蝕


(3)增加了第三面護盾——隱身盾(金蛋) 它的作用是保護副盾 它能讓副盾隱身 如果攻擊者找不到副盾 那麼主盾將免受攻擊(嚴格意義上講 它違背了最簡原則 因為即使沒有它 本題也應該安全 所以此盾的難度故意設得很低 大致相當於簽到題 但卻增加了“蛋生雞,雞生蛋”的樂趣話題)如果你是在比賽期間看到了此文 那麼恭喜你 你已經找到了副盾


但是 找到了副盾 並不等於攻破了此題


真正的挑戰 才剛開始


Good Luck!



不妨認為現在 ControlDword 已經確定了,接下來只要看後面的計算即可。


後面的計算只包含一種操作,即查一張 255 x 255 的表,看起來是把某個原群 (Q, •) 的二元運算 • 的結果打成了表而不是直接寫出運算過程,以此來對程式進行混淆。(這也是一種通用方法。TODO:粘幾個link過來)注意到 ControlDword 用來控制運算的左右順序,所以多半這個二元運算不是可交換的,驗證一下可以發現確實如此。同時注意到這張表是一個 Latinsquare,因此它是一個擬群。簡單驗證一下其他容易發現的性質可以發現結合律也不滿足,但存在恰好一個冪等元素 89。


注意到後面一通運算的輸入和輸出都是可見字串,我們只能控制中間混合進去的 serial,這個擬群一定是通過某種形式構造的,具有某種性質,否則很難實現以上這一點。


<!-- TODO(riatre): 補充更多細節 -->


顯然,本題的祕密就藏在這個擬群的構造方式上了。觀察 a•b/a 的結果(這裡中間當然有一番各種嘗試):


[[0, 210, 209, 1, 2, 208, ...],

[210, 1, 208, 82, 127, 80, ...],

[209, 208, 2, 80, 130, 127, ...],

[1, 82, 80, 3, 5, 86, ...],

[2, 127, 130, 5, 4, 133, ...],

[208, 80, 127, 86, 133, 5, ...],

...]


這提示了我們這個擬群可能是 medial 的。驗證一下發現確實如此。閱讀一會兒各種 pdf,發現有一個叫 Bruck-Murdoch-Toyoda theorem 的東西,接下來就是體力活了。(TODO:描述一下體力活是怎麼做的)


經過一番體力活之後,我們可以得到程式裡的 kBinOpTable 的其中一種可能的構造是下面這個玩意:


const int his2our[255] = {1, 182, 30, 108, 59, 211, 242, 34, 16, 240, 130, 137, 35, 168, 88, 215, 117, 197, 24, 166, 228, 56, 64, 63, 91, 216, 45, 94, 159, 14, 100, 141, 129, 43, 21, 123, 118, 205, 188, 92, 210, 154, 120, 237, 74, 245, 235, 244, 116, 17, 146, 142, 53, 226, 37, 20, 93, 85, 41, 195, 126, 26, 2, 67, 31, 55, 209, 224, 4, 202, 155, 49, 254, 44, 122, 131, 70, 114, 77, 18, 69, 136, 145, 80, 175, 46, 32, 163, 66, 0, 25, 171, 86, 161, 82, 170, 157, 42, 158, 198, 50, 72, 110, 68, 217, 234, 214, 152, 179, 218, 147, 201, 9, 19, 241, 11, 22, 222, 103, 121, 10, 52, 239, 207, 149, 183, 164, 248, 193, 212, 172, 236, 23, 135, 178, 150, 189, 185, 39, 128, 13, 81, 169, 230, 173, 180, 38, 225, 15, 48, 102, 57, 132, 251, 134, 40, 107, 3, 51, 199, 27, 250, 186, 62, 187, 71, 65, 6, 139, 101, 5, 227, 153, 213, 79, 89, 176, 247, 112, 181, 125, 206, 208, 97, 252, 12, 246, 87, 243, 8, 113, 96, 177, 83, 60, 223, 238, 84, 58, 124, 184, 231, 232, 253, 221, 36, 33, 249, 106, 143, 219, 160, 156, 140, 99, 78, 229, 105, 28, 144, 151, 73, 196, 127, 111, 190, 220, 200, 76, 167, 115, 192, 7, 203, 95, 148, 54, 29, 194, 47, 138, 191, 98, 233, 174, 165, 119, 133, 61, 75, 104, 109, 162, 90, 204};

int our2his[255]; // inverse map of his2our

int BinOp(int a, int b) {

return our2his[(241 * his2our[a] + 227 * his2our[b]) % 255];

}


注意到對映的過程可以拿出來放到輸入和輸出的時候做,也就是說對映到 Z_{255} 上之後這個運算是線性的,所以可以直接解。


解決


Sage 程式碼。

import struct

import copy

control_dwords = []

with open('tianshu.txt') as fp:

data = fp.read()

for i in xrange(0, len(data) // 4 * 4, 4):

control_dwords.append(struct.unpack('>I', data[i:i+4])[0])

k0, k1, his2our = eval("(241, 227, [1, 182, 30, 108, 59, 211, 242, 34, 16, 240, 130, 137, 35, 168, 88, 215, 117, 197, 24, 166, 228, 56, 64, 63, 91, 216, 45, 94, 159, 14, 100, 141, 129, 43, 21, 123, 118, 205, 188, 92, 210, 154, 120, 237, 74, 245, 235, 244, 116, 17, 146, 142, 53, 226, 37, 20, 93, 85, 41, 195, 126, 26, 2, 67, 31, 55, 209, 224, 4, 202, 155, 49, 254, 44, 122, 131, 70, 114, 77, 18, 69, 136, 145, 80, 175, 46, 32, 163, 66, 0, 25, 171, 86, 161, 82, 170, 157, 42, 158, 198, 50, 72, 110, 68, 217, 234, 214, 152, 179, 218, 147, 201, 9, 19, 241, 11, 22, 222, 103, 121, 10, 52, 239, 207, 149, 183, 164, 248, 193, 212, 172, 236, 23, 135, 178, 150, 189, 185, 39, 128, 13, 81, 169, 230, 173, 180, 38, 225, 15, 48, 102, 57, 132, 251, 134, 40, 107, 3, 51, 199, 27, 250, 186, 62, 187, 71, 65, 6, 139, 101, 5, 227, 153, 213, 79, 89, 176, 247, 112, 181, 125, 206, 208, 97, 252, 12, 246, 87, 243, 8, 113, 96, 177, 83, 60, 223, 238, 84, 58, 124, 184, 231, 232, 253, 221, 36, 33, 249, 106, 143, 219, 160, 156, 140, 99, 78, 229, 105, 28, 144, 151, 73, 196, 127, 111, 190, 220, 200, 76, 167, 115, 192, 7, 203, 95, 148, 54, 29, 194, 47, 138, 191, 98, 233, 174, 165, 119, 133, 61, 75, 104, 109, 162, 90, 204])")

our2his = [None] * 255

for i in xrange(255):

our2his[his2our[i]] = i

kForestEntry = '\xd5\xe2\xca\xc7\xc4\xa7\xb7\xa8\xc9\xad\xc1\xd6\xc8\xeb\xbf\xda'

kForestExit = '\xc9\xad\xc1\xd6\xb5\xc4\xb3\xf6\xbf\xda\xd4\xda\xd5\xe2\xc0\xef'

kForestEntry = map(ord, kForestEntry)

kForestExit = map(ord, kForestExit)

def permute(data):

return map(lambda x: his2our[x], data)

def inverse_permute(data):

return map(lambda x: our2his[x], data)

kForestEntry = permute(kForestEntry)

kForestExit = permute(kForestExit)

class Expr(object):

def __init__(self, var=None):

self.coeff = [0] * 16

self.const = 0

if var is not None:

self.coeff[var] = 1

def fma(self, k, a):

result = copy.deepcopy(self)

if isinstance(a, int):

result.const = (result.const + k * a) % 255

else:

assert isinstance(a, Expr), type(a)

for i in xrange(16):

result.coeff[i] = (result.coeff[i] + k * a.coeff[i]) % 255

result.const = (result.const + k * a.const) % 255

return result

def qg_op(a, b):

return Expr().fma(k0, a).fma(k1, b)

buffie = [None] * 432

buffie[:16] = kForestEntry

serial = [Expr(i) for i in xrange(16)]

def op(a, b, ctrl):

if ctrl & 1:

a, b = b, a

return qg_op(a, b)

for blkno in xrange(1, 27):

last_block = buffie[blkno * 16 - 16:blkno * 16]

for i in xrange(16):

as_be_dword = control_dwords[blkno * 16 - 16 + i]

cur_byte = last_block[0]

for j in xrange(1, 16):

cur_byte = op(cur_byte, last_block[j], as_be_dword)

as_be_dword >>= 1

for j in xrange(16):

cur_byte = op(cur_byte, serial[j], as_be_dword)

as_be_dword >>= 1

buffie[blkno * 16 + i] = cur_byte

mat = []

rhs = []

for i in xrange(16):

vec = map(lambda x: Mod(x, 255), buffie[416 + i].coeff)

val = Mod((kForestExit[i] - buffie[416+i].const) % 255, 255)

# print vec, val

mat.append(vec)

rhs.append(val)

mat = matrix(mat)

rhs = vector(rhs)

solution = mat.solve_right(rhs)

print ''.join(map(chr, inverse_permute(solution))).encode('hex').upper()


執行即可得到答案 64AD6A44B63F9EA5630D4E13335D54A6,驗證可以發現 CRC 也符合,執行一下會發現程式輸出了 “恭喜你走出森林了”。



合作伙伴 

看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路


騰訊安全應急響應中心 

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

看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路

看雪CTF.TSRC 2018 團隊賽 第七題 『魔法森林』 解題思路



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



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












相關文章