源码杂谈

NDK中so的加载分析

//java层加载
#参数为库文件名,不包含库文件的扩展名,必须是在JVM属性Java.library.path所指向的路径中
System.loadLibrary("libname");
#参数必须为库文件的绝对路径,可以是任意路径;  
System.load("lib_path");    
libcore/ojluni/src/main/java/java/lang/System.java
public static void load(String filename) {
      Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
}

public static void loadLibrary(String libname) {
      Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
libcore/ojluni/src/main/java/java/lang/Runtime.java
synchronized void load0(Class<?> fromClass, String filename) {
    if (!(new File(filename).isAbsolute())) {
        throw new UnsatisfiedLinkError("Expecting an absolute path of the library: " + filename);
    }
    if (filename == null) {
        throw new NullPointerException("filename == null");
    }
    String error = nativeLoad(filename, fromClass.getClassLoader());
    if (error != null) {
        throw new UnsatisfiedLinkError(error);
    }
}

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) {
            throw new UnsatisfiedLinkError(loader + " couldn't find \"" +System.mapLibraryName(libraryName) + "\"");
        }
        String error = nativeLoad(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 = nativeLoad(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);
 }
libcore/ojluni/src/main/native/Runtime.c
JNIEXPORT jstring JNICALL
Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename,jobject javaLoader)
{
    return JVM_NativeLoad(env, javaFilename, javaLoader);
}
art/openjdkjvm/OpenjdkJvm.cc
JNIEXPORT jstring JVM_NativeLoad(JNIEnv* env,
                                 jstring javaFilename,
                                 jobject javaLoader) {
  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,
                                         &error_msg);
    if (success) {
      return nullptr;
    }
  }
  env->ExceptionClear();
  return env->NewStringUTF(error_msg.c_str());
}
art/runtime/java_vm_ext.cc

java_vm_ext.cc ---> native_loader.cpp ---> OpenNativeLibrary()

java_vm_ext.cc ---> FindSymbol()

bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                  const std::string& path,
                                  jobject class_loader,
                                  std::string* error_msg) {
  ...
  OpenNativeLibrary(){
      ...
      android_dlopen_ext()
      ...
  }
  FindSymbol(){
    ...
    FindSymbolWithNativeBridge()/FindSymbolWithoutNativeBridge(){
      ...
      dlsym()
      ...
    }
    ...
  }
  ...
}

简单归纳一下调用流程:

System.loadLibrary()
Runtime.loadLibrary()
Runtime.loadLibrary0()
Runtime.java ---> nativeload()
Runtime.c ---> Runtime_nativeLoad()
OpenjdkJvm.cc ---> JVM_NativeLoad()
java_vm_ext.cc ---> LoadNativeLibrary()

LoadNativeLibrary() 分为两步
1.dlfcn.cpp ---> dlopen() //加载init()/init_array()
2.libdl.cpp ---> dlsym() //加载jni_onload()

frida-trace展示调用流程

继续往后走进入到init/init_array

extern "C" 
void _init(void) { } 
      -------> 编译生成后在.init段  
  
__attribute__((constructor)) 
void _init(void) { } 
      -------》编译生成后在.init_array段  
      
__attribute__((destructor))
void final(void) { }
      -------》编译生成后在.final_array段  
      
# RegisterNative效率高于直接调用JNI本地函数(静态注册)

- 1.调用 System. loadlibrarye()方法,将包含本地方法具体实现的C++运行库加载![JB7_XF%YY6S1]I367G3)TST.png](https://upload-images.jianshu.io/upload_images/9548468-3c52ae3c0609d2ae.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

到内存中
- 2.Java虚拟机检索加载进来的库函数符号,在其中査找与Java本地方法拥有相同签名的JNI本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数
- 3.Android Framework这类复杂的系统下,拥有大量的包含本地方法的Java类,Java虛机加载相应运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率,所以有了RegisterNative()
      
      
# 调用顺序
linker

#  \bionic\linker\dlfcn.cpp  
--->  do_dlopen() ---> find_library() 

## \bionic\linker\linker.cpp  
---> CallConstructors()

# 调用init  
---> CallFunction("DT_INIT", init_func)     

# 调用init_array  
---> CallArray("DT_INIT_ARRAY", init_array, init_array_count, false); 
    

CallConstructors

// so库文件加载完毕以后调用构造函数  
void soinfo::CallConstructors() {  
      
  if (constructors_called) {  
    return;  
  }  
  
  // We set constructors_called before actually calling the constructors, otherwise it doesn't  
  // protect against recursive constructor calls. One simple example of constructor recursion  
  // is the libc debug malloc, which is implemented in libc_malloc_debug_leak.so:  
  // 1. The program depends on libc, so libc's constructor is called here.  
  // 2. The libc constructor calls dlopen() to load libc_malloc_debug_leak.so.  
  // 3. dlopen() calls the constructors on the newly created  
  //    soinfo for libc_malloc_debug_leak.so.  
  // 4. The debug .so depends on libc, so CallConstructors is  
  //    called again with the libc soinfo. If it doesn't trigger the early-  
  //    out above, the libc constructor will be called again (recursively!).  
  constructors_called = true;  
  
  if ((flags & FLAG_EXE) == 0 && preinit_array != NULL) {  
    // The GNU dynamic linker silently ignores these, but we warn the developer.  
    PRINT("\"%s\": ignoring %d-entry DT_PREINIT_ARRAY in shared library!",  
          name, preinit_array_count);  
  }  
  
  // 调用DT_NEEDED类型段的构造函数  
  if (dynamic != NULL) {  
    for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {  
      if (d->d_tag == DT_NEEDED) {  
        const char* library_name = strtab + d->d_un.d_val;  
        TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name);  
        find_loaded_library(library_name)->CallConstructors();  
      }  
    }  
  }  
  
  TRACE("\"%s\": calling constructors", name);  
  
  // DT_INIT should be called before DT_INIT_ARRAY if both are present.  
  // 先调用.init段的构造函数  
  CallFunction("DT_INIT", init_func);  
  // 再调用.init_array段的构造函数  
  CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);  
}  

CallFunction

// 构造函数调用的实现  
void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) {  
  
  // 判断构造函数的调用地址是否符合要求  
  if (function == NULL || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {  
    return;  
  }  
  
  // function_name被调用的函数名称,function为函数的调用地址  
  // [ Calling %s @ %p for '%s' ] 字符串为在 /system/bin/linker 中查找.init和.init_array段调用函数的关键  
  TRACE("[ Calling %s @ %p for '%s' ]", function_name, function, name);  
  // 调用function函数  
  function();  
  TRACE("[ Done calling %s @ %p for '%s' ]", function_name, function, name);  
  
  // The function may have called dlopen(3) or dlclose(3), so we need to ensure our data structures  
  set_soinfo_pool_protection(PROT_READ | PROT_WRITE);  
}  

CallArray

void soinfo::CallArray(const char* array_name UNUSED, linker_function_t* functions, size_t count, bool reverse) {  
  if (functions == NULL) {  
    return;  
  }  
  
  TRACE("[ Calling %s (size %d) @ %p for '%s' ]", array_name, count, functions, name);  
  
  int begin = reverse ? (count - 1) : 0;  
  int end = reverse ? -1 : count;  
  int step = reverse ? -1 : 1;  
  
  // 循环遍历调用.init_arrayt段中每个函数  
  for (int i = begin; i != end; i += step) {  
    TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]);  
      
    // .init_arrayt段中,每个函数指针的调用和上面的.init段的构造函数的实现是一样的  
    CallFunction("function", functions[i]);  
  }  
  
  TRACE("[ Done calling %s for '%s' ]", array_name, name);  
}  

以上为安卓4.4.4.rc的源码
以下为安卓9.0.0_r8的源码


第一个dlfcn.cpp 中的申明就不看了,直接看到linker.cpp,代码量确实比4.4.4多多了,但是逻辑都差不多,可以找到 call_constructors

call_constructors源码

call_constructorsIDA反汇编版
找到两个关键点调用
call_function
  • 和以前一样 call_array() 就是遍历一个数组重复去调用 call_function()
  • 守住 call_function() 就可以守住 init()init_array()
安卓9.0.0_r8 linker断点位置

下面归纳一下从源码的角度考虑脱壳方向

5.0以下的脱壳时机

dvmDexFileOpenPartial(addr, len, &pDvmDex)
dexFileParse(const u1* data, size_t length, int flags)

5.0~8.0以下的脱壳时机

DexFile::
OpenMemory
(const byte* base ,size_t size, const std::string& location, uint32_t location_checksum, MemMap* mem_map)

8.0及8.0以上的脱壳时机

DexFile::
OpenCommon
(const uint8_t* base, size_t size, const std::string& location, uint32_t location_checksum, const OatDexFile* oat_dex_file, bool verify, bool verify_checksum, std::string* error_msg, VerifyResult* verify_result)

OpenCommon

  • 汇编位于libdexfile.so (/system/lib/libdexfile.so)
  • 源码位于dex_file_loader.cc (源码
OpenCommon偏移地址

代表脱壳项目:frida-unpack

拦截位置0
拦截位置1

说到脱壳除了系统关键函数的拦截,还有另一个思路来自hluwa的FRIDA-DEXDump
思路是基于frida的内存dex035魔数的搜索

frida_dexdump

除了使用frida的动态注入也可以选择使用ida调试断点关键函数,使用idc脚本dump内存,或者是使用xposed插件来实现脱壳,xposed插件:比如WrBug的 dumpDex 以及集成了 dumpDex 的 易开发 ,或者是 ApkShelling 都可以用来脱壳,以上针对一二代壳,至于三代壳可以考虑使用DexHunter

或者是使用frida_dump
dump_dex使用到的关键点在libart.so中的DefineClass

DefineClass

  • 基于内存关键字搜索(dex035)
  • 基于openCommen函数断点
  • 基于DefineClass函数断点
  • Fart 函数方法体抽取

Android ClassLoader

对于ClassLoader的基础理解可以让我在插件化,或者是存在动态加载dex的时候找到dex,frida枚举classloader并尝试加载heap中存在class即可找到对应dex的classloader,自然找到了classloader的pathList,一般情况下加壳的apk使用的ClassLoader也是自定义继承自PathClassLoader,所以在使用frida脚本的时候也需要我们手动的去切换一下ClassLoader,使用默认的PathClassLoader可能会找不到类

ClassLoader继承关系

BootClassLoader
位于:libcore/ojluni/src/main/java/java/lang/ClassLoader.java

SecureClassLoader
位于:libcore/ojluni/src/main/java/java/security/SecureClassLoader.java
(URLClassLoader extends SercureClassLoader)

URLClassLoader
位于:libcore/ojluni/src/main/java/java/net/URLClassLoader.java

BootstrapClassLoader
位于:external/icu/icu4j/main/classes/core/src/com/ibm/icu/impl/*ClassLoaderUtil.java

BaseDexClassLoader
位于:libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
(和java虚拟机中不同的是BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见)

PathClassLoader
位于:libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java

DexClassLoader
位于:libcore/dalvik/src/main/java/dalvik/system/DexClassLoader.java

以下着重分析最常见,常用的 BaseDexClassLoader

BaseDexClassLoader的构造函数包含四个参数,分别为:
  • dexPath,指目标类所在的APK或jar文件的路径,类装载器将从该路径中寻找指定的目标类
  • File optimizedDirectory,由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex,PathClassLoader默认文件存放的路径/data/dalvik-cache
  • libPath,指目标类中所使用的C/C++库存放的路径
  • ClassLoader,是指该装载器的父装载器,一般为当前执行类的装载器

源码中可以发现BaseDexClassLoader维护了一个
DexPathList pathList
DexPathList中维护一个dex列表
Element[] dexElements
DexFile用来解析dex
DexFile dexFile;

    public Enumeration<String> entries() {
        return new DFEnum(this);
    }

    /*
     * Helper class.
     */
    private static class DFEnum implements Enumeration<String> {
        private int mIndex;
        @UnsupportedAppUsage
        private String[] mNameList;

        DFEnum(DexFile df) {
            mIndex = 0;
            mNameList = getClassNameList(df.mCookie);
        }

        public boolean hasMoreElements() {
            return (mIndex < mNameList.length);
        }

        public String nextElement() {
            return mNameList[mIndex++];
        }
    }
从Dex中获取所有类
function HookDexMethod(){
    Java.perform(function(){
        var DexFileclass = Java.use("dalvik.system.DexFile");
        var DexPathListclass = Java.use("dalvik.system.DexPathList");
        var BaseDexClassLoaderclass = Java.use("dalvik.system.BaseDexClassLoader");
        Java.enumerateClassLoaders({
            onMatch: function (loader) {
                try{
                    console.log("\n"+loader)
                    var BaseDexClassLoader_obj = Java.cast(loader, BaseDexClassLoaderclass);
                    var pathList = BaseDexClassLoader_obj.pathList.value;
                    console.warn("\npathList\t---->\t",pathList);
                    var dexElements = Java.cast(pathList, DexPathListclass).dexElements.value;
                    console.warn("\ndexElements\t---->\t",dexElements);
                    dexElements.forEach(function(element,index,arrays){
                        try {
                            try{var dexfile = element.dexFile.value;}catch(e){}
                            var dexfileobj = Java.cast(dexfile, DexFileclass);
                            const DFEnumName = dexfileobj.entries();
                            while(DFEnumName.hasMoreElements()){
                                var className = DFEnumName.nextElement().toString()
                                if(className.indexOf("lzy")!=-1){
                                    console.log(className)
                                }
                            }
                        }catch(e){
                            console.log(e)
                        }
                    })
                }catch(e){
                }
            },
            onComplete:function(){}
        })
    })
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342