持續的高溫天氣下的我:
唯一能續命的就是,坐在空調房里吹著冷氣,吃著西瓜,看CTF 比賽!
第十二題作者以被16人攻破的成績,獲得第6名的成績
第12題過后,攻擊方
在本題表現突出,上升4位,從第9名升至第5名。
距離比賽還有三題!
所剩機會不多了,
誰能最終親臨頒獎典禮現場?
真的是非常期待?。?/p>
看雪版主&評委 點評
本題設計新穎,沒有作任何反調試,依據C++17/14/11的語法特性展開,基本杜絕了IDA靜態分析的可能性,以考察選手動態調試能力。本題還考察選手對x64調試器的使用熟練度以及對系統常見動態庫的熟悉度。
看雪.京東 2018 CTF 第十一題 作者簡介
/user-48080
第十二題出題者簡介:
,目前就職大連暗泉,從事游戲安全,工控安全,區塊鏈安全等相關工作。
看雪.京東 2018 CTF 第十二題 設計思路
參賽題目:
運行平臺:win7 x64,純64位PE
題目答案:
注冊方式:
.exe 注冊碼
注冊成功的提示:
設計:
1)首先是驗證輸入長度 30已經加載dll找不到輸入點,驗證其中9出現的次數
2)驗證頭9個輸入字符是否符合規則,因為是單個字母的hash對比,可以根據hash算法窮舉(單個字符只有26*2+10種hash,簡單就能爆破)
3)驗證整體字符hash,此處設計+30長度的杜絕了爆破
4)分割字符1,第一個9后的5個字母全部復制作為DLL名稱,考察懟系統動態庫熟悉程度
5)分割字符2,第二個9到第三個9之間作為API名稱,考察pe文件導出表函數查找能力(長度為13的API函數固定dll導出的名稱收集)
6)PE搜索API加載dll
7)PE搜索API從DLL獲取API
8)調用API,因為傳入參數的原因必然返回一個小于0的值
9)驗證返回值
10)本題沒有作任何反調試工作,只是進行了大量的C++17/14/11的語法特性展開,基本杜絕了IDA靜態分析的可能性,以考察選手動態調試能,64位exe 考察選手x64調試器的使用熟練度。
答題方法:
1)動態調試找到總長度30,和必須含有三個9
2)繼續調試找到9個字母的hash和hash算法,用算法生成62個可輸入的字符hash直接查表得到9個字符(如果需要代碼,請說明)
3)先暫時patch掉整體Hash驗證,以進行下面的分析
4)分析分割得到5個字母長度被或者獲取的DLL過程,
熟悉系統DLL名稱,猜到是NTDLL(考察選手系統常見動態庫的熟悉度)
5)然后是根據后面的字符長度,找到NTDLL導出的API名字(PE文件),逐個帶入驗證整體hash
6)這時就可以得到完整的key
7)至此就解題完畢。
看雪.京東 2018 CTF 第十二題解析——在紙老虎的海洋中尋求破解之道
本解析來自看雪論壇
0x00 前言
一道紙老虎的題目,關鍵校驗函數F5出來有2k多行,看起來可怕難逆,實際上很多沒用的代碼,解題關鍵在于定位有用的代碼。
0x01 尋找校驗函數
隨便翻一下發現這個函數很像關鍵校驗函數,F5一看,很可怕,2400多行。xref一下,發現調用他的很可能是main函數,動態調試發現的確是(通過命令行參數識別,這題輸入是命令行參數)。再看看校驗函數,一開始覺得這題估計做不出了。(做題靠猜233)
(話說IDA沒法自動識別64位PE的main函數的嗎?還是做了混淆?)
看了一下加上動態調試發現很可能是一個函數
然后main函數如下:
if ( argc == 2 )
{
?my_strcpy(input, 260i64, argv[1], 260i64);
?check(&retaddr);
?result = 0;
}
else
{
? ?//else里面的代碼也很恐怖
? ?//但是題目明顯參數不為2就會給出"input like this:crackme.exe mykey"的提示
? ?//猜測是一個動態生成字符串并輸出的功能
}
會把輸入復制到一個全局變量,然后check會去訪問它,并且校驗。
0x02 紙老虎check函數
回到看起來很難逆check函數,思路是,不可能一行行逆這個東西,要找關鍵代碼,即,它在哪里訪問了輸入,怎么訪問的?說的高端點,叫做靜態污點分析。
check 1
那么直接定位到訪問input的地方,首先我看到的是這個:
if ( sub_140002AC0(iter) == -5808510693665524758i64 )// check [0:9]
{
?iter[0] = input[1];
?if ( sub_140002AC0(iter) == -5808494200991101593i64 )
?{
? ?iter[0] = input[2];
? ?if ( sub_140002AC0(iter) == -5808519489758550446i64 )
? ?{
? ? ?iter[0] = input[3];
? ? ?if ( sub_140002AC0(iter) == -5808507395130640125i64 )
? ? ?{
? ? ? ?iter[0] = input[4];
? ? ? ?if ( sub_140002AC0(iter) == -5808522788293435079i64 )
? ? ? ?{
? ? ? ? ?iter[0] = input[5];
? ? ? ? ?if ( sub_140002AC0(iter) == -5808606351177179115i64 )
? ? ? ? ?{
? ? ? ? ? ?iter[0] = input[6];
? ? ? ? ? ?if ( sub_140002AC0(iter) == -5808608550200435537i64 )
? ? ? ? ? ?{
? ? ? ? ? ? ?iter[0] = input[7];
? ? ? ? ? ? ?if ( sub_140002AC0(iter) == -5808609649712063748i64 )
? ? ? ? ? ? ?{
? ? ? ? ? ? ? ?iter[0] = input[8];
? ? ? ? ? ? ? ?if ( sub_140002AC0(iter) == -5808599754107409849i64 )
? ? ? ? ? ? ? ?{ ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ?v42 = 0i64;
? ? ? ? ? ? ? ? ?v37 = 0;
? ? ? ? ? ? ? ? ?v43 = 329472i64;
? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ?else
? ? ? ? ? ? ? ?{
? ? ? ? ? ? ? ? ?v42 = 0x100000000i64;
? ? ? ? ? ? ? ? ?v43 = 3750656i64;
? ? ? ? ? ? ? ?}
? ? ? ? ? ? ? ? ?//其他else一樣
很有可能是一個校驗,動態調試也發現input并未被改變,那么逐字節爆破
signed __int64 __fastcall sub_140002AC0(char *a1)
{
? ?char v1; // dl
? ?signed __int64 result; // rax
? ?signed __int64 v3; // rax
? ?v1 = *a1;
? ?for (result = -3750763034362895579i64; *a1; result = 1099511628211i64 * v3)
? ?{
? ? ? ?++a1;
? ? ? ?v3 = result ^ v1;
? ? ? ?v1 = *a1;
? ?}
? ?return result;
}
signed __int64 arr[9] = { -5808510693665524758i64,
-5808494200991101593i64,
-5808519489758550446i64,
-5808507395130640125i64,
-5808522788293435079i64,
-5808606351177179115i64,
-5808608550200435537i64,
-5808609649712063748i64,
-5808599754107409849i64 };
for (size_t i = 0; i < 9; i++)
{
? ?for (size_t j = 0; j < 256; j++)
? ?{
? ? ? ?if (sub_140002AC0((char*)(&j)) == arr[i])
? ? ? ?{
? ? ? ? ? ?printf("%c", (char)j);
? ? ? ?}
? ?}
}
得出答案為,試一下發現不對(怎么可能這么簡單
check 2
接下來看到的是一個疑似長度的校驗
?do
? ?++len;
?while ( input[len] );
?LODWORD(v307) = len;
?HIDWORD(v307) = 5 - HIDWORD(len);
?v529 = ((unsigned int)len + ((unsigned __int64)HIDWORD(len) << 32)) >> 12;
//無用代碼,稍微有點長
?if ( (len & 0xFFF) + (v529 << 12) == 30 )
? ? ?//...
看起來很復雜的操作,但是稍微看一下加上動態調試一下的話,就能發現這是在檢查長度為30
check 3
緊接著看后面的代碼,又訪問了input
if ( (len & 0xFFF) + (v529 << 12) == 30 )
{
?v8 = 0;
?i_1 = 0;
?if ( len )
?{
? ?iter_input = input;
? ?do
? ?{
? ? ?v184 = 0;
? ? ?v11 = *iter_input;
? ? ?LOBYTE(v184) = v11;
? ? ?v12 = (char *)&v184;
? ? ?v13 = -3750763034362895579i64;
? ? ?if ( v11 )
? ? ?{
? ? ? ?do
? ? ? ?{
? ? ? ? ?v13 = 1099511628211i64 * (v13 ^ v11);
? ? ? ? ?v11 = *++v12;
? ? ? ?}//實際上是內聯了sub_140002AC0
? ? ? ?while ( *v12 );
? ? ?}
? ? ?v14 = v8 + 1;
? ? ?if ( v13 != -5808600853619038060i64 )
? ? ? ?v14 = v8;
? ? ?v8 = v14;
? ? ?++i_1;
? ? ?++iter_input;
? ?}
? ?while ( i_1 < len );
? ?v1 = 32i64;
? ?if ( v14 >= 3 ) ? ? ? ? ? ? ? ? ? ? ? ? ? // 3個以上的9
? ?{
? ? ? ?//...
? ?}
大概邏輯是,滿足((char*)(&j)) == -的,要有3個以上。
同樣爆破一下
for (size_t j = 0; j < 256; j++)
{
? ?if (sub_140002AC0((char*)(&j)) == check3more)
? ?{
? ? ? ?printf("%c", (char)j);
? ?}
}
結果為9
check 4
if?( sub_140002AC0(input) ==?5728707748789076223i64 )// check whole input
{
? ?LODWORD(v46) =?31;
? ?LODWORD(v47) =?0;
}
整個input的hash(貌似是一個hash函數,但是又沒有雪崩效應)的檢查,很明顯現在條件不足。
check 5
其實到現在為止,還并沒有找到爆破點,只是一直在“猜”。因為一般來講,一個判斷,更加復雜(明顯有更復雜的代碼)或者更難到達(比方說等于情況,任意輸入一般都是不等于的可能性要更大)的一條路徑,是正確答案所需要被走到的。前面幾個check已經加載dll找不到輸入點,也是根據這個“猜”的。
現在走下一個check,
memset(dll_name, 0i64, 0x104ui64);
v80 = strstr(input, "9");
v81 = v80;
dll_name[0] = v80[1];
dll_name[1] = v80[2];
dll_name[2] = v80[3];
dll_name[3] = v80[4];
dll_name[4] = v80[5]; ? ? ? ? ? // 第一個9后面取5個字節
mystrcat(dll_name, 260ui64, ".DLL", 260ui64);
v82 = strstr(v81 + 1, "9");
snd9 = v82;
*strstr(v82 + 1, "9") = 0;
memset(&proc_name, 0i64, 260ui64);
my_strcpy(&proc_name, 260i64, snd9 + 1, 260i64); //取第二個9到第三個9之間的字符串
其中這些函數的識別,肯定不是通過靜態逆向,點進去發現一堆SSE就能嚇跑人了,識別是通過猜+調試。
然后這剛好對應大于3個9的check,說明應該沒逆錯。
其中有個把.DLL加在后面,難道是把輸入作為一個dll名?看了一下發現題目并沒有給出dll附件。
那么繼續看先吧,看看這兩個參數是怎么被用的。這后面又有大量的代碼,不管它們,用xref。發現一個調用
module_addr = ((__int64 (__fastcall *)(char *))((char *)v94 + *(unsigned int *)((char *)&v94[*(unsigned __int16 *)((char *)v94 + 2 * v98 + (unsigned int)v95[9])] + (unsigned int)v95[7])))(dll_name);
::module_addr = module_addr;
函數地址貌似是動態生成的,猜測這是一個函數,一看,果然是。
然后的下一次被使用(當然現在還不知道是
v116 = (__int64 (__fastcall *)(_QWORD, signed __int64))((__int64 (__fastcall *)(__int64, char *))((char *)v109 + *(unsigned int *)((char *)&v109[*(unsigned __int16 *)((char *)v109 + 2 * v111 + (unsigned int)v110[9])] + (unsigned int)v110[7])))( module_addr, &proc_name); // GetProcAddr
if ( v116 )
{
? ?len_1 = -1i64;
? ?do
? ? ?++len_1;
? ?while ( input[len_1] );
? ?critical = v116(0i64, len_1);
}
else
{
? ?//...大量垃圾代碼
}
if ( critical >= 0 )
{
? ?//錯誤
}
else
{
? ?//正確
}
動態調試,發現call的函數是。然后是關鍵,動態調試改發現后面如果=0的值,所以明顯是要給個能用的dll名和相應的函數名,使得其調用返回負數。
所以flag大概應該是。
但是,的API,一般都是返回0或正的,錯誤返回負那是Linux的API。這里我又卡了很久,想了好多種方法,最后決定還是猜一猜。首先dll名要是5的長度,第一個想到的就是ntdll.dll,然后去目錄下找,發現還有一個是wow64.dll,其他的感覺有點偏,應該不會用上(目前先這么猜
然后把所有長度為13的導出函數弄下來,我用的是IDA(啥的并沒有辦法復制...
然后通過check 4的那個hash值,來判斷。(就先不管負不負了
然后注意,文件系統不分大小寫,所以ntdll或者NTDLL甚至NtDlL都可以,這個坑了我很久,也是后面才想到的。
具體爆破代碼:
const char* funcs[81] = { "TppTimerpFree","TpReleaseWork","TpReleaseWait","RtlAreBitsSet","LdrpUnloadDll","LdrpSnapThunk","RtlUnlockHeap","RtlLoadString","TppTimerAlloc","EtwEventWrite","RtlStartRXact","RtlAbortRXact","TpWaitForWait","RealSuccessor","RebalanceNode","RtlGetVersion","RtlpNtOpenKey","RtlCopyString","RtlSetAllBits","RtlFreeHandle","ZwQueryObject","NtOpenProcess","NtOpenSection","ZwCreateEvent","ZwSetValueKey","NtCancelTimer","ZwAccessCheck","NtAlertThread","NtCompactKeys","NtCompressKey","ZwConnectPort","ZwCreateTimer","NtCreateToken","ZwFilterToken","ZwOpenSession","ZwQueryEaFile","ZwQueryMutant","NtRequestPort","NtSetUuidSeed","ZwStopProfile","ZwUnloadKeyEx","DbgBreakPoint","DebugService2","RtlFillMemory","RtlZeroMemory","StringCbCopyW","RtlRemoteCall","LdrpCreateKey","PfxFindPrefix","PfxInitialize","IsTimeExpired","WaitForWerSvc","WerpProcessId","DbgUiContinue","RtlpLockStack","RtlApplyRXact","TpReleasePool","TpWaitForWork","LdrReadMemory","ResCHitsFlush","RtlCreateHeap","RtlIdnToAscii","RtlpTpIoAlloc","LdrResRelease","Wow64LogPrint","NameToOrdinal","Wow64FreeHeap","whNtWriteFile","whNtReplyPort","whNtCreateKey","whNtOpenEvent","whNtDeleteKey","whNtLoadKeyEx","whNtOpenKeyEx","whNtOpenTimer","whNtRenameKey","whNtSaveKeyEx","whNtSetEaFile","whNtTestAlert","whNtUnloadKey","Wow64pLongJmp" };
const char* libs[] = { "NTDLL", "WOW64" };
char flag[31] = "KXCTF20189AAAAA9XXXXXXXXXXXXX9";
for (size_t l = 0; l < 2; l++)
{
? ?memcpy(flag + 10, libs[l], 5);
? ?for (size_t b = 0; b < 32; b++)
? ?{
? ? ? ?for (size_t s = 0; s < 5; s++)
? ? ? ?{
? ? ? ? ? ?if (b & (1 << s))
? ? ? ? ? ?{
? ? ? ? ? ? ? ?flag[10 + s] |= 0x20;//轉成小寫
? ? ? ? ? ?}
? ? ? ?}
? ? ? ?for (size_t i = 0; i < 64; i++)
? ? ? ?{
? ? ? ? ? ?memcpy(flag + 16, funcs[i], 13);
? ? ? ? ? ?if (sub_140002AC0(flag) == 5728707748789076223i64)
? ? ? ? ? ?{
? ? ? ? ? ? ? ?printf("%s", flag);
? ? ? ? ? ?}
? ? ? ?}
? ?}
}
最終輸出為
0x03 多解的可能性
實際上,flag并不一定是要我所說的那種形式,...9...也是有可能的,其中函數名長度小于13,然后后面的找一個序列使得check 4哈希碰撞,畢竟那么多長度為5的dll,還有那么多導出函數,有沒有這種可能性呢?(我是懶的弄了...
合作伙伴
京東集團是中國收入最大的互聯網企業之一,于2014年5月在美國納斯達克證券交易所正式掛牌上市,業務涉及電商、金融和物流三大板塊。
京東是一家技術驅動成長的公司,并發布了“第四次零售革命”下的京東技術發展戰略。信息安全作為保障業務發展順利進行的基石發揮著舉足輕重的作用。為此,京東信息安全部從成立伊始就投入大量技術和資源,支撐京東全業務線安全發展,為用戶、供應商和京東打造強大的安全防護盾。
隨著京東全面走向技術化,大力發展人工智能、大數據、機器自動化等技術,將過去十余年積累的技術與運營優勢全面升級。面向AI安全、IoT安全、云安全的機遇及挑戰,京東安全積極布局全球化背景下的安全人才,開展前瞻性技術研究,成立了硅谷研發中心、安全攻防實驗室等,并且與全球AI安全領域知名的高校、研究機構建立了深度合作。
京東不僅積極踐行企業安全責任,同時希望以中立、開放、共贏的態度,與友商、行業、高校、政府等共同建設互聯網安全生態,促進整個互聯網的安全發展。
CTF 旗幟已經升起,等你來戰!
掃描二維碼,立即參戰!
?看雪.京東 2018 CTF
看雪2018安全開發者峰會
2018年7月21日,擁有18年悠久歷史的老牌安全技術社區——看雪學院聯手國內最大開發者社區CSDN,傾力打造一場技術干貨的饕餮盛宴——2018 安全開發者峰會,將在國家會議中心隆重舉行。會議面向開發者、安全人員及高端技術從業人員,是國內開發者與安全人才的年度盛事。此外峰會將展現當前最新、最前沿技術成果,匯聚年度最強實踐案例,為中國軟件開發者們呈獻了一份年度技術實戰解析全景圖。
戳下圖↓,立即購票,享5折優惠!
戳原文,立刻加入戰斗!