我的GitHub | 我的博客 | 我的微信 | 我的郵箱 |
---|---|---|---|
baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
目錄
反編譯 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