仅适用于安卓 5 及以上版本(ART 模式)
安卓 4.4 以前使用的是 Dalvik 模式,两者在脱壳上有所区别。
壳可以分为几类#
加壳技术 | 技术特点 | 脱壳难度 | 说人话就是: |
---|---|---|---|
第一代:DEX 整体加密型壳 | 采用 Dex 整体加密,动态加载运行的机制 | 较容易被还原,通过自动化脱壳工具或脚本即可从内存中 dump 出 dex 文件 | 把 dex 隐藏起来 |
第二代:DEX 函数抽取型壳 | 粒度更细,将方法单独抽取出来,加密保存,解密执行 | 可以从根本上进行还原的,dump 出所有的运行时的方法体,填充到 dump 下来的 dex 中去的,这也是 fart 的核心原理 | 隐藏 dex 并删掉期中中部分方法 |
第三代:VMP、Dex2C 壳 | 独立虚拟机解释执行、语义等价语法迁移,强度最高 | Dex2C 目前是没有办法还原的,只能跟踪进行分析; VMP 虚拟机解释执行保护的是映射表,只要心思细、功夫深,是可以将映射表还原的 | 虚拟机里跑虚拟机 |
不过目前遇到的大多是一代和二代壳,三代暂未遇到过。实际上,APP 加的壳越复杂,运行效率越低,像支付宝和微信,就从不加壳(因为他们的服务都在云端,本地的修改不影响云数据,所以加不加壳其实无所谓)
本次脱壳目标为一代壳。
加壳 APP 运行原理#
APP启动
⬇️
先加载壳的dex
⬇️
壳读取源app的dex文件
⬇️
解密源dex文件
⬇️
将解密后的dex载入内存
⬇️
通过映射表还原抽取的方法(二代壳需要)
⬇️
源dex运行
三代壳是先运行虚拟机,再运行 dex。
脱壳原理#
目的:拿到可供分析的字节码(dex)
目标:内存中已解密的源 dex
那么如果已知解密的源 dex 的内存基址以及文件长度,那么拿到这个 dex 源码就简单多了。
翻一翻
安卓源码(戳我直达)
或者你自己动态调试一下安卓 App 的加载过程,就会发现,在 libart.so 中有一个函数 Openmemory
提供了 dex 加载,从这里可以获得源 dex 的内存基址(base)以及文件长度(size):
std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file,
std::string* error_msg) {
CHECK_ALIGNED(base, 4); // various dex file structures must be word aligned
std::unique_ptr<DexFile> dex_file(
new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
if (!dex_file->Init(error_msg)) {
dex_file.reset();
}
return std::unique_ptr<const DexFile>(dex_file.release());
}
Android 4.4 版本之前 系统函数在 libdvm.so
Android 5.0 之后 系统函数在 libart.so
Dex 文件头部格式#
dex 文件分三块:头部、索引区、数据区。此处只讲解头部格式。
文件头部格式:
struct header_item{
ubyte[8] magic;//文件魔数 8字节
unit checksum;//校验 4字节
ubyte[20] siganature;//签名 20字节
uint file_size;//文件大小 4字节
uint header_size;//头文件大小
unit endian_tag;//一般为固定值0x12345678
uint link_size;//链接数据大小
uint link_off;//链接数据偏移
uint map_off;//data区的数据集合
uint string_ids_size;//字符串数量
uint string_ids_off;//字符串偏移
uint type_ids_size;//所有类型数量
uint type_ids_off;//类型偏移
uint proto_ids_size;//方法参数信息数量
uint proto_ids_off;//偏移
uint method_ids_size;//方法信息数量
uint method_ids_off;//偏移
uint class_defs_size;//类信息个数
uint class_defs_off;//偏移
uint data_size;//数据大小
uint data_off;//偏移
}
此处主要关注文件魔数,这是用于识别 dex 文件的标志位。前四位是 dex 标志,后四位是版本号(安卓编译版本)。
{ 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00 } = “dex\n035\0”
由于 dex 文件中采用的是小端字节序的编码方法(
不理解什么是字节序吗?戳我!
),实际存储的数据应该是:
{ 0x0a 0x78 0x65 0x64 0x00 0x35 0x33 0x30 } = “\nxed\0530”
Java 怎么调用 OpenMemory?#
javaCode.OpenMemory的导出函数名(参数等...);
⬇️
OpenMemory导出函数名映射到libart.so文件中的OpenMemory方法
可以理解为:OpenMemory导出函数名
是 libart.so 文件的对外开放的接口。
但是这个导出函数名不是一成不变的,在不同的安卓版本上,这个名称是不一样的。
我们可以通过导出虚拟机上的 libart.so 文件来获取导出函数名。
Android 10.0 以前,libart.so 在/system/lib 文件夹下,64 位的在/system/lib64
Android 10.0 以后,libart.so 被集成到了 apex 中
- 使用 IDA 打开 libart.so 文件,搜索 OpenMemory 获取导出函数名:
dexElements 数组#
- 在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。
- DexClassLoader 可以用来加载 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件
- DexClassLoader 和 PathClassLoader 的基类 BaseDexClassLoader 查找 class 是通过其内部的 DexPathList pathList 来查找的
- DexPathList 内部有一个 Element[] dexElements 数组,其 findClass() 方法(源码如下)的实现就是遍历该数组,查找 class ,一旦找到需要的类,就直接返回,停止遍历:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
总结一下,一个 class 的加载过程为:
DexClassLoader
⬇️
DexPathList
⬇️
Element[]
而,Element
存储了我们的 Dex 文件数据,下面截取了部分代码:
public Element(File file, ZipFile zipFile, DexFile dexFile) {
this.file = file;
this.zipFile = zipFile;
this.dexFile = dexFile;
}
那么 dexElements 数组
就是存储了所有的 Dex 文件数据。
那么基础问题都解决了,就可以上脚本了~
脱壳脚本讲解#
脚本地址:
戳我直达
,这是个典型的一代壳脱壳脚本。
JS 部分#
var DEX_MAGIC = 0x0A786564;
var dexrec = [];
var openmemory = Module.findExportByName(
"libart.so",
"_ZN3art7DexFile10OpenMemoryEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPNS_6MemMapEPKNS_10OatDexFileEPS9_"
);
if(openmemory != undefined) {
console.log("openmemory at" + openmemory);
Interceptor.attach(openmemory, {
onEnter: function (args) {
if(Memory.readU32(args[1]) == DEX_MAGIC) {
dexrec.push(args[1]);
}
},
onLeave: function (retval) {
}
});
}
if(Java.available) {
Java.perform(function(){
var dexBase64 = "";
var application = Java.use("android.app.Application");
var BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
var Base64 = Java.use("android.util.Base64");
var FileOutputStream = Java.use("java.io.FileOutputStream");
var DexClassLoader = Java.use("dalvik.system.DexClassLoader");
var reflectField = Java.use("java.lang.reflect.Field");
var reflectMethod = Java.use("java.lang.reflect.Method");
var reflectObject = Java.use("java.lang.Object");
var reflectClass = Java.use("java.lang.Class");
var reflectString = Java.use("java.lang.String");
var reflectClassloader = Java.use("java.lang.ClassLoader");
if(application != undefined) {
application.attach.overload('android.content.Context').implementation = function(context) {
var result = this.attach(context);
var classloader = context.getClassLoader();
var filesDir = context.getFilesDir();
var codeCacheDir = context.getCodeCacheDir();
console.log("files dir: " + filesDir);
console.log("code cache dir: " + codeCacheDir);
if(classloader != undefined) {
var casedloader = Java.cast(classloader, BaseDexClassLoader);
var dexbytes = Base64.decode(dexBase64, 0);
var dexpath = filesDir + "/emmm.dex";
var fout = FileOutputStream.$new(dexpath);
fout.write(dexbytes, 0, dexbytes.length);
fout.close();
console.log("write dex to " + dexpath);
var dexstr = dexpath.toString();
var cachestr = codeCacheDir.toString();
var dyndex = DexClassLoader.$new(
dexstr,
cachestr,
cachestr,
classloader
);
console.log(dyndex.toString());
var EnumerateClass = dyndex.loadClass("com.smartdone.EnumerateClass");
var castedEnumerateClass = Java.cast(EnumerateClass, reflectClass);
var methods = castedEnumerateClass.getDeclaredMethods();
var loadAllClass = undefined;
for(var i in methods) {
console.log(methods[i].getName());
if(methods[i].getName() == "loadAllClass") {
console.log("find loadAllClass");
loadAllClass = methods[i];
}
}
if(loadAllClass != undefined) {
console.log("loadAllClass: " + loadAllClass.toString());
var args = Java.array('Ljava.lang.Object;',[classloader]);
var classlist = loadAllClass.invoke(null , args);
console.log("start dump dex ");
for(var i in dexrec) {
if(Memory.readU32(dexrec[i]) == DEX_MAGIC) {
var dex_len = Memory.readU32(dexrec[i].add(0x20));
var dumppath = filesDir.toString() + "/" + dex_len.toString(0x10) + ".dex";
console.log(dumppath);
var dumpdexfile = new File(dumppath, "wb");
dumpdexfile.write(Memory.readByteArray(dexrec[i], dex_len));
dumpdexfile.close();
console.log("write file to " + dumppath);
}
}
}
} else {
console.error("unable get classloader");
}
return result;
}
}
});
}
Dex 部分#
此处 JAVA 代码由 Smali 转换而来
package com.smartdone;
import android.util.Log;
import dalvik.system.DexFile;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
public class EnumerateClass {
private static final String TAG = "FRiDA_UNPACK";
public static ArrayList getClassNameList(ClassLoader classLoader) {
int i;
ArrayList classNameList = new ArrayList();
try {
Object dexElements = EnumerateClass.getObjectField(EnumerateClass.getObjectField(classLoader, "pathList"), "dexElements");
int dexElementsLength = Array.getLength(dexElements);
i = 0;
while(true) {
label_8:
if(i >= dexElementsLength) {
goto label_24;
}
Enumeration enumerations = ((DexFile)EnumerateClass.getObjectField(Array.get(dexElements, i), "dexFile")).entries();
while(true) {
if(!enumerations.hasMoreElements()) {
++i;
break;
}
classNameList.add(((String)enumerations.nextElement()));
}
}
}
catch(Exception v1) {
goto label_24;
}
++i;
goto label_8;
label_24:
Collections.sort(classNameList);
return classNameList;
}
public static String[] getClassNameListArray(ClassLoader classLoader) {
ArrayList namelist = EnumerateClass.getClassNameList(classLoader);
String[] retval = new String[namelist.size()];
namelist.toArray(((Object[])retval));
return retval;
}
public static Object getObjectField(Object object, String fieldName) {
Class clazz = object.getClass();
while(!clazz.getName().equals(Object.class.getName())) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
catch(NoSuchFieldException e) {
e.printStackTrace();
clazz = clazz.getSuperclass();
}
catch(IllegalAccessException e2) {
e2.printStackTrace();
}
}
return null;
}
public static void loadAllClass(ClassLoader classLoader) {
int v6_1;
Method[] methods;
Class clazz;
try {
Iterator v1 = EnumerateClass.getClassNameList(classLoader).iterator();
while(true) {
if(!v1.hasNext()) {
return;
}
Object v2 = v1.next();
clazz = classLoader.loadClass(((String)v2));
methods = clazz.getDeclaredMethods();
Log.d("FRiDA_UNPACK", "load class: " + clazz.getName());
v6_1 = 0;
label_19:
while(v6_1 < methods.length) {
goto label_20;
}
}
label_20://此处循环获取所有method并进行反射调用
Method method = methods[v6_1];
Object[] objs = new Object[method.getParameterTypes().length];
Log.d("FRiDA_UNPACK", "try to load method: " + clazz.getName() + "-->" + method.getName());
method.invoke(null, objs);
Log.d("FRiDA_UNPACK", "success");
++v6_1;
goto label_19;
}
catch(Throwable v0) {
}
}
}
另一种脱壳方案#
之前讲过了,安卓的 libart.so 文件提供了
OpenMemory
函数来加载 dex,如果在加载后,将 dex 直接写出去,也可达到脱壳效果。
std::unique_ptr<const DexFile> DexFile::OpenMemory(const uint8_t* base,
size_t size,
const std::string& location,
uint32_t location_checksum,
MemMap* mem_map,
const OatDexFile* oat_dex_file,
std::string* error_msg) {
CHECK_ALIGNED(base, 4); // various dex file structures must be word aligned
std::unique_ptr<DexFile> dex_file(
new DexFile(base, size, location, location_checksum, mem_map, oat_dex_file));
if (!dex_file->Init(error_msg)) {
dex_file.reset();
}
//Start:此处开始加入自己的dex导出代码
__android_log_print(ANDROID_LOG_DEBUG,"DexFile::OpenMemory","size is:%zu,location is:%s", size, location.c_str());
//此处只是针对com.autohome.mycar编写的导出代码,并不具有通用性,通用型的应该有更好的写法
//此处还有一个问题,这个只针对单dex项目写的导出,多dex未做处理
if (!strcmp(location.c_str(),"/data/data/com.autohome.mycar/.jiagu/classes.dex"))
{
//获取文件指针
int fd = open("/data/data/com.autohome.mycar/classes.dex",O_CREAT|O_EXCL|O_WRONLY,S_IRWXU);
__android_log_print(ANDROID_LOG_DEBUG,"copy is starting!","hello");
if (fd>0)
write(fd,base,size);//写出dex数据
else
__android_log_print(ANDROID_LOG_DEBUG,"copy is failed!","codeis:%d",fd);
close(fd);
}
//End:此处开始加入自己的dex导出代码
return std::unique_ptr<const DexFile>(dex_file.release());
}
- 理论上这种方案可以脱大部分的壳,不过有一个缺点,需要自己编译 rom 并刷机。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论