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