看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

Editor發表於2019-07-03


七月的風,八月的雨,親愛的夥伴,你在哪裡?是否分得清乾溼垃圾?是否解得開KCTF的賽題?


將雨未雨,迎著七月清晨的微風,伸伸胳膊,我們又迎來全新的一天。小區樓下,猝不及防,收到來自清潔阿姨直擊靈魂的一問,“你是什麼垃圾?”“額,我……”微笑jpg.


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


玩笑過後,我們來步入正題,昨天我們對無人攻破的部落衝突進行了解析,今天我們一起來聆聽下迷霧中的琴聲,看你是否能猜透其中奧祕~


題目簡介


題目背景:


清晨的薄霧,溼潤了空氣,沁人心脾。花骨朵上還殘留著晶瑩剔透的露珠,順著花瓣慢慢滴落在地上的甲殼蟲身上。可是如此放鬆的時刻卻十分短暫。霧氣並沒有隨著太陽的升起而逐漸消散,反而越發濃重,不一會就變成了“霧裡看花”。


突然正前方的花叢裡發出了一陣迷亂的琴聲,走過去發現有一群猴子正在彈琴。原來,這是一個外星的琴師養的一群猴子。由於長期的潛移默化,猴子們對琴師的16絃琴產生了濃厚的興趣,經常上去彈彈。


顯然,猴子們的彈奏是有規律的,只是地球人聽不懂而已。而要想通過此關,你就必須要讓這曲子彈出特定的韻味。於是你決定改編一下琴絃的程式,讓猴子們變成真正的程式猿。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


本題共有1420人圍觀,截至比賽結束只有1人攻破此題。是第二賽段很有難度的一道題。


攻破此題的唯一戰隊是:


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


接下來我們來對題目進行解析,破解琴聲中的奧祕。



看雪評委crownless點評


這道題可以從兩個線索入手。


第一個線索是賽題程式的資源節是可執行的,而正常編譯出的程式的資源節沒有執行權,從這種角度入手難度較大。


第二個線索是用肉眼看一看這個程式的資源,可以發現異常的彩色點,進而找到分析程式的突破口。


以上兩條線索,任意發現一條都可以破解程式,饒有趣味。



出題團隊簡介


本題出題戰隊 Archaia :


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


團隊簡介:我們是剛入坑的萌新,請大佬帶帶我們~



設計思路


程式沒有加殼,可以直接跟蹤除錯。


首先通過斷Messagebox找到程式各個判斷的地方,改掉驗證字元長度,驗證字元輸入和驗證CRC的分支後我們發現程式最後跳進了0x004044C8。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


繼續跑,程式直接異常了。


重新開啟程式,對這個地址下斷,根據看記憶體的值可以判斷這裡是一個16B的陣列,而最後程式會跳到這個陣列裡執行程式碼。


通過對程式的分析,可以知道里面有一個CRC48校驗,要構造出能彈出Win的shellcode不難,但碰不上CRC校驗。所以此路不通。


要構造出CRC校驗,就只能用10B完成彈出Win的任務,然後用剩下的6B滿足CRC校驗。然而,當時的暫存器中也並沒有存放”Win”的地址,光是call Messagebox 和push 字串地址就佔掉了10B了。所以,光靠這10B想完成任務也是行不通的。


因此可以肯定:這10B最終將跳轉到其他的地方執行程式碼,而剩下的6B是根據這10B和CRC校驗反推出來的。


那麼我們就得找其他的地方是否可以執行出彈出帶”Win”的Messagebox程式碼,作者是否留下了什麼線索或漏洞。


線索一:這個exe的資源節是可執行的!!!


眾所周知,正常編譯出的exe的資源節沒有執行權。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


然而,這個exe的4個節區都是可讀可寫可執行。這說明了資料區和資源區是可以在執行中修改資料並且可以執行程式碼的!


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


簡單觀察可以發現:資源節中4個位元組為一組,第4個位元組固定是FF,明顯有規律。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


但是,這裡很明顯有不符合規律的資料出現在了資源裡面。


這個地址是:0x1A390h + 2000h + 400000h = 41C390h

在Debug裡面反彙編就能看出這裡是一段彈出messagebox的程式碼。


顯然,那10B應該跳到這裡來!


如果覺得檢視程式各節的許可權,屬於高難度技術手段,還有更簡單的線索二:


線索二:用肉眼看一看這個程式的資源


這個程式沒有什麼其它資源,基本上就是圖示了。用工具提取icon資源區圖示,看看!


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


這裡其他圖片都沒有彩色的點,這裡卻有彩色的點。有彩色點的圖片是Icon的第6張。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


找到資源icon的第6個圖示資源區,注意這個。


根據上下格式這裡有一段二進位制和正常的icon資料不同(正常的格式4位元組一組第4位元組為FF),這也說明了為什麼圖片上有莫名的斑點。


這裡可以拿到檔案地址 1A390h 。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


程式沒有開啟隨機基址 基址位置 0x00400000h。


這個可疑的程式碼地址就有了 0x1A390h + 2000h + 400000h = 41C390h。


以上兩條線索,任意發現一條,都能找到這段程式碼的地址41C390h。


接下來就能看到:


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


這裡是一段執行彈窗的程式。但它只是彈窗,卻沒有‘Win’。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


我們在程式中搜尋”Win”的ASCII(呼叫的A版),發現程式中找不到這個字串,那麼程式裡是通過其他運算來算出這個字串的。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


這裡push了字串而且push之前還有一個4位元組異或暫存器eax的操作, [0x403FC] 的值是主視窗的視窗控制程式碼,當引數傳給MessageBoxA了,並且 彈框結束後jmp回 0x00401331。


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


0x00401331 這個地址正好在校驗完成並跳入陣列執行程式碼之後,可以肯定圖片裡的函式是執行正確序列號用的。


這裡是序列號成功彈出”Win”的執行程式碼, ”Win”的字串長度正好又是4個位元組,那麼41c38b地址的值異或eax 後為 Win的ascii碼加個’\0’。


57 69 6E 00 xor 2F 3F 5A 12 = 78 56 34 12


由於上面的 and al,0xFF 執行後 eax不可能是 78 56 34 12 所以鎖定函式入口在0x0041c398。


Eax 是人為寫入的,所以得出陣列裡的執行10位元組程式碼 mov eax, 0x12345678  jmp 0x0041C398。


對應的機器碼B8 78563412 E9 C67E0100。


剩餘6位元組可從CRC結果中恢復,恢復演算法是:


具體步驟和普通的CRC相同,已知被除數B878563412E9C67E0100????????????,可從crackme中得到除數0x180000030000 (對應恢復時0x80000c000001),餘數0xb2a289cf038b0000,容易解出餘下6位元組。


下一步,  分析CrackMe可知,程式首先接收序列號,用於初始化陣列,交給5個執行緒,經過300輪變換後,這個16位元組的陣列將被作為程式碼執行。

執行緒每輪修改陣列需要知道兩部分引數。


所有執行緒都共用一個相同的PRNG演算法1和種子產生的隨機數列,把每輪產生的隨機數作為引數傳給一個固定的洗牌演算法,得到在[0,15]中的兩個數x,y,決定修改陣列中哪兩個位置的值。


每個執行緒各自還有一個PRNG演算法2和不同的種子,根據每輪產生的隨機數得到L,R和q,由L,R決定執行緒如何修改這兩處的值。5個執行緒的q合在一起改變PRNG內部的狀態。


執行緒對陣列的變換具體是:

將陣列x位上的數字取出,加上L,記為x0

將陣列y位上的數字取出,加上R,記為y0


計算  x1=3*x0+y0

        y1=x0-2*y0


然後將x1放到x位 ,將y1放到y位。


顯然只要知道<x,y,L,R>就可以從本輪陣列還原出上一輪修改之前的陣列的值。


破解者可以自己實現5個執行緒,使用和CrackMe中相同的演算法和初始種子,並在每輪計算完成後記錄下本輪的<x,y,L,R>。


用記錄下來的<x,y,L,R>還原300輪後的正確的16個位元組,得到300輪之前的16個位元組,就可以得到正確的序列號。



解題思路


本題解題思路由看雪論壇風間仁提供:


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


sn長度為32


.text:00401250callds:GetWindowTextLengthA
.text:00401256cmpeax, 20h
.text:00401259jzshortloc_401296


hex2bin(大寫的16進位制)

sn = hex2bin(sn);

sn ^= key;


.text:00401299callx_init_sn
.data:0040401Cg_xor_keydb0E9h, 4Fh, 6Eh, 20h, 78h, 1Ah, 7, 0Fh, 0, 17h, 36h, 9, 0Ah, 7, 1Fh, 0Ch


建立5個執行緒分別計算,5個執行緒的一輪都計算結束後才會進行下一輪計算, 共300輪。


每個執行緒更新sn中的兩個位元組(二元一次方程),把計算中用到的sn的2個索引值和1箇中間變數儲存下來, 用來做逆運算(不用管隨機數什麼的)。


.text:00401900x_start_check


計算過程:


DWORD xorshift32(DWORD x)
{
x ^= x <<13;
x ^= x >>17;
x ^= x <<5;
returnx;
}

// m2, m3
DWORD transform1(DWORD a, DWORD b)
{
DWORD m = a ^ (a >>7);
DWORD n = b ^ (m <<7);
returnm ^ b ^ (n <<6);
}

voidswap8(BYTE& a, BYTE& b)
{
BYTE t = a;
a = b;
b = t;
}

string g_temp_data;

voidone_round(DWORDid)
{
WORD v_00;
WORD v_01;
WORD v_02;
DWORD v_03;
// wait for g_h_ary[id]
if(g_thread_inited[id])
{
v_00 = (WORD)g_4045EC ^ g_rnd0[id];
v_01 = (WORD)g_4045EC ^ g_rnd1[id];
v_02 = (WORD)g_4045EC ^ g_rnd2[id];
v_03 = g_4045EC ^ g_rnd_xorshift[id];
}else
{
g_thread_inited[id] =1;
v_00 =3923;
v_01 =28;
v_02 =1;
v_03 =id+0x1D4B42;
}

BYTE v36[16] = {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E,0x0F};

// Wichmann–Hill
g_rnd0[id] =171* v_00 %30269;
g_rnd1[id] =172* v_01 %30307;
g_rnd2[id] =170* v_02 %30323;
g_rnd_xorshift[id] = xorshift32(v_03);

WORD rnd_sum = (WORD)(g_rnd0[id] + g_rnd2[id] + g_rnd1[id]);
g_40402C[id] += rnd_sum;
g_404040[id] -= rnd_sum;
g_404068[id] *= rnd_sum;
g_40407C[id] -= rnd_sum;
g_404054[id] += rnd_sum;

DWORD v20;
DWORD v44;
DWORD v38;

DWORD v47 = g_404068[id];
DWORD m0 = g_404040[id];
DWORD m1 = g_40407C[id];
DWORD m2 = g_40402C[id];
DWORD m3 = g_404054[id];
DWORD v15 = g_404068[id];

for(intk =0; k <32; k++)
{
v20 = transform1(m2, m3);
v44 = transform1(m0, v20);


DWORD v22 = v47;
v47 = m3;
m2 = v22;

BYTE v21 = (BYTE)(v20 * ((v15 >>2) + m0 +1)) &0x0F;
BYTE v26 = ((BYTE)v44 * ((BYTE)m2 + (BYTE)(m1 >>2) +1)) &0xF;
swap8(v36[v26], v36[v21]);

m0 = m1;
v38 = m1;
m1 = v20;
m3 = v44;
v15 = v47;
}

g_40407C[id] = v20;
g_404054[id] = v44;
g_40402C[id] = m2;
g_404040[id] = v38;
g_404068[id] = v47;

BYTE v28 = (BYTE)g_rnd_xorshift[id];
WORD v28_2 = v28 * v28;
BYTE v33 = g_buf256[(BYTE)(v28_2 >>8) ^ g_buf256[(BYTE)v28_2]];
BYTE sn1_idx = v36[2*id];
BYTE sn0_idx = v36[2*id+1];
g_temp_data.append((char*)&v28,1);
g_temp_data.append((char*)&sn1_idx,1);
g_temp_data.append((char*)&sn0_idx,1);

BYTE sn0 = g_sn[sn0_idx];
BYTE sn1 = g_sn[sn1_idx];
g_sn[sn1_idx] = sn0 +3* (v33 + sn1);
g_sn[sn0_idx] = (BYTE)(v33 + sn1 + g_buf256[v28] -2* sn0);

g_4045F8[id] = v33 ^ g_buf256[v28];
// release g_h
}

voidone_round_rev(PBYTE temp_cur,inti,intk)
{
BYTE v28 = temp_cur[0];
BYTE sn1_idx = temp_cur[1];
BYTE sn0_idx = temp_cur[2];
WORD v28_2 = v28 * v28;
BYTE v33 = g_buf256[(BYTE)(v28_2 >>8) ^ g_buf256[(BYTE)v28_2]];

BYTE new_sn0 = g_sn[sn0_idx];
BYTE new_sn1 = g_sn[sn1_idx];
g_sn[sn1_idx] = (2*new_sn1 + new_sn0 -7*v33 - g_buf256[v28]) *0xB7;
g_sn[sn0_idx] = (new_sn1 -3*new_sn0 +3*g_buf256[v28]) *0xB7;
}

voidtest2()
{
string str = util::hex2bin("11121314212223243132333441424344");
memcpy(g_sn, str.c_str(), str.size());
xor_buf(g_sn,sizeof(g_sn), g_xor_key);
for(inti =0; i <300; i++)
{
intk;
for(k =0; k <5; k++)
{
one_round(k);
}
g_4045EC =0;
for(k =0; k <5; k++)
{
g_4045EC += g_4045F8[k];
}
}
print_buf(g_sn,sizeof(g_sn));
// 0x21, 0x19, 0x4A, 0xB9, 0x7E, 0x63, 0x04, 0x5F, 0xEA, 0xC3, 0xFC, 0x7C, 0x70, 0xC4, 0x80, 0xB2
util::SaveFile(g_temp_data,"temp_data.dat");
}

voidtest2_rev()
{
BYTE buf[16] = {
0x21,0x19,0x4A,0xB9,0x7E,0x63,0x04,0x5F,0xEA,0xC3,0xFC,0x7C,0x70,0xC4,0x80,0xB2
};
memcpy(g_sn, buf,sizeof(buf));
util::LoadFile(g_temp_data,"temp_data.dat");
PBYTE temp = (PBYTE)g_temp_data.c_str();

for(inti =300-1; i >=0; i--)
{
for(intk =5-1; k >=0; k--)
{
PBYTE temp_cur = temp + i *5*3+ k *3;
one_round_rev(temp_cur, i, k);
}
}
xor_buf(g_sn,sizeof(g_sn), g_xor_key);
printf("%s\n", util::bin2hex(g_sn,sizeof(g_sn)).c_str());
}

校驗sn的hash


.text:00401A39 lea edi, [esp+60h+var_31]
...
.text:00401B1B xor eax,18000h
.text:00401B20 xor ecx,0C00h
.text:00401B26 shld ecx, eax,1
.text:00401B2A add eax, eax
.text:00401B2C sub edx,1
.text:00401B2F jnzshortloc_401B11
.text:00401B31 cmp eax,38B0000h
.text:00401B36 jnzshortloc_401B55
.text:00401B38 cmp ecx,0B2A289CFh
.text:00401B3E jnzshortloc_401B55

// multi -> one
__int64hash_v(__int64 v)
{
for(inti =0; i <64; i++)
{
if(v <0)
{
v ^=0x00000C0000018000;
}
v <<=1;
}
returnv;
}

__int64hash_sn(PBYTE sn)
{
__int64 v1 = util::byteswap64(*(__int64*)(sn));
__int64 v2 = util::byteswap64(*(__int64*)(sn+8));
__int64 v;
v = hash_v(v1);
v ^= v2;
v = hash_v(v);
returnv;
}


校驗通過後, 跳向sn執行彈框(介面上提示彈出Win表示正確)。


.text:00401321movecx,[ebp+hDlg]
.text:00401324movg_hDlg,ecx
.text:0040132Amoveax,offsetg_sn
.text:0040132Fjmpeax
.text:00401331movedx,[ebp+hDlg]
.text:00401334pushedx;hWnd
.text:00401335callds:DestroyWindow
.text:0040133Bjmpshortloc_401380


嘗試構造shellcode(未通過hash驗證)。


004044C883C00Caddeax,0C
004044CB6A00push0
004044CD50push eax
004044CE50push eax
004044CF - E91ECEFFFF jmp004012F2
004044D4 Win

83C00C6A005050E91E CE FF FF57696E00


觀察所有MessageBoxA的呼叫, 其中第1個引數都是mov ecx, hDlg; push ecx的形式。


而00401324處卻把hDlg額外儲存到了全域性變數g_hDlg中:


.text:004012E8pushoffsetCaption;lpCaption
.text:004012EDpushoffsetText;lpText
.text:004012F2movecx,[ebp+hDlg]
.text:004012F5pushecx;hWnd
.text:004012F6callds:MessageBoxA

.text:00401321movecx,[ebp+hDlg]
.text:00401324movg_hDlg,ecx
.text:0040132Amoveax,offsetg_sn
.text:0040132Fjmpeax

.data:004043FCg_hDlg


搜尋g_hDlg的引用(搜尋16進位制FC434000),另外找到1處引用(位於資源6.ico中, 所有區段都是RWE的)。


根據提示要彈出Win, 所以跳到這裡時eax必須為0x125A3F2F ^ 0x006E6957(Win) = 0x12345678。


.rsrc:0041C398xords:dword_41C3BB,eax
.rsrc:0041C39Epush0
.rsrc:0041C3A0pushoffsetdword_41C3BB
.rsrc:0041C3A5pushoffsetdword_41C3BB
.rsrc:0041C3AApushg_hDlg
.rsrc:0041C3B0callds:MessageBoxA
.rsrc:0041C3B6jmploc_401331
.rsrc:0041C3BBdword_41C3BBdd125A3F2Fh


再次構造shellcode(未通過hash驗證)。 


004044C8 B878563412mov eax,12345678
004044CD3105BBC34100xordwordptr[41C3BB], eax
004044D3 - E9 C67E0100 jmp0041C39E

B8785634123105BB C34100E9 C67E0100


看來得用到hash驗證了, 固定前10個位元組, 後面6個位元組用z3約束求解

執行指令碼得到: b878563412e9c67e0100a6e11213382f。


逆運算得到sn(正解): 8CF4BD334ACF8F1222B70EA1FF8085D6。


from z3 import *


deffn(n):
v = n
foriinrange(64):
bit = (v>>63) &1
v ^= bit *0xC0000018000
v <<=1
v &=0xFFFFFFFFFFFFFFFF
returnv


defbig_int64_to_str(v):
s =''
foriinrange(8):
s += chr((v>>((7- i) *8)) &0xFF)
returns


deftest1():
a = BitVec('a',64)
b = BitVec('b',64)
v = fn(a)
v ^= b
v = fn(v)
solver = Solver()
solver.add(v ==0xB2A289CF038B0000)
solver.add(a ==0xB878563412E9C67E)
solver.add(((b>>56) &0xFF) ==1)
solver.add(((b>>48) &0xFF) ==0)
print solver.check()
m = solver.model()
print (big_int64_to_str(m[a].as_long()) + big_int64_to_str(m[b].as_long())).encode('hex')
return


deftest():
test1()
return


test()


這裡的mov與jmp並不是一定得緊挨著的, 另外0041C396那一句也可以利用。


兩個多解:


sn: 6A0E470F088F93B27E3366C375FCE132


sn: 8AE0942B5CE0778A643DDBAAE3E46CC3


moveax,0x12345678
jmp0041C398

.rsrc:0041C396andal, 0FFh
.rsrc:0041C398xords:dword_41C3BB,eax




END



看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

主辦方

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

看雪學院(www.kanxue.com)是一個專注於PC、移動、智慧裝置安全研究及逆向工程的開發者社群!建立於2000年,歷經19年的發展,受到業內的廣泛認同,在行業中樹立了令人尊敬的專業形象。平臺為會員提供安全知識的線上課程教學,同時為企業提供智慧裝置安全相關產品和服務。 



看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

合作伙伴

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路


看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

上海紐盾科技股份有限公司(www.newdon.net)成立於2009年,是一家以“網路安全”為主軸,以“科技源自生活,紐盾服務社會”為核心經營理念,以網路安全產品的研發、生產、銷售、售後服務與相關安全服務為一體的專業安全公司,致力於為數字化時代背景下的使用者提供安全產品、安全服務以及等級保護等安全解決方案。



看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路



10大議題正式公佈!第三屆看雪安全開發者峰會重磅來襲!



看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路

看雪.紐盾 KCTF 2019 Q2 | 第八題點評及解題思路小手一戳,瞭解更多


詳情連結:https://ctf.pediy.com/game-team_list-10-6.htm

相關文章