通过Frida-Labs入门Frida
2024-07-12 00:37:19

Frida-Labs

项目地址:

https://github.com/DERE-ad2001/Frida-Labs/tree/main

请确保你已正确下载Frida和在设备(模拟器)上传对应版本的Frida-server

例:

pip list

image-20240708185307993

image-20240708180201125

请确保已经获取root权限,并且frida-server具有执行权限

image-20240708180046775

模板:

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

image-20240708212030897

frida-ps -Uai

frida-ps:显示有关 Android 设备上运行的进程的信息。
-U:此选项用于列出USB连接设备(物理设备或模拟器)上的进程。
-a:此选项用于列出所有进程,而不仅仅是当前用户拥有的进程。
-i:此选项用于包含有关每个进程的详细信息,例如进程 ID(PID)和进程名称。

image-20240708212153921

类名获取例:

image-20240708212428894

Frida 0x1

image-20240708171735710

填写空值点击提交按钮将退出程序

填写无法通过验证的值点击提交按钮提示try again

JEB反编译:

image-20240708175207784

程序注册了一个按钮,并为这个按钮添加了一个点击事件监听器,当按钮被点击时,将会调用onClick()方法,在方法中先是获取了输入的文本,并进行数字验证,如果输入的是有效的数字,就调用check()方法进行验证,该方法传入了两个参数,一个应该是获取随机数,一个是输入的数字;

image-20240708175613532

获取0 到 99 之间的随机整数;

image-20240708175745415

验证输入的数字和随机数之间是否符合(随机数 * 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

image-20240709001033890

但是并没有任何的输出,原因是什么呢?因为获取随机数的方法是在程序启动时已经加载完毕,我们现在去hook当然是什么都看不懂,所以需要在程序加载的同时注入脚本

frida -U -f com.ad2001.frida0x1 -l .\frida0x1.js --no-pause

–no-pause:确保应用在启动后不会暂停

image-20240709001335405

image-20240709001226076

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

image-20240709001656821

image-20240709001547704

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

image-20240709002250264

随便在框里输入数据,点击按钮触发check方法即可

image-20240709002050933

Frida 0x2

image-20240709092926408

没有按钮也没有输入之类的,直接反编译看看:

image-20240709093132828

程序存在一个静态方法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
image-20240709093617398

Frida 0x3

image-20240709093953409

程序存在一个按钮,猜测是点击进行一些校验,校验成功就将flag显示;

反编译:

image-20240709093941383

程序注册了一个点击事件,需要经过校验Checker.code == 0x200通过时就可以显示flag,注意这个Checker是一个类

image-20240709160343903

这个类有一个静态的变量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

image-20240709161331887

image-20240709161241186

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
image-20240709161848031 image-20240709161858419

Frida 0x4

image-20240709162058654

依旧是啥也没有,那就直接反编译吧

反编译后还是啥都没有

image-20240709162410674

存在check类,类中有一个方法get_flag,当a == 0x539通过校验后将能返回flag值,但是程序中并没有对这个方法进行调用,并且它不是静态方法,需要先将对象实例化,再通过这个实例去调用这个方法返回flag;

image-20240709162446557

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

image-20240709171045180

Frida 0x5

image-20240709171216913

依旧是啥没没有。。上JEB反编译看看:

image-20240709172136772

与上一题类似,只是现在获取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

image-20240709175726787

image-20240709174429976

Frida 0x6

image-20240709175149561

又又又是啥也没有,直接反编译吧

image-20240709175035602

这次依旧是在MainActivity中存在一个能获取flag的方法,与上一题不一样的是校验条件,这一题将接收一个checker实例作为参数,再根据实例中的num1和num2校验;

image-20240709175054897

手动创建一个符合条件的实例,传递给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

image-20240709222623393

image-20240709222355082

Frida 0x7

image-20240709222656351

反编译吧~

image-20240709224304230

可以看到程序创建了一个checker的实例ch,并将它作为参数传入了flag函数中

image-20240709224439625

并且在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

image-20240709230039572

image-20240709230108452

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

image-20240709231250656

image-20240709231231095

Frida 0x8

image-20240709231459663

终于是有一个输入框和按钮了,反编译看看吧

image-20240709232118624

可以看到调用了cmpstr函数处理了我们的输入,当校验通过时多半会输出flag

接下来看看cmpstr:

image-20240709234631549

可以看到程序加载了一个名为frida0x8的库(.so);

通过native关键字声明了一个本地方法cmpstr,所以这个方法将在so层实现;

image-20240709234850188

在JEB中就可以看到所有的lib文件,包含4种架构,选择适合你的架构,在IDA中反编译一下:

image-20240709235044765

image-20240709235111994

image-20240709235326840

可以看到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

image-20240710231650568

接下来只需要在程序输入框中输入Closure提交即可

image-20240710231745193

可以看到通过了条件,但是并没有flag,但是已经在frida中获得flag了:

image-20240710231839675

再将正确的flag提交试试,此时的flag不是Closure是无法触发将strcmp的返回值置为0的

image-20240710232004037

照样通过

Frida 0x9

image-20240710232110664

存在一个提交按钮,应该有校验之类的,JEB启动:

image-20240710232417476

image-20240710232441611

可以看到程序调用了一个库函数check_flag,并且没有参数,那就直接IDA启动看看这个库函数

image-20240710232519090

image-20240710232845457

可以看到这个函数只返回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

image-20240710233232120

image-20240710233208934

Frida 0xA

程序一打开就闪退,直接上JEB看看

image-20240710234251974

image-20240710234446695

可以看到是可以正常打开显示内容的,内容应该是从库函数stringFromJNI中返回的

并没有做任何退出关闭操作,换个Android 11的模拟器试试:

image-20240710234412937

可以了,接下来就是IDA分析一下库了,定位一下函数:

image-20240710234615419

image-20240710234805119

可以看到就是返回一串字符串而已,并没有其他操作了,再找找和flag相关函数看看:

image-20240710234919916

ok,还真有,那这题就是一个手动调用库函数了,注意get_flag需要两个参数,并且需要满足a2 + a1 == 3

image-20240711000333017

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

image-20240711001731062

可以看到没有输出,是正常的,毕竟程序是通过日志输出flag的

image-20240711001857470

根据日志TAG FLAG捕获一下日志:

adb -s 127.0.0.1:5555 logcat -s FLAG

image-20240711001936367

Frida 0xB

image-20240711171109405

程序带有一个按钮,JEB查看反编译结果:

image-20240711002259358

程序加载了frida0xb库,引用了getFlag函数,点击按钮将调用getFlag函数,IDA反编译看看getFlag函数:

image-20240711002350380

可以看到伪代码什么都没有,看看汇编==》

image-20240711002603103

能看到在汇编中对比了0xDEADBEEF和0x539,这当然是不相等的,自然也就不会跳转至关于flag的函数地址==》IDA将省略这段不会跳转的代码

image-20240711171617357

程序将会直接返回,所以我们的目的也很明显,让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

image-20240712001435934

可以看到我们成功修改为NOP,接下来只需要去程序上触发一下getFlag函数==》

捕获日志,注意此处日志的TAG为“FLAG :”

image-20240712001816067

adb logcat | Select-String "FLAG :"
#或者adb shell下执行
logcat |grep -i "FLAG :"

image-20240712001943773

当然,我们可以修改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);

image-20240712002538938

image-20240712002509088

或者修改0xDEADBEEF为0x539,一般是长度长的可以改为小的,小的改长的在有些时候可能会出现溢出:

image-20240712003148859

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);

image-20240712003329449

image-20240712003342575