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