Android动态链接库so的加载与调试

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.ccLoadNativeLibrary()函数,此函数继续检测并执行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

  1. adb shell ./android_server
  2. new shell window: adb forward tcp:23946 tcp:23946
  3. IDA 打开dex
  4. 下断点
  5. 设置debugger remote arm
  6. 设置process option localhost
  7. 打开app
  8. attach

IDA调试so

断在JNI方法

静态下断点,然后attach

  1. adb shell ./android_server
  2. new shell window: adb forward tcp:23946 tcp:23946
  3. IDA 打开so
  4. 下断点
  5. 设置debugger remote arm
  6. 设置process option localhost
  7. 打开app
  8. attach

断在.init,init_array,JNI_Onload

由于我们这里一般采用attach附加调试,即在我们attach之前已经启动了相应的进程。所以如果进程加载了so动态链接库,其中.init段、.init_array段、JNI_Onload()方法已经执行完毕,采用如上的方法下断点也不会停。

方法一:静态下断点(只开一个IDA窗口)

静态下断点,调试启动app,然后attach,jdb恢复调试

  1. adb shell ./android_server
  2. new shell window: adb forward tcp:23946 tcp:23946
  3. ida 打开 so
  4. 下断点(.init,init_array,jni_Onload)
  5. 设置debugger remote arm
  6. 设置process option localhost(到此为止和之前完全相同)
  7. 打开ddms(jdk8)
  8. adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
  9. attach
  10. jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
方法二:双开定位(开启两个IDA窗口)
  1. adb shell ./android_server
  2. new shell window: adb forward tcp:23946 tcp:23946
  3. 第一个IDA 打开 so
  4. 计算.init,init_array,JNI_Onload的偏移
  5. 开启另一个空白IDA,设置debugger remote arm
  6. 设置process option其中的hostname为localhost
  7. 设置debugger options 勾选 suspend on library load/unload 以及suspend on process entry point
  8. 打开ddms(jdk8)
  9. adb shell am start -D -n com.yaotong.crackme/com.yaotong.crackme.MainActivity
  10. attach
  11. jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700
  12. 在IDA中运行调试F9直到断到linker处,查看模块窗口中调试so的基地址
  13. 通过刚才计算的偏移得到断点真实地址,下断点,运行
两方法区别

二者在调试时机以及成功下断点的原理上没有区别,区别是我们何时让IDA知道断点下在哪里

  • 静态下断点时,当IDA动态调试的时候会识别相同的模块,然后自动同步断点的位置
  • 双开定位是我们自己告诉IDA断点下在哪里,稍有繁琐

注:当采取静态下断点时,有时会出现其他模块的汇编出现识别的问题,我在学习中就发现我在这种模式下的libc.so中的fopen函数的汇编不能被很好的识别,导致无法继续调试。而当我采取双开定位这种方式时,fopen可以正确被识别。猜测也许是IDA在同步相同的模块时出现了问题,所以当遇到问题时也许可以多尝试几种方法。

反调试

  • 现象:当我们尝试着去附加到一个进程时,进程却闪退了,这里即和有可能存在反调试
  • 原理:在应用初始化时可能启动了一些反调试的线程来进行测试,代码实现很有可能就在.init段 .init_array段以及JNI_Onload()函数中,当然也可能在JAVA代码中
  • 解决:将断点下在未运行反调试机制之前,然后想办法破坏反调试机制,patch掉响应函数,或者修改其判断的寄存器的值等等