看雪.京东 2018 CTF 第十题 暗风吹雨 点评与解析
比赛快结束了,还未有人提交第十题的答案
在看雪CTF竞赛交流群中引发了大家广泛的讨论,
就在比赛即将结束之时!看雪CTF 熟悉的面孔 ccfer出现
拿下第一血
大家纷纷变身段子手,小编选几个截图给大家分享一下
防守方半道盲人以1血的成绩稳坐第一名
本题过后 风间仁 继续稳居首位,
acdxvfscd 继续稳步上升,从第三名上升至第二名。
poyoten 再进前十名。
风间仁曾在看雪2017 CTF 年中赛中取得第一名
此次究竟能否继续卫冕冠军呢?
让我们拭目以待!
看雪版主&评委 netwind 点评
本题作者运用了几种较为新颖的代码保护技巧,使其变得难以攻破。本题一共5个操作,分别运用了directx异构系统,侧信道,竞态条件,spectre漏洞以及神经网络的方法。同时此题也有缺点,比如验证正确的序列号时间较长,操作之间结合并不特别紧密等等。总体来看,此题构思精巧,特别有创意,希望能给大家带来不一样的破解体验。
看雪.京东 2018 CTF 第十题 作者简介
半道盲人
bbs.pediy.com/user-678397
第十题出题者简介:
看雪ID:半道盲人,一流大学末流学生,知名战队不知名队员。少时近视,其父尝哂之曰半盲,因以为号焉。兴趣广泛,皆驳杂不精。唯擅给大佬递茶而已。
看雪.京东 2018 CTF 第十题 设计思路
序及瞎扯淡
本来之前说好,今年年中开源一个 VM 壳的,不使用密码学强度的算法同时,做到即使开源也难以破解... 但是由于这几个月打了几场国际性比赛,平常偷懒玩游戏,以及处理自己实验室的诸多事情,到现在也没写出来。正值此次看雪 CTF,在下选了几个思路,出到这个 CrackMe 里面,也算是在下对软件保护这个领域未来方向的一些思考和试探吧。
设计哲学
说哲学有点玄,但是这个的确是方向性的东西,而不是各种技巧之类的。
首先,这个 CrackMe 的目标不是为了难。如果单纯为了难度,在下可以使用各种技巧能把这个程序难度再拔高一个层次。恰恰相反,在下尽力把一些不必要的难度去除,比如使用清晰的结构,关闭编译器优化,避免高度内联化,循坏展开,避免使用向量指令,等等。
其次,这次逆向题避开了大多传统的代码保护技术,包括加壳、反调试、虚拟机保护、自修改程序、常量展开、花指令等等。其实,这个 CrackMe 专注于一些新的方法,可能效果不如传统方法,但是希望能给大家更广阔的思路,也是希望大家能多指点在下。
大致流程
本 CrackMe 按照惯例开源,源代码见附件,曾经某届看雪 CTF 在下开源过一些现在看起来非常糟糕的代码,如今回想起还十分愧疚... 这次依旧能力有限,不过代码质量应该勉强能入各位法眼。
这次题目有点俄罗斯套娃,实际上是有 5 个不同的操作拼接而成。而其中两个操作是利用了隐蔽信道的技术,所以这题叫做暗风吹雨,取 暗风吹雨入寒窗之意。
要求输入为 16 个数字字母组合。
Base64 解码成 96 位. 每 32 位为一大组。
对数据进行 5 个操作,分别为 T0, C0, T1, C1, T2. T 的意思是 transform,数据的混淆主要靠 T 操作。C 意为 covert,即更为隐蔽的操作。
将变换后的结果与预设值比较,作为判断 Flag 的依据。
所有的操作设计上都可逆,故而此算法可逆。而每个操作都使用了一种代码保护思路:
T0
T0 里面用到了异构计算。微软于 2012 年左右提出的 C++ AMP,是一个基于 DirectX 的异构计算标准,2016年以后基本已经不在维护了。可是即使如此,其相对于其他标准如 OpenCL 和 CUDA 来说有着很多优势。为了兼容性以及代码体积考虑,故选择了 C++ AMP。
T0 实际上做了一次 GF(2) 上的矩阵乘法。矩阵由随机数生成器由固定种子生成。且其逆矩阵验算后存在,故而也可逆。实际上许多串行算法完全可以挪到 GPU 上,天然的异构指令集,稀缺的文档,是一个非常不错的代码保护方法。
C0
C0 是一个利用 Cache 命中的侧信道来执行的一个 SBox。侧信道的第一个好处,就是隐蔽。如果不是如本程序刻意降低难度的话,对于数据的 cross reference 根本无法溯源到原始信息,加上侧信道甚至可以在进程之间通信,一个高手的设计可以让数据泥牛入海,无迹可寻。
其第二个好处,就是能破坏 Taint 分析。因为污点分析是基于数据流的,Covert Channel 则在显式的数据流之外打通了新的数据流,同理对于 Concolic Execution 也具有相当的抵抗力。
但是缺点也很严重,就是其不稳定性。侧信道很容易受外界因素影响,所以实际应用中会增加诸如 BCH 编码之类的纠错码。而且,难以避免的 RDTSC/RDTSCP 指令也使得这个方法在程序中十分显眼。
T1
T1 里面利用了多线程的 Race Condition,多个线程对同一个数赋值,只有最慢的线程的结果保留了下来,而类似于停机问题,得到任意程序的执行时间是一个不可解的问题。故而可以构造出不同速度的线程,对数据进行不同的操作,从而实现代码混淆。
T1 开了 4 个线程,每个线程的速度和数据的高两位有关。然后对数据的低 30 位进行循环移位和异或。
这种方法虽然是在下提出,但研究的也并不够成熟,具体效果看看实战吧。
C1
C1 实际上是利用了今年的 Spectre 漏洞。目前 Intel 和 AMD 似乎尚无硬件解决的意向,故而可以故意构造出这样一个信道,通过训练分支预测器,使得推测执行能够 touch 到目标内存区域。错误的输入数据无法训练好分支预测,通过检测目标区域是否缓存命中,故而可以做到代码混淆。
T2
T2 里面有很多浮点数运算,实际上是由两个简易的神经网络拼接而成。
第一个网络有32个输入,两个隐藏层都有32个神经元,激活函数是 Sigmoid,然后到8个输出,激活函数是round取整。
这个网络的作用主要是 permutation。
然后, 对于每一个输出,作为第二个小网络的输入。第二个网络有 16 个输出,相当于一个分类器,实际上是一个 SBox。
这个结构被串联在一起,不容易看出。如果注意到,round 函数作为激活函数可以说是非常之罕见,应该就能把两个网络分离开来。
破解思路
破解 T0 需要逆向 Shader 代码,这个得看 MSDN 上的文档。网络上也有相关工具。
T0 中的矩阵是可逆,计算出逆矩阵后乘以程序中的结果,可恢复 T0 的计算。
T1 根据头两位计算出实际有效的线程id,然后据此恢复。
T2 需要观察网络的输出,据此猜测网络的作用,小网络可以直接爆破。
C0 和 C1 实际上看懂了就能做出来,基本没有变换。
技术总结
其实程序无非就是两个方面,数据流和控制流。放在逆向 CrackMe 上面,就是要么跟着输入走,要么跟着代码走。
几个T操作,实际上混淆的是控制流,把代码变成 Direct X Shader Blob,或者变成神经网络的运算,都相当于异构。而 T2 这样,用并行的线程,同时读写一块内存,也是相当于打乱了控制流分析。
几个C操作,则实际上是隐藏了数据流,因为都不是通过 CPU 显式访存访问到的。
如果是多线程,通过 prime & probe 和 race condition 来访存呢?即打乱了代码执行顺序,也打乱了数据流向。这也大概是我那个尚未完成的 VM 代码保护的主要思路之一了。
尾声
首先,很感谢看雪论坛举办的这次比赛,能给我的一些不成熟的想法一个实践的机会,我也很喜欢这个技术交流的平台,衷心希望它越做越好。
其次,很感谢 @poyoten 在我学习逆向的过程中的很多指导,虽然素未蒙面,也算是在下的逆向导师了。还有感谢@swings 老师的亲自授课,虽然我啥都没学会... 但是好歹知道了大佬大概应该长什么样,以及上次 @渡 的信手一笔,给我的逆向学习指点了方向。
最后感谢 @netwind 的许多建议,这次题目一改再改,不断进步,很多都是在其忠告之下完善的。还有曾经 @linhanshi 于我的指点,以及看雪群里各位大佬的讨论,都给了我诸多启发,这里就不再一一感谢了。
在比赛开始之后看到后面 lelfei 有一道 NeuralCrackme,令我感到很惊喜,因为竟然有人也想到了使用神经网络来混淆代码。有一种道不孤矣的感觉。在下和 lelfei 之间并没有交流过这方面的技术,也希望这次比赛结束后多多探讨。这道题里神经网络的部分,也正好算作抛砖引玉了。
附:(阅读原文查看)
main.cpp 是程序源码。
CrackMe.7z 是程序。
reverse.c 是逆向程序的代码。
Flag 为 ThisIsAnEasyFlag
程序在 Windows 7 x64 SP1 上, 安装 MSVC 2015 运行库, Universal CRT,.NET 4.7 后测试通过。
Windows 10 17868 x64 上同样测试通过,其他高于 Windows 7 的系统在安装运行库后应该能正常运行。
不支持 Windows XP。
可执行程序的 SHA-256 为 5c455acfb6c36bd5d271af208e4b51cee617814c7fb07f7b8adc780d834aaa72。
看雪.京东 2018 CTF 第十题解析
*本解析来自看雪论坛 ccfer
这题最大收获就是学习了一下DirectX HLSL,那些amp的函数我也没查到参数,只能无奈的随便猜猜了。
验证流程都在这里:
.text:004035C0 call sub_401DF0 //输入转换base64
.text:004035C5 call sub_401E80 //一些初始化(seed/表/矩阵)
.text:004035CA call @T0@0 //Shader Model 5 Assembly (DirectX HLSL)
.text:004035CF call @C0@0 //没分析,直接单字节穷举,然后查表
.text:004035D4 call @T1@0 //这个fork函数调试难受,容易卡住,最后利用最近学到的输出反馈给输入,很快发现了周期性,然后就好办了
.text:004035D9 call @C1@0 //没分析,直接单字节穷举,然后查表
.text:004035DE call @T2@0 //没分析透彻,仿佛看到了傅里叶变换的味道,直接晚上开机穷举,早上起来出结果了。
大部分时间都花在研究那个T0中那个HLSL验证上了。
执行文件中有两段HLSL,偏移40102Ch处长度4CCh,偏移401530h处长度714h。
可以直接用D3D反汇编:
void test_10_decompile(BYTE *dxbc_ptr,DWORD len)
{
HMODULE hDxd;
DWORD rv = 0;
DWORD blob[0x400];
DWORD D3DCreateBlob;
DWORD D3DDisassemble;
hDxd = LoadLibrary("D3dcompiler_47.dll");
if (hDxd)
{
D3DCreateBlob = (DWORD)GetProcAddress(hDxd,"D3DCreateBlob");
D3DDisassemble = (DWORD)GetProcAddress(hDxd,"D3DDisassemble");
if (D3DDisassemble)
{
rv = ((DWORD (__stdcall *)(DWORD,DWORD *))D3DCreateBlob)(0x400,&blob[0]);
rv = ((DWORD (__stdcall *)(BYTE *,DWORD,DWORD,char *,DWORD *))D3DDisassemble)(dxbc_ptr,len,0,"",&blob[0]);
//返回的反汇编数据在blob结构中,我直接内存抓出来了
}
}
}
偏移40102Ch处第一段:
//
// Generated by Microsoft (R) HLSL Shader Compiler 10.1
//
//
// Buffer Definitions:
//
// cbuffer CB_1
// {
//
// uint _const_buffer_1[28]; // Offset: 0 Size: 436
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim HLSL Bind Count
// ------------------------------ ---------- ------- ----------- -------------- ------
// _GV_buffer_rw_0 UAV byte r/w u0 1
// _GV_buffer_rw_1 UAV byte r/w u1 1
// CB_1 cbuffer NA NA cb1 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Input
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Output
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer CB1[24], immediateIndexed
dcl_uav_raw u0
dcl_uav_raw u1
dcl_input vThreadID.xyz
dcl_temps 2
dcl_thread_group 32, 8, 1
imul [precise(x)] null, r0.x, vThreadID.z, cb1[0].x
imad [precise(x)] r0.x, vThreadID.y, cb1[1].x, r0.x
iadd [precise(x)] r0.x, r0.x, vThreadID.x
ult [precise(y)] r0.y, r0.x, cb1[2].x
if_nz r0.y
udiv [precise(x)] r0.x, r1.x, r0.x, cb1[4].x
iadd [precise(y)] r0.y, r1.x, cb1[21].x
iadd [precise(y)] r0.y, r0.y, cb1[23].x
ishl [precise(y)] r0.y, r0.y, l(2)
ld_raw_indexable [precise(y)](raw_buffer)(mixed,mixed,mixed,mixed) r0.y, r0.y, u1.xxxx
ushr [precise(y)] r0.y, r0.y, r0.x
and [precise(y)] r0.y, r0.y, l(1)
imad [precise(x)] r0.x, cb1[7].x, r0.x, cb1[11].x
iadd [precise(x)] r0.x, r1.x, r0.x
iadd [precise(x)] r0.x, r0.x, cb1[14].x
ishl [precise(x)] r0.x, r0.x, l(2)
store_raw u0.x, r0.x, r0.y
endif
ret
// Approximately 19 instruction slots used
偏移401530h处第二段:
//
// Generated by Microsoft (R) HLSL Shader Compiler 10.1
//
//
// Buffer Definitions:
//
// cbuffer CB_1
// {
//
// uint _const_buffer_1[41]; // Offset: 0 Size: 644
//
// }
//
//
// Resource Bindings:
//
// Name Type Format Dim HLSL Bind Count
// ------------------------------ ---------- ------- ----------- -------------- ------
// _GV_buffer_rw_0 UAV byte r/w u0 1
// _GV_buffer_rw_1 UAV byte r/w u1 1
// _GV_buffer_rw_2 UAV byte r/w u2 1
// CB_1 cbuffer NA NA cb1 1
//
//
//
// Input signature:
//
// Name Index Mask Register SysValue Format Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Input
//
// Output signature:
//
// Name Index Mask Register SysValue Format Used
// -------------------- ----- ------ -------- -------- ------- ------
// no Output
cs_5_0
dcl_globalFlags refactoringAllowed
dcl_constantbuffer CB1[37], immediateIndexed
dcl_uav_raw u0
dcl_uav_raw u1
dcl_uav_raw u2
dcl_input vThreadID.xyz
dcl_temps 3
dcl_indexableTemp x0[3], 4
dcl_thread_group 32, 8, 1
imul [precise(x)] null, r0.x, vThreadID.z, cb1[0].x
imad [precise(x)] r0.x, vThreadID.y, cb1[1].x, r0.x
iadd [precise(x)] r0.x, r0.x, vThreadID.x
ult [precise(y)] r0.y, r0.x, cb1[2].x
if_nz r0.y
udiv [precise(x)] r0.x, r1.x, r0.x, cb1[4].x //r0.x=n/3,r1.x=n%3
mov [precise(x)] x0[0].x, l(-257992108802048.000000) //0xD76AA478
mov [precise(x)] x0[1].x, l(-7545063049677084100000000.000000) //0xE8C7B756
mov [precise(x)] x0[2].x, l(0.000000) //0x242070DB,这几个常数貌似有问题,用https://github.com/James-Jones/HLSLCrossCompiler里的工具反编译对比后修正一下
iadd [precise(y)] r0.y, r1.x, cb1[11].x //r
iadd [precise(y)] r0.y, r0.y, cb1[14].x //r0.y=r
iadd [precise(z)] r0.z, r0.x, cb1[24].x //d
iadd [precise(z)] r0.z, r0.z, cb1[27].x //r0.z=d
mov [precise(w)] r0.w, l(0) //
mov [precise(y)] r1.y, l(32) //
mov [precise(zw)] r1.zw, r0.yyyz //r1.z=r,r1.w=d
mov [precise(x)] r2.x, l(-1) //
loop
breakc_z r2.x
ishl [precise(yz)] r2.yz, r1.wwzw, l(0, 2, 2, 0) //r2.y=4d,r2.z=4r
ld_raw_indexable [precise(y)](raw_buffer)(mixed,mixed,mixed,mixed) r2.y, r2.y, u1.xxxx //r2.y=u1[d]
ld_raw_indexable [precise(z)](raw_buffer)(mixed,mixed,mixed,mixed) r2.z, r2.z, u0.xxxx //r2.z=u0[r]
imad [precise(w)] r0.w, r2.y, r2.z, r0.w //r0.w+=u1[d] * u0[r]
iadd [precise(w)] r1.w, r1.w, cb1[20].x //r1.w += 0x20
iadd [precise(z)] r1.z, r1.z, cb1[7].x //r1.z += 3
iadd [precise(y)] r1.y, r1.y, l(-1) //r1.y--
ult [precise(x)] r2.x, l(0), r1.y //
endloop
ishl [precise(y)] r0.y, l(1), r0.x //r0.y=1<<d
mov [precise(z)] r0.z, x0[r1.x + 0].x //r0.z=k[r]
and [precise(y)] r0.y, r0.y, r0.z //r0.y &= k[r]
bfi [precise(x)] r0.x, l(1), r0.x, r0.w, l(0) //
xor [precise(x)] r0.x, r0.x, r0.y //
iadd [precise(y)] r0.y, r1.x, cb1[34].x //
iadd [precise(y)] r0.y, r0.y, cb1[36].x //
ishl [precise(y)] r0.y, r0.y, l(2) //
imm_atomic_xor [precise(x)] r0.x, u2, r0.y, r0.x //
endif
ret
// Approximately 39 instruction slots used
第一段HLSL就是把输入的12字节按位拆开,利用GPU的96个线程,每个处理一位,大概可以这样模拟:
void hlsl1(DWORD *t,BYTE *in)
{
DWORD i;
for (i=0;i<96;i++)
{
t[i] = (*(DWORD *)&in[(i%3)*4] >> i/3) & 1;;
}
}
第二段HLSL也是利用GPU的96个线程,每个处理一位,实现的模2矩阵乘法,大概可以这样模拟:
void hlsl2(DWORD *t,DWORD *array,BYTE *out)
{
DWORD d,r;
DWORD m;
DWORD i,j;
DWORD b;
DWORD k[3] = {0xD76AA478,0xE8C7B756,0x242070DB,0};
for (i=0;i<96;i++)
{
m = 0;
d = i / 3;
r = i % 3;
for (j=0;j<0x20;j++)
{
m += t[r+j*4] * array[d+j*0x20];
}
b = k[r] & (1 << d);
b ^= (m & 1) << d;
*(DWORD *)&out[r*4] ^= b;
}
}
正规解法应该可以求出逆矩阵,以前有个软件winiso的注册算法用的就是这种模2矩阵,因为3个DWORD可以单独处理,在穷举承受范围内,所以还是穷举半小时出结果。
最终结果:ThisIsAnEasyFlag
合作伙伴
京东集团是中国收入最大的互联网企业之一,于2014年5月在美国纳斯达克证券交易所正式挂牌上市,业务涉及电商、金融和物流三大板块。
京东是一家技术驱动成长的公司,并发布了“第四次零售革命”下的京东技术发展战略。信息安全作为保障业务发展顺利进行的基石发挥着举足轻重的作用。为此,京东信息安全部从成立伊始就投入大量技术和资源,支撑京东全业务线安全发展,为用户、供应商和京东打造强大的安全防护盾。
随着京东全面走向技术化,大力发展人工智能、大数据、机器自动化等技术,将过去十余年积累的技术与运营优势全面升级。面向AI安全、IoT安全、云安全的机遇及挑战,京东安全积极布局全球化背景下的安全人才,开展前瞻性技术研究,成立了硅谷研发中心、安全攻防实验室等,并且与全球AI安全领域知名的高校、研究机构建立了深度合作。
京东不仅积极践行企业安全责任,同时希望以中立、开放、共赢的态度,与友商、行业、高校、政府等共同建设互联网安全生态,促进整个互联网的安全发展。
CTF 旗帜已经升起,等你来战!
扫描二维码,立即参战!
看雪.京东 2018 CTF
看雪2018安全开发者峰会
2018年7月21日,拥有18年悠久历史的老牌安全技术社区——看雪学院联手国内最大开发者社区CSDN,倾力打造一场技术干货的饕餮盛宴——2018 安全开发者峰会,将在国家会议中心隆重举行。会议面向开发者、安全人员及高端技术从业人员,是国内开发者与安全人才的年度盛事。此外峰会将展现当前最新、最前沿技术成果,汇聚年度最强实践案例,为中国软件开发者们呈献了一份年度技术实战解析全景图。
戳下图↓,立即购票,享5折优惠!