Frida-Labs
项目地址:
https://github.com/DERE-ad2001/Frida-Labs/tree/main
请确保你已正确下载Frida和在设备(模拟器)上传对应版本的Frida-server
例:
pip list
请确保已经获取root权限,并且frida-server具有执行权限
模板:
Java.perform(function() {
//声明变量表示目标Android应用程序中的JAVA类
//<package_name> Android应用程序包名
//<class> 目标方法所在类
var <class_reference> = Java.use("<package_name>.<class>");
//<class_reference> 代表类的变量名
//<method_to_hook> 要hook的方法名
//<args> 参数
<class_reference>.<method_to_hook>.implementation = function(<args>) {
}
})
包名获取:
运行frida-server
frida-ps -Uai
frida-ps:显示有关 Android 设备上运行的进程的信息。
-U:此选项用于列出USB连接设备(物理设备或模拟器)上的进程。
-a:此选项用于列出所有进程,而不仅仅是当前用户拥有的进程。
-i:此选项用于包含有关每个进程的详细信息,例如进程 ID(PID)和进程名称。
类名获取例:
Frida 0x1
填写空值点击提交按钮将退出程序
填写无法通过验证的值点击提交按钮提示try again
JEB反编译:
程序注册了一个按钮,并为这个按钮添加了一个点击事件监听器,当按钮被点击时,将会调用onClick()方法,在方法中先是获取了输入的文本,并进行数字验证,如果输入的是有效的数字,就调用check()方法进行验证,该方法传入了两个参数,一个应该是获取随机数,一个是输入的数字;
获取0 到 99 之间的随机整数;
验证输入的数字和随机数之间是否符合(随机数 * 2 + 4 == 输入),符合条件将打印正确的flag;
为此,我们有多种绕过验证方法:
1、获取随机数生成的值;
Java.perform(function() {
var MA = Java.use("com.ad2001.frida0x1.MainActivity");
MA.get_random.implementation = function() {
var ran_ret = this.get_random(); //获取get_random值
console.log("get_random: ",ran_ret); //打印获取的值
return ran_ret; //由于get_random是有返回值的,拦截了总得给它还回去
}
})
运行:
frida -U -F .\frida0x1.js
但是并没有任何的输出,原因是什么呢?因为获取随机数的方法是在程序启动时已经加载完毕,我们现在去hook当然是什么都看不懂,所以需要在程序加载的同时注入脚本
frida -U -f com.ad2001.frida0x1 -l .\frida0x1.js --no-pause
–no-pause:确保应用在启动后不会暂停
2、hook get_random()之类的方法,返回一个我们指定的数值
Java.perform(function() {
var a= Java.use("com.ad2001.frida0x1.MainActivity");
a.get_random.implementation = function(){
return 1;
}
})
frida -U -f com.ad2001.frida0x1 -l .\frida0x1.js --no-pause
3、直接hook check的两个参数,变成我们指定的值
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x1.MainActivity");
a.check.overload('int', 'int').implementation = function(a, b) {
this.check(1, 6);
}
});
check方法就不是在程序初始化就调用了,而是点击事件调用的,就不用在加载时注入了
frida -U -F -l .\frida0x1.js
随便在框里输入数据,点击按钮触发check方法即可
Frida 0x2
没有按钮也没有输入之类的,直接反编译看看:
程序存在一个静态方法get_flag,用于在屏幕上显示flag,但是并没有任何关于它的调用,也就是说我们需要在注入脚本中去手动去调用这个静态方法,并且这个方法有一个参数a,需要让a==4919才能通过验证
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x2.MainActivity");
a.get_flag(4919); //调用
});
frida -U -F -l .\frida0x2.js
Frida 0x3
程序存在一个按钮,猜测是点击进行一些校验,校验成功就将flag显示;
反编译:
程序注册了一个点击事件,需要经过校验Checker.code == 0x200通过时就可以显示flag,注意这个Checker是一个类
这个类有一个静态的变量code,为0,还存在一个静态方法increase,它能使code+2,但是在程序中并没有对这个方法进行任何形式的调用;
为此,我们大致有两个方向,一个是hook Checker类中的code变量,让它直接等于0x200就可以通过条件;另外一个方向是主动去调用increase方法0x100次让code累加成0x200也可以通过校验;
请注意此时我们的目标类已经是 Checker了
1、
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x3.Checker");
a.code.value = 0x200;
});
frida -U -F -l .\frida0x3.js
2、
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x3.Checker");
for (var i = 0; i < 0x100; i++) {
a.increase();
console.log("code: ",a.code.value);
}
});
frida -U -F -l .\frida0x3.js
Frida 0x4
依旧是啥也没有,那就直接反编译吧
反编译后还是啥都没有
存在check类,类中有一个方法get_flag,当a == 0x539通过校验后将能返回flag值,但是程序中并没有对这个方法进行调用,并且它不是静态方法,需要先将对象实例化,再通过这个实例去调用这个方法返回flag;
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x4.Check");
var check = a.$new(); //手动创建实例
var flag = check.get_flag(0x539); //调用获得返回值flag
console.log("flag: ",flag);
});
frida -U -F -l .\frida0x4.js
Frida 0x5
依旧是啥没没有。。上JEB反编译看看:
与上一题类似,只是现在获取flag的方法在MainActivity里,与上一题不同的是,这一题MainActivity已经在应用程序启动的时候实例化了,所以我们只需要获取这个实例再进行调用就可以了
Java.perform(function() {
//运行时枚举指定JAVA类的实例
Java.choose("com.ad2001.frida0x5.MainActivity",{
//onMatch对于发现的每个实例都执行回调函数Java.choose
//M_NEW为匹配到的实例
onMatch: function(M_NEW){
console.log("找到实例");
M_NEW.flag(0x539);
},
//onComplete可选,为操作完成后任务
onComplete: function(){}
})
});
frida -U -F -l .\frida0x5.js
Frida 0x6
又又又是啥也没有,直接反编译吧
这次依旧是在MainActivity中存在一个能获取flag的方法,与上一题不一样的是校验条件,这一题将接收一个checker实例作为参数,再根据实例中的num1和num2校验;
手动创建一个符合条件的实例,传递给get_flag即可:
Java.perform(function() {
//运行时枚举指定JAVA类的实例
Java.choose("com.ad2001.frida0x6.MainActivity",{
onMatch: function(M_New){
console.log("找到实例M_New");
var checker = Java.use("com.ad2001.frida0x6.Checker");
var C_New = checker.$new();
C_New.num1.value = 0x4D2;
C_New.num2.value = 0x10E1;
M_New.get_flag(C_New);
},
onComplete: function(){}
})
});
frida -U -F -l .\frida0x6.js
Frida 0x7
反编译吧~
可以看到程序创建了一个checker的实例ch,并将它作为参数传入了flag函数中
并且在Checker类中通过构造函数去赋值,原程序中赋的值并不能通过校验条件
==》有两种方法
1、跟上一题一样,自己创建一个checker的实例,在调用flag传入实例通过验证,区别是需要通过构造函数传参
Java.perform(function() {
//运行时枚举指定JAVA类的实例
Java.choose("com.ad2001.frida0x7.MainActivity",{
onMatch: function(M_New){
console.log("找到实例M_New");
var checker = Java.use("com.ad2001.frida0x7.Checker");
var C_New = checker.$new(0x201,0x201);
M_New.flag(C_New);
},
onComplete: function(){}
})
});
frida -U -F -l .\frida0x7.js
2、checker类不是有构造函数吗,我们直接hook掉,让他直接可以赋值为我们想要的值
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x7.Checker");
a.$init.implementation = function(param){ //通过$init即可hook构造函数
this.$init(0x201,0x201);
}
});
当然,由于创建Checker实例位于onCreate中,我们将要随着程序加载注入
frida -U -f com.ad2001.frida0x7 -l .\frida0x7.js --no-pause
Frida 0x8
终于是有一个输入框和按钮了,反编译看看吧
可以看到调用了cmpstr函数处理了我们的输入,当校验通过时多半会输出flag
接下来看看cmpstr:
可以看到程序加载了一个名为frida0x8的库(.so);
通过native关键字声明了一个本地方法cmpstr,所以这个方法将在so层实现;
在JEB中就可以看到所有的lib文件,包含4种架构,选择适合你的架构,在IDA中反编译一下:
可以看到Java_com_ad2001_frida0x8_MainActivity_cmpstr中获取了我们的输入,并将这段输入使用strcmp函数与一段密文s2进行比较,当它们相等时,将会返回0,从而返回0 == 0 ==》1通过校验条件
所以我们可以直接hook strcmp函数,输出他的两个参数,第二个参数就是密文了,或者直接让strcmp返回0即可,但是这样是拿不到flag,只能通过这个校验条件:
//枚举所有导出函数
var all_0x8_exp = Module.enumerateExports("libfrida0x8.so");
console.log("libfrida0x8.so 所有导出函数:\n",JSON.stringify(all_0x8_exp, null, 2));
//从lib中寻找指定函数的地址,若不知道lib名称可以传NULL,找不到会抛出异常
var cmpstr_addr = Module.getExportByName("libfrida0x8.so","Java_com_ad2001_frida0x8_MainActivity_cmpstr")
var strcmp_addr = Module.getExportByName("libc.so", "strcmp")
var strcmp_null_addr = Module.getExportByName(null, "strcmp")
console.log("\ncmpstr_addr:",cmpstr_addr);
console.log("strcmp_addr:",strcmp_addr);
console.log("strcmp_null_addr:",strcmp_null_addr);
//功能与getExportByName相同,找不到返回null
var Closure_addr = Module.findExportByName("libfrida0x8.so","Closure")
console.log("Closure_addr:",Closure_addr);
//获取基地址
var libc_base = Module.getBaseAddress("libfrida0x8.so")
console.log("libc_base:",libc_base);
var libc_base_add_cmpstr = Module.getBaseAddress("libfrida0x8.so").add(0x8c0)
console.log("libc_base_add_cmpstr:",libc_base_add_cmpstr);
//获取所有导入
var all_0x8_imp = Module.enumerateImports("libfrida0x8.so")
console.log("libfrida0x8.so 所有导入函数:\n",JSON.stringify(all_0x8_imp, null, 2));
var strcmp_addr = Module.findExportByName("libc.so", "strcmp");
var out_put; //全局变量,接收第一个参数
Interceptor.attach(strcmp_addr, { //将回调附加到指定的函数地址
onEnter: function (args) { //进入回调函数时调用
var arg0 = Memory.readUtf8String(args[0]);
var flag = Memory.readUtf8String(args[1]);
out_put = arg0;
if (arg0.includes("Closure")) { //过滤器
console.log("args[0]: ",arg0);
console.log("args[1]: ",flag);
}
},
onLeave: function (retval) { //退出后调用
if (out_put.includes("Closure")) { //过滤器,根据第一个参数的值,防止修改所有的strcmp
retval.replace(0); //返回0
}
}
});
frida -U -F -l .\frida0x8.js
接下来只需要在程序输入框中输入Closure提交即可
可以看到通过了条件,但是并没有flag,但是已经在frida中获得flag了:
再将正确的flag提交试试,此时的flag不是Closure是无法触发将strcmp的返回值置为0的
照样通过
Frida 0x9
存在一个提交按钮,应该有校验之类的,JEB启动:
可以看到程序调用了一个库函数check_flag,并且没有参数,那就直接IDA启动看看这个库函数
可以看到这个函数只返回1,并不能通过校验,这边直接hook,将返回值改为0x539就行了
var flag_addr = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag");
Interceptor.attach(flag_addr, { //将回调附加到指定的函数地址
onEnter: function (args) { //进入回调函数时调用
},
onLeave: function (retval) { //退出后调用
retval.replace(0x539); //返回0x539
}
});
frida -U -F -l .\frida0x9.js
Frida 0xA
程序一打开就闪退,直接上JEB看看
可以看到是可以正常打开显示内容的,内容应该是从库函数stringFromJNI中返回的
并没有做任何退出关闭操作,换个Android 11的模拟器试试:
可以了,接下来就是IDA分析一下库了,定位一下函数:
可以看到就是返回一串字符串而已,并没有其他操作了,再找找和flag相关函数看看:
ok,还真有,那这题就是一个手动调用库函数了,注意get_flag需要两个参数,并且需要满足a2 + a1 == 3
var get_flag_addr = Module.findExportByName("libfrida0xa.so","get_flag")
console.log("get_flag_addr:",get_flag_addr);
//主动调用一个没有被导入的函数需要通过基地址+偏移的方式
var get_flag_addr = Module.findBaseAddress("libfrida0xa.so").add(0x206B0)
console.log("get_flag_addr:",get_flag_addr);
//创建NativePointer对象
var get_flag_ptr = new NativePointer(get_flag_addr);
//创建NativeFunction对象,参数为NativePointer对象,返回类型,原函数参数
const get_flag = new NativeFunction(get_flag_ptr, 'void', ['int', 'int']);
get_flag(1,2); //主动调用
frida -U -F -l .\frida0xA.js
可以看到没有输出,是正常的,毕竟程序是通过日志输出flag的
根据日志TAG FLAG捕获一下日志:
adb -s 127.0.0.1:5555 logcat -s FLAG
Frida 0xB
程序带有一个按钮,JEB查看反编译结果:
程序加载了frida0xb库,引用了getFlag函数,点击按钮将调用getFlag函数,IDA反编译看看getFlag函数:
可以看到伪代码什么都没有,看看汇编==》
能看到在汇编中对比了0xDEADBEEF和0x539,这当然是不相等的,自然也就不会跳转至关于flag的函数地址==》IDA将省略这段不会跳转的代码
程序将会直接返回,所以我们的目的也很明显,让getFlag执行关于flag的相关代码,通过0xDEADBEEF和0x539的校验(将JNZ改为JMP或者修改0xDEADBEEF为0x539)或者去掉这层校验,去掉这层校验代码(NOP)将继续顺序执行至处理flag部分,也能达成我们的目的;
var libc_base = Module.getBaseAddress("libfrida0xb.so") //libc基地址
var jnz = libc_base.add(0x170CE); //JNZ所在偏移
console.log("libc_base : ",libc_base);
console.log("jnz : ",jnz);
Memory.protect(jnz, 0x1000, "rwx"); //赋予rwx权限
var writer = new X86Writer(jnz);
// 读取内存范围内的指令
var size = 0x30; // 读取的字节数
var instructionBytes = Memory.readByteArray(jnz, size);
console.log("instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("instructions :",instructions);
try {
for (var i = 0; i < 0x170D4-0x170CE; i++) { //填充6个NOP
writer.putNop()
}
writer.flush();// 刷新指令缓存
} finally {
writer.dispose();// 释放writer对象
}
var instructionBytes = Memory.readByteArray(jnz, size);
console.log("new instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("new instructions :",instructions);
frida -U -F -l .\frida0xB.js
可以看到我们成功修改为NOP,接下来只需要去程序上触发一下getFlag函数==》
捕获日志,注意此处日志的TAG为“FLAG :”
adb logcat | Select-String "FLAG :"
#或者adb shell下执行
logcat |grep -i "FLAG :"
当然,我们可以修改JNZ为JMP:
var libc_base = Module.getBaseAddress("libfrida0xb.so") //libc基地址
var jnz = libc_base.add(0x170CE); //JNZ所在偏移
console.log("libc_base : ",libc_base);
console.log("jnz : ",jnz);
Memory.protect(jnz, 0x1000, "rwx"); //赋予rwx权限
var writer = new X86Writer(jnz);
// 读取内存范围内的指令
var size = 0x30; // 读取的字节数
var instructionBytes = Memory.readByteArray(jnz, size);
console.log("instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("instructions :",instructions);
try {
writer.putBytes([0xEB, 0x0A]); //替换为JMP,0x0A为条件不满足则跳转
writer.flush();
} finally {
writer.dispose();
}
var instructionBytes = Memory.readByteArray(jnz, size);
console.log("new instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(jnz, instructionBytes);
console.log("new instructions :",instructions);
或者修改0xDEADBEEF为0x539,一般是长度长的可以改为小的,小的改长的在有些时候可能会出现溢出:
var libc_base = Module.getBaseAddress("libfrida0xb.so") //libc基地址
var DEADBEEF = libc_base.add(0x170C0); //0xDEADBEEF所在偏移
console.log("libc_base : ",libc_base);
console.log("DEADBEEF : ",DEADBEEF);
Memory.protect(DEADBEEF, 0x1000, "rwx"); //赋予rwx权限
var writer = new X86Writer(DEADBEEF);
// 读取内存范围内的指令
var size = 0x30; // 读取的字节数
var instructionBytes = Memory.readByteArray(DEADBEEF, size);
console.log("instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(DEADBEEF, instructionBytes);
console.log("instructions :",instructions);
try {
writer.putBytes([0xC7, 0x45, 0xDC, 0x39, 0x05, 0x00, 0x00, 0x00, 0x00]); //修改为0x539
writer.flush();
} finally {
writer.dispose();
}
var instructionBytes = Memory.readByteArray(DEADBEEF, size);
console.log("new instructionBytes :",instructionBytes);
// 解析并输出汇编指令
var instructions = Instruction.parse(DEADBEEF, instructionBytes);
console.log("new instructions :",instructions);