查看原文
其他

看雪.京东 2018 CTF 第十二题 破解之道 点评与解析

看雪CTF 看雪学院 2019-05-27

持续的高温天气下的我:

唯一能续命的就是,坐在空调房里吹着冷气,吃着西瓜,看CTF 比赛!

第十二题作者cvcvxk以被16人攻破的成绩,获得第6名的成绩

第12题过后,攻击方

jackandkx在本题表现突出,上升4位,从第9名升至第5名。

距离比赛还有三题!

所剩机会不多了,

谁能最终亲临颁奖典礼现场?

真的是非常期待啊!



看雪版主&评委 netwind 点评


本题设计新颖,没有作任何反调试,依据C++17/14/11的语法特性展开,基本杜绝了IDA静态分析的可能性,以考察选手动态调试能力。本题还考察选手对x64调试器的使用熟练度以及对系统常见动态库的熟悉度。


看雪.京东 2018 CTF 第十一题 作者简介


cvcvxk

bbs.pediy.com/user-48080

第十二题出题者简介:


cvcvxk,目前就职大连暗泉,从事游戏安全,工控安全,区块链安全等相关工作。 


看雪.京东 2018 CTF 第十二题 设计思路



参赛题目:Window Crackme

运行平台:win7 x64,纯64位PE

 

题目答案:KXCTF20189NTDLL9DbgUiContinue9

 

注册方式:

Crackme.exe 注册码

 

注册成功的提示:


 

设计:

1)首先是验证输入长度 30,验证其中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,因为传入参数的原因DbgUiContinue必然返回一个小于0的值

9)验证返回值<0就输出注册成功

10)本题没有作任何反调试工作,只是进行了大量的C++17/14/11的语法特性展开,基本杜绝了IDA静态分析的可能性,以考察选手动态调试能,64位exe 考察选手x64调试器的使用熟练度。

 

答题方法:

1)动态调试找到总长度30,和必须含有三个9

2)继续调试找到9个字母的hash和hash算法,用算法生成62个可输入的字符hash直接查表得到9个字符(如果需要代码,请说明)

3)先暂时patch掉整体Hash验证,以进行下面的分析

4)分析分割得到5个字母长度被LoadLibraryA或者GetModuleHandleA获取的DLL过程,

熟悉系统DLL名称,猜到是NTDLL(考察选手系统常见动态库的熟悉度)

5)然后是根据后面的字符长度,找到NTDLL导出的API名字(PE文件),逐个带入验证整体hash

6)这时就可以得到完整的key

7)至此就解题完毕。





看雪.京东 2018 CTF 第十二题解析——在纸老虎的海洋中寻求破解之道



本解析来自看雪论坛 holing






0x00 前言




一道纸老虎的题目,关键校验函数F5出来有2k多行,看起来可怕难逆,实际上很多没用的代码,解题关键在于定位有用的代码。






0x01 寻找校验函数




随便翻一下发现0x140002E00这个函数很像关键校验函数,F5一看,很可怕,2400多行。xref一下,发现调用他的很可能是main函数,动态调试发现的确是(通过命令行参数识别,这题输入是命令行参数)。再看看校验函数,一开始觉得这题估计做不出了。(做题靠猜233)

 

(话说IDA没法自动识别64位PE的main函数的吗?还是做了混淆?)

 

看了一下加上动态调试发现0x14000E55C很可能是一个strcpy函数

 

然后main函数如下:

if ( argc == 2 )
{
 my_strcpy(input, 260i64, argv[1], 260i64);
 check(&retaddr);
 result = 0;
}
else
{
   //else里面的代码也很恐怖
   //但是题目明显参数不为2就会给出"input like this:crackme.exe mykey"的提示
   //猜测是一个动态生成字符串并输出的功能
}


strcpy会把输入复制到一个全局变量,然后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);
       }
   }
}


得出答案为KXCTF2018,试一下发现不对(怎么可能这么简单


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
   {
       //...
   }


大概逻辑是,满足sub_140002AC0((char*)(&j)) == -5808600853619038060i64的,要有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(sub_140002AC0貌似是一个hash函数,但是又没有雪崩效应)的检查,很明显现在条件不足。


check 5


其实到现在为止,还并没有找到爆破点,只是一直在“猜”。因为一般来讲,一个判断,更加复杂(明显有更复杂的代码)或者更难到达(比方说等于情况,任意输入一般都是不等于的可能性要更大)的一条路径,是正确答案所需要被走到的。前面几个check,也是根据这个“猜”的。

 

现在走下一个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,说明应该没逆错。

 

其中有个strcat把.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;


函数地址貌似是动态生成的,猜测这是一个LoadLibrary函数,x64dbg一看,果然是。

 

然后proc_name的下一次被使用(当然现在还不知道是proc_name

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的函数是GetProcAddr。然后critical是关键,动态调试改eflags发现后面如果critical<0,可以显示正确信息。而如果GetProcAddr返回NULL,else分支中永远会产生>=0的critical值,所以明显是要给个能用的dll名和相应的函数名,使得其调用返回负数。

 

所以flag大概应该是 KXCTF20189AAAAA9XXXXXXXXXXXXX9。

 

但是,windows的API,一般都是返回0或正的,错误返回负那是Linux的API。这里我又卡了很久,想了好多种方法,最后决定还是猜一猜。首先dll名要是5的长度,第一个想到的就是ntdll.dll,然后去System32目录下找,发现还有一个是wow64.dll,其他的感觉有点偏,应该不会用上(目前先这么猜

 

然后把所有长度为13的导出函数弄下来,我用的是IDA(LordPE啥的并没有办法复制...

 

然后通过check 4的那个hash值,来判断。(就先不管负不负了

 

然后注意,windows文件系统不分大小写,所以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);
           }
       }
   }
}


最终输出为KXCTF20189NTDLL9DbgUiContinue9






0x03 多解的可能性




实际上,flag并不一定是要我所说的那种形式,KXCTF20189AAAAA9...9...也是有可能的,其中函数名长度小于13,然后后面的找一个序列使得check 4哈希碰撞,毕竟那么多长度为5的dll,还有那么多导出函数,有没有这种可能性呢?(我是懒的弄了...






合作伙伴



京东集团是中国收入最大的互联网企业之一,于2014年5月在美国纳斯达克证券交易所正式挂牌上市,业务涉及电商、金融和物流三大板块。

 

京东是一家技术驱动成长的公司,并发布了“第四次零售革命”下的京东技术发展战略。信息安全作为保障业务发展顺利进行的基石发挥着举足轻重的作用。为此,京东信息安全部从成立伊始就投入大量技术和资源,支撑京东全业务线安全发展,为用户、供应商和京东打造强大的安全防护盾。

 

随着京东全面走向技术化,大力发展人工智能、大数据、机器自动化等技术,将过去十余年积累的技术与运营优势全面升级。面向AI安全、IoT安全、云安全的机遇及挑战,京东安全积极布局全球化背景下的安全人才,开展前瞻性技术研究,成立了硅谷研发中心、安全攻防实验室等,并且与全球AI安全领域知名的高校、研究机构建立了深度合作。

 

京东不仅积极践行企业安全责任,同时希望以中立、开放、共赢的态度,与友商、行业、高校、政府等共同建设互联网安全生态,促进整个互联网的安全发展。


CTF 旗帜已经升起,等你来战!

扫描二维码,立即参战!




看雪.京东 2018 CTF 


看雪.京东 2018 CTF 第一题点评与解析

看雪.京东 2018 CTF 第二题点评与解析

看雪.京东 2018 CTF 第三题点评与解析

看雪.京东 2018 CTF 第四题点评与解析

看雪.京东 2018 CTF 第五题点评与解析

看雪.京东 2018 CTF 第六题点评与解析

看雪.京东 2018 CTF 第七题 密室逃脱 点评与解析

看雪.京东 2018 CTF 第八题 薛定谔之猫 点评与解析

看雪.京东 2018 CTF 第九题 PWN-羞耻player 点评与解析

看雪.京东 2018 CTF 第十题 暗风吹雨 点评与解析

看雪.京东 2018 CTF 第十一题 PWN-3pigs 点评与解析



看雪2018安全开发者峰会

2018年7月21日,拥有18年悠久历史的老牌安全技术社区——看雪学院联手国内最大开发者社区CSDN,倾力打造一场技术干货的饕餮盛宴——2018 安全开发者峰会,将在国家会议中心隆重举行。会议面向开发者、安全人员及高端技术从业人员,是国内开发者与安全人才的年度盛事。此外峰会将展现当前最新、最前沿技术成果,汇聚年度最强实践案例,为中国软件开发者们呈献了一份年度技术实战解析全景图。



戳下图↓,立即购票,享5折优惠!








戳原文,立刻加入战斗!


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存