看雪·众安 2021 KCTF 秋季赛 | 第八题设计思路及解析
看雪·众安 2021 KCTF秋季赛的第八题《群狼环伺》已于今天中午12点截止答题!
本题共有3支战队成功破解,分别是辣鸡战队、金左手、mininep。
恭喜辣鸡战队用时100442秒拿下“一血”,接下来和我一起来看看该赛题的设计思路和相关解析吧~
出题团队简介
赛题设计思路
本题目是安卓平台的crackme,算法简单,可玩性高,有兴趣可以随时交流。
规则2的demo为:KCTF2021-android-crackme.apk
其中规则2的两组序列号如下:
第一组序列号:
name:KCTF
serial:3633386636373733353933626439316437383865383931653663663432656661
第二组序列号:
name:4DD6F06301B04D13
serial:6461323135643835663832623133666234313635636637383266613665363961
(通过命令shasum -a 256 crackme.apk计算apk得文件hash:
4dd6f06301b04d13f76c513326be681de153ff743ce17f411a35e09a08a90d37)
设计思路:
1、对输入name字符串通过SHA1算法,计算得到16字节的hash值;
2、hash值做rc4加密运算,得到16字节value1值;
3、对部分代码计算SHA256得到16字节的密钥Key;
4、将输入64个字符password转为16字节的16进制表示(如:前四个字节64613231转换后为0xda21)和Key做3DES解密得到value2,当password不足64字节时提示错误;
5、比较value1 == value2时,则为正确的密钥对。
保护方法:
1、代码中有效隐藏了系统函数调用,仅保留了一个memset;
2、增加了常规的检测非法运行手段;
3、通过自研的保护方案将原so中的原始指令进行替换,转换后一条指令对应多条指令,并且部分指令经过编码生成数据,运行时对数据进行解析运行。原函数内部的字符串和函数访问通过模拟指令执行,能够有效隐藏相关调用。
解题思路:
由于对代码段的指令做了变形保护,通过相同功能的函数实现指令替代运行,所以反编译时,较难看出算法逻辑,需要对模拟代码标识出相应的指令来分析逻辑,当然也存在不需要标记的情况.
春季赛中4为选手均是通过对数据流分析,比对每种指令运行次数,以及数据输入及输出做观察跟踪出的序列号。当然也可以通过人肉找到关键比较处跟踪到序列号。
关于作者:喜欢阅读与运动,专注于互联网安全行业从业十多年之久。
主攻方向研究二进制文件保护技术,基于指令虚拟化保护技术,研发的安全产品覆盖诸多平台包括Android,Linux, IOS及物联网平台,致力于通过技术手段,以减少对用户的恶意攻击及破坏行为,让世界多一些友善。
赛题解析
IDA上来一看,没啥函数:
Interceptor.attach(baseAddr.add(0x789),{
onEnter: function (args)
{
console.log((this.context as any).lr.sub(baseAddr));
console.log(JSON.stringify(this.context));
console.log(hexdump((this.context as any).r0,{
offset:0,
length:128*3,
header:true,
ansi:true
}));
//Enc Data
console.log(hexdump((this.context as any).r1,{
offset:0,
length:8,
header:true,
ansi:true
}));
},
onLeave: function (ret)
{
//After Enc
console.log(hexdump((this.context as any).r2,{
offset:0,
length:8,
header:true,
ansi:true
}));
}
});
可以看到将输入转成hex再转hex后,加密了一次,随后是加密相同的内容:
function swapkey(addr:NativePointer){
var key = []
for(var i = 0;i<48;i+=1){
key.push(addr.add(i*8).readU64());
}
for(var i = 0;i<48;i+=1){
addr.add(i*8).writeU64(key[47-i]);
}
}
当提交成功时,会出现"恭喜成功",但是在java层代码和native层代码没有找到这个字符串,这里无意尝试把以下内容修改了。
Interceptor.attach(baseAddr.add(0xae765),
{
onEnter: function (args)
{
//console.log(args[1].readCString(),args[1]);
if(args[1].readCString() == "恭喜成功" || args[1].readCString() == "输入错误"){
var mainAddr = Module.findBaseAddress("libcrackme.so");
console.log((this.context as any).lr.sub(mainAddr));
for(var i=0;i<64;i++){
//console.log((this.context as any).sp.add(i*4).readPointer(),(this.context as any).sp.add(i*4).readPointer().sub(mainAddr));
}
console.log(hexdump(args[1]));
console.log(JSON.stringify(this.context));
console.log(hexdump(this.context.sp.add(0xB0),{
offset:0,
length:192,
header:true,
ansi:true
}));
debugger;
}
},
onLeave: function (ret)
{
}
}
);
这样可以在判断完结果后将程序断下(frida的debugger命中会暂停当前线程)。 搜一下正确输入的加密结果,运行程序时保持name相同,serial不同。 bd 3a b0 69 39 40 f8 cd 42 0d e3 8a 79 db 52 bd 找到一个:
if(true){
//DES Decrypt
swapkey((this.context as any).r0);
//Data 1
if((this.context as any).lr.sub(baseAddr) == 0xe8b5){
var encdata = [0x45,0x68,0x97,0xa3,0x29,0x2a,0x7f,0xd4];
(this.context as any).r1.writeByteArray(encdata);
}
//Data 2
if((this.context as any).lr.sub(baseAddr) == 0xe8e5){
var encdata = [0xf5,0x90,0x73,0x57,0x46,0x02,0xae,0xd5];
swapkey((this.context as any).r0);//Swap Again
(this.context as any).r1.writeByteArray(encdata);
}
}
应该输入的hex 为 63 8f 67 73 59 3b d9 1d 78 8e 89 1e 6c f4 2e fa 再转换一次: 3633386636373733353933626439316437383865383931653663663432656661,即为正确答案。 部分代码如下:
function swapkey(addr:NativePointer){
var key = []
for(var i = 0;i<48;i+=1){
key.push(addr.add(i*8).readU64());
}
for(var i = 0;i<48;i+=1){
addr.add(i*8).writeU64(key[47-i]);
}
}
function hook(){
var baseAddr = Module.findBaseAddress("libcrackme.so");
Interceptor.attach(baseAddr.add(0x789),
{
onEnter: function (args)
{
console.log((this.context as any).lr.sub(baseAddr));
//console.log(JSON.stringify(this.context));
// console.log(hexdump((this.context as any).r0,{
// offset:0,
// length:128*3,
// header:true,
// ansi:true
// }));
if(true){
//DES Decrypt
swapkey((this.context as any).r0);
//Data 1
if((this.context as any).lr.sub(baseAddr) == 0xe8b5){
var encdata = [0x45,0x68,0x97,0xa3,0x29,0x2a,0x7f,0xd4];
(this.context as any).r1.writeByteArray(encdata);
}
//Data 2
if((this.context as any).lr.sub(baseAddr) == 0xe8e5){
var encdata = [0xf5,0x90,0x73,0x57,0x46,0x02,0xae,0xd5];
swapkey((this.context as any).r0);//Swap Again
(this.context as any).r1.writeByteArray(encdata);
}
}
//Enc Data
console.log(hexdump((this.context as any).r1,{
offset:0,
length:8,
header:true,
ansi:true
}));
},
onLeave: function (ret)
{
//After Enc
console.log(hexdump((this.context as any).r2,{
offset:0,
length:8,
header:true,
ansi:true
}));
}
}
);
}
function hookart(){
var baseAddr = Module.findBaseAddress("/system/lib/libart.so");
//var baseAddr = Module.findExportByName(null,"_ZN3art12_GLOBAL__N_18CheckJNI12NewStringUTFEP7_JNIEnvPKc");
console.log("Art",baseAddr)
Interceptor.attach(baseAddr.add(0xae765),
{
onEnter: function (args)
{
//console.log(args[1].readCString(),args[1]);
if(args[1].readCString() == "恭喜成功" || args[1].readCString() == "输入错误"){
var mainAddr = Module.findBaseAddress("libcrackme.so");
console.log((this.context as any).lr.sub(mainAddr));
for(var i=0;i<64;i++){
//console.log((this.context as any).sp.add(i*4).readPointer(),(this.context as any).sp.add(i*4).readPointer().sub(mainAddr));
}
console.log(hexdump(args[1]));
console.log(JSON.stringify(this.context));
console.log(hexdump(this.context.sp.add(0xB0),{
offset:0,
length:192,
header:true,
ansi:true
}));
debugger;
}
},
onLeave: function (ret)
{
}
}
);
}
往期解析
1、看雪·众安 2021 KCTF 秋季赛 | 第二题设计思路及解析
2、看雪·众安 2021 KCTF 秋季赛 | 第三题设计思路及解析
3、看雪·众安 2021 KCTF 秋季赛 | 第四题设计思路及解析
4、看雪·众安 2021 KCTF 秋季赛 | 第五题设计思路及解析
5、看雪·众安 2021 KCTF 秋季赛 | 第六题设计思路及解析
6、看雪·众安 2021 KCTF 秋季赛 | 第七题设计思路及解析
第九题《万事俱备》正在火热进行中,
👆还在等什么,快来参赛吧!球分享
球点赞
球在看