Frida-Labs wp
arch3rn4r

0x1 同一个类中的静态方法

分析
基本结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle bundle) {
public void onClick(View view) {
if (TextUtils.isDigitsOnly(obj)) {
MainActivity.this.check(i, Integer.parseInt(obj));
}
}
});
}

void check(int i, int i2) {
if ((i * 2) + 4 == i2) {
}}

满足check的逻辑就可以,i2=i*2+4
这里的check方法会主动触发,不需要额外调用,在这里,只需要更改原来check的逻辑就可以了

方法一
以下代码的编写是在jadx里直接“复制为frida片段”获得

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout(function() {
    Java.perform(function() {
        console.log("hookTest1 is called");
        let MainActivity = Java.use("com.ad2001.frida0x1.MainActivity");
        MainActivity["check"].implementation = function (i, i2) {
            console.log(`MainActivity.check is called: i=${i}, i2=${i2}`);
            i2=i*2+4; //在这里定义i2
            console.log(`i2 is changed to ${i2}`);
            this["check"](i, i2);
        };
    });
}, 0);

方法二
以下代码调用Frida 的 JavaScript API。
onMatch: function (instance) { … }
该回调函数会在每次找到 className 指定的类的实例时被调用。
nComplete: function () { … }

  • 这个回调函数在 Frida 完成了对所有目标进程的内存扫描,并且枚举完所有 className 指定的类的实例之后被调用。
  • 可以在 onComplete 函数中执行一些清理工作,或者打印一些总结信息。onComplete: function () {},这意味着在枚举完成之后什么也不做。
    1
    2
    3
    4
    5
    6
    7
    Java.choose("com.ad2001.frida0x1.MainActivity", {
        onMatch: function (instance) {
            console.log("[+] 找到 MainActivity 实例,自动触发解密");
            instance.check(0, 4); // 传入 i=0, i2=4
        },
        onComplete: function () {}
    });

方法三
a.check.overload(‘int’, ‘int’).implementation = function(a, b) { … }

  • .overload(‘int’, ‘int’): 指定要 hook 的 check 方法的重载版本。 check 方法可能存在多个重载版本,’int’, ‘int’ 表示选择接受两个 int 类型参数的版本。 这很重要,因为如果方法有多个重载,你必须明确指定要 hook 的版本。
  • .implementation = function(a, b) { … }: 设置 hook 的具体实现。 你在这里定义的函数将会在每次调用被 hook 的 check 方法时被执行。
  • this.originalMethod() 用于调用原始的方法(如果需要)
    1
    2
    3
    4
    5
    6
    7
    Java.perform(function() {
    var a = Java.use("com.ad2001.frida0x1.MainActivity");
    a.check.overload('int', 'int').implementation = function(a, b) {
    this.check(4, 12);
    }
    });

0x2 主动调用同一个类中的静态方法

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0497R.layout.activity_main);
f103t1 = (TextView) findViewById(C0497R.id.textview);
}

public static void get_flag(int a) {
if (a == 4919) {

}
}

这次目标方法不再原本的程序流程内,需要额外进行调用
java.choose和在jadx直接赋值frida片段都是调用的实例化对象,而get_flag没有被实例化

Java.choose 只能找到已经实例化的对象,所以这个代码不行

1
2
3
4
5
6
Java.choose("com.ad2001.frida0x2.MainActivity",{
    onMatch:function(instance){
        instance.get_flag(4919);
    }
    ,onComplete:function(){}
});

方法一
Frida 的 Java.use 返回的类包装器对象(这里是 MainActivity)直接暴露了其静态成员(包括方法和字段),允许你像访问普通 JavaScript 对象的属性一样访问和调用它们。(它看起来非常像你在 Java 中直接编写 MainActivity.get_flag(4919);)
这个方法隐式的处理了上下文的关系,比方法二更简洁
.<方法>

1
2
3
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x2.MainActivity");
})

那么实际操作就是

1
2
3
4
Java.perform(function() {
        var a = Java.use("com.ad2001.frida0x2.MainActivity");
a.get_flag(4919);
})

方法二
核心是调用.call方法,此代码也可行

1
2
3
4
5
Java.perform(function(){
    var MainActivity = Java.use("com.ad2001.frida0x2.MainActivity");
console.log("[*] 主动调用。。。");
MainActivity.get_flag.call(MainActivity, 4919);
});

调用静态方法时,.call 的第一个参数是类包装器;调用实例方法时,.call 的第一个参数是实例对象

  • 对于 Java 的静态方法,它们不依赖于任何特定的对象实例,它们属于类本身。当你在 Frida 中使用 .call() 调用一个静态 Java 方法时,你需要提供一个上下文。按照约定和实现,将类包装器本身 (MainActivity) 作为 this 上下文传递给 call 是正确的做法,它告诉 Frida 这个方法调用是针对这个类的静态上下文。

0x3同一个包下的不同类

分析

1
2
3
4
5
6
7
package com.ad2001.frida0x3;
public class MainActivity extends AppCompatActivity {
public void onClick(View v) {
if (Checker.code == 512) {
}
}
}

check类

1
2
3
4
5
6
7
package com.ad2001.frida0x3;
public class Checker {
static int code = 0;
public static void increase() {
code += 2;
}
}

这里的check不再和主方法在一个类,但是在它的运行逻辑中
因此不需要主动调用,只需要更改它本身逻辑
(这里可以对照0x1的方法三)

思路一
循环调用checker的increase()方法来递增值

1
2
3
4
5
6
7
8
9
10
11
Java.perform(function() {
    var Checker = Java.use("com.ad2001.frida0x3.Checker");
    console.log("code当前值为"+Checker.code.value);
   
    Checker.code.value=0;
    for(var i=0;i<256;i++){
        Checker.increase();
    }
    console.log("触发函数后的值为"+Checker.code.value);  

});

思路二
把code直接改成512
修改值的模板如下

1
2
3
4
5
6
Java.perform(function (){

var <class_reference> = Java.use("<package_name>.<class>");
<class_reference>.<variable>.value = <value>;

})

最终实现

1
2
3
4
5
6
Java.perform(function (){

var a = Java.use("com.ad2001.frida0x3.Checker"); // class reference
a.code.value = 512;

})

0x4 实例化

mainactivity

1
2
3
4
5
6
7
8
package com.ad2001.frida0x4;
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0497R.layout.activity_main);
this.f103t1 = (TextView) findViewById(C0497R.id.txtview);
}
}

check

1
2
3
4
5
6
7
8
9
10
package com.ad2001.frida0x4;
public class Check {
public String get_flag(int a) {
if (a == 1337) {
}
return new String(decoded);
}
return "";
}
}

这次需要进行实例化
$new()

模板

1
2
3
4
5
6
7
Java.perform(function() {

var <class_reference> = Java.use("<package_name>.<class>");
var <class_instance> = <class_reference>.$new(); // Class Object
<class_instance>.<method>(); // Calling the method

})

使用效果
这样就可以在控制台打印flag

1
2
3
4
5
6
7
8
Java.perform(function() {

var check = Java.use("com.ad2001.frida0x4.Check");
var check_obj = check.$new(); // Class Object
var res = check_obj.get_flag(1337); // Calling the method
console.log("FLAG " + res);

})

这个将会直接调用新的实例化后的静态方法,传入值然后直接解密,和MainActivity没关系了

0x5 有上下文的实例化

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ad2001.frida0x5;
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0497R.layout.activity_main);
this.f103t1 = (TextView) findViewById(C0497R.id.textview);
}

public void flag(int code) {
if (code == 1337) {

this.f103t1.setText(decryptedText);

}
}
}

看起来可以模仿0x1的方法二

1
2
3
4
5
6
7
Java.choose("com.ad2001.frida0x5.MainActivity",{
    onMatch:function(instance){
        console.log("[+] 找到实例,自动触发解密");
        instance.flag(1337);
    },
    onComplete:function(){}
});

但是出现了报错,报错如下:

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
[Pixel 4::Frida 0x5 ]->Error: java.lang.ClassNotFoundException: Didn't find class "com.ad2001.frida0x5.MainActivity" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /product/lib64, /system/lib64, /product/lib64]]       
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:115)
at apply (native)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:97)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:488)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:949)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:954)
at _make (frida/node_modules/frida-java-bridge/lib/class-factory.js:165)
at use (frida/node_modules/frida-java-bridge/lib/class-factory.js:62)
at _chooseObjectsArtPreA12 (frida/node_modules/frida-java-bridge/lib/class-factory.js:335)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:303)
at kt (frida/node_modules/frida-java-bridge/lib/android.js:586)
Error: java.lang.ClassNotFoundException: Didn't find class "com.ad2001.frida0x5.MainActivity" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /product/lib64, /system/lib64, /product/lib64]]
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:115)
at apply (native)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:97)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:488)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:949)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:954)
at _make (frida/node_modules/frida-java-bridge/lib/class-factory.js:165)
at use (frida/node_modules/frida-java-bridge/lib/class-factory.js:62)
at _chooseObjectsArtPreA12 (frida/node_modules/frida-java-bridge/lib/class-factory.js:335)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/class-factory.js:303)
at kt (frida/node_modules/frida-java-bridge/lib/android.js:586)

原因如下;因为 flag 方法内部执行了 this.f103t1.setText(),这是一个 UI 操作。UI 操作必须在 Android 的主线程(UI 线程)执行。 因此,你需要将调用包装在 Java.scheduleOnMainThread 中,否则可能会遇到 CalledFromWrongThreadException 错误。
改进后的脚本
Hook 一个晚期方法: Hook 一个已知在 MainActivity 加载后肯定会执行的方法(例如 Activity.onResume),然后在该 Hook 内部执行 Java.choose

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
35
36
37
38
39
Java.perform(function () {
var activityClass = "com.ad2001.frida0x5.MainActivity";
var hookInstalled = false;

// 尝试 Hook Activity 的 onResume
try {
var Activity = Java.use("android.app.Activity");
Activity.onResume.implementation = function () {
console.log("[*] Activity.onResume() called. Target class should be loaded.");

// 只执行一次 Java.choose
if (!hookInstalled) {
hookInstalled = true; // 防止重复执行 choose
Java.choose(activityClass, {
onMatch: function (instance) {
console.log("[+] Found instance: " + instance + " via onResume hook.");
Java.scheduleOnMainThread(function () {
console.log("[*] Calling instance.flag(1337) on main thread...");
instance.flag(1337);
});
// return 'stop'; // 如果只需要一个实例
},
onComplete: function () {
console.log("[*] Java.choose search complete (triggered from onResume).");
}
});
}
// 调用原始的 onResume 方法
this.onResume();
};
console.log("[*] Hook installed on Activity.onResume. Waiting for activity to resume...");
} catch (e) {
console.error("[-] Failed to hook Activity.onResume: " + e);
console.error("[-] Falling back to simple setTimeout...");
// 如果 Hook 失败 (例如权限问题),回退到 setTimeout
setTimeout(function () { /* ... setTimeout code from above ... */ }, 3000);
}
});

看起来可以模仿0x2

1
2
3
4
Java.perform(function () {
    var a = Java.use("com.ad2001.frida0x5.MainActivity");
    a.flag(1337);
})

但是出现了报错,报错如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[Pixel 4::Frida 0x5 ]-> Error: flag: cannot call instance method without an instance
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1139)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:3)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:4)
Error: flag: cannot call instance method without an instance
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1139)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:3)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:4)

0x5和0x2的区别在于一个是public void flag(int code)而另一个是静态方法public static void get_flag(int a) (差了一个static
如果把0x5当成静态方法来处理那么就会出现很多的报错

看起来可以模仿0x4

那么现在就需要调用实例化方法
在0x4我们为了调用get_flag实例化了Check类

1
2
3
4
5
6
7
8
9
10
package com.ad2001.frida0x4;
public class Check {
public String get_flag(int a) {
if (a == 1337) {
}
return new String(decoded);
}
return "";
}
}

那么现在为了调用flag我们要调用MainActivity类吗

1
2
3
4
5
6
7
package com.ad2001.frida0x5;
public class MainActivity extends AppCompatActivity {
public void flag(int code) {
if (code == 1337) {
}
}
}

这个代码是调用MainActivity的

1
2
3
4
5
6
Java.perform(function () {
let MainActivity = Java.use("com.ad2001.frida0x5.MainActivity");

var main = MainActivity.$new();
main.flag(1337);
});

它出现的报错是

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
[Pixel 4::Frida 0x5 ]-> Error: flag: cannot call instance method without an instance
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1139)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:3)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:4)
Error: flag: cannot call instance method without an instance
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1139)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:3)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:4)
Error: java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-6,10,main] that has not called Looper.prepare()
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1237)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:643)
at apply (native)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1141)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:4)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:7)
Error: java.lang.RuntimeException: Can't create handler inside thread Thread[Thread-7,10,main] that has not called Looper.prepare()
at <anonymous> (frida/node_modules/frida-java-bridge/lib/env.js:124)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1237)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:643)
at apply (native)
at value (frida/node_modules/frida-java-bridge/lib/class-factory.js:1141)
at e (frida/node_modules/frida-java-bridge/lib/class-factory.js:610)
at <anonymous> (D:\anquan_question\frida\test.js:4)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:7)

为什么不能实例化MainActivity?
由于 Android 的生命周期和线程规则,直接使用 Frida 创建 MainActivity 或任何 Android 组件的实例可能很棘手。Android 组件(如 Activity 子类)依赖于应用程序上下文才能正常运行。在 Frida 中,您可能缺少必要的上下文。Android UI 组件通常需要具有关联 Looper 的特定线程。如果您正在处理 UI 任务,请确保您位于具有活动 Looper 的主线程上。Activity 是更大的 Android 应用程序生命周期的一部分。创建 MainActivity 的实例可能需要应用程序处于特定状态,并且通过 Frida 管理整个生命周期可能并不简单。总之,为 MainActivity 创建实例不是一个好主意。
这里的解决方法是:
当 Android 应用程序启动时,系统会创建 MainActivity 的实例(或在 AndroidManifest.xml 文件中指定的启动器 Activity)。MainActivity 实例的创建是 Android 应用程序生命周期的一部分。因此,我们只需使用 frida 来获取 MainActivity 的实例,然后调用 flag() 方法来获取我们的标志。

在这里介绍一个新的模板
在现有实例上调用方法
Java.performNow :用于在 Java 运行时的上下文中执行代码的函数。
Java.choose:在运行时枚举指定 Java 类(作为第一个参数提供)的实例。

1
2
3
4
5
6
7
8
Java.performNow(function() {
Java.choose('<Package>.<class_Name>', {
onMatch: function(instance) {
// TODO
},
onComplete: function() {}
});
});

onMatch:onMatch 回调函数针对在 Java.choose作期间找到的指定类的每个实例执行。该回调函数接收当前实例作为其参数。可以在 onMatch 回调中定义要对每个实例执行的自定义操作。
onComplete 回调在 Java.choose作完成后执行作或清理任务。此块是可选的,如果您在搜索完成后不需要执行任何特定作,则可以选择将其留空。

实际操作如下

1
2
3
4
5
6
7
8
9
Java.performNow(function() {
Java.choose('com.ad2001.frida0x5.MainActivity', {
onMatch: function(instance) { // "instance" is the instance for the MainActivity
console.log("Instance found");
instance.flag(1337); // Calling the function
},
onComplete: function() {}
});
});

0x6 实例字段

主要逻辑如下
MainActivity

1
2
3
4
5
6
7
8
9
10
11
package com.ad2001.frida0x6;
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
}

public void get_flag(Checker A) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
if (1234 == A.num1 && 4321 == A.num2) {

}
}
}

Checker

1
2
3
4
5
package com.ad2001.frida0x6;
public class Checker {
int num1;
int num2;
}

这次get_flag获取的参数是Checker类,而现在要修改的是类中定义的数
所以现在要做的是,实例化,修改值,调用get_flag()

看起来可以模仿0x3

1
2
3
4
5
6
7
Java.perform(function (){

var check = Java.use("com.ad2001.frida0x6.Checker"); // class reference
check.num1.value = 1234;
    check.num2.value = 4321;

})

报错如下

1
2
3
4
5
6
7
8
9
10
Error: Cannot access an instance field without an instance
at set (frida/node_modules/frida-java-bridge/lib/class-factory.js:1321)
at <anonymous> (D:\anquan_question\frida\test.js:3)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOps (frida/node_modules/frida-java-bridge/index.js:250)
at <anonymous> (frida/node_modules/frida-java-bridge/index.js:225)
at <anonymous> (frida/node_modules/frida-java-bridge/lib/vm.js:12)
at _performPendingVmOpsWhenReady (frida/node_modules/frida-java-bridge/index.js:244)
at perform (frida/node_modules/frida-java-bridge/index.js:204)
at <eval> (D:\anquan_question\frida\test.js:7)

依旧是实例和静态的问题
0x3是static int code = 0;
0x6是int num1;
不能在一个上直接访问或修改属于实例的字段。需要先创建一个 Checker 类的对象(实例),然后才能访问或修改那个特定对象的 num1 和 num2 字段。

实例化,修改值,调用get_flag()
现在需要实例化一个类

1
2
var Checker = Java.use("com.ad2001.frida0x6.Checker");
var check = Checker.$new();

然后修改它的值(参照0x3)

1
2
check.num1.value = 1234;
check.num2.value = 4321;

为了实例化调用get_flag,参照0x5使用框架

1
2
3
4
5
6
7
8
Java.performNow(function() {
Java.choose('<Package>.<class_Name>', {
onMatch: function(instance) {
// TODO
},
onComplete: function() {}
});
});

实际操作如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Java.performNow(function () {
    Java.choose('com.ad2001.frida0x6.MainActivity', {
        onMatch: function (instance) {
var Checker = Java.use("com.ad2001.frida0x6.Checker");
            var check = Checker.$new();
            check.num1.value = 1234;
            check.num2.value = 4321;

            console.log("get_flag called");
            instance.get_flag(check);
        },
        onComplete: function () { }
    });
});

Java.perform vs Java.performNow 的影响: 对于这个特定的场景,Java.choose 本身就是一个需要等待匹配的操作,其 onMatch 回调是异步触发的。无论外层是 perform 还是 performNow,Java.choose 都能被正确地启动。

0x7 构造函数(Constructor)

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.ad2001.frida0x7;
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
Checker ch = new Checker(123, 321);
try {
flag(ch);
}
}

public void flag(Checker A) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException {
if (A.num1 > 512 && 512 < A.num2) {

this.f103t1.setText(decrypted);
}
}
}

checker

1
2
3
4
5
6
7
8
9
10
package com.ad2001.frida0x7;
public class Checker {
int num1;
int num2;

Checker(int a, int b) {
this.num1 = a;
this.num2 = b;
}
}

模板

1
2
3
4
5
6
7
8
9
10
Java.perform(function() {
var <class_reference> = Java.use("<package_name>.<class>");
<class_reference>.$init.implementation = function(<args>){

/*

*/

}
});

$init: 在 Frida 中,$init 是一个特殊的名称,用来代表 Java 类的构造函数。Java 中的构造函数名称与类名相同,但 Frida 为了提供统一的 Hook 机制,使用了 $init 这个标识符。

实际操作

1
2
3
4
5
6
Java.perform(function() {
var a = Java.use("com.ad2001.frida0x7.Checker");
a.$init.implementation = function(param){
this.$init(600, 600);
}
});

注意时机
在 onCreate 方法中创建 Checker 类的实例,原始的 onCreate 方法会 立即 创建 Checker 对象 (new Checker(123, 321);)。在之后 尝试设置的 Checker 构造函数 Hook 根本不会被触发,因为 Checker 对象已经创建完毕了。
所以要尽可能地早
要hook到oncreate,那么就要使用更快的附加方式:Spawnfrida -U -f 包名 -l 脚本名称

1
frida -U -f com.ad2001.frida0x7 -l test.js

其他思路
不修改原始流程,额外调用。手动创建一个新的 Checker 对象,直接给符合要求的值,主动调用找到的 MainActivity 实例的 flag 方法,传入新创建的对象

1
2
3
4
5
6
7
8
9
10
11
Java.performNow(function () {
    Java.choose("com.ad2001.frida0x7.MainActivity", {
        onMatch: function (instance) {
            var Checker = Java.use("com.ad2001.frida0x7.Checker");
            var Checker_obj = Checker.$new(513, 513);
            instance.flag(Checker_obj);
        },
        onComplete: function () { }
    });
});

用这种方法普通地附加上去就可以了

1
frida -U "Frida 0x7" -l test.js

0x8 静态so

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.ad2001.frida0x8;
public class MainActivity extends AppCompatActivity {
public native int cmpstr(String str);

static {
System.loadLibrary("frida0x8");
}

protected void onCreate(Bundle savedInstanceState) {
public void onClick(View v) {
int res = MainActivity.this.cmpstr(ip);
if (res == 1) {

}
}
});
}
}

将程序解包得到so文件
java -jar apktool.jar d frida0x8.apk -o ./frida0x8
so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool __fastcall Java_com_ad2001_frida0x8_MainActivity_cmpstr(__int64 a1, __int64 a2, __int64 a3)
{
int v4; // [xsp+20h] [xbp-C0h]
int i; // [xsp+24h] [xbp-BCh]
char *s1; // [xsp+30h] [xbp-B0h]
char s2[100]; // [xsp+74h] [xbp-6Ch] BYREF
__int64 v10; // [xsp+D8h] [xbp-8h]

v10 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
s1 = (char *)_JNIEnv::GetStringUTFChars(a1, a3, 0LL);
for ( i = 0; i < __strlen_chk("GSJEB|OBUJWF`MBOE~", 0xFFFFFFFFFFFFFFFFLL); ++i )
s2[i] = aGsjebObujwfMbo[i] - 1;
s2[__strlen_chk("GSJEB|OBUJWF`MBOE~", 0xFFFFFFFFFFFFFFFFLL)] = 0;
v4 = strcmp(s1, s2);
__android_log_print(3, "input ", "%s", s1);
__android_log_print(3, "Password", "%s", s2);
_JNIEnv::ReleaseStringUTFChars(a1, a3, s1);
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return v4 == 0;
}

它在这里恒返回0,而在java中它需要等于1
现在我们想修改Java_com_ad2001_frida0x8_MainActivity_cmpstr()函数,那么首先我们要找到它在哪

这里是几种找到函数地址的方法
Module.enumerateExports()
此 API 枚举来自指定模块的所有导出 (符号) 。导出的函数由 Java 空间中的应用程序使用。它需要一个参数,该参数是要枚举其导出的模块 (共享库或可执行文件) 的名称
现在,让我们尝试获取 cmpstr 函数的地址。

1
2
3
4
5
6
7
8
[Pixel 4::Frida 0x8 ]-> Module.enumerateExports("libfrida0x8.so")[0]
{
"address": "0x782945e864",
"name": "Java_com_ad2001_frida0x8_MainActivity_cmpstr",
"type": "function"
}
[Pixel 4::Frida 0x8 ]-> Module.enumerateExports("libfrida0x8.so")[0]["address"]
"0x782945e864"

Module.getExportByName(modulename, exportName) 函数从模块 (shared library) 中检索具有给定名称的导出元件的地址

1
2
[Pixel 4::Frida 0x8 ]-> Module.getExportByName("libfrida0x8.so","Java_com_ad2001_frida0x8_MainActivity_cmpstr")
"0x782945e864"

Module.findExportByName()与 Module.getExportByName() 相同。唯一的区别是,如果未找到导出, 则 Module.getExportByName() 会引发异常,而 Module.findExportByName() 将返回 null

Module.getBaseAddress() 返回给定模块的基址。

1
2
[Pixel 4::Frida 0x8 ]-> Module.getBaseAddress("libfrida0x8.so")
"0x782945e000"

有了基址后就只需要找到函数的偏移量,这个偏移量可以在ida里直接看到,比如cmpstr的偏移就是0x864
相加就是0x782945e864

1
2
3
4
5
6
7
8
9
.text:0000000000000864
.text:0000000000000864 ; =============== S U B R O U T I N E =======================================
.text:0000000000000864
.text:0000000000000864 ; Attributes: bp-based frame
.text:0000000000000864
.text:0000000000000864 ; bool __fastcall Java_com_ad2001_frida0x8_MainActivity_cmpstr(__int64, __int64, __int64)
.text:0000000000000864 EXPORT Java_com_ad2001_frida0x8_MainActivity_cmpstr
.text:0000000000000864 Java_com_ad2001_frida0x8_MainActivity_cmpstr
.text:0000000000000864 ; DATA XREF: LOAD:0000000000000380↑o

Module.enumerateImports()与 Module.enumerateExports() 类似, 它将为我们提供所有模块的导入

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
[Pixel 4::Frida 0x8 ]-> Module.enumerateImports("libfrida0x8.so")
[
{
"address": "0x7914724d84",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "__cxa_finalize",
"slot": "0x782945fcd0",
"type": "function"
},
{
"address": "0x7914724c08",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "__cxa_atexit",
"slot": "0x782945fcd8",
"type": "function"
},
{
"address": "0x7914720b0c",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "__register_atfork",
"slot": "0x782945fce0",
"type": "function"
},
{
"address": "0x79146e8cc0",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "__strlen_chk",
"slot": "0x782945fcf0",
"type": "function"
},
{
"address": "0x79146bf740",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "strcmp",
"slot": "0x782945fcf8",
"type": "function"
},
{
"address": "0x791456f714",
"module": "/system/lib64/liblog.so",
"name": "__android_log_print",
"slot": "0x782945fd00",
"type": "function"
},
{
"address": "0x791470d4d4",
"module": "/apex/com.android.runtime/lib64/bionic/libc.so",
"name": "__stack_chk_fail",
"slot": "0x782945fd10",
"type": "function"
}
]
[Pixel 4::Frida 0x8 ]-> Module.enumerateImports("libfrida0x8.so")[4]['address']
"0x79146bf740"

获取函数地址的方法知道了,现在可以开始利用函数了
在这里提供一个模板

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(targetAddress, {
onEnter: function (args) {
console.log('Entering ' + functionName);
// Modify or log arguments if needed
},
onLeave: function (retval) {
console.log('Leaving ' + functionName);
// Modify or log return value if needed
}
});

刚开始我的尝试是直接将它的返回值改成1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Java.perform(function () {
var targetSo = "libfrida0x8.so";
    var funcName = "Java_com_ad2001_frida0x8_MainActivity_cmpstr";
    var fridaretAddr = Module.findExportByName(targetSo, funcName);   
console.log(fridaretAddr);
if (fridaretAddr != null) {
Interceptor.attach(fridaretAddr, {
onEnter: function (args) {
},
onLeave: function (retval) {
retval.replace(1);
console.log("retval", retval.toInt32());
}
})
}
});

但是什么也没有发生,它只是将我的返回值改成了我的输入值,我想那不是flag
这是它的日志

1
2
3
4
5
flame:/ # logcat | grep "11156"
04-17 09:29:09.618 11156 11189 I Adreno : PFP: 0x016ee185, ME: 0x00000000
04-17 09:31:57.953 11156 11156 I AssistStructure: Flattened final assist data: 1372 bytes, containing 1 windows, 8 views
04-17 09:32:10.740 11156 11156 D input : AAA
04-17 09:32:10.740 11156 11156 D Password: FRIDA{NATIVE_LAND}

其实在日志里就解密好了
s2就是目标字符串,它是加密字符串,但是在和输入进行比较时会进行解密,这里也在日志里打印了解密结果

1
2
3
4
5
6
for ( i = 0; i < __strlen_chk("GSJEB|OBUJWF`MBOE~", 0xFFFFFFFFFFFFFFFFLL); ++i )
s2[i] = aGsjebObujwfMbo[i] - 1;
s2[__strlen_chk("GSJEB|OBUJWF`MBOE~", 0xFFFFFFFFFFFFFFFFLL)] = 0;
v4 = strcmp(s1, s2);
__android_log_print(3, "input ", "%s", s1);
__android_log_print(3, "Password", "%s", s2);

到这里就解出题目了

换个思路
我们要使res=1,那么就要输入和它想要对比的字符一样的字符串,那么和我们进行对比的字符串是什么呢?我们能获取到strcmp函数的第二个参数,也就是和我们输入进行对比的字符吗?

先查找strcmp都在什么地方出现

1
2
3
4
5
6
7
8
9
10
var strcmp_adr = Module.findExportByName("libfrida0x8.so", "strcmp");
Interceptor.attach(strcmp_adr, {
onEnter: function (args) {
console.log("Hooking the strcmp function");
},

onLeave: function (retval) {
}

});

出来了很多结果
那么怎么定位我们的目标strcmp函数(和我们的输入相关联的strcmp)
可以肯定目标strcmp和其他strcmp不同的地方就在于传入的参数不同
那么现在来查找函数的参数

函数声明如下

1
int strcmp( const char *string1, const char *string2 );

使用 Memory.readUtf8String() API。它使用提供的地址从内存中读取 utf 字符串。args 是一个指针数组,其中包含 strcmp 函数的参数。因此,要访问第一个参数,我们可以使用 arg[0]。

1
2
3
4
5
6
7
8
9
10
11
12
var strcmp_adr = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp_adr, {
onEnter: function (args) {
//console.log("try find args");
var arg0 = Memory.readUtf8String(args[0]);
console.log(arg0);
},

onLeave: function (retval) {
}

});

这是其中一部分数据,看来调用strcmp的方法很多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[Pixel 4::Frida 0x8 ]-> Ljava/lang/String;
Lcom/android/internal/telephony/ProxyController;
Lcom/android/internal/view/IInputMethodClient$Stub;
Lcom/android/internal/telephony/PhoneSubInfoController;
Landroid/view/View;
Landroid/view/autofill/AutofillManager$AutofillClient;
Landroid/net/Uri$HierarchicalUri;
Landroid/os/UserHandle;
Ljava/lang/Object;
Lcom/android/internal/telephony/euicc/EuiccConnector$AvailableState;
Lcom/android/i18n/phonenumbers/Phonemetadata$NumberFormat;
Landroid/content/IContentProvider;
Landroid/os/Bundle;
Ljava/lang/String;
Ljava/lang/String;

第一个参数是我们输入的参数,那么现在进行过滤
现在输入hello,来确认目标地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var strcmp_adr = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp_adr, {
onEnter: function (args) {
var arg0 = Memory.readUtf8String(args[0]);
if (arg0.includes("hello")) {
console.log(arg0);
//console.log(strcmp_adr);
//获取指定strcmp地址
}
},

onLeave: function (retval) {
}

});

现在得到的数据就只有一条

1
2
[Pixel 4::Frida 0x8 ]-> hello

然后按照之前的方法获取第二个参数Memory.readUtf8String(args[1]);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var strcmp_adr = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp_adr, {
onEnter: function (args) {
var arg0 = Memory.readUtf8String(args[0]);
var arg1 = Memory.readUtf8String(args[1]);
if (arg0.includes("hello")) {
console.log(arg0);
//console.log(strcmp_adr);
console.log(arg1);
}
},

onLeave: function (retval) {
}

});

得到flag

1
2
[Pixel 4::Frida 0x8 ]-> hello
FRIDA{NATIVE_LAND}

0x9 修改静态so返回值

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.ad2001.a0x9;
public class MainActivity extends AppCompatActivity {
public native int check_flag();

static {
System.loadLibrary("a0x9");
}

protected void onCreate(Bundle savedInstanceState) {
stener(new View.OnClickListener() {
public void onClick(View v) {
if (MainActivity.this.check_flag() == 1337) {

}
});
}
}

so

1
2
3
4
__int64 Java_com_ad2001_a0x9_MainActivity_check_1flag()
{
return 1LL;
}

简单的修改返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
var flag_adr = Module.findExportByName("liba0x9.so", "Java_com_ad2001_a0x9_MainActivity_check_1flag");
Interceptor.attach(flag_adr, {
onEnter: function (args) {
console.log("catch the flag");

},
onLeave: function (retval) {
//retval = 1337;//错误的赋值
retval.replace(1337);
console.log("change the result");
}

});

模板

1
2
3
4
5
6
7
8
9
10
11
Interceptor.attach(targetAddress, {
onEnter: function (args) {
console.log('Entering ' + functionName);
// 修改参数
},
onLeave: function (retval) {
console.log('Leaving ' + functionName);
// 修改返回值
}
});

这里的函数名不是check_flag而是check_1flag,这和jni的命名规则有关,特殊转义,下划线 _** → _1

0xA 动态so

java

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.ad2001.frida0xa;

public final class MainActivity extends AppCompatActivity {
public final native String stringFromJNI();
protected void onCreate(Bundle savedInstanceState) {

activityMainBinding.sampleText.setText(stringFromJNI());
}

static {
System.loadLibrary("frida0xa");
}
}

so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall Java_com_ad2001_frida0xa_MainActivity_stringFromJNI(_JNIEnv *a1)
{
const char *v1; // x0
__int64 v3; // [xsp+18h] [xbp-48h]
_BYTE v5[24]; // [xsp+40h] [xbp-20h] BYREF
__int64 v6; // [xsp+58h] [xbp-8h]

v6 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
std::string::basic_string<decltype(nullptr)>();
v1 = (const char *)sub_1DD3C(v5);
v3 = _JNIEnv::NewStringUTF(a1, v1);
std::string::~string(v5);
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return v3;
}

前面这两个主要是把文本 “Hello Hackers”显示在我们的 TextView 中。
继续找
然后在so这里找到了get_flag()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__int64 __fastcall get_flag(__int64 result, int a2)
{
int i; // [xsp+Ch] [xbp-44h]
char v3[20]; // [xsp+34h] [xbp-1Ch] BYREF
__int64 v4; // [xsp+48h] [xbp-8h]

v4 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
if ( (_DWORD)result + a2 == 3 )
{
for ( i = 0; i < __strlen_chk("FPE>9q8A>BK-)20A-#Y", 0xFFFFFFFFFFFFFFFFLL); ++i )
v3[i] = aFpe9q8aBk20aY[i] + 2 * i;
v3[19] = 0;
result = __android_log_print(3, "FLAG", "Decrypted Flag: %s", v3);
}
_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
return result;
}

它会把最终的flag打印result = __android_log_print(3, "FLAG", "Decrypted Flag: %s", v3);在日志里

接下来分析get_flag(),两个参数相加等于3

1
2
get_flag(__int64 result, int a2)
if ( (_DWORD)result + a2 == 3 )

接下来就是怎么hook动态注册的函数,并且修改函数的参数值

get_flag()这个名字看起来就是动态注册的,静态注册的命名很特别:Java_{PackageName}_{ClassName}_{MethodName}

尝试导出函数,它也不在可导出列表

1
2
3
4
5
[Pixel 4::Frida 0xA ]-> Module.findExportByName("libfrida0xa.so", "Java_com_ad2001_frida0xa_MainActivity_stringF 
romJNI")
"0x72e0b0fbe0"
[Pixel 4::Frida 0xA ]-> Module.findExportByName("libfrida0xa.so", "get_flag")
null

使用0x8控制静态方法的模板不可行

这里给个新的模板

1
2
3
var native_adr = new NativePointer(<address_of_the_native_function>);
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);
native_function(<arguments>);
1
var native_adr = new NativePointer(<address_of_the_native_function>);

要在 frida 中调用native函数,我们需要一个 NativePointer 对象。我们应该将要调用的原生函数的地址传递给 NativePointer 构造函数。接下来,我们将创建 NativeFunction 对象 ,它表示我们想要调用的实际原生函数。它围绕原生函数创建了一个 JavaScript 包装器,允许我们从 frida 调用该原生函数

1
const native_function = new NativeFunction(native_adr, '<return type>', ['argument_data_type']);

第一个参数应该是 NativePointer 对象,第二个参数是原生函数的返回类型,第三个参数是要传递给原生函数的参数的数据类型列表。

1
native_function(<arguments>);

接着就可以调用定义好的方法了

那么如何查找动态注册的native的地址呢?
打开ida查看它的偏移,可以得到偏移为0x1DD60

1
2
3
4
5
6
7
8
9
10
11
12
.text:000000000001DD5C                 RET
.text:000000000001DD5C ; } // starts at 1DD3C
.text:000000000001DD5C ; End of function sub_1DD3C
.text:000000000001DD5C
.text:000000000001DD60
.text:000000000001DD60 ; =============== S U B R O U T I N E =======================================
.text:000000000001DD60
.text:000000000001DD60 ; Attributes: bp-based frame
.text:000000000001DD60
.text:000000000001DD60 ; __int64 __fastcall get_flag(__int64 result, int)
.text:000000000001DD60 EXPORT _Z8get_flagii
.text:000000000001DD60 _Z8get_flagii ; DATA XREF: LOAD:0000000000001648↑o

所在库就是基址
找到libfrida0xa.so的基址
var base = Module.findBaseAddress("libfrida0xa.so");
然后加上偏移
var flag_adr = base.add(0x1DD60);
就得到了目标地址

具体操作

1
2
3
4
5
6
7
8
9
var base = Module.findBaseAddress("libfrida0xa.so");
var flag_adr = base.add(0x1DD60);
var native_adr = new NativePointer(flag_adr); //创建NativePointer

//创建NativeFunction
const get_flag = new NativeFunction(native_adr, 'void', ['int', 'int']);

//调用
get_flag(1, 2);

在日志里看到flag

1
2
3
4
5
6
7
8
9
10
11
 :>frida-ps -Uai
PID Name Identifier
----- ------------------- ---------------------------------------
19645 frida 0xA com.ad2001.frida0xa



flame:/ # logcat | grep "19645"
04-17 10:12:22.425 19645 19692 D ProfileInstaller: Installing profile for com.ad2001.frida0xa
04-17 10:23:42.752 19645 19691 D FLAG : Decrypted Flag: FRIDA{DONT_CALL_ME}
04-17 10:23:43.537 19645 19691 D FLAG : Decrypted Flag: FRIDA{DONT_CALL_ME}

0xB 加花的so

主要逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.ad2001.frida0xb;
public final class MainActivity extends AppCompatActivity {
public final native void getFlag();

protected void onCreate(Bundle savedInstanceState) {
btn.setOnClickListener(new View.OnClickListener() {
public final void onClick(View view) {
MainActivity.onCreate$lambda$0(MainActivity.this, view);
}
});
}

public static final void onCreate$lambda$0(MainActivity this$0, View it) {
Intrinsics.checkNotNullParameter(this$0, "this$0");
this$0.getFlag();
}

static {
System.loadLibrary("frida0xb");
}
}

so

1
2
3
4
void Java_com_ad2001_frida0xb_MainActivity_getFlag()
{
;
}

这次的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
.text:0000000000015220
.text:0000000000015220 ; __unwind {
.text:0000000000015220 SUB SP, SP, #0x60
.text:0000000000015224 STP X29, X30, [SP,#0x50+var_s0]
.text:0000000000015228 ADD X29, SP, #0x50
.text:000000000001522C STUR X0, [X29,#var_18]
.text:0000000000015230 STUR X1, [X29,#var_20]
.text:0000000000015234 MOV W8, #0xDEADBEEF
.text:000000000001523C STUR W8, [X29,#var_24]
.text:0000000000015240 LDUR W8, [X29,#var_24]
.text:0000000000015244 SUBS W8, W8, #0x539
.text:0000000000015248 B.NE loc_1532C
.text:000000000001524C B loc_15250
.text:0000000000015250 ; ---------------------------------------------------------------------------
.text:0000000000015250
.text:0000000000015250 loc_15250 ; CODE XREF: Java_com_ad2001_frida0xb_MainActivity_getFlag+2C↑j
.text:0000000000015250 STR XZR, [SP,#0x50+var_30]
.text:0000000000015254 ADRL X8, aJEhmwbmxezisdm ; "j~ehmWbmxezisdmogi~Q"
.text:000000000001525C STR X8, [SP,#0x50+var_38]
.text:0000000000015260 LDR X8, [SP,#0x50+var_38]
.text:0000000000015264 STUR X8, [X29,#var_8]
.text:0000000000015268 MOV X8, #0xFFFFFFFFFFFFFFFF
.text:000000000001526C STUR X8, [X29,#var_10]
.text:0000000000015270 LDUR X0, [X29,#var_8] ; char *
.text:0000000000015274 LDUR X1, [X29,#var_10] ; size_t
.text:0000000000015278 BL .__strlen_chk
.text:000000000001527C STR X0, [SP,#0x50+var_40]
.text:0000000000015280 LDR X8, [SP,#0x50+var_40]
.text:0000000000015284 ADD X0, X8, #1 ; unsigned __int64
.text:0000000000015288 BL ._Znam ; operator new[](ulong)
.text:000000000001528C STR X0, [SP,#0x50+var_48]
.text:0000000000015290 STR XZR, [SP,#0x50+var_30]
.text:0000000000015294 B loc_15298

这是一个关键跳转

1
2
.text:0000000000015248                 B.NE            loc_1532C
.text:000000000001524C B loc_15250

loc_1532c会直接跳到末尾结束函数

1
2
3
4
5
6
7
8
9
10
.text:000000000001532C ; ---------------------------------------------------------------------------
.text:000000000001532C
.text:000000000001532C loc_1532C ; CODE XREF: Java_com_ad2001_frida0xb_MainActivity_getFlag+28↑j
.text:000000000001532C ; Java_com_ad2001_frida0xb_MainActivity_getFlag:loc_15328↑j
.text:000000000001532C LDP X29, X30, [SP,#0x50+var_s0]
.text:0000000000015330 ADD SP, SP, #0x60 ; '`'
.text:0000000000015334 RET
.text:0000000000015334 ; } // starts at 15220
.text:0000000000015334 ; End of function Java_com_ad2001_frida0xb_MainActivity_getFlag
.text:0000000000015334

而loc_15250会执行一段解密逻辑并正常走完函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000015250 loc_15250                               ; CODE XREF: Java_com_ad2001_frida0xb_MainActivity_getFlag+2C↑j
.text:0000000000015250 STR XZR, [SP,#0x50+var_30]
.text:0000000000015254 ADRL X8, aJEhmwbmxezisdm ; "j~ehmWbmxezisdmogi~Q"
.text:000000000001525C STR X8, [SP,#0x50+var_38]
.text:0000000000015260 LDR X8, [SP,#0x50+var_38]
.text:0000000000015264 STUR X8, [X29,#var_8]
.text:0000000000015268 MOV X8, #0xFFFFFFFFFFFFFFFF
.text:000000000001526C STUR X8, [X29,#var_10]
.text:0000000000015270 LDUR X0, [X29,#var_8] ; char *
.text:0000000000015274 LDUR X1, [X29,#var_10] ; size_t
.text:0000000000015278 BL .__strlen_chk
.text:000000000001527C STR X0, [SP,#0x50+var_40]
.text:0000000000015280 LDR X8, [SP,#0x50+var_40]
.text:0000000000015284 ADD X0, X8, #1 ; unsigned __int64
.text:0000000000015288 BL ._Znam ; operator new[](ulong)
.text:000000000001528C STR X0, [SP,#0x50+var_48]
.text:0000000000015290 STR XZR, [SP,#0x50+var_30]
.text:0000000000015294 B loc_15298
.text:0000000000015298 ; ---------------------------------------------------------------------------

那么现在的解题思路就是:
1.nop掉.text:0000000000015248 B.NE loc_1532C
2.将它替换成.text:000000000001524C B loc_15250

B.NE代表Branch if Not Equal,也就是当两个操作数不相等时才会跳转
B代表跳转

修改内存:https://learnfrida.info/advanced_usage/#patching-memory
patchcode
Memory.patchCode API 允许我们修改 X 地址处的 N 个字节,该地址以 NativePointer 的形式给出,此指针必须是可写的,以便我们对其进行修改。在某些系统(如 iOS)中,地址指针在映射到内存之前会写入临时位置。因此,代码编写者的第一个参数应该是 code 参数,而不是 openPtr)。

1
2
3
4
5
6
7
Memory.patchCode(openPtr, Process.pageSize, function (code) {
const cw = new X86Writer(code, { pc: openPtr });
cw.putNopPadding(Process.pageSize);
cw.putRet();
cw.flush();
}
);

这里是X86Writer,不同的架构要使用不同的方法,我的手机是Arm架构,在这里我用的是Arm64Writer
可以查看api:https://frida.re/docs/javascript-api/#x86writer

实际操作如下
按照之前的方法找到loc_1532C地址,然后进行patch

1
2
3
4
5
6
7
8
9
var libbase = Module.findBaseAddress("libfrida0xb.so");
var jmp = libbase.add(0x15248);

Memory.patchCode(jmp, 4, function (code) {
const cw = new Arm64Writer(code, { pc: jmp });
cw.putNop(); //进行nop替换
cw.flush();
}
);

另一个模板

1
2
3
4
5
6
7
8
9
var writer = new X86Writer(<address_of_the_instruction>);
try {
// Insert instructions
// Flush the changes to memory
writer.flush();
} finally {
// Dispose of the X86Writer to free up resources
writer.dispose();
}

关于flush():
调用 flush 方法将更改应用于内存。这可确保将修改后的指令写入内存位置。

finally 块用于确保 X86Writer 资源得到正确清理。调用 dispose 方法释放 X86Writer 实例关联的资源。
此模板是x86,因此如果我要用在我自己的手机上我需要对其进行更改

现在来看方法二,替换指令

  • putBImm(target): put a B instruction
    2.将它替换成.text:000000000001524C B loc_15250
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var libbase = Module.findBaseAddress("libfrida0xb.so");
    var jmp = libbase.add(0x15248);
    var target = libbase.add(0x1524c);
    Memory.patchCode(jmp, 4, function (code) {
    const cw = new Arm64Writer(code, { pc: jmp });
    //cw.putNop();
    cw.putBImm(target);
    cw.flush();
    }
    );

另一种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var adr = Module.findBaseAddress("libfrida0xb.so").add(0x15248);  // Addres of the b.ne instruction
Memory.protect(adr, 0x1000, "rwx");
var writer = new Arm64Writer(adr); // ARM64 writer object
var target = Module.findBaseAddress("libfrida0xb.so").add(0x1524c); // Address of the next instruction b LAB_00115250

try {

writer.putBImm(target); // Inserts the <b target> instruction in the place of b.ne instruction
writer.flush();

console.log(`Branch instruction inserted at ${adr}`);
} finally {

writer.dispose();

}

修改内存权限

“此指针必须是可写的”
所以如果该页权限受限就可能无法正常patch,这个时候就需要进行权限修改
Memory.protect 函数的语法为:

1
Memory.protect(address, size, protection);

address:用于更改保护的内存区域的起始地址。
size:内存区域的大小(以字节为单位)。
protection:内存区域的保护属性。

通常需要指定一个页的大小

1
Memory.protect(jnz, 0x1000, "rwx");

这句单独写在外面,这个是x86的利用脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var jnz = Module.getBaseAddress("libfrida0xb.so").add(0x20e2a - 0x00010000);
Memory.protect(jnz, 0x1000, "rwx");
var writer = new X86Writer(jnz);

try {

writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()
writer.putNop()

writer.flush();

} finally {

writer.dispose();
}

字节对齐

ARM64的nop是4字节,x86的nop是一字节

  • ARM64 (A64 指令集):

    • 架构类型: RISC (精简指令集计算) 风格。虽然现代 ARM 很复杂,但 A64 保留了 RISC 的一些关键特性。
    • 指令长度: **固定长度 (Fixed-Length)**。所有 A64 指令都是 4 字节 (32位) 长。
    • 对齐要求: 指令必须存储在 4 字节对齐的地址上。CPU 总是从 4 字节对齐的地址开始,一次读取 4 个字节作为一条指令进行解码。
  • x86 / x86-64:

    • 架构类型: CISC (复杂指令集计算) 风格。
    • 指令长度: **可变长度 (Variable-Length)**。x86 指令的长度可以从 1 字节到 15 字节不等。指令的第一个字节(或几个字节)通常包含操作码 (Opcode),后续字节可能包含操作数、寻址模式信息或前缀等。
    • 对齐要求: 指令可以从任何字节地址开始。虽然处理器为了性能也喜欢对齐的指令(特别是跳转目标),但硬件设计必须能够处理从任意字节边界开始的指令。

参考资料

https://github.com/DERE-ad2001/Frida-Labs/
https://learnfrida.info/advanced_usage/
https://codeshare.frida.re/

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
总字数 55.5k 访客数