Android加壳与脱壳(7)——Fdex2源码解析和frida版本实现
一、前言
本实验主要是解析Fdex2源码解析和实现frida版本的Fdex2,Fdex2是一个针对第一代壳整体加壳的脱壳框架,不需要通过定位so层中的地址,仅仅只需要在java层就可以实现对第一代壳的脱去,主要是利用两个函数getBytes()
和getDex()
,目前仅仅针对于Android 7.0及以下版本,缺点:如果一些壳不经过loadClass这个流程,就无法脱下来
本文实验分为两个版本:Android7.0及以下使用frida_fdex2、Android8.0及以上使用dumpDexByCookie.js来实现
本文收集的参考博客:
app的加壳与脱壳——此博客主要讲述看雪2w班的相关知识,十分的详细,建议仔细详细读
Android万能脱壳机 ——此博客主要提出两种主流脱壳思路,第一种思路为本文mCooike实现
Frida写抽取壳——用Frida写抽取壳的学习
frida hook so文件笔记总结——看雪2w班frida hook的学习笔记很详细,建议阅读
RDex——和本文实现思路一致
FART脱壳机流程分析——对FART脱壳机流程的解析
Dex脱壳格式详解——对Dex格式解析很清楚
二、Fdex2原理解析
1.原理解析
android中的java.lang.Class类拥有一个方法public native Dex getDex();
,这意味着我们能通过Class对象的getDex
方法获取到Dex
对象,Dex
类中有一个方法public byte[] getBytes()
,我们能通过此方法获取获取该class对象关联的dex数据。这里采用的是Xposed去hook应用的ClassLoader.loadClass
方法区dump解密后的dex数据
2.getBytes()和getDex()作用
(1)getDex()
通过getDex()
来反射调用获得Dex类
(2)getBytes()
然后通过该类对象去调用getBytes(),来获取数据信息
三、Fdex2实现
1.配置Xposed环境
2.编写Fdex2源码
(1)获取getBytes()和getDex()的方法引用
Class Dex;
Method Dex_getBytes;
Method getDex;
String packagename;
...
public void initRefect(){
try {
// public byte[] getBytes()
Dex = Class.forName("com.android.dex.Dex");
Dex_getBytes = Dex.getDeclaredMethod("getBytes",null);
// public Dex getDex()
getDex = Class.forName("java.lang.Class").getDeclaredMethod("getDex",null);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
(2)编写hook代码
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
initRefect();
XposedBridge.log("目标包名:"+ lpparam.packageName);
String str = "java.lang.ClassLoader";
String str2 = "loadClass";
final String packagename = "com.jiongji.andriod.card";
// protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException
if(lpparam.packageName.equals(packagename)){
// public static Unhook findAndHookMethod(String var0, ClassLoader var1, String var2, Object... var3)
XposedHelpers.findAndHookMethod(str, lpparam.classLoader, str2, String.class, Boolean.TYPE, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
//获取hook函数返回类
Class cls = (Class) param.getResult();
if(cls == null){
return;
}
String name = cls.getName();
XposedBridge.log("当前类名:"+name);
byte[] bArr = (byte[]) Dex_getBytes.invoke(getDex.invoke(cls,null)); //Class.getDex().getBytes()
if(bArr == null) {
XposedBridge.log("数据为空,返回");
return;
}
XposedBridge.log("开始写数据");
String dex_path = "/data/data/"+packagename+"/"+packagename+"_"+bArr.length+".dex";
XposedBridge.log(dex_path);
File file = new File(dex_path);
if(file.exists()){
return;
}
writeByte(bArr,file.getAbsolutePath());
}
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
}
});
}
}
图示如下:
(3)保存数据
public void writeByte(byte[] bArr,String str){
try {
OutputStream outputStream = new FileOutputStream(str);
outputStream.write(bArr);
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
XposedBridge.log("文件写出失败");
}
}
3.运行及结果演示
(1)编译安装xposed模块,然后重启
(2)打开目标应用,运行
四、Frida版本修改(针对android7.0)
1.frida实现getDex()和getBytes()
function fdex2() {
Java.perform(function () {
// java.lang.Class.getDex
// com.android.dex.Dex.getBytes
// var Class = Java.use("java.lang.Class");
// console.log(Class);
// var getDex = Class.getDex.overloads;
// if(getDex !=null){
// console.log(getDex);
// }
// var Dex = Java.use("com.android.dex.Dex");
// var getBytes = Dex.getBytes.overloads;
// if(getBytes != null){
// console.log(getBytes);
// }
Java.enumerateClassLoadersSync().forEach(function (loader) {
try {
var Class = loader.loadClass("java.lang.Class");
var methods= Class.getDeclaredMethods();
methods.forEach(function (method) {
console.log(method);
})
}catch (e){
}
try{
var Dex = loader.loadClass("com.android.dex.Dex");
var methods= Dex.getDeclaredMethods();
methods.forEach(function (method) {
console.log(method);
})
}catch (e){
}
})
})
}
function main(){
fdex2();
}
setImmediate(main)
2.fdex2 frida版本的实现
// public static void savedex(byte[] content,String filepath) throws IOException {
// FileOutputStream fileOutputStream = new FileOutputStream(new File(filepath));
// fileOutputStream.write(content);
// fileOutputStream.close();
// }
function savedex(dexbytes,dexpath) {
Java.perform(function () {
var File = Java.use("java.io.File");
var FileOutputStream = Java.use("java.io.FileOutputStream");
var fileobj = File.$new(dexpath);
var fileOutputStreamobj = FileOutputStream.$new(fileobj);
fileOutputStreamobj.write(dexbytes);
fileOutputStreamobj.close();
console.warn("[dumpdex]"+dexpath);
})
}
function fdex2(classname) {
Java.perform(function () {
Java.enumerateClassLoadersSync().forEach(function (loader) {
try{
var ThisClass = loader.loadClass(classname);
//var Class = Java.use("java.lang.Class");
var dexobj = ThisClass.getDex();
var dexbytearray = dexobj.getBytes();
var savedexpath = "/sdcard/"+classname + ".dex";
savedex(dexbytearray,savedexpath);
}catch (e){
}
})
})
}
使用方法:
(1)附加脚本
frida -FU -l Fdex2.js --no-pause //attach方式启动APP 不需要setImmediate()
(2)脱取指定的Activity
3.fdex2脱壳时机点
(1)onCreate函数执行完
//时机点
function main() {
Java.perform(function () {
var StubApp = Java.use("com.stub.StubApp");
//时机点1
StubApp.onCreate.implementation = function () {
var result = this.onCreate();
console.log("StubAPP.onCreate called over!");
fdex2("com.touchtv.module_live.view.activity.PreviewListActivity");
return result;
}
})
}
setImmediate(main)
执行命令:
可以直接对包名进行进行附加:
frida -U -f com.touchtv.touchtv -l Fdex2.js --no-pause //spwan模式在APP启动之前就开始附加 需要setImmediate()
(2)attchBaseContext方法
//时机点
function main() {
Java.perform(function () {
var StubApp = Java.use("com.stub.StubApp");
//时机点2
StubApp.attachBaseContext.implementation = function (arg0) {
var result = this.attachBaseContext(arg0);
console.log("StubAPP.attachBaseContext called over!");
fdex2("com.touchtv.module_live.view.activity.PreviewListActivity");
return result;
}
})
}
setImmediate(main)
执行命令同上
我们验证时机点在attchBaseContext()之前,发现无法hook,说明dex文件在attachBaseContext()之后才解密
4.fdex2的扩展使用
1.使用frida枚举所有Classloader
2.确定正确的ClassLoader并获取目标类的Class对象
3.通过Class对象获取得到dex对象
4.通过dex对象获取内存字节流并保存
五、高版本的类似fdex2脱壳的实现(android 8.0以上)
1.高版本脱壳实现原理图
获取最终的mCookie
(1)mCookie脱壳原理解析
核心思路:反射 + mCookie(其实脱壳的点太多了,这是其中一个)
步骤:
1、找到加固apk的任一class,一般选择主Application或Activity
2、通过该类找到对应的Classloader
3、通过该Classloader找到BaseDexClassLoader
4、通过BaseDexClassLoader找到其字段DexPathList
5、通过DexPathList找到其变量Element数组dexElements
6、迭代该数组,该数组内部包含DexFile结构
7、通过DexFile获取其变量mCookie和mFileName(这个名字没什么鸟用)
至此我们已经获取了mCookie
对该mCookie的解释(有些现在记不太清楚了):
#1、4.4以下好像,mCookie对应的是一个int值,该值是指向native层内存中的dexfile的指针
#2、5.0是一个long值,该值指向native层std::vector<const DexFile*>* 指针,注意这里有多个dex,你需要找到你要的
#3、我还测试了8.0手机,该值也是一个long型的值,指向底层vector,但是vector下标0是oat文件,从1开始是dex文件
// 至于你手机是那个版本,如果没有落入我上面描述的,你需要自己看看代码
8、根据mCookie对应的值做转换,最终你能找到dexfile内存指针
9、把该指针转换为dexfile结构,通过findClassDef来匹配你所寻找的dex是你要的dex
10、dump写文件
代码说明(代码包括java层和native层,但java层只需定义一个native函数即可):
1、代码核心部分为dump_dex.h 和 dump_dex.cpp,里面涉及你需要自行实现的部分(我测试用的5.0.2 moto手机,
如果你的手机版本或者手机型号不同,你可能需要修改我表明的地方)
2、代码相对简单,你可以自行阅读
坑:
此方法思路相对简单,但是操作相对繁琐
1、你需要重打包apk
2、如果遇到签名校验,你同时需要在重打包中加入hook签名代码
详细参考博客:Android万能脱壳机
2.获取mCookie
function dumpDexBymCookie() {
Java.perform(function () {
Java.choose("dalvik.system.DexFile",{
onMatch:function (dexfile) {
var mCookie = dexfile.mCookie.value;
console.log(mCookie.$className);
var Array = Java.use("java.lang.reflect.Array");
var size = Array.getLength(mCookie);
var i = 0;
for(i=0;i<size;i++){
console.log(i+"->"+Array.getLong(mCookie,i));
}
},onComplete:function () {
console.warn("Search DexFile over!");
}
})
})
}
3.通过mCookie获取dexFile起始地址和大小
var savepath = "/sdcard/"
function savedexfile(dexfileptr) {
try {
//将mCookie转换为对应的dexfile内存指针
var dexfilebegin = ptr(dexfileptr).add(Process.pointerSize*1).readPointer(); //dex的初始指针
var dexfilesize = ptr(dexfileptr).add(Process.pointerSize*2).readU32(); //dex的大小
var dex = new File(savepath+"_"+dexfilesize+".dex","a");
if(dex!=null){
var content = ptr(dexfilebegin).readByteArray(dexfilesize);
dex.write(content);
dex.flush();
dex.close();
console.warn("[dumpdex]"+savepath+"_"+dexfilesize+".dex");
}
} catch (e) {
}
}
注意:在Android8.0即更高版本上使用保存,很可能是frida_server版本过低导致出错
六、实验总结
本文主要仿造fdex2思想,针对于第一代的整体加固,开发支持android全版本的自动化脱壳脚本