本文首发于 i春秋公众号 巧用Frida与Unidbg快速在CTF中解题
题目名称:计时挑战,你能当人肉计时器么?
题目下载地址:https://pan.baidu.com/s/11BcKF6LTWQTeqYmUuP_ukA 提取码(fjiw)
题目练习地址:https://www.ichunqiu.com/battalion?t=1&r=70900
Frida是一个方便快捷的hook框架,在安卓逆向中是必不可少的hook工具。而对代码量不大的CTF安卓逆向题目来讲,使用Frida更是如鱼得水,调试程序、打印输出等都极为方便,就算有相应的检测,由于CTF中代码量并不是很大的情况,也可以快捷的定位、并对其进行hook绕过。而Unidbg可以对so层中的函数进行模拟执行,对于CTF题目来讲,往往无需太多的“补环境”,Unidbg就能将指定的函数模拟执行起来并获得结果。在本文中笔者将以全国大学生信息安全竞赛中的“计时挑战”为例,对两种工具的实践使用进行讲解。
计时挑战
你能当人肉计时器么?
将apk安装到测试机中,先查看功能
有两个按钮,当点击START
按钮时 END
按钮下的文本框会开始输出计时,当点击了END
按钮后,会出现提示too soon
。当再次点击START
按钮时,会重新进行计时。
观察完题目的大概作用后,使用Jdax反编译工具将APK载入,代码量比较少,只有MainActivity
一个类。
首先当点击START
按钮时,会执行它的onClick方法,代码如下
((Button) findViewById(R.id.bt1)).setOnClickListener(new View.OnClickListener() { // from class: an.droid.j.MainActivity.2
@Override // android.view.View.OnClickListener
public void onClick(View view) {
MainActivity.this.start_time = System.currentTimeMillis(); // 点击按钮后记录点击按钮的初始时间start_time
MainActivity mainActivity = MainActivity.this;
mainActivity.count_time = mainActivity.start_time; // 将计数时间count_time赋值为初始时间start_time
handler.postDelayed(runnable, 0L); // 使用handler,开一个线程执行runnable的内容
}
});
而执行的runnable
代码如下:
final Runnable runnable = new Runnable() { // from class: an.droid.j.MainActivity.1
@Override // java.lang.Runnable
public void run() {
MainActivity.this.now_time = System.currentTimeMillis(); // 获取当前时间
long j = MainActivity.this.now_time - MainActivity.this.count_time; // 计算时间的差值j
MainActivity mainActivity = MainActivity.this;
mainActivity.df_all = mainActivity.now_time - MainActivity.this.start_time; // 计算时间的总差值df_all
if (j > 100) { // 当时间差值j大于100时
MainActivity.this.count_time += 100; // 将计数count_time添加100ms,确保差值j始终<100ms,达到每100毫秒就执行一次if内语句的效果
MainActivity mainActivity2 = MainActivity.this;
mainActivity2.zygote = mainActivity2.p(mainActivity2.zygote); // 调用p方法,更新zygote
textView.setText(String.format("Time:%.1f", Double.valueOf(Math.floor(MainActivity.this.df_all / 100) / 10.0d))); // 将时间秒数更新到文本框上
}
handler.postDelayed(this, 30L); // 每30秒执行一次run方法
}
};
当点击END
按钮时,执行的回调方法如下:
((Button) findViewById(R.id.bt2)).setOnClickListener(new View.OnClickListener() { // from class: an.droid.j.MainActivity.3
@Override // android.view.View.OnClickListener
public void onClick(View view) {
handler.removeCallbacks(runnable);
if (MainActivity.this.df_all / 100 == 99999) { // 如果当点击按钮时候,时间总差值恰好为9999秒
TextView textView2 = textView;
StringBuilder sb = new StringBuilder();
sb.append("flag{");
MainActivity mainActivity = MainActivity.this;
sb.append(mainActivity.j(mainActivity.zygote)); // 执行j方法,传入迭代后的zygote来得到flag,如果传入的zygote不正确,也无法得到正确的flag
sb.append("}");
textView2.setText(sb.toString()); // 将flag字符串设置到文本框上来展示
} else if (MainActivity.this.df_all / 100 < 9999) {
textView.setText("too soon"); // 时间太短,提示
} else {
textView.setText("too late"); // 时间太长,提示
}
}
});
到这里,我们便对该APK的执行流程分析完成了。当点击START
按钮时,程序就开始计时,并每隔100
毫秒对zygote
进行一次p
方法操作。当点击END
按钮时,如果时间恰好过了9999900
毫秒,则计算并输出正确的flag。
每隔100
毫秒执行一次p
方法,所以说我们来计算输出正确flag时,需要执行99999
次p
方法来得到正确的zygote
。最后通过j
方法来计算获得最终的flag。
public class MainActivity extends AppCompatActivity {
long df_all;
/* renamed from: t */
long start_time;
/* renamed from: t1 */
long count_time;
/* renamed from: t2 */
long now_time;
int zygote = 1357024680;
public native String j(int i);
public native int p(int i);
static {
System.loadLibrary("j");
}
}
而其中涉及到的p
方法与j
方法均为native方法,接下来便是需要深入so层分析了。
取出位于APK压缩包中lib/armeabi/
下的libj.so
和bak_libj.so
文件,虽然在程序代码System.loadLibrary("j");
中只加载了libj.so
一个so文件,但这里却有两个。
先使用IDA32载入libj.so
,对so文件进行分析。第一步先查看有无初始化函数,发现没有;接着查看是否有JNI_OnLoad
函数,发现也没有,那就是静态注册了。
我们很明显的发现在函数列表中存在三个静态注册的函数,如下图所示。
但在Java层只用了其中两个函数,先不管它,先分析p方法。双击进入p方法,缩略图就让人望而却步。
但如今我们有葫芦娃大佬编写的obpo-project/obpo-plugin: An ida plugin for recovering control flow flattening (github.com)插件。使用插件后F5反编译结果如下图所示。
我们当然可以对这部分代码还原为C代码来编写脚本计算,但在比赛过程中,为了争分夺秒且保证准确性来讲,我们可以使用Frida来进行Java层的主动调用或者Unidbg来进行模拟执行。
当然我们先分析完其余的函数,查看j方法
该函数恒定返回结果"FlagLostHelpMeGetItBack"
,也就是对于源程序的调用逻辑来讲,就算真的能把握好时间,也没有办法获得真正的flag,因为该函数只会返回一个虚假的固定字符串。
继续查看init
方法,该方法并没有在Java层调用。
该方法是将传入的double数据,分别取高四位与低四位作为两个数据,然后计算一个含“libinit”
的字符串并返回。
该so含有两个静态注册的函数
ej
函数并没有做操作,自然我将目标放在了j
函数上,该函数的逻辑如下图
同样是接受一个double类型的数据,然后让其高四位与低四位两个数参与运算,最终返回一个含"hav3f4n"
的字符串。
对于关键的函数全部分析完毕后,我们便开始动手编写脚本了。
出于学习与兴趣,在时间不紧迫的情况下,我们可以慢慢对算法进行还原。但在比赛途中,我们需要的是最短的时间解出题目抢夺Flag,是时候感受一下Frida的威力了。
在原程序中,每100毫秒才会计算一次g
方法,这可等不及!我们可以选择使用Frida来主动调用g
方法,让其快速的完成99999
次的运算。
相关代码如下:
function fff() {
Java.perform(function () {
console.log("start");
var mainActivity = null;
// 内存中搜索an.droid.j.MainActivity的实例对象
Java.choose("an.droid.j.MainActivity", {
onMatch: function (instance) {
mainActivity = instance;
console.log(mainActivity.zygote.value);
},
onComplete: function () {
console.log("search completed");
},
});
var n = 0;
var zy = 1357024680; // 程序中zygote的初始值
var start = new Date().getTime();
for (n = 0; n < 99999 + 1; ++n) {
console.log(n, "-->", zy); // 在这里打印zygote的值
zy = mainActivity.p(zy);
}
var stime = new Date().getTime() - start;
console.log("stime: ",stime,"ms");
});
}
setTimeout(fff, 2000); // 延迟2秒,等待MainActivity实例化
编写完脚本后,脚本运行以及输出如下:
$ frida -H 192.168.0.102:8888 -f an.droid.j -l jstz.js --no-pause
____
/ _ | Frida 12.8.0 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://www.frida.re/docs/home/
Spawned `an.droid.j`. Resuming main thread!
[Remote::an.droid.j]-> start
1357024680
search completed
0 --> 1357024680
1 --> 1357024967
2 --> 1357024968
3 --> 1357025255
4 --> 1357050156
5 --> 1357050443
6 --> 1357064799
7 --> 1357066339
8 --> 1357066340
9 --> 1357066627
...
...
99992 --> 1738885579
99993 --> 1738885580
99994 --> 1738885867
99995 --> 1738885868
99996 --> 1738886155
99997 --> 1738911056
99998 --> 1738911343
99999 --> 1738911344
stime: 37650 ms
可见只用了37秒,就得到了99999次运算后zygote
的结果。
接下来便是对zygote
进行j
方法运算得到最终的flag了。
但libj.so
中的Java_an_droid_j_MainActivity_j
函数是恒定输出的,我们只能把目标放在Java_an_droid_j_MainActivity_init
函数、或bak_libj.so
中的Java_an_droid_j_MainActivity_j
函数。
由于bak_libj.so
并没有被加载,我们使用Unidbg来对其进行模拟调用。
简单搭建一个Unidbg架子,加载bak_libj.so
后模拟执行其中的j
方法。
package an.droid.j;
import com.github.unidbg.Module;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class MainActivity {
public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
int temp = 1738911344;
System.out.println("flag{" + mainActivity.runJ(temp) + "}");
}
private final AndroidEmulator emulator;
private final Memory memory;
private final VM vm;
private Module module;
public MainActivity() {
// 创建模拟器
emulator = AndroidEmulatorBuilder
.for32Bit().build();
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
// 根据APK文件创建VM
vm = emulator.createDalvikVM(new File(MainActivity.class.getResource("jstz.apk").getFile()));
vm.loadLibrary(new File(MainActivity.class.getResource("bak_libj.so").getFile()), false);
}
public String runJ(double i) {
DvmObject obj = ProxyDvmObject.createObject(vm, this);
// 执行函数Java_an_droid_j_MainActivity_j(D)Ljava/lang/String;
DvmObject object = obj.callJniMethodObject(emulator, "j(D)Ljava/lang/String;", i);
String result = (String) object.getValue();
return result;
}
}
得到结果
flag{ichav3f4nnbp}
但是很遗憾,不正确。
只剩下一个选择了,继续使用Unidbg来模拟执行libj.so
中的Java_an_droid_j_MainActivity_init
函数。
package an.droid.j;
import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.DvmObject;
import com.github.unidbg.linux.android.dvm.VM;
import com.github.unidbg.linux.android.dvm.jni.ProxyDvmObject;
import com.github.unidbg.memory.Memory;
import java.io.File;
public class MainActivity {
public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
int temp = 1738911344;
System.out.println("flag{" + mainActivity.runInit(temp) + "}");
}
private final AndroidEmulator emulator;
private final Memory memory;
private final VM vm;
public MainActivity() {
emulator = AndroidEmulatorBuilder
.for32Bit().build();
memory = emulator.getMemory();
memory.setLibraryResolver(new AndroidResolver(23));
vm = emulator.createDalvikVM(new File(MainActivity.class.getResource("jstz.apk").getFile()));
vm.setVerbose(false);
vm.loadLibrary("j",false);
}
public String runInit(int i){
DvmObject obj = ProxyDvmObject.createObject(vm,this);
DvmObject object = obj.callJniMethodObject(emulator, "init(D)Ljava/lang/String;", i);
String result = (String) object.getValue();
return result;
}
public int runP(int i){
DvmObject obj = ProxyDvmObject.createObject(vm,this);
int number = obj.callJniMethodInt(emulator, "p(I)I", i);
return number;
}
}
得到结果flag{nllibinitxv!}
,输入、正确,就是我们心心念念的Flag啦!
同样我们也可以使用Unidbg对整个流程进行模拟,相关代码如下
public class MainActivity {
int zygote = 1357024680;
public static void main(String[] args) {
MainActivity mainActivity = new MainActivity();
// 使用Unidbg模拟执行99999次并计算Flag
long start = System.currentTimeMillis();
for(int i =0;i<99999;i++){
mainActivity.zygote = mainActivity.runP(mainActivity.zygote);
}
System.out.println("zygote = " +mainActivity.zygote);
System.out.println("flag{" + mainActivity.runInit(mainActivity.zygote) + "}");
long times = System.currentTimeMillis() - start;
System.out.println("times = " + times);
}
}
得到的输出如下:
flag{nllibinitxv!}
times = 61553
可见Unidbg的模拟执行速度还是慢了些的,用了整整61秒,不如Frida的主动调用速度快。
通过对本例子的讲解,Frida和Unidbg对于某些情况来说确实是更好的一种选择,特别是在比赛途中时间紧迫的情况下。相信大家已经感受到了Frida与Unidbg的魅力了,还不赶紧学起来!