-
-
[原创]2020KCTF KeyMe设计说明和破解思路
-
发表于: 2020-10-25 17:03 3517
-
看雪的神仙师傅太多了,菜鸡自认为做不出题来,所以只能转向防守方出个题了。
之前提交题目的时候正好碰上考试,设计说明写得有些简略,我再补充一下吧。
用户名长度1~254,注册码长度448,注册码字符集为0-9a-f
将user取md5后,前8字节、后8字节分别再次md5,分别作为第一、二部分进行验证。
由于验证逻辑是f'(serial) == md5(user)
,所以md5碰撞并不会导致多解(理论上存在多个user对于同一个serial的可能,但是不会存在一个user对于多个serial的情况)
将serial从hex字符串转换成byte数组,长度224,前32字节作为第一部分验证的输入,后192字节作为第二部分验证的输入。
然后分两部分进行验证。
一个超小的vm。
涉及的小算法我之前在看雪发过:https://bbs.pediy.com/thread-261646.htm。在上文的基础上多加了一个&运算。这个小算法没多大难度,但是编译出来的汇编指令是很臃肿的,嵌套了15层的话,指令大概有几十万条。指令膨胀的特性用来配合vm真的是再合适不过了。
编译后只涉及了六种汇编指令,mov, shr, shl, and, xor, add,所以我把这六种指令设计成了一个小vm,太低级了,甚至可能都不配被称之为vm。
指令格式如下:
由于vm指令的设计问题,只适用于所涉及的变量不多于16个的汇编代码。因为源操作数和目标操作数分别只有4个bit来标识。
一个字段可能会有多个含义。比如and_param既可以是&运算的参数,又可以从中提取两个bit表示填充的垃圾指令的长度。
为了增加(一点点)难度,我还把vm的字节码改了4个字节。vm_data中的第1122145处开始的4个byte数据应该175 179 150 244 ,为了迷惑师傅们,我改成了111 112 113 114,然后使用TLS回调改回正确的字节码0xf496b3af。这个数据是最后一个&运算的参数。而且由于字节码是实时异或更新的,所以此后的十几条指令都是错的。
上一步的输出转为这一步的输入。
这一步是调用了微软的crypto api,算法是aes256 cbc, iv=0000000000000000,key是sha256(1_L0V3_BXS_F0REVER!)
看起来简单,但我是写了个shellcode的,调用比较隐蔽。当然这肯定难不倒师傅们,动调一下就知道了。
动态获取kernel32.dll的基址,通过比较hash获取LoadLibraryA的地址,导入advapi32.dll,然后继续通过比较hash获取CryptAcquireContextA,CryptCreateHash,CryptHashData,CryptDeriveKey,CryptEncrypt的地址,分别push参数后调用。
然后再走一波aes 256 ecb,key是Wo YongYuan XiHuan KanXun LunTan。不过这次是魔改的aes。
(才发现KanXun拼错了。。。题目都提交了,改的话太麻烦了,所以就先这样吧)
原版aes的使用的不可约多项式是283,我改成了299。
具体的魔改原理,师傅们可以参考
https://blog.csdn.net/u011516178/article/details/81221646。
为了欺骗师傅们使用的识别加密算法的插件,我还特意保留了原版的s盒和逆s盒。
上学期准备计网结课考试的时候,就觉得这个算法放到逆向里,绝对很有意思。
这个算法本质上来说,其实就是n维空间向量的合成与分解。
单独的算法demo在这:码分复用DEMO - 狗剩DDoG (iyzyi.com)。本题结束前该文不公开。
每次传输的值为-4, -2, 0, -2, 4,共五种不同数值,原来想加个哈夫曼之类的压缩一下数据,但是实在没时间继续改了,所以简单地使用3bit来表示,有浪费的bit位。第二部分的serial的长达192Byte的罪魁祸首就在这儿。
192Byte -> 32Byte
这个算法用于解多元单模线性方程组。我这里选用的模数是65423。
注册机是将16Byte的数据,分别乘以一个系数(由key数组生成)后求和取模,进行16次,16B的数据生成32B的结果,共32Byte。多次计算,构成单模多元线性方程组。
验证的时候就是用模意义下的高斯消元求出原来的那16个1B的数据。
好像和第三题 重返地球
考察的知识点有点冲突了。不过问题不大,因为这个算法是写在验证机里的,写注册机的时候可以把这个算法直接提取出来用。不是考察的重点。
这个思路来自《加密与解密》第四版的645页。
超级有意思的一个思路,师傅们可以去读下。
具体的可以看KEYGEN脚本。
最后输出16B,与user的md5的后8字节的md5进行验证
首先去下花指令。花指令的数量不少,手动去花不太现实。不过好在种类不多,一共也就十种左右吧。发现一次后就可以写个脚本批量去一下。
然后过反调试。反调试菜鸡我接触不多,所以本题的反调试只是聊胜于无罢了。而且我故意让反调试清一色地调用exit()(其实是交题时间截止在即,没时间继续改了,逃),所以只需要找到一处反调试,就可以查看交叉引用,直接找到所有的反调试。由于出题人水平受限,本题的反调试没多大意思。
最后一步就是逆向了。大多数都是算法求逆,考验算法功底。这里捡几个关键的点来说。
拿到指令流的方式有两种。
一种是去掉花指令和反调试后把相关的代码提取出来,用它来处理一下字节码,即可拿到指令流。不过前面我也说了,字节码最后修改了4个错误的数据,由于异或操作,最后的大概十几条指令是错误的。需要发现tls回调修改的正确的数据,手动改一下字节码中的错误数据,然后再跑脚本。
另一种就是pin插桩。推荐这个方式。完美绕过我设置的tls回调的坑。
处理字节码得到伪代码后,读开头的几百行,应该能反应过来其实是一个嵌套的格式(吧)。
然后到伪代码最后的几百行提取所涉及的参数就行。如下:
这个vm难度不大,比较低级的水平。(可能的)难点在于嵌套格式的识别。
一个坑点在于最后一个&的参数被改过了,(或许)需要发现TLS回调的那个函数。
字节码的臃肿不知道会不会对师傅们造成一点点小麻烦。不知道师傅们有没有什么优雅的处理方式。菜鸡我等师傅们的wp出来后学习一波。
两种方法:
有写过shellcode经验的师傅,可以直接把汇编扣出来,改下hash,使得CryptEncrypt函数改成CryptDecrypt,就能跑,注意CryptEncrypt比CryptDecrypt多个参数,需要删掉最后一个参数。
或者有写过微软crypto api经验的可以直接知道是aes256 cbc, iv=0000000000000000,密钥被sha256了。(可以去msdn上查。但是好像查不到是cbc。我出题的时候试了一下,才发现其实是cbc)
关于不可约多项式生成s盒和逆s盒,可以参考
https://blog.csdn.net/u011516178/article/details/812216461
一个思路是动调拿到真正的s盒,然后去生成逆s盒。
由于使用了利用md5进行check
(见下一步),所以整个第二部分的验证函数都需要提取出来,用于从一组已知的注册码中求解出H数组(key)。
正向的算法很复杂,但是直接导出就能用。
逆向的算法就是一个用key加密flag,先乘后加然后取模,一共进行16次,组成单模多元线性方程组,输出等式右侧的计算结果。
不过,由验证机里的正向算法推出注册机的逆向算法可能会有些难度。
需要先用已知的一组注册码求出攻击脚本中的H数组,这一过程需要导出整个第二部分的正向算法。
有了H数组后,就可以逆向写出求解注册码的逻辑。
uint16_t xor_data[]
=
{
0x0123
,
0x4567
,
0x89ab
,
0xcdef
,
0x0f1e
,
0x2d3c
,
0x4b5a
,
0x6978
};
int
pointer
=
0
;
uint32_t reg[
16
]
=
{
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
};
struct VmCmd{
uint32_t and_param;
/
/
&运算的参数
uint8_t rubbish_size;
/
/
垃圾指令大小
uint8_t xor_index;
/
/
异或的索引
uint8_t reg_byte;
/
/
(dst<<
4
)
+
src
uint8_t dst;
/
/
目的操作数
uint8_t src;
/
/
源操作数
uint8_t op_byte;
/
/
六种指令
uint8_t other_op_param;
/
/
其他运算的参数
};
uint32_t encrypt_vm(uint32_t plain){
reg[
15
]
=
plain;
/
/
[ebp
+
plain]
while
(pointer < (sizeof(vm_data)
/
sizeof(vm_data[
0
]))) {
VmCmd vcmd;
vcmd.and_param
=
*
(uint32_t
*
)(vm_data
+
pointer);
vcmd.rubbish_size
=
(((vcmd.and_param >>
16
) &
1
) <<
1
)
+
((vcmd.and_param >>
7
) &
1
);
vcmd.xor_index
=
(((vcmd.and_param >>
27
) &
1
) <<
2
)
+
(((vcmd.and_param >>
19
) &
1
) <<
1
)
+
((vcmd.and_param >>
8
) &
1
);
vcmd.reg_byte
=
*
(vm_data
+
pointer
+
4
) ^ ((xor_data[vcmd.xor_index] >>
8
) &
0xff
);
vcmd.dst
=
(vcmd.reg_byte >>
4
) &
0xf
;
vcmd.src
=
(vcmd.reg_byte) &
0xf
;
vcmd.op_byte
=
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size) ^ (xor_data[vcmd.xor_index] &
0xff
);
vcmd.other_op_param
=
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size
+
1
);
uint16_t new_data
=
((
*
(vm_data
+
pointer
+
4
)) <<
8
)
+
(
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size));
for
(
int
i
=
0
; i <
7
; i
+
+
){
xor_data[i]
=
xor_data[i
+
1
];
}
xor_data[
7
]
=
new_data;
pointer
+
=
4
+
1
+
vcmd.rubbish_size
+
1
+
1
;
if
(vcmd.op_byte &
64
){
/
/
and
reg[vcmd.dst] &
=
vcmd.and_param;
}
else
if
(vcmd.op_byte &
32
){
/
/
shr
reg[vcmd.dst] >>
=
vcmd.other_op_param;
}
else
if
(vcmd.op_byte &
16
){
/
/
shl
reg[vcmd.dst] <<
=
vcmd.other_op_param;
}
else
if
(vcmd.op_byte &
8
){
/
/
xor
reg[vcmd.dst] ^
=
reg[vcmd.src];
}
else
if
(vcmd.op_byte &
4
){
/
/
mov
reg[vcmd.dst]
=
reg[vcmd.src];
}
else
if
(vcmd.op_byte &
2
){
/
/
add
reg[vcmd.dst]
+
=
reg[vcmd.src];
}
}
return
reg[
14
];
/
/
eax
}
uint16_t xor_data[]
=
{
0x0123
,
0x4567
,
0x89ab
,
0xcdef
,
0x0f1e
,
0x2d3c
,
0x4b5a
,
0x6978
};
int
pointer
=
0
;
uint32_t reg[
16
]
=
{
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
,
0
};
struct VmCmd{
uint32_t and_param;
/
/
&运算的参数
uint8_t rubbish_size;
/
/
垃圾指令大小
uint8_t xor_index;
/
/
异或的索引
uint8_t reg_byte;
/
/
(dst<<
4
)
+
src
uint8_t dst;
/
/
目的操作数
uint8_t src;
/
/
源操作数
uint8_t op_byte;
/
/
六种指令
uint8_t other_op_param;
/
/
其他运算的参数
};
uint32_t encrypt_vm(uint32_t plain){
reg[
15
]
=
plain;
/
/
[ebp
+
plain]
while
(pointer < (sizeof(vm_data)
/
sizeof(vm_data[
0
]))) {
VmCmd vcmd;
vcmd.and_param
=
*
(uint32_t
*
)(vm_data
+
pointer);
vcmd.rubbish_size
=
(((vcmd.and_param >>
16
) &
1
) <<
1
)
+
((vcmd.and_param >>
7
) &
1
);
vcmd.xor_index
=
(((vcmd.and_param >>
27
) &
1
) <<
2
)
+
(((vcmd.and_param >>
19
) &
1
) <<
1
)
+
((vcmd.and_param >>
8
) &
1
);
vcmd.reg_byte
=
*
(vm_data
+
pointer
+
4
) ^ ((xor_data[vcmd.xor_index] >>
8
) &
0xff
);
vcmd.dst
=
(vcmd.reg_byte >>
4
) &
0xf
;
vcmd.src
=
(vcmd.reg_byte) &
0xf
;
vcmd.op_byte
=
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size) ^ (xor_data[vcmd.xor_index] &
0xff
);
vcmd.other_op_param
=
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size
+
1
);
uint16_t new_data
=
((
*
(vm_data
+
pointer
+
4
)) <<
8
)
+
(
*
(vm_data
+
pointer
+
4
+
1
+
vcmd.rubbish_size));
for
(
int
i
=
0
; i <
7
; i
+
+
){
xor_data[i]
=
xor_data[i
+
1
];
}
xor_data[
7
]
=
new_data;
pointer
+
=
4
+
1
+
vcmd.rubbish_size
+
1
+
1
;
if
(vcmd.op_byte &
64
){
/
/
and
reg[vcmd.dst] &
=
vcmd.and_param;
}
else
if
(vcmd.op_byte &
32
){
/
/
shr
reg[vcmd.dst] >>
=
vcmd.other_op_param;
}
else
if
(vcmd.op_byte &
16
){
/
/
shl
reg[vcmd.dst] <<
=
vcmd.other_op_param;
}
else
if
(vcmd.op_byte &
8
){
/
/
xor
reg[vcmd.dst] ^
=
reg[vcmd.src];
}
else
if
(vcmd.op_byte &
4
){
/
/
mov
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)