FrenchYeti/dexcalibur

View on GitHub
inspectors/DynamicLoader/main.js

Summary

Maintainability
D
1 day
Test Coverage
const Fs = require("fs");
const Path = require("path")

const HOOK = require("../../src/HookManager.js");
const CLASS = require("../../src/CoreClass.js");

const Inspector = require("../../src/Inspector.js");
const InspectorFactory = require("../../src/InspectorFactory.js");
const Event = require("../../src/Event.js");
const Logger = require("../../src/Logger.js")();
const AnalysisHelper = require("../../src/AnalysisHelper.js");
const ut = require("../../src/Utils.js");
const DexHelper = require('../../src/DexHelper');


// ===== INIT =====

var DynLoaderInspector = new InspectorFactory({

    id: "DynamicLoader",
    name: "Dynamic class loader inspector",
    description: "Update the application representation with Custom classloader and reflection data",

    startStep: Inspector.STEP.POST_APP_SCAN,

    useGUI: true,

    db: {
        dbms: 'inmemory',
        type: 'index',
        name: 'dex'
    },

    tags: {
        "dynamic_loading": ["invoked", "loaded"]
    },

    color: 'purple',


    hookSet: {
        require: ["Common", "Reflect"],
        hookShare: {
            classloader: [],
            additionalDex: []
        },
        hooks: [
            {
                //when: HOOK.BEFORE,
                method: "java.lang.Class.getMethod(<java.lang.String><java.lang.Class>[])<java.lang.reflect.Method>",
                onMatch: function (ctx, event) {
                    ctx.getInspector("DynamicLoader").emits("hook.reflect.method.get", event);
                },
                interceptReplace: `  
                        var ret = meth_@@__METHDEF__@@.call(this, arg0, arg1);
                        var cls = Java.cast( ret.getDeclaringClass(), DEXC_MODULE.common.class.java.lang.Class);
                        
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: true, 
                            data: {
                                __meth__: DEXC_MODULE.reflect.getMethodSignature(ret,arg1),
                                __hidden__trace: DEXC_MODULE.common.getStackTrace()
                            },
                            after: true, 
                            msg: "Class.getMethod()", 
                            tags: [{
                                style:"purple",
                                text: "invoke"
                            }], 
                            action: "Update" 
                        });
            
                        //  if(!@@__CTX__@@_invokeHooked) @@__CTX__@@_startInvokeHooking();
            
                        return ret;
                `
            }, {
                //when: HOOK.BEFORE,
                method: "java.lang.Class.forName(<java.lang.String><boolean><java.lang.ClassLoader>)<java.lang.Class>",
                onMatch: function (ctx, event) {
                    ctx.getInspector("DynamicLoader").emits("hook.reflect.class.get", event);
                },
                interceptAfter: `  
            
                        //var clscl = Java.cast( arg2.getClass(), DEXC_MODULE.common.class.java.lang.Class);
            
                        //var types = Java.array( this.getParameterTypes(), DEXC_MODULE.common.class.java.lang.Class);
            
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: true, 
                            data: {
                                __class__: arg0.toString()
                            },
                            after: true, 
                            msg: "Class.forName()", 
                            tags: [{
                                style:"purple",
                                text: "dynamic"
                            }], 
                            action: "Update" 
                        });
                `
            }, {
                //when: HOOK.BEFORE,
                method: "dalvik.system.BaseDexClassLoader.findClass(<java.lang.String>)<java.lang.Class>",
                onMatch: function (ctx, event) {
                    ctx.getInspector("DynamicLoader").emits("hook.dex.find.class", event);
                },
                interceptAfter: `   
                        // get classname
                        var cls = Java.cast(ret, CLS.java.lang.Class);
                        // collect methods
                        // cls.getMethods();
            
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: true, 
                            data: {
                                __class__: cls.getName()
                            },
                            after: true, 
                            msg: "BaseDexClassLoader.findClass()", 
                            tags: [{
                                style:"purple",
                                text: "dynamic"
                            }], 
                            action:"Log" 
                        });
                `
            }, {
                //when: HOOK.BEFORE,
                method: "dalvik.system.DexClassLoader.<init>(<java.lang.String><java.lang.String><java.lang.String><java.lang.ClassLoader>)<void>",
                onMatch: function (ctx, data) {
                    // the evvent data contains the bytecode of the Dex file        
                    ctx.getInspector("DynamicLoader").emits("hook.dex.classloader.new", data);
                },
                interceptBefore: `   
                
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: true, 
                            data: {
                                arg0: arguments[0],
                                arg1: arguments[1],
                                arg2: arguments[2],
                                __hidden__data: DEXC_MODULE.common.readFile(arguments[0])
                            },
                            after: true, 
                            msg: "DexClassLoader.<init>()", 
                            tags: [{
                                style:"purple",
                                text: "dynamic"
                            }], 
                            action:"Log" 
                        });
            
                `
            }, {
                //when: HOOK.BEFORE,
                method: "dalvik.system.DexFile.loadDex(<java.lang.String><java.lang.String><int>)<dalvik.system.DexFile>",
                onMatch: function (ctx, event) {
                    ctx.getInspector("DynamicLoader").emits("hook.dex.load", event);
                },
                variables: {
                    names: new HOOK.VariableArray([])
                },
                interceptBefore: `
                
                        var doCondition = true;
            
            
                        if(@@__VAR__@@.names.indexOf(arguments[0])>-1) 
                            doCondition = false;
                        
            
            
                        if(doCondition){
                            send({ 
                                id:"@@__HOOK_ID__@@", 
                                match: true, 
                                data: {
                                    dex: arguments[0],
                                    odex: arguments[1],
                                    arg2: arguments[2],
                                    isNew: true,
                                    __hidden__data: DEXC_MODULE.common.readFile(arguments[0])
                                },
                                after: false, 
                                msg: "DexFile.loadDex()", 
                                tags: [{
                                    style:"purple",
                                    text: "dynamic"
                                }], 
                                action:"Log" 
                            });
                        }else{
                            send({ 
                                id:"@@__HOOK_ID__@@", 
                                match: true, 
                                data: {
                                    dex: arguments[0],
                                    odex: arguments[1],
                                    arg2: arguments[2],
                                    isNew: false,
                                    __hidden__data: null
                                },
                                after: false, 
                                msg: "DexFile.loadDex()", 
                                tags: [{
                                    style:"purple",
                                    text: "dynamic"
                                }], 
                                action:"Log" 
                            });
                        }
            
                        
                `
            }, {
                //when: HOOK.BEFORE,
                method: [
                    "dalvik.system.DexFile.<init>(<java.io.File>)<void>",
                    "dalvik.system.DexFile.<init>(<java.lang.String>)<void>",
                ],
                onMatch: function (ctx, event) {
                    ctx.getInspector("DynamicLoader").emits("hook.dex.new", event);
                },
                interceptBefore: `     
                        if(isInstanceOf(arg0,"java.io.File"))
                            path = arg0.getAbsolutePath();
                        else
                            path = arg0;
            
                        // DEXC_MODULE.common.copy(path, "dexfile.dex");
            
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: false, 
                            data: {
                                path: path,
                            },
                            after: false, 
                            msg: "DexFile.<init>()", 
                            tags: [{
                                style:"purple",
                                text: "dynamic"
                            }], 
                            action:"Log" 
                        });
                `
            }/*,{
                method: "android.os.Parcelable$ClassLoaderCreator.createFromParcel(<android.os.Parcel><java.lang.ClassLoader>)<java.lang.Object>",
                onMatch: function(ctx,event){
                   
                },
                interceptBefore: `    
                        
                        var parcel = arg0;
                        var cll = arg1;
            
                        send({ 
                            id:"@@__HOOK_ID__@@", 
                            match: true, 
                            data: {
                                classloader: "--" //arg1.getClass,
                            }
                            after: true, 
                            msg: "ClassLoaderCreator.createFromParcel()", 
                            tags: [{
                                style:"purple",
                                text: "dynamic"
                            }], 
                            action:"Log" 
                        });
                `
            }*/

        ]
    },

    eventListeners: {

        "hook.dex.load": function (ctx, event) {
            if (event.data.data.isNew == false) return null;

            let hook = ctx.hook.getHookByID(ut.b64_decode(event.data.hook));

            Logger.info("[INSPECTOR][TASK] DynLoaderInspector new Dex file loaded :\tDex: ", event.data.data.dex);

            // update variable for next time
            hook.getVariable('names').getData().push(event.data.data.dex);
        },

        "hook.dex.new": function (ctx, event) {
            Logger.info("[INSPECTOR][TASK] DynLoaderInspector new Dex file", event.data.data.path);
        },

        "hook.reflect.class.get": function (ctx, event) {
            // get CFG
            //let db = ctx.analyze.db;

            // search if the method exists
            console.log(event.data);

            Logger.info("[INSPECTOR][TASK] DynLoaderInspector search Class ", event.data.data.signature);
        },

        "hook.reflect.method.get": function (ctx, event) {
            Logger.info("[INSPECTOR][TASK] DynLoaderInspector search Method ");

            //console.log(event);
            if (event == null || event.data == null || event.data.data == null) return false;
            let data = event.data.data, caller = null, callers = null, methd = null;

            //console.log(data);
            meth = ctx.find.get.method(data.__meth__);

            // find the callers by inspecting the stacktrace
            if (data.__hidden__trace != null && data.__hidden__trace.length > 2) {
                callers = ctx.find.method("__signature__:^" + ut.RegExpEscape(data.__hidden__trace[1].cls + "." + data.__hidden__trace[1].meth + "("));

                //  if no result, do nothing
                // try to resolve reference (it may be an inherited method)
                if (callers.count() == 0) {
                    Logger.info("Callers of '", data.__hidden__trace[1].cls + "." + data.__hidden__trace[1].meth, "' not found!");
                    return false;
                }

                // if more than one result, try to filter with filename/line number
                if (callers.count() > 1) {
                    Logger.warn("[INSPECTOR][TASK] DynLoaderInspector search Method : there are more than one result");
                } else {
                    //console.log(callers.get(0));
                    caller = callers.get(0);
                }
            } else {
                //  no trace ==> try another heuristic
                Logger.debug("No hidden trace");
                return false;

            }

            // not able to correlate (TODO : keep a track)
            if (caller == null || meth == null) {
                Logger.debug("Caller not found")
                return false;
            }

            // tag the method as "invoked dynamically"
            if (!meth.hasTag(AnalysisHelper.TAG.Invoked.Dynamically))
                meth.addTag(AnalysisHelper.TAG.Invoked.Dynamically);

            // update callers of the calleed methods
            // console.log('caller:',caller);
            meth.addCaller(caller);

            // , { tag: AnalysisHelper.TAG.Called.Dynamically }
            // update method used by the method performing the invoke
            call = new CLASS.Call({
                caller: caller,
                calleed: meth,
                instr: null,
                line: data.__hidden__trace[2].line,
                tags: [AnalysisHelper.TAG.Invoked.Dynamically]
            });

            caller.addMethodUsed(meth, call);

        },

        "hook.dex.find.class": function (ctx, event) {
            Logger.info("[INSPECTOR][TASK] DynLoaderInspector external class loaded dynamically ");
            if (event == null || event.data == null || event.data.data == null) return false;

            let data = event.data.data;
            let cls = ctx.find.get.class(data.__class__);
            //console.log(cls, data);

            if (cls == null) {
                cls = ctx.analyze.addClassFromFqcn(data.__class__);
            }


            if (!cls.hasTag(AnalysisHelper.TAG.Discover.Dynamically))
                cls.addTag(AnalysisHelper.TAG.Discover.Dynamically);
        },

        "hook.dex.classloader.new": function (ctx, event) {
            // 1. save gathered bytecode to a file
            // 2. disassemble this file 
            // 3. Analyze & update graph
            // 4. Workspace cleanup



            let rtWorkingDir = ctx.workspace.getRuntimeBcDir();
            let dexFileName = Path.basename(event.data.data.arg0);
            let localDexFile = Path.join(rtWorkingDir, dexFileName, dexFileName);
            let stat = null, ignore = false;
            let inspector = ctx.getInspector("DynamicLoader");

            Logger.info('Analyzing "'+dexFileName+'" at : '+localDexFile);

            // check if file exist
            if (Fs.existsSync(localDexFile)) {
                stat = Fs.lstatSync(localDexFile);

                if (stat.size == event.data.data.__hidden__data.length) {
                    // TODO : then if it is identic do checksum
                    ignore = true;
                    return;
                }
            }

            // create the folder where the dex file will be written
            if (!Fs.existsSync(Path.join(rtWorkingDir, dexFileName))) {
                Fs.mkdirSync(Path.join(rtWorkingDir, dexFileName));
            }

            if (ignore) return null;

            let data = Buffer.from(event.data.data.__hidden__data);

            Fs.open(localDexFile, 'w+', 0o666, function (err, fd) {
                if (err) {
                    Logger.error("TODO : An error occured when file is created ", err);
                    return;
                }

                Fs.write(fd, data, function (err, written, buffer) {
                    if (err) {
                        Logger.error("TODO : An error occured when file is written ", err);
                        return;
                    }

                    Fs.close(fd, function (err) {
                        if (err) {
                            Logger.error("TODO : An error occured when file is closed ", err);
                            return;
                        }

                        Logger.debug("Start to disassemble " + localDexFile);

                        // disass file
                        let destFolder = Path.join(rtWorkingDir, dexFileName, "smali");
                        (async function () {
                            let success = await DexHelper.disassemble(localDexFile, destFolder);

                            if (success != null) {
                                // do incremental static analysis  of destfolder
                                ctx.analyze.path(destFolder);
                                inspector.getDB().getIndex('dex').addEntry(new CLASS.File({
                                    name: dexFileName,
                                    path: localDexFile,
                                    remotePath: event.data.data.arg0
                                }));
                                inspector.save();
                            } else {
                                Logger.error('[DYNAMIC LOADER] Runtime DEX analysis failed.')
                            }
                        })();
                        /*
                        ctx.dexHelpers.disassembleFile(
                            localDexFile,
                            function(destFolder, err, stdout, stderr){
                                if(err){
                                    //todo
                                }else{
                                    ctx.analyze.path(destFolder);
                                    DynDB.getIndex('dex').addEntry(new CLASS.File({
                                        name: dexFileName,
                                        path: localDexFile,
                                        remotePath: event.data.data.arg0
                                    }));
                                    DynLoaderInspector.save();
                                }
    
                                // remove tmp files
                            });*/
                    });
                })
            })

            // 3. decompile resulting files
            // 4. update internal database

        },

        "hook.reflect.method.call": function (ctx, event) {
            Logger.info("[INSPECTOR][TASK] DynLoaderInspector method invoked dynamically ");
            //console.log(event);
            if (event == null || event.data == null || event.data.data == null) return false;
            let data = event.data.data;

            //console.log(data);
            let meth = ctx.find.get.method(data.s);
            //onsole.log(data);

            /*
                let rettype = ctx.find.get.class(event.data.data.ret)
    
            // if meth == null, the method is unknow and the graph should be updated
            if(meth == null){
                let ref = new CLASS.Method();
    
    
                ref.setReturnType(event.data)
                
    
            }*/
        },

        

        "dxc.fullscan.post_deploy": function (ctx, event) {
            Logger.info("[INSPECTOR][TASK] Trying to restore previous data of DynLoaderInspector ... ");
            let currentInspector = ctx.getInspector("DynamicLoader");

            currentInspector.restore();

            // generate hooks for every class which extend ClassLoader

            let classes = ctx.find.class("extends.name:ClassLoader");
            let meth = [];

            classes.foreach((pOffset,pCls) => {
                let m = pCls.getMethod({ name: '<init>' }, true);
                m.map(x=>{
                    meth.push(x);
                })
            });

            // remove already hooked methods from methods to hook
            let hooks = ctx.hook.getHooks();
            let validMethods = [];

            meth.map( vMethod => {
                if(!ctx.hook.isProbing(vMethod))
                    validMethods.push(vMethod.signature());
            })

            meth = null;

            if(validMethods.length > 0){
                currentInspector.hookSet.addIntercept({
                    //when: HOOK.BEFORE,
                    method: validMethods,
                    onMatch: function (ctx, event) {
                        console.log(event);
                        ctx.getInspector("DynamicLoader").emits("hook.classloader.new", event);
                    },
                    interceptBefore: `     
    
                            var data ={}; 
                            var path="", path2="";
    
                            for(var i=0; i<arguments.length; i++){
                                if(isInstanceOf(arguments[i],"java.io.File")){
                                    data['arg'+i] = arguments[i].getAbsolutePath();
                                    data['__hidden__targ'+i] = 'f';
                                }
                                else if(isInstanceOf(arguments[i],"java.net.URL")){
                                    data['arg'+i] = arguments[i].toString();
                                    data['__hidden__targ'+i] = 'u';
                                }
                                else{
                                    data['arg'+i] = arguments[i];
                                    data['__hidden__targ'+i] = 's';
                                }
                            }

                
                            send({ 
                                id:"@@__HOOK_ID__@@", 
                                match: false, 
                                data: data,
                                after: false, 
                                msg: "@@__FQCN__@@.@@__METHNAME__@@()", 
                                tags: [{
                                    style:"purple",
                                    text: "dynamic"
                                }], 
                                action:"trace" 
                            });
                        `
                });
                currentInspector.hookSet.deploy();
            }            
        }
    }
});




module.exports = DynLoaderInspector;