[Android]对init_array段调用的方法进行Hook
对init_array段调用的方法进行Hook
前言
在某个风和日丽、阳光灿烂、万里无云、空气清新、绿树成荫、蝉鸣如雷、炎热难耐、烈日当空、热浪滚滚、炎夏炙烤、炎炎夏日、烈日炎炎、艳阳高照、晴空万里、暑气逼人、炙热的阳光、烈日炎炎、汗如雨下、火辣辣的天气、烈日当空、酷热难耐的下午,Dev1l师傅发来了一个Frida检测的小Demo,反编译后发现主要逻辑都在init_array段,故写下本篇文章,记录下探索中学到的知识。
情景复现
由于Dev给的Demo用途还需要保密,因此这里我写一个init_array执行逻辑相同的Demo,来演示如何通过Fridahook .init_array段中的内容。
Demo Java层实现
package com.swdd.testapp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import com.swdd.testapp.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("testapp");
}
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
TextView tv = binding.sampleText;
tv.setText(stringFromJNI());
}
public native String stringFromJNI();
}
没错就是简单的Android studio生成的一个模板,加载了Native方法之后就会更改MainActivity渲染的一个TextView。
Native层实现
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_swdd_testapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
本身Native层是返回一个Hello from C++,那我们要如何在.init_array中运行它呢请看接下来的代码。
#include <jni.h>
#include <string>
#include <android/log.h>
#define LOG_TAG "GenFunction"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
char Text[512] = "Hello from C++";
static void genText(const char * newText){
strcat(Text,newText);
}
static void Gen() __attribute__((constructor()));
static void Gen(){
genText("\nHello .init_array");
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_swdd_testapp_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
genText("\nhello stringFromJNI");
return env->NewStringUTF(Text);
}
我们只需要在需要放到.init_array中运行的方法生命的时候加一段描述就好__attribute__((constructor()))
此处其实也可以使用section去描述段名,我们这里使用constructor就是代表的.init_arry段。接下来看看app运行效果吧:

这里为了方便后续演示,我分别在.init_array中和strFromJni中各调用了一次。
尝试开始HOOK
首先我们对Native层进行分析。

指的注意的是我们这里的genText是非导出的情况,所以我们讨论的也是非导出情况下的Hook。
首先我们在segments窗口中看到各个段的偏移
双击.init_array即可进入。

这里我们可以记录下genText方法的偏移是0x808

这样我们第一次尝试Hook:
function main() {
Java.perform(function () {
var baseAddress = Module.getBaseAddress("libtestapp.so");
console.log(baseAddress);
Interceptor.attach(baseAddress.add(0x808), {
onEnter: function (args) {
var input = args[0].readUtf8String();
console.log("Hook Success -> ", input);
}, onLeave: function (retval) { }
});
});
}
//setImmediate(main);
setTimeout(main, 200);
此时发现问题,我们如果使用setTimeout(main,200)
的话就只能Hook到fromJni了,.init_array由于时间问题导致无法HOOK,那么我们如果改成setImmediate会如何呢:
由于时间过早,还没有加载到libtestapp.so我们就开始hook了导致进程报错。
解决思路
首先根据so的加载机制:
+---------------------+ +------------------------+
| | | |
| Java Runtime | | Native Runtime |
| (Dalvik/ART VM) | | (Native Executable) |
| | | |
+---------------------+ +------------------------+
| |
| |
| System.loadLibrary() |
|------------------------------>|
| |
| |
| Runtime Linker |
| (e.g., ld.so on Linux) |
| |
| +-------------------------+ |
| | | |
| | Load .so File | |
| | (dlopen) | |
| | | |
| +-------------------------+ |
| | |
| | |
| +-------------------------+ |
| | | |
| | Resolve Symbols | |
| | (dlsym) | |
| | | |
| +-------------------------+ |
| | |
| | |
| +-------------------------+ |
| | | |
| | Initialize .so | |
| | | |
| +-------------------------+ |
| | |
| | |
| +-------------------------+ |
| | | |
| | Execution | |
| | (Invoke Native Code) | |
| | | |
| +-------------------------+ |
| |
| |
那么最开始要启动的肯定是dlopen了,(在 android 7.0 之后使用的是 android_dlopen_ext),该方法储存在libdl.so 中,因此我们首先可以通过加载libdl.so,对android_dlopen_ext进行Hook,来获取更早的时机。
Hook android_dlopen_ext:
function main() {
var dlopenAdd = Module.findExportByName("libdl.so", "android_dlopen_ext");
Interceptor.attach(dlopenAdd, {
onEnter: function (args) {
console.log("Loaded dlopen with -> " + args[0].readCString());
}, onLeave(retVal) {
}
})
}
setImmediate(main);

可以发现加载libtestapp.so的时机还挺早的,接下来我们就要对libtestapp.so这个字段做过滤了,找到libtestapp.so才做处理
function main() {
var dlopenAdd = Module.findExportByName("libdl.so", "android_dlopen_ext");
Interceptor.attach(dlopenAdd, {
onEnter: function (args) {
if (args[0].readCString().indexOf("libtestapp.so") != -1) {
console.log("Loaded dlopen with -> " + args[0].readCString());
}
}, onLeave(retVal) {
}
})
}
setImmediate(main);
如果此时直接Hook依旧会产生报错:

说明时机依旧还是太早了,那么如何操作呢,我们看到如下流程:
+------------------------+ +-------------------------+
| | | |
| Android Framework | | Native Executable |
| | | |
+------------------------+ +-------------------------+
| |
| loadLibrary("mylib") |
|---------------------------------->|
| |
| Runtime Linker (ld.so) |
|---------------------------------->|
| |
| Find and Load mylib.so |
|---------------------------------->|
| |
| Call Constructors |
| (callConstructorAdd) |
|---------------------------------->|
| |
| |
| Initialize |
| (init_array) |
|---------------------------------->|
| |
| Execute |
| (main function) |
|---------------------------------->|
| |
可以发现Init_array之前还有一个 Call Constructors , 这个时候已经加载了libtestapp.so了,我们即可进行操作。
Hook callConstructorAdd
callConstructorAdd函数在编译后导出名字为:__dl__ZN6soinfo17call_constructorsEv
Symbols = Process.getModuleByName("linker64").enumerateSymbols(); 去获取他的相对偏移就好了。
function startHook() {
Java.perform(function () {
var baseAddress = Module.getBaseAddress("libtestapp.so");
console.log(baseAddress);
Interceptor.attach(baseAddress.add(0x808), {
onEnter: function (args) {
var input = args[0].readUtf8String();
console.log("Hook Success -> ", input);
}, onLeave: function (retval) { }
});
});
}
function main() {
var dlopenAdd = Module.findExportByName("libdl.so", "android_dlopen_ext");
Interceptor.attach(dlopenAdd, {
onEnter: function (args) {
if (args[0].readCString().indexOf("libtestapp.so") != -1) {
console.log("Loaded dlopen with -> " + args[0].readCString());
var Symbols = Process.getModuleByName("linker64").enumerateSymbols();
for (var index = 0; index < Symbols.length; index++) {
if (Symbols[index].name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
console.log("callConstructorAdd -> " , Symbols[index].address);
}
}
}
}, onLeave(retVal) {
}
})
}
setImmediate(main);
//setTimeout(main, 200);
这样我们就拿到了callConstructorAdd内存中的地址:
接下来就可以Hook它了:
function main() {
var dlopenAdd = Module.findExportByName("libdl.so", "android_dlopen_ext");
Interceptor.attach(dlopenAdd, {
onEnter: function (args) {
if (args[0].readCString().indexOf("libtestapp.so") != -1) {
console.log("Loaded dlopen with -> " + args[0].readCString());
var Symbols = Process.getModuleByName("linker64").enumerateSymbols();
for (var index = 0; index < Symbols.length; index++) {
if (Symbols[index].name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
console.log("callConstructorAdd -> ", Symbols[index].address);
Interceptor.attach(Symbols[index].address, {
onEnter: function (args) {
console.log("callConstructorAdd Called!");
}, onLeave: function (retval) { }
});
}
}
}
}, onLeave(retVal) {
}
})
}
setImmediate(main);

这个时候我们发现调用了非常多次,所以我们需要做一个Hook过的标记,Hook过之后就不再hook了,接下来我们再调用hook .init_array中调用的方法的代码:
EXP:
注意StartHook在onLeave和onEnter中效果是一样的。
function startHook() {
Java.perform(function () {
var baseAddress = Module.getBaseAddress("libtestapp.so");
console.log(baseAddress);
Interceptor.attach(baseAddress.add(0x808), {
onEnter: function (args) {
var input = args[0].readUtf8String();
console.log("Hook Success -> ", input);
}, onLeave: function (retval) { }
});
});
}
function main() {
var dlopenAdd = Module.findExportByName("libdl.so", "android_dlopen_ext");
var isHooked = false;
Interceptor.attach(dlopenAdd, {
onEnter: function (args) {
if (args[0].readCString().indexOf("libtestapp.so") != -1) {
console.log("Loaded dlopen with -> " + args[0].readCString());
var Symbols = Process.getModuleByName("linker64").enumerateSymbols();
for (var index = 0; index < Symbols.length; index++) {
if (Symbols[index].name.indexOf("__dl__ZN6soinfo17call_constructorsEv") != -1) {
console.log("callConstructorAdd -> ", Symbols[index].address);
Interceptor.attach(Symbols[index].address, {
onEnter: function (args) {
if (!isHooked) {
console.log("callConstructorAdd Called!");
isHooked = true;
startHook();
}
}, onLeave: function (retval) { }
});
}
}
}
}, onLeave(retVal) {
}
})
}
setImmediate(main);
//setTimeout(main, 200);