看雪 2016 CTF 第七题 点评和解析

回复 星标
更多

看雪 2016 CTF 第七题 点评和解析

看雪版主&评委netwind点评
此题有一定难度,作者通过两个相互监视的反调试线程,阻止攻击者下断、dump数据,加大了破解难度。算法采用了一个取数游戏的算法,用取数过程来生成注册码,攻击者在逆向出算法后,需要花时间消化理解算法,从而获取答案,比较考验攻击者的耐心。遗憾的时候,作者考虑不周全导致出现了多解。
作者简介
sinianluoye,大四学生,热衷于C/C++,算法和数据结构,大三时为了了解C++底层的执行情况接触逆向
作品思路
1. 反调试部分
A. 反调试线程中:
a. 核心的代码通过触发SEH启动
b. 两个反调试线程相互检测,防止反调试线程被挂起/修改
c. 通过对比当前eip所在页面代码和exe文件代码判断代码是否在内存中被修改(比如下了int3断点)
d. 通过读取当前eip和反调试函数所在页面内存属性判断代码是否被下了内存访问断点
e. 通过读取调试寄存器判断是否被下了硬件断点
f. 通过RDTSC判断是否超时
B. 数据部分:
g. 通过修改核心数据的内存属性来防止dump数据
2. 算法的核心思路
问题描述:
Alice和Bob在一个数组的两端分别取数,为了取得数的和最大而努力,他们都足够聪明,问他们将如何取数
解释:
两人取数的过程就是验证码,验证码生成过程如下:
(不好描述,举个栗子)
比如一共10个数取数的过程是在左边取了3个右边取了2个再从左边取了3个然后又从右边取了2个
那么最终的验证码为3232
3. 其他限制
a. 数组长1010单最终取应生成的验证码的前969次取数作为实际的验证码,因为实际上最后步数(41次)默认了去上一次相反的方向
b. 因为存在相等的数据,所以可能生成的验证码有很多,所以我取了两个特征进行特判(长度为85切第10位为'F')
4. 难点:
a. 需要理解算法的含义才能知道问题实际求的是什么,只dump算法是得不到正确答案的
b. 需要有一定的编程功底才能最终算出验证码,暴力枚举是不可行的

破解思路:

(个人逆向功底不到位,只能提供思路了- -,自己写的程序自己破不了实在尴尬)

1.反调试部分:

bp CreateThread 找到反调试函数入口点(两个),挂起全部线程,NOP掉这俩线程,保存文件

2.算法部分:

我直接拿源码说话了

508546

这是验证之前计算值的代码 dfs的含义有dp功底理解起来应该不难

508546

下面选择了一位分析者HighHand的详细分析:
CrackMe含有反调试功能,需要借助StrongOD插件将部分反调试过掉,CrackMe会创建3个线程,其中一个线程为计算序列号,另外两个为反调试检测。通过对CreateThread 下断可以发现
线程1为计算线程

508546

线程2和3为反调试线程

508546

通过将线程2和3挂起可过掉反调试。
排除干扰后,下面分析算法流程
第一步,crackme创建了一个表,记t,大小为0x0FC8字节

508546

通过逆向可得出
t = (unsigned long *)malloc(0x0FC8);
t[0] = 0x6C35B49D;
t[1] = 0xA645500D;
t[2] = 0xCB9E682E;
int v0 = 3;
do
{
t[v0] = t[v0 - 3] + t[v0 - 1] * t[v0 - 2];
v0 += 1;
} while (v0 < 0x03f2);
第二步,通过输入的序列号计算并且得到结果

508546

计算方法为
1.序列号限制

508546

长度必须为 0x55,并且第9个必须为’F’,否则 [13BEE70] = 0,后面会有检查。
2.对每个序列号字符进行处理

508546

序列号组成为 1-9 A-Z a-z

508546

序列号转换到数字(记cnt)后求和,且必须等于969,【关键】后面会有检查。
循环处理cnt次

508546

上图中 [42E1FC] 记作n,初始值为1,每一个字符与0交换一次,也就是最终交换0x55次(序列号长度)。
函数00402769为主要运算逻辑

508546

逆向代码如下

508546

3.继续对结果处理

508546

当m1 <= m2时继续交换求和
以上计算的过程分析完毕,对我们有用的是 xx, m1, m2
第三步,验证结果
这里有4个验证函数,通过IDA反编译分别为

508546

508546

508546

508546

对我们有用的是
1.数据

508546

2.异或
__int64 z1 = x1 ^ x2 ^ y2 ^ y1;
assert(z1 == 0);
3.加减
__int64 z2 = y1 + x2 - x1 - y2;
assert(z2 == 0);
4.限制
cnt == 969; m1<m2
上述为主要的验证手段。
第四步,算法求逆
通过分析该算法无法求逆,具体分析过程为
__int64 k1 = y1 - y2;
__int64 k2 = y2 ^ y1;
表达式为 (x1 - k1) = (k2^x1),发现 x1 不唯一,同时无法确定 m1 和 m2 的值,最终无法求逆。
第五步,枚举序列号
通过上述分析,只有列举序列号了,枚举分两种,一种是正向枚举,另一种是反向枚举,这个crackme无论正反都不会节约计算量。
确认解空间,由于序列号85(0x55)个字符,每个字符为63种(不含0),所以解空间为63的85次方,可以说是巨大的解空间,枚举无望。
还好,cnt必须为969,第9个序列号必须为F,这些限制为我们提供了机会。
选择正向枚举,代码如下:

508546

508546

508546

508546

508546

508546

508546

508546

508546

508546

508546

上述代码使用多线程进行枚举,线程个数取决于输入参数,同时输出结果保存到文件。
输出的文件内容为:
FzzzzzzzzFzzzzzzB11111121111311111111111211111112111111111111111111111111111112111111
2A D8 35 F6 FE 00 00 00 64 E9 83 D8 FA 00 00 00
DzzzzzzzzFzzzzzzB21111111111111111111121111111111111111111112211212111112111111211111
2A D8 35 F6 FE 00 00 00 64 E9 83 D8 FA 00 00 00
输出的16进制为x1 和x2的值

508546

508546

看雪安全 · 看雪众测
持续关注安全16年,专业为您服务!
快,关注这个公众号,一起涨姿势~

508546

此帖已被锁定,无法回复
新窗口打开 关闭