导语
双十一快乐!大家的手还在吗?
经过昨晚的狂欢,我们迎来的第九题的尾声。
今天中午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
#include
#include
#include
#include
using namespace std;
int main()
{
/*
const size_t dir_code[] = {
myHash('0'),myHash('3'),
myHash('2'), myHash('7'),
myHash('4'), myHash('A'),
myHash('5'), myHash('B'),
myHash('6'), myHash('C'),
myHash('1'), myHash('F'),
myHash('8'), myHash('E'),
myHash('9'), myHash('D') };
*/
//中间28字符
//702EBB1 C799FB6 7ACD2FC AB89FBA
//342A0C31163EB4C BC80D36327BBCBE14C8C81B060572 712E33C0BFC1CFC3B33C
// 左12的base16编码
string s1 = "342A0C31163E";
// 右20的base16编码
string s2 = "712E33C0BFC1CFC3B33C";
int xch[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解码
byte dec_64[100] = { 0 };
// base32解码
byte dec_32[100] = { 0 };
// base16解码
byte dec_16[100] = { 0 };
// 编码后字串
char enc_str[100] = { 0 };
// 待base32/base16解码部分
byte to_dec[100] = { 0 };
//UINT64 dwcnt = 0;
DWORD dwKeys = 0;
// 经常用到的数字
int nblockLen = 32;
hash hsh;
// 自定义CRC32函数
typedef boost::crc_optimal<32, 0x04C27EB9, 0xFFFFFFFF, 0xFFFFFFFF, true, true>crc_32_self;
crc_32_self crc32;
cout << "Computing..." << endl;
DWORD dws = clock();
lstrcpyA(enc_str, s1.c_str());
lstrcpyA(enc_str + 44, s2.c_str());
DWORD dwCrc32 = 0;
DWORD dwOtherKeys = 0;
// 中间32位字符
// B4C BC80D36 327BBCB E14C8C8 1B060572
for (char i1 : {'5', 'B'})
for (char i2 : {'4', 'A'})
for (char i3 : {'6', 'C'})
for (char i4 : {'5', 'B'})
for (char i5 : {'6', 'C'})
for (char i6 : {'8', 'E'})
for (char i7 : {'0', '3'})
for (char i8 : {'9', 'D'})
for (char i9 : {'0', '3'})
for (char i10 : {'6', 'C'})
for (char i11 : {'0', '3'})
for (char i12 : {'2', '7'})
for (char i13 : {'2', '7'})
for (char i14 : {'5', 'B'})
for (char i15 : {'5', 'B'})
for (char i16 : {'6', 'C'})
for (char i17 : {'5', 'B'})
for (char i18 : {'8', 'E'})
for (char i19 : {'1', 'F'})
for (char i20 : {'4', 'A'})
for (char i21 : {'6', 'C'})
for (char i22 : {'8', 'E'})
for (char i23 : {'6', 'C'})
for (char i24 : {'8', 'E'})
for (char i25 : {'1', 'F'})
for (char i26 : {'5', 'B'})
for (char i27 : {'0', '3'})
for (char i28 : {'6', 'C'})
for (char i29 : {'0', '3'})
for (char i30 : {'5', 'B'})
for (char i31 : {'2', '7'})
for (char i32 : {'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_t sz16 = base16Decode(dec_16, enc_str, 64);
if (!sz16) continue;
// 组合成未编码前的字符串
dec_16[nblockLen - 1] ^= dec_16[0];
for (size_t l = 0; l != nblockLen - 1; l++)
dec_16[l] ^= dec_16[l + 1];
for (int i = 0; i < nblockLen; i++)
to_dec[i] = dec_16[xch[i]];
to_dec[0] ^= to_dec[nblockLen - 1];
for (size_t l = nblockLen - 1; l != 0; l--)
to_dec[l] ^= to_dec[l - 1];
to_dec[nblockLen] = 0;
// 先进行base32解码(32字符解码成20字符)
size_t sz32 = base32Decode(dec_32, (char*)to_dec, nblockLen);
if (!sz32) continue;
// 再进行base64解码(20字符解码成15字符)
size_t sz64 = 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();
return 0;
}
可以看到,找到了一个解通过了std::hash函数,但是未通过crc32验证。找到正确解一共花了20分钟左右,枚举完所有解空间一共花了到30分钟左右。
题外话:
当过关此CM后的音乐设定为《喀秋莎》嘿嘿!请你欣赏吧!
输入key为:
r9cOlg1ewH3S08XYu5l1O0W7xCg4Np
成功过关的截图:
温馨提示
每道题结束过后都会看到很多盆友的精彩解题分析过程,因为公众号内容的限制,每次题目过后我们将选出一篇与大家分享。解题方式多种多样,各位参赛选手的脑洞也种类繁多,想要看到更多解题分析的小伙伴们可以前往看雪论坛【CrackMe】版块查看哦!