反编译 AndroidKiller 逆向 字节码 实践案例
目录
反编译 AndroidKiller 逆向 实践案例 MD
PS:以下所有内容,包括测试用来逆向的APP均已脱敏
参考我的其他博客内容:
AndroidKiller 简介
Android Killer 是一款可以对APK进行反编译的工具,它能够对反编译后的Smali
文件进行修改,并将修改后的文件进行打包。
AndroidKiller的基本功能:
- 可以修改清单文件,比如修改包名
- 可以修改(替换)任意资源文件,比如 string、drawable、color、layout 等,以及 assets、raw 等
- 可以对整个工程中的字符或文件进行搜索(支持匹配编码、匹配文件类型、匹配范围)
- 可以查看对应的 smali 、class 源码,以及反编译的 java 源码
可以对修改后的工程重新打包
插件升级
软件中的Apktool
可能会因为版本太低导致 apk 的反编译失败,此时需要到 Apktool 官网去下载最新版本的Apktool。
下载完成后找到解压好的 AndroidKiller 目录下的bin\apktool\apktool
目录将下载的最新版的 apktool 复制进去。
然后修改 AndroidKiller 根目录下的bin\apktool
下的apktool.bat
和apktool.ini
文件,将里面对应的文件名改为你下载的最新的 apktool 文件名。
基本使用
使用 AndroidKiller 对 Apk 进行反编译只需要将 Apk 文件拖入软件即可。
反编译后我们可以对资源文件
等进行简单的修改,修改后后点击左上角菜单栏中的Android -- 编译
即可重新编译成APK。
另外我们也可以点击 smali 目录,查看反编译的 smali 源码,并可以在某一 smali 源码文件中右键 -- 查看 -- 查看源码
通过 Java Decompiler
查看反编译的 java 源码。
实践案例
修改清单文件
经常会修改包名、应用图标、应用名称等基本信息,只需在这里找到相应的res并修改即可。
另外还可以查看其使用的Application,因为一般一些初始化操作、全局常量的设置等都是在Application里面。
<manifest package="包名" >
<application android:icon="图标" android:label="名称" android:name="使用的Application">
打印 debug 级别的日志
或者说是开启debug模式,一般有如下几种处理思路
方式一:直接代理 Log 类
按如下方式便可以 hook 住系统 Log 的方法调用,我们可以在 Log.d 等方法被调用 前/后 做自己的逻辑,比如对日志进行过滤、把日志保存到文件中。
public class XposedZygoteInit implements IXposedHookZygoteInit {
@Override
public void initZygote(StartupParam startupParam) throws Throwable {
//hook住某个方法。参数:类名,类加载器,方法名,参数列表,Hook成功后的回调
XposedHelpers.findAndHookMethod("android.util.Log", StartupParam.class.getClassLoader(), "d", String.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
//Log.i("bqt", "【代理系统的Log类,可以在这里把日志保存到文件中】" + param.args[0] + " ," + param.args[1]);
}
});
}
}
方式二:通过修改字段值修改判断条件
比如,反编译一款 APP 后发现其打印日志的部分逻辑如下:
public class LogTool{
public static boolean a = ;
public static String b = "通用的tag";
public static void a(String paramString) {
if (a) {
Log.d(b, paramString);
}
}
在 release 包中,这个值静态字段 a 肯定是 false,我们只需将其改为 true 即可打印 debug 下才会打印的日志。
hook 方式如下:
public class XposedInit implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Exception {
if (lpparam.packageName.equals(应用的包名,进入if代表此应用启动了,可以开始hook了)) {
Field field = XposedHelpers.findClass("完整路径.LogTool", lpparam.classLoader).getField("a");
field.setAccessible(true);
field.setBoolean(null, true); //静态字段的 the object whose field should be modified is null
}
}
}
PS:hook 系统类不能通过 IXposedHookLoadPackage
,而需要通过 IXposedHookZygoteInit
,因为此应用启动前,系统类已经被加载过了,已经错过了 hook 的时机了。
方式三:通过修改方法返回值修改判断条件
比如,反编译一款 APP 后发现其打印日志的部分逻辑如下:
public class LogTool {
public static void d(String paramString1, String paramString2) {
if (isLogAble(LogLevel.DEBUG)) {
//...
}
}
private static boolean isLogAble(LogLevel paramLogLevel) {
if (logcatLevel.value == LogLevel.OFF.value) {
return false;
}
if (logcatLevel.value == LogLevel.ALL.value) {
return true;
}
return logcatLevel.compare(paramLogLevel) >= 0;
}
}
类似上述案例,我们只需hook住isLogAble方法,并且将返回值直接返回true即可跳过日志级别判断。
hook 方式如下:
XposedHelpers.findAndHookMethod("类的完整路径", lpparam.classLoader, "isLogAble", "参数的完整路径", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.i("bqt", "【" + Arrays.toString(param.args) + "】");
param.setResult(true);
}
});
方式四:修改 BuildConfig.DEBUG 中的 DEBUG 常量值
这种方式和上面那种方式类似,不过适用场景更广泛。
APP编译时会自动为每一个module生成一个 BuildConfig
类,其中包含一些经常使用到的与环境相关的常量,例如:
public final class BuildConfig {
public static final boolean DEBUG = Boolean.parseBoolean("true"); //是否是调试模式
public static final String APPLICATION_ID = "com.bqt.test"; //包名
public static final String BUILD_TYPE = "debug";
public static final String FLAVOR = "";
public static final int VERSION_CODE = 1;
public static final String VERSION_NAME = "1.0";
}
很多时候,我们判断是否应打印日志就是根据 BuildConfig.DEBUG
来确定的,所以我们只需要修改这个值就可以了。
修改这个值的方式肯定不是通过Xposed框架,因为这些常量都是 final 类型的,动态框架肯定是无法修改的,一种可行的方式:
- 修改
.class
文件,具体详见:Javassist 字节码 简介 案例 MD - 重新打成jar包,具体详见:字节码 反编译 APKTool 重新打jar包 MD
- 重新编译APK(借助AndroidKiller)
如何hook混淆后指定类中的指定方法
首先需要明白,混淆后的类名、方法名、成员变量名等都是固定的,反编译后你看到的名字是什么,运行时它的名字就是什么,不会再变的
然后你还需要知道,他的名字虽然是固定的,但是往往你没办法直接hook,因为很多混淆后的名字都是非法字符,你即没法输入(IDE不识别),也没法通过编译(编译器不识别),所以需要采用特殊的方式来hook。
以下是一种可供参考的案例:
//从指定类中混淆后的方法名中获取匹配的方法
String methodName = "";
Method[] methods = XposedHelpers.findClass("o.ayc", lpparam.classLoader).getDeclaredMethods();
for (Method method : methods) {
method.setAccessible(true);
//匹配返回值,这里只是简单字符串匹配,更精确的可以通过类型匹配
if (method.getReturnType().toString().contains("HttpURLConnection")) {
methodName = method.getName();
break;
}
}
通过上述方法,便可跳过IDE和编译器
的名称规范的检查,在运行时
便可匹配混淆后的类或方法(运行时并不会进行规范性检查)。
完整案例
基本步骤
1、安装 XposedInstaller.apk,安装后启动此应用,安装 framework ,重启手机
2、AS工程中添加依赖
compileOnly 'de.robv.android.xposed:api:82:sources'//xposed依赖,注意这个版本号和framework版本号并不是一致的
implementation files('libs/javassist.jar')//非必须
3、application下添加三个meta-data
<meta-data
android:name="xposedmodule"
android:value="true"/>
<meta-data
android:name="xposeddescription"
android:value="这是对你使用xposed能完成功能的简要描述"/>
<meta-data
android:name="xposedminversion"
android:value="89"/>
4、编写Hook逻辑
5、配置完整的Hook类类名
新建assets文件夹,文件夹下新建xposed_init
文件(文件名固定),在文件中填写hook逻辑所在类的fully qualified class name:
com.bqt.test.temp.XposedInit
com.bqt.test.temp.XposedZygoteInit
6、有任何代码变动,都需重装APP后重启手机才能生效
XposedInit
public class XposedInit implements IXposedHookLoadPackage {
@Override
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) throws Exception {
if (lpparam.packageName.equals("应用1的包名,进入if代表此应用启动了,可以开始hook了")) {
//开启日志打印
XposedHelpers.findAndHookMethod("类的完整路径", lpparam.classLoader, "isLogAble", "参数的完整路径", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.i("bqt", "日志级别【" + Arrays.toString(param.args) + "】");
param.setResult(true);
}
});
//从指定类中混淆后的方法名中获取匹配的方法
String methodName = "";
Method[] methods = XposedHelpers.findClass("o.ayc", lpparam.classLoader).getDeclaredMethods();
for (Method method : methods) {
method.setAccessible(true);
//匹配返回值,这里只是简单字符串匹配,更精确的可以通过类型匹配
if (method.getReturnType().toString().contains("HttpURLConnection")) {
methodName = method.getName();
break;
}
}
//hook住某个方法。参数:类名,类加载器,方法名,参数列表,Hook成功后的回调
XposedHelpers.findAndHookMethod("o.ayc", lpparam.classLoader, methodName, String.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
Log.i("bqt", "1-【" + param.method.getName() + "】" + param.args[0]);
}
});
//只能hook具体的方法,不能hook接口的方法或抽象的方法。比如只能hook住某个Runnable的实现类的run方法,而不能hook住所有Runnable的run方法
XposedHelpers.findAndHookMethod("o.ayf", lpparam.classLoader, "run", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.i("bqt", "2-【" + param.thisObject.getClass().toString() + "】");
}
});
} else if (lpparam.packageName.equals("应用2的包名")) {
//开启日志打印
Field field = XposedHelpers.findClass("完整路径.LogTool", lpparam.classLoader).getField("a");
field.setAccessible(true);
field.setBoolean(null, true); //静态字段的 the object whose field should be modified is null
//注意,基本类型变量的class文件不能使用其相应包装类型来标识,例如 boolean.class 不能使用 Boolean.class 来代替
XposedHelpers.findAndHookMethod("包名.a", lpparam.classLoader, "a", long.class, String.class, boolean.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
Log.i("bqt", "3-【" + Arrays.toString(param.args) + "】");
}
});
}
}
}
XposedZygoteInit
public class XposedZygoteInit implements IXposedHookZygoteInit {
@Override
public void initZygote(StartupParam startupParam) throws Throwable {
XposedHelpers.findAndHookMethod("android.util.Log", StartupParam.class.getClassLoader(), "d", String.class, String.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
//Log.i("bqt", "【代理系统的Log类,可以在这里把日志保存到文件中】" + param.args[0] + " ," + param.args[1]);
}
});
}
}
2019-09-30
本文来自博客园,作者:白乾涛,转载请注明原文链接:https://www.cnblogs.com/baiqiantao/p/11611811.html