查看原文
其他

看雪2022 KCTF 春季赛 | 第八题设计思路及解析

2022 KCTF 看雪学苑 2022-07-01
看雪 2022 KCTF春季赛 于5月10日中午12点正式开赛!
第八题《迷雾散去》已于今日中午12点截止答题。此题持续了4天,看来有点难度。
经统计,此题围观人数1387人,共4支战队成功破解。


接下来和我一起来看看该赛题的设计思路和相关解析吧!



出题团队简介


第八题《迷雾散去》出题方 【ArmVMP】战队:



战队介绍:专注于虚拟化技术保护Android, Linux, IOS, Windows全系统平台。


关于作者:热爱阅读与运动,专注于互联网安全行业。主攻方向基于二进制的指令虚拟化保护技术,研发的安全产品覆盖诸多平台包括Android,Linux,IOS及物联网平台,致力于通过技术手段,以减少恶意攻击及破坏行为。




赛题设计思路


背景

漆黑夜晚的海滩上,发现一个遗落很久的木箱,箱子很重,边角用钢板缝合,尝试用普通的石块工具根本砸不开.
潮水有节奏的拍打着沙滩,饥肠辘辘的漂泊者望着箱子发呆,里面一定有水果罐头或是金币皇冠...
挣扎许久忽然在箱子一角发现印有模糊的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-秋季赛-第八题群狼环伺的设计方法:

  1. 对输入的name字符串通过SHA1算法计算得到16字节的hash值

  2. 对hash值做rc4加密运算得到16字节value1值

  3. 选取部分代码计算SHA256得到16字节的密钥Key值

  4. 将输入的64字符长度password转为16字节的16进制表示(如:前8个字符64613231转换后为2字节的0xda21)和Key做3DES解密运算得到value2,当password不足64字节时提示错误

  5. 比较value1 == value2时,则为正确的密钥对,提示输入正确

 

增加防护技巧

  1. 题目设计为双进程模式,算法的校验分别放在两个进程中,并通过管道通信且父子进程使用相互ptrace防调试处理

  2. 管道通信读写及部分操作如: open/read/getpid等系统调用通过中断svc指令实现

  3. 父子进程均增加/proc/self/status调试检测

  4. 算法及防护代码编译后的文件以二进制方式,对函数指令做了vm保护处理

 

解题思路

主要是去除防调试功能及调试检测功能

  1. 对父进程的ptrace操作进行nop填充

  2. 对父进程中fork后创建ptrace线程pthread_create操作进行nop填充

  3. 对父子进程中/proc/self/status调试检测均进行nop填充

  4. 跟踪管道通信时,子进程read读数据操作,读取到value值后会进行校验,重点关注eor指令对数据进行的运算操作


vm保护原理

正常编译后的代码通过反编译工具,逻辑结构很清晰,门槛低的小白都可以拿来用f5分析,所以代码防护很重要,最起码能防部分小白。最初级的加壳保护运行后会解密所以动态调试和dump内存也能获取到原代码,这时保护后勿需还原最终以保护态运行就会提高分析门槛。X86时期辉煌无数的vmprotect技术令大部分人闻风丧胆。该技术放到arm架构同样适用。原理也大同小异: 保护流程主要由三部分组成:反汇编器,解释器vmachine和链接器。
  • 反汇编器:负责解析输入二进制文件,输出函数指令和数据流

  • 解释器:负责对vm指令和数据流进行解释执行

  • 链接器:负责将vmachine嵌入新二进制文件中,并对地址相关的指令做重定位

 反汇编器取汇编指令列表中的指令根据指令类型生成对应的模拟代码,如push/pop, ldr/str, bl, add, mov等,由于对指令进行了重新编码故称为指令模拟 。
原始代码开始替换为一条b 指令跳转到新生成的模拟代码运行,这样做可以避免其 他函数调用该函数时能正常工作。其余位置可以填充随机字段或者清空。 

解释器工作原理

针对ARM平台ELF格式的二进制程序的代码保护,兼容Arm&Thumb指令集, 首先反编译被保护的函数获取指令流。
其次对指令流类型分别处理进行重定位,和需要模拟的指令进行编码生成vmdata最后通过链接7⃣️将解释器和原二进制文件进行patch生成新二进制。
由于编译后的汇编指令地址相关性,当把指令放到到千里之外,且不影响运行首要任务就是对地址相关指令做重定位。
  1. ldr指令进行分析做重定位如下 

.text:000016C2 19 4C LDR R4, =(__stack_chk_guard_ptr - 0x16CC) .text:000016C4 C3 B0 SUB SP, SP, #0x10C .text:000016C6 01 AE ADD R6, SP, #0x120+var_11C .text:000016C8 7C 44 ADD R4, PC .text:000016CA 24 68 LDR R4, [R4] … .text:00001728 A0 38 00 00 off_1728 DCD __stack_chk_guard_ptr - 0x16CC 
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 
  1. 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  给出:


程序用了ptrace,防止调试程序,这样IDA,CheatEngine都是用不了的,不过还是可以用scanmem(https://github.com/scanmem/scanmem)读取内存查看内存信息(需要自行编译+改部分代码,此处略过) 程序最后会有 "顺利通关" 字样,找到相关内存并dump:

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 = 0while(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:




去hook vmp函数找一下,hook后再运行会崩溃,不过能输出寄存器信息,也能暂停程序留出dump内存的机会。
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



 

第九题《同归于尽》正在进行中

👆还在等什么,快来参赛吧!

如何成为一名出色的CTF选手?
*点击图片查看详情

入门-基础-进阶-强化,只需四个阶段!摇身一变成为主力、中坚力量




- End -



球分享

球点赞

球在看



戳“阅读原文”展开第九题的战斗!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存