在现在的CTF比赛赛制中,前三名解出题目的队伍都会有额外加分,如何快速的解出问题,对于参赛选手来说至关重要。因为往往就是一分之差,淘汰多支队伍。
我们以最近一道CTF 安卓题目为例,利用frida工具我们拿到了二血,今天我来分享一下,如何快速的capture apk’s flag
首先jeb来加载hw02.apk,jeb工具可自行下载
这里可以根据Manifest来分析出apk的主入口activity,这里是MainActivity
Input 变量为用户的输入,从上图代码所示,判断input输入的字符串格式 “flag{”开头 , “}” 结尾。字符串长度不能低于10。否则会报错 tryagain~ 或者 flag format error ~
重点逻辑在 this.checkFlag 这个native函数,它的处理逻辑在so层,输入的参数就是 flag{xxx}中间的那坨xxx。
将apk解压缩,发现有两个架构的so文件,我们的测试机是64位的,那就从arm64-v8a里获取so吧
使用ida工具来分析libcheckflag.so文件
看到init_array 有初始化的过程
这里会修改.bss段变量的值。暂时不管。
JNI_OnLoad 函数对 checkFlag函数进行动态注册了
主要的逻辑在于这里sub_DD78函数了
看第一个箭头,v13代表字符串的长度,这里v13 长度需要超过15,低于15长度就直接退出了。
第二个箭头处Sub_DFC8是干嘛的?跟进去
GetStringUTFChars是将java层传入的jstring转化为char*, 这里我们简单的理解就是获取用户输入的字符串的。我们知道GetStringUTFChars有三个参数,这里ida对函数优化之后,没有显示任何参数。右键选择”Force call type”,就可以看到三个参数了。
接下来,我们只需要关注下面的三个重要的函数,经过这三个函数的运算之后,会产生一个新的值,固定值"bb4ME6An/z82AwX5r0FXgwJwzp3JaFgW7JtmKc4T9Q=="进行strcmp对比。相同就可以返回true了
sub_E018 我们来看一下函数内部逻辑
sub_E56C
sub_E7F8 这个函数巨复杂,直接分析变得不可能且会耗费大量的时间
综上所述,这三个函数内部逻辑都很复杂,如果直接去逆推,需要花很长时间,这里我们采用frida工具来帮助我们快速的解题。
Hook函数实现代码
var libhm = Process.findModuleByName("libcheckflag.so");
if(libhm != undefined)
{
var modulebase = libhm.base;
console.log("base:"+modulebase);
var sub_E018 = modulebase.add(0xE018);
var sub_E56C = modulebase.add(0xE56C);
var sub_E7F8 = modulebase.add(0xE7F8);
Interceptor.attach(sub_E018,{
onEnter: function(args){
//console.log(Memory.readCString(args[0]));
},
onLeave: function(retval){
//console.log("retval:"+retval);
//Memory.readUtf8String(retval);
//retval.replace(1)
}
});
Interceptor.attach(sub_E56C,{
onEnter: function(args){
//console.log(Memory.readCString(args[0]));
},
onLeave: function(retval){
//console.log("retval:"+retval);
//Memory.readUtf8String(retval);
//retval.replace(1)
}
});
Interceptor.attach(sub_E7F8,{
onEnter: function(args){
//console.log(Memory.readCString(args[0]));
},
onLeave: function(retval){
console.log("retval:"+retval);
var data = Memory.readUtf8String(retval);
var result = “bb4ME6An/z82AwX5r0FXgwJwzp3JaFgW7JtmKc4T9Q==”;
var index= (round-1) *4;
var result1 =result.substring(index,index+4);
if(data.substring(index,index+4) == result1)
{
console.log("[+]Found:"+Memory.readUtf8String(retval));
}
//retval.replace(1)
}
});
}
Frida hook了这些关键的函数之后,我们就可以看到函数的输入参数,和输出结果。
我们尝试输入不同的字符来看输出情况
最终得出结论:Input输入的字符长度,除去flag{} 之外有31位时,计算出的结果才和
bb4M E6An /z82 AwX5 r0FX gwJw zp3J aFgW7Jtm Kc4 T9Q== 一样长,这个长度是线性增加的,可以采用爆破的方法。而且每输入3个字符,对应输出结果的4个字符。
所以我们先找第一个三字符 xyz 对应 bb4M
第二组 三字符 xyz 对应E6An ,那这里我们以第二组为例,第一组我们爆破出来为qeo
注意上面hook函数中的var index = (round-1)*4;这里的round对应的是第几组。
爆破函数实现代码
var MainActivity$1= Java.use("com.ssj.hw02.MainActivity$1");
var MainActivity = Java.use("com.ssj.hw02.MainActivity");
var ma = MainActivity.$new()
MainActivity$1.onClick.implementation =function(){
for(var j =0; j <pool.length; j++)
{
for(var i =0; i <pool.length; i++)
{
for(var k =0; k <pool.length; k++)
{
var data = "qeo"+pool[j]+pool[i]+pool[k]+"1111111111111111111111111";
console.log(data);
ma.checkFlag(data);
}
}
}
}
这样就可以知道第二轮的3字符为irk
我们再重新设置爆破函数中的变量 var data = "qeoirk"+pool[j]+pool[i]+pool[k]+"1111111111111111111111";
将hook函数中的round=3
以此类推,将所有的字符全部猜解出。
qeoirklnxcvxfgiefhdweruoulksdnm
最终提交的flag为 flag{ qeoirklnxcvxfgiefhdweruoulksdnm}
这样就可以快速的爆破出所有的字符了,省去了大量的中间逆向和逻辑细节了,帮助你快速破解此类题目。