Frida-Hook

Frida-Hook

Frida

Frida常用模块

Frida文档

Frida是一种动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为,但是持久化还是要依靠Xposed和HookZz开发框架。使用Frida导出的API和方法堆内存空间里的对象方法进行监视、修改或者替换的一段代码

我们可以直接编写JS脚本,然后命令行模式(CLI)运行,也可以使用Python进行JS脚本的注入工作(RPC模式)

Frida命令行提示

1
2
3
4
5
6
7
8
9
setTimeout(
function(){//匿名函数
Java.perform(function(){//调用Frida的API函数Java.perform(),然后这个API函数又接受了一个匿名方法,匿名方法调用打印函数
console.log("hello world")
})
}
)

//调用setTimeout()方法将函数注册到JS运行库,然后在JS运行库中调用Java.perform()方法将函数注册到APP的Java运行库并在这之中执行该函数

首先保证frida-server在手机上运行,然后使用frida-ps -U查看手机上正在运行的进程,接着在计算机的shell中执行frida -U -l testFrida.js Easy_Xor命令以attach模式注入指定应用,-l表示脚本路径

JS匿名函数

Frida Hook Java类

首先我们编写一个简单的APK

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MainActivity extends AppCompatActivity {

@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();
}
func(50,30);
}
}
public void func(int x,int y){
String TAG="MainActivity";
Log.d(TAG, String.valueOf(x+y));
}
}

每隔一秒就打印一次50+30的结果

我们的目标是编写Hook func()函数并打印出func()函数的参数值,也就是Hook到func函数之后执行我们的Hook代码,然后再调用func函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function main(){
console.log("Script Loaded Successfully")
Java.perform(function(){
console.log("Inside Java Perform Function")
var MainActivity=Java.use('com.flag.test1.MainActivity')
console.log("Find Class")//定位类成功
MainActivity.func.implementation=function(x,y){
console.log("x => ",x,"y => ",y)
var ret_value=this.func(x,y);
return ret_value
}
})
}
setImmediate(main)

下面截图可以看到参数值被成功打印

使用main函数存放Hook脚本,然后调用Frida的API函数Java.perfrom()将脚本的内容注入到Java运行库,匿名函数内容是监控和修改Java函数逻辑的主体内容,所有对App中Java层的操作都必须包裹在Java.perfrom()函数

匿名函数中,调用Frida的API函数Java.use(),这个函数饿参数是Hook的函数所在类的全类名路径,参数的类型是一个字符串类型。这个函数的返回值 动态 地为相应Java类获取一个JavaScript Wrapper,可以通俗地理解为一个JavaScript对象。当获取到JavaScript对象之后,通过".“来获取我们要Hook的方法,implementation关键字表示实现MainActivity对象的func()方法。通过”="连接一个匿名函数,参数内容和原Java内容一致,JS不需要指明参数类型,匿名函数的内容取决于想要修改这个Hook函数的逻辑。通过this.fun()函数再次调用原函数,然后把原来的参数传递进这个func()函数,最后通过return返回

setImmediate(Frida的API函数)函数传递的参数是要被执行 的函数,比如传入main参数,表示当Frida注入App后立即执行 main()函数,而setTimeout一般用于延时注入

在Hook一个函数时,还有一个地方需要注意,那就是最好不要 修改被Hook的函数的返回值类型,否则可能会引起程序崩溃等问 题,比如直接通过调用原函数将原函数的返回值返回

我们可以在调用函数时修改参数,此时使用adb logcat即可发现Log.d结果改变

重载方法的Hook实现

方法重载

Hook时只需要指定函数签名即可

对于Hook另一个方法

Java层主动调用

主动调用分为实例方法和类函数

类函数和实例方法通俗地讲就是静态的方法和动态的方法,类函数使用关键字static修饰,和对应类是绑定的,也就是当类的字节码文件加载到内存时,类函数的入口地址就会分配完成。而实例方法只有当类对象创建之后才分配入口地址。从而实例方法可以被类创建的对象调用

如果是类函数主动调用,直接使用Java.use()函数找到类进行调用即可,如果是实例方法的主动调用,则需要再找到对应的实例后对方法进行调用,Java.close()函数可以在Java的堆中寻找指定类的实例

我们在APK中写一个类方法和实例方法

接下来写代码即可

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
function main(){
console.log("Script Run Successfully")
Java.perform(function(){
console.log("Java Internal")
//静态方法主动调用
//找到要主动调用静态方法所在的类
var MainActivity=Java.use('com.flag.test1.MainActivity')
console.log("Find Class")
MainActivity.secret()
//实例方法主动调用
//第一个参数是类的全包名、第二个参数是相应类的对象,然后进行主动调用、第三个表示调用完成

//第二种方法
//获取到类之后直接new一个实例
//let object=Class.$new()
//调用实例方法
//object.method()
Java.choose('com.flag.test1.MainActivity',{
onMatch:function(instance){
console.log("instance found",instance)
instance.secret_instance()
},
onComplete:function(){
console.log('search Complete')
}
})
})
}

setImmediate(main)

类方法主动调用

实例方法主动调用,我们也可以直接new一个实例然后进行调用实例方法

Frida Hook Native层

也是分为两种情况

一种是静态注册,一种是动态注册,**在JNI逆向过程中首先需要找到Java层函数在native层中对应的函数地址,然后再进行Hook。 Android系统为了快速找到JNI函数在native层的对应函数, 因而其命名规则都是固定的,并且在内存中也是不会发生变化的 **

逆向流程

使用Objection注入进程之后,使用memory list modules查看so文件是否被注入,so文件在内存中的加载基址、文件大小以及文件目录都打印出来了

接下来查看所有导出符号 memory list exports libfrida_so.so,其中有类型、符号以及起始地址

我们可以找到StringFromJNI这个函数的起始地址,**在Android中每次加载模块时基地址都是变化的,最终会导致每 次加载后的函数地址也是不同的,真正可靠的是函数地址相对于模块基地址的偏移 **,使用当前函数起始地址减去模块加载地址即可

如果在cpp文件中没有声明函数为extern “C”,导出时会进行重载,我们可以使用C++ filt工具对函数名进行还原

native层Hook基础

so层常用API

ida结构体创建和导入

ida对so层的分析

so层分析

native层导出函数

下面是Frida Hook native层函数的模板

1
2
3
4
5
6
7
8
Interceptor.attach(addr,{
onEnter(args){
//do something with args
},
onLeave(ret){
//do something with ret
}
})

实现native层Hook得API函数是Interceptor.attach()函数,他的第一个参数是要Hook得函数地址,第二个参数是callback回调,在这个函数中存在两个函数,onEnter()函数是在函数调用前产生的回调,在这个函数中可以处理函数参数的相关内容,被Hook的函数参数内容以数组的方式存储在onEnter()函数的参数args中,onLeave()函数则是在被Hook的目标函数执行完成之后执行的函数,被Hook的函数返回值用onLeave()函数中的ret变量来表示,简单来说就是一个处理函数参数,一个处理返回值

关键的一步就是要获取Hook函数的首地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//如果是导出函数,那么可以通过下面两个API函数获得相应函数的首地址,第一个参数是模块名或者null(如果为null,会在所有模块扫描导出符号名),第二个参数是目标函数的导出符号名
Module.getExportByName(moduleName|null,exportName)
Module.findExportByName(moduleName|null,exportName)

Mudule.enumearteExports();//获取导出函数表
//获取导入表和导出表都是可以的
var Exports=Module.enumerateExports();
for(var i=0;i<Exports.length;++i){
console.log(Exports[i].name+" "+Exports[i].address);//获取导出函数
}
//获取到函数的基址之后即可完成Hook和调用
//也可以通过getExportByName传入函数名进行获取

Process.enumerateModules();//获取所有apk加载的so文件

hexdump(addr);//获取内存中的值

为了将返回值的内容打印出来,调用了

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
function hook_native(){
console.log("=====Hooking Native=====")
//get exportFunction Addr
var addr=Module.getExportByName("libfrida_so.so","Java_com_flag_frida_1so_MainActivity_stringFromJNI")
console.log(addr)
//Hook Function
Interceptor.attach(addr,{
onEnter: function(args){

//print args
console.log("JniEnv pointer => ",args[0])
console.log("Jobj pointer => ",args[1])
},
onLeave: function(retval){
//get return value
console.log("Retutn Value => ",Java.vm.getEnv().getStringUtfChars(retval,null).readCString())
console.log("====================")
}
})
}


function main(){
hook_native()
}
setImmediate(main)

首先使用Java.vm.getEnv()获取当前线程的JNIEnv结构,并参照JNI函数GetStringUtfChars()获取到的Java的字符串对用的C字符首地址,获取到C字符串指针后,通过readCString()这个API函数读取内存中的C字符串

为了能够保证stringFromJNI()函数的执行在完成Hook后执行, 这 里 先 在 MainActivity 类 的 onCreate() 函 数 中 加 上 一 个 对 stringFromJNI()函数的循环调用

native未导出函数

由于每次App重新运行后native函数加载的绝对地址,而Frida也为我们提供了获取基址的函数,Module.findBaseAddress(name)或者Module.getBaseAddress(name)获取对应模块的及地址,然后通过add(offset)函数传入固定的便宜来获取最后函数的绝对地址

下面是动态注册的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
jstring JNICALL stringFromJNI(JNIEnv*env,jobject MainActivity){
std::string hello="Hello from C++ Firda_so";
return env->NewStringUTF(hello.c_str());//返回字符串
};

jint JNI_OnLoad(JavaVM*vm,void*reserved){
JNIEnv* env;
vm->GetEnv((void**)&env,JNI_VERSION_1_6);//获取JVM中的线程信息
//创建一一对应的函数表
JNINativeMethod methods[]={
{"stringFromJNI","()Ljava/lang/String;",(void*)stringFromJNI},
};
env->RegisterNatives(env->FindClass("com/flag/frida_so/MainActivity"),methods,1);
return JNI_VERSION_1_6;//返回JNI版本
}

frida_hook_libart,这个项目包含着对JNI函数和art相关函数的Frida_Hook脚本,这里要使用的是RegisterNatives()函数的Hook脚本

在github下载下来发现不会打印偏移,去网上找了一个可以打印offset的代码

计算offset的代码

也可以直接通过so文件获取

为了能顺利地在RegisterNatives()函数未被调用前Hook到,需要使用spwan模式(即将APP的启动权限交给Frida,Frida会重启程序)注入进程

此时StringFromJNI函相对so文件的偏移为

获取到函数偏移之后即可使用查找函数地址的方式获取相应函数地址进行Hook,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function main(){
//获取so文件的模块基址
var native_addr=Module.findBaseAddress('libfrida_so.so')
console.log('native_addr is =>',native_addr)
var stringfromJNi=native_addr.add(0xf11c)
console.log("StringFromJNI address => ",stringfromJNi)
//获取函数的地址

//获取到之后即可进行Hook
Interceptor.attach(stringfromJNi,{
onEnter:function(args){
console.log("jnienv pointer => ",args[0])
console.log("jobj pointer => ",args[1])
},
onLeave:function(retval){
console.log("retval => ",Java.vm.getEnv().getStringUtfChars(retval,null).readCString())
console.log("=================")
}
})

}
setImmediate(main)

native主动调用和替代函数

native函数替代

Interceptor.replace

1
2
3
4
5
6
7
8
9
10
11
Interceptor.replace(addr,NativeCallback);
//其中addr是替换函数的地址
//NativeCallback是一个回调函数

Interceptor.replace(this.context.x2,new NativeCallback(function(a1){
console.log("Replace Success");
return;
},'void',[]));

//NativeCallBack(replace_function,Return_Type,Arrgument_List);第一个参数是替换之后的函数,第二个参数是函数的返回值类型,第三个参数是参数列表,[]表示为空
//addr必须是一个NativePointer

主动调用

感觉和replace差不多,得到需要主动调用的函数的地址之后通过**NativeFunction()**创建一个函数后调用

1
2
3
4
5
NativeFunction(addr,Return_Type,Arrgument_List);

var add_method = new NativeFunction(Module.findExportByName('libhello.so', 'c_getSum'), 'int',['int','int']);
//输出结果 那结果肯定就是 3
console.log("result:",add_method(1,2));

如果传入的参数是结构体

native修改函数的参数和返回值

修改函数的返回值

使用**.replace()**

首先使用Interceptor.attach()拦截到我们想要修改的函数,

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(connect,{
onEnter:function(args){
//args是参数数组,使用args[0]等即可访问
},
onLeave:function(retval){
console.log("Change Success!!");
//使用replace修改参数和返回值
retval.replace(1);//将返回值修改为1表示连接端口成功
}
})

修改数值参数

使用ptr()即可,ptr可以将数值转为NativePointer类型

**toInt32()**将参数转为int型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Interceptor.attach(nativePointer, {
onEnter: function(args){
send(args[0]);
send(args[1]);
send(args[2].toInt32());
send(args[3].toInt32());
send(args[4].toInt32());
args[4] = ptr(1000); //new NativePointer
send(args[4].toInt32());
},
onLeave: function(retval){
send(retval.toInt32());
retval.replace(10000);
send(retval.toInt32());
}
})

修改字符串参数

有多种办法,这里采用将字符串写入内存中的办法来修改

主要用到Memory.allocUtf8String(str),然后直接将地址赋值给参数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
var newStr="new String";
var newstraddr=Memory.allocUtf8String(newStr);//写入内存,返回字符串第一个字符的地址
var strcpy=Module.findExportByName("libc.so","strcpy");
Interceptor.attach(strcpy,{
//对于数值参数的修改,使用ptr()即可
onEnter:function(args){
args[1]=newstraddr;
console.log(args[1].readCString());
},
onLeave:function(retval){
console.log("hello");
}
})

libssl和libc库的Hook

其中会使用到frida-trace

RPC及其自动化

可以使用Python完成JS脚本对进程的注入以及相应的Hook

首先先修改APK代码

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
public class MainActivity extends AppCompatActivity {
private String total="hello";
@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();
}
func(50,30);
func("Jack");
}
}
public void func(int x,int y){
String TAG="MainActivity";
Log.d(TAG, String.valueOf(x+y));
}

public void func(String name){
Log.d("MainActivity",name);
}

void secret(){
total+=" secretFunc";
Log.d("MainActivity","Call Static Secret Method");
}

void secret_instance(){
Log.d("MainActivity","Call Static Secret_Instance Method");
}
}

编写JS脚本获取值(instance.total.value)和主动调用实例方法

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
function RunMethod(){
Java.perform(function(){
Java.choose('com.flag.test1.MainActivity',{
onMatch: function(instance){
instance.secret()
},
onComplete: function(){
}}
)
})
}


function main(){
console.log("Script Run Successfully")
Java.perform(function(){
console.log("Java Internal")
var MainActivity=Java.use('com.flag.test1.MainActivity')
//动态函数主动调用
RunMethod()
Java.choose('com.flag.test1.MainActivity',{
onMatch:function(instance){
//由于是类变量,必须通过对象来获取
console.log('total value=',instance.total.value);
},
onComplete:function(){
console.log("search Complete")
}
})

})

}
setImmediate(main);

接下来我们使用RPC远程调用,首先将两个函数导出,使得外部可以调用,在JS代码末尾加上RPC相关代码

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
import frida
import sys

def on_message(message,data):
if message['type']=='send':
print("[*] {0}".format(message['payload']))
else:
print(message)
#需要设置timeout时间,attach中的内容看firda-ps -U进程内容
#使用get_usb_device获取usb设备句柄,然后attach到需要hook的进程上
process = frida.get_usb_device(3000).attach('test1')
#最好不要有中文,否则GBK编码无法通过
with open('get_value.js') as f:
jscode=f.read()
#读取js脚本文件之后加载编写的JS代码
script=process.create_script(jscode)
#注册了自己的消息对应的函数,每当JS需要输出是都会经过这里指定的on_message进行
script.on('message',on_message)
#加载
script.load()

command=""
#通过script.exports访问在JS中定义的导出名,进而对齐进行调用
while 1==1:
command=input()
if command=="1":
break
elif command=="2":
script.exports.getvalue()
elif command=="3":
script.exports.runmethod()

Frida-InlineHook

也就是Hook一条汇编指令

实际上和Hook函数差不多,都是使用Interceptor.attach,获取到指令地址就可以对其进行操作

1
2
3
4
5
6
7
8
9
10
Interceptor.attach(func2,{
onEnter:function(args){
console.log(this.context.x11);
this.context.x11=0x45;
console.log(this.context.x11);
},
onLeave:function(retval){
console.log(this.context.x11);
}
})

修改汇编指令

首先我们要将地址的权限修改为可读可写可执行(不然有时候会有bug),使用的是Memory.protect()

1
2
Memory.protect(addr,size,'rwx');
//表示将addr处size个字节的属性修改为可读可写可执行

接下来就需要使用写入内存的函数writeByteArray

Frida其他用法

  • hexdump打印内存的数据——可以用于打印加密后的结果和函数参数

    1
    hexdump(address)
  • 打印调用栈(也可以使用Objection)

  • Frida创建Java数组

    使用Java.array()

    1
    2
    var objArray = Java.array("Ljava.lang.String;",["12","2","3","4","4"]);
    //第一个参数表示数组的类型,第二个参数表示数组中的内容
  • Frida读取内存数据并转为字节数组并转为字符串

    有时候我们需要将内存中的数据读取到Hook代码中并进行运算

    使用到的是readByteArray(size)

    1
    2
    addr.readByteArray(size);
    //从addr处读取size大小的字节数组

    接下来使用js自带的Uint8Array来接收

    1
    var bytes= new Uint8Array(addr.readByteArray(size));

    接下来可以使用JS自带的**String.fromCharCode()**将字节转为字符

    1
    2
    3
    4
    5
    var final=''
    for(var i=0;i<24;++i){
    final+=String.fromCharCode(encodeanwser[i]^xorkey[i]);
    }
    console.log(final);

    这样就可以将xor后的结果转为字符串后打印出来

  • Frida从内存中读取字符串

    使用addr.readCString()

    1
    var xorkey = addr1.readCString();

    这样读取到的即为字符串,读取到’\0’为止

  • JS中将字符串转为字节数组

Python实现常见加密

Python中bytes和String的转换

因为加密的数据都需要为字节流,所以要先转为bytes型数据

对于数组直接使用bytes

1
2
b = [0x12, 0x34, 0x56, 0x99]
print(bytes(b))

bytes转为String使用decode()方法即可

String转为bytes使用encode()方法即可

Hash

主要用到的是hashlib,Python自带的

直接通过hashlib.md5()等即可进行加密

1
2
3
4
5
6
import hashlib

enc1 = hashlib.md5(flag)
# hexdigest()转为十六进制格式的字符串
print(enc1.digest())
#degiest()可以将密文转为bytes型数据

提供的加密有

使用起来也是比较方便的,这里以md5为例

1
print(hashlib.md5(b'helloworld').hexdigest())

如果想得到字节流,只需要将hexdigest()改为**digest()**即可

1
hashlib.md5(b'helloworld').digest()

对称加密

1
2
3
4
5
6
from Crypto.Cipher import AES
from Crypto.Cipher import Blowfish
from Crypto.Cipher import ARC4
from Crypto.Cipher import Salsa20
from Crypto.Cipher import ChaCha20
from Crypto.Cipher import DES

使用的方法也是一致的,首先先new一个对象,需要传入key、指定的Mode、IV

然后通过**.encrypt(flag),.decrypt(enc)**进行解密,注意必须是字节流

1
2
3
4
5
6
# 创建一个AES对象
aes = AES.new(password, AES.MODE_ECB)
# 接着通过aes对象进行加密
enc = aes.encrypt(flag)
print(enc)
print(aes.decrypt(enc).decode())

下面使用AES的CBC模式进行加密

1
2
3
4
5
6
7
8
aes1 = AES.new(password, AES.MODE_CBC, iv)

enc2 = aes1.encrypt(flag)
print(enc2)
# CBC模式下解密需要重新new一个AES对象
aes2 = AES.new(password, AES.MODE_CBC, iv)
# decode将bytes转为String
print((aes2.decrypt(enc2)).decode())

需要注意的是CBC模式下解密需要重新new一个AES对象

Base64编码解码

1
2
import base64
print(base64.b64encode(b'123123'))

也可以指定其他base家族的

Java实现常见加密、编码

bytes和String的互相转换

参考

Base64编码解码