Frida_Practice

Frida-Practice

样本链接

https://github.com/OWASP/owasp-mastg/tree/master/Crackmes

Uncrackable3

校验so和dex

首先创建一个HashMap<key,value>,<key,value>是Entry的一个实体,获取资源值对应的字符串后转为Long类型,然后使用put方法存入HashMap中,获得到对应的文件后进行CRC校验

对so文件和dex文件进行CRC校验防止修改

遍历HashMap

调试检测

创建异步任务来检测调试

doInBackground方法是自定义的线程任务,onPostExecute在线程任务执行后进行,参数接收线程任务执行结果,参数为对UI控件进行设置,execute()用户手动调用异步任务

Root

Root环境检测

  • test-keys表示为非官方发布版本, release-keys为官方发布版本

  • 检查su命令-检测常用目录下是否存在su、判断系统环境变量是否存在su

  • 检测指定路径下的文件是否存在

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var clazz=Java.use("sg.vantagepoint.util.RootDetection")
clazz.checkRoot1.implementation=function(args){
return false;
}
clazz.checkRoot2.implementation=function(args){
return false;
}
clazz.checkRoot3.implementation=function(args){
return false;
}
var clazz11=Java.use("sg.vantagepoint.util.IntegrityCheck");
clazz11.isDebuggable.implementation=function(args){
return false;
}

Frida检测

使用Interceptor.replace替换循环检测Frida的函数

第一个参数是要替换函数的地址,第二个参数是一个NativePointer类型的函数,一般由new NativeCallback创建,NativeCallback包括(函数的实现,函数的返回类型,函数的参数类型列表)。返回类型最好保持和原函数一致。同时我们也可以使用NativeFunction来主动调用原函数或是so层函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//也可以直接Hook strstr比较函数 
var addr=Module.findExportByName("libc.so","strstr");
console.log(addr);
//console.log(addr);
Interceptor.attach(addr,{
onEnter:function(args){
//console.log(args[1].readCString());
},
onLeave:function(retval){
retval.replace(0);
return retval;
}
})

var pt_create_func=Module.findExportByName("libc.so","pthread_create");
console.log("pt_create_func_addr:",pt_create_func);
var detect_frida_addr=null;
Interceptor.attach(pt_create_func,{
onEnter:function(args){
if(detect_frida_addr==null){
var base_addr=Module.findBaseAddress("libfoo.so");
detect_frida_addr=base_addr.add(0x30D0);

console.log("Ptread_Addr:",base_addr);
Interceptor.replace(this.context.x2,new NativeCallback(function(a1){
console.log("Replace Success");
return;
},'void',[]));
}
},
onLeave:function(retval){

}
})

如果想要在替换的函数中使用原函数的参数,需要注意参数类型,比如传入的env、jclass、data是指针类型,那么参数列表中就要声明为"pointer"

Hook 函数用于获取参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var MainActivity = Java.use("sg.vantagepoint.uncrackable3.MainActivity");//Hook MainActivity的构造器,否则会提示so未加载
MainActivity.$init.implementation = function() {
this.$init();
attachToSecretGenerator();
};
var addr2=Module.findExportByName("libfoo.so","Java_sg_vantagepoint_uncrackable3_CodeCheck_bar");
Interceptor.attach(addr2,{
onEnter:function(args){
console.log((args[2].readByteArray(32)));//读取数据
var addr1=Module.findBaseAddress("libfoo.so");
addr1=addr1.add(0x15038);
console.log("=====xor_key=====");
console.log(hexdump(addr1));
},
onLeave:function(retval){

}
})
})
}

function attachToSecretGenerator() {

Interceptor.attach(Module.findBaseAddress('libfoo.so').add(0x10E0), {
onEnter: function(args) {
//因为参数会被加密,我们需要使用一个变量进行存储,再在onLeave打印加密结果
this.answerLocation = args[0];
},
onLeave: function(retval) {

console.log(this.answerLocation.readByteArray(32));
}
});
}

frida测试题

wp

Challenge-Six

反编译后的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.dta.test.frida.activity;

import android.view.View;
import com.dta.test.frida.R;
import com.dta.test.frida.base.BaseActivity;
import com.dta.test.frida.base.Level;

/* loaded from: classes.dex */
public class SixthActivity extends BaseActivity {
@Override // com.dta.test.frida.base.BaseActivity
protected String getActivityTitle() {
return "第六关";
}

@Override // com.dta.test.frida.base.BaseActivity
protected String getDescription() {
return getString(R.string.sixth);
}

@Override // com.dta.test.frida.base.BaseActivity, android.view.View.OnClickListener
public void onClick(View view) {
super.onClick(view);
try {
Class<?> cls = Class.forName("com.dta.test.frida.activity.RegisterClass");
if (((Boolean) cls.getDeclaredMethod("next", new Class[0]).invoke(cls.newInstance(), new Object[0])).booleanValue()) {
gotoNext(Level.Seventh);
} else {
failTip();
}
} catch (Exception unused) {
failTip();
}
}
}

代码中使用反射调用com.dta.test.frida.activity.RegisterClass中的next方法,然后接收其返回值

Frida为我们提供了java.registerClass来注册类到内存中,其格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var RegisterClass=Java.registerClass({
name:"com.dta.test.frida.activity.RegisterClass",
//superClass:
//类中定义的方法
methods:{
//定义next方法
next: {
//返回值类型
returnType: "boolean",
//函数参数列表,这里为空参
argumentTypes:[],
//实现方法
implementation: function(){
return true//返回true
}
}
//构造器
// $init: function () {
// console.log('Constructor called');
// }

}
})

这样子就注册好了next方法,但是我们仍无法通过这一关

1
console.log(RegisterClass.$new().next());

使用上述代码创建一个类的实例后调用next方法会发现我们的类成功注册并且next方法创建成功

这里的问题出在了ClassLoader上

翻看Class.forName()函数参数列表可以知道在其参数列表中存在ClassLoader

默认为当前类的ClassLoader,在Android中默认的活动加载器为PathClassLoader ,Frida加载我们自定义类RegisterClass使用的ClassLoader跟SixthActivity的PathClassLoader并不是同一个ClassLoader,所以导致我们使用PathClassLoader来加载会出现ClassNotFoundException

1
2
var targetClassLoader = RegisterClass.class.getClassLoader()//获取类加载器,也就是我们自定义的RegisterClass的加载器
console.log(targetClassLoader);

使用class.getClassLoader获取类加载器后打印可知其使用的是DexClassLoader

由于使用双亲委派机制,所以我们可以修改其父加载器使其能正确加载

安卓类加载器的继承关系图

查看源图像

双亲委派机制是指当加载类时首先不会选择自身进行加载,而是先交由其父加载器进行,如果父加载器加载不了再由自己进行加载,所以相当于从ClassLoader往下开始进行加载

我们可以在PathClassLoader与BaseDexClassLoader之间插入我们定义类的加载器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var targetClassLoader = RegisterClass.class.getClassLoader()//获取类加载器,也就是我们自定义的RegisterClass的加载器
console.log(targetClassLoader);
Java.enumerateClassLoaders({//枚举类加载器中能加载SixthActivity的
onMatch: function(loader){
try{
if(loader.findClass("com.dta.test.frida.activity.SixthActivity")){
// PathClassLoader
var PathClassLoader = loader
//通过parent来获取类加载器的父类加载器
var BootClassLoader = PathClassLoader.parent.value
PathClassLoader.parent.value = targetClassLoader
targetClassLoader.parent.value = BootClassLoader//在BootClassLoader和PathClassLoader中插入我们的类加载器,这样当BootClassLoader加载失败就会回到我们自定义的加载器来进行加载
}

}catch(e){

}
},
onComplete: function(){
console.log("Completed!")
}
})

也可以Hook Class.forName方法,指定ClassLoader为RegisterClass的加载器

Anti-Frida&Pass

Frida-Detect

**由于Frida是代码注入工具,所以Frida的模块会在进程中留下痕迹,proc是Linux中的伪文件系统,而在内存中存在/proc/pid目录存储进程的一些信息,应用程序可以访问获得内核的信息,/proc/pid/maps显示进程的内存区域映射信息 **

  1. 通过maps检测:扫描/proc/pid/maps文件中的内存分布,寻找是否打开了/data/local/tmp路径下的so,(Frida在运 行时会先确定/data/local/tmp路径下是否有re.frida.server文件夹,若没有则创建该文件夹并存放frida-agent.so等文件),我们使用cat命令即可获得

  2. 通过task检测:扫描task目录下所有/task/pid/status中的Name字段是否存在frida注入的特征,具体线程名为gmain、gdbus、gum-js-loop,一般这三个线程是在第11-13行,同时也会存在Name字段为pool-frida的线程

  3. 通过fd检测:通过readlink查看/proc/pid/fd和/proc/task/pid/fd下所有的文件,检测是否有frida相关文件

  4. 端口检测:Frida默认的端口为27042,检测该TCP端口是否开放

  5. 通过D-Bus检测: Frida是通过D-Bus协议进行通信的,所以可以遍历/proc/net/tcp文件,向每个开放的端口发送 D-Bus 的 认证消息 AUTH ,如果端口回复了 REJECT ,那么这个端口就是frida-server

Easy-AntiFrida-Pass

PortCheck

ThreadCheck

MapsCheck

TraceCheck

MemoryCheck

maps显示当前正在运行进程的地址映射或进程的内存地址空间的文件

fdCheck

总结

从上面的各种检测方式中,可以看出主要方式都是通过打开特定的文件和进程内存文件,然后使用字符串比较函数strstr,或者使用stat判断文件是否存在,也可以进行端口检测,Frida默认端口是27042

对于端口检测,可以Hook掉connect函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const module=Process.getModuleByName("libc.so");
console.log(module.base);
//var methods=module.enumerateExports();
// for(var i=0;i<methods.length;++i){
// console.log(methods[i].name);
// }
var connect=module.findExportByName("connect");
console.log(connect);
Interceptor.attach(connect,{
onEnter:function(args){

},
onLeave:function(retval){
console.log("Change Success!!");
//使用replace修改参数和返回值
retval.replace(1);//将返回值修改为1表示连接端口成功
}
})

也可以修改frida-server默认端口,**/data/local/tmp # ./frida-server-15.1.17-android-arm64 -l 0.0.0.0:1234,然后adb forward tcp:1234 tcp:1234,脚本运行: frida -H 127.0.0.1:1234 -f owasp.mstg.uncrackable3 -l .\Uncrackme3.js **

所以我们只需要Hook掉libc中的strstr函数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const module=Process.getModuleByName("libc.so");
//首先先获取模块,然后在模块的导出表中循寻找strstr函数,
var strstr=module.findExportByName("strstr");
console.log(strstr);
Interceptor.attach(strstr,{
onEnter:function(args){
//console.log(args[1].readCString());//C字符串读取为JS字符串
},
onLeave:function(retval){
retval.replace(0);
//修改strstr函数的返回值
}
})

var stat=module.findExportByName("stat");
console.log(stat);
Interceptor.attach(stat,{
onEnter:function(args){

},
onLeave:function(retval){
retval.replace(1);
}
})

虽然这里的System.LoadLibrary是static属性的,但是不知道为啥没有提前加载=。=,所以需要我们手动Hook主活动的构造器

1
2
3
4
var MainActivity = Java.use("com.yimian.envcheck.MainActivity");//让他加载so
MainActivity.$init.implementation = function() {
this.$init();//调用构造器
};

当然我们也可以Hook掉比较的字符串,首先先在内存中写入我们用于比较的字符串(allocUtf8String),然后将地址在onEnter回调函数中进而修改strstr函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
var newStr="new String";
var newstraddr=Memory.allocUtf8String(newStr);//写入内存,返回字符串第一个字符的地址
var strcpy=module.findExportByName("strstr");
Interceptor.attach(strcpy,{
//对于数值参数的修改,使用ptr()即可,字符串则需要在内存中Alloc后重新将地址赋值给参数
onEnter:function(args){
args[1]=newstraddr;
console.log(args[1].readCString());
},
onLeave:function(retval){

}
})

在Memory的check中使用的是逐字符比较,我们可以Hook掉汇编指令中的比较值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const byte1=[0x5F,0x41,0x01,0x71];
const byte2=[0x7F,0xE9,0x01,0x71];
const byte3=[0x7F,0xE9,0x01,0x71];
const byte4=[0x3F,0x42,0x01,0x71];
const byte5=[0x5F,0x42,0x01,0x71];

var module_base=Module.findBaseAddress("libtestfrida.so");
//console.log(module_base);
var func=module_base.add(0x001D5C);
console.log(Instruction.parse(func));

console.log(func);
var func2=module_base.add(0x01E54);
var func5=module_base.add(0x1FF8);
var func3=module_base.add(0x01F24);
var func4=module_base.add(0x20BC);
//console.log(Instruction.parse(func4));
//查看地址出的arm汇编
// console.log(Instruction.parse(func));
//console.log(Instruction.parse(func2));
Memory.protect(func,8,'rwx');
Memory.protect(func2,8,'rwx');
Memory.protect(func3,8,'rwx');
Memory.protect(func4,8,'rwx');
Memory.protect(func5,8,'rwx');
func.writeByteArray(byte1);
func2.writeByteArray(byte2);
func3.writeByteArray(byte3);
func4.writeByteArray(byte4);
func5.writeByteArray(byte5);

首先我们先找到想要Hook的CMP汇编地址,然后根据机器码创建byte数组,最后使用writeByteArray写入即可

以0x1DA4为例,[5F,C9,01,71]是地址中原来的数据,C9是用于比较的数,我们只需要修改其即可,最后写入内存中

可以使用Instruction.prase(addr)打印汇编代码,使用Memory.protect(addr,size,“rwx”);来设置修改内存的属性和大小

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(func2,{
onEnter:function(args){
console.log(this.context.x11);
this.context.x11=0x45;
console.log(this.context.x11);
},
onLeave:function(retval){
console.log(this.context.x11);
}
})

也可以使用Frida进行inlineHook,并且修改寄存器的值,这里的W11是X11的低32位

最终效果