看雪2022 KCTF 春季赛 | 第八题设计思路及解析
第八题《迷雾散去》已于今日中午12点截止答题。此题持续了4天,看来有点难度。
经统计,此题围观人数1387人,共4支战队成功破解。
接下来和我一起来看看该赛题的设计思路和相关解析吧!
出题团队简介
战队介绍:专注于虚拟化技术保护Android, Linux, IOS, Windows全系统平台。
赛题设计思路
背景
漆黑夜晚的海滩上,发现一个遗落很久的木箱,箱子很重,边角用钢板缝合,尝试用普通的石块工具根本砸不开.潮水有节奏的拍打着沙滩,饥肠辘辘的漂泊者望着箱子发呆,里面一定有水果罐头或是金币皇冠...
挣扎许久忽然在箱子一角发现印有模糊的PIPEOR,他思索片刻嘴角露出久违的笑容。
题目信息
本题是安卓平台规则2的crackme文件名:KCTF2022-sprint-android-crackme.apk
公开序列号如下:name:KCTFserial:3432356538383237303738386163323436323438663964343063393831366663
第二组序列号:name:C8EB85C90E69EDC8serial:3638386461396366623135623535353361323862656630656561326334303931
通过命令shasum -a 256 KCTF2022-sprint-android-crackme.apk计算apk得文件hash:c8eb85c90e69edc8ee85e7233c6cb91aa633f6ff5dc4882ca3fc3ba45225d8c1
设计思路
本题算法延续了KCTF2021-秋季赛-第八题群狼环伺的设计方法:
对输入的name字符串通过SHA1算法计算得到16字节的hash值
对hash值做rc4加密运算得到16字节value1值
选取部分代码计算SHA256得到16字节的密钥Key值
将输入的64字符长度password转为16字节的16进制表示(如:前8个字符64613231转换后为2字节的0xda21)和Key做3DES解密运算得到value2,当password不足64字节时提示错误
比较value1 == value2时,则为正确的密钥对,提示输入正确
增加防护技巧
题目设计为双进程模式,算法的校验分别放在两个进程中,并通过管道通信且父子进程使用相互ptrace防调试处理
管道通信读写及部分操作如: open/read/getpid等系统调用通过中断svc指令实现
父子进程均增加/proc/self/status调试检测
算法及防护代码编译后的文件以二进制方式,对函数指令做了vm保护处理
解题思路
主要是去除防调试功能及调试检测功能
对父进程的ptrace操作进行nop填充
对父进程中fork后创建ptrace线程pthread_create操作进行nop填充
对父子进程中/proc/self/status调试检测均进行nop填充
跟踪管道通信时,子进程read读数据操作,读取到value值后会进行校验,重点关注eor指令对数据进行的运算操作
vm保护原理
正常编译后的代码通过反编译工具,逻辑结构很清晰,门槛低的小白都可以拿来用f5分析,所以代码防护很重要,最起码能防部分小白。最初级的加壳保护运行后会解密所以动态调试和dump内存也能获取到原代码,这时保护后勿需还原最终以保护态运行就会提高分析门槛。X86时期辉煌无数的vmprotect技术令大部分人闻风丧胆。该技术放到arm架构同样适用。原理也大同小异: 保护流程主要由三部分组成:反汇编器,解释器vmachine和链接器。反汇编器:负责解析输入二进制文件,输出函数指令和数据流
解释器:负责对vm指令和数据流进行解释执行
链接器:负责将vmachine嵌入新二进制文件中,并对地址相关的指令做重定位
原始代码开始替换为一条b 指令跳转到新生成的模拟代码运行,这样做可以避免其 他函数调用该函数时能正常工作。其余位置可以填充随机字段或者清空。
解释器工作原理
针对ARM平台ELF格式的二进制程序的代码保护,兼容Arm&Thumb指令集, 首先反编译被保护的函数获取指令流。其次对指令流类型分别处理进行重定位,和需要模拟的指令进行编码生成vmdata最后通过链接7⃣️将解释器和原二进制文件进行patch生成新二进制。
由于编译后的汇编指令地址相关性,当把指令放到到千里之外,且不影响运行首要任务就是对地址相关指令做重定位。
ldr指令进行分析做重定位如下
Thumb指令16C2处指令为ldr r4, [pc, #0x64], 访问的数据地址为 (16C2&0xFFF4) + (PC+4) + 0x64 = 0x1728
意思是从0x1728中获取数据即0x38A0赋值给R4寄存器 。
R4寄存器在0x16C8地址处会跟PC寄存器相加即0x38A0+(PC+4)=0x4F6C 结合0x16CA指令读内存操作,也就是说程序是在读取0x4F6C地址处的数据 这段代码当移动到其他地址运行时PC寄存器会变为移动后的地址 。
例如这段代码移动到 .text:00006000 19 4C LDR R4, =(__stack_chk_guard_ptr - 0x16CC) .text:00006002 C3 B0 SUB SP, SP, #0x10C .text:00006004 01 AE ADD R6, SP, #0x120+var_11C .text:00006006 7C 44 ADD R4, PC .text:00006008 24 68 LDR R4, [R4] … .text:0000606C A0 38 00 00 off_1728 DCD __stack_chk_guard_ptr - 0x16CC
移动后0x6000地址指令读取0x606C数据为0x38A0到R4寄存器。 在0x6006地址时R4与PC相加结果为0x38A0+(0x6006+4) = 0x98AA 这与原来的0x4F6C是不相符的,为了正确读取0x4F6C地址就需要对 对0x606C处的数据做重定位即0x4F6C = X+(0x6006+4)得出X=0x213C。
将0x606C的0x38A0修改为0x213C这样才能确保访问正确。
对模拟运行的指令重新编码,如:.text:000016D8 01 F0 D2 FD BL 0x1BA8
BL指令是相对当前地址到跳转目标地址=0x16D8+0x1BA8 = 0x3280
当运行时如果通过POP {PC}模拟就需要将运行时的真实内存地址即基地址+偏移地址,假设基地址和偏移地址存储在变量中就可以通过如下方式进行:LABEL_BASE: .word 0x62220000 LABEL_DISP: .word 0x3280 PUSH {R0-R15} LDR R0, LABEL_BASE LDR R1, LABEL_DISP ADD R0, R1 STR R0, [SP, #0x3C] POP {R0-R12} ADD SP, #0xC LDR PC, [SP, #-4]
这样重定位和指令模拟就实现了,通过同样的方法实现其他指令的模拟。
赛题解析
本赛题解析由看雪论坛专家 ThTsOd 给出:
dump出附近内存,用作对比,没啥想法。 ida查算法特征,发现有个des,算法和上次比赛的代码差不多,可以hook
frida hook后发现没有输出,写个debugger发现程序停住,说明frida hook代码还是执行了的,可以将信息输出到文件里。
部分代码:
mylogfile = new File("/data/data/a.b.c/d1.txt","wb");
Interceptor.attach(baseAddr.add(0x4d15),{
onEnter:function(args){
var mylog = "";
mylog += "DES Enc:" + (this.context as any).lr.sub(baseAddr) + "\n";
// mylog += JSON.stringify(this.context) +"\n";
// mylog += (this.context as any).lr.sub(baseAddr) + "\n";
// // mylog += hexdump(args[0],{
// // offset:0,
// // length:0x100,
// // header:true,
// // ansi:true
// // })
// // mylog+="\n";
// mylog += hexdump(args[1],{
// offset:0,
// length:0x10,
// header:true,
// ansi:true
// })
// mylog+="\n";
// mylog += hexdump(args[2],{
// offset:0,
// length:0x10,
// header:true,
// ansi:true
// })
// mylog+="\n";
mylogfile.write(mylog);
mylogfile.flush();
},onLeave:function(ret){
var mylog = "";
mylog += "After DES Enc:" + (this.context as any).lr.sub(baseAddr) + "\n";
// mylog += JSON.stringify(this.context) +"\n";
// mylog += hexdump((this.context as any).r2,{
// offset:0,
// length:0x10,
// header:true,
// ansi:true
// })
// mylog+="\n";
mylogfile.write(mylog);
mylogfile.flush();
count+=1
if(count==2){
//debugger;
}
}
});
找出返回地址0xe409,观察发现vmp代码有点相似结构 44 f0 0d e5 ? ? 00 ea,ghidra强制识别这些arm代码试试:
f=open("libcrackme.so","rb")
data = f.read()
f.close()
result = 0
while(result != -1):
result = data.find(bytearray.fromhex("44 F0 0D E5"),result+4)
print(hex(result+0x1000))
hook字符串函数,得到最后输出字符串位置:
function hookart(){
var baseAddr = Module.findBaseAddress("/apex/com.android.runtime/lib/libart.so");
//var baseAddr = Module.findExportByName(null,"_ZN3art12_GLOBAL__N_18CheckJNI12NewStringUTFEP7_JNIEnvPKc");
console.log("Art",baseAddr)
Interceptor.attach(baseAddr.add(0x2C85D1),
{
onEnter: function (args)
{
if(args[1].readCString() == "您输错了" || args[1].readCString() == "顺利过关"){
console.log(args[1].readCString(),args[1]);
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].add(0xe0),{
offset:0,
length:128,
header:true,
ansi:true
}));
console.log(JSON.stringify(this.context));
console.log(hexdump(this.context.sp.sub(0),{
offset:0,
length:128,
header:true,
ansi:true
}));
memset_log = false;
mylogfile.close();
//debugger;
}
},
onLeave: function (ret)
{
}
}
);
}
计算下最后输出字符串位置返回是14664:
Interceptor.attach(baseAddr.add(0xf628),{
onEnter:function(args){
if(true || (this.context as any).sp.sub(0x44).readPointer().sub(baseAddr) == ptr(0x145e8)){
var mylog = "";
mylog += (this.context as any).sp.sub(0x44).readPointer().sub(baseAddr) + " Result " + (this.context as any).lr.sub(baseAddr) +"\n";
mylog += JSON.stringify(this.context)+"\n";
// mylog += hexdump((this.context as any).r3,{
// offset:0,
// length:(this.context as any).r0.sub((this.context as any).r3).toUInt32(),
// header:true,
// ansi:true
// })
mylog+="\n";
console.warn("!! ",JSON.stringify(this.context))
mylogfile.write(mylog);
mylogfile.flush();
debugger;
}
},onLeave:function(ret){
}
});
附近hook函数找了一下,没有找到判定条件,不过能确定正确输入会有0xc,不正确的输入则是0x0。 去跟des算法,发现是加密用户名的,和序列号没啥关系,发现走到后面,程序没有断下。
发现1e48c 跳转到xref较少的地方。
一个个试,发现找到一个地方正好是卡在des加密后,得出判断结果前,且正好有返回结果为0xc。
观察也可以发现有个循环结构,多次尝试hook输出信息,根据寄存器信息和dump出的内存猜出xor。
KCTF对应序列号为:3432354538383237303738384143323436323438463944343043393831364643
第九题《同归于尽》正在进行中
👆还在等什么,快来参赛吧!球分享
球点赞
球在看