看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

Editor發表於2017-11-13

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

導語

雙十一快樂!大家的手還在嗎?

經過昨晚的狂歡,我們迎來的第九題的尾聲。

今天中午12點,第九題結束。此題依然無人攻破。

本屆CTF到此就結束了。

比賽結果將於下週一宣佈~

首先祝大家光棍節快樂!

you're not single

接下來讓我們一起來看看第九題的點評、出題思路和解析。

看雪評委netwind點評

該題作者把程式碼保護技術運用得爐火純青,製作精良。作者對關鍵的演算法都進行了多重SMC處理,對SMC加密演算法又進行了雙重虛擬化處理,接著對IAT進行了三重跳轉處理,最後作者透過一個迷宮演算法來對註冊碼進行驗證。

第九題作者簡介

作者不問年少,本名蔣超。畢業於西南交大計算機系,現在鐵路行業任資料庫系統工程師。愛好計算機程式設計(熟悉C/c++/Basic等語言)、電影、遊戲、逆向分析、跑步、象棋等。

2008年左右接觸看雪,我認識了很多朋友,謝謝你們的幫助,讓我不斷成長,也學到了很多新的知識!

第九題設計思路

製作背景

九重妖塔為本人精心設計編譯,目的是製作一款經典旗艦版CM。本作品花費了近半年左右時間製作完成。對關鍵的演算法都 進行了多重SMC處理(至少兩重),對SMC加密演算法進行了VM2處理(雙重虛擬化)。最後成品進行了自定義義加殼處理,只是對IAT進行了處理,三重跳轉。開始只是針對32位系統處理,後來應大家要求,改為相容64位的版本。

為製作此CM,本人也專門溫習了一下線性代數、並學習了外殼編寫、VM技術相關知識,花費了不少功夫,當然收穫也頗豐。

在製作此CM期間,得到了看雪大牛們尤其是ccfer的熱心的支援,在此表示感謝!

製作工具:

Python、VS2015、IDA、OD、WinHex等。

用到的庫主要為boost、 Eigen、 WTL、Mbedtls、Ufmod(背景音樂)。

製作過程

製作過程過程十分繁瑣,總體步驟如下:

1、 編譯crackme原始碼,生成原始檔記為X。

2、 對X的指定SMC演算法進行自定義VM處理,儲存出位元組碼vm1,。

3、 修改原始檔,加入vm1,然後編譯確保無誤後,再次對SMC演算法進行自定義VM處理,儲存出位元組碼vm2。

4、 修改原始檔,加入vm2,並儲存出vm1中的switch跳轉表到自定義地址。編譯,確保無誤。

5、用IDA記錄需要修改或校驗的各函式起、止地址,內部SMC的起、止地址以及抽取硬編碼的資料儲存地址。

6、結合5的資料,修復各處SMC,記為Y。

7、對Y進行IAT自定義加密殼處理,進行API地址三級跳轉。(殼的程式碼參考了CyxvcProtect原始碼)

遇到的困難

外殼的編寫、VM處理,在這之前本人一概不會。專門花費時間去學習、研究,在製作Cm的過程中,經歷了軟體無盡的崩潰,尤其是處理VM2轉移到x64系統的過程中,更是崩潰不斷。經歷了N次編譯、除錯。往事不堪回首,那是最難熬的時光!

另外,本人一開始是手工記錄檢視每處的地址起止及內部SMC偏移,結果不僅慢,而且易錯。一度因為第5步導制製作停止,後來發現了IDAPython這個好東西,然後又專門停止製作去學了學。磨刀不誤砍柴工,用指令碼幫忙查詢函式地址果然省心省力有效率!

逆向鎮仙手段

1、 從記憶體載入user32.dll,查詢並呼叫GetWindowTextW、MessageBoxW、SetTimer函式獲取輸入。

目的:使斷點失效,無法定位到關鍵點。

2、使用大量SMC對關鍵程式碼進行加密處理。基本關鍵程式碼都是兩重以上加密。

目的:使IDA的靜態分析失效,不能直接F5檢視演算法流程。

3、 使用OnTimer、OnIdle對硬體斷點DR0~Dr3、採用函式指標,對特定函式的前20位元組軟體斷點進行探測。被探測的函式如下:

GetWindowTextW、 SetTimer、 PostMessageW、 ExitProcess、 PostQuitMessage、 CreateFileW

4、採用boost::signals2訊號/插槽機制來處理上面的探測結果。若發現被除錯,直接斷開OnButtonClick處訊號所連線的函式,並將Leave函式接入訊號中。

若未發現偵錯程式,正常的訊號響應插槽順序為:

1、DecStartGame  2、StartGame  3、EncStartGame

發現偵錯程式後,按下Button的響應訊號對應的插槽為:

Leave

5、記憶體校驗。對OnTimer、OnInitDialog、Initialize函式的程式碼進行自定義crc32計算,並以得到的值作為金鑰解碼StartGame、LetsGo(迷宮驗證)、MMulEqual函式(矩陣方程驗證函式)。

6、檔案檢驗。校驗證整個檔案的程式碼段,計算其自定義crc32值,並作為金鑰解碼OnInitDialog、MoveThere(運動軌跡驗證)

7、演算法修改。對標準演算法的SHA256、Base64/32/16的基本碼錶進行修改,使用自定義碼錶。如下為修改後的SHA1的reset函式。

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

8、VM2技術。應該是當前第一個出現的使用雙重虛化的CrackMe,雖然只是進行簡單的SMC加密演算法的雙重虛化。

9、偏方。SetWinEventHook對OD進行指定“打擊”。採用了論壇中某位大俠的方法,針對OD的特徵窗體,發現即退出。

主要驗證手段

1、將輸入分為A、B兩部分,對A進行平面迷宮驗證+矩陣方程驗證。

2、對A進行std::hash+crc32驗證。

3、以A的sha1值作為key,使用rc6解碼B部分驗證,對B進行9*9*9的三維迷宮驗證。

平面迷宮驗證

取base16編碼後的前20字元用於迷宮驗證,base64(base64前20字母)=40個字母,正好對應於走出迷宮所需要的步數。自定義一個9*9的矩陣迷宮。如下:

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

迷宮的資料其實只需要1位即可,0表示可通行,1表示不可通行。此迷宮資料採用32位整型資料,隨機生成後,再對相應位進行賦值0,1。由於是9*9的迷宮,按照每一行用9bit表示的話,1個32位資料可以表示3*9bit位。這就可以“隨心所欲”地選擇使用哪個段的9bit表示一行。我這裡採用的是row % 3 + col的方式來表示,所以對於不同的行,測試的bit位是不一樣的。

如下圖,對第0行的5,6,7列置0,即設定為可通行。

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

下面顯示的為用*0表示的迷宮,以及程式隨機生成的經過上面自定義方式混淆後的迷宮資料(未加密):

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

然後再對上面的混淆資料使用第一階段所產生的64個運動軌跡的座標點中的前20個軌跡對40個迷宮數字進行異或加密。(每條軌跡有兩個座標點(ptFrom, ptTo),採用ptTo的(x,y)座標值對迷宮陣列每個值異或操作。)

以第一階段點的運動軌跡的最後一個座標點作為迷宮起點,每走一步就產生與迷宮陣列的一個座標對映。這裡所說的轉換是平面座標系中點的運動與陣列的座標的轉換並不是直接對應的。如下圖:

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

以迷宮陣列的入口索引(1,0)建立座標,則平面點的運動對應於迷宮陣列的座標如上面所示。

所以判斷迷宮的出口點其實並不在迷宮陣列中,而是對應於點相對於迷宮出口時在平面內移動的最後一點的座標。(這有點繞,就像是遊戲中大地圖與小地圖關係一樣。)

逆向矩陣方程驗證

此處使用了簡單的矩陣方程: AXB=C,在A、B、C已知的情況下求矩陣X。

取後20個base16字母,再次對其進行base16編碼。然後每兩個字元組成1個數,形成5*4的矩陣。與設定的矩陣異或後當做矩陣X參與計算。

要求出X,在等式兩邊左乘矩陣A的逆、右乘矩陣B的逆即可。然後將得到的結果進行base16解碼即可獲得後20位base16的編碼字元。

九重三維迷宮驗證

以前半段字串A的hash值作為金鑰,解碼第二段3D迷宮遊戲。使用到的有std::hash、自定義crc32、自定義sha1、自定義rc6。

若透過前半段字串的驗證,則解碼成功,進入後半段字串驗證。後半段驗證採用的是9*9*9的三維空間迷宮進行驗證:

A、 玩家初始有100.0的血值

B、  每往下走一層都會遇到BOSS的阻撓,攻擊力為1.1*普通怪的攻擊。

C、  若直接在本層遇到怪物,則受到1.03*普通怪的攻擊。

D、 直接向下穿行若遇到怪,則會受到前層怪1.3倍攻擊加上後一層怪1.2倍攻擊!

E、  最後一層若直接向下穿越,會受到1.5倍於當前攻擊

F、  比較行動步數是否耗完、是否運動到指定點、及玩家的血值是否為1.0來判斷是否成功闖過九層妖塔。

前半段輸入窮舉測試

在獲得了左邊13位base16字母,右邊20位base16字母的情況下,中間的31個字母根據第一階段驗證可知每個移動軌跡可能有兩個字母,所以只能進行2^31窮舉。然後拼合左,右的字母進行base16解碼,再進行尾、首逐位異或,還原字母順序,再進行首、尾逐位異或後即可判斷是否正確。

下面給出窮舉原始碼,在AMD Athlon 7750,4GB, WIN7下測試執行成功。(假設已經透過了迷宮,與矩陣方程運算知道了前半段加密後的左右兩端的字串)

#include "stdafx.h"

#include

#include

#include

#include

#include

using namespace std;

int main()

{

/*

const size_t dir_code[] = {

myHash('0'),myHash('3'),

myHash('2'), myHash('7'),

myHash('4'), myHash('A'),

myHash('5'), myHash('B'),

myHash('6'), myHash('C'),

myHash('1'), myHash('F'),

myHash('8'), myHash('E'),

myHash('9'), myHash('D') };

*/

//中間28字元

//702EBB1 C799FB6 7ACD2FC AB89FBA

//342A0C31163EB4C BC80D36327BBCBE14C8C81B060572 712E33C0BFC1CFC3B33C

// 左12的base16編碼

string s1 = "342A0C31163E";

// 右20的base16編碼

string s2 = "712E33C0BFC1CFC3B33C";

int xch[32] = { 0x0C,  0x01,  0x09,  0x18,  0x00,  0x1B,  0x19,  0x1F,  0x1D,  0x0F,

0x12,  0x1C,  0x0E,  0x0D,  0x17,  0x10,  0x1A,  0x03,  0x14,  0x11,

0x08,  0x13,  0x16,  0x0A,  0x1E,  0x07,  0x06,  0x0B,  0x05,  0x04,

0x02,  0x15 };

// base64解碼

byte dec_64[100] = { 0 };

// base32解碼

byte dec_32[100] = { 0 };

// base16解碼

byte dec_16[100] = { 0 };

// 編碼後字串

char enc_str[100] = { 0 };

// 待base32/base16解碼部分

byte to_dec[100] = { 0 };

//UINT64 dwcnt = 0;

DWORD dwKeys = 0;

// 經常用到的數字

int nblockLen = 32;

hash hsh;

// 自定義CRC32函式

typedef boost::crc_optimal<32, 0x04C27EB9, 0xFFFFFFFF, 0xFFFFFFFF, true, true>crc_32_self;

crc_32_self crc32;

cout << "Computing..." << endl;

DWORD dws = clock();

lstrcpyA(enc_str, s1.c_str());

lstrcpyA(enc_str + 44, s2.c_str());

DWORD dwCrc32 = 0;

DWORD dwOtherKeys = 0;

// 中間32位字元

// B4C BC80D36 327BBCB E14C8C8 1B060572

for (char i1 : {'5', 'B'})

for (char i2 : {'4', 'A'})

for (char i3 : {'6', 'C'})

for (char i4 : {'5', 'B'})

for (char i5 : {'6', 'C'})

for (char i6 : {'8', 'E'})

for (char i7 : {'0', '3'})

for (char i8 : {'9', 'D'})

for (char i9 : {'0', '3'})

for (char i10 : {'6', 'C'})

for (char i11 : {'0', '3'})

for (char i12 : {'2', '7'})

for (char i13 : {'2', '7'})

for (char i14 : {'5', 'B'})

for (char i15 : {'5', 'B'})

for (char i16 : {'6', 'C'})

for (char i17 : {'5', 'B'})

for (char i18 : {'8', 'E'})

for (char i19 : {'1', 'F'})

for (char i20 : {'4', 'A'})

for (char i21 : {'6', 'C'})

for (char i22 : {'8', 'E'})

for (char i23 : {'6', 'C'})

for (char i24 : {'8', 'E'})

for (char i25 : {'1', 'F'})

for (char i26 : {'5', 'B'})

for (char i27 : {'0', '3'})

for (char i28 : {'6', 'C'})

for (char i29 : {'0', '3'})

for (char i30 : {'5', 'B'})

for (char i31 : {'2', '7'})

for (char i32 : {'2', '7'})

{

//// 迴圈計數加1

//dwcnt++;

// 此處直接賦值,比呼叫sprintf_s更快!

enc_str[12] = i1;

enc_str[13] = i2;

enc_str[14] = i3;

enc_str[15] = i4;

enc_str[16] = i5;

enc_str[17] = i6;

enc_str[18] = i7;

enc_str[19] = i8;

enc_str[20] = i9;

enc_str[21] = i10;

enc_str[22] = i11;

enc_str[23] = i12;

enc_str[24] = i13;

enc_str[25] = i14;

enc_str[26] = i15;

enc_str[27] = i16;

enc_str[28] = i17;

enc_str[29] = i18;

enc_str[30] = i19;

enc_str[31] = i20;

enc_str[32] = i21;

enc_str[33] = i22;

enc_str[34] = i23;

enc_str[35] = i24;

enc_str[36] = i25;

enc_str[37] = i26;

enc_str[38] = i27;

enc_str[39] = i28;

enc_str[40] = i29;

enc_str[41] = i30;

enc_str[42] = i31;

enc_str[43] = i32;

//sprintf_s(enc_str, 100, "%s%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%s",

//s1.c_str(), i1, i2, i3, i4, i5, i6, i7, i8, i9, i10, i11, i12, i13, i14, i15,

//i16, i17, i18, i19, i20, i21, i22, i23, i24, i25, i26, i27, i28, i29, s2.c_str());

// 直接進行64位解碼(解碼成32個字元)

size_t sz16 = base16Decode(dec_16, enc_str, 64);

if (!sz16) continue;

// 組合成未編碼前的字串

dec_16[nblockLen - 1] ^= dec_16[0];

for (size_t l = 0; l != nblockLen - 1; l++)

dec_16[l] ^= dec_16[l + 1];

for (int i = 0; i < nblockLen; i++)

to_dec[i] = dec_16[xch[i]];

to_dec[0] ^= to_dec[nblockLen - 1];

for (size_t l = nblockLen - 1; l != 0; l--)

to_dec[l] ^= to_dec[l - 1];

to_dec[nblockLen] = 0;

// 先進行base32解碼(32字元解碼成20字元)

size_t sz32 = base32Decode(dec_32, (char*)to_dec, nblockLen);

if (!sz32) continue;

// 再進行base64解碼(20字元解碼成15字元)

size_t sz64 = base64Decode(dec_64, (char*)dec_32, 20);

if (!sz64) continue;

if (0xF933063C == hsh((char*)dec_64))

{

// 自定義CRC32

crc32.process_bytes((char*)dec_64, 15);

dwCrc32 = crc32.checksum();

crc32.reset();

if (0x5490B744 == dwCrc32)

{

// 透過了兩個hash的解

dwKeys++;

cout << (clock() - dws) / 1000 << " seconds in finding a to_dec=" << (char*)to_dec << ", key=" << (char*)dec_64 << " crc32=" << dwCrc32 << endl;

}

else

{

// 透過了std::hash,但未透過 crc32的解

dwOtherKeys++;

cout << "find a match: to_dec=" << (char*)to_dec << ", key=" << (char*)dec_64 << " ,but does not match crc32. crc32=" << dwCrc32 << endl;

}

}

else

{

// 成功解碼,但未透過hash函式的解

cout << (clock() - dws) / 1000 << " seconds used decoding a to_dec=" << (char*)to_dec << ", key=" << (char*)dec_64 << " failed in passing hash func!" << endl;

}

}

cout << dwKeys << " keys to origin input, " << dwOtherKeys << " keys passed std::hash but failed in passing crc32." << endl;

cout << "Compute finished. Cost " << (clock() - dws) / 1000 << " seconds." << endl;

getchar();

return 0;

}

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

可以看到,找到了一個解透過了std::hash函式,但是未透過crc32驗證。找到正確解一共花了20分鐘左右,列舉完所有解空間一共花了到30分鐘左右。

題外話:

當過關此CM後的音樂設定為《喀秋莎》嘿嘿!請你欣賞吧!

輸入key為:

r9cOlg1ewH3S08XYu5l1O0W7xCg4Np

成功過關的截圖:

看雪.騰訊TSRC 2017 CTF 秋季賽 第九題點評及解析思路

溫馨提示

每道題結束過後都會看到很多盆友的精彩解題分析過程,因為公眾號內容的限制,每次題目過後我們將選出一篇與大家分享。解題方式多種多樣,各位參賽選手的腦洞也種類繁多,想要看到更多解題分析的小夥伴們可以前往看雪論壇【CrackMe】版塊檢視哦!

相關文章