本文讨论如何hook目标apk中的 so 中的函数。
实现 so hook 的一个主要思想,就是将目标apk当成是我们自己编写的就行,就像开发中hook系统调用一样。有了这个思维,思路会宽广许多。
在 Android 中加载一个so会使用到 System.loadLibrary 方法。
在 native 中,一般加载 so 是使用的 dlopen 方法。
由于 System.loadLibrary 最终也会调用到 dlopen 方法,所以我们先看 dlopen 方法流程。
dlopen 会调用到 do_dlopen 里面去:
1937??void*?do_dlopen(const?char*?name,?int?flags,
1938??????????????????const?android_dlextinfo*?extinfo,
1939??????????????????const?void*?caller_addr)?{
1945????...
2011??
2012????ProtectedDataGuard?guard;
2013????soinfo*?si?=?find_library(ns,?translated_name,?flags,?extinfo,?caller);
2014????loading_trace.End();
2015??
2016????if?(si?!=?nullptr)?{
2017??????void*?handle?=?si->to_handle();
2018??????...
2021??????si->call_constructors();
2022??????...
2026??????return?handle;
2027????}
2028??
2029????return?nullptr;
2030??}
核心逻辑就是使用 find_library 方法获取到 so 相关信息,然后调用其 call_constructors 方法。
call_constructors 里面会调用 init 与 init_array 相关代码:
388??void?soinfo::call_constructors()?{
389????...
417??
418????//?DT_INIT?should?be?called?before?DT_INIT_ARRAY?if?both?are?present.
419????call_function("DT_INIT",?init_func_,?get_realpath());
420????call_array("DT_INIT_ARRAY",?init_array_,?init_array_count_,?false,?get_realpath());
421??
422????...
425??}
这就是为啥 init 与 init_array 会在so加载后就执行的原因了。
我们再分析 ?System.loadLibrary 流程,它会调用到 LoadNativeLibrary 方法:
796??bool?JavaVMExt::LoadNativeLibrary(JNIEnv*?env,
797????????????????????????????????????const?std::string&?path,
798????????????????????????????????????jobject?class_loader,
799????????????????????????????????????jstring?library_path,
800????????????????????????????????????std::string*?error_msg)?{
801????...
866????void*?handle?=?android::OpenNativeLibrary(env,
867??????????????????????????????????????????????runtime_->GetTargetSdkVersion(),
868??????????????????????????????????????????????path_str,
869??????????????????????????????????????????????class_loader,
870??????????????????????????????????????????????library_path,
871??????????????????????????????????????????????&needs_native_bridge,
872??????????????????????????????????????????????error_msg);
873??
874????...
915????bool?was_successful?=?false;
916????void*?sym?=?library->FindSymbol("JNI_OnLoad",?nullptr);
917????if?(sym?==?nullptr)?{
918??????VLOG(jni)?<<?"[No?JNI_OnLoad?found?in?\""?<<?path?<<?"\"]";
919??????was_successful?=?true;
920????}?else?{
921??????//?Call?JNI_OnLoad.??We?have?to?override?the?current?class
922??????//?loader,?which?will?always?be?"null"?since?the?stuff?at?the
923??????//?top?of?the?stack?is?around?Runtime.loadLibrary().??(See
924??????//?the?comments?in?the?JNI?FindClass?function.)
925??????ScopedLocalRef<jobject>?old_class_loader(env,?env->NewLocalRef(self->GetClassLoaderOverride()));
926??????self->SetClassLoaderOverride(class_loader);
927??
928??????VLOG(jni)?<<?"[Calling?JNI_OnLoad?in?\""?<<?path?<<?"\"]";
929??????typedef?int?(*JNI_OnLoadFn)(JavaVM*,?void*);
930??????JNI_OnLoadFn?jni_on_load?=?reinterpret_cast<JNI_OnLoadFn>(sym);
931??????int?version?=?(*jni_on_load)(this,?nullptr);
932??
933??????...
959????return?was_successful;
960??}
函数分为两个片段,第一个是 OpenNativeLibrary,它会调用到 dlopen 方法,也就是我们上面分析的过程。
第二个片段,会先找到 JNI_OnLoad 符号,然后调用这个函数,这就是为啥 JNI_OnLoad 会紧跟在 init 与 init_array 后面执行的原因。
有了上面的基础,我们再来介绍一下如何做 so 的 hook。arm 的hook会比较麻烦,但是好在有 github,我们可以使用开源的一些库做到开箱即用。
hook 分两种,一种是 plt hook,一种是 inline hook。这两种先简单介绍,后面我们开篇 elf 的时候在细说。简单来说,plt hook 只能 hook plt 表中存在的函数。inline hook 可以 hook 几乎所有的函数。
看一个例子来理解为啥要有这两种 hook:
extern?"C"
JNIEXPORT?void?JNICALL
Java_com_example_nativehooktarget_MainActivity_hookMe(JNIEnv
??????????????????????????????????????????????????????*env,
??????????????????????????????????????????????????????jobject?thiz
)?{
????if?(test_hook("hookMe"))?{
????????__android_log_print(4,?"hook_so",?"hookMe?success");
????}?else?{
????????__android_log_print(4,?"hook_so",?"hookMe?failed");
????}
}
在同一个文件里面写这两个方法:
Java_com_example_nativehooktarget_MainActivity_hookMe
test_hook
编译后,看其汇编:
95c:???91181800????????add?????x0,?x0,?#0x606
????if?(test_hook("hookMe"))?{
?960:???97ffffa8????????bl??????800?<_Z9test_hookPKc>
?964:???36000100????????tbz?????w0,?#0,?984?<Java_com_example_nativehooktarget_MainActivity_hookMe+0x40>
看到对 test_hook 函数的调用直接使用了绝对地址 800,这样的函数,我们是没有办法使用 plt hook 的,因为 plt 表里面根本就没有这个函数的符号。
本来是想使用 inline hook 框架,字节开源的,虽然肯定比内部的版本号低了些,但是够用了:
https://github.com/bytedance/android-inline-hook
但是发现它不是纯 native 的,还需要在 java 里面初始化,由于 classLoader 的问题,用起来会非常的麻烦。虽然说 xposed 将模块的代码注入了app进程,但是它们还是使用了不同的 classLoader,so的加载也是与 classLoader有关,就会遇见各种奇葩问题。
所以还是使用老版的 sandhook,将代码 copy 进来,在 cmakelists.txt 里面配置,然后编写hook代码:
//
//?Created?by?root?on?12/9/23.
//
#include?"jni.h"
#include?<cstring>
#include?<android/log.h>
#include?"sandhook_native.h"
void?*orig?=?nullptr;
typedef?char?*(*type_t)(char?*,?char?*);
char*?proxy(char?*str1,?char?*str2)?{
????//?invoke?origin?method
????char?*?result?=?((type_t)?orig)(str1,?str2);
????if?(strcmp(str2,?"test_hook")?==?0)?{
????????return?str1;
????}
????__android_log_print(4,?"hook_so",?"proxy?origin?result?%s",?result);
????return?result;
}
void?do_hook_test_hook()?{
????const?char?*libc_path?=?"/system/lib64/libc.so";
????orig?=?SandInlineHookSym(libc_path,?"strstr",?reinterpret_cast<void?*>(&proxy));
????__android_log_print(4,?"hook_so",?"hook?result?%p",?orig);
}
extern?"C"?jint?JNICALL?JNI_OnLoad(JavaVM?*vm,?void?*reserved)?{
????do_hook_test_hook();
????return?JNI_VERSION_1_6;
}
这里我们选择 hook libc 中的 strstr 函数,由于 sandhook 的限制,我们只能将hook时机放在JNI_OnLoad 处。
编译好模块的 so 后,将其 push 到目标 app 的 lib下:
/data/app/com.example.nativehooktarget-prsFV1IVkibTNbuAUhlI-w==/lib/arm64/libsohook.so
这样,我们就可以在模块里面加载这个 so,然后让其加载后自动 hook:
XposedHelpers.findAndHookMethod(
????????????????"java.lang.Runtime",
????????????????loadPackageParam.classLoader,
????????????????"loadLibrary0",
????????????????ClassLoader.class,
????????????????String.class,
????????????????new?XC_MethodHook()?{
????????????????????@Override
????????????????????protected?void?beforeHookedMethod(MethodHookParam?param)?throws?Throwable?{
????????????????????????super.beforeHookedMethod(param);
????????????????????????XposedBridge.invokeOriginalMethod(param.method,?param.thisObject,?new?Object[]?{
????????????????????????????????param.args[0],?"sohook"
????????????????????????});
????????????????????????Log.e("hook_so",?"beforeHookedMethod");
????????????????????}
????????????????});
注意,这里需要使用app的 classLoader,param.thisObject 就是 app 的classLoader,且为了避免循环调用,所以需要使用 invokeOriginalMethod 来调用 loadLibrary0 方法。
这里我们在加载目标 so 之前,先加载我们的 so,然后 hook 目标so中的方法。
最后hook结果如下:
I??hook?result?0x7473fc2000
E??beforeHookedMethod
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??junk?code
I??junk?code
I??_init?success
I??junk?code
I??junk?code
I??my_init_array1?success
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??proxy?origin?result?(null)
I??junk?code
I??junk?code
I??hookMe?success
可以看到,对 so 中的hook都成功了。
也是使用 sandhook 中的 api:
void?*libnativebase?=?SandGetModuleBase("libnative-lib.so");
在加上函数的偏移地址即可:
unsigned?long?tmpaddr?=?(unsigned?long)?libnativebase?+?0xf67c;
void?*testhookaddr?=?reinterpret_cast<void?*>(tmpaddr);
testhookfunction?=?reinterpret_cast<testhook>(testhookaddr);
LOGD("libnative-lib.so?base:%p,testfuncaddr:%p",libnativebase,(void*)tmpaddr);
不过,用起来还是感觉很蛋疼,而且这些代码都还没考虑 32与64 的区别。
总的来说,这个可以自己玩玩,总感觉差点什么东西,比如,我想使用地址来hook,但是就必须要等到so加载之后才行,这样就hook不到初始化时间,就有点烦。或许将 shadowhook 改一下,用起来会更舒服。