DVM 类加载原理:

DEX 文件加载到内存中 DvmDex 结构后,还没有完成类的解析工作,我们将 DEX 中的类填充到 ClassObject 结构的过程称为类加载。

ClassObject 用来描述一个完整的类,其中 Method 结构用于描述类的方法:


struct ClassObject : Object {
-- snip --
/* static, private, and <init> methods */
int directMethodCount;
Method* directMethods;
/* virtual methods defined in this class; invoked through vtable */
int virtualMethodCount;
Method* virtualMethods;
-- snip --
};

其包含了指令位置指针:

struct Method {
-- snip --
/* the actual code */
const u2* insns; /* instructions, in memory-mapped .dex */
-- snip --
};

Android DVM 提供了三种类加载方法:

(1)使用 Class.forName 显式加载
(2)使用 ClassLoader.loadClass 显式加载
(3)隐式加载,比如 new 操作符,当相应类未被访问过时,则发生隐式加载

其中,

Class.forName 调用 DVM 的 Dalvik_java_lang_Class_classForName 函数;
ClassLoader.loadClass 调用 Dalvik_dalvik_system_DexFile_defineClassNative 函数;
隐式加载调用 dvmResolveClass 函数;

调用关系如下:

DEX dump 时机:

观察 Dalvik_dalvik_system_DexFile_defineClassNative 函数实现:

static void Dalvik_dalvik_system_DexFile_defineClassNative(const u4* args,
JValue* pResult)
{
-- snip --
if (pDexOrJar->isDex)
pDvmDex = dvmGetRawDexFileDex(pDexOrJar->pRawDexFile);
else
pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile); clazz = dvmDefineClass(pDvmDex, descriptor, loader);
-- snip --
}

选取这个函数插入脱壳代码的原因有以下:

  1. 函数位于关键路径
    无论何种类加载方式,必然会执行到 Dalvik_dalvik_system_DexFile_defineClassNative 函数

  2. 包含内存中 DEX 结构,且可以通过 pDexOrJar->fileName 匹配 APK
    通过 pDvmDex = dvmGetJarFileDex(pDexOrJar->pJarFile) 处代码,可以取到内存中 DEX 文件结构信息,这些信息包括:

/*
* Internal struct for managing DexFile.
*/
struct DexOrJar {
char* fileName; // Unique String,可用来命中待脱壳 APP
bool isDex;
bool okayToFree;
RawDexFile* pRawDexFile;
JarFile* pJarFile; // 内存中 zip(APK) 文件结构
u1* pDexMemory; // malloc()ed memory, if any
};

pDvmDex 表示一个打开的 ODEX 文件,DvmDex 结构体有一个 memMap 成员,用来表示 ODEX 文件对应的内存信息:

/*
* Some additional VM data structures that are associated with the DEX file.
*/
struct DvmDex {
-- snip --
/* shared memory region with file contents */
bool isMappedReadOnly;
MemMapping memMap;
-- snip --
};

其中 addr 代表这块内存起始地址,length 代表这块内存大小:

/*
* Use this to keep track of mapped segments.
*/
struct MemMapping {
void* addr; /* start of data */
size_t length; /* length of data */ void* baseAddr; /* page-aligned base address */
size_t baseLength; /* length of mapping */
};

想要 dump 目标 DEX,只需匹配 pDexOrJar->fileName 到相应的 fileName 时,通过 memMap->addr 和 memMap->length 定位到 ODEX 的内存位置, dump 出来即可。

ODEX 文件是为了提高 DVM 运行效率而设计的,它通过将引用到的 framework APIs 替换成预加载 vtable 的索引,提高方法查找和运行效率,因此 ODEX 是与具体设备强相关的,更具体来说是与 /system/framework 目录下的 odex 文件强相关的。

通过 backsmali 与 /system/framework 下 odex,便可以将 ODEX 恢复为 DEX 文件,网上有很多这方面材料,不再赘述。

解决几个问题:

目前,仍然有一个值得关注的问题:一个类加载完,其初始化(如<clinit>)可能仍未执行。

由于 <clinit> 先于其他任何类方法执行,因此加固程序可以在 <clinit> 中做些手脚,实现对方法指令的动态修改,即初始化前我们 dump 出的 DEX 可能完全是错的。

解决办法是,使用 dvmDefineClass 遍历 DEX 的所有类,通过 dvmIsClassInitialized 判断类是否已经初始化过,并调用 dvmInitClass 主动初始化所有类。这样内存中的类,都是初始化过的,这时便可以 dump 出相对正确的 ODEX 文件。

另外还有一个问题:

图中标志的 code_off 表示一个 direct_method 方法的指令字节码相对于 ODEX 头的偏移,而它的取值范围完全可以在 ODEX 内存区域之外,因此如果单纯根据 memMap 的 addr 和 length 进行 dump, 可能缺失关键的指令数据。解决办法是将不在 ODEX 内存区域的指令单独存储为一个 extra 文件中附在 dump 出的 ODEX 之后,并修复 code_off 等偏移。

Dexhunter 的弱点:

DexHunter 通过在类加载过程中插入代码,主动遍历并初始化所有类,然后进行内存 dump。但在类初始化完成后,DVM 并不保证方法指令正确。

因此对抗 DexHunter 的一个办法是,将指令还原选在 Dalvik_dalvik_system_DexFile_defineClassNative 函数执行完,方法指令执行前的某个位置,比如 Hook dvmDefineClass 函数。

另外一个方法是自己实现 Dalvik_dalvik_system_DexFile_defineClassNative 函数,通过 pDexOrJar->fileName 可以发现,360可能用了类似办法。

Dalvik源码阅读笔记(二)的更多相关文章

  1. werkzeug源码阅读笔记(二) 下

    wsgi.py----第二部分 pop_path_info()函数 先测试一下这个函数的作用: >>> from werkzeug.wsgi import pop_path_info ...

  2. werkzeug源码阅读笔记(二) 上

    因为第一部分是关于初始化的部分的,我就没有发布出来~ wsgi.py----第一部分 在分析这个模块之前, 需要了解一下WSGI, 大致了解了之后再继续~ get_current_url()函数 很明 ...

  3. Detectron2源码阅读笔记-(二)Registry&build_*方法

    ​ Trainer解析 我们继续Detectron2代码阅读笔记-(一)中的内容. 上图画出了detectron2文件夹中的三个子文件夹(tools,config,engine)之间的关系.那么剩下的 ...

  4. Dalvik源码阅读笔记(一)

    dalvik 虚拟机启动入口在 JNI_CreateJavaVM(), 在进行完 JNIEnv 等环境设置后,调用 dvmStartup() 函数进行真正的 DVM 初始化. jint JNI_Cre ...

  5. Android源码阅读笔记二 消息处理机制

    消息处理机制: .MessageQueue: 用来描述消息队列2.Looper:用来创建消息队列3.Handler:用来发送消息队列 初始化: .通过Looper.prepare()创建一个Loope ...

  6. Apollo源码阅读笔记(二)

    Apollo源码阅读笔记(二) 前面 分析了apollo配置设置到Spring的environment的过程,此文继续PropertySourcesProcessor.postProcessBeanF ...

  7. 【原】FMDB源码阅读(二)

    [原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...

  8. Three.js源码阅读笔记-5

    Core::Ray 该类用来表示空间中的“射线”,主要用来进行碰撞检测. THREE.Ray = function ( origin, direction ) { this.origin = ( or ...

  9. jdk源码阅读笔记-LinkedHashMap

    Map是Java collection framework 中重要的组成部分,特别是HashMap是在我们在日常的开发的过程中使用的最多的一个集合.但是遗憾的是,存放在HashMap中元素都是无序的, ...

随机推荐

  1. noip2014无线网络发射器选址

    题目描述 随着智能手机的日益普及,人们对无线网的需求日益增大.某城市决定对城市内的公共场所覆盖无线网. 假设该城市的布局为由严格平行的 129 条东西向街道和 129 条南北向街道所形成的网格状,并且 ...

  2. 浙江省赛 ZOJ4029

    Now Loading!!! Time Limit: Second Memory Limit: KB DreamGrid has integers . DreamGrid also has queri ...

  3. 事务隔离级别引发的"血案"

    事务引发的"血案"见的多了也麻木了,这回遇到个事务隔离级别的"案子",坑了我小半天的时间...也怪自己细节不牢.. 敲着代码遇到这么一个怪事情: class X ...

  4. 转 Visual C++6.0 与matlab联合编程(2)----Visual C++6.0 环境下编译和调试MEX文件

    我的最初想法是利用matlab的mex命令调用C++程序生成动态链接库的,但是测试程序(文中另附)通过了,自己的实际应用程序却没有过.还是把方法贴在这儿,以便自己以后进行整理. http://shij ...

  5. mysql sql_mode=only_full_group_by问题?

    视图查询的时候本地数据库报错: 解决办法: 1.查看sql_mode select @@global.sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_Z ...

  6. POJ 1001 Exponentiation(大数运算)

    POJ 1001 Exponentiation 时限:500 ms   内存限制:10000 K 提交材料共计: 179923   接受: 43369 描述:求得数R( 0.0 < R < ...

  7. shell 基本概述

    SHELL的概念 SHELL是一个命令行解释器,它为用户提供了一个向Linux内核发送请求以便运行程序的界面系统级程序, 用户可以用shell来启动,挂起,停止甚至是编写一些程序. ​ Shell还是 ...

  8. python全栈开发笔记-----------概要

    Python开发 开发: 开发语言: 高级语言:python.Java.php  .C#   .Go .ruby . C++ .... ===>字节码 低级语言:C.汇编            ...

  9. Ant在Java项目中的使用(一眼就看会)

    参考:http://www.cnblogs.com/zhengqiang/p/5557155.html Ant是跨平台的构建工具,它可以实现项目的自动构建和部署等功能.在本文中,主要让读者熟悉怎样将A ...

  10. mac系统下安装Composer和laravel

    先手动下载Composer 地址:https://getcomposer.org/composer.phar 下载后mv composer.phar /usr/local/bin/composer 这 ...