李美熹 SPACE


又一个WordPress站点

首页 >全部文章 > 正文内容
2017 CTF 秋季赛 第九题点评及解析思路 看雪.腾讯TSRC-看雪学院

看雪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函数。
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的矩阵迷宫。如下:

迷宫的数据其实只需要1位即可,0表示可通行,1表示不可通行。此迷宫数据采用32位整型数据,随机生成后,再对相应位进行赋值0,1。由于是9*9的迷宫,按照每一行用9bit表示的话,1个32位数据可以表示3*9bit位。这就可以“随心所欲”地选择使用哪个段的9bit表示一行。我这里采用的是row % 3 + col的方式来表示,所以对于不同的行,测试的bit位是不一样的。
如下图,对第0行的5,6,7列置0厉承先,即设置为可通行。

下面显示的为用*0表示的迷宫,以及程序随机生成的经过上面自定义方式混淆后的迷宫数据(未加密):

然后再对上面的混淆数据使用第一阶段所产生的64个运动轨迹的坐标点中的前20个轨迹对40个迷宫数字进行异或加密。(每条轨迹有两个坐标点(ptFrom, ptTo),采用ptTo的(x,y)坐标值对迷宫数组每个值异或操作。)
以第一阶段点的运动轨迹的最后一个坐标点作为迷宫起点,每走一步就产生与迷宫数组的一个坐标映射。这里所说的转换是平面坐标系中点的运动与数组的坐标的转换并不是直接对应的。如下图:

以迷宫数组的入口索引(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<iostream>
#include<string>
#include<Windows.h>
#include<ctime>
#include<boost/crc.hpp>
usingnamespacestd;
intmain()
{

//中间28字符
//702EBB1 C799FB6 7ACD2FC AB89FBA
//342A0C31163EB4C BC80D36327BBCBE14C8C81B060572 712E33C0BFC1CFC3B33C
// 左12的base16编码
strings1 = "342A0C31163E";
// 右20的base16编码
strings2 = "712E33C0BFC1CFC3B33C";
intxch[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解码
bytedec_64[100] = { 0 };
// base32解码
bytedec_32[100] = { 0 };
// base16解码
bytedec_16[100] = { 0 };
// 编码后字串
charenc_str[100] = { 0 };
// 待base32/base16解码部分
byteto_dec[100] = { 0 };
//UINT64 dwcnt = 0;
DWORDdwKeys = 0;
// 经常用到的数字
intnblockLen = 32;
hash<string> hsh;
// 自定义CRC32函数
typedefboost::crc_optimal<32, 0x04C27EB9, 0xFFFFFFFF, 0xFFFFFFFF, true, true>crc_32_self;
crc_32_self crc32;
cout <<"Computing..."<<endl;
DWORDdws = clock();
lstrcpyA(enc_str, s1.c_str());
lstrcpyA(enc_str + 44, s2.c_str());
DWORDdwCrc32 = 0;
DWORDdwOtherKeys = 0;
// 中间32位字符
// B4C BC80D36 327BBCB E14C8C8 1B060572
for(chari1 : {'5', 'B'})
for(chari2 : {'4', 'A'})
for(chari3 : {'6', 'C'})
for(chari4 : {'5', 'B'})
for(chari5 : {'6', 'C'})
for(chari6 : {'8', 'E'})
for(chari7 : {'0', '3'})
for(chari8 : {'9', 'D'})
for(chari9 : {'0', '3'})
for(chari10 : {'6', 'C'})
for(chari11 : {'0', '3'})
for(chari12 : {'2', '7'})
for(chari13 : {'2', '7'})
for(chari14 : {'5', 'B'})
for(chari15 : {'5', 'B'})
for(chari16 : {'6', 'C'})
for(chari17 : {'5', 'B'})
for(chari18 : {'8', 'E'})
for(chari19 : {'1', 'F'})
for(chari20 : {'4', 'A'})
for(chari21 : {'6', 'C'})
for(chari22 : {'8', 'E'})
for(chari23 : {'6', 'C'})
for(chari24 : {'8',陈荣竣 'E'})
for(chari25 : {'1', 'F'})
for(chari26 : {'5', 'B'})
for(chari27 : {'0', '3'})
for(chari28 : {'6', 'C'})
for(chari29 : {'0', '3'})
for(chari30 : {'5', 'B'})
for(chari31 : {'2', '7'})
for(chari32 : {'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_tsz16 = base16Decode(dec_16, enc_str, 64);
if(!sz16) continue;
// 组合成未编码前的字符串
dec_16[nblockLen - 1] ^= dec_16[0];
for(size_tl = 0; l != nblockLen - 1; l++)
dec_16[l] ^= dec_16[l + 1];
for(inti = 0; i < nblockLen; i++)
to_dec[i] = dec_16[xch[i]];
to_dec[0] ^= to_dec[nblockLen - 1];
for(size_tl = nblockLen - 1; l != 0; l--)
to_dec[l] ^= to_dec[l - 1];
to_dec[nblockLen] = 0;
// 先进行base32解码(32字符解码成20字符)
size_tsz32 = base32Decode(dec_32, (char*)to_dec, nblockLen);
if(!sz32) continue;
// 再进行base64解码(20字符解码成15字符)
size_tsz64 = 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();
return0;
}

可以看到,找到了一个解通过了std::hash函数,但是未通过crc32验证飞天神鼠。找到正确解一共花了20分钟左右,枚举完所有解空间一共花了到30分钟左右。
题外话:
当过关此CM后的音乐设定为《喀秋莎》嘿嘿!请你欣赏吧!
输入key为:
r9cOlg1ewH3S08XYu5l1O0W7xCg4Np
成功过关的截图:

温馨提示
每道题结束过后都会看到很多盆友的精彩解题分析过程,因为公众号内容的限制,每次题目过后我们将选出一篇与大家分享。解题方式多种多样,各位参赛选手的脑洞也种类繁多,想要看到更多解题分析的小伙伴们可以前往看雪论坛【CrackMe】版块查看哦乔丽娅!
合作伙伴

腾讯安全应急响应中心
TSRC,腾讯安全的先头兵,肩负腾讯公司外部安全事件的响应处理工作;在这个没有硝烟的战场上,我们与两万多名安全专家并肩而行,捍卫全球亿万用户的信息、财产安全;一直以来,我们怀揣感恩之心,努力构建开放的TSRC交流平台,回馈安全社区;未来,我们将继续携手安全行业精英,探索互联网安全新方向,共建互联网安全生态。

热门推荐| 看雪CTF
看雪.腾讯TSRC 2017 CTF 秋季赛 第一题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第二题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第三题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第四题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第五题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第六题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第七题点评及解析思路
看雪.腾讯TSRC 2017 CTF 秋季赛 第八题点评及解析思路
【火热报名中】看雪Android 安全训练营,11月17号开课啦!
蛰伏17年,看雪社区主办《安全开发者峰会》将于11月18日在北京举行
上一篇:贝瓦儿歌串烧 下一篇:1月内蒙¥低至799!精选的景点,优质的服务,看得见的性价比!-桂平环球蜗牛国旅

繁华落尽 转瞬即逝

我们需要透过一系列的训练来突破关卡,我们需要达到一个不受到过去历史的羁绊的心境,透过这样的心境,进而引导成为一个适合进行前进到战士人,我们需要成为一个完美无缺的战士,我们的目标是遵循着力量进入无限的领域和穿越!