一、前言
本实验主要是解析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()的方法引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Class Dex; Method Dex_getBytes; Method getDex; String packagename; ... public void initRefect () { try { Dex = Class.forName("com.android.dex.Dex" ); Dex_getBytes = Dex.getDeclaredMethod("getBytes" ,null ); getDex = Class.forName("java.lang.Class" ).getDeclaredMethod("getDex" ,null ); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } }
(2)编写hook代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
@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" ; if (lpparam.packageName.equals(packagename)){ XposedHelpers.findAndHookMethod(str, lpparam.classLoader, str2, String.class, Boolean.TYPE, new XC_MethodHook () { @Override protected void afterHookedMethod (MethodHookParam param) throws Throwable { super .afterHookedMethod(param); 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 )); 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)保存数据
1 2 3 4 5 6 7 8 9 10
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()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
function fdex2 () { Java .perform (function () { 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版本的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
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 dexobj = ThisClass .getDex (); var dexbytearray = dexobj.getBytes (); var savedexpath = "/sdcard/" +classname + ".dex" ; savedex (dexbytearray,savedexpath); }catch (e){ } }) }) }
使用方法:
(1)附加脚本
1
frida -FU -l Fdex2 .js --no-pause
(2)脱取指定的Activity
3.fdex2脱壳时机点
(1)onCreate函数执行完
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
function main () { Java .perform (function () { var StubApp = Java .use ("com.stub.StubApp" ); 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)
执行命令:
可以直接对包名进行进行附加:
1
frida -U -f com.touchtv .touchtv -l Fdex2 .js --no-pause
(2)attchBaseContext方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
function main () { Java .perform (function () { var StubApp = Java .use ("com.stub.StubApp" ); 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 2 3 4
1. 使用frida枚举所有Classloader 2. 确定正确的ClassLoader并获取目标类的Class对象3. 通过Class对象获取得到dex对象4. 通过dex对象获取内存字节流并保存
五、高版本的类似fdex2脱壳的实现(android 8.0以上)
1.高版本脱壳实现原理图
获取最终的mCookie
(1)mCookie脱壳原理解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
核心思路:反射 + mCookie(其实脱壳的点太多了,这是其中一个) 步骤: 1 、找到加固apk的任一class,一般选择主Application或Activity2 、通过该类找到对应的Classloader3 、通过该Classloader找到BaseDexClassLoader4 、通过BaseDexClassLoader找到其字段DexPathList5 、通过DexPathList找到其变量Element数组dexElements6 、迭代该数组,该数组内部包含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是你要的dex10 、dump写文件代码说明(代码包括java层和native 层,但java层只需定义一个native 函数即可): 1 、代码核心部分为dump_dex.h 和 dump_dex.cpp,里面涉及你需要自行实现的部分(我测试用的5.0 .2 moto手机,如果你的手机版本或者手机型号不同,你可能需要修改我表明的地方) 2 、代码相对简单,你可以自行阅读坑: 此方法思路相对简单,但是操作相对繁琐 1 、你需要重打包apk2 、如果遇到签名校验,你同时需要在重打包中加入hook签名代码
详细参考博客:Android万能脱壳机
2.获取mCookie
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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起始地址和大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
var savepath = "/sdcard/" function savedexfile (dexfileptr ) { try { var dexfilebegin = ptr (dexfileptr).add (Process .pointerSize *1 ).readPointer (); var dexfilesize = ptr (dexfileptr).add (Process .pointerSize *2 ).readU32 (); 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全版本的自动化脱壳脚本
- source:security-kitchen.com
评论