算法分析:XCTF 4th-WHCTF-2017
1.下載附件是一個(gè)32位無殼console的exe,運(yùn)行一遍發(fā)現(xiàn)基本邏輯就是提示輸入一個(gè)字符串,輸入以后通過一個(gè)判斷來提示不同內(nèi)容

2.既然是exe,那先用OD跑一邊
①查看是否有關(guān)鍵字符串
發(fā)現(xiàn)關(guān)鍵字符“Wrong!”,這個(gè)字符就在我們輸入錯(cuò)誤后提示的,可以看第一大步的圖,雙擊找到這個(gè)字符串的引用(如下圖)

②大概分析引用“Wrong!”的代碼段

發(fā)現(xiàn)跳轉(zhuǎn)到輸出“Wrong!”函數(shù)的jnz在00401341,跳轉(zhuǎn)條件就是eax不等于1,那說明只要eax只要等于1則就不跳轉(zhuǎn),執(zhí)行輸出“Right!flag is your input”,那現(xiàn)在我們需要做的就是逆推EAX的來源(如上圖)
③逆推EAX的來源
00401337? |.? 8B4424 08???? mov eax,dword ptr ss:[esp+0x8]?????????? ;? kernel32.BaseThreadInitThunk
0040133B? |.? 83C4 08?????? add esp,0x8
0040133E? |.? 83F8 01?????? cmp eax,0x1
00401341? |.? 75 07???????? jnz short 1c40a4a4.0040134A
00401343? |.? 68 90A04000?? push 1c40a4a4.0040A090?????????????????? ;? Right!flag is your input\n
00401348? |.? EB 05???????? jmp short 1c40a4a4.0040134F
0040134A? |>? 68 C0A04000?? push 1c40a4a4.0040A0C0???????????? ??????;? Wrong!\n
0040134F? |>? E8 1C000000?? call 1c40a4a4.00401370
閱讀代碼可以看到第一步跟蹤到的eax是在00401337處被ss:[esp+0x8]賦值,那我們斷點(diǎn)到00401337查看ss:[esp+0x8]的值,但是斷點(diǎn)運(yùn)行后居然沒停下來而直接判斷輸出了“Wrong!”,(如下圖)

往上瀏覽代碼,發(fā)現(xiàn)在004012D4出還有一個(gè)Wrong字符串和提示我們輸入的信息,那說明這里有有一個(gè)初步的對(duì)我們輸入的判斷(如下圖)

004012A4? |.? 68 D0A04000?? push 1c40a4a4.0040A0D0?????????????????? ;? Please input flag:
004012A9? |.? E8 C2000000?? call 1c40a4a4.00401370
004012AE? |.? 8D4424 0C???? lea eax,dword ptr ss:[esp+0xC]
004012B2? |.? 50??????????? push eax
004012B3? |.? 68 C8A04000?? push 1c40a4a4.0040A0C8?????????????????? ;? %31s
004012B8? |.? E8 7A010000?? call 1c40a4a4.00401437
004012BD? |.? 8D7C24 14???? lea edi,dword ptr ss:[esp+0x14]
004012C1? |.? 83C9 FF?????? or ecx,0xFFFFFFFF
004012C4? |.? 33C0????????? xor eax,eax
004012C6? |.? 83C4 0C?????? add esp,0xC
004012C9? |.? F2:AE???????? repne scas byte ptr es:[edi]
004012CB? |.? F7D1????????? not ecx
004012CD? |.? 49??????????? dec ecx
004012CE? |.? 5F??????????? pop edi
004012CF? |.? 83F9 13?????? cmp ecx,0x13
004012D2? |. ?74 1D???????? je short 1c40a4a4.004012F1
004012D4? |.? 68 C0A04000?? push 1c40a4a4.0040A0C0?????????????????? ;? Wrong!\n
004012D9? |.? E8 92000000?? call 1c40a4a4.00401370
004012DE? |.? 68 B8A04000?? push 1c40a4a4.0040A0B8?????????????????? ;? pause
004012E3? |.? E8 B9000000?? call 1c40a4a4.004013A1
?
閱讀上面代碼可以知道要執(zhí)行輸出“Wrong!”則使得004012D2處不跳轉(zhuǎn),那我們的目的是要讓他不執(zhí)行,所以要使得ecx等于0x13(19),經(jīng)過我對(duì)004012D2處斷點(diǎn)測(cè)試得到這個(gè)0x13(19)就是要求輸入的長(zhǎng)度等于19
那我們繼續(xù)往下走,發(fā)現(xiàn)004012D2處的跳轉(zhuǎn)跳到了004012FA1(如下圖)

那我們斷點(diǎn)來到這個(gè)004012D2函數(shù)(記得輸入19個(gè)字符)(如下圖)
跳轉(zhuǎn)成功以后執(zhí)行了CreatfeFileA函數(shù),第一個(gè)參數(shù)便是文件名“Your_input”,那我們執(zhí)行完CreateFileA后exe就會(huì)在它所在目錄創(chuàng)建一個(gè)叫作Your_input的文件(如果文件在則不創(chuàng)建),最后CreatfeFileA函數(shù)返回Your_input文件句柄值,返回的值放在eax中(通常函數(shù)返回值都是存在eax中,這里就是)

繼續(xù)往下走(如下圖)
程序繼續(xù)執(zhí)行WriteFile函數(shù),第一個(gè)函數(shù)便是待寫入文件的文件的句柄值(hfile),第二個(gè)函數(shù)便是待寫入的數(shù)據(jù)存儲(chǔ)地址(Buffer),第三個(gè)便是要寫入的字節(jié)數(shù)(nBytesToWrite ),那就可以知道exe是想把0019FF14處的內(nèi)容寫19個(gè)字節(jié)到剛才生成的Your_input文件中,查看0019FF14地址便可以看到寫入的字符就是我們輸入的19個(gè)字符


?
那我們先執(zhí)行完WriteFile函數(shù)去看看是否存在Your_input文件并且寫入成功了(如下圖)


打開Your_input文件以后發(fā)現(xiàn)里面的內(nèi)容根本就不是我們寫入的1234567890123456789啊,而是一些其他字符但是WriteFile寫入的字符串就是1234567890123456789,所以WriteFile指定出現(xiàn)了問題,我們跟進(jìn)WriteFile看下函數(shù)(如下圖)
?

?
跟進(jìn)WriteFile內(nèi)發(fā)現(xiàn)居然是個(gè)jmp?。?!,那說明這個(gè)函數(shù)被做過手腳啊,通常這種寫jmp的都是對(duì)這個(gè)函數(shù)進(jìn)行了HOOK,所以,我們跳到00401080去看看到底發(fā)生了啥?(如下圖)

果然是對(duì)WriteFile函數(shù)進(jìn)行了HOOK,在執(zhí)行了401000、401140函數(shù)后才執(zhí)行了
WriteFile函數(shù),那決定最終寫入Your_input文件的字符串重點(diǎn)就是在這兩個(gè)函數(shù)了,所以我們就跟進(jìn)401000、401140兩個(gè)函數(shù)進(jìn)行分析,此時(shí)為了方便分析我們采用IDA來分析
拖進(jìn)IDA pro 直接按G跳轉(zhuǎn)到401000(如下圖)

401000函數(shù)代碼
int __cdecl sub_401000(int a1, int a2)
{
? char i; // al
? char v3; // bl
? char v4; // cl
? int v5; // eax
?
? for ( i = 0; i < a2; ++i )
? {
??? if ( i == 18 )
??? {
????? *(_BYTE *)(a1 + 18) ^= 0x13u;
??? }
??? else
??? {
????? if ( i % 2 )
??????? v3 = *(_BYTE *)(i + a1) - i;
????? else
??????? v3 = *(_BYTE *)(i + a1 + 2);
????? *(_BYTE *)(i + a1) = i ^ v3;
??? }
? }
? v4 = 0;
? if ( a2 <= 0 )
??? return 1;
? v5 = 0;
? while ( byte_40A030[v5] == *(_BYTE *)(v5 + a1) )
? {
??? v5 = ++v4;
??? if ( v4 >= a2 )
????? return 1;
? }
? return 0;
}
401000兩個(gè)參數(shù)a1,a2是什么呢?如下圖

可以看到esi是我們輸入字符的長(zhǎng)度,edi是我們輸入的字符長(zhǎng)度,但是這里有個(gè)入棧細(xì)節(jié)
0040108C?? .? 56?????????????????? push esi
0040108D?? .? 57????????????????? ?push edi
0040108E?? .? E8 6DFFFFFF????????? call 1c40a4a4.00401000
這里的推入?yún)?shù)入棧是從參數(shù)的左邊開始向右邊推入,例如:
//c調(diào)用約定
void add(a,b){}
//對(duì)應(yīng)匯編
push b
push a
call add
所以esi是ida中偽代碼的a2,也就是我們輸入字符的長(zhǎng)度,edi是ida中偽代碼的a1,也就是我們輸入字符所在的地址,那么此時(shí)我們閱讀401000函數(shù)偽代碼,就可以得到邏輯:
1.循環(huán)a2(0x13)次,循環(huán)體內(nèi):判斷此時(shí)的循環(huán)次數(shù)是否為19次,如果是第19次循環(huán)的話則將我們輸入的字符串第19位字符a[18]與0x13異或再返回第19位,如果循環(huán)次數(shù)取模2不為0的話則將a[i+a1]-i的值賦值給v3,否則將a[i+a1+2]賦值給v3,最后,無論當(dāng)前循環(huán)次數(shù)是否取模2等于0都將v3異或循環(huán)次數(shù)i的值賦值給a[i]
2.循環(huán)結(jié)束后判斷a2(我們輸入字符的長(zhǎng)度)是否小于等于0,滿足的話則推出函數(shù)返回1,但是看來這個(gè)判斷是毫無意義的
3.判斷我們輸入的字符串的每一位字符是否等于byte_40A030數(shù)組中的每一個(gè)對(duì)應(yīng)的值,,等于的話就一直循環(huán),知道循環(huán)了strlen(byte_40A030)后退出函數(shù)返回1,那我們查看一下byte_40A030數(shù)組是多少?(如圖)
?

strlen(byte_40A030)就是等于我們輸入字符串的正確長(zhǎng)度(0x13),其實(shí)這里可以有很大程度可以確定byte_40A030就是flag最后的加密結(jié)果,但是為了嚴(yán)謹(jǐn)我們還是通過函數(shù)調(diào)用來具體分析

如上圖,找到了401000函數(shù)的調(diào)用者401080,我們分析一下401080的邏輯:
定義整數(shù)變量v5接受401000的返回值(剛才分析過,只有0或1),接下來執(zhí)行401140函數(shù),跟進(jìn)去看以后就只是一個(gè)HOOK相關(guān)的功能,不影響數(shù)據(jù)
最下面有一個(gè)判斷,如果v5不為0則將lpNumberOfBytesWritten指向的值賦值為1
if ( v5 )
*lpNumberOfBytesWritten = 1;
?
往上一看lpNumberOfBytesWritten就是WriteFile中的第三個(gè)參數(shù),也就是我們?cè)O(shè)定寫入文件的字節(jié)數(shù),從這里就得不到更多信息了,所以我們現(xiàn)在需要找到WriteFile函數(shù)的調(diào)用者,如下圖

主函數(shù)調(diào)用了WriteFile函數(shù),我們先理一下主函數(shù)的邏輯是什么?
if ( NumberOfBytesWritten == 1 )
????? sub_401370(aRightFlagIsYou);
??? else
????? sub_401370(aWrong);
?
通過跟入sub_401370函數(shù)發(fā)現(xiàn)該函數(shù)就是一個(gè)printf函數(shù),參數(shù)就是輸出的字符串變量,aRightFlagIsYou變量是提示我們輸入的flag正確的字符串,aWrong字符串是提示我們輸入的flag是錯(cuò)誤的字符串,而要想執(zhí)行提示我們輸入正確則需要使得NumberOfBytesWritten == 1,這個(gè)NumberOfBytesWritten 就是HOOK了WriteFile的函數(shù)中做出的賦值更改,但是在主函數(shù)中查案代碼發(fā)現(xiàn)NumberOfBytesWritten 還參與了sub_401240函數(shù),而且還是在hook之后,也就是說有可能這個(gè)sub_401240函數(shù)更改了NumberOfBytesWritten ,那到底sub_401240函數(shù)是否對(duì)NumberOfBytesWritten 做出更改還是得跟進(jìn)去才知道如下圖

a1是我們輸入得字符串所在的地址,a2是NumberOfBytesWritten所在的地址,閱讀代碼邏輯:
將字符串v4="This_is_not_the_flag"中v4[a1 - v4 + result]元素與v4[result]對(duì)比,如果相等則循環(huán)以下代碼:
if ( ++result >= (int)(v3 - 1) )
????? {
??????? if ( result == 21 )
??????? {
????????? result = (int)a2;
????????? *a2 = 1;
??????? }
??????? return result;
????? }
從上面代碼中可以看出唯一對(duì)NumberOfBytesWritten操作且等于1的地方只有for中的if滿足條件執(zhí)行,那我們就想辦法去執(zhí)行這個(gè)*a2 = 1,但是再仔細(xì)讀代碼后發(fā)現(xiàn)這個(gè)函數(shù)中我們輸入的字符串未參與任何運(yùn)算,也得不到一個(gè)中繼的加密值,所以我們無法從這個(gè)函數(shù)中獲得任何切確的信息,而且一看v4字符串是"This_is_not_the_flag",這很有可能說明作者寫這個(gè)函數(shù)是用來誤導(dǎo)我們的,所以綜上所述我們只能去找NumberOfBytesWritten的另一個(gè)函數(shù),也就是之前我們分析過的sub_401000函數(shù),因?yàn)閯偛盼覀兩厦嬉呀?jīng)分析過了,所以先通過sub_401000中的寫出byte_40A030變量來反推出我們輸入的flag因該是多少,解密腳本如下:
#include
#include
?
int main()
{
??????? int v3 = 0;
??????? unsigned char a1[] =
??????? {
??????????????? 0x61, 0x6A, 0x79, 0x67, 0x6B, 0x46, 0x6D, 0x2E, 0x7F, 0x5F,
??????????????? 0x7E, 0x2D, 0x53, 0x56, 0x7B, 0x38, 0x6D, 0x4C, 0x6E, 0x00
??????? };
??????? for (size_t i = 19; i >0; i--)
??????? {
??????????????? if (i == 18)
??????????????? {
??????????????????????? *(BYTE *)(a1 + 18) ^=? 0x13;
?
??????????????? }
??????????????? else
??????????????? {
??????????????????????? v3 = *(BYTE *)(i + a1) ^ i;
??????????????????????? if (i%2)
??????????????????????? {
??????????????????????????????? *(BYTE *)(i + a1) = v3 + i;
?? ?????????????????????}
??????????????????????? else
??????????????????????? {
??????????????????????????????? *(BYTE *)(i + a1 + 2) = v3;
??????????????????????? }
??????????????? }
??????? }
??????? printf("%s",a1);
??????? system("pause");
??????? return 0;
}
?
此時(shí)我們得到了我們應(yīng)該輸入的flag,此時(shí)NumberOfBytesWritten的值已經(jīng)是1了,那我們繼續(xù)帶入下面的sub_401240中看這個(gè)函數(shù)是否會(huì)將NumberOfBytesWritten改為0就可以了,經(jīng)過測(cè)試,sub_401240帶入我們推出的flag也是使得NumberOfBytesWritten變?yōu)?,所以main函數(shù)執(zhí)行了輸出提示正確的功能。
