本文讨论如何hook目标apk中的 so 中的函数。
实现 so hook 的一个主要思想,就是将目标apk当成是我们自己编写的就行,就像开发中hook系统调用一样。有了这个思维,思路会宽广许多。
so 打开流程
在 Android 中加载一个so会使用到 System.loadLibrary 方法。
在 native 中,一般加载 so 是使用的 dlopen 方法。
由于 System.loadLibrary 最终也会调用到 dlopen 方法,所以我们先看 dlopen 方法流程。
dlopen 会调用到 do_dlopen 里面去:
yaml
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 相关代码:
javascript
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 方法:
ini
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 后面执行的原因。
hook 函数
有了上面的基础,我们再来介绍一下如何做 so 的 hook。arm 的hook会比较麻烦,但是好在有 github,我们可以使用开源的一些库做到开箱即用。
hook 分两种,一种是 plt hook,一种是 inline hook。这两种先简单介绍,后面我们开篇 elf 的时候在细说。简单来说,plt hook 只能 hook plt 表中存在的函数。inline hook 可以 hook 几乎所有的函数。
看一个例子来理解为啥要有这两种 hook:
arduino
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
编译后,看其汇编:
csharp
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 框架,字节开源的,虽然肯定比内部的版本号低了些,但是够用了:
但是发现它不是纯 native 的,还需要在 java 里面初始化,由于 classLoader 的问题,用起来会非常的麻烦。虽然说 xposed 将模块的代码注入了app进程,但是它们还是使用了不同的 classLoader,so的加载也是与 classLoader有关,就会遇见各种奇葩问题。
所以还是使用老版的 sandhook,将代码 copy 进来,在 cmakelists.txt 里面配置,然后编写hook代码:
arduino
//
// 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下:
bash
/data/app/com.example.nativehooktarget-prsFV1IVkibTNbuAUhlI-w==/lib/arm64/libsohook.so
这样,我们就可以在模块里面加载这个 so,然后让其加载后自动 hook:
typescript
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结果如下:
css
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都成功了。
so 中函数主动调用
也是使用 sandhook 中的 api:
ini
void *libnativebase = SandGetModuleBase("libnative-lib.so");
在加上函数的偏移地址即可:
ini
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 改一下,用起来会更舒服。