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的导入函数
导出函数:
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取到函数名和地址:
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进制。
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 ); }, onLeave (retval ) {} }); } }
运行,点击可以看到返回值已经改变:
0x04 Hook返回值 修改返回值时,用 “=” 并不会报错,但这样的修改不会影响程序变量,如:
onLeave (retval ) { retval = 888 ; console .log ("retval:" ,retval); }
效果:
正确写法需要使用 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 ); console .log ("retval:" ,retval); } }); } }
0x05 hook引用传递参数值 引用传递是C++常见函数写法,这样是没有返回值的。
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 ()); console .log ("参数处内存:\r\n" + hexdump (args[0 ])); this .args0 = args[0 ]; }, onLeave (retval ) { console .log ("onLeave..." ); console .log ("引用参数:" + this .args0 .readCString ()); console .log ("参数处内存:\r\n" + hexdump (this .args0 )); } }); } }
0x06 获取so基址 使用 findBaseAddress() 可以hook到so的基址:
function hookBaseAddress ( ) { const address = Module .findBaseAddress ("libnative-lib.so" ); if (address) { console .log (address); } }
使用 cat /proc/(进程pid)/maps 命令可以验证libnative-lib.so基址。
0x07 hook未导出函数 如果遇到so中动态注册函数的情况,又该如何Hook?在测试用例中增加一个动态注册的printDynamic()。
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) { const char * className = "com/zyc/fridasodemo/Calc" ; jclass clazz = env->FindClass(className); if (clazz == NULL){ return JNI_FALSE; } 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; if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1 ; } assert (env != NULL); if (!registerNatives(env)){ return -1 ; } return JNI_VERSION_1_6; }
打release包后用ida检查so的导出函数发现并没有get_two_num()。
通过 JNI_OnLoad() 找到其地址:
然后就可以用 基址+偏移 方式进行Hook了:
function hookDynamic ( ) { const soAddress = Module .findBaseAddress ("libnative-lib.so" ); console .log ("so基址:" + soAddress); if (soAddress) { const methodAddress = soAddress.add (0x0690 ); if (methodAddress) { console .log ("函数地址:" + methodAddress); Interceptor .attach (methodAddress, { onEnter (args ) { console .log ("hook动态注册函数的参数:" + Java .vm .getEnv ().getStringUtfChars (args[2 ], null ).readCString ()); }, onLeave (retval ) { console .log ("hook动态注册函数的返回值:" + Java .vm .getEnv ().getStringUtfChars (retval, null ).readCString ()); } }); } } }
运行成功:
****
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 ) { let soName = args[0 ].readCString (); if (soName.indexOf ("libnative-lib.so" ) != -1 ) { console .log ("成功加载到了-->" + soName); this .hasloaded = true ; } }, onLeave (retval ) { if (this .hasloaded ) { hooImEx (); } } }); } }
运行,点击按钮导致so加载,Hook到加载触发导入/导出函数打印:
0x09 读写内存 通过内存地址我们可以直接操作内存数据,以下面字符串为例,这是用例中print()返回的字符串:
直接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-" ); strAddress.writeUtf8String ("123" ) console .log ("\r\n打印这段字符串:" + strAddress.readCString ()); console .log ("读16字节:\r\n" ); console .log (strAddress.readByteArray (16 )); } }
也可以按照字节修改:
strAddress.writeByteArray([0x41,0x41,0x41]);
0x10 Hook JNI函数 很多时候我们需要通过hook JNI函数来达到目的,比如Hook RegisterNatives()拿到动态注册的函数,或是NewStringUTF()查看加解密字符串的构建等,实现JNI的hook可以使用下面两种方法:
偏移计算:拿到JNIEnv结构体的地址,加上函数在结构体中的偏移即可。
libart.so:从libart.so中遍历出要找到的JNINativeMethod。
通过偏移计算 首先把jni.h中JNINativeInterface结构体声明格式化成单行形式(点此下载 ),如果要hook NewStringUTF(),从文件中找到其在168行 :
则其偏移地址为 env指向地址 + (168-1) x 指针大小 ,以此写出:
function hookJni ( ) { Java .perform (function ( ) { 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); } }); }); }
通过libart.so 使用 Module.enumerateSymbols() 遍历JNI 函数,假设我们要hook RegisterNatives():
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 ) { console .log ("拿到了函数:" + element.name ); console .log ("函数地址:" + element.address ); Interceptor .attach (element.address , { onEnter (args ) { console .log ("附加到RegisterNatives()..." ); let JNINativeMethod = args[2 ]; 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() 一直获取不到值,查看官网才知道:
换个环境来运行就OK了:
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 ); if (methodAddr) { const fun = new NativeFunction (methodAddr, "pointer" , ["pointer" , "pointer" ]); const env = Java .vm .tryGetEnv (); let jstr = env.newStringUtf ("zyc zyc" ); let ret = fun (env, jstr); console .log ("返回值:" , env.getStringUtfChars (ret, null ).readCString ()); } }); }
运行:
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); } }
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" ); 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' ); }, onLeave (retval ) {} }); } }
运行,然后hook print()函数