资讯详情

Frida Android hook

From:

1、r0ysue 大佬

2、Frida 环境

2.1 pyenv

2.2 frida 安装

2.3 安装 objection

2.4 frida 使用

frida 帮助 和 frida-server 帮助

2.5 frida 开发环境建设

3、FRIDA 基础

3.1 frida 查看当前存在的过程

3.2 frida 打印参数,修改返回值

3.3 frida 寻找 instance,主动调用。

3.4 frida rpc

3.5 frida 动态修改

3.6 API List

4、Frida 动静结合分析

4.1 Objection

objection ----->启动并注入内存

objection ----->memory 命令

memory list modules ( 查看加载的 so 库)

memory list exports ( 查看 so 库的导出函数表)

memory dump(dump 内存空间)

memory search(搜索 内存空间)

objection ----->android 命令

android heap execute 实例ID 实例方法

android hooking list activities/services

android intent launch_activity/launch_service activity/服务

android hooking list classes

android hooking list class_methods 类名

hook 类的方法(hook 类中的所有方法 / 具体方法)

grep trick 和 文件保存( objection log )

4.2 案例学习

案例学习 1:《仿VX数据库原型取证逆向分析

案例学习 2:主动调用爆破密码

5、Frida hook 基础(一)

5.1 Frida hook : 打印参数,返回值 / 设置返回值 / 主动调用

5.2 Frida hook : 静态/非静态函数主动调用 以及 设置静态/非静态成员变量值

5.3 Frida hook : 内部类、枚举类的方法并 hook,trace原型1

5.4 Frida hook : hook 动态加载的 dex,与查找 interface,

5.5 Frida hook : 枚举 class,trace原型2

5.6 Frida hook : 搜索 interface 具体实现类

枚举 实现 接口的类

6、Frida hook 基础(二)

6.1 spawn / attach

6.2 Frida hook : hook构造函数/打印栈回溯

6.3 Frida hook : 打印栈回溯

6.4 Frida hook : 手动加载 dex 并调用

7、Frida 打印 与 参数构造

gson 打印 Java 对象的内容

char[] / [Object Object]

byte[] 类型 转 String

修改传递的参数 ( 示例: java array 构造 )

类的多态:强制类型转换 Java.cast

interface / Java.registerClass

成员内部类 / 匿名内部类

hook enum

HashMap 和 Map 类型 转 String

获得 context

打印 non-ascii

8、Frida native hook : NDK 开发入门

9、Frida native hook : JNIEnv 和 反射

基本的JNIEnv用法-toc">9.1 以 jni字符串 来掌握基本的 JNIEnv用法

9.2 Java 反射

10、Frida 反调试 与 反反调试

11、Frida native hook : 符号 hook JNI、art&libc

11.1 Native函数的Java Hook及主动调用

11.2 jni.h 头文件导入

11.3 JNI 函数符号 hook

11.4 JNI 函数参数、返回值打印和替换

12、Frida native hook : JNI_Onload / 动态注册 / inline_hook / native层调用栈打印

12.1 JNI_Onload / 动态注册原理

12.2 Frida hook RegisterNative

12.3 native 层调用栈打印

12.4 主动调用去进行方法参数替换

12.5 inline hook ( so库里面的函数 )

13、Frida native hook : Frida hook native app 实战

14、Frida trace 四件套

14.1 jni trace : trace jni

14.2 strace : trace syscall

14.3 frida-trace : trace libc(or more)

art trace

14.4 hook_artmethod : trace java 函数调用

14.5 修改AOSP源码打印

15、Frida native hook : init_array 开发和自动化逆向

15.1 init_array原理 ( so 加载、启动、执行 )

15.2 IDA静态分析 init_array

15.3 IDA 动态调试 so

15.4 init_array && JNI_Onload "自吐"

JNI_Onload

init_array

15.5 native层未导出函数主动调用(任意符号和地址)

16、C/C++ hook

16.1 Native/JNI层参数打印和主动调用参数构造

16.2 C/C++编成 so 并引入 Frida 调用其中的函数


1、r0ysue 大佬

这篇文章完全来源于 r0ysue 的知识星球,推荐下大佬的星球

2、Frida 环境

github 地址:https://github.com/frida/frida

2.1 pyenv

python 全版本随机切换,这里提供 macOS上的配置方法

brew update
brew install pyenv
echo -e 'if command -v pyenv 1>/dev/null 2>&1; then\n  eval "$(pyenv init -)"\nfi' >> ~/.bash_profile


下载一个3.8.2,下载真的很慢,要慢慢等
pyenv install 3.8.2

pyenv versions
sakura@sakuradeMacBook-Pro:~$ pyenv versions
  system
* 3.8.2 (set by /Users/sakura/.python-version)
切换到我们装的
pyenv local 3.8.2
python -V
pip -V
原本系统自带的
python local system
python -V

另外当你需要临时禁用 pyenv 的时候

把这个注释了然后另开终端就好了。关于卸载某个 python 版本

Uninstalling Python Versions
As time goes on, you will accumulate Python versions in your $(pyenv root)/versions directory.

To remove old Python versions, pyenv uninstall command to automate the removal process.

Alternatively, simply rm -rf the directory of the version you want to remove. 
You can find the directory of a particular Python version with the pyenv 
prefix command, e.g. pyenv prefix 2.6.8.

2.2 frida 安装

如果直接按下述安装则会直接安装 frida 和 frida-tools 的最新版本。

pip install frida
pip install frida-tools
frida --version
frida-ps --version

也可以通过 安装旧版本的 frida,例如 12.8.0

pyenv install 3.7.7
pyenv local 3.7.7
pip install frida==12.8.0
pip install frida-tools==5.3.0

老版本 frida 和 对应关系,对应关系很好找:

2.3 安装 objection

安装命令:

pyenv local 3.8.2
pip install objection
objection -h


pyenv local 3.7.7
pip install objection==1.8.4
objection -h

2.4 frida 使用

下载 frida-server 并解压,在这里下载 frida-server :https://github.com/frida/frida/releases

先 adb shell,然后切换到 root 权限,把之前 push 进来的 frida server 改个名字叫 fs,然后运行 frida

adb push /tmp/frida-server-12.8.0-android-arm64 /data/local/tmp
mv frida-server-12.8.0-android-arm64 fs
chmod 777 fs
./fs

如果要监听端口,就

./fs -l 0.0.0.0:8888

frida 帮助 和 frida-server 帮助

frida --help

frida-server --help

2.5 frida 开发环境搭建

  1. 安装         git clone https://github.com/oleavr/frida-agent-example.git         cd frida-agent-example/         npm install
  2. 使用 vscode 打开此工程,在 agent 文件夹下编写 js,会有智能提示。
  3. npm run watch 会监控代码修改自动编译生成 js 文件
  4. python 脚本或者 cli 加载 _agent.js 命令:

下面是测试脚本

s1.js

function main() {
    Java.perform(function x() {
        console.log("sakura")
    })
}
setImmediate(main)

loader.py

import time
import frida

device8 = frida.get_device_manager().add_remote_device("192.168.0.9:8888")
pid = device8.spawn("com.android.settings")
device8.resume(pid)
time.sleep(1)
session = device8.attach(pid)
with open("si.js") as f:
    script = session.create_script(f.read())
script.load()
input() #等待输入

解释一下:这个脚本就是先通过 来找到 device,然后以 spawn 方式启动 settings,然后 attach 到上面,并执行 frida 脚本。

当代码里面没有指定端口时,需要手动转发端口:

import sys
import time
import frida

js_code = '''
function main() {
    Java.perform(function x() {
        console.log("sakura")
    })
}
setImmediate(main);
'''


def on_message(msg, data):
    if msg['type'] == 'send':
        print(f'[*] {msg["payload"]}')
    else:
        print(msg)


if __name__ == '__main__':

    select = 1

    if 1 == select:
        # ########################### 会自动重启 app ###########################
        # 会自动重启 app
        device = frida.get_remote_device()
        pid = device.spawn(["com.android.settings"])
        device.resume(pid)
        time.sleep(1)
        process = device.attach("com.android.settings")
        script = process.create_script(js_code)
        script.load()
        input('按任意键继续')  # 等待输入
    elif 2 == select:
        # ############ 需要先手动启动 app , 然后才能执行脚本进行 hook #############
        # get_remote_device 获取远程设备 (get_usb_device)  attach 附加进程
        process = frida.get_remote_device().attach('com.android.settings')
        script = process.create_script(js_code)
        script.on('message', on_message)  # 绑定 js 回调
        script.load()
        sys.stdin.read()
    pass

运行结果:

3、FRIDA 基础

3.1 frida 查看当前存在的进程

frida-ps 命令:

frida-ps -U 查看通过 usb 连接的 android 手机上的进程。可以通过 grep 过滤就可以找到我们想要的包名。

sakura@sakuradeMacBook-Pro:~$ frida-ps -U
  PID  Name
-----  ---------------------------------------------------
 3640  ATFWD-daemon
  707  adbd
  728  adsprpcd
26041  android.hardware.audio@2.0-service
  741  android.hardware.biometrics.fingerprint@

3.2 frida 打印参数和修改返回值

package myapplication.example.com.frida_demo;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    private String total = "@@@###@@@";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        while (true){

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            fun(50,30);
            Log.d("sakura.string" , fun("LoWeRcAsE Me!!!!!!!!!"));
        }
    }

    void fun(int x , int y ){
        Log.d("sakura.Sum" , String.valueOf(x+y));
    }

    String fun(String x){
        total +=x;
        return x.toLowerCase();
    }

    String secret(){
        return total;
    }
}

注入的 js 代码:

function main() {
    console.log("Enter the Script!");
    Java.perform(function x() {
        console.log("Inside Java perform");
        var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");
        // 重载找到指定的函数
        MainActivity.fun.overload('java.lang.String').implementation = function (str) {
            //打印参数
            console.log("original call : str:" + str);
            //修改结果
            var ret_value = "sakura";
            return ret_value;
        };
    })
}
setImmediate(main);

查看设备。()

sakura@sakura:~$ frida-ps -U | grep frida
8738  frida-helper-32
8897  myapplication.example.com.frida_demo
 
// -f 是通过 spawn,也就是重启 apk 注入 js
sakura@sakura:~$ frida -U -f myapplication.example.com.frida_demo -l frida_demo.js
...
original call : str:LoWeRcAsE Me!!!!!!!!!
12-21 04:46:49.875 9594-9594/myapplication.example.com.frida_demo D/sakura.string: sakura

3.3 frida 寻找 instance,主动调用。

function main() {
    console.log("Enter the Script!");
    Java.perform(function x() {
        console.log("Inside Java perform");
        var MainActivity = Java.use("myapplication.example.com.frida_demo.MainActivity");

        // overload 选择被重载的对象
        MainActivity.fun.overload('java.lang.String').implementation = function (str) {
            //打印参数
            console.log("original call : str:" + str);
            //修改结果
            var ret_value = "sakura";
            return ret_value;
        };

        // 寻找类型为 classname 的实例
        Java.choose("myapplication.example.com.frida_demo.MainActivity", {
            onMatch: function (x) {
                console.log("find instance :" + x);
                console.log("result of secret func:" + x.secret());
            },
            onComplete: function () {
                console.log("end");
            }
        });
    });
}
setImmediate(main);

3.4 frida rpc

function callFun() {
    Java.perform(function fn() {
        console.log("begin");
        Java.choose("myapplication.example.com.frida_demo.MainActivity", {
            onMatch: function (x) {
                console.log("find instance :" + x);
                console.log("result of fun(string) func:" + x.fun(Java.use("java.lang.String").$new("sakura")));
            },
            onComplete: function () {
                console.log("end");
            }
        })
    })
}
rpc.exports = {
    callfun: callFun
};

Python 调用:

import time
import frida

device = frida.get_usb_device()
pid = device.spawn(["myapplication.example.com.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo_rpc_call.js") as f:
    script = session.create_script(f.read())

def my_message_handler(message, payload):
    print(message)
    print(payload)

script.on("message", my_message_handler)
script.load()

script.exports.callfun()

执行:

sakura@sakura:~/frida-agent-example/agent$ python frida_demo_rpc_loader.py 
begin
find instance :myapplication.example.com.frida_demo.MainActivity@1d4b09d
result of fun(string):sakura
end

3.5 frida 动态修改

即将手机上的 app 的内容发送到 PC 上的 frida python 程序,然后处理后返回给 app,然后 app 再做后续的流程,核心是理解 send/recv 函数。

<TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="please input username and password"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <EditText
        android:id="@+id/editText"
        android:layout_width="fill_parent"
        android:layout_height="40dp"
        android:hint="username"
        android:maxLength="20"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.095" />

    <EditText
        android:id="@+id/editText2"
        android:layout_width="fill_parent"
        android:layout_height="40dp"
        android:hint="password"
        android:maxLength="20"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.239" />

    <Button
        android:id="@+id/button"
        android:layout_width="100dp"
        android:layout_height="35dp"
        android:layout_gravity="right|center_horizontal"
        android:text="提交"
        android:visibility="visible"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.745" />
public class MainActivity extends AppCompatActivity {

    EditText username_et;
    EditText password_et;
    TextView message_tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        password_et = (EditText) this.findViewById(R.id.editText2);
        username_et = (EditText) this.findViewById(R.id.editText);
        message_tv = ((TextView) findViewById(R.id.textView));

        this.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                if (username_et.getText().toString().compareTo("admin") == 0) {
                    message_tv.setText("You cannot login as admin");
                    return;
                }
                //hook target
                message_tv.setText(
                    "Sending to the server :" + 
                    Base64.encodeToString(
					    (username_et.getText().toString() + ":" + password_et.getText().toString()).getBytes(), 
						Base64.DEFAULT
                    )
                );
            }
        });
    }
}

先分析问题,我的最终目标是让 message_tv.setText 可以”发送”username为admin的base64字符串。 那肯定是 hook TextView.setText 这个函数。

console.log("Script loaded successfully ");
Java.perform(function () {
    var tv_class = Java.use("android.widget.TextView");
    tv_class.setText.overload("java.lang.CharSequence").implementation = function (x) {
        var string_to_send = x.toString();
        var string_to_recv;
        send(string_to_send); // send data to python code
        recv(function (received_json_object) {
            string_to_recv = received_json_object.my_data
            console.log("string_to_recv: " + string_to_recv);
        }).wait(); //block execution till the message is received
        var my_string = Java.use("java.lang.String").$new(string_to_recv);
        this.setText(my_string);
    }
});

Python 脚本:

import time
import frida
import base64

def my_message_handler(message, payload):
    print(message)
    print(payload)
    if message["type"] == "send":
        print(message["payload"])
        data = message["payload"].split(":")[1].strip()
        print( 'message:', message)
        #data = data.decode("base64")
        #data = data
        data = str(base64.b64decode(data))
        print( 'data:',data)
        user, pw = data.split(":")
        print( 'pw:',pw)
        #data = ("admin" + ":" + pw).encode("base64")
        data = str(base64.b64encode(("admin" + ":" + pw).encode()))
        print( "encoded data:", data)
        script.post({"my_data": data})  # send JSON object
        print( "Modified data sent")

device = frida.get_usb_device()
pid = device.spawn(["myapplication.example.com.frida_demo"])
device.resume(pid)
time.sleep(1)
session = device.attach(pid)
with open("frida_demo2.js") as f:
    script = session.create_script(f.read())
script.on("message", my_message_handler)
script.load()
input()

执行和输出:

sakura@sakuradeMacBook-Pro:~/gitsource/frida-agent-example/agent$ python frida_demo_rpc_loader2.py 
Script loaded successfully 
{'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'}
None
Sending to the server :c2FrdXJhOjEyMzQ1Ng==

message: {'type': 'send', 'payload': 'Sending to the server :c2FrdXJhOjEyMzQ1Ng==\n'}
data: b'sakura:123456'
pw: 123456'
encoded data: b'YWRtaW46MTIzNDU2Jw=='
Modified data sent
string_to_recv: b'YWRtaW46MTIzNDU2Jw=='

参考链接:https://github.com/Mind0xP/Frida-Python-Binding

3.6 API List

  • Java.choose(className: string, callbacks: Java.ChooseCallbacks): void 通过扫描 Java VM 的堆来枚举 className类 的 live instance。

  • Java.use(className: string): Java.Wrapper<{}> 动态为 className 生成 JavaScript Wrappe r,可以通过调用$new()来调用构造函数来实例化对象。 在实例上调用 $dispose() 以对其进行显式清理,或者等待JavaScript对象被gc。

  • Java.perform(fn: () => void): void 附加到 VM 后要运行的 函数。确保当前线程连接到 VM 并调用 fn。 如果应用程序的 "  " 还不可用,将延迟调用 fn。 如果不需要访问应用程序的类,请使用 Java.performNow() 

  • send(message: any, data?: ArrayBuffer | number[]): void 将 JSON 序列化后的 message 发送到您的基于Frida 的应用程序,并包含(可选)一些原始二进制数据。The latter is useful if you e.g. dumped some memory using NativePointer#readByteArray().

  • recv(callback: MessageCallback): MessageRecvOperation Requests callback to be called on the next message received from your Frida-based application. This will only give you one message, so you need to call recv() again to receive the next one.

  • wait(): void 堵塞,直到 message 已经 receive 并且 callback 已经执行完毕并返回

4、Frida 动静态结合分析

4.1 Objection

  • 参考这篇文章:
  • objection:https://pypi.org/project/objection/

命令:objection -d -g package_name explore

sakura@sakura:~$ objection -d -g com.android.settings explore
[debug] Agent path is: /Users/sakura/.pyenv/versions/3.7.7/lib/python3.7/site-packages/objection/agent.js
[debug] Injecting agent...
Using USB device `Google Pixel`
[debug] Attempting to attach to process: `com.android.settings`
[debug] Process attached!
Agent injected and responds ok!

     _   _         _   _
 ___| |_|_|___ ___| |_|_|___ ___
| . | . | | -_|  _|  _| | . |   |
|___|___| |___|___|_| |_|___|_|_|
      |___|(object)inject(ion) v1.8.4

     Runtime Mobile Exploration
        by: @leonjza from @sensepost

[tab] for command suggestions
com.android.settings on (google: 8.1.0) [usb] #

memory list modules ( 查看加载的 so 库 )

查看内存中加载的 module,命令:memory list modules

com.android.settings on (google: 8.1.0) [usb] # memory list modules
Save the output by adding `--json modules.json` to this command
Name                     Base          Size                  Path
-----------------------  ------------  --------------------  ------------------------------------
app_process64            0x64ce143000  32768 (32.0 KiB)      /system/bin/app_process64
libandroid_runtime.so    0x7a90bc3000  1990656 (1.9 MiB)     /system/lib64/libandroid_runtime.so
libbinder.so             0x7a9379f000  557056 (544.0 KiB)    /system/lib64/libbinder.so

memory list exports ( 查看 so 库的导出函数表 )

查看库的导出函数:memory list exports libssl.so

com.android.settings on (google: 8.1.0) [usb] # memory list exports libssl.so
Save the output by adding `--json exports.json` to this command
Type      Name                                                   Address
--------  -----------------------------------------------------  ------------
function  SSL_use_certificate_ASN1                               0x7c8ff006f8
function  SSL_CTX_set_dos_protection_cb                          0x7c8ff077b8
function  SSL_SESSION_set_ex_data                                0x7c8ff098f4
function  SSL_CTX_set_session_psk_dhe_timeout                    0x7c8ff0a754
function  SSL_CTX_sess_accept                                    0x7c8ff063b8
function  SSL_select_next_proto                                  0x7c8ff06a74

memory dump()

memory dump all 文件名
memory dump from_base 起始地址 字节数 文件

memory search()

用法:memory search "<pattern eg: 41 41 41 ?? 41>" (--string) (--offsets-only)

android heap search instances 类名

在内存堆上搜索类的实例 ,命令:

sakura@sakura:~$ objection -g myapplication.example.com.frida_demo explore
Using USB device `Google Pixel`
Agent injected and responds ok!

[usb] # android heap search instances myapplication.example.com.frida_demo
.MainActivity
Class instance enumeration complete for myapplication.example.com.frida_demo.MainActivity
Handle    Class                                              toString()
--------  -------------------------------------------------  ---------------------------------------------------------
0x2102    myapplication.example.com.frida_demo.MainActivity  myapplication.example.com.frida_demo.MainActivity@5b1b0af

android heap execute 实例ID 实例方法

调用实例的方法,
命令:android heap execute 实例ID 实例方法

android hooking list activities/services

查看当前可用的 activity 或者 service
命令:android hooking list activities/services

com.android.settings on (google: 8.1.0) [usb] # android hooking list services
com.android.settings.SettingsDumpService
com.android.settings.TetherService
com.android.settings.bluetooth.BluetoothPairingService

android intent launch_activity/launch_service activity/服务

直接启动 activity 或者服务 
命令:android intent launch_activity/launch_service activity/服务

示例:这个命令比较有趣的是用在如果有些设计的不好,可能就直接绕过了密码屏等直接进去。
android intent launch_activity com.android.settings.DisplaySettings

android hooking list classes

列出内存中所有的类 :android hooking list classes

android hooking list class_methods 类名

内存中搜索指定类的所有方法 
命令:android hooking list class_methods 类名

com.android.settings on (google: 8.1.0) [usb] # android hooking list class_methods java.nio.charset.Charset
private static java.nio.charset.Charset java.nio.charset.Charset.lookup(java.lang.String)
private static java.nio.charset.Charset java.nio.charset.Charset.lookup2(java.lang.String)
private static java.nio.charset.Charset java.nio.charset.Charset.lookupViaProviders(java.lang.String)

android hooking search classes 关键字

在内存中所有已加载的类中搜索     示例:android hooking search classes     // 搜索 类 中包含

android hooking search methods 关键字

在内存中所有已加载的类的方法中搜索包含特定关键词的方法

命令:android hooking search methods display    // 搜索方法中包含 display 关键字的 方法

com.android.settings on (google: 8.1.0) [usb] # android hooking search methods display
Warning, searching all classes may take some time and in some cases, crash the target application.
Continue? [y/N]: y
Found 5529 classes, searching methods (this may take some time)...
android.app.ActionBar.getDisplayOptions
android.app.ActionBar.setDefaultDisplayHomeAsUpEnabled
android.app.ActionBar.setDisplayHomeAsUpEnabled

hook 类的方法(hook 类里的所有方法 / 具体某个方法)

  •     这样就可以 hook 这个类里面的所有方法,每次调用都会被 log 出来。
  •     在上面的基础上,额外 dump 参数,栈回溯,返回值     示例:android hooking watch class xxx.MainActivity --dump-args --dump-backtrace --dump-return
  • android hooking watch class_method 方法名         // 可以直接 hook 到所有重载         android hooking watch class_method xxx.MainActivity.fun --dump-args --dump-backtrace --dump-return

hook 单个方法

  • android hooking watch class_method xxx.MainActivity.fun "参数1, 参数2" --dump-args --dump-backtrace --dump-return

objection log 默认是不能用 grep 过滤的,但是可以通过 objection run xxx | grep yyy 的方式,从终端通过管道来过滤。用法如下

sakura@sakura:~$ objection -g com.android.settings run memory list modules | grep libc
Warning: Output is not to a terminal (fd=1).
libcutils.so         0x7a94a1c000  81920 (80.0 KiB)      /system/lib64/libcutils.so
libc++.so            0x7a9114e000  983040 (960.0 KiB)    /system/lib64/libc++.so
libc.so              0x7a9249d000  892928 (872.0 KiB)    /system/lib64/libc.so
libcrypto.so         0x7a92283000  1155072 (1.1 MiB)     /system/lib64/libcrypto.so

有的命令后面可以通过 --json logfile 来直接保存结果到文件里。

有的可以通过查看 .objection (位置:C:\Users\用户名\.objection )文件里的输出 log 来查看结果。

sakura@sakurade:~/.objection$ cat *log | grep -i display
android.hardware.display.DisplayManager
android.hardware.display.DisplayManager$DisplayListener
android.hardware.display.DisplayManagerGlobal

4.2 案例学习

案例学习 1:《仿VX数据库原型取证逆向分析》

案例学习case1:《仿VX数据库原型取证逆向分析》

附件链接​:https://www.52pojie.cn/forum.php?mod=viewthread&tid=1082706

android-backup-extractor工具链接:https://github.com/nelenkov/android-backup-extractor

sakura@sakurade:~/Desktop/frida_learn$ java -version
java version "1.8.0_141"

sakura@sakuradeMacBook-Pro:~/Desktop/frida_learn$ java -jar abe-all.jar unpack 1.ab 1.tar
0% 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% 55% 56% 57% 58% 59% 60% 61% 62% 63% 64% 65% 66% 67% 68% 69% 70% 71% 72% 73% 74% 75% 76% 77% 78% 79% 80% 81% 82% 83% 84% 85% 86% 87% 88% 89% 90% 91% 92% 93% 94% 95% 96% 97% 98% 99% 100%
9097216 bytes written to 1.tar.

...
sakura@sakurade:~/Desktop/frida_learn/apps/com.example.yaphetshan.tencentwelcome$ ls
Encryto.db _manifest  a          db

装个夜神模拟器玩

sakura@sakura:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb connect 127.0.0.1:62001
* daemon not running. starting it now on port 5037 *
adb E  5139 141210 usb_osx.cpp:138] Unable to create an interface plug-in (e00002be)
* daemon started successfully *
connected to 127.0.0.1:62001
sakura@sakuradeMacBook-Pro:/Applications/NoxAppPlayer.app/Contents/MacOS$ ./adb shell
dream2qltechn:/ # whoami
root
dream2qltechn:/ # uname -a
Linux localhost 4.0.9+ #222 SMP PREEMPT Sat Mar 14 18:24:36 HKT 2020 i686

肯定还是先定位目标字符串 Wait a Minute,What was happend?

jadx 搜索字符串

重点在 a() 代码里,其实是根据明文的 name 和 password,然后 aVar.a(a2 + aVar.b(a2, contentValues.getAsString("password"))).substring(0, 7) 再做一遍复杂的计算并截取7位当做密码,传入 getWritableDatabase 去解密 demo.db 数据库。

所以我们 hook一下 getWritableDatabase 即可。

// 首先查看要注入的进程
frida-ps -U
...
5662  com.example.yaphetshan.tencentwelcome
...

// 使用 objection 注入
objection -d -g com.example.yaphetshan.tencentwelcome explore

看一下源码

package net.sqlcipher.database;
...
public abstract class SQLiteOpenHelper {
    ...
    public synchronized SQLiteDatabase getWritableDatabase(char[] cArr) {
     
        

也可以 objection search 一下这个 method

(samsung: 7.1.2) [usb] # android hooking search methods getWritableDatabase
Warning, searching all classes may take some time and in some cases, crash the target application.
Continue? [y/N]: y
Found 4650 classes, searching methods (this may take some time)...

android.database.sqlite.SQLiteOpenHelper.getWritableDatabase
...
net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase

hook 一下这个 method

[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return
- [incoming message] ------------------
{
  "payload": "Attempting to watch class \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m and method \u001b[32mgetWritableDatabase\u001b[39m.",
  "type": "send"
}
- [./incoming message] ----------------
(agent) Attempting to watch class net.sqlcipher.database.SQLiteOpenHelper and method getWritableDatabase.
- [incoming message] ------------------
{
  "payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31mjava.lang.String\u001b[39m)",
  "type": "send"
}
- [./incoming message] ----------------
(agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String)
- [incoming message] ------------------
{
  "payload": "Hooking \u001b[32mnet.sqlcipher.database.SQLiteOpenHelper\u001b[39m.\u001b[92mgetWritableDatabase\u001b[39m(\u001b[31m[C\u001b[39m)",
  "type": "send"
}
- [./incoming message] ----------------
(agent) Hooking net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase([C)
- [incoming message] ------------------
{
  "payload": "Registering job \u001b[94mjytq1qeyllq\u001b[39m. Type: \u001b[92mwatch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase\u001b[39m",
  "type": "send"
}
- [./incoming message] ----------------
(agent) Registering job jytq1qeyllq. Type: watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase
...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] #

hook 好之后再打开这个 apk

(agent) [1v488x28gcs] Called net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(java.lang.String)
...
(agent) [1v488x28gcs] Backtrace:
	net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(Native Method)
	com.example.yaphetshan.tencentwelcome.MainActivity.a(MainActivity.java:55)
	com.example.yaphetshan.tencentwelcome.MainActivity.onCreate(MainActivity.java:42)
	android.app.Activity.performCreate(Activity.java:6692)
...
(agent) [1v488x28gcs] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)

...
...mple.yaphetshan.tencentwelcome on (samsung: 7.1.2) [usb] # jobs list
Job ID         Hooks  Type
-----------  -------  -----------------------------------------------------------------------------
1v488x28gcs        2  watch-method for: net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase

找到参数 ae56f99

剩下的就是用这个密码去打开加密的 db。

然后base64解密一下就好了。

还有一种策略是主动调用,即自己去调用 a 函数以触发 getWritableDatabase 的数据库解密。先寻找 a 所在类的实例,然后 hook getWritableDatabase,最终主动调用 a。这里幸运的是 a 没有什么奇奇怪怪的参数需要我们传入。

[usb] # android heap search instances com.example.yaphetshan.tencentwelcome.MainActivity
Class instance enumeration complete for com.example.yaphetshan.tencentwelcome.MainActivity
Handle    Class                                               toString()
--------  --------------------------------------------------  ----------------------------------------------------------
0x20078a  com.example.yaphetshan.tencentwelcome.MainActivity  com.example.yaphetshan.tencentwelcome.MainActivity@1528f80

[usb] # android hooking watch class_method net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase --dump-args --dump-backtrace --dump-return
[usb] # android heap execute 0x20078a a
(agent) [taupgwkum4h] Arguments net.sqlcipher.database.SQLiteOpenHelper.getWritableDatabase(ae56f99)

案例学习 2:主动调用爆破密码

附件链接:https://bbs.pediy.com/thread-257745.htm

因为直接找 Unfortunately,note the right PIN :(找不到,可能是把字符串藏在什么资源文件里了。 review 代码之后找到校验的核心函数,逻辑就是将 input 编码一下之后和密码比较,这肯定是什么不可逆的加密。

public static boolean verifyPassword(Context context, String input) {
    if (input.length() != 4) {
        return false;
    }
    byte[] v = encodePassword(input);
    byte[] p = "09042ec2c2c08c4cbece042681caf1d13984f24a".getBytes();
    if (v.length != p.length) {
        return false;
    }
    for (int i = 0; i < v.length; i++) {
        if (v[i] != p[i]) {
            return false;
        }
    }
    return true;
}

这里就爆破一下密码。

// 查看 app 进程
frida-ps -U | grep qualification
7660  org.teamsik.ahe17.qualification.easy

// frida 命令执行 js 进行 hook
frida -U -f org.teamsik.ahe17.qualification.easy -l force.js

# 上面命令执行成功后,会进入 frida,再输入 %resume 然后回车,即可让程序继续执行

或者使用 -F 大写F 参数,

js 脚本:

function main() {
    Java.perform(function x() {
        console.log("In Java perform")
        var verify = Java.use("org.teamsik.ahe17.qualification.Verifier")
        var stringClass = Java.use("java.lang.String")
        var p = stringClass.$new("09042ec2c2c08c4cbece042681caf1d13984f24a")
        var pSign = p.getBytes()
        // var pStr = stringClass.$new(pSign)
        // console.log(parseInt(pStr))
        for (var i = 999; i < 10000; i++){
            var v = stringClass.$new(String(i))
            var vSign = verify.encodePassword(v)
            if (parseInt(stringClass.$new(pSign)) == parseInt(stringClass.$new(vSign))) {
                console.log("yes: " + v)
                break
            }
            console.log("not :" + v)
        }
    })
}
setImmediate(main)

 

 ... not :9080 not :9081 not :9082 yes: 9083

这里注意 parseInt

5、Frida hook 基础(一)

  • 调用
  • 设置 (同名)成员变量
  • 内部类,枚举类的函数并hook,trace原型1
  • 查找接口,hook动态加载dex
  • 枚举class,trace原型2
  • objection 不能切换 classloader

5.1 Frida hook : 打印参数、返回值 / 设置返回值 / 主动调用

demo 就不贴了,还是先定位登录失败点,然后搜索字符串。

public class LoginActivity extends AppCompatActivity {
    /* access modifiers changed from: private */
    public Context mContext;

    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        this.mContext = this;
        setContentView((int) R.layout.activity_login);
        final EditText editText = (EditText) findViewById(R.id.username);
        final EditText editText2 = (EditText) findViewById(R.id.password);
        ((Button) findViewById(R.id.login)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                String obj = editText.getText().toString();
                String obj2 = editText2.getText().toString();
                if (TextUtils.isEmpty(obj) || TextUtils.isEmpty(obj2)) {
                    Toast.makeText(LoginActivity.this.mContext, 
                    "username or password is empty.", 1).show();
                } else if (LoginActivity.a(obj, obj).equals(obj2)) {
                    LoginActivity.this.startActivity(
                        new Intent(LoginActivity.this.mContext, FridaActivity1.class));
                    LoginActivity.this.finishActivity(0);
                } else {
                    Toast.makeText(LoginActivity.this.mContext, "Login failed.", 1).show();
                }
            }
        });
    }

LoginActivity.a(obj, obj).equals(obj2) 分析之后可得 obj2 来自 password,由从 username 得来的 obj,经过 a 函数运算之后得到一个值,这两个值相等则登录成功。所以这里关键是 hook a 函数的参数,最简脚本如下。

//打印参数、返回值
function Login(){
    Java.perform(function(){
        Java.use("com.example.androiddemo.Activity.LoginActivity").a.overload('java.lang.String', 'java.lang.String').implementation = function (str, str2){
            var result = this.a(str, str2);
            console.log("args0:" + str + " args1:" + str2 + " result:" + result);
            return result;
        }
    })
}
setImmediate(Login)

观察输入和输出,这里也可以直接主动调用。

function login() {
    Java.perform(function () {
        console.log("start")
        var login = Java.use("com.example.androiddemo.Activity.LoginActivity")
        var result = login.a("1234","1234")
        console.log(result)
    })
}
setImmediate(login)

输出:

...
start
4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb
然后
adb shell input text "4e4feaea959d426155a480dc07ef92f4754ee93edbe56d993d74f131497e66fb"

接下来是第一关

public abstract class BaseFridaActivity extends AppCompatActivity implements View.OnClickListener {
    public Button mNextCheck;

    public void CheckSuccess() {
    }

    public abstract String getNextCheckTitle();

    public abstract void onCheck();

    /* access modifiers changed from: protected */
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView((int) R.layout.activity_frida);
        this.mNextCheck = (Button) findViewById(R.id.next_check);
        this.mNextCheck.setOnClickListener(this);
        Button button = this.mNextCheck;
        button.setText(getNextCheckTitle() + ",点击进入下一关");
    }

    public void onClick(View view) {
        onCheck();
    }

    public void CheckFailed() {
        Toast.makeText(this, "Check Failed!", 1).show();
    }
}
...

public class FridaActivity1 extends BaseFridaActivity {
    private static final char[] table = {'L', 'K', 'N', 'M', 'O', 'Q', 'P', 'R', 'S', 'A', 'T', 'B', 'C', 'E', 'D', 'F', 'G', 'H', 'I', 'J', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'o', 'd', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'e', 'f', 'g', 'h', 'j', 'i', 'k', 'l', 'm', 'n', 'y', 'z', '0', '1', '2', '3', '4', '6', '5', '7', '8', '9', '+', '/'};

    public String getNextCheckTitle() {
        return "当前第1关";
    }

    public void onCheck() {
        try {
            if (a(b("请输入密码:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")) {
                CheckSuccess();
                startActivity(new Intent(this, FridaActivity2.class));
                finishActivity(0);
                return;
            }
            super.CheckFailed();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static String a(byte[] bArr) throws Exception {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i <= bArr.length - 1; i += 3) {
            byte[] bArr2 = new byte[4];
            byte b = 0;
            for (int i2 = 0; i2 <= 2; i2++) {
                int i3 = i + i2;
                if (i3 <= bArr.length - 1) {
                    bArr2[i2] = (byte) (b | ((bArr[i3] & 255) >>> ((i2 * 2) + 2)));
                    b = (byte) ((((bArr[i3] & 255) << (((2 - i2) * 2) + 2)) & 255) >>> 2);
                } else {
                    bArr2[i2] = b;
                    b = 64;
                }
            }
            bArr2[3] = b;
            for (int i4 = 0; i4 <= 3; i4++) {
                if (bArr2[i4] <= 63) {
                    sb.append(table[bArr2[i4]]);
                } else {
                    sb.append('=');
                }
            }
        }
        return sb.toString();
    }

    public static byte[] b(String str) {
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            GZIPOutputStream gZIPOutputStream = new GZIPOutputStream(byteArrayOutputStream);
            gZIPOutputStream.write(str.getBytes());
            gZIPOutputStream.finish();
            gZIPOutputStream.close();
            byte[] byteArray = byteArrayOutputStream.toByteArray();
            try {
                byteArrayOutputStream.close();
                return byteArray;
            } catch (Exception e) {
                e.printStackTrace();
                return byteArray;
            }
        } catch (Exception unused) {
            return null;
        }
    }
}

关键函数在  a(b("请输入密码:")).equals("R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=")

这里应该直接 hook a,让其返回值为 R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=就可以进入下一关了。

function ch1() {
    Java.perform(function () {
        console.log("start")
        Java.use("com.example.androiddemo.Activity.FridaActivity1").a.implementation = function (x) {
            return "R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL="
        }
    })
}

上面的代码 hook 了 。在写 hook 的 js 代码时, 区别:

  • hook 静态函数时候,不需要 overload
  • hook 类的成员函数时,需要 overload

上面代码是,然后调用,

下面代码是 

5.2 Frida hook : 主动调用静态/非静态函数 以及 设置静态/非静态成员变量的值

总结:

  • 静态函数直接 use class 然后调用方法,非静态函数需要先 choose 实例然后调用
  • 设置成员变量的值,写法是 xx.value = yy,其他方面和函数一样。
  • 如果有一个成员变量和成员函数的名字相同,则在其前面加一个_,如 _xx.value = yy

然后是第二关

public class FridaActivity2 extends BaseFridaActivity {
    private static boolean static_bool_var = false;
    private boolean bool_var = false;

    public String getNextCheckTitle() {
        return "当前第2关";
    }

    private static void setStatic_bool_var() {
        static_bool_var = true;
    }

    private void setBool_var() {
        this.bool_var = true;
    }

    public void onCheck() {
        if (!static_bool_var || !this.bool_var) {
            super.CheckFailed();
            return;
        }
        CheckSuccess();
        startActivity(new Intent(this, FridaActivity3.class));
        finishActivity(0);
    }
}

这一关的关键在于下面的 if 判断要为 false,则 static_bool_var 和 this.bool_var 都要为 true。

if (!static_bool_var || !this.bool_var) {
    super.CheckFailed();
    return;
}

这样就要调用 setBool_var 和 setStatic_bool_var 两个函数了。

function ch2() {
    Java.perform(function () {
        console.log("start")
        var FridaActivity2 = Java.use("com.example.androiddemo.Activity.FridaActivity2")

        // hook 静态函数 直接调用
        FridaActivity2.setStatic_bool_var()

        // hook 动态函数,找到 instance 实例,从 实例中 调用函数方法
        Java.choose("com.example.androiddemo.Activity.FridaActivity2", {
            onMatch: function (instance) {
                instance.setBool_var()
            },
            onComplete: function () {
                console.log("end")
            }
        })
    })
}
setImmediate(ch2)

接下来是第三关

public class FridaActivity3 extends BaseFridaActivity {
    private static boolean static_bool_var = false;
    private boolean bool_var = false;
    private boolean same_name_bool_var = false;

    public String getNextCheckTitle() {
        return "当前第3关";
    }

    private void same_name_bool_var() {
        Log.d("Frida", static_bool_var + " " + this.bool_var + " " + this.same_name_bool_var);
    }

    public void onCheck() {
        if (!static_bool_var || !this.bool_var || !this.same_name_bool_var) {
            super.CheckFailed();
            return;
        }
        CheckSuccess();
        startActivity(new Intent(this, FridaActivity4.class));
        finishActivity(0);
    }
}

关键是让 if (!static_bool_var || !this.bool_var || !this.same_name_bool_var)为 false,则三个变量都要为 true

function ch3() {
    Java.perform(function () {
        console.log("start")
        var FridaActivity3 = Java.use("com.example.androiddemo.Activity.FridaActivity3")
        FridaActivity3.static_bool_var.value = true
        
        Java.choose("com.example.androiddemo.Activity.FridaActivity3", {
            onMatch: function (instance) {
                instance.bool_var.value = true
                instance._same_name_bool_var.value = true
            },
            onComplete: function () {
                console.log("end")
            }
        })
    })
}

注意:类里有一个 都叫做  same_name_bool_var ,这种时候在成员变量前加一个 _,修改值的形式为  xx.value = yy

5.3 Frida hook : 内部类,枚举类的方法 并 hook,trace原型1

总结:

  • 对于内部类,通过   或者
  • 对 use 得到的 clazz 应用反射,如 clazz.class.getDeclaredMethods() 可以得到 ,即

接下来是第四关

public class FridaActivity4 extends BaseFridaActivity {
    public String getNextCheckTitle() {
        return "当前第4关";
    }
 
    private static class InnerClasses {
        public static boolean check1() { return false;}
        public static boolean check2() { return false;}
        public static boolean check3() { return false;}
        public static boolean check4() { return false;}
        public static boolean check5() { return false;}
        public static boolean check6() { return false;}
        private InnerClasses() {}
    }
 
    public void onCheck() {
        if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3() 
         || !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6())
        {
            super.CheckFailed();
            return;
        }
        CheckSuccess();
        startActivity(new Intent(this, FridaActivity5.class));
        finishActivity(0);
    }
}

这一关的关键是让 if (!InnerClasses.check1() || !InnerClasses.check2() || !InnerClasses.check3() || !InnerClasses.check4() || !InnerClasses.check5() || !InnerClasses.check6()) 中的所有 check 全部返回 true。

其实这里唯一的问题就是寻找内部类 InnerClasses,对于内部类的 hook,通过 去 use。

function ch4() {
    Java.perform(function () {
        var InnerClasses = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses")
        console.log("start")
        InnerClasses.check1.implementation = function () { return true }
        InnerClasses.check2.implementation = function () { return true }
        InnerClasses.check3.implementation = function () { return true }
        InnerClasses.check4.implementation = function () { return true }
        InnerClasses.check5.implementation = function () { return true }
        InnerClasses.check6.implementation = function () { return true }
    })
}

利用反射,获取类中的所有 method 声明,然后字符串拼接去获取到方法名,例如下面的 check1,然后就可以批量 hook,而不用像我上面那样一个一个写。

var inner_classes = Java.use("com.example.androiddemo.Activity.FridaActivity4$InnerClasses")
var all_methods = inner_classes.class.getDeclaredMethods();

...
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check1(),
public static boolean com.example.androiddemo.Activity.FridaActivity4$InnerClasses.check2(),
public static boolean com.example.androiddemo.Activit

标签: 连接器dc62pk87

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台