so加载过程(linker前)
我们在java中是通过,System.loadLibrary()方法加载so的,这里跟踪一下其调用过程,可以通过http://androidxref.com/查看源码,这里我们以android的8.0.0_r4
版本为例
static {
System.loadLibrary("TestJni");
}
private static native String getResult();
System.loadLibrary()
方法定义位于:
/libcore/ojluni/src/main/java/java/lang/System.java
第1656行
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
方法调用了loadLibrary0()方法
loadLibrary0()
方法定义位于:
libcore/ojluni/src/main/java/java/lang/Runtime.java
第998行
synchronized void loadLibrary0(ClassLoader loader, String libname) {
if (libname.indexOf((int)File.separatorChar) != -1) {
throw new UnsatisfiedLinkError(
"Directory separator should not appear in library name: " + libname);
}
String libraryName = libname;
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : getLibPaths()) {
String candidate = directory + filename;
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
String error = doLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
lastError = error;
}
}
if (lastError != null) {
throw new UnsatisfiedLinkError(lastError);
}
throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}
这个方法主要有两个作用
- 找到lib的全称
- 调用doLoad加载lib库
我们继续跟踪doLoad函数
doLoad()
方法定义位于
libcore/ojluni/src/main/java/java/lang/Runtime.java
第1070行
private String doLoad(String name, ClassLoader loader) {
// Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
// which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
// The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
// libraries with no dependencies just fine, but an app that has multiple libraries that
// depend on each other needed to load them in most-dependent-first order.
// We added API to Android's dynamic linker so we can update the library path used for
// the currently-running process. We pull the desired path out of the ClassLoader here
// and pass it to nativeLoad so that it can call the private dynamic linker API.
// We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
// beginning because multiple apks can run in the same process and third party code can
// use its own BaseDexClassLoader.
// We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
// dlopen(3) calls made from a .so's JNI_OnLoad to work too.
// So, find out what the native library search path is for the ClassLoader in question...
String librarySearchPath = null;
if (loader != null && loader instanceof BaseDexClassLoader) {
BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
librarySearchPath = dexClassLoader.getLdLibraryPath();
}
// nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
// of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
// internal natives.
synchronized (this) {
return nativeLoad(name, loader, librarySearchPath);
}
}
继续调用了nativeLoad函数
nativeLoad()
这个函数的声明位于:
libcore/ojluni/src/main/java/java/lang/Runtime.java
第1104行
private static native String nativeLoad(String filename, ClassLoader loader,
String librarySearchPath);
发现这是一个native方法,那么函数的实现则应该是在c的源码中,经过搜索找到源码位于:
libcore/ojluni/src/main/native/Runtime.c
第77行
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,
jobject javaLoader, jstring javaLibrarySearchPath)
{
return JVM_NativeLoad(env, javaFilename, javaLoader, javaLibrarySearchPath);
}
继续调用了JVM_NativeLoad()方法
JVM_NativeLoad()
方法定义位于:
art/runtime/openjdkjvm/OpenjdkJvm.cc
第322行
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
jstring javaFilename,
jobject javaLoader,
jstring javaLibrarySearchPath) {
ScopedUtfChars filename(env, javaFilename);
if (filename.c_str() == NULL) {
return NULL;
}
std::string error_msg;
{
art::JavaVMExt* vm = art::Runtime::Current()->GetJavaVM();
bool success = vm->LoadNativeLibrary(env,
filename.c_str(),
javaLoader,
javaLibrarySearchPath,
&error_msg);
if (success) {
return nullptr;
}
}
// Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
env->ExceptionClear();
return env->NewStringUTF(error_msg.c_str());
}
继续调用LoadNativeLibrary()方法
LoadNativeLibrary()
方法定义位于:
art/runtime/java_vm_ext.cc
第766行,此方法异常复杂,我们不做深入分析,该方法大致做以下几步
- 调用android::OpenNativeLibrary打开lib库
- 调用library->FindSymbol(“JNI_OnLoad”, nullptr)找到lib中的JNI_OnLoad这个方法
- 执行JNI_OnLoad方法
android::OpenNativeLibrary()
方法定义位于:
system/core/libnativeloader/native_loader.cpp
第458行,位于名为android的名称空间中
void* OpenNativeLibrary(JNIEnv* env,
int32_t target_sdk_version,
const char* path,
jobject class_loader,
jstring library_path,
bool* needs_native_bridge,
std::string* error_msg) {
#if defined(__ANDROID__)
UNUSED(target_sdk_version);
if (class_loader == nullptr) {
*needs_native_bridge = false;
return dlopen(path, RTLD_NOW);
}
std::lock_guard<std::mutex> guard(g_namespaces_mutex);
NativeLoaderNamespace ns;
if (!g_namespaces->FindNamespaceByClassLoader(env, class_loader, &ns)) {
// This is the case where the classloader was not created by ApplicationLoaders
// In this case we create an isolated not-shared namespace for it.
if (!g_namespaces->Create(env,
target_sdk_version,
class_loader,
false,
library_path,
nullptr,
&ns,
error_msg)) {
return nullptr;
}
}
if (ns.is_android_namespace()) {
android_dlextinfo extinfo;
extinfo.flags = ANDROID_DLEXT_USE_NAMESPACE;
extinfo.library_namespace = ns.get_android_ns();
void* handle = android_dlopen_ext(path, RTLD_NOW, &extinfo);
if (handle == nullptr) {
*error_msg = dlerror();
}
*needs_native_bridge = false;
return handle;
} else {
void* handle = NativeBridgeLoadLibraryExt(path, RTLD_NOW, ns.get_native_bridge_ns());
if (handle == nullptr) {
*error_msg = NativeBridgeGetError();
}
*needs_native_bridge = true;
return handle;
}
#else
UNUSED(env, target_sdk_version, class_loader, library_path);
*needs_native_bridge = false;
void* handle = dlopen(path, RTLD_NOW);
if (handle == nullptr) {
if (NativeBridgeIsSupported(path)) {
*needs_native_bridge = true;
handle = NativeBridgeLoadLibrary(path, RTLD_NOW);
if (handle == nullptr) {
*error_msg = NativeBridgeGetError();
}
} else {
*needs_native_bridge = false;
*error_msg = dlerror();
}
}
return handle;
#endif
}
继续调用dlopen()方法打开lib库
dlopen()
在android8.0.0_r4源码版本中方法定义位于:
bionic/libdl/libdl.c
第101行
在android7.0.0_r1源码版本中方法定义位于:
bionic/linker/dlfcn.cpp
第85行
可以看到在老版本android源码中,我们已经进入了linker的实现部分
so加载中执行的函数顺序
linker是Android系统动态库so的加载器/链接器,并且linker本身就是一个动态链接库。当linker加载so时,会先执行so文件中的.init
段代码,然后执行.init_array
段中所指向的函数。当linker加载完返回到位于:art/runtime/java_vm_ext.cc
的LoadNativeLibrary()
函数,此函数继续检测并执行so库中的JNI_Onload()
方法。
所以在整个so加载过程中函数执行的顺序如下:
.init段 -> .init_array段指向的函数 -> JNI_Onload() -> java_com_XXX
编写init段代码
在编写so的c++工程中添加如下代码
#ifdef __cplusplus
extern "C" {
#endif
void _init(void){
//函数体
}
#ifdef __cplusplus
}
#endif
编写.init_array段代码
在编写so的c++工程中添加如下代码
void __attribute__((constructor)) myConstructor(void){
//函数体
}
编写JNI_Onload()
在编写so的c++工程中添加如下代码
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){
//函数体
}
IDA调试
IDA 调试介绍
本地调试和远程调试
- 本地调试是在本机上直接运行或者附加一个进程
- 远程调试需要在远程的机器上运行IDA的一个server,才可以进行启动调试和附加调试
run 和 attach 的区别
就是IDA何时参与调试程序的区别:
- run为用IDA启动一个进程去调试
- attach为当一个进程已经启动以后再用IDA附加上去调试
- 当调试启动以后两者没有区别
一般我们选择attach方法进行调试
IDA 调试dex
静态下断点,然后attach
- adb shell ./android_server
- new shell window: adb forward tcp:23946 tcp:23946
- IDA 打开dex
- 下断点
- 设置debugger remote arm
- 设置process option localhost
- 打开app
- attach
IDA调试so
断在JNI方法
静态下断点,然后attach
- adb shell ./android_server
- new shell window: adb forward tcp:23946 tcp:23946
- IDA 打开so
- 下断点
- 设置debugger remote arm
- 设置process option localhost
- 打开app
- attach
断在.init,init_array,JNI_Onload
由于我们这里一般采用attach附加调试,即在我们attach之前已经启动了相应的进程。所以如果进程加载了so动态链接库,其中.init段、.init_array段、JNI_Onload()方法已经执行完毕,采用如上的方法下断点也不会停。
方法一:静态下断点(只开一个IDA窗口)
静态下断点,调试启动app,然后attach,jdb恢复调试
- adb shell ./android_server
- new shell window: adb forward tcp:23946 tcp:23946
- ida 打开 so
- 下断点(.init,init_array,jni_Onload)
- 设置debugger remote arm
- 设置process option localhost(到此为止和之前完全相同)
- 打开ddms(jdk8)
adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
- attach
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
方法二:双开定位(开启两个IDA窗口)
- adb shell ./android_server
- new shell window: adb forward tcp:23946 tcp:23946
- 第一个IDA 打开 so
- 计算.init,init_array,JNI_Onload的偏移
- 开启另一个空白IDA,设置debugger remote arm
- 设置
process option
其中的hostname为localhost - 设置
debugger options
勾选suspend on library load/unload
以及suspend on process entry point
- 打开ddms(jdk8)
adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
- attach
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
- 在IDA中运行调试F9直到断到linker处,查看模块窗口中调试so的基地址
- 通过刚才计算的偏移得到断点真实地址,下断点,运行
两方法区别
二者在调试时机以及成功下断点的原理上没有区别,区别是我们何时让IDA知道断点下在哪里
- 静态下断点时,当IDA动态调试的时候会识别相同的模块,然后自动同步断点的位置
- 双开定位是我们自己告诉IDA断点下在哪里,稍有繁琐
注:当采取静态下断点时,有时会出现其他模块的汇编出现识别的问题,我在学习中就发现我在这种模式下的libc.so中的fopen函数的汇编不能被很好的识别,导致无法继续调试。而当我采取双开定位这种方式时,fopen可以正确被识别。猜测也许是IDA在同步相同的模块时出现了问题,所以当遇到问题时也许可以多尝试几种方法。
反调试
- 现象:当我们尝试着去附加到一个进程时,进程却闪退了,这里即和有可能存在反调试
- 原理:在应用初始化时可能启动了一些反调试的线程来进行测试,代码实现很有可能就在.init段 .init_array段以及JNI_Onload()函数中,当然也可能在JAVA代码中
- 解决:将断点下在未运行反调试机制之前,然后想办法破坏反调试机制,patch掉响应函数,或者修改其判断的寄存器的值等等