使用Frida简单实现函数粒度脱壳
2020-07-08 10:56:10 Author: bbs.pediy.com(查看原文) 阅读量:1330 收藏

    本题来自于2W班的第一题,完成某APP的脱壳。题目中的APP实现了自定义ClassLoader导致默认版本Fart无法正常脱壳,需要自己定制。这里尝试使用Frida进行脱壳,脚本完全模仿默认版本的Fart运行流程进行编写,当然很多函数Frida完全没修改源码来的直接方便。这里也是为了熟悉下Frida所以进行的尝试,很多函数也都是直接用Frida编码实现,比如解析Dex中类,计算Dex函数代码长度等,相对于hanbing老师的Frida脱壳麻烦很多。水平太差只能用笨方法了。

    实现过程中,加深以下几点知识点的理解

    1.了解Fart,尝试解决一些自定义问题

    2.Frida 遍历Dex类,类方法,类函数

    3.Frida主动调用指定函数

    4.自定义ClassLoader对脱壳的影响

解题步骤

一、解题思路

    首先直接使用fart是肯定不行了,就不重复写了。脱下来的Dex大多都是抽取的,除了一些被动调用的函数能顺便Dump下来。由于编译源码比较麻烦,所以这里使用Frida脚本来实现。

二、查看一些被动调用还原的代码

    用的yang大佬的dump脚本,Dump下Dex后,发现是自定义ClassLoader导致Fart无法正常运行。

三、编写Frida脱壳脚本

   需要解决的问题:

        Frida遍历ClassLoader, 类 ,类函数,并依次调用

        Hook函数运行流程中某一处,获取当时dex中函数的代码并保存

四、遍历类并遍历函数调用

    1.枚举ClassLoader类代码

function hook_java(){
    Java.perform(function(){
        console.log("---------------Java.enumerateClassLoaders");
        Java.enumerateClassLoaders({
            onMatch: function(cl){
                fartwithClassloader(cl);
            },
            onComplete: function(){
            }
        });
    });
}

function fartwithClassloader(cl){
    Java.perform(function(){    
        var clstr = cl.$className.toString();
        if(clstr.indexOf("java.lang.BootClassLoader") >= 0 ){
            return
        }
        console.log("  |------------",cl.$className);

        var class_BaseDexClassLoader = Java.use("dalvik.system.BaseDexClassLoader");
        var pathcl = Java.cast(cl, class_BaseDexClassLoader);
        console.log(".pathList",pathcl.pathList.value);

        var class_DexPathList = Java.use("dalvik.system.DexPathList");
        var dexPathList = Java.cast(pathcl.pathList.value, class_DexPathList);
        console.log(".dexElements:",dexPathList.dexElements.value.length);

        var class_DexFile = Java.use("dalvik.system.DexFile");
        var class_DexPathList_Element = Java.use("dalvik.system.DexPathList$Element");
        for(var i=0;i<dexPathList.dexElements.value.length;i++){
            var dexPathList_Element = Java.cast(dexPathList.dexElements.value[i], class_DexPathList_Element);
            // console.log(".dexFile:",dexPathList_Element.dexFile.value);
            if(dexPathList_Element.dexFile.value){
                //可能为空
                var dexFile = Java.cast(dexPathList_Element.dexFile.value, class_DexFile);
                var mcookie = dexFile.mCookie.value;
                // console.log(".mCookie",dexFile.mCookie.value);
                if(dexFile.mInternalCookie.value){
                    // console.log(".mInternalCookie",dexFile.mInternalCookie.value);
                    mcookie = dexFile.mInternalCookie.value;
                }
                var classNameArr = dexPathList_Element.dexFile.value.getClassNameList(mcookie);
                console.log("dexFile.getClassNameList.length:",classNameArr.length);
                console.log("     |------------Enumerate ClassName Start");
                for(var i=0; i<classNameArr.length; i++){
                    // console.log("      ",classNameArr[i]);
                    if(classNameArr[i].indexOf(TestCalss) > -1){
                        loadClassAndInvoke(cl, classNameArr[i]);
                    }
                }
                console.log("     |------------Enumerate ClassName End");
            }
        }
    });
}

    根据获取ClassLoader继承链,可以找到dalvik.system.DexPathList$Element类,根据此类即可获取dexFile字段枚举所有类。此处主要是Java.cast的使用,具体参考ClassLoader的源码。

    2.获取类函数

var classResult = Java.use(className).class;
if(!classResult) return;
var methodArr = classResult.getDeclaredConstructors();
methodArr = methodArr.concat(classResult.getDeclaredMethods());

很容易就可以获取构造函数和普通函数列表

    3.调用类函数

    实现了2种方法,第一种通过Java层java.lang.reflect.Method的函数public native Object invoke(Object obj, Object... args)

var argsTypes = methodArr[i].getParameterTypes();
var args = []
// int类型
var class_int = Java.use("java.lang.Integer");
args[0] = class_int.$new(0x1);

// String类型
var class_String = Java.use("java.lang.String");
args[0] = class_String.$new("TEST");

// 例:android.os.Bundle类型,OnCreate
var class_Bundle = Java.use("android.os.Bundle");
args[0] = class_Bundle.$new();
// 参数列表
var arr = Java.array("Ljava.lang.Object;",args);
methodArr[i].setAccessible(true)
console.log("invoke result:",methodArr[i].invoke(null,arr));

// 非静态需要传第一个参数
// var class_MainActivity = Java.use("com.aipao.hanmoveschool.activity.MainActivity");
// class_MainActivity.$new();
// Java.choose("com.aipao.hanmoveschool.activity.MainActivity",{
//     onMatch: function(ins){
//         try {
//             console.log(methodArr[i].invoke(ins,arr)); //.overload('java.lang.Object', '[Ljava.lang.Object;')
//         } catch (error) {
//             console.log("Java.choose:[",methodArr[i].toString(),']',error);
//         }
//     },
//     onComplete: function(){
//     }
// });

    这种调用方式非常繁琐,每个类型都要创建对应类的对象,如果是构造参数不是空的就麻烦死了。

    好处就是如果参数正常可以保证函数正常运行。

    最初的时候就是想像fart一样直接调用ArtMethod::Invoke,但是当时很多参数不知道怎么传送。

    后面是第一种方式太复杂,很多函数基本上无法调用,所以找到了第二种方式。代码如下

var invokeSize = Memory.alloc(0x10).writeU32(6);
var invokeStr = Memory.alloc(0x100).writeUtf8String("fart");
var allocPrettyMethod = Memory.alloc(0x100);
var allocPrettyMethodInit = []
ArtMethod_invoke_replace(ptr(methodArr[i].getArtMethod()), ptr(0), ptr(0), 6, invokeSize, invokeStr);

    直接使用函数getArtMethod()获取到ArtMethod的指针。这里虽然在ArtMethod::invoke运行时会报错,但是可以进入到invoke方法,获取当时的函数代码

五、HOOK art_method.cc文件中的ArtMethod::Invoke,根据参数Dump函数

    1.Hook代码使用lasting-yang大佬的代码,主要是使用PrettyMethod打印出函数名,好做个过滤

    2.具体DumpCode的代码会有一些BUG,只解决影响Dump的,也有些还没解决的就跳过Dump 

var dex_code_item_offset_ = args[0].add(sizeU32*2).readU32();
var dex_method_index_ = args[0].add(sizeU32*3).readU32();
if(dex_code_item_offset_ <= 0){
    //com.aipao.hanmoveschool.activity.StepDetector$OnSensorChangeList
    console.log("dex_code_item_offset_ error:",dex_code_item_offset_);
    return;
}
// console.log("dex_code_item_offset_:",dex_code_item_offset_.toString
// console.log("dex_method_index_:",dex_method_index_.toString(16));
if(DexBase){
    var addrCodeOffset = DexBase.add(dex_code_item_offset_);
    // console.log("addrCodeOffset:",hexdump(addrCodeOffset));
    var tries_size = addrCodeOffset.add(sizeShort*3).readU16(); 
    var insns_size = addrCodeOffset.add(sizeU32*3).readU16(); 
    if(tries_size > 256){
        console.log("tries_size:",tries_size.toString(16));
        console.log("insns_size:",insns_size.toString(16));
        return;
    }
    // console.log("tries_size:",tries_size.toString(16));
    // console.log("insns_size:",insns_size.toString(16));
    var codeLen = 16 + insns_size*2;
    if(tries_size > 0){
        var addrTryStart = addrCodeOffset.add(codeLen);
        // if(addrTryStart.readU16() == 0){ //padding
        //     addrTryStart = addrTryStart.add(0x2);
        // }
        if(codeLen %4 != 0){ //padding
            addrTryStart = addrTryStart.add(0x2);
        }
        // console.log("addrTryStart:",hexdump(addrTryStart));
        var addrTryEnd = addrTryStart.add(sizePointer*tries_size);
        var addrCodeEnd = CodeItemEnd(addrTryEnd);
        codeLen = addrCodeEnd - addrCodeOffset;
    }
    var allins = "";
    for(var i=0;i<codeLen;i++){
        var u8data = addrCodeOffset.add(i).readU8();
        if(u8data <= 0xF){
            allins += "0";
        }
        allins += u8data.toString(16);
    }
    var codedtl = "{name:"+methodName+
        ",method_idx:"+dex_method_index_+
        ",offset:"+dex_code_item_offset_+
        ",code_item_len:"+codeLen+
        ",ins:"+allins+
    "};";
    console.log(codedtl);
    write_file_log(codedtl);
    dumpMethodNameInvoke.push(methodName);

    主要是如何计算codeLen,如果有try的函数就复杂很多。

    除了计算codeLen,还有些函数的code_item_offset异常,比如代码中就有判断offset是0的,直接就是dex文件头了,应该是在哪里有还原吧。

    对于tries_size,insns_size异常并没有去一个个函数去查看什么问题。直接选择跳过。

    3.DexBase的获取

    比较偷懒,直接使用网上随便找的DumpDex的Frida代码,Dump下抽取后的Dex后,直接判断下长度。对于多dex没考虑。

Interceptor.attach(addr_ClassLinker_DefineClass, {
        onEnter: function(args){
            if(DexBase) {
                //找到就不运行下面了
                return;
            }
            console.log("addr_ClassLinker_DefineClass:",DexBase);
            var dex_file = args[5];
            var base = ptr(dex_file).add(Process.pointerSize).readPointer();
            var size = ptr(dex_file).add(Process.pointerSize *2).readUInt();
            console.log("base:",base,"\tsize:",size);
            if(size > 0x3b0000 && size < 0x3f0000){
                DexBase = base;
            }
        },
        onLeave: function(retval) {

        }
    });

    Dex长度是0x3be578,取了个范围,ArtMethod::Invoke运行的时候就会获取DexBase。要注意的就是Dump前要触发ClassLinker::DefineClass,一般是切换下界面,点点按键就有新的类创建触发了。

    4.关于ArtMethod::Invoke不能hook到很多函数

    由于对Fart流程没理解,所以耽误了不少时间。问了hanbingle大佬后才知道这里只是通过反射运行的函数才能HOOK到。

    另外我使用replace 比attach hook到的更少了,一直不知道什么问题。但是使用replace如果不调用ArtMethod::Invoke原始函数也不会触发程序填充函数,所以也就还是只用attach了。

六、使用Frida脱壳脚本

    上方的Frida编写时是对应另外一个APK进行编写的,所以到了本题也有一些修改,很不方便的一点就是Dex在内存中位置的取值是写死的,具体可以查看上传的代码。

    1.由于自己写的Frida脚本就是按着fart的思路来写的,所以也会在自定义ClassLoader这里出错。

    Error: Cast from 'com.bytedance.frameworks.plugin.core.DelegateClassLoader' to 'dalvik.system.BaseDexClassLoader' isn't possible

    错误是由于DelegateClassLoader直接继承至ClassLoader,不能转换为BaseDexClassLoader,也无法枚举出所有ClassName

    2.这时候虽然枚举不出来类,但是Java.use("com.sup.android.superb.SplashActivity")是正常的。那么可以直接不枚举Class,直接指定一个类名,然后枚举它的函数主动调用,Dump下对应Code。

function hook_java(){
    Java.perform(function(){
        loadClassAndInvoke("com.sup.android.superb.SplashActivity");
    });
}

function loadClassAndInvoke(className) {
    Java.perform(function(){
        try {
            var classResult = Java.use(className).class;
            if(!classResult) return;

            var methodArr = classResult.getDeclaredConstructors();
            methodArr = methodArr.concat(classResult.getDeclaredMethods());

            console.log(className,"\t",methodArr.length);
            for(var i=0;i<methodArr.length;i++){
                var  methodName = methodArr[i].toString();
                if(methodName.indexOf(TestFunction) > -1){
                    if(methodName in dumpMethodName){
                        continue;
                    }
                    console.log("methodName:",methodName);
                    // c++层调用
                    if(ArtMethod_invoke_replace){
                        //每次都会报错,但是我还没找到更方便的
                        try{
                            dumpMethodName.push(methodName);
                            // console.log("getArtMethod:", hexdump(ptr(methodArr[i].getArtMethod())));
                            ArtMethod_invoke_replace(ptr(methodArr[i].getArtMethod()), ptr(0), ptr(0), 6, invokeSize, invokeStr);
                        } catch(error){
                            // console.log("ArtMethod_invoke error:[",className,"]",error);
                        }
                    }
                }
            }
            
        } catch (error) { 
            console.log("loadClassAndInvoke error:[",className,"]",error);
        }
    });
}

    这时候Dump是成功的,还原到Dex文件,这个类就修复了。

    

    3.那么现在问题就是如何枚举Dex的ClassName。其实这里可以直接使用Fart的8958236_classlist_execute.txt文件即可。但是还是想试试能不能直接通过ClassLoader枚举出来类。

七、解决枚举Dex类

    1.这时候查看8958236_classlist_execute.txt,发现里面其实是有我们需要枚举的类,现在就是看这个怎么枚举来的。

 

    2.8958236_classlist_execute.txt来源,他其实是通过解析Dex文件得来的。具体可以查看Fart源码的dumpdexfilebyExecute方法。那么得出结论,Fart虽然枚举出来了这些类,但其实也不是通过ClassLoader枚举,没有参考价值。

    3.先看看普通的ClassLoader枚举类的方式      ->这里代表继承自

    PathClassLoader->dalvik.system.BaseDexClassLoader

    dalvik.system.BaseDexClassLoader.pathList->dalvik.system.DexPathList

    pathList.dexElements->dalvik.system.DexPathList$Element

    dexElements.dexFile->dalvik.system.DexFile

    dexFile.getClassNameList

    那其实也就是获取到对应DexFile对象然后调用getClassNameList方法,看下getClassNameList方法好像也就是解析Dex文件,也不能参考。

    4.查看ClassLoader.java源码可以看到一些与 java.lang.Package类相关的字段和函数。而Package也并不是Dex相关。

    同时ClassLoader类也有字段private transient long classTable;看着比较像,但是Frida得出值为0.

    5.再次查看com.bytedance.frameworks.plugin.core.DelegateClassLoader类,发现有个字段名叫pathClassLoader。尝试枚举后发现其实pathClassLoader字段对应的DexFile只能枚举出100多个类,和6000多差的太远。

    6.看了一圈,决定这里也通过自己解析DexFile文件来实现枚举Class

DexBase = base;
DexSize = size;
// console.log("DexBase:",hexdump(base));
var string_ids_size = DexBase.add(0x38).readU32();
var string_ids_off = DexBase.add(0x3c).readU32();
console.log("uint string_ids_size:",string_ids_size); //.toString(1
console.log("uint string_ids_off:",string_ids_off);
var type_ids_size = ptr(DexBase).add(0x40).readU32();
var type_ids_off = ptr(DexBase).add(0x44).readU32();
console.log("uint type_ids_size:",type_ids_size);
console.log("uint type_ids_off:",type_ids_off);
                
var class_idx = ptr(DexBase).add(0x60).readU32();
var class_defs_off = ptr(DexBase).add(0x64).readU32();
console.log("uint class_idx:",class_idx);
console.log("uint class_defs_off:",class_defs_off);
// var offsetStrEnd = DexBase.add(type_ids_off);
// console.log("offsetStrEnd:",offsetStrEnd);
for(var i=0; i<class_idx; i++){
    var offsetClass = DexBase.add(class_defs_off+i*0x20);
    // console.log("offsetClass:",offsetClass);
    var type_idx = offsetClass.readU32();
    // console.log("type_idx:",type_idx);
    var descriptor_idx = DexBase.add(type_ids_off+type_idx*0x4).rea
    // console.log("descriptor_idx:",descriptor_idx);
    var offsetStr = DexBase.add(string_ids_off + descriptor_idx*4).
    // console.log("offsetStr:",offsetStr);
    if(offsetStr > size){
        console.log("offsetStr > size:",offsetStr,">",size);
        break;
    }
    var addrStr =  DexBase.add(offsetStr);
    // console.log("addrStr:", hexdump(addrStr));
    // console.log("addrStr.readU32:",);
    var classNameLen =  addrStr.readU8();
    if(classNameLen > 0x7f){
        //这里类名都没超过0x7F
        console.log("ClassName Len > 0x7f:",addrStr);
        var lebdtl = DecodeUnsignedLeb128(addrStr);
        addrStr = addrStr.add(lebdtl[1]);
    }else{
        addrStr = addrStr.add(1);
    }
    // console.log("addrStr:",addrStr);
    // 读utf16有错误
    // var str = addrStr.readUtf16String();
    var str = addrStr.readUtf8String();
    // console.log(i,":", str);
    // console.log(hexdump(addrStr));
    // break;
    str = str.replace(/L([^;]+);/,"$1").replace(/\//g,'.');
    classArr.push(str);
}
console.log("classArr.length:",classArr.length);

    枚举出6895个类,枚举类问题解决。

八、脱壳操作

    1.修改脚本,直接根据指定DexFile文件枚举出的类列表依次主动调用。具体操作和那个作业类似。

function hook_java(){
    console.log("--------------------Start Invoke:",new Date().getTime());
    for(var i=0; i<classArr.length; i++ ){
        if(classArr[i].indexOf(TestCalss) >= 0){
            console.log("class:",classArr[i]);
            loadClassAndInvoke(classArr[i]);
        }
    }
    console.log("--------------------End Invoke:",new Date().getTime());
    dump_dex("fixed.dex");
}

    2.Dump包含com.sup.android字符串的类,共2926个函数体,修复后查看Dex,可以看到com.sup.android下的一些类函数都还原了。

    3.直接Dump修复整个Dex的所有函数,这里直接把过滤字符置空即可

    程序运行了大概20多分钟才结束,非常慢,Dump出的Bin文件40多M

    共Dump下14万方法,修复后查看Dex文件

    对比文件修改的地方非常多。

    大多数函数也已经修复了。现在问题就是一次运行太慢了。

九、优化整体脱壳速度

    1.根据之前被动调用脱下来的函数可以得出结论,函数被修复后就一直保存在Dex文件中了。那么可以直接获取所有类主动调用,过程中不Dump下每个函数,而是等全部类主动调用完后Dump下当时内存的Dex文件。

    2.不进行hook或者直接return都可,这里还是留着,直接return.

    这样时间大概只有3-4分钟,快了一些。

    3.再最初获取到Dex文件的时候Dump一次保存问init.dex。另外在主动调用完之后再保存一份fixed.dex。

    fixed.dex中相对于init.dex也填充了很多函数体。

    4.对比整体Dex和函数粒度修复的Dex

    整体Dump的比函数粒度修复的多了一点,应该是函数粒度有些运行BUG

    那么像这种填充函数体后可以直接Dump的还是直接Dump整体Dex更快也更稳定。

十、总结

    1.本题特别之处就是自定义ClassLoader导致不能通过ClassLoader枚举出类,直接解析Dex文件也方便解决

    2.自己写的这个脚本,其实和网上整体DexDump就多了一个主动调用,只是可以单个函数调试,查看某个函数如何填充的。便于个人理解,实际作用倒也不大。

    3.示例程序没有禁止Frida,方便很多。

APK超过8M无法直接上传https://pan.baidu.com/s/1GgAa5EF5rtZ5HX9vguSfnA s0oe

[公告]看雪论坛2020激励机制上线了!发帖不减雪币了!如何获得积分快速升级?


文章来源: https://bbs.pediy.com/thread-260540.htm
如有侵权请联系:admin#unsafe.sh