Android Hook 之 Frida

一、Frida简介

Frida是一款流行的hook框架,通杀android\ios\linux\win\osx等各平台,相比xposed和substrace cydia更加便捷。Frida的官网为:http://www.frida.re/,其核心原理是实现了一套inline hook框架,在Andorid平台的具体原理可参考这里

本文重点介绍Frida在android平台的使用。

二、环境配置

官方教程https://frida.re/docs/installation/,主要操作如下:

  1. pip install frida-tools 安装frida环境
  2. 在root的手机上运行frida server端
    • 1.下载对应平台的frida-server (https://github.com/frida/frida/releases)
    • 2.adb push frida-server /data/lcoal/tmp
    • 3.chmod 755 frida-server
    • 4.setenforce 0(运行前关闭SELinux)
    • 5../frida-server &
  3. 建立调试通道
    • USB连接+默认端口号可直接略过
    • 改变端口号需要 adb forward tcp:12345 tcp:12345
  4. 完成配置,直接在PC端运行hook脚本即可

需要注意的是,frida的客户端和服务度版本应该相同,否则会出现不可预知的其他错误。使用“frida –version” 查看frida的版本

三、免root实现frida hook(重打包)

注意: 无root环境下frida的部分功能受限

  1. apktool反编译apk

    1
    $ apktool d test.apk -o test
  2. 将对应版本的gadget拷贝到/lib目录
    下载地址:
    https://github.com/frida/frida/releases/

  3. smali注入加载library,选择application类或者Activity入口.

    1
    2
    const-string v0, "frida-gadget" 
    invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
  4. 回编译apk

    1
    $ apktool b -o newtest.apk test/
  5. 重新签名安装运行.成功后启动app会有如下日志

Frida: Listening on TCP port 27042

四、frida-python模板

以下为frida hook的框架代码,只需要修改jscode就可以实现自定义的hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import frida, sys
def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)
jscode = """
Java.perform(function () {
// 要hook的类名
var MainActivity = Java.use('com.example.xxx.MainActivity');
// hook按钮点击事件
MainActivity.onClick.implementation = function (v) {
console.log('hook onClick');
//修改MainActivity中属性的值
this.m.value = 0;
this.n.value = 1;
this.cnt.value = 999;
//打印日志
console.log('Done:' + JSON.stringify(this.cnt));
// 执行原始方法代码
this.onClick(v);
};
});
"""
device = frida.get_usb_device()
# 设置要注入的进程名或者pid
# attach mode
process = device.attach('com.tencent.mm')
# spawn mode
pid = device.spawn('com.tencent.mm')
process = device.attach(pid)
device.resume(pid)

script = process.create_script(jscode)
script.on('message', on_message)
print('[*] Running hook')
script.load()
sys.stdin.read()

五、采坑记录

1.延时hook问题

spawn模式下,有时候hook的lib还没有被加载到,导致无法hook,选择hook的时机很关键。

可以在hook代码前,用enumerateModulesSync验证是否加载

1
2
3
4
5
var mod = Process.enumerateModulesSync();
var modLen = mod.length;
for (var i = 0; i < modLen; i++) {
console.log("[+] module: " + mod[i]['name']);
}

确认是延时问题后,有以下几种解决办法:

  • 如果不在意时效性,启动时不加“–no-pause”,可以等一段时间后,手动”%resume”
  • 使用setTimeout(func, delay) 函数延时
  • 使用frida的Module.load()或Module.ensureInitialized()来手动加载lib
  • hook libart.so中的LoadNativeLibrary()或java层的System.loadLibrary(),判断加载完成特定so后,再进行后续的hook。

(1)hook libart.so中的LoadNativeLibrary()代码:

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
function readStdString(str) {
if ((str.readU8() & 1) === 1) { // size LSB (=1) indicates if it's a long string
return str.add(2 * Process.pointerSize).readPointer().readUtf8String();
}
return str.add(1).readUtf8String();
}
//find LoadNativeLibrary address
var LoadNativeLibrary_offset = 0;
var JNIload_instruct_offset = 0;
var mod_art = Process.findModuleByName("libart.so");
if (mod_art) {
var art_exports = mod_art.enumerateExports()
for (var i=0;i<art_exports.length;i++ ) {
if (art_exports[i].name.indexOf("LoadNativeLibrary") != -1) {
console.log("find LoadNativeLibrary:"+art_exports[i].name, art_exports[i].address);
LoadNativeLibrary_offset = art_exports[i].address.sub(mod_art.base);
console.log("LoadNativeLibrary offset: "+LoadNativeLibrary_offset);
break;
}
}
}
//hook LoadNativeLibrary
Interceptor.attach(mod_art.base.add(LoadNativeLibrary_offset), {
onEnter: function (args) {
this.pathName = readStdString(ptr(this.context.r2));
console.log("[*] [LoadNativeLibrary] in pathName =", this.pathName);
},
onLeave: function (retval) {
console.log("[*] [LoadNativeLibrary] out pathName =", this.pathName);
}
});

(2)hook System.loadLibrary()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const System = Java.use('java.lang.System');
const Runtime = Java.use('java.lang.Runtime');
const SystemLoad_2 = System.loadLibrary.overload('java.lang.String');
const VMStack = Java.use('dalvik.system.VMStack');
SystemLoad_2.implementation = function(library) {
//console.log("Loading dynamic library => " + library);
try {
const loaded = Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), library);
if(library.includes("DexHelper")) {
console.log("loaded DexHelper...");
//此处添加对应的hook代码
}
return loaded;
} catch(ex) {
console.log(ex);
}
};

2. hook不上的原因

  • 要hook的函数是不是正好被打了热补丁?
  • hook的目标属于动态load, 需要确定时机或手动长期load到内存
  • hook的进程是否正确,比如com.tencent.mm有好几个进程

对于多进程问题,参考child gating(https://frida.re/news/2018/04/28/frida-10-8-released/)和spawn gating, 示例代码:

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
# -*- coding: utf-8 -*-
import codecs
import frida
import sys
import threading


#device = frida.get_remote_device()
device = frida.get_device_manager().enumerate_devices()[-1]
print(device)


pending = []
sessions = []
scripts = []
event = threading.Event()

jscode = """
Java.perform(function() {
//your js code
})
"""

def spawn_added(spawn):

event.set()
if(spawn.identifier.startswith('com.ss.android.ugc.aweme')):
print('spawn_added:', spawn)
session = device.attach(spawn.pid)
script = session.create_script(jscode)
script.on('message', on_message)
script.load()
device.resume(spawn.pid)

def spawn_removed(spawn):
print('spawn_removed:', spawn)
event.set()

def on_message(spawn, message, data):
print('on_message:', spawn, message, data)

def on_message(message, data):
if message['type'] == 'send':
print("[*] {0}".format(message['payload']))
else:
print(message)

device.on('spawn-added', spawn_added)
device.on('spawn-removed', spawn_removed)

device.enable_spawn_gating()

event = threading.Event()
print('Enabled spawn gating')

pid = device.spawn(["com.ss.android.ugc.aweme"])

session = device.attach(pid)
print("[*] Attach Application id:",pid)
device.resume(pid)
sys.stdin.read()

3. 同名函数返回值不同

hook中可能出现“函数名参数完全相同,但返回值不同的情况”,这种情况frida暂时无法实现,测试此情况代码编译不会通过,但编译后可以改bytecode,不影响执行结果

  • 这种情况可以枚举函数的overloads属性,挨个hook,或者使用数组索引hook,例如a.overloads[1].implement=xxxx

4. 使用enumerateLoadedClasses报“VM::GetEnv failed”错误

enumerateLoadedClasses需要在Java.perform()下使用,参考https://github.com/frida/frida/issues/237

5. app没有入口,无法使用spawn

Failed to spawn: unable to find application with identifier ‘com.dzvuhumnjt.kfwmalytfds’
如果包名是正确的,应该是这个包名是一个服务,没有lunch activity, 此时只能靠启动后抢时间的方式来attach, spawn行不通

6. 判断一个函数是public还是privte

反射出 method 类来调getModifiers()

7. Magisk Hide

unable to access process with pid 1204 due to system restrictions; try sudo sysctl kernel.yama.ptrace_scope=0, or run Frida as root
magisk 里面的一个叫magisk hide的东西会妨碍frida的进程,关掉这个即可

8. 明明有这个函数却hook不了,即使自己调用也无法触发

常见于一些final或static函数,系统进行了优化,在开头使用Java.deoptimizeEverything() 强制VM使用其解释器执行所有操作。参考https://github.com/frida/frida/issues/1298

例如JIT的内联优化(method lining)会根据调用频次决定是否走解释执行,也会出现这种情况。

解决方案:

(1)Frida API

Java.deoptimizeEverything()
Java.deoptimizeBootImage() 14.2新增https://frida.re/news/2021/02/10/frida-14-2-released/#deoptimization

(2)系统级别

adb shell setprop dalvik.vm.usejit false (关闭JIT)
adb shell cmd package compile –reset com.tencent.mm (清除配置文件数据并移除经过编译的代码)

参考https://github.com/frida/frida/issues/817
https://source.android.google.cn/devices/tech/dalvik/jit-compiler

六、 Frida基础数据类型

为以后写脚本时方便,这里列举下常用的类型:

Int

1
2
var Integerclass = Java.use("java.lang.Integer");
var myint = Integerclass.$new(1440);

Long

1
2
var Longclass = Java.use("java.lang.Long");
var myint = Longclass.$new(27893952512)

Bool

1
Java.use("java.lang.Boolean").$new(true);

ByteArray

  1. js传给python

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // js中send(byte[]数组)
    def on_message(message, data):
    if message['type'] == 'send':
    data = message['payload']
    for i in range(len(data)):
    data[i] = data[i] & 0xff
    open("tmp",'wb').write(bytearray(data))
    else:
    pass
  2. python传给js

    1
    var a = Java.array('byte', [ 0xac,0x37,0x43,0x4f,0xaf,0xa8]);

遍历map

1
2
3
4
5
6
7
8
9
10
11
var mapcls = Java.use("java.util.Map");
var mymap = Java.cast(a1, mapcls);
var result = "";
var keyset = mymap.keySet();
var it = keyset.iterator();
while(it.hasNext()){
var keystr = it.next().toString();
var valuestr = mymap.get(keystr).toString();
result += keystr+":"+valuestr+" ";
}
console.log(result);

七、 Frida Hook实例

以下为我曾经用过或自己实现的frida脚本,根据不同的使用场景进行了分类。

1. 打印hook函数的返回值

直接运行一遍原函数,将结果赋值给一个新变量,输出并return新变量即可,例如:
1
2
3
4
5
6
7
8
Java.perform(function () {
var cls = Java.use("com.tencent.mm.sdk.platformtools.w");
cls.w.overload("java.lang.String","java.lang.String").implementation=function(p1,p2){
var req = this.w(p1,p2);
console.log(req);
return req;
};
});

2. hook重载函数

1
2
cls.loadUrl.overload("java.lang.String").implementation = function(param)……
cls.loadUrl.overload("java.lang.String","java.util.Map").implementation=

3. 打印java函数的调用栈

1
2
3
4
5
6
7
var Exc = Java.use("java.lang.Exception");
var Log = Java.use("android.util.Log");
xxxx.implementation = function(param){
var e = Exc.$new("");
var log = Log.$new();
console.log(log.getStackTraceString(e));
};


1
console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()))

4. 打印native函数调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log("begin====");
var libavmp = Module.findBaseAddress("libsgavmpso-6.4.20.so");
var func = ptr(parseInt(libavmp)+0x1ea);
console.log("libavmp base: "+libavmp);
console.log("function base: "+func);
Interceptor.attach(func, {
onEnter: function(args) {
console.log(Thread.backtrace(this.context,Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(" "));


},
onLeave:function(retval){
console.log("retval: "+retval);
}
});

5. dump内存

1
2
var data = Memory.readByteArray(ptr(0x824a9000), 159744);
console.log({type: 'data-for-you' }, data);

6. Hook Android IMEI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function () {
var TM = Java.use("android.telephony.TelephonyManager");
console.log("hook start...");

TM.getSimSerialNumber.overload().implementation = function () {
console.log("Called - getSimSerialNumber(String)");
var temp = this.getSimSerialNumber();
console.log(temp);
return "123456789";
};

TM.getDeviceId.overload().implementation = function () {
console.log("Called - deviceID()");
var temp = this.getDeviceId();
console.log(temp);
return "867979021642856";
};

});

7. Hook Android webview http请求

主要针对以下webview中的以下函数:

1
2
3
4
5
- loadUrl(String url)   
- loadUrl(String url, Map<String, String> additionalHttpHeaders)
- loadData(String data, String mimeType, String encoding)
- loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
- postUrl(String url, byte[] postData)
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
Java.perform(function () {
var cls = Java.use("android.webkit.WebView");
console.log("class start:------------------");

cls.loadUrl.overload("java.lang.String").implementation = function(param){
console.log("loadUrl hooked "+param);
this.loadUrl("file:///sdcard/1.html");
};

cls.loadUrl.overload("java.lang.String","java.util.Map").implementation = function(p1,p2){
console.log("loadUrl2 hooked"+p1 + p2);
this.loadUrl("file:///sdcard/1.html",null);
};

cls.loadDataWithBaseURL.implementation = function(p1,p2,p3,p4,p5){
console.log("loadDataWithBaseURL hooked"+p1 + p2);
this.loadDataWithBaseURL("file:///sdcard/1.html",null,null,null,null);
};


cls.postUrl.implementation = function(p1,p2){
console.log("postUrl hooked"+p1);
this.postUrl("file:///sdcard/1.html",null);
};


});

8. 获取context

1
2
var currentApplication = Java.use('android.app.ActivityThread').currentApplication();
var context = currentApplication.getApplicationContext();

9. 创建bundle对象

1
2
3
4
5
6
7
8
9
var bundle = Bundle.$new();
//调用实例方法需要使用call
Bundle.putString.call(bundle,'key1','value1')
//也可以指定具体的参数类型
//Bundle.putString.overload('java.lang.String','java.lang.String').call(bundle,'key1','value1')
this.getIntent().putExtra('testBundle',bundle)
//activity.getIntent().getBundleExtra("testBundle");
var outB = this.getIntent().getBundleExtra('testBundle')
console.log(outB);

10. hook 对象

1
2
3
4
5
6
7
//读取实例对象的属性值,对于得到的对象,需要使用Java.cast()方法转换后才可以使用
// PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),0);
// String pkg = packageInfo.packageName;
var t = this.getPackageManager().getPackageInfo(this.getPackageName(),0);
var packageInfo = Java.cast(t.$handle, PackageInfo);
var pkg = packageInfo.packageName.value
console.log(pkg)

11. 导出函数并被任意调用

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
rdev = frida.get_remote_device()
session = rdev.attach("com.eg.android.AlipayGphone")

jscode="""
rpc.exports = {
myfunc: function(aa,bb,cc){
Java.perform(function () {
try{
classf = Java.use('com.alipay.android.phone.wallet.sharetoken.service.f');
var result = classf.a(aa,bb,cc);//a为static函数
//f = Hrida.$new(); 非static函数需要new一个实例
console.log("myfunc result: "+result);
return result.toString()

}catch(e){
console.log(e)
}
});
//return result
}
}
"""
script = session.create_script(jscode)
script.on("message", my_message_handler)
script.load()

command = ""
while 1 == 1:
command = raw_input("Exit: 9999 0: default others: zhikouling")
if command == "9999":
break
else:
a = "b54578ff9d5fcbf6"
b = None
c = "快来吱付寳"
script.exports.myfunc(a,b,c)

需要获取返回值的情况

1
2
3
4
5
6
7
8
9
10
11
12
rpc.exports.foo = function () {
return new Promise(function (resolve, reject) {
Java.perform(function () {
try {
var result = ...
resolve(result);
} catch (e) {
reject(e);
}
});
});
};

12. hook构造函数

1
2
obj.$init.implementation = function (){
}

13.枚举所有加载的类

1
2
3
4
5
6
7
8
9
10
Java.enumerateLoadedClasses({
onMatch: function(classname){
if (classname.indexOf("XmlPullParser")>-1){
console.log(classname);
}
},

onComplete: function (){
}
});

14.打印函数参数类型

upload successful

15.注册一个类(Java.registerClass)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Java.perform(function() {
var FLAG_SECURE = 0x2000;

var Runnable = Java.use("java.lang.Runnable");
var DisableSecureRunnable = Java.registerClass({
name: "me.bhamza.DisableSecureRunnable",
implements: [Runnable],
fields: {
activity: "android.app.Activity",
},
methods: {
$init: [{
returnType: "void",
argumentTypes: ["android.app.Activity"],
implementation: function (activity) {
this.activity.value = activity;
}
}],
run: function() {
var flags = this.activity.value.getWindow().getAttributes().flags.value; // get current value
flags &= ~FLAG_SECURE; // toggle it
this.activity.value.getWindow().setFlags(flags, FLAG_SECURE); // disable it!
console.log("Done disabling SECURE flag...");
}
}
});

Java.choose("com.example.app.FlagSecureTestActivity", {
"onMatch": function (instance) {
var runnable = DisableSecureRunnable.$new(instance);
instance.runOnUiThread(runnable);
},
"onComplete": function () {}
});
});

16.输出一个类的所有field和Method

1
2
3
4
5
6
7
8
9
10
11
12
13
const Class = Java.use("com.Awesome.App.MainActivity");
function inspectClass(obj) {
const obj_class = Java.cast(obj.getClass(), Class);
const fields = obj_class.getDeclaredFields();
const methods = obj_class.getMethods();
console.log("Inspect " + obj.getClass().toString());
console.log("\tFields:");
for (var i in fields)
console.log("\t" + fields[i].toString());
console.log("\tMethods:");
for (var i in methods)
console.log("\t" + methods[i].toString());
}

17.修改native函数的返回值

1
2
3
4
5
6
7
8
9
10
11
Interceptor.attach(Module.getExportByName('libnative-lib.so', 'Jniint'), {
onEnter: function(args) {
this.first = args[0].toInt32(); // int
console.log("on enter with: " + this.first)
},
onLeave: function(retval) {
const dstAddr = Java.vm.getEnv().newIntArray(1117878);
console.log("dstAddr is : " + dstAddr.toInt32())
retval.replace(dstAddr);
}
});

18.枚举native导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//enumerateExports(null,...   所有lib
Module.enumerateExports("mylib.so", {
onMatch: function(e) {
if (e.type == 'function') {
console.log("name of function = " + e.name);

if (e.name == "Java_example_decrypt") {
console.log("Function Decrypt recognized by name");
Interceptor.attach(e.address, {
onEnter: function(args) {
console.log("Interceptor attached onEnter...");
},
onLeave: function(retval) {
console.log("Interceptor attached onLeave...");
}
});
}
}
},
onComplete: function() {}
});

19.内存中注入so和dex

1
2
3
4
5
6

module_libex = Module.load("/data/local/tmp/xxx.so")
//so如果有依赖其他,也需要注入内存

Java.openClassFile("/data/local/tmp/xxxx.dex").load()
//dx 制作 dex, push 到手机里 或者 编译一个helloword带上lib

20.使用google的gson打印object

参考:https://bbs.pediy.com/thread-259186.htm

21.利用反射找类的Interface

https://bbs.pediy.com/thread-259631.htm

其他参考链接

  1. https://github.com/FloatingGuy/fg-Blog/blob/7df0bd47b42d11b787fe394259fa30288307ae48/source/_posts/%E5%BC%80%E5%8F%91/frida%20hook%20%E6%93%8D%E4%BD%9C%E6%89%8B%E5%86%8C.md
  2. r0ysue的相关文章 https://bbs.pediy.com/user-home-581423.htm
  3. Frida学习笔记 https://api-caller.com/2019/03/30/frida-note/#%E8%99%9A%E6%8B%9F%E6%9C%BA%E5%A0%86%E6%A0%88