Android逆向分析——ZjDroid

ZjDroid是一个基于Xposed框架的脱壳工具。
Xposed本质上是一个动态劫持框架,通过替换系统启动时的zygote 进程为自带的zygote进程,加载XposedBridge.jar,开发者就可以通过这个jar包提供的API实现对所有的Function的劫持。具体的后面分析Xposed时再详细看吧。

总之呢,Xposed框架是一个非常牛逼的框架,可以便捷地修改系统而不需要刷包,基于这一框架可以制作许多强大的模块以实现各种功能,并且支持安装与卸载。
模块的开发也暂时不涉及,就先说说ZjDroid这个脱壳模块的使用吧。

源码地址为:https://github.com/halfkiss/ZjDroid

因为需要安装Xposed Framework,所以需要手机的root权限,而且模块安装完成后需要重启下手机替换Zygote进程。

跟着源码的流程来看吧。

首先是assets目录下的xposed_init
com.android.reverse.mod.ReverseXposedModule
这个文件记录了整个模块的入口类。

看一下这个类做了什么:

public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable {
        // TODO Auto-generated method stub
        if(lpparam.appInfo == null || 
                (lpparam.appInfo.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) !=0){
            return;
        }else if(lpparam.isFirstApplication && !ZJDROID_PACKAGENAME.equals(lpparam.packageName)){
          Logger.PACKAGENAME = lpparam.packageName;
          Logger.log("the package = "+lpparam.packageName +" has hook");
          Logger.log("the app target id = "+android.os.Process.myPid());
          PackageMetaInfo pminfo = PackageMetaInfo.fromXposed(lpparam);
          ModuleContext.getInstance().initModuleContext(pminfo);
          DexFileInfoCollecter.getInstance().start();
          LuaScriptInvoker.getInstance().start();
          ApiMonitorHookManager.getInstance().startMonitor();
        }else{

        }
    }

这个类里面只实现了一个handleLoadPackage方法,里面主要是进行了一些初始化操作。包括
创建一个PackageMetaInfo对象;
ModuleContext,应该就是完成模块相关功能的初始化;
DexFileInfoCollecter,收集dex的相关信息;
LuaScriptInvoker,脚本相关信息;
ApiMonitorHookManager,API监控。

继续看ModuleContext的initModuleContext接口实现:

public void initModuleContext(PackageMetaInfo info) {
        this.metaInfo = info;
        String appClassName = this.getAppInfo().className;
        if (appClassName == null) {
            Method hookOncreateMethod = null;
            try {
                hookOncreateMethod = Application.class.getDeclaredMethod("onCreate", new Class[] {});
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            hookhelper.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook());

        } else {
            Class<?> hook_application_class = null;
            try {
                hook_application_class = this.getBaseClassLoader().loadClass(appClassName);
                if (hook_application_class != null) {
                    Method hookOncreateMethod = hook_application_class.getDeclaredMethod("onCreate", new Class[] {});
                    if (hookOncreateMethod != null) {
                        hookhelper.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook());
                    }
                }
            } catch (ClassNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                // TODO Auto-generated catch block
                Method hookOncreateMethod;
                try {
                    hookOncreateMethod = Application.class.getDeclaredMethod("onCreate", new Class[] {});
                    if (hookOncreateMethod != null) {
                        hookhelper.hookMethod(hookOncreateMethod, new ApplicationOnCreateHook());
                    }
                } catch (NoSuchMethodException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }

            }
        }
    }

获取Application的onCreate方法,并对这个方法通过hookMethod进行拦截。

拦截之后,看下ApplicationOnCreateHook这个类的实现:

private class ApplicationOnCreateHook extends MethodHookCallBack {

        @Override
        public void beforeHookedMethod(HookParam param) {
            // TODO Auto-generated method stub

        }

        @Override
        public void afterHookedMethod(HookParam param) {
            // TODO Auto-generated method stub
            if (!HAS_REGISTER_LISENER) {
                fristApplication = (Application) param.thisObject;
                IntentFilter filter = new IntentFilter(CommandBroadcastReceiver.INTENT_ACTION);
                fristApplication.registerReceiver(new CommandBroadcastReceiver(), filter);
                HAS_REGISTER_LISENER = true;
            }
        }

    }

beforeHookedMethod没做什么,但是在afterHookedMethod中,添加了一个广播,也即实现了设备中每个应用程序启动后都会注册这样一个广播,后面我们再去发送对应action的广播时,每个程序都会收得到了。

继续看这个广播的实现:

public class CommandBroadcastReceiver extends BroadcastReceiver {

    public static String INTENT_ACTION = "com.zjdroid.invoke";
    public static String TARGET_KEY = "target";
    public static String COMMAND_NAME_KEY = "cmd";

    @Override
    public void onReceive(final Context arg0, Intent arg1) {
        // TODO Auto-generated method stub
        if (INTENT_ACTION.equals(arg1.getAction())) {
            try {
                int pid = arg1.getIntExtra(TARGET_KEY, 0);
                if (pid == android.os.Process.myPid()) {
                    String cmd = arg1.getStringExtra(COMMAND_NAME_KEY);
                    final CommandHandler handler = CommandHandlerParser
                            .parserCommand(cmd);
                    if (handler != null) {
                        new Thread(new Runnable() {
                            @Override
                            public void run() {
                                // TODO Auto-generated method stub
                                handler.doAction();
                            }
                        }).start();
                    }else{
                        Logger.log("the cmd is invalid");
                    }
                }
            } catch (Exception e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

}

里面就定义了一个onReceive方法,这个方法会去解析广播的intent。
首先是通过arg1.getIntExtra(TARGET_KEY, 0)获取了pid,只有指定的应用才会去对广播中的内容做响应;接下来通过arg1.getStringExtra(COMMAND_NAME_KEY)获取命令的字符串cmd,这里就指定了接收到广播后需要完成怎样的操作。
解析完intent后,会创建一个CommandHandler的对象handler,调用它的parserCommand(cmd)方法来解析命令;然后通过run()里面的handler.doAction()开始执行命令。

具体有哪些命令呢,看下CommandHandlerParser这个类:

public static CommandHandler parserCommand(String cmd) {
        CommandHandler handler = null;
        try {
            JSONObject jsoncmd = new JSONObject(cmd);
            String action = jsoncmd.getString(ACTION_NAME_KEY);
            Logger.log("the cmd = " + action);
            if (ACTION_DUMP_DEXINFO.equals(action)) {
                handler = new DumpDexInfoCommandHandler();
            } else if (ACTION_DUMP_DEXFILE.equals(action)) {
                if (jsoncmd.has(PARAM_DEXPATH_DUMPDEXCLASS)) {
                    String dexpath = jsoncmd.getString(PARAM_DEXPATH_DUMPDEXCLASS);
                    handler = new DumpDexFileCommandHandler(dexpath);
                } else {
                    Logger.log("please set the " + PARAM_DEXPATH_DUMPDEXCLASS + " value");
                }
            } else if (ACTION_BACKSMALI_DEXFILE.equals(action)) {
                if (jsoncmd.has(PARAM_DEXPATH_DUMPDEXCLASS)) {
                    String dexpath = jsoncmd.getString(PARAM_DEXPATH_DUMPDEXCLASS);
                    handler = new BackSmaliCommandHandler(dexpath);
                } else {
                    Logger.log("please set the " + PARAM_DEXPATH_DUMPDEXCLASS + " value");
                }
            } else if (ACTION_DUMP_DEXCLASS.equals(action)) {
                if (jsoncmd.has(PARAM_DEXPATH_DUMPDEXCLASS)) {
                    String dexpath = jsoncmd.getString(PARAM_DEXPATH_DUMP_DEXFILE);
                    handler = new DumpClassCommandHandler(dexpath);
                } else {
                    Logger.log("please set the " + PARAM_DEXPATH_DUMPDEXCLASS + " value");
                }
            } else if (ACTION_DUMP_HEAP.equals(action)) {
                handler = new DumpHeapCommandHandler();
            } else if (ACTION_INVOKE_SCRIPT.equals(action)) {
                if (jsoncmd.has(FILE_SCRIPT)) {
                    String filepath = jsoncmd.getString(FILE_SCRIPT);
                    handler = new InvokeScriptCommandHandler(filepath, ScriptType.FILETYPE);
                } else {
                    Logger.log("please set the " + FILE_SCRIPT);
                }

            } else if (ACTION_DUMP_MEMERY.equals(action)) {
                int start = jsoncmd.getInt(PARAM_START_DUMP_MEMERY);
                int length = jsoncmd.getInt(PARAM_LENGTH_DUMP_MEMERY);
                handler = new DumpMemCommandHandler(start, length);
            } else {
                Logger.log(action + " cmd is invalid! ");
            }
        } catch (JSONException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return handler;
    }

逻辑很清晰,首先是初始化一个handler对象,然后开始解析。可以看到cmd命令是json格式的,从ACTION_NAME_KEY得到具体的action,然后针对不同的action,将handler实例化为不同的CommandHandler对象,执行各自的doAction方法。
具体action有以下取值:

  • ACTION_DUMP_DEXINFO 获取dex的相关信息;
  • ACTION_DUMP_DEXFILE 这里还需要一个dexpath变量,来实现dex文件的dump操作;
  • ACTION_BACKSMALI_DEXFILE 同样也需要dexpath变量,将dex转化为smali文件;‘
  • ACTION_DUMP_DEXCLASS 需要dexpath变量,dump dex的class;
  • ACTION_DUMP_HEAP dump相关堆栈信息;
  • ACTION_INVOKE_SCRIPT 需要指明脚本文件的路径,执行脚本;
  • ACTION_DUMP_MEMERY 需要对应内存的起始位置和长度,dump一段指定内存。

这里也可以看出ZjDroid可以实现哪些功能了,看下这些功能具体怎么实现的:

1.DumpDexInfoCommandHandler();

public class DumpDexInfoCommandHandler implements CommandHandler {

    @Override
    public void doAction() {
        HashMap<String, DexFileInfo> dexfileInfo = DexFileInfoCollecter.getInstance().dumpDexFileInfo();
        Iterator<DexFileInfo> itor = dexfileInfo.values().iterator();
        DexFileInfo info = null;
        Logger.log("The DexFile Infomation ->");
        while (itor.hasNext()) {
            info = itor.next();
            Logger.log("filepath:"+ info.getDexPath()+" mCookie:"+info.getmCookie());
        }
        Logger.log("End DexFile Infomation");
    }

}

通过DexFileInfoCollecter的实例化对象的dumpDexFileInfo()方法,获取dexfileInfo的一个hash表,然后利用一个迭代器去打印DexFileInfo 的相关信息,包括filepath和mCookie。

看下dumpDexFileInfo()的实现:

public HashMap<String, DexFileInfo> dumpDexFileInfo() {
        HashMap<String, DexFileInfo> dexs = new HashMap<String, DexFileInfo>(dynLoadedDexInfo);
        Object dexPathList = RefInvoke.getFieldOjbect("dalvik.system.BaseDexClassLoader", pathClassLoader, "pathList");
        Object[] dexElements = (Object[]) RefInvoke.getFieldOjbect("dalvik.system.DexPathList", dexPathList, "dexElements");
        DexFile dexFile = null;
        for (int i = 0; i < dexElements.length; i++) {
            dexFile = (DexFile) RefInvoke.getFieldOjbect("dalvik.system.DexPathList$Element", dexElements[i], "dexFile");
            String mFileName = (String) RefInvoke.getFieldOjbect("dalvik.system.DexFile", dexFile, "mFileName");
            int mCookie = RefInvoke.getFieldInt("dalvik.system.DexFile", dexFile, "mCookie");
            DexFileInfo dexinfo = new DexFileInfo(mFileName, mCookie, pathClassLoader);
            dexs.put(mFileName, dexinfo);
        }
        return dexs;
    }

逻辑也比较简单,基本上通过反射完成。先通过默认类加载器PathClassLoader得到的dexPathList对象,再得到dexElements对象,最后得到具体的dexFile对象。对于每一个dexFile对象,会去获取它的mFileName和mCookie,这个mCookie是dex文件的唯一标识。最后,会创建一个DexFileInfo的结构来保存这些值。

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dexinfo”}’

2.DumpDexFileCommandHandler(dexpath);

public class DumpDexFileCommandHandler implements CommandHandler {

    private String dexpath;

    public DumpDexFileCommandHandler(String dexpath) {
        this.dexpath = dexpath;
    }

    @Override
    public void doAction() {
        // TODO Auto-generated method stub
        String filename = ModuleContext.getInstance().getAppContext().getFilesDir()+"/dexdump.odex";
        DexFileInfoCollecter.getInstance().dumpDexFile(filename, dexpath);
        Logger.log("the dexfile data save to ="+filename);
    }
}

首先通过ModuleContext实例化对象的getAppContext()获取运行时环境,然后通过getFilesDir()获取沙箱路径,拼接上”/dexdump.odex”构建一个文件名,就是dump出来的dex文件路径,然后通过DexFileInfoCollecter的实例化对象的dumpDexFile(filename, dexpath)实现dex的dump。

看下dumpDexFile的具体实现:

public void dumpDexFile(String filename, String dexPath) {
        File file = new File(filename);
        try {
            if (!file.exists())
                file.createNewFile();
            int mCookie = this.getCookie(dexPath);
            if (mCookie != 0) {
                FileOutputStream out = new FileOutputStream(file);
                ByteBuffer data = NativeFunction.dumpDexFileByCookie(mCookie, ModuleContext.getInstance().getApiLevel());
                data.order(ByteOrder.LITTLE_ENDIAN);
                byte[] buffer = new byte[8192];
                data.clear();
                while (data.hasRemaining()) {
                    int count = Math.min(buffer.length, data.remaining());
                    data.get(buffer, 0, count);
                    try {
                        out.write(buffer, 0, count);
                    } catch (IOException e1) {
                        // TODO Auto-generated catch block
                        e1.printStackTrace();
                    }
                }
            } else {
                Logger.log("the cookie is not right");
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

基本上就是检查要写入的文件存不存在,不存在则创建一个,接着通过native方法dumpDexFileByCookie获取数据,做一下大小端转化,然后写入数据。

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dex”,”dexpath”:”……”}’

3.BackSmaliCommandHandler(dexpath);

public class BackSmaliCommandHandler implements CommandHandler {

    private String dexpath;

    public BackSmaliCommandHandler(String dexpath) {
        this.dexpath = dexpath;
    }

    @Override
    public void doAction() {
        // TODO Auto-generated method stub
        String filename = ModuleContext.getInstance().getAppContext().getFilesDir()+"/dexfile.dex";
        DexFileInfoCollecter.getInstance().backsmaliDexFile(filename, dexpath);
        Logger.log("the dexfile data save to ="+filename);
    }

}

具体的backsmaliDexFile()方法:

    public void backsmaliDexFile(String filename, String dexPath) {
        File file = new File(filename);
        try {
            if (!file.exists())
                file.createNewFile();
            int mCookie = this.getCookie(dexPath);
            if (mCookie != 0) {
                MemoryBackSmali.disassembleDexFile(mCookie, filename);
            } else {
                Logger.log("the cookie is not right");
            }
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”backsmali”,”dexpath”:”……”}’

流程都差不多,贴下代码就不详细说了。

4.DumpClassCommandHandler(dexpath);

    public void doAction() {
        // TODO Auto-generated method stub
        String[] loadClass = DexFileInfoCollecter.getInstance().dumpLoadableClass(dexpath);
        if (loadClass != null) {
            Logger.log("Start Loadable ClassName ->");
            String className = null;
            for (int i = 0; i < loadClass.length; i++) {
                className = loadClass[i];
                if (!this.isFilterClass(className)) {
                    Logger.log("ClassName = " + className);
                }
            }
            Logger.log("End Loadable ClassName");
        }else{
            Logger.log("Can't find class loaded by the dex");
        }
    }
    public String[] dumpLoadableClass(String dexPath) {
        int mCookie = this.getCookie(dexPath);
        if (mCookie != 0) {
            return (String[]) RefInvoke.invokeStaticMethod("dalvik.system.DexFile", "getClassNameList", new Class[] { int.class },
                    new Object[] { mCookie });
        } else {
            Logger.log("the cookie is not right");
        }
        return null;

    }

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_class”,”dexpath”:”……”}’

5.DumpHeapCommandHandler();

public class DumpHeapCommandHandler implements CommandHandler {

    private static String dumpFileName;

    public DumpHeapCommandHandler() {
        dumpFileName = android.os.Process.myPid()+".hprof";
    }

    @Override
    public void doAction() {
        // TODO Auto-generated method stub
        String heapfilePath =ModuleContext.getInstance().getAppContext().getFilesDir()+"/"+dumpFileName;
        HeapDump.dumpHeap(heapfilePath);
        Logger.log("the heap data save to ="+ heapfilePath);
    }
}
    public static void dumpHeap(String filename) {
        try {
            Debug.dumpHprofData(filename);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_heap”}’

6.InvokeScriptCommandHandler(filepath, ScriptType.FILETYPE);

public class InvokeScriptCommandHandler implements CommandHandler {

    private String script;
    private String filePath;
    private ScriptType type;

    public static enum ScriptType {
        TEXTTYPE, FILETYPE
    }

    public InvokeScriptCommandHandler(String str, ScriptType type) {
        this.type = type;
        if (type == ScriptType.TEXTTYPE)
            this.script = str;
        else if (type == ScriptType.FILETYPE)
            this.filePath = str;
    }

    @Override
    public void doAction() {
        Logger.log("The Script invoke start");
        if (this.type == ScriptType.TEXTTYPE) {
            LuaScriptInvoker.getInstance().invokeScript(script);
        } else if (this.type == ScriptType.FILETYPE) {
            LuaScriptInvoker.getInstance().invokeFileScript(filePath);
        } else {
            Logger.log("the script type is invalid");
        }
        Logger.log("The Script invoke end");

    }

}

可以看到这里支持的脚本有两种类型,TEXTTYPE和FILETYPE。会根据不同的类型去执行不同的脚本调用函数,但是逻辑是一样的。

    public void invokeScript(String script){
        LuaState luaState = LuaStateFactory.newLuaState();
        luaState.openLibs();
        this.initLuaContext(luaState);
        int error = luaState.LdoString(script);
        if(error!=0){
            Logger.log("Read/Parse lua error. Exit");
            return;
        }

        luaState.close();
    }

    public void invokeFileScript(String scriptFilePath){
        LuaState luaState = LuaStateFactory.newLuaState();
        luaState.openLibs();
        this.initLuaContext(luaState);
        int error = luaState.LdoFile(scriptFilePath);
        if(error!=0){
            Logger.log("Read/Parse lua error. Exit");
            return;
        }
        luaState.close();
    }

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”invoke”,”filepath”:”**“}’

7.DumpMemCommandHandler(start, length);

public class DumpMemCommandHandler implements CommandHandler {

    private String dumpFileName;
    private int start;
    private int length;

    public DumpMemCommandHandler(int start, int length){
        this.start = start;
        this.length = length;
        this.dumpFileName = String.valueOf(start);
    }

    @Override
    public void doAction() {
        // TODO Auto-generated method stub
        String memfilePath = ModuleContext.getInstance().getAppContext().getFilesDir()+"/"+dumpFileName;
        MemDump.dumpMem(memfilePath,start, length);       
        Logger.log("the mem data save to ="+ memfilePath);
    }

}
    public static void dumpMem(String filepath, int start, int length) {
        ByteBuffer buffer = NativeFunction.dumpMemory(start, length);
        File file = new File(filepath);
        if (!file.exists()) {
            try {
                file.createNewFile();
                file.setWritable(true);
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }
        try {
            saveByteBuffer(new FileOutputStream(file), buffer);
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

命令:am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_mem”,”start”:……,”length”:……}’

另外,还有两个常用的命令:

打印日志:adb logcat -s zjdroid-shell-packagename
接口监控:adb logcat -s zjdroid-apimonitor-packagename

最后,对所有命令做个小结:

  • 获取当前dex文件信息
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dexinfo”}’
  • dump dex文件
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_dex”,”dexpath”:”……”}’
  • dump smali文件
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”backsmali”,”dexpath”:”……”}’
  • dump加载的class
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_class”,”dexpath”:”……”}’
  • dump java的堆栈信息
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_heap”}’
  • 执行脚本
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”invoke”,”filepath”:”**“}’
  • dump指定内存
    am broadcast -a com.zjdroid.invoke –ei target pid –es cmd ‘{“action”:”dump_mem”,”start”:……,”length”:……}’
  • 打印日志
    adb logcat -s zjdroid-shell-packagename
  • 监控敏感API
    adb logcat -s zjdroid-apimonitor-packagename
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值