第12章内容优化

This commit is contained in:
feiocng 2023-07-09 16:02:05 +08:00
parent d029dcfa87
commit 3656255ad2

View File

@ -1,22 +1,26 @@
# 第十二章 逆向实战 # 第十二章 逆向实战
通过本书前文,对`Android`系统循循渐进的讲解,从基础到编译,再到内置,分析源码,添加独有的功能。在最后一章中,将结合前文中学习的知识,进行一个案例的实战,在实战的过程中,首先要对需求进行分析,然后将需要实现的目标进行拆分,最后对其逐一实现,最后组合并测试效果。在实现的过程中,最重要的是思考实现的思路和方向,然后再追寻源码,去寻找入手点。 本书内容的展开,从安卓源码编译再到系统功能内置,从源码分析到独有功能添加,无一不是遵循由简入繁,原理与实践相结合,因为笔者始终认为这才是系统化学习知识的最佳方案。
在本书最后一章中,将结合前文所学知识进行一个案例实战。在实战过程中,首先需要对需求进行分析,并将需要实现的目标进行拆分。然后逐一实现这些目标,并测试其效果。在实现的过程中,思考实现的思路和方向是至关重要的。随后可以深入追寻源码,在其中寻找切入点以便更好地完成任务。
## 12.1 案例实战 ## 12.1 案例实战
`JniTrace`是一个逆向分析中常用的基于`frida`实现的工具,在`native`函数调用时,`c++`代码可以通过`JNI`来访问`Java`中的类,类成员以及函数,而`JniTrace`工具主要负责对所有`JNI`的函数调用进行监控,输出所有调用的`JNI`函数,传递的参数,以及其返回值。 `JniTrace`是一个常用的逆向分析工具,基于`frida`实现。在调用本地函数时C++代码可以通过JNI访问Java类、成员和函数。而`JniTrace`工具主要用于监控所有JNI函数调用并输出这些函数的调用、传递参数和返回值。
但是由于`frida`过于知名,导致大多数情况下,开发者们都会对其进行检测,所以在使用时常常会面临各种反制手段导致无法进行下一步的分析。为了能够躲避检测并继续使用`JniTrace`,逆向人员将其迁移到了更隐蔽的类`Xposed`框架中(例如`LSPosed`)。 然而,由于`frida`过于知名,为了防止软件被调试与分析,导致大多数情况下恶意软件开发者与互联网业务软件厂商会对其进行检测。因此,在使用时经常面临各种反制手段使得无法继续分析。为了躲避检测并继续使用`JniTrace`, 逆向人员将其迁移到更隐蔽的框架中如LSPosed)。
而对比`Hook`的方案来说,从`AOSP`中修改,则完全不存在有`Hook`的痕迹,但是相对而言,开发也更沉重一些,因为需要对系统有一定的理解,并且需要重复的编译系统来进行测试。 相比Hook方案从AOSP中进行修改则完全没有Hook痕迹存在。但相应地需要更深入地理解系统并重复编译系统以进行测试。
在这一章的实战中,将讲解如何从`AOSP`的角度完成`JniTrace`这样的功能,并且使用配置进行管理,让其仅对目标进程生效,仅对目标`native`函数生效。 在本章的实战中将讲解如何从AOSP角度完成类似JniTrace功能并使用配置管理使其仅对目标进程和本地函数生效。
在前文中提到了处理`RegisterNative`输出时的问题:它会在运行时对所有进程监控,并生成大量的运行时日志输出。为优化这一点,在处理时可以通过配置获取当前进程是否为目标进程来选择性打印日志信息。同样的优化方法也适用于这个例子中的配置管理。
在前文讲解`RegisterNative`的输出时,注意到当时的处理将会对所有的进程生效,导致输出过于庞大,在,优化的处理也是从一个配置中,获取到当前进程是否为目标进程,才进行对应的打桩输出。在这个例子中的配置管理同样适用该优化。
## 12.2 需求 ## 12.2 需求
本案例的需求是参考`JniTrace`,修改`AOSP`源码实现对`JNI`函数调用的监控。所以第一步,是了解`JniTrace`,安装该工具,并开发简单的`demo`来测试其对`JNI`函数监控的效果。 ​本案例的需求是参考`JniTrace`,修改`AOSP`源码实现对`JNI`函数调用的监控。所以第一步,是了解`JniTrace`,安装该工具,并开发简单的`demo`来测试其对`JNI`函数监控的效果。
### 12.2.1 功能分析 ### 12.2.1 功能分析
@ -26,19 +30,14 @@
pip install jnitrace pip install jnitrace
``` ```
由于该工具是基于`frida`实现的,需要在手机中运行`frida-server`,在地址`https://github.com/frida/frida/releases`中下载`frida-server`,开发环境是`AOSP12`的情况直接下载`16`任意版本即可。然后将其推送到手机的`/data/local/tmp`目录中,并运行。具体命令如下。 ​由于该工具是基于`frida`实现的,需要在手机中运行`frida-server`,在地址`https://github.com/frida/frida/releases`中下载`frida-server`,开发环境是`AOSP` 12的情况直接下载`16`任意版本即可。然后将其推送到手机的`/data/local/tmp`目录中,并运行。具体命令如下。
``` ```
adb push ./frida-server-16.1.1-android-arm64 /data/local/tmp adb push ./frida-server-16.1.1-android-arm64 /data/local/tmp
adb forward tcp:27042 tcp:27042 adb forward tcp:27042 tcp:27042
adb shell adb shell
su su
cd /data/local/tmp cd /data/local/tmp
chmod +x ./frida-server-16.1.1-android-arm64 chmod +x ./frida-server-16.1.1-android-arm64
// 为防止出现错误先将selinux关闭 // 为防止出现错误先将selinux关闭
@ -76,7 +75,7 @@ public class MainActivity extends AppCompatActivity {
} }
``` ```
修改`stringFromJNI`的实现,让其通过`JNI`调用`MainActivity`中的`demo`函数。 ​修改`stringFromJNI`的实现,让其通过`JNI`调用`MainActivity`中的`demo`函数。
```c++ ```c++
extern "C" JNIEXPORT jstring JNICALL extern "C" JNIEXPORT jstring JNICALL
@ -91,13 +90,13 @@ Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
} }
``` ```
案例准备就绪后,接着通过命令,让`JniTrace`启动应用并监控`JNI`的调用,操作如下。 ​案例准备就绪后,接着通过命令,让`JniTrace`启动应用并监控`JNI`的调用,操作如下。
``` ```
jnitrace -l libnativedemo.so cn.mik.nativedemo jnitrace -l libnativedemo.so cn.mik.nativedemo
``` ```
默认会以`spawn`的方式进行附加,所以应用会自动拉起,点击按钮触发`JNI`调用,`JniTrace`则会输出日志如下。 ​默认会以`spawn`的方式进行附加,所以应用会自动拉起,点击按钮触发`JNI`调用,`JniTrace`则会输出日志如下。
``` ```
/* TID 6996 */ /* TID 6996 */
@ -178,11 +177,11 @@ jnitrace -l libnativedemo.so cn.mik.nativedemo
### 12.3.1 配置文件的访问权限 ### 12.3.1 配置文件的访问权限
既然是配置管理,那么肯定是从一个文件中读取数据,而该配置文件必须符合条件是所有`APP`应用都有权限读取,而在`Android`中,每个应用都有各自的用户身份,而不同用户之间的访问权限是受限的。在`Android8`以前,`sdcard`中还没有用户访问具体目录时,只要打开`sdcard`权限,即可访问同一个文件。但是在当前编译的`AOSP12`中已经无法访问`sdcard`下的任意文件了。 ​既然是配置管理,那么肯定是从一个文件中读取数据,而该配置文件必须符合条件是所有`APP`应用都有权限读取,而在`Android`中,每个应用都有各自的用户身份,而不同用户之间的访问权限是受限的。在`Android8`以前,`sdcard`中还没有用户访问具体目录时,只要打开`sdcard`权限,即可访问同一个文件。但是在当前编译的`AOSP12`中已经无法访问`sdcard`下的任意文件了。
要解决这种各类应用访问同一个配置文件,有多种解决方式。例如通过自定义系统服务来访问具体文件,这样所有进程只要调用系统服务获取配置数据即可。例如通过共享内存,也可以达到相同的效果。 ​要解决这种各类应用访问同一个配置文件,有多种解决方式。例如通过自定义系统服务来访问具体文件,这样所有进程只要调用系统服务获取配置数据即可。例如通过共享内存,也可以达到相同的效果。
在这个案例中,将采用另一种简单的方式来解决该问题。在`Android`中有一个特殊的目录是`/data/local/tmp`,下面开始简单测试,在该目录创建一个文件。 ​在这个案例中,将采用另一种简单的方式来解决该问题。在`Android`中有一个特殊的目录是`/data/local/tmp`,下面开始简单测试,在该目录创建一个文件。
``` ```
echo "test" > /data/local/tmp/config.json echo "test" > /data/local/tmp/config.json
@ -203,7 +202,7 @@ protected void onCreate(Bundle savedInstanceState) {
} }
``` ```
测试发现,能成功的读取到文件,这里的重点在于,该文件是`shell`身份创建的,不带有身份标识,所以只要有访问权限就能正常读取,`selinux`不会拦截该操作。而`App`应用创建的文件则无法进行读取。下面看看`shell`创建的文件和应用创建的文件之间的区别。 ​测试发现,能成功的读取到文件,这里的重点在于,该文件是`shell`身份创建的,不带有身份标识,所以只要有访问权限就能正常读取,`selinux`不会拦截该操作。而`App`应用创建的文件则无法进行读取。下面看看`shell`创建的文件和应用创建的文件之间的区别。
``` ```
-rw-r--r-- 1 root root u:object_r:shell_data_file:s0 5 2023-04-13 23:19 config.json -rw-r--r-- 1 root root u:object_r:shell_data_file:s0 5 2023-04-13 23:19 config.json
@ -214,13 +213,13 @@ protected void onCreate(Bundle savedInstanceState) {
### 12.3.2 配置文件的结构 ### 12.3.2 配置文件的结构
为了访问方便,配置文件以`json`的格式进行存储,在执行进入应用主进程后,则读取该配置文件,然后再根据配置的值进行相应的处理。下面是该配置文件的内容。 ​为了访问方便,配置文件以`json`的格式进行存储,在执行进入应用主进程后,则读取该配置文件,然后再根据配置的值进行相应的处理。下面是该配置文件的内容。
``` ```
[{"packageName":"cn.mik.nativedemo","isJNIMethodPrint":true,"isRegisterNativePrint":true,"jniModuleName":"libnativedemo.so","jniFuncName":"stringFromJNI"}] [{"packageName":"cn.mik.nativedemo","isJNIMethodPrint":true,"isRegisterNativePrint":true,"jniModuleName":"libnativedemo.so","jniFuncName":"stringFromJNI"}]
``` ```
为了便于访问,使用一个对应的类对象来解析该配置文件,类结构定义如下。 ​为了便于访问,使用一个对应的类对象来解析该配置文件,类结构定义如下。
```java ```java
public class PackageItem { public class PackageItem {
@ -245,9 +244,9 @@ public class PackageItem {
### 12.3.3 解析配置文件 ### 12.3.3 解析配置文件
当任意应用程序启动到`ActivityThread`中的主进程入口时,就可以执行解析配置文件逻辑,然后进行相应的处理了,而在`ActivityThread``Application`创建后调用的时机,和应用中的`onCreate`调用时机其实相差不大的,但是在测试的时候,在`ActivityThread`中写代码会导致每次修改后,要等待重新编译和刷机,所以完全可以选择先在正常的应用`onCreate`中写入要解析的代码,在最后流程完全跑通后,再将测试无误的代码放入`ActivityThread`中。 ​当任意应用程序启动到`ActivityThread`中的主进程入口时,就可以执行解析配置文件逻辑,然后进行相应的处理了,而在`ActivityThread``Application`创建后调用的时机,和应用中的`onCreate`调用时机其实相差不大的,但是在测试的时候,在`ActivityThread`中写代码会导致每次修改后,要等待重新编译和刷机,所以完全可以选择先在正常的应用`onCreate`中写入要解析的代码,在最后流程完全跑通后,再将测试无误的代码放入`ActivityThread`中。
这里使用`fastjson`将配置文件内容解析成类对象,下面是解析的代码。 ​这里使用`fastjson`将配置文件内容解析成类对象,下面是解析的代码。
```java ```java
@Override @Override
@ -278,9 +277,9 @@ protected void onCreate(Bundle savedInstanceState) {
### 12.3.4 配置参数的传递 ### 12.3.4 配置参数的传递
由于`JNI`的调用部分是在`native`中进行,所以获取到的配置内容,需要将其传递到`native`层,并将其保存在一个可以全局访问的位置。便于后续打桩时获取配置的参数。 ​由于`JNI`的调用部分是在`native`中进行,所以获取到的配置内容,需要将其传递到`native`层,并将其保存在一个可以全局访问的位置。便于后续打桩时获取配置的参数。
传递数据到`native`层,必然是需要新定义一个`native`函数,在这个案例实现中,在文件`libcore/dalvik/src/main/java/dalvik/system/DexFile.java`中添加了`native`函数实现配置数据的传递。修改如下。 ​传递数据到`native`层,必然是需要新定义一个`native`函数,在这个案例实现中,在文件`libcore/dalvik/src/main/java/dalvik/system/DexFile.java`中添加了`native`函数实现配置数据的传递。修改如下。
```java ```java
public final class DexFile { public final class DexFile {
@ -308,7 +307,7 @@ static JNINativeMethod gMethods[] = {
``` ```
参数传递到`native`层后,需要将其保存到一个全局能够访问的位置,便于后续`JNI`触发时进行判断。在案例中,我选择将其存放在`Runtime`中。修改`art/runtime/runtime.h`文件如下。 ​参数传递到`native`层后,需要将其保存到一个全局能够访问的位置,便于后续`JNI`触发时进行判断。在案例中,我选择将其存放在`Runtime`中。修改`art/runtime/runtime.h`文件如下。
```c++ ```c++
typedef struct{ typedef struct{
@ -339,7 +338,7 @@ class Runtime {
} }
``` ```
这样在能访问到`Runtime`的任意地方都能获取到该配置了。现在就可以实现前面的`initConfig`函数了,将`java`传递过来的对象,转换为`c++`对象存储到`Runtime`中。具体实现如下。 ​这样在能访问到`Runtime`的任意地方都能获取到该配置了。现在就可以实现前面的`initConfig`函数了,将`java`传递过来的对象,转换为`c++`对象存储到`Runtime`中。具体实现如下。
```c++ ```c++
@ -450,7 +449,7 @@ typedef const struct JNIInvokeInterface* JavaVM;
#endif #endif
``` ```
接着看看`_JNIEnv`的定义描述。 ​接着看看`_JNIEnv`的定义描述。
```c++ ```c++
struct _JNIEnv { struct _JNIEnv {
@ -493,7 +492,7 @@ struct JNINativeInterface {
}; };
``` ```
根据上面源码分析,能够看到`JNIEnv`实际就是`JNINativeInterface`的指针,而该指针对应的结构体中存储着函数表,接下来看是如何给`functions`进行赋值的。 ​根据上面源码分析,能够看到`JNIEnv`实际就是`JNINativeInterface`的指针,而该指针对应的结构体中存储着函数表,接下来看是如何给`functions`进行赋值的。
```c++ ```c++
JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg) JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg)
@ -513,7 +512,7 @@ JNIEnvExt::JNIEnvExt(Thread* self_in, JavaVMExt* vm_in, std::string* error_msg)
} }
``` ```
继续分析`GetFunctionTable`实现。 ​继续分析`GetFunctionTable`实现。
```c++ ```c++
const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) { const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) {
@ -525,7 +524,7 @@ const JNINativeInterface* JNIEnvExt::GetFunctionTable(bool check_jni) {
} }
``` ```
继续进入查看`GetJniNativeInterface`实现逻辑 ​继续进入查看`GetJniNativeInterface`实现逻辑
```c++ ```c++
const JNINativeInterface* GetJniNativeInterface() { const JNINativeInterface* GetJniNativeInterface() {
@ -535,7 +534,7 @@ const JNINativeInterface* GetJniNativeInterface() {
} }
``` ```
到这里就对`JNI`对应函数进行赋值了。 ​到这里就对`JNI`对应函数进行赋值了。
```c++ ```c++
template<bool kEnableIndexIds> template<bool kEnableIndexIds>
@ -556,20 +555,20 @@ struct JniNativeInterfaceFunctions {
}; };
``` ```
根据上面的源码分析,知道了`JNI`中函数对应的实现就在文件`art/runtime/jni/jni_internal.cc`中实现。明白了这个原理后,接下来添加一个打桩函数简单的输出信息,来确定流程是否正确。 ​根据上面的源码分析,知道了`JNI`中函数对应的实现就在文件`art/runtime/jni/jni_internal.cc`中实现。明白了这个原理后,接下来添加一个打桩函数简单的输出信息,来确定流程是否正确。
```c++ ```c++
static jobject CallObjectMethodV(JNIEnv* env, jobject obj, jmethodID mid, va_list args) { static jobject CallObjectMethodV(JNIEnv* env, jobject obj, jmethodID mid, va_list args) {
CHECK_NON_NULL_ARGUMENT(obj); CHECK_NON_NULL_ARGUMENT(obj);
CHECK_NON_NULL_ARGUMENT(mid); CHECK_NON_NULL_ARGUMENT(mid);
ALOGD("mikrom %s",__FUNCTION__); ALOGD("jnitrace %s",__FUNCTION__);
ScopedObjectAccess soa(env); ScopedObjectAccess soa(env);
JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, args)); JValue result(InvokeVirtualOrInterfaceWithVarArgs(soa, obj, mid, args));
return soa.AddLocalReference<jobject>(result.GetL()); return soa.AddLocalReference<jobject>(result.GetL());
} }
``` ```
编译后成功看到了大量该函数调用的日志,接下来需要封装一个函数,在这个函数中根据前文传递的配置进行判断是否需要输出`JNI`调用的相关信息。符合条件才进行打桩。具体实现如下。 ​编译后成功看到了大量该函数调用的日志,接下来需要封装一个函数,在这个函数中根据前文传递的配置进行判断是否需要输出`JNI`调用的相关信息。符合条件才进行打桩。具体实现如下。
```c++ ```c++
void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& , void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
@ -582,15 +581,15 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
return; return;
} }
// 打桩信息 // 打桩信息
ALOGD("mikrom ShowVarArgs %s %s %s %s %p",runtime->GetProcessPackageName().c_str(),runtime->GetConfigItem().jniModuleName, ALOGD("jnitrace ShowVarArgs %s %s %s %s %p",runtime->GetProcessPackageName().c_str(),runtime->GetConfigItem().jniModuleName,
runtime->GetConfigItem().jniFuncName,funcname,Thread::Current()); runtime->GetConfigItem().jniFuncName,funcname,Thread::Current());
} }
``` ```
这里使用了两个条件来控制打桩,`isJNIMethodPrint`表示是否要对`JNI`监控打桩,而`jniEnable`则表示,当前是否应该打桩,例如在指定的`native`函数调用期间,才打桩输出,或者指定动态库加载后,才进行打桩,这个过滤条件大大的降低了对无效日志的输出,提高分析的效率。 ​这里使用了两个条件来控制打桩,`isJNIMethodPrint`表示是否要对`JNI`监控打桩,而`jniEnable`则表示,当前是否应该打桩,例如在指定的`native`函数调用期间,才打桩输出,或者指定动态库加载后,才进行打桩,这个过滤条件大大的降低了对无效日志的输出,提高分析的效率。
`jniEnable`默认是`false`的,值不应由配置文件决定,而是在调用过程中进行赋值,所有`native`函数开始执行和执行结束时都会经过`JniMethodStart``JniMethodEnd`函数,所以只需要在进入该函数时,将该字段修改为`true`,在结束时,再将其关闭即可。下面是实现代码。 `jniEnable`默认是`false`的,值不应由配置文件决定,而是在调用过程中进行赋值,所有`native`函数开始执行和执行结束时都会经过`JniMethodStart``JniMethodEnd`函数,所以只需要在进入该函数时,将该字段修改为`true`,在结束时,再将其关闭即可。下面是实现代码。
```c++ ```c++
extern uint32_t JniMethodStart(Thread* self) { extern uint32_t JniMethodStart(Thread* self) {
@ -606,7 +605,7 @@ extern uint32_t JniMethodStart(Thread* self) {
// 当前开始函数为要监控的目标函数时则开启输出JNI // 当前开始函数为要监控的目标函数时则开启输出JNI
if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){ if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){
runtime->GetConfigItem().jniEnable=true; runtime->GetConfigItem().jniEnable=true;
ALOGD("mikrom enter jni %s",methodname.c_str()); ALOGD("jnitrace enter jni %s",methodname.c_str());
} }
} }
//endadd //endadd
@ -629,7 +628,7 @@ extern void JniMethodEnd(uint32_t saved_local_ref_cookie, Thread* self) {
// 当前结束函数为要监控的目标函数时则关闭输出JNI // 当前结束函数为要监控的目标函数时则关闭输出JNI
if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){ if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){
runtime->GetConfigItem().jniEnable=false; runtime->GetConfigItem().jniEnable=false;
ALOGD("mikrom leave jni %s",methodname.c_str()); ALOGD("jnitrace leave jni %s",methodname.c_str());
} }
} }
//endadd //endadd
@ -650,7 +649,7 @@ static mirror::Object* JniMethodEndWithReferenceHandleResult(jobject result,
std::string methodname=native_method->PrettyMethod(); std::string methodname=native_method->PrettyMethod();
if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){ if(strstr(methodname.c_str(),runtime->GetConfigItem().jniFuncName)){
runtime->GetConfigItem().jniEnable=false; runtime->GetConfigItem().jniEnable=false;
ALOGD("mikrom leave jni %s",methodname.c_str()); ALOGD("jnitrace leave jni %s",methodname.c_str());
} }
} }
//endadd //endadd
@ -658,22 +657,24 @@ static mirror::Object* JniMethodEndWithReferenceHandleResult(jobject result,
} }
``` ```
## 12.5 打桩函数分类 ## 12.5 打桩函数分类
前文中仅仅对其中类似`CallObjectMethodV`函数,进行简单的输出,而实际场景中,大量的`JNI`函数调用,并非有着这些参数,所以需要将`ShowVarArgs`进行封装,并有多种重载实现。具体重载参数需要根据实际`JNI`函数中有哪些参数来决定。在这里篇幅有限,所以不会将所有的`JNI`函数情况进行处理,主要将前文中测试中调用到的`JNI`函数进行对应处理。 ​前文中仅仅对其中类似`CallObjectMethodV`函数,进行简单的输出,而实际场景中,大量的`JNI`函数调用,并非有着这些参数,所以需要将`ShowVarArgs`进行封装,并有多种重载实现。具体重载参数需要根据实际`JNI`函数中有哪些参数来决定。在这里篇幅有限,所以不会将所有的`JNI`函数情况进行处理,主要将前文中测试中调用到的`JNI`函数进行对应处理。
根据`JniTrace`中的日志,需要对四个`JNI`函数进行打桩处理,分别是`GetMethodID、GetStringUTFChars、NewStringUTF、CallObjectMethodV`。根据这些函数对应的参数,对打桩的函数进行重载处理。 ​根据`JniTrace`中的日志,需要对四个`JNI`函数进行打桩处理,分别是`GetMethodID、GetStringUTFChars、NewStringUTF、CallObjectMethodV`。根据这些函数对应的参数,对打桩的函数进行重载处理。
### 12.5.1 GetMethodID打桩
在开始修改代码前,先看看该函数的定义。 ### 12.5.1 GetMethodID插桩
​在开始修改代码前,先看看该函数的定义。
```c++ ```c++
// 根据函数名,以及对应的函数签名来获取对应函数 // 根据函数名,以及对应的函数签名来获取对应函数
static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig); static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, const char* sig);
``` ```
再看看`JniTrace`对于该函数的输出。 ​再看看`JniTrace`对于该函数的输出。
``` ```
/* TID 6996 */ /* TID 6996 */
@ -691,7 +692,7 @@ static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, c
310 ms |-> 0x7c01191a0c: _ZN7_JNIEnv11GetMethodIDEP7_jclassPKcS3_+0x3c (libnativedemo.so:0x7c01183000) 310 ms |-> 0x7c01191a0c: _ZN7_JNIEnv11GetMethodIDEP7_jclassPKcS3_+0x3c (libnativedemo.so:0x7c01183000)
``` ```
该输出中关键展示了函数所在类的类名称、函数名称、函数签名以及所找到的对应函数id调用堆栈。参考该类型的`JNI`调用,下面重构一个相应的打桩函数。 该输出中关键展示了函数所在类的类名称、函数名称、函数签名以及所找到的对应函数id调用堆栈。参考该类型的`JNI`调用,下面重构一个相应的打桩函数。
```c++ ```c++
// 是否需要打印 // 是否需要打印
@ -714,14 +715,14 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,const char* funcna
ArtMethod* method = jni::DecodeArtMethod(methodID); ArtMethod* method = jni::DecodeArtMethod(methodID);
pid_t pid = getpid(); pid_t pid = getpid();
// 前面加上标志是为了方便搜索日志 // 前面加上标志是为了方便搜索日志
ALOGD("%s /* TID %d */","mikrom",pid); ALOGD("%s /* TID %d */","jnitrace",pid);
ALOGD("%s [+] JNIEnv->%s","mikrom",funcname); ALOGD("%s [+] JNIEnv->%s","jnitrace",funcname);
ALOGD("%s |- jclass :%s","mikrom",className); ALOGD("%s |- jclass :%s","jnitrace",className);
ALOGD("%s |- char* :%p","mikrom",name); ALOGD("%s |- char* :%p","jnitrace",name);
ALOGD("%s |: %s","mikrom",name); ALOGD("%s |: %s","jnitrace",name);
ALOGD("%s |- char* :%p","mikrom",sig); ALOGD("%s |- char* :%p","jnitrace",sig);
ALOGD("%s |: %s","mikrom",sig); ALOGD("%s |: %s","jnitrace",sig);
ALOGD("%s |= jmethodID :0x%x {%s}","mikrom",method->GetMethodIndex(),method->PrettyMethod().c_str()); ALOGD("%s |= jmethodID :0x%x {%s}","jnitrace",method->GetMethodIndex(),method->PrettyMethod().c_str());
} }
``` ```
@ -739,9 +740,10 @@ static jmethodID GetMethodID(JNIEnv* env, jclass java_class, const char* name, c
} }
``` ```
### 12.5.2 GetStringUTFChars打桩
参考上面的流程,首先了解该函数的定义结构。 ### 12.5.2 GetStringUTFChars插桩
​参考上面的流程,首先了解该函数的定义结构。
```c++ ```c++
static const char* GetStringUTFChars(JNIEnv* env, jstring java_string, jboolean* is_copy); static const char* GetStringUTFChars(JNIEnv* env, jstring java_string, jboolean* is_copy);
@ -762,7 +764,7 @@ static const char* GetStringUTFChars(JNIEnv* env, jstring java_string, jboolean*
313 ms |-> 0x7c01191b4c: _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh+0x34 (libnativedemo.so:0x7c01183000) 313 ms |-> 0x7c01191b4c: _ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh+0x34 (libnativedemo.so:0x7c01183000)
``` ```
看的出来这个函数非常的简单,主要是对返回值进行输出即可。添加打桩函数如下。 ​看的出来这个函数非常的简单,主要是对返回值进行输出即可。添加打桩函数如下。
```c++ ```c++
void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& , void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
@ -773,18 +775,18 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
return; return;
} }
pid_t pid = getpid(); pid_t pid = getpid();
ALOGD("%s /* TID %d */","mikrom",pid); ALOGD("%s /* TID %d */","jnitrace",pid);
ALOGD("%s [+] JNIEnv->%s","mikrom",funcname); ALOGD("%s [+] JNIEnv->%s","jnitrace",funcname);
if(is_copy== nullptr){ if(is_copy== nullptr){
ALOGD("%s |- jboolean* : %d","mikrom",false); ALOGD("%s |- jboolean* : %d","jnitrace",false);
}else{ }else{
ALOGD("%s |- jboolean* : %d","mikrom",*is_copy); ALOGD("%s |- jboolean* : %d","jnitrace",*is_copy);
} }
ALOGD("%s |= char* : %s","mikrom",data); ALOGD("%s |= char* : %s","jnitrace",data);
} }
``` ```
修改原调用函数如下。 ​修改原调用函数如下。
```c++ ```c++
@ -794,17 +796,18 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
ShowVarArgs(soa,__FUNCTION__,is_copy,bytes); ShowVarArgs(soa,__FUNCTION__,is_copy,bytes);
return bytes; return bytes;
} }
``` ``
`
### 12.5.3 NewStringUTF ### 12.5.3 NewStringUTF
该函数同样非常简单,和上一个函数相反,只需要将参数打印即可,无需处理返回值,函数定义如下。 ​该函数同样非常简单,和上一个函数相反,只需要将参数打印即可,无需处理返回值,函数定义如下。
```c++ ```c++
static jstring NewStringUTF(JNIEnv* env, const char* utf); static jstring NewStringUTF(JNIEnv* env, const char* utf);
``` ```
接着看`JniTrace`的输出,同样非常简单。 ​接着看`JniTrace`的输出,同样非常简单。
``` ```
/* TID 6996 */ /* TID 6996 */
@ -815,7 +818,7 @@ static jstring NewStringUTF(JNIEnv* env, const char* utf);
314 ms |= jstring : 0x99 { hello } 314 ms |= jstring : 0x99 { hello }
``` ```
添加对应打桩函数如下。 ​添加对应打桩函数如下。
```c++ ```c++
void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& , void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
@ -825,13 +828,13 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& ,
return; return;
} }
pid_t pid = getpid(); pid_t pid = getpid();
ALOGD("%s /* TID %d */","mikrom",pid); ALOGD("%s /* TID %d */","jnitrace",pid);
ALOGD("%s [+] JNIEnv->%s","mikrom",funcname); ALOGD("%s [+] JNIEnv->%s","jnitrace",funcname);
ALOGD("%s |- char* : %d","mikrom",data); ALOGD("%s |- char* : %d","jnitrace",data);
} }
``` ```
调整原函数调用该打桩如下。 ​调整原函数调用该打桩如下。
```c++ ```c++
@ -845,11 +848,12 @@ static jstring NewStringUTF(JNIEnv* env, const char* utf) {
} }
``` ```
### 12.5.4 CallObjectMethodV打桩
这个`JNI`函数不同于前面几种函数,在前几个函数中,参数是明确固定的,而`CallObjectMethodV`是通过`JNI`,调用一个`java`函数,而为此`java`函数提供的所有参数的类型,以及参数个数。都是未知的。而这些参数的信息同样是需要打桩展示出来的。 ### 12.5.4 CallObjectMethodV插桩
将测试样例中,被调用的`java`函数进行调整,将测试函数新增参数,并且使用`JniTrace`观察`CallObjectMethodV`的输出结果。样例函数修改如下。 ​这个`JNI`函数不同于前面几种函数,在前几个函数中,参数是明确固定的,而`CallObjectMethodV`是通过`JNI`,调用一个`java`函数,而为此`java`函数提供的所有参数的类型,以及参数个数。都是未知的。而这些参数的信息同样是需要打桩展示出来的。
​将测试样例中,被调用的`java`函数进行调整,将测试函数新增参数,并且使用`JniTrace`观察`CallObjectMethodV`的输出结果。样例函数修改如下。
```java ```java
public String demo(int a,float b,long c,String d){ public String demo(int a,float b,long c,String d){
@ -857,7 +861,7 @@ public String demo(int a,float b,long c,String d){
} }
``` ```
同时修改`native`函数中使用`JNI`调用的逻辑。 ​同时修改`native`函数中使用`JNI`调用的逻辑。
```c++ ```c++
extern "C" JNIEXPORT jstring JNICALL extern "C" JNIEXPORT jstring JNICALL
@ -873,7 +877,7 @@ Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
} }
``` ```
再次使用`JniTrace`观察到的`CallObjectMethodV`输出如下。 ​再次使用`JniTrace`观察到的`CallObjectMethodV`输出如下。
``` ```
/* TID 18863 */ /* TID 18863 */
@ -892,13 +896,13 @@ Java_cn_mik_nativedemo_MainActivity_stringFromJNI(
2169 ms |-> 0x6f5d458ae4: _ZN7_JNIEnv16CallObjectMethodEP8_jobjectP10_jmethodIDz+0xc4 (libnativedemo.so:0x6f5d44a000) 2169 ms |-> 0x6f5d458ae4: _ZN7_JNIEnv16CallObjectMethodEP8_jobjectP10_jmethodIDz+0xc4 (libnativedemo.so:0x6f5d44a000)
``` ```
从日志中能看到,参数列表被解析后将具体的值进行输出,而返回值的部分,如果是`jobject`,则将其类型进行输出。明白具体需求后,接着就可以开始根据`CallObjectMethodV`类型参数定义来准备打桩函数了。定义描述如下。 ​从日志中能看到,参数列表被解析后将具体的值进行输出,而返回值的部分,如果是`jobject`,则将其类型进行输出。明白具体需求后,接着就可以开始根据`CallObjectMethodV`类型参数定义来准备打桩函数了。定义描述如下。
```c++ ```c++
static jobject CallObjectMethodV(JNIEnv* env, jobject obj, jmethodID mid, va_list args); static jobject CallObjectMethodV(JNIEnv* env, jobject obj, jmethodID mid, va_list args);
``` ```
根据参考,需要输出调用的目标函数,参数,以及其返回值。而其他的类似调用函数的情况,和该函数差不多的处理,只是返回值的输出不同。优化后的打桩函数如下。 ​根据参考,需要输出调用的目标函数,参数,以及其返回值。而其他的类似调用函数的情况,和该函数差不多的处理,只是返回值的输出不同。优化后的打桩函数如下。
```c++ ```c++
// 输出JNI的参数部分 // 输出JNI的参数部分
@ -912,10 +916,10 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,
ArtMethod* method = jni::DecodeArtMethod(mid); ArtMethod* method = jni::DecodeArtMethod(mid);
pid_t pid = getpid(); pid_t pid = getpid();
ALOGD("%s /* TID %d */","mikrom",pid); ALOGD("%s /* TID %d */","jnitrace",pid);
ALOGD("%s [+] JNIEnv->%s","mikrom",funcname); ALOGD("%s [+] JNIEnv->%s","jnitrace",funcname);
ALOGD("%s |- jmethodID :0x%x {%s}","mikrom",method->GetMethodIndex(),method->PrettyMethod().c_str()); ALOGD("%s |- jmethodID :0x%x {%s}","jnitrace",method->GetMethodIndex(),method->PrettyMethod().c_str());
ALOGD("%s |- va_list :%p","mikrom",&vaList); ALOGD("%s |- va_list :%p","jnitrace",&vaList);
uint32_t shorty_len = 0; uint32_t shorty_len = 0;
const char* shorty = const char* shorty =
@ -943,16 +947,16 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,
ObjPtr<mirror::Class> cls=receiver->GetClass(); ObjPtr<mirror::Class> cls=receiver->GetClass();
if (cls->DescriptorEquals("Ljava/lang/String;")){ if (cls->DescriptorEquals("Ljava/lang/String;")){
ObjPtr<mirror::String> retStr =soa.Decode<mirror::String>(ret); ObjPtr<mirror::String> retStr =soa.Decode<mirror::String>(ret);
ALOGD("%s |= jstring :%s","mikrom",(const char*)retStr->GetValue()); ALOGD("%s |= jstring :%s","jnitrace",(const char*)retStr->GetValue());
}else{ }else{
std::string temp; std::string temp;
const char* className= cls->GetDescriptor(&temp); const char* className= cls->GetDescriptor(&temp);
ALOGD("%s |= jobject :%p {%s}","mikrom",&ret,className); ALOGD("%s |= jobject :%p {%s}","jnitrace",&ret,className);
} }
} }
``` ```
很多`JNI`的调用函数除了返回值的处理不同,其他调用部分都是相同的,所以将打印参数部分和打印返回值部分拆分开来,而参数`va_list`的解析,可以直接参考`AOSP`源码中,函数`BuildArgArrayFromVarArgs`对其的处理。实现如下。 ​很多`JNI`的调用函数除了返回值的处理不同,其他调用部分都是相同的,所以将打印参数部分和打印返回值部分拆分开来,而参数`va_list`的解析,可以直接参考`AOSP`源码中,函数`BuildArgArrayFromVarArgs`对其的处理。实现如下。
```c++ ```c++
void VarArgsShowArg(const ScopedObjectAccessAlreadyRunnable& soa, void VarArgsShowArg(const ScopedObjectAccessAlreadyRunnable& soa,
@ -966,32 +970,32 @@ void VarArgsShowArg(const ScopedObjectAccessAlreadyRunnable& soa,
case 'C': case 'C':
case 'S': case 'S':
case 'I': case 'I':
ss<<"mikrom"<<" |: jint : "<<va_arg(ap, jint)<<"\n"; ss<<"jnitrace"<<" |: jint : "<<va_arg(ap, jint)<<"\n";
break; break;
case 'F': case 'F':
ss<<"mikrom"<<" |: jfloat : "<<va_arg(ap, jdouble)<<"\n"; ss<<"jnitrace"<<" |: jfloat : "<<va_arg(ap, jdouble)<<"\n";
break; break;
case 'L':{ case 'L':{
jobject obj=va_arg(ap, jobject); jobject obj=va_arg(ap, jobject);
ObjPtr<mirror::Object> receiver =soa.Decode<mirror::Object>(obj); ObjPtr<mirror::Object> receiver =soa.Decode<mirror::Object>(obj);
if(receiver==nullptr){ if(receiver==nullptr){
ss<<"mikrom"<<" |: jobject : null\n"; ss<<"jnitrace"<<" |: jobject : null\n";
break; break;
} }
ObjPtr<mirror::Class> cls=receiver->GetClass(); ObjPtr<mirror::Class> cls=receiver->GetClass();
if (cls->DescriptorEquals("Ljava/lang/String;")){ if (cls->DescriptorEquals("Ljava/lang/String;")){
ObjPtr<mirror::String> argStr =soa.Decode<mirror::String>(obj); ObjPtr<mirror::String> argStr =soa.Decode<mirror::String>(obj);
ss<<"mikrom"<<" |: jstring : "<<(const char*)argStr->GetValue()<<"\n"; ss<<"jnitrace"<<" |: jstring : "<<(const char*)argStr->GetValue()<<"\n";
}else{ }else{
ss<<"mikrom"<<" |: jobject : "<<&obj<<"\n"; ss<<"jnitrace"<<" |: jobject : "<<&obj<<"\n";
} }
break; break;
} }
case 'D': case 'D':
ss<<"mikrom"<<" |: jdouble : "<<va_arg(ap, jdouble)<<"\n"; ss<<"jnitrace"<<" |: jdouble : "<<va_arg(ap, jdouble)<<"\n";
break; break;
case 'J': case 'J':
ss<<"mikrom"<<" |: jlong : "<<va_arg(ap, jlong)<<"\n"; ss<<"jnitrace"<<" |: jlong : "<<va_arg(ap, jlong)<<"\n";
break; break;
} }
} }
@ -1002,51 +1006,53 @@ void VarArgsShowArg(const ScopedObjectAccessAlreadyRunnable& soa,
到这里案例中使用的相关`JNI`函数处理就添加完成了,编译后刷入测试机。当点击样例中的按钮时,最后输出效果如下所示。 到这里案例中使用的相关`JNI`函数处理就添加完成了,编译后刷入测试机。当点击样例中的按钮时,最后输出效果如下所示。
``` ```
mikrom enter jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() 0x74656a77b0 jnitrace enter jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() 0x74656a77b0
mikrom /* TID 5465 */ jnitrace /* TID 5465 */
mikrom [+] JNIEnv->GetMethodID jnitrace [+] JNIEnv->GetMethodID
mikrom |- jclass :Lcn/mik/nativedemo/MainActivity; jnitrace |- jclass :Lcn/mik/nativedemo/MainActivity;
mikrom |- char* :0x7275d69f1b jnitrace |- char* :0x7275d69f1b
mikrom |: demo jnitrace |: demo
mikrom |- char* :0x7275d69eef jnitrace |- char* :0x7275d69eef
mikrom |: (IFLjava/lang/String;)Ljava/lang/String; jnitrace |: (IFLjava/lang/String;)Ljava/lang/String;
mikrom |= jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)} jnitrace |= jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)}
mikrom /* TID 5465 */ jnitrace /* TID 5465 */
mikrom [+] JNIEnv->NewStringUTF jnitrace [+] JNIEnv->NewStringUTF
mikrom |- char* : newdemo jnitrace |- char* : newdemo
mikrom /* TID 5465 */ jnitrace /* TID 5465 */
mikrom [+] JNIEnv->CallObjectMethodV jnitrace [+] JNIEnv->CallObjectMethodV
mikrom |- jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)} jnitrace |- jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)}
mikrom |- va_list :0x7fe0461bb0 jnitrace |- va_list :0x7fe0461bb0
mikrom |: jint : 1 jnitrace |: jint : 1
mikrom |: jfloat : 2 jnitrace |: jfloat : 2
mikrom |: jstring : newdemo jnitrace |: jstring : newdemo
mikrom |= jstring :3.0newdemo jnitrace |= jstring :3.0newdemo
mikrom /* TID 5465 */ jnitrace /* TID 5465 */
mikrom [+] JNIEnv->GetStringUTFChars jnitrace [+] JNIEnv->GetStringUTFChars
mikrom |- jboolean* : 0 jnitrace |- jboolean* : 0
mikrom |= char* : 3.0newdemo jnitrace |= char* : 3.0newdemo
mikrom /* TID 5465 */ jnitrace /* TID 5465 */
mikrom [+] JNIEnv->NewStringUTF jnitrace [+] JNIEnv->NewStringUTF
mikrom |- char* : 3.0newdemo jnitrace |- char* : 3.0newdemo
mikrom leave jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() jnitrace leave jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI()
``` ```
## 12.6 调用栈展示 ## 12.6 调用栈展示
经过调整后,打桩函数已经非常接近`JniTrace`的输出效果了,但是还有最后的一点区别是在于`JNI`函数的调用堆栈,不仅仅需要看到函数的参数和返回值,还需要知道是在哪里触发了该函数。而获取调用堆栈地址,在`AOSP`源码是有相关支持的,当应用崩溃时,在`logcat`中能看到详细的堆栈信息。 ​经过调整后,打桩函数已经非常接近`JniTrace`的输出效果了,但是还有最后的一点区别是在于`JNI`函数的调用堆栈,不仅仅需要看到函数的参数和返回值,还需要知道是在哪里触发了该函数。而获取调用堆栈地址,在`AOSP`源码是有相关支持的,当应用崩溃时,在`logcat`中能看到详细的堆栈信息。
### 12.6.1 xUnwind获取调用栈 ### 12.6.1 xUnwind获取调用栈
`xUnwind`是一个开源工具,该工具将堆栈获取进行了封装,并且有简单的`demo`演示如何获取堆栈。下载地址:`https://github.com/hexhacking/xUnwind.git`。接下来将分析该工具是如何实现的获取堆栈,然后再将其内置到`AOSP`中,在`JNI`函数调用结束时获取堆栈进行输出。 `xUnwind`是一个开源工具,该工具将堆栈获取进行了封装,并且有简单的`demo`演示如何获取堆栈。下载地址:`https://github.com/hexhacking/xUnwind.git`。接下来将分析该工具是如何实现的获取堆栈,然后再将其内置到`AOSP`中,在`JNI`函数调用结束时获取堆栈进行输出。
`xUnwind`提供了三种调用堆栈回溯方案。 `xUnwind`提供了三种调用堆栈回溯方案。
- `CFI (Call Frame Info)`:由安卓系统库提供。 - `CFI (Call Frame Info)`:由安卓系统库提供。
- `EH (Exception handling GCC extension)`:由编译器提供。 - `EH (Exception handling GCC extension)`:由编译器提供。
- `FP (Frame Pointer)`:只支持 `ARM64` - `FP (Frame Pointer)`:只支持 `ARM64`
其中`CFI`主要针对`java`层调用回溯,`FP`仅支持`ARM64`。根据功能需要,选择`EH`方案进行调用栈回溯。下面看看该工具是如何实现的,首先查看`JNI_OnLoad`的实现。 ​其中`CFI`主要针对`java`层调用回溯,`FP`仅支持`ARM64`。根据功能需要,选择`EH`方案进行调用栈回溯。下面看看该工具是如何实现的,首先查看`JNI_OnLoad`的实现。
```c++ ```c++
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
@ -1099,13 +1105,13 @@ static void sample_signal_register(void) {
} }
``` ```
在这个函数中主要是对信号指定了处理函数,`SIGSEGV` 是一种表示段错误的信号,通常意味着进程访问了无法访问的内存地址。 ​在这个函数中主要是对信号指定了处理函数,`SIGSEGV`是一种表示段错误的信号,通常意味着进程访问了无法访问的内存地址。
`SIGABRT` 是一种表示异常终止的信号,通常由调用 `abort()` 函数或 C++ 异常处理程序显式引发。 `abort()` 函数会向进程发送 `SIGABRT` 信号,并导致进程异常终止。 `SIGABRT`是一种表示异常终止的信号,通常由调用`abort()`函数或 C++ 异常处理程序显式引发。`abort()`函数会向进程发送`SIGABRT`信号,并导致进程异常终止。
如果进程接收到 `SIGABRT` 信号,操作系统将其发送给进程,并中断进程的正常执行流程。此时,进程通常会尝试处理该信号并进行恢复或退出。如果进程没有为 `SIGABRT` 信号设置信号处理程序,则默认行为是终止进程。 ​如果进程接收到`SIGABRT`信号,操作系统将其发送给进程,并中断进程的正常执行流程。此时,进程通常会尝试处理该信号并进行恢复或退出。如果进程没有为`SIGABRT`信号设置信号处理程序,则默认行为是终止进程。
接着开始跟踪`EH`的调用栈获取函数`sample_test_eh`的实现。 ​接着开始跟踪`EH`的调用栈获取函数`sample_test_eh`的实现。
```c++ ```c++
static void sample_test_eh(JNIEnv *env, jobject thiz, jboolean with_context, jboolean signal_interrupted) { static void sample_test_eh(JNIEnv *env, jobject thiz, jboolean with_context, jboolean signal_interrupted) {
@ -1115,7 +1121,7 @@ static void sample_test_eh(JNIEnv *env, jobject thiz, jboolean with_context, jbo
} }
``` ```
继续查看`sample_test`的实现。 ​继续查看`sample_test`的实现。
```c++ ```c++
@ -1139,9 +1145,9 @@ static void sample_test(int solution, jboolean remote_unwind, jboolean with_cont
} }
``` ```
由于堆栈信息需要通过触发信号后,在信号处理函数中获取,所以这里的参数没办法传过去,这里就将参数放到了全局变量中。然后信号函数执行完毕后,将会填充`g_frames`调用栈的地址信息,最后使用`xunwind_frames_log`函数解析调用栈,最后输出详细的调用栈。 ​由于堆栈信息需要通过触发信号后,在信号处理函数中获取,所以这里的参数没办法传过去,这里就将参数放到了全局变量中。然后信号函数执行完毕后,将会填充`g_frames`调用栈的地址信息,最后使用`xunwind_frames_log`函数解析调用栈,最后输出详细的调用栈。
接下来查看`SIGABRT`信号的处理函数`sample_sigabrt_handler`的实现。 ​接下来查看`SIGABRT`信号的处理函数`sample_sigabrt_handler`的实现。
```java ```java
@ -1295,7 +1301,7 @@ void xu_printer_append_string(xu_printer_t *self, const char *str) {
} }
``` ```
到了最后输出的部分可以看到,支持三种方式输出,在调用时,根据不同的需求来选择合适的输出方式。 ​到了最后输出的部分可以看到,支持三种方式输出,在调用时,根据不同的需求来选择合适的输出方式。
* `XU_PRINTER_TYPE_LOG` 直接`logcat`输出信息 * `XU_PRINTER_TYPE_LOG` 直接`logcat`输出信息
* `XU_PRINTER_TYPE_DUMP `将堆栈信息写入到指定描述符 * `XU_PRINTER_TYPE_DUMP `将堆栈信息写入到指定描述符
@ -1464,7 +1470,7 @@ const char* kbacktrace(bool with_context,const char* moduleName) {
#include "kbacktrace.h" #include "kbacktrace.h"
#define TAG "mikrom" #define TAG "jnitrace"
#define LOG_PRIORITY ANDROID_LOG_INFO #define LOG_PRIORITY ANDROID_LOG_INFO
#define ALOGI(fmt, ...) __android_log_print(LOG_PRIORITY, TAG, fmt, ##__VA_ARGS__) #define ALOGI(fmt, ...) __android_log_print(LOG_PRIORITY, TAG, fmt, ##__VA_ARGS__)
@ -1485,7 +1491,7 @@ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *, void *) {
``` ```
修改完样例后,动态库中的`xunwind_frames_get`函数也需要调整,为其新增参数,将动态库名称传递进去,让其仅返回包含该动态库名称的调用栈。修改如下。 ​修改完样例后,动态库中的`xunwind_frames_get`函数也需要调整,为其新增参数,将动态库名称传递进去,让其仅返回包含该动态库名称的调用栈。修改如下。
```c++ ```c++
char *xunwind_frames_get(uintptr_t *frames, size_t frames_sz, const char *prefix,const char* moduleName) { char *xunwind_frames_get(uintptr_t *frames, size_t frames_sz, const char *prefix,const char* moduleName) {
@ -1643,7 +1649,7 @@ libkbacktrace.so
#include <android/log.h> #include <android/log.h>
#include <dlfcn.h> #include <dlfcn.h>
#define TAG "mikrom" #define TAG "jnitrace"
#define LOG_PRIORITY ANDROID_LOG_INFO #define LOG_PRIORITY ANDROID_LOG_INFO
#define ALOGI(fmt, ...) __android_log_print(LOG_PRIORITY, TAG, fmt, ##__VA_ARGS__) #define ALOGI(fmt, ...) __android_log_print(LOG_PRIORITY, TAG, fmt, ##__VA_ARGS__)
@ -1722,12 +1728,12 @@ DexFile_initConfig(JNIEnv* env, jobject ,jobject item) {
if(handle_kbacktrace!=nullptr){ if(handle_kbacktrace!=nullptr){
citem.kbacktrace= dlsym(handle_kbacktrace, "_Z10kbacktracebPKc"); citem.kbacktrace= dlsym(handle_kbacktrace, "_Z10kbacktracebPKc");
if(citem.kbacktrace==nullptr){ if(citem.kbacktrace==nullptr){
ALOGD("mikrom kbacktrace is null.err:%s",dlerror()); ALOGD("jnitrace kbacktrace is null.err:%s",dlerror());
}else{ }else{
ALOGD("mikrom kbacktrace:%p.",citem.kbacktrace); ALOGD("jnitrace kbacktrace:%p.",citem.kbacktrace);
} }
}else{ }else{
ALOGD("mikrom handle_kbacktrace is null.err:%s",dlerror()); ALOGD("jnitrace handle_kbacktrace is null.err:%s",dlerror());
} }
} }
runtime->SetConfigItem(citem); runtime->SetConfigItem(citem);
@ -1743,7 +1749,7 @@ typedef const char* (*kbacktraceFunc)(bool,const char*);
const char* getBacktrace(const char* moduleName){ const char* getBacktrace(const char* moduleName){
Runtime* runtime=Runtime::Current(); Runtime* runtime=Runtime::Current();
if(runtime->GetConfigItem().kbacktrace== nullptr){ if(runtime->GetConfigItem().kbacktrace== nullptr){
ALOGD("mikrom kbacktrace is null"); ALOGD("jnitrace kbacktrace is null");
return nullptr; return nullptr;
} }
kbacktraceFunc kbacktrace=(kbacktraceFunc)runtime->GetConfigItem().kbacktrace; kbacktraceFunc kbacktrace=(kbacktraceFunc)runtime->GetConfigItem().kbacktrace;
@ -1759,67 +1765,45 @@ void ShowVarArgs(const ScopedObjectAccessAlreadyRunnable& soa,const char* funcna
const char* className= c->GetDescriptor(&temp); const char* className= c->GetDescriptor(&temp);
ArtMethod* method = jni::DecodeArtMethod(methodID); ArtMethod* method = jni::DecodeArtMethod(methodID);
pid_t pid = getpid(); pid_t pid = getpid();
ALOGD("%s /* TID %d */","mikrom",pid); ALOGD("%s /* TID %d */","jnitrace",pid);
ALOGD("%s [+] JNIEnv->%s","mikrom",funcname); ALOGD("%s [+] JNIEnv->%s","jnitrace",funcname);
ALOGD("%s |- jclass :%s","mikrom",className); ALOGD("%s |- jclass :%s","jnitrace",className);
ALOGD("%s |- char* :%p","mikrom",name); ALOGD("%s |- char* :%p","jnitrace",name);
ALOGD("%s |: %s","mikrom",name); ALOGD("%s |: %s","jnitrace",name);
ALOGD("%s |- char* :%p","mikrom",sig); ALOGD("%s |- char* :%p","jnitrace",sig);
ALOGD("%s |: %s","mikrom",sig); ALOGD("%s |: %s","jnitrace",sig);
ALOGD("%s |= jmethodID :0x%x {%s}","mikrom",method->GetMethodIndex(),method->PrettyMethod().c_str()); ALOGD("%s |= jmethodID :0x%x {%s}","jnitrace",method->GetMethodIndex(),method->PrettyMethod().c_str());
Runtime* runtime=Runtime::Current(); Runtime* runtime=Runtime::Current();
const char* backtrace= getBacktrace(runtime->GetConfigItem().jniModuleName); const char* backtrace= getBacktrace(runtime->GetConfigItem().jniModuleName);
ALOGD("-------------------------mikrom Backtrace-------------------------\n%s\n",backtrace); ALOGD("-------------------------Backtrace-------------------------\n%s\n",backtrace);
} }
``` ```
到这里`JniTrace``AOSP`版本就完成了,其他的函数调用参考前面的做法即可,最后优化后的日志输入如下。 到这里`JniTrace``AOSP`版本就完成了,其他的函数调用参考前面的做法即可,最后优化后的日志输入如下。
``` ```
mikrom enter jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() 0x7a0bc06010 jnitrace enter jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() 0x7a0bc06010
mikrom /* TID 5641 */ jnitrace /* TID 5641 */
mikrom [+] JNIEnv->GetMethodID jnitrace [+] JNIEnv->GetMethodID
mikrom |- jclass :Lcn/mik/nativedemo/MainActivity; jnitrace |- jclass :Lcn/mik/nativedemo/MainActivity;
mikrom |- char* :0x781eb6cf1b jnitrace |- char* :0x781eb6cf1b
mikrom |: demo jnitrace |: demo
mikrom |- char* :0x781eb6ceef jnitrace |- char* :0x781eb6ceef
mikrom |: (IFLjava/lang/String;)Ljava/lang/String; jnitrace |: (IFLjava/lang/String;)Ljava/lang/String;
mikrom |= jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)} jnitrace |= jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)}
-------------------------mikrom Backtrace------------------------- -------------------------Backtrace-------------------------
#05 pc 000000000000e9dc /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv11GetMethodIDEP7_jclassPKcS3_+60) #05 pc 000000000000e9dc /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv11GetMethodIDEP7_jclassPKcS3_+60)
#06 pc 000000000000e888 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+80) #06 pc 000000000000e888 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+80)
mikrom /* TID 5641 */ jnitrace /* TID 5641 */
mikrom [+] JNIEnv->NewStringUTF jnitrace [+] JNIEnv->NewStringUTF
mikrom |- char* : newdemo jnitrace |- char* : newdemo
-------------------------mikrom Backtrace------------------------- -------------------------Backtrace-------------------------
#05 pc 000000000000ea14 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv12NewStringUTFEPKc+44) ......
#06 pc 000000000000e89c /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+100)
mikrom /* TID 5641 */
mikrom [+] JNIEnv->CallObjectMethodV
mikrom |- jmethodID :0x277 {java.lang.String cn.mik.nativedemo.MainActivity.demo(int, float, java.lang.String)}
mikrom |- va_list :0x7ffb41b790
mikrom |: jint : 1
mikrom |: jfloat : 2
mikrom |: jstring : newdemo
mikrom |= jstring :3.0newdemo
-------------------------mikrom Backtrace-------------------------
#06 pc 000000000000eae4 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv16CallObjectMethodEP8_jobjectP10_jmethodIDz+196)
#07 pc 000000000000e8bc /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+132)
mikrom /* TID 5641 */
mikrom [+] JNIEnv->GetStringUTFChars
mikrom |- jboolean* : 0
mikrom |= char* : 3.0newdemo
-------------------------mikrom Backtrace-------------------------
#05 pc 000000000000eb54 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh+52)
#06 pc 000000000000e8d0 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+152)
mikrom /* TID 5641 */
mikrom [+] JNIEnv->NewStringUTF
mikrom |- char* : 3.0newdemo
-------------------------mikrom Backtrace-------------------------
#05 pc 000000000000ea14 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (_ZN7_JNIEnv12NewStringUTFEPKc+44)
#06 pc 000000000000e910 /data/app/~~MjwExmAtQBa8X1Xp3ifz_g==/cn.mik.nativedemo-YbmCkQ7SdhNqbL7iXfOeug==/lib/arm64/libnativedemo.so (Java_cn_mik_nativedemo_MainActivity_stringFromJNI+216)
mikrom leave jni java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI()
``` ```
## 12.7 本章小结
本章完整介绍了系统代码插桩方式来实现`Jnitrace`功能。该功能用于动态跟踪分析程序执行时的JNI调用参数与栈详情信息。对比python版本的`Jnitrace`工具使用Frida代码编写的跟踪工具拥有着便捷与扩展性强等特点但缺点是这种方式需要处理大量调用对程序的并发处理能力要求极高在一些JNI调用频繁的程序上执行分析可能会出现并发崩溃的情况使用系统代码插桩的优点是在高并发调用下跟踪代码仍然能正常的输出分析内容具有较高的稳定性。
系统定制涉及到技术原理与代码解读的部分着实枯燥,很感谢读者朋友们能坚持看完本书。本书所有的内容到这里就要讲完了,然后,值得被扩展与定制的功能非常之多,限于篇幅限制,作者水平十分有限,很多功能没有在本书中涉及与讨论。有兴趣的朋友,可以关注微信公众号[软件安全与逆向分析]微信号feicong_sec。作者会在上面讨论一些系统定制修改与安全技术相关的话题。