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