Android逆向之frida

介绍frida在frida-serverfrida-gadget两种注入方式下的使用,objection的使用,frida hook实现绕过root检测、绕过IDA反调试检测等

frida是一种支持windows、macos、linux、android、ios多平台,采用动态二进制插桩技术(DBI)的hook框架,并支持native级别的hook。

注入方式

frida注入方式有frida-server和frida-gadget两种方式,主要区别是:frida-server方式需要预先在安卓中运行起来frida服务端程序,需要root权限;而frida-gadget不需要root权限,但需要被hook的app运行之前将其so动态库运行起来。

frida-server

准备好台已root的手机,注入步骤如下:

  1. 下载对应手机版本的frida-server并解压

  2. 将解压之后将文件push到准备好的已root手机中

    1
    adb push frida-server-xxx-android-xxx /data/local/tmp/frida-server
  3. 通过"adb root"或者"su"运行frida-server,这里为了启动方便,写了一个.bat批处理文件

    1
    2
    adb root
    adb shell < cmd.bat

    cmd.bat内容如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    cd /data/local/tmp

    #判断进程是否存在
    ps | grep frida-server

    if [ $? -ne 0 ]
    then
    chmod 777 frida-server
    ./frida-server &
    echo "start successfully!"
    ps | grep frida-server
    else
    echo "runing!"
    fi

frida-gadget

不需要root,注入步骤如下:

  1. 下载对应手机版本的frida-gadget并解压得到.so动态库

  2. 通过smali插桩让app提早加载.so动态库,例如入口处执行

    1
    2
    const-string v0, "frida-gadget"
    invoke-static {v0}, Ljava/lang/System;>loadLibrary(Ljava/lang/String;)V
  3. 确认注入APP有网络访问权限,如果没有则在AndroidManifest.xml中添加

    1
    <uses-permission android:name="android.permission.INTERNET" />
  4. 重新打包、签名APK,并在手机中运行检查是否正常运行起来

注入工具运行起来后,安装下两个python库fridafrida-tools,就可以执行注入脚本了

1
2
pip3 install -U frida
pip3 install -U frida-tools

安装之后在python的Scripts目录下就会多出frida(方便调试和交互的REPL)、frida-ps(列出进程)、frida-trace(跟踪函数调用)、frida-discover(发现内部函数)、frida-ls-devices(列出多设备)、frida-kill(杀死进程)这些工具,可以在terminal执行下frida-ps -U测试下frida-server或者frida-gadget是否能正常运行起来。

objection

objection是基于frida,支持android和ios系统的一个移动工具包,可以很方便的实现APP的hook和监视。它是一个python库,使用前先用pip3 install objection安装一下,它借助frida实现功能,所以使用时先要在手机中运行起来frida-server或者frida-gadget注入程序。这里列举一些使用方法:

  • 使用时先连接到目标app上

    1
    objection --gadget APP包名 explore
  • 列出app所有的activity

    1
    android hooking list activities
  • 启动APP中某个activity

    1
    android intent launch_activity activity名
  • 监视某个方法返回值、参数、调用堆栈信息

    1
    android hooking watch class_method 包名.类名.方法名  --dump-args --dump-return --dump-backtrace
  • 监视某个类下的所有方法的调用情况

    1
    android hooking watch class 包名.类名

frida的使用

注入程序运行后,还需要编写运行脚本并运行起来,注入脚本使用javascript语言编写,编写注入脚本后,可以通过Frida CLI直接将注入脚本运行起来frida -U app包名 -l js脚本名,也可以通过python程序运行js注入脚本,python程序相比Frida CLI更为灵活,还可以通过rpc与注入程序实现交互,js脚本编写可以参考官网的JavaScript API

运行模式

frida支持aattachspawn两种运行模式:attach是通过ptrace附加到已经存在的进程,修改进程内存实现;spawn是启动一个新的进程并挂起,启动同时注入frida代码,注入完成后调用resume恢复进程实现,适用于在进程启动前的一些hook比如hook RegisterNative,但干扰项多,且容易卡死。

  • attach模式的"hello world"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import sys
    import frida

    appPackageName =""
    jscode = """
    function main() {
    Java.perform(function x() {
    console.log("hello world")
    })
    }
    setImmediate(main)
    """
    device = frida.get_usb_device()
    session = device.attach(appPackageName)
    script = session.create_script(jscode)
    script.load()
    sys.stdin.read()
  • spawn模式的"hello world"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import sys
    import frida

    appPackageName =""
    jscode = """
    function main() {
    Java.perform(function x() {
    console.log("hello world")
    })
    }
    setImmediate(main)
    """
    device = frida.get_usb_device()
    pid = device.spawn(appPackageName)
    device8.resume(pid)
    time.sleep(1)
    session = device.attach(pid)
    script = session.create_script(jscode)
    script.load()
    sys.stdin.read()

注入方法

为方便编写脚本,整理的一些常用js注入脚本方法:

方法 说明
Java.perform(fn) 当前线程附加到Java VM并且调用fn方法
Java.androidVersion 显示安卓系统版本号
Java.enumerateClassLoaders 枚举类加载器
Java.enumerateLoadedClasses 枚举所有java类
Process.enumerateModules 枚举so动态库
Process.enumerateThreads 枚举线程
Java.user(className).$new() 使用构造器创建java实例
a.$dispose() 显示释放对象,否则等待js垃圾回收
Java.cast(A, B) 类型转换
Java.choose(“xx.xx”,{…}) 堆上查找对象实例
Java.array(‘int’, [ 1003, 1005, 1007 ]); 创建数组
Java.registerClass({name: ‘xx.xx.xx’ }); 创建新的java类
Interceptor.attach(target, callbacks) native函数拦截,target是拦截函数地址
Interceptor.detachAll 让之前Interceptor.attach拦截的回调函数失效
const p_= new NativePointer(“100”); 同等与C语言中的指针
Module.load 加载指定so文件,返回一个Module对象
Module.findExportByName(exportName) 寻找指定so中export库中的函数地址
Module.findBaseAddress(name) 获取so的基地址
Memory.alloc 内存分配
Memory.copy 内存复制
Memory.writeByteArray 写入内存
Memory.readByteArray 内存读取
Memory.scan 内存中搜索数据

frida的功能实现

收藏整理一些功能实现的frida代码:

rpc远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import frida
def on_message(message, data):
if message['type'] == 'send':
print(message['payload'])
elif message['type'] == 'error':
print(message['stack'])
session = frida.get_usb_device().attach('app包名')
source = '''
function callSecretFun() { //定义导出函数
Java.perform(function () { //找到隐藏函数并且调用
//...
});
}
rpc.exports = {
callsecretfunction: callSecretFun
//把callSecretFun函数导出为callsecretfunction符号,导出名不可以有大写字母或者下划线
};
'''
script = session.create_script(source)
script.on('message', on_message)
script.load()
script.exports.callsecretfunction()
session.detach()

hook native方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Java.perform(function () {
//对So层的导出函数getSum进行拦截
Interceptor.attach(Module.findExportByName(".so库名" , "方法名"),{
//每次函数调用的时候会执行onEnter回调函数
onEnter: function(args) {
//输出
console.log('Context information:');
//输出上下文因其是一个Objection对象,需要它进行接送、转换才能正常看到值
console.log('Context : ' + JSON.stringify(this.context));
//输出返回地址
console.log('Return : ' + this.returnAddress);
//输出线程id
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
},
//函数执行完成之后会执行onLeave回调函数
onLeave:function(retval){
}
});
});

hook动态注册RegisterNatives

jni的java方法与native方法匹配有静态注册和动态注册两种方式,静态注册直接通过方法名匹配,动态可以通过hook RegisterNatives函数来获取所有动态注册的native函数地址

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
var RevealNativeMethods = function () {
var pSize = Process.pointerSize;
var env = Java.vm.getEnv();
var RegisterNatives = 215, FindClassIndex = 6;
var jclassAddress2NameMap = {};

function getNativeAddress(idx) {
return env.handle.readPointer().add(idx * pSize).readPointer();
}

// intercepting FindClass to populate Map<address, jclass>
Interceptor.attach(getNativeAddress(FindClassIndex), {
onEnter: function (args) {
jclassAddress2NameMap[args[0]] = args[1].readCString();
}
});
// RegisterNative(jClass*, .., JNINativeMethod *methods[nMethods], uint nMethods)
Interceptor.attach(getNativeAddress(RegisterNatives), {
onEnter: function (args) {
var jClass = jclassAddress2NameMap[args[0]].split('/');
console.log('jClass', jClass)
for (var i = 0, nMethods = parseInt(args[3]); i < nMethods; i++) {
/*
https://android.googlesource.com/platform/libnativehelper/+/master/include_jni/jni.h#129
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
*/
var structSize = pSize * 3; // = sizeof(JNINativeMethod)
var methodsPtr = ptr(args[2]);
var signature = methodsPtr.add(i * structSize + pSize).readPointer();
var fnPtr = methodsPtr.add(i * structSize + (pSize * 2)).readPointer(); // void* fnPtr

var methodName = methodsPtr.add(i * structSize).readPointer().readCString();
console.log('\x1b[3' + '6;01' + 'm', JSON.stringify({
module: DebugSymbol.fromAddress(fnPtr)['moduleName'], // https://www.frida.re/docs/javascript-api/#debugsymbol
class: jClass,
method: methodName, // methodsPtr.readPointer().readCString(), // char* name
signature: signature.readCString(), // char* signature TODO Java bytecode signature parser { Z: 'boolean', B: 'byte', C: 'char', S: 'short', I: 'int', J: 'long', F: 'float', D: 'double', L: 'fully-qualified-class;', '[': 'array' } https://github.com/skylot/jadx/blob/master/jadx-core/src/main/java/jadx/core/dex/nodes/parser/SignatureParser.java
address: fnPtr
}), '\x1b[39;49;00m');
}
}
});
}
setImmediate(function () {
RevealNativeMethods();
});

和IDA地址相互转换

1
2
3
4
5
6
7
8
9
10
11
function memAddress(memBase, idaBase, idaAddr) {
var offset = ptr(idaAddr).sub(idaBase);
var result = ptr(memBase).add(offset);
return result;
}

function idaAddress(memBase, idaBase, memAddr) {
var offset = ptr(memAddr).sub(memBase);
var result = ptr(idaBase).add(offset);
return result;
}

绕过root检测

每个APP的root检测方式都不尽相同,具体APP都要具体分析,若是比较常规的文件检测,检测system、sbin、data 目录下的su文件是否存在和一些root软件如Superuser是否存在,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
/sbin/su  
/system/bin/su
/system/bin/failsafe/su
/system/xbin/su
/system/xbin/busybox
/system/sd/xbin/su
/data/local/su
/data/local/xbin/su
/data/local/bin/su
/system/app/Superuser.apk
/system/etc/init.d/99SuperSUDaemon
/dev/com.koushikdutta.superuser.daemon/
/system/xbin/daemonsu

比如root检测代码为:

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
public class roottest{
public static boolean root() {
boolean bool1 = detectmethods();
return (bool1)
}

private static boolean detectmethods() {
String[] arrayOfString = new String[10];
arrayOfString[0] = "/system/app/Superuser.apk";
arrayOfString[1] = "/sbin/su";
arrayOfString[2] = "/system/bin/su";
arrayOfString[3] = "/system/xbin/su";
arrayOfString[4] = "/data/local/xbin/su";
arrayOfString[5] = "/data/local/bin/su";
arrayOfString[6] = "/system/sd/xbin/su";
arrayOfString[7] = "/system/bin/failsafe/su";
arrayOfString[8] = "/data/local/su";
arrayOfString[9] = "/su/bin/su";
int a = arrayOfString.length;
int b = 0;
while (a < b) {
if (new File(arrayOfString[a]).exists()) {
return true;
}
a += 1;
}
return false;
}

使用frida绕过root检测的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function bypass_root_detection() {
Java.perform(() => {
Java.use("java.io.File")["exists"].implementation = function () {
var ret = this.exists();
var path = this.path.value;
if (
path.endsWith("/su") ||
path.endsWith("/busybox") ||
path.indexOf("uperuser.") != -1 ||
path.indexOf("/daemonsu" != -1) ||
path.startsWith("/data")
) {
console.log("detecting root", this.path.value);
return false;
}
return ret;
};
});
}

绕过IDA反调试检测

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
function bypass_anti_debug() {
var openPtr = Module.findExportByName(null, "open");
var open = new NativeFunction(openPtr, "int", ["pointer", "int"]);
var pid;
var fds = {};
Java.perform(() => {
var Process = Java.use("android.os.Process");
pid = Process["myPid"].call(Process);
console.warn("current pid:", pid);
});

/**
* step1: 替换 /proc/下的关键文件,绕过检测
* 调用前须知:
* 强制退出,正常情况下(非debug, 非hook)启动app
* adb shell pidof com.taobao.trip 获取到当前app进程id -> pid
* cp /proc/self/status /data/local/tmp/status
* cp /proc/16748/stat /data/local/tmp/stat
* cp /proc/16748/wchan /data/local/tmp/wchan
*/
Interceptor.attach(Module.findExportByName(null, "open"), {
onEnter: function (args) {
var fname = args[0].readCString();

// fname.indexOf('/proc/' + pid + "/maps")
if (
fname.indexOf("/proc/self/status") != -1 ||
fname.indexOf("/proc/" + pid + "/stat") != -1 ||
fname.indexOf("/proc/" + pid + "/wchan") != -1
) {
this.flag = true;
this.fname = fname;
}
},

onLeave: function (retval) {
if (this.flag) {
fds[retval] = this.fname;
var fake_fd;
if (this.fname.indexOf("/proc/self/status") != -1) {
fake_fd = open(Memory.allocUtf8String("/data/local/tmp/status"), 0);
} else if (this.fname.indexOf("/proc/" + pid + "/stat") != -1) {
fake_fd = open(Memory.allocUtf8String("/data/local/tmp/stat"), 0);
} else if (this.fname.indexOf("/proc/" + pid + "/wchan") != -1) {
fake_fd = open(Memory.allocUtf8String("/data/local/tmp/wchan"), 0);
}

console.warn(this.fname, "描述符 ->", retval);
retval.replace(ptr(fake_fd));
console.error("\t替换为 ->", ptr(fake_fd));
}
},
});

["read", "pread", "readv"].forEach((fnc) => {
Interceptor.attach(Module.findExportByName(null, fnc), {
onEnter: function (args) {
var fd = args[0];
if (fd in fds) {
//console.log(`${fnc}: ${fds[fd]}\t`);
}
},
});
});
}

绕过frida检测

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
function bypass_frida_detection() {
// cond1: 通过关键词查询是否包含 frida/xposed
Interceptor.attach(Module.findExportByName("libc.so", "strstr"), {
onEnter: function (args) {
var haystack = args[0];
var needle = args[1];
this["frida"] = Boolean(0);

haystack = Memory.readUtf8String(haystack);
needle = Memory.readUtf8String(needle);

if (haystack.indexOf("frida") != -1 || haystack.indexOf("xposed") != -1) {
// console.log("detecting:", haystack, " -> ", needle)
// console.log('so 调用链:\n' + Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join('\n'), 'white', 'stacktrace');

this["frida"] = Boolean(1);
}
},

onLeave: function (retVal) {
if (this.frida) {
retVal.replace(0);
}
return retVal;
},
});

// cond2: 避免打开 /proc/self(pid)/maps,发现frida-agent-32.so
Interceptor.attach(Module.findExportByName("libc.so", "fopen"), {
onEnter: function (args) {
var name = args[0].readCString();
this["name"] = name;
console.log(name);
},

onLeave: function (retVal) {
var name = this["name"];
if (name.endsWith("/maps")) {
// todo...
//retVal.replace(0x0)
}

return retVal;
},
});
}

frida原理

frida原理

frida的Android实现是frida-java-bridge,借助它可以获取创建JavaVm、获取JNIEnv从而实现创建java对象、创建java类、java方法hook等操作。

Dalvik hook是先把自定义的js代码转换为了native代码,然后把要hook的java函数变成native函数,并修改函数的入口为自定义的内容。

ART Hook常见方法也是先把自定义的js代码转换为了native代码,然后替换方法的入口点,即ArtMethod的entry_point_from_quick_compiledcode,并将原方法的信息备份到entry_point_fromjni,替换后的入口点,会重新准备栈和寄存器,执行hook的方法。

可以参考Frida源码分析frida源码阅读之frida-java

参考


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!