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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章