0x00 测试用例

本次的hook代码都用 frida-tools方式 书写。首先写一个简单的程序用来测试。后续的测试就在这个程序上小修小改,不做赘述。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/btn_print"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="print()" />

<Button
android:id="@+id/btn_add"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:text="addThreeNum()" />

</LinearLayout>
package com.zyc.fridasodemo;

import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button btnPrint;
private Button btnAddThreeNum;

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

btnPrint = findViewById(R.id.btn_print);
btnPrint.setOnClickListener(this);
btnAddThreeNum = findViewById(R.id.btn_add);
btnAddThreeNum.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_print:
String print = Calc.print();
Toast.makeText(this, print, Toast.LENGTH_SHORT).show();
break;
case R.id.btn_add:
int add = Calc.addThreeNum(1, 2, 3);
Toast.makeText(this, String.valueOf(add), Toast.LENGTH_SHORT).show();
break;
}
}
}
package com.zyc.fridasodemo;

public class Calc {
static {
System.loadLibrary("native-lib");
}

public native static String print();

public native static int addThreeNum(int a, int b, int c);
}
#include <jni.h>
#include <string>

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Tag", __VA_ARGS__)

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zyc_fridasodemo_Calc_print(JNIEnv *env, jclass clazz) {
const char *str = "Hello c++";
return env->NewStringUTF(str);
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_zyc_fridasodemo_Calc_addThreeNum(JNIEnv *env, jclass clazz, jint a, jint b, jint c) {
return a + b + c;
}

0x01 遍历导入/导出函数

首先通过ida查看so的导入函数

img

导出函数:

img

hook so的导入/导出函数需要分别用到 Module.enumerateImports()Module.enumerateExports() 两个函数。

function hookImEx() {
console.log("以下是导入函数:");
var importMethods = Module.enumerateImports("libnative-lib.so");
for (let index = 0; index < importMethods.length; index++) {
const element = importMethods[index];
console.log(JSON.stringify(element));
}

console.log("以下是导出函数:");
var exportMethods = Module.enumerateExports("libnative-lib.so");
for (let index = 0; index < exportMethods.length; index++) {
const element = exportMethods[index];
console.log(JSON.stringify(element));
}
}

待so加载好后运行,可以从下列对象的name、address取到函数名和地址:

img

0x02 通过函数名hook导出函数

function hookByName() {
const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
if (address) {
Interceptor.attach(address, {
onEnter(args) {
console.log("onEnter...");
console.log("args[0]:",args[0]);
console.log("args[1]:",args[1]);
console.log("args[2]:",args[2]);
console.log("args[3]:",args[3]);
console.log("args[4]:",args[4]);
},
onLeave(retval) {
console.log("onLeave...");
console.log("retval:",retval); // 函数返回值
}
});
}
}

待so加载好后运行,其中arg[0]为 JNIEnv * 地址,arg[1]为 jclass地址(如果是非静态函数,则是jobject)。后面为三个int,使用 .toInt32 可以转为10进制。

img

0x03 Hook参数

根据上面获取的 args 我们可以修改参数,但注意参数得是 NativePointer,直接用 “=” 赋值会报错“expected a pointer”。

function hookParam(){
const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
if (address) {
Interceptor.attach(address, {
onEnter(args) {
args[2] = ptr(1000); // 写成 args[2]=1000 会报错
},
onLeave(retval) {}
});
}
}

运行,点击可以看到返回值已经改变:

img

0x04 Hook返回值

修改返回值时,用 “=” 并不会报错,但这样的修改不会影响程序变量,如:

onLeave(retval) {
retval = 888;
console.log("retval:",retval);
}

效果:

img

正确写法需要使用 replace() 函数:

function hookReturn(){
const address = Module.findExportByName("libnative-lib.so", "Java_com_zyc_fridasodemo_Calc_addThreeNum");
if (address) {
Interceptor.attach(address, {
onEnter(args) {},
onLeave(retval) {
retval.replace(32); //用 replace() 不要用 =
console.log("retval:",retval);

// 如果返回值是jstring,得用下面方式替换
//var env = Java.vm.getEnv(); //获取env对象,即第一个参数
//var jstrings = env.newStringUtf("xxxx"); //返回的是字符串指针,使用jni函数构造一个newStringUtf对象用来代替这个指针
//retval.replace(jstrings);
}
});
}
}

img

0x05 hook引用传递参数值

引用传递是C++常见函数写法,这样是没有返回值的。

//char* str = "hello"; //这样写会报错,str指向静态存储区不允许修改
char str[] = "hello";

void change(char* str){
cs[0] = '1';
cs[1] = '2';
cs[2] = '3';
}

只要Hook onEnter()onLeave() 时的指针(参数),就能知道函数的作用:

function hookPoint() {
const address = Module.findExportByName("libnative-lib.so", "_Z6changePc");
if (address) {
console.log("\r\n函数地址:" + address);
Interceptor.attach(address, {
onEnter(args) {
console.log("onEnter...");
console.log("引用参数:" + args[0].readCString()); //打印cstring
console.log("参数处内存:\r\n" + hexdump(args[0]));
this.args0 = args[0]; //保存参数,给onLeave()中使用
},
onLeave(retval) {
console.log("onLeave...");
console.log("引用参数:" + this.args0.readCString());
console.log("参数处内存:\r\n" + hexdump(this.args0));
}
});
}
}

img

0x06 获取so基址

使用 findBaseAddress() 可以hook到so的基址:

function hookBaseAddress() {
const address = Module.findBaseAddress("libnative-lib.so");
if (address) {
console.log(address); //这里我获取到的是 0xb8f91000
}
}

使用 cat /proc/(进程pid)/maps 命令可以验证libnative-lib.so基址。

img

0x07 hook未导出函数

如果遇到so中动态注册函数的情况,又该如何Hook?在测试用例中增加一个动态注册的printDynamic()。

// public native static String printDynamic(String a); //com.zyc.fridasodemo.Calc类增加此方法

// static + JNI_OnLoad动态注册,ida看不到导出
static jstring print_Dynamic(JNIEnv *env, jclass clazz, jstring a){
return a;
}

static JNINativeMethod methods[] = {
{"printDynamic","(Ljava/lang/String;)Ljava/lang/String;",(void*)print_Dynamic},
};

static int registerNatives(JNIEnv *env) {
//找到声明native方法的类
const char* className = "com/zyc/fridasodemo/Calc";
jclass clazz = env->FindClass(className);
if(clazz == NULL){
return JNI_FALSE;
}

//注册函数 参数:java类 所要注册的函数数组 注册函数的个数
int methodsNum = sizeof(methods)/ sizeof(methods[0]);
if(env->RegisterNatives(clazz,methods,methodsNum) < 0){
return JNI_FALSE;
}
return JNI_TRUE;
}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
JNIEnv* env = NULL;
//获取JNIEnv
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return -1;
}

assert(env != NULL);

if(!registerNatives(env)){
return -1;
}
//返回jni 的版本
return JNI_VERSION_1_6;
}

打release包后用ida检查so的导出函数发现并没有get_two_num()。

img

通过 JNI_OnLoad() 找到其地址:

img

然后就可以用 基址+偏移 方式进行Hook了:

function hookDynamic() {
const soAddress = Module.findBaseAddress("libnative-lib.so");
console.log("so基址:" + soAddress);
if (soAddress) {
const methodAddress = soAddress.add(0x0690); // thrumb指令集要+1
if (methodAddress) {
console.log("函数地址:" + methodAddress);
Interceptor.attach(methodAddress, {
onEnter(args) {
console.log("hook动态注册函数的参数:" + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()); //打印jstring要用getStringUtfChars
},
onLeave(retval) {
console.log("hook动态注册函数的返回值:" + Java.vm.getEnv().getStringUtfChars(retval, null).readCString());
}
});
}
}
}

运行成功:

img****

0x08 Hook so加载(dlopen)

当遇到“等so加载完后进行某Hook操作时”,可以Hook dlopen(有的是android_dlopen_ext)方法来确定so的加载。PS:不推荐Hook System.loadLibrary(),因为加载so的方式并不止这一种,而且最终都会调用 **dlopen()**。

function hookDlopen() {
let dlopenAddr = Module.findExportByName(null, "dlopen");
if (dlopenAddr) {
Interceptor.attach(dlopenAddr, {
onEnter(args) { // dlopen(const char* filename, int flags)
let soName = args[0].readCString(); // "/data/app/com.zyc.fridasodemo-1/lib/x86/libnative-lib.so"
if (soName.indexOf("libnative-lib.so") != -1) {
console.log("成功加载到了-->" + soName);
this.hasloaded = true;
}
},
onLeave(retval) { //onLeave()中才是dlopen()加载完成后
if (this.hasloaded) {
hooImEx(); // 等so加载完成就执行导入/导出函数打印
}
}
});
}
}

运行,点击按钮导致so加载,Hook到加载触发导入/导出函数打印:

img

0x09 读写内存

通过内存地址我们可以直接操作内存数据,以下面字符串为例,这是用例中print()返回的字符串:

img

直接Hook操作这段内存修改:

function hookMem() {
const soAddress = Module.findBaseAddress("libnative-lib.so");
console.log("so基址:" + soAddress);
if (soAddress) {
console.log("读取...");
const strAddress = soAddress.add(0x0D40);
console.log("打印这段字符串:" + strAddress.readCString());
console.log("读16字节:");
console.log(strAddress.readByteArray(16));

console.log("写入...");
Memory.protect(strAddress, Process.pageSize, "rw-"); //修改内存页属性后再写入,不然可能报access violation accessing
strAddress.writeUtf8String("123") //内存写入字符串,该方法末尾会自动添加'\0'
console.log("\r\n打印这段字符串:" + strAddress.readCString());
console.log("读16字节:\r\n");
console.log(strAddress.readByteArray(16));
}
}

img

也可以按照字节修改:

strAddress.writeByteArray([0x41,0x41,0x41]);

img

0x10 Hook JNI函数

很多时候我们需要通过hook JNI函数来达到目的,比如Hook RegisterNatives()拿到动态注册的函数,或是NewStringUTF()查看加解密字符串的构建等,实现JNI的hook可以使用下面两种方法:

偏移计算:拿到JNIEnv结构体的地址,加上函数在结构体中的偏移即可。

libart.so:从libart.so中遍历出要找到的JNINativeMethod。

通过偏移计算

首先把jni.h中JNINativeInterface结构体声明格式化成单行形式(点此下载),如果要hook NewStringUTF(),从文件中找到其在168行 :

img

则其偏移地址为 env指向地址 + (168-1) x 指针大小 ,以此写出:

function hookJni() {
Java.perform(function () { //这样才能取到env,否则为null
const envAddr = ptr(Java.vm.tryGetEnv().handle);
console.log("env地址:" + envAddr);
const envPointAddr = envAddr.readPointer();
console.log("env指向地址:" + envPointAddr);

const envPointAddr168 = envPointAddr.add((168 - 1) * Process.pointerSize);
console.log("env偏移168地址:" + envPointAddr168);
const newStringUtfAddr = envPointAddr168.readPointer();
console.log("jni函数newStringUtf地址:" + newStringUtfAddr);

Interceptor.attach(newStringUtfAddr, {
onEnter(args) {
console.log("附加到newStringUtf函数...");

let arg1 = args[1].readCString();
console.log("NewStringUTF的args[1]:" + arg1);
},
onLeave(retval) {
console.log("附加到newStringUtf函数返回...");

let ret = Java.vm.getEnv().getStringUtfChars(retval, null).readCString();
console.log("返回值:" + ret);
}
});
});
}

img

通过libart.so

使用 Module.enumerateSymbols() 遍历JNI 函数,假设我们要hook RegisterNatives():

/*
* typedef struct {
* const char* name;
* const char* signature;
* void* fnPtr;
* } JNINativeMethod;
*/

function hookJniByArtSym() {
var artSymbol = Module.enumerateSymbols("libart.so");
if (artSymbol) {
for (let i = 0; i < artSymbol.length; i++) {
const element = artSymbol[i];
if (element.name.indexOf("RegisterNatives") != -1) { //函数名有干扰字符,所以用indexOf而不是==,同时要排除CheckJNI函数
console.log("拿到了函数:" + element.name);
console.log("函数地址:" + element.address);

Interceptor.attach(element.address, {
onEnter(args) {
console.log("附加到RegisterNatives()...");

let JNINativeMethod = args[2]; // jstring print_Dynamic(JNIEnv *env, jclass clazz, jstring a)
console.log("动态注册的函数名称:" + JNINativeMethod.readPointer().readCString());
console.log("动态注册的函数签名:" + JNINativeMethod.add(Process.pointerSize).readPointer().readCString());
console.log("动态注册的函数地址:" + JNINativeMethod.add(Process.pointerSize*2).readPointer().readCString());
},
onLeave(retval) { }
});
}
}
}
}

运行发现 Module.enumerateSymbols() 一直获取不到值,查看官网才知道:

img

换个环境来运行就OK了:

img

0x11 主动调用

主动调用Native函数需要用到 NativeFunction(address, returnType, argTypes[, abi]) ,其中returnType类型如下:

void
pointer
int
uint
long
ulong
char
uchar
size_t
ssize_t
float
double
int8
uint8
int16
uint16
int32
uint32
int64
uint64
bool

写一个测试函数方便frida调用:

jstring go(JNIEnv *env,jstring a){
const char *str1 = "Hello c++ ";
const char *str2 = env->GetStringUTFChars(a,0);
const char * ret = (std::string(str1) + std::string(str2)).c_str();
LOGE("go %s",ret);
return env->NewStringUTF(ret);
}

通过函数地址和签名构建 NativeFunction

function hookForward() {
Java.perform(function () {
const soAddr = Module.findBaseAddress("libnative-lib.so");
const methodAddr = soAddr.add(0x9540); //ida静态分析查看到go()偏移0x9540
if (methodAddr) {
const fun = new NativeFunction(methodAddr, "pointer", ["pointer", "pointer"]); //jstring , env* 都是pointer
const env = Java.vm.tryGetEnv();
let jstr = env.newStringUtf("zyc zyc");
let ret = fun(env, jstr);
console.log("返回值:", env.getStringUtfChars(ret, null).readCString());
}
});
}

运行:

img

0x12 写入文件

写入文件其实就是主动调用 libc.so 中文件操作相关函数(记得留意apk的读写权限):

FILE *fopen(const char *filename, const char *mode);
int fputs(const char *str, FILE *stream);
int fclose(FILE *stream)

Hook代码:

function hookWriteFile() {
//拿到文件相关函数地址
const addr_fopen = Module.findExportByName("libc.so", "fopen");
const addr_fputs = Module.findExportByName("libc.so", "fputs");
const addr_fclose = Module.findExportByName("libc.so", "fclose");
if (addr_fopen && addr_fputs && addr_fclose) {
//通过地址构建函数
const fopen = new NativeFunction(addr_fopen, "pointer", ["pointer", "pointer"]);
const fputs = new NativeFunction(addr_fputs, "int", ["pointer", "pointer"]);
const fclose = new NativeFunction(addr_fclose, "int", ["pointer"]);

//打开文件
console.log("打开文件...");
let filename = Memory.allocUtf8String("/data/local/tmp/zyc.txt");
let open_mode = Memory.allocUtf8String("w");
let file = fopen(filename, open_mode);

//写入内容
console.log("写入内容...");
let content = Memory.allocUtf8String("zyc zyc\n");
let retval = fputs(content, file);

//关闭文件
console.log("关闭文件...");
fclose(file);
}
}

img

0x13 打印堆栈

Native中增加几个函数:

void c() {}
void b() { c(); }
void a() { b(); }

extern "C"
JNIEXPORT jstring JNICALL
Java_com_zyc_fridasodemo_Calc_print(JNIEnv *env, jclass clazz) {
a();
...
}

onEnter()使用 Thread.backtrace() 获取堆栈信息:

function hookBacktrace() {
const address = Module.findExportByName("libnative-lib.so", "_Z1cv"); //内存中 c() -- _Z1cv()
if (address) {
Interceptor.attach(address, {
onEnter(args) {
console.log('c() called from:\n' +
Thread.backtrace(this.context, Backtracer.ACCURATE)
.map(DebugSymbol.fromAddress).join('\n') + '\n'); //map与join用于格式化
},
onLeave(retval) {}
});
}
}

运行,然后hook print()函数

img