-
-
一个自定义classloader的函数抽取壳样本
-
2020-11-9 19:18 4894
-
题目出自2W班7月第三题
题目要求:基于frida实现的fart的一个版本是通过对ClassLink类中的LoadMethod函数进行hook实现对函数粒度的脱壳的。请编写基于xposed实现的fart版本插件,能够实现对函数粒度的脱壳
根据题目要求,可以知道,我们通过hook LoadMethod即可得到DexFile进而得到base和size,即可dump出dex。如果有函数抽取,即遍历所有的类得到codeitem后还原即可。
本次实践分为两个部分,一个即是原始思路,照葫芦画瓢参照FART的脱壳思路去编写代码,遇到困难暂时没法解决的时候,转变思路二,改用FART配合xposed去实现。
下面是解题过程:
首先我们可以分析frida-fart是如何实现demp dex的。
粗略分析一下就是
- 拿到dexfile 就可以拿到base size
- 拿到artmethod 就可以拿到codeitem offset和method idx
- 计算codeitem的长度
- dump出来
那么我们在so层也可以画葫芦试试
测试安卓版本为8.1
部分代码如下
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 | void * pVoid = old_loadmethod3(thiz, thread, dex_file, it, klass, artmethod); __android_log_print( 5 , "hookso" , "pVoid ptr:%p" , pVoid); 获取base和size const DexHeader * base = dex_file.pHeader; size_t size = dex_file.pHeader - >fileSize; 获取code item offset和method idx uint32_t codeItemOffset = artmethod - >dex_code_item_offset_; uint32_t idx = artmethod - >dex_method_index_; hook prettymethod方法 主动调用获得方法名 const std::string &string = prettyMethodFunction(artmethod, artmethod, true); 通过偏移可以拿到codeitem long codeItemAddr = ( long ) base + codeItemOffset; CodeItem * codeItem = (CodeItem * ) codeItemAddr; 这部分代码可以直接dump dex出来 int pid = getpid(); char dexFilePath[ 100 ] = { 0 }; sprintf(dexFilePath, "/sdcard/xxxxx/%p %d LoadMethod.dex" , base, size); mkdir( "/sdcard/xxxxx" , 0777 ); int fd = open (dexFilePath, O_CREAT | O_RDWR, 666 ); if (fd > 0 ) { ssize_t i = write(fd, base, size); if (i > 0 ) { close(fd); } } ... |
上述看起来一步一步的确是可以拿得到codeitem,然后进一步拿得到ins的。
但是有一个问题我一直没法解决,就是某些方法会报access violation 异常。我通过搜索发现这个是底层发出来的异常,软件层貌似没法catch。
通过使用frida-fart我发现frida版本的也有这样的问题,但是frida这边可以通过try catch捕捉,让程序继续行走。
因为暂时无法解决这个问题,所以我放弃这个方向。尝试用fart进行dump。
这里偷懒,就直接用寒冰大佬的xp+fart rom继续dump,就没有自己编译源码了。这里环境android6.0
那么这时候思路就转变了,通过hook loadMethod方法可以拿到artmethod,
而根据fart代码,dumpArtMethod这个方法,传入artmethod即可dump。
1 2 3 4 5 6 7 8 9 | 先执行原来的loadMethod逻辑 void * pVoid = old_loadmethod3(thiz, thread, dex_file, it, klass, artmethod); __android_log_print( 5 , "hookso" , "pVoid ptr:%p" , pVoid); 然后通过so层hook dumpArtmethod函数,并将参数传入 try { dumpArtMethodFunction(artmethod); }catch (...){ } |
上述通过so层调用FART的dumpArtMethodFunction方法,可以dump出dex,以及classlist。这是FART自带的功能。
而在java层,我们需要遍历所有类。
首先hook掉DexClassLoader和PathClassLoader的构造函数,并将classloader存起来。
因为是自定义的Xposed,而所以名字为XcustomBridge。其实这里就是Xposed,仅供参考,不可照抄
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 | XcustomBridge.hookAllConstructors(DexClassLoader. class , new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super .afterHookedMethod(param); final ClassLoader classLoader = (ClassLoader) param.thisObject; XcustomBridge.log( "DexClassLoader:" + classLoader.toString()); mClassLoaders.add(classLoader); } }); XcustomBridge.hookAllConstructors(PathClassLoader. class , new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super .afterHookedMethod(param); final ClassLoader classLoader = (ClassLoader) param.thisObject; XcustomBridge.log( "PathClassLoader:" + classLoader.toString()); mClassLoaders.add(classLoader); } }); |
紧接着加载我们的so,并遍历所有的classloader,执行loadClass操作。
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 | XcustomHelpers.findAndHookMethod(Application. class , "attach" , Context. class , new XC_MethodHook() { @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { super .afterHookedMethod(param); XcustomBridge.log( "attach after" ); mContext = (Context) param.args[ 0 ]; XcustomHelpers.callMethod(Runtime.getRuntime(), "doLoad" , "/system/lib/libnative-lib.so" , mContext.getClassLoader()); new Thread(new Runnable() { @Override public void run() { try { Thread.sleep( 30 * 1000 ); } catch (InterruptedException e) { e.printStackTrace(); Log.e( "hook1" , Log.getStackTraceString(e)); } for ( int i = 0 ; i < mClassLoaders.size(); i + + ) { ClassLoader classLoader = mClassLoaders.get(i); TestClassloader(classLoader); } fart(); } }).start(); } }); |
将dump下来后bin文件批量恢复后,即可查看到,函数已经恢复了。
但是如果点多几个函数查看
会发现部分函数并没有复原
那么通过回想,我们可以得知,还原的代码其实都是app启动过程中调用过的方法,所以dump下来后,是包含代码的。因此这个壳也是函数抽取壳,而且恢复后不会复原。
而查看FART dump出来的ins文件,发现也缺了很多数据,那么这里可以猜测是某个环节出了问题导致没有dump出来。
然后通过回溯error log 可以发现是classloader的锅
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | / * * * 01 - 06 00 : 31 : 12.925 12000 - 12060 / com.sup.android.superb W / System.err: java.lang.IllegalArgumentException: Expected receiver of type dalvik.system.BaseDexClassLoader, but got com.bytedance.frameworks.plugin.core.DelegateClassLoader * 01 - 06 00 : 31 : 12.925 12000 - 12060 / com.sup.android.superb W / System.err: at java.lang.reflect.Field.get(Native Method) * 01 - 06 00 : 31 : 12.925 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.getFieldOjbect(Hook3.java: 206 ) * 01 - 06 00 : 31 : 12.925 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.TestClassloader(Hook3.java: 259 ) * 01 - 06 00 : 31 : 12.925 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.fart(Hook3.java: 240 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3$ 3 $ 1.run (Hook3.java: 95 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at java.lang.Thread.run(Thread.java: 818 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: java.lang.NullPointerException: null receiver * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at java.lang.reflect.Field.get(Native Method) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.getFieldOjbect(Hook3.java: 206 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.TestClassloader(Hook3.java: 260 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3.fart(Hook3.java: 240 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at com.l.sevenclasshook.hook.Hook3$ 3 $ 1.run (Hook3.java: 95 ) * 01 - 06 00 : 31 : 12.926 12000 - 12060 / com.sup.android.superb W / System.err: at java.lang.Thread.run(Thread.java: 818 ) |
那么这里我们知道了这个app有自定义的classloader。
根据提示,我们可以发现这是一个直接继承于classloader的类。
那么来看看通常我们是如何获取classlist的。
1 2 3 4 5 6 7 8 9 10 11 12 | Field pathList_Field = (Field) getClassField(appClassloader, "dalvik.system.BaseDexClassLoader" , "pathList" ); Object pathList_object = getFieldOjbect( "dalvik.system.BaseDexClassLoader" , appClassloader, "pathList" ); Object [] ElementsArray = ( Object []) getFieldOjbect( "dalvik.system.DexPathList" , pathList_object, "dexElements" ); Field dexFile_fileField = null; try { dexFile_fileField = (Field) getClassField(appClassloader, "dalvik.system.DexPathList$Element" , "dexFile" ); } catch (Exception e) { e.printStackTrace(); } catch (Error e) { e.printStackTrace(); } ...... |
我们是通过获得BaseDexClassLoader的pathList字段从而进一步往下获取classList的。但是由于我们现在是直接继承于classloader,所以我们要想办法获取classlist。
那么这里就有两种方法可以实现了。
第一个,fart使用过程中是dump classlist出来。
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 | if (appClassloader instanceof BaseDexClassLoader) { } else if (appClassloader instanceof ClassLoader) { List <String> nameList = new ArrayList<>(); BufferedReader br = null; try { br = new BufferedReader(new InputStreamReader(new FileInputStream(new File (Environment.getExternalStorageDirectory() + "/8958236_classlist.txt" )), "UTF-8" )); String lineTxt = null; while ((lineTxt = br.readLine()) ! = null) { if (!TextUtils.isEmpty(lineTxt)) { String name = lineTxt.replace( "L" , " ").replaceAll(" / ", " . ").replace(" ; ", " "); Log.e( "hook1" , "after replace name:" + name); nameList.add(name); } } br.close(); } catch (Exception e) { Log.e( "hook1" , Log.getStackTraceString(e)); } for (String name : nameList) { try { Log.e( "Hook1" , "===================>loadClass:" + name); appClassloader.loadClass(name); } catch (Exception e) { Log.e( "Hook1" , Log.getStackTraceString(e)); } } return ; } |
因此这里我们直接遍历文件,然后loadClass即可。
接下来我们对dex方法体进行批量恢复后,可以看到,函数都恢复了。
少部分没有恢复的函数,是因为这个classloader没有找到类,报class not found。接下来我们可以将所有classloader都跑一遍这个list即可
方法二
其实fart也是解析DexFile从而拿到classlist的。
既然有了整个dexFile文件,那么有啥不能解析的呢。
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 | if (size = = 8958236 ) { u4 classDefSize = dex_file.pHeader - >classDefsSize; u4 classDefsOff = dex_file.pHeader - >classDefsOff; __android_log_print( 5 , "hookso" , "base:%p" , (void * ) base); __android_log_print( 5 , "hookso" , "classDefSize:%ld classDefsOff:%p" , classDefSize, (void * ) classDefsOff); u4 typeIdsOff = dex_file.pHeader - >typeIdsOff; u4 stringIdsOff = dex_file.pHeader - >stringIdsOff; __android_log_print( 5 , "hookso" , "typeIdsOff:%p stringIdsOff:%p" , (void * ) typeIdsOff, (void * ) stringIdsOff); int i; for (i = 0 ; i < classDefSize; i + + ) { long currClassAddr = ( long ) classDefsOff + i * 32 + ( long ) base; __android_log_print( 5 , "hookso" , "currClass ptr:%p" , (void * ) currClassAddr); int * idx = ( int * ) (currClassAddr); __android_log_print( 5 , "hookso" , "currClassIdx:%i" , * idx); int tmpIdx = * idx; long currTypeIdAddr = (( long ) typeIdsOff + 4 * tmpIdx + ( long ) base); int * currTypeIdx = ( int * ) currTypeIdAddr; __android_log_print( 5 , "hookso" , "currTypeIdx:%ld" , * currTypeIdx); tmpIdx = * currTypeIdx; long currStringOffAddr = (( long ) stringIdsOff + 4 * tmpIdx + ( long ) base); int * currStringOff = ( int * ) currStringOffAddr; __android_log_print( 5 , "hookso" , "currStringOff:%ld" , * currStringOff); tmpIdx = * currStringOff; long off = ( long ) base + tmpIdx; __android_log_print( 5 , "hookso" , "string off:%p" , (void * ) off); const uint8_t * strPtr = (uint8_t * ) off; DecodeUnsignedLeb128(&strPtr); char * classname = (char * ) strPtr; __android_log_print( 5 , "hookso" , "classname:%s" , classname); } } |
在这里我们可以一层一层的不断获取不断遍历,就可以拿到我们想要的classname
拿到classname后我们就可以用这个去遍历loadClass后再dump dex即可。
可以看到这个题目最后想要说的就是自定义classloader如何进行函数抽取还原,那么这里我们需要熟悉Dexfile的文件结构以及了解classloader如何使用
[2022夏季班]《安卓高级研修班(网课)》月薪两万班招生中~