Frida RPC开发姿势
Frida是一款基于Python和JavaScript的进程级Hook框架,其中JavaScript语言承担了Hook函数的主要工作,而Python语言则相当于一个提供给外界的绑定接口,使用者可以通过Python语言将JavaScript脚本注入进程中.官方也在下方仓库中提供了通过Python远程外部调用JavaScript中函数的方式.
https://github.com/frida/frida-python
下面简单介绍下通过Python实现Frida注入的基本方式.
获取设备
1
2
3
4
5
6
7
8
import frida
# 无线连接
# /data/local/tmp/frida-server -l 0.0.0.0:6666
# Wifi ADB监听IP和端口为192.168.2.111:5555
device = frida . get_device_manager () . add_remote_device ( "192.168.2.111:6666" )
# 有线连接
device = frida . get_usb_device ()
注入进程
1
2
3
4
5
6
7
8
9
10
11
import frida
import time
# spawn方式注入进程
pid = device . spawn ([ 'com.example.luodemo' ])
device . resume ( pid )
time . sleep ( 1 )
session = device . attach ( pid )
# attach模式注入进程
session = device . attach ( 'com.example.luodemo' )
注入脚本
1
2
3
4
5
6
7
8
9
import frida
script = session . create_script ( '''
setImmediate(Java.perform(function(){
console.log('hello python frida')
}))
''' ) #读入Hook脚本内容
script . load () #将脚本加载进进程空间中
文件方式注入进程
1
2
3
with open ( "LuoHook.js" , encoding = "UTF-8" ) as f :
script = session . create_script ( f . read ())
script . load ()
测试Apk
下方代码示例中主要写了一个静态的Native函数method01以及动态的实例函数method02.
Java代码:
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
package com.example.luodemo ;
import androidx.appcompat.app.AppCompatActivity ;
import android.os.Bundle ;
import android.util.Log ;
import android.widget.TextView ;
import com.example.luodemo.databinding.ActivityMainBinding ;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "[XiaLuoHun]" ;
// Used to load the 'luodemo' library on application startup.
static {
System . loadLibrary ( "luodemo" );
}
private ActivityMainBinding binding ;
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
binding = ActivityMainBinding . inflate ( getLayoutInflater ());
setContentView ( binding . getRoot ());
// Example of a call to a native method
TextView tv = binding . sampleText ;
tv . setText ( stringFromJNI ());
while ( true ){
try {
Thread . sleep ( 1000 );
} catch ( InterruptedException e ) {
e . printStackTrace ();
}
Log . i ( TAG , method01 ( "LuoHun" ));
Log . i ( TAG , method02 ( method01 ( "LuoHun" )));
}
}
/**
* A native method that is implemented by the 'luodemo' native library,
* which is packaged with this application.
*/
public native String stringFromJNI ();
/**
* AES加密, CBC, PKCS5Padding
*/
public static native String method01 ( String str );
/**
* AES解密, CBC, PKCS5Padding
*/
public native String method02 ( String str );
}
C代码:
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
#include <jni.h>
#include <string>
#include "aes_utils.h"
#include "tools.h"
#include "junk.h"
#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_luodemo_MainActivity_stringFromJNI (
JNIEnv * env ,
jobject /* this */ ) {
std :: string hello = "Hello from C++" ;
return env -> NewStringUTF ( hello . c_str ());
}
jstring JNICALL method01 ( JNIEnv * env , jclass jcls , jstring str_ ) {
if ( str_ == nullptr ) return nullptr ;
const char * str = env -> GetStringUTFChars ( str_ , JNI_FALSE );
char * result = AES_128_CBC_PKCS5_Encrypt ( str );
env -> ReleaseStringUTFChars ( str_ , str );
jstring jResult = getJString ( env , result );
free ( result );
return jResult ;
}
JNIEXPORT jstring JNICALL method02 ( JNIEnv * env , jobject jcls , jstring str_ ) {
if ( str_ == nullptr ) return nullptr ;
const char * str = env -> GetStringUTFChars ( str_ , JNI_FALSE );
char * result = AES_128_CBC_PKCS5_Decrypt ( str );
env -> ReleaseStringUTFChars ( str_ , str );
jstring jResult = getJString ( env , result );
free ( result );
return jResult ;
}
JNIEXPORT jint JNI_OnLoad ( JavaVM * vm , void * reserved ){
JNIEnv * env ;
vm -> GetEnv (( void ** ) & env , JNI_VERSION_1_6 );
JNINativeMethod methods [] = {
{ "method01" , "(Ljava/lang/String;)Ljava/lang/String;" , ( void * ) method01 },
{ "method02" , "(Ljava/lang/String;)Ljava/lang/String;" , ( void * ) method02 },
};
env -> RegisterNatives ( env -> FindClass ( "com/example/luodemo/MainActivity" ), methods , NELEM ( methods ));
return JNI_VERSION_1_6 ;
}
Java层主动调用与RPC
Js 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
function hookMethod01 (){
Java . perform ( function (){
var MainActivity = Java . use ( 'com.example.luodemo.MainActivity' )
MainActivity . method01 . implementation = function ( str ){
var result = this . method01 ( str ) //Hook时的主动调用
console . log ( 'str =>' , str )
console . log ( 'result =>' , result )
return result
}
})
}
function invokeMethod01 ( plainText ){
var result = ''
Java . perform ( function (){
var MainActivity = Java . use ( 'com.example.luodemo.MainActivity' )
var javaString = Java . use ( 'java.lang.String' )
//var plainText = 'LuoHun'
result = MainActivity . method01 ( javaString . $new ( plainText ))
console . log ( 'plainText =>' , plainText )
console . log ( 'result =>' , result )
})
return result
}
function invokeMethod02 ( cipherText ){
var result = ''
Java . perform ( function (){
Java . choose ( 'com.example.luodemo.MainActivity' ,{
onMatch : function ( instance ){
var javaString = Java . use ( 'java.lang.String' )
//var cipherText = '64a91c1b8eedb22b9c28d4b98e16ecc3'
result = instance . method02 ( javaString . $new ( cipherText ))
console . log ( 'cipherText =>' , cipherText )
console . log ( 'result =>' , result )
},
onComplete (){
}
})
})
return result
}
rpc . exports = {
method01 : invokeMethod01 ,
method02 : invokeMethod02
}
// function main(){
// //hookMethod01()
// //invokeMethod01('LuoHun')
// invokeMethod02('64a91c1b8eedb22b9c28d4b98e16ecc3')
// }
// setImmediate(main())
Python调用
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
import frida
import time
import json
def on_message ( message , payload ):
print ( message )
print ( payload )
# 无线连接
# /data/local/tmp/frida-server -l 0.0.0.0:6666
# Wifi ADB监听IP和端口为192.168.2.111:5555
#device = frida.get_device_manager().add_remote_device("192.168.2.111:6666")
# 有线连接
device = frida . get_usb_device ()
# 启动App
pid = device . spawn ([ 'com.example.luodemo' ])
device . resume ( pid )
time . sleep ( 1 )
session = device . attach ( pid )
# 加载脚本
with open ( "LuoHook.js" , encoding = "UTF-8" ) as f :
script = session . create_script ( f . read ())
script . on ( 'message' , on_message ) #调用消息处理
script . load ()
api = script . exports #获取导出函数列表
print ( 'method01 => encode_result: ' + api . method01 ( 'LuoHun' ))
print ( 'method02 => decode_result: ' + api . method02 ( '64a91c1b8eedb22b9c28d4b98e16ecc3' ))
input ()
Native层函数主动调用
Hook静态注册的JNI函数
可以使用IDA或者如下的Objection命令来查看指定库文件的导出符号.
1
memory list exports libluodemo . so -- json Luo . json
以Hook stringFromJNI函数为例.
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
function hookMethod (){
var stringFromJNI = Module . findExportByName ( 'libluodemo.so' , 'Java_com_example_luodemo_MainActivity_stringFromJNI' )
//var libluodemo_addr = Module.findBaseAddress("libluodemo.so")
//var stringFromJNI = libluodemo_addr.add(0x1FBE0) //打开IDA看偏移
Interceptor . attach ( stringFromJNI , {
onEnter : function ( args ){
//console.log('Enter stringFromJNI')
},
onLeave : function ( retval ){
var env = Java . vm . getEnv ()
//构造一个新字符串
var jstrings = env . newStringUtf ( "Hello XiaLuoHun" );
//替换返回值
retval . replace ( jstrings );
}
})
}
function hook_dlopen () {
Interceptor . attach ( Module . findExportByName ( null , "dlopen" ), {
onEnter : function ( args ) {
var pathptr = args [ 0 ];
if ( pathptr !== undefined && pathptr != null ) {
var path = ptr ( pathptr ). readCString ();
//console.log("dlopen:", path);
}
},
onLeave : function ( retval ) {
}
})
Interceptor . attach ( Module . findExportByName ( null , "android_dlopen_ext" ), {
onEnter : function ( args ) {
var pathptr = args [ 0 ];
if ( pathptr !== undefined && pathptr != null ) {
var path = ptr ( pathptr ). readCString ();
//console.log("android_dlopen_ext:", path);
if ( path . indexOf ( "libluodemo.so" ) >= 0 ) {
this . target = true ;
}
}
},
onLeave : function ( retval ) {
if ( this . target ) {
hookMethod ()
}
}
});
}
setImmediate ( hook_dlopen );
Hook动态注册的JNI函数
首先使用下方仓库中的hook_RegisterNatives.js脚本来获取动态注册后函数的所在地址.
https://github.com/lasting-yang/frida_hook_libart
可写出Hook动态注册的Native函数method01代码如下:
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
function hookMethod (){
var libluodemo_addr = Module . findBaseAddress ( "libluodemo.so" )
var method01 = libluodemo_addr . add ( 0x1fd64 )
//方式一
// Interceptor.attach(method01, {
// onEnter:function(args){
// console.log('args[2] =>', Java.vm.getEnv().getStringUtfChars(args[2], null).readCString())
// },
// onLeave:function(retval){
// console.log('retval =>', Java.vm.getEnv().getStringUtfChars(retval, null).readCString())
// }
// })
//方式二
var addr_func = new NativeFunction ( method01 , 'pointer' , [ 'pointer' , 'pointer' , 'pointer' ])
Interceptor . replace ( method01 , new NativeCallback ( function ( arg0 , arg1 , arg2 ){
var result = addr_func ( arg0 , arg1 , arg2 ) //Hook中的主动调用
console . log ( 'arg2 =>' , Java . vm . getEnv (). getStringUtfChars ( arg2 , null ). readCString ())
console . log ( 'result =>' , Java . vm . getEnv (). getStringUtfChars ( result , null ). readCString ())
return result
}, 'pointer' , [ 'pointer' , 'pointer' , 'pointer' ]))
}
function hook_dlopen () {
Interceptor . attach ( Module . findExportByName ( null , "dlopen" ), {
onEnter : function ( args ) {
var pathptr = args [ 0 ];
if ( pathptr !== undefined && pathptr != null ) {
var path = ptr ( pathptr ). readCString ();
//console.log("dlopen:", path);
}
},
onLeave : function ( retval ) {
}
})
Interceptor . attach ( Module . findExportByName ( null , "android_dlopen_ext" ), {
onEnter : function ( args ) {
var pathptr = args [ 0 ];
if ( pathptr !== undefined && pathptr != null ) {
var path = ptr ( pathptr ). readCString ();
//console.log("android_dlopen_ext:", path);
if ( path . indexOf ( "libluodemo.so" ) >= 0 ) {
this . target = true ;
}
}
},
onLeave : function ( retval ) {
if ( this . target ) {
hookMethod ()
}
}
});
}
setImmediate ( hook_dlopen );
主动调用
从上面Hook的第二种方式,可以得到下述的主动调用代码.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function invoke_func ( addr , contents ){
var result = null
var func = new NativeFunction ( addr , 'pointer' , [ 'pointer' , 'pointer' , 'pointer' ])
Java . perform ( function (){
var env = Java . vm . getEnv ()
var jstring = env . newStringUtf ( contents )
result = func ( env , ptr ( 1 ), jstring )
result = env . getStringUtfChars ( result , null )
})
return result
}
function inmvoke_method01 (){
var base = Module . findBaseAddress ( "libluodemo.so" )
var method01_addr = base . add ( 0x1fd64 )
var result = invoke_func ( method01_addr , 'LuoHun' )
console . log ( 'result =>' , result . readCString ())
}
setImmediate ( inmvoke_method01 );
注意
在JNI函数中,第一个参数一定是JNIEnv的指针,第二个参数取决于对应JNI函数在Java层中是静态还是动态函数,分别对应jclass类型和jobject类型.若第二个参数没有使用则可以任意传递相同类型的数据,否则就需要通过env对象进行构造.
主动加载模块并调用其中的函数
解压Apk,将对应的模块推送到手机的/data/app目录下,并以Root身份赋予所有权限.
1
2
3
4
5
6
7
8
adb push . \ libluodemo . so / data / local / tmp
adb shell
su
mv libluodemo . so / data / app
chmod 777 / data / app / libluodemo . so
//关闭selinux
setenforce 0
加载模块并自动调用的函数代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function invoke_func ( addr , contents ){
var result = null
var func = new NativeFunction ( addr , 'pointer' , [ 'pointer' , 'pointer' , 'pointer' ])
Java . perform ( function (){
var env = Java . vm . getEnv ()
var jstring = env . newStringUtf ( contents )
result = func ( env , ptr ( 1 ), jstring )
result = env . getStringUtfChars ( result , null )
})
return result
}
function inmvoke_method01 (){
var base = Module . load ( '/data/app/libluodemo.so' ). base
var method01_addr = base . add ( 0x1fd64 )
var result = invoke_func ( method01_addr , 'LuoHun' )
console . log ( 'result =>' , result . readCString ())
}
setImmediate ( inmvoke_method01 );