文中相关工具下载链接:https://pan.baidu.com/s/1_bknFSnsYxLUNJ3WTulEFA 提取码:4qo8
我的所有原创Android知识体系,已打包整理到GitHub.努力打造一系列适合初中高级工程师能够看得懂的优质文章,欢迎star~
1. 反编译基操
1.1 借鉴code
一般来说,如果只是想借鉴一下友商的code,我们只需要拿到对方的apk,拖到jadx里面就行.jadx能查看apk的xml布局和java代码.jadx有时候会出现部分class反编译失败的情况,这时可以试试Bytecode-Viewer,它也能反编译,
而且还能反编译出jadx不能反编译的class.但是如果apk是已加固了的,那么jadx是不能查看代码的.这时需要脱壳,然后再进行反编译.
1.2 修改执行逻辑
如果是想修改程序的执行逻辑,则需要修改smali代码.
如何拿smali代码?
这时需要用到apktool,使用命令:apktool d xx.apk
即可将apk逆向完成,拿到smali代码.这里如果反编译失败了且报错org.jf.dexlib2.dexbacked.DexBackedDexFile$NotADexFile: Not a valid dex magic value: cf 77 4c c7 9b 21 01 cd
,则试试apktool d xx.apk -o xx --only-main-classes
这条命令.
然后用VS Code打开,这里最好在VS Code里面装一个Smali
插件,用于在VS Code里面支持smali语法,高亮之类的.完成之后大概是这个样子:
[图片上传失败...(image-be9ad-1601170309045)]
环境倒是OK了,回到正题,我们需要修改执行逻辑.在此之前,我们最好先简单学习一下smali的基本语法,详情见我之前写过的文章反编译基础知识.
修改好逻辑之后,我们需要将这些代码重新打包成apk,此时需要用到apktool,执行:apktool b xx
.执行完成之后,输出的apk会在xx/dist
目录下.它打包出来的是没有签名的apk,需要签名才能安装.
签名需要用到autosign这个工具包,使用命令java -jar signapk.jar testkey.x509.pem testkey.pk8 debug.apk debug_signed.apk
2. 加日志
有时候,你可能需要在修改原有执行逻辑之后,在代码里面加点日志,方便查看打出来的包逻辑是否正确.这里我摸索出一个简单的方式打日志,写一个日志打印工具类,然后将这个工具类转成smali文件,然后放入apk反编译出来的smali代码文件夹中,
之后就可以在这个项目的任何smali中使用这个工具类了.下面详细介绍一下:
2.1 写日志打印工具类LogUtil
这个日志打印工具类是为了外界方便调用的,所以需要让外界调用的时候尽量简单.下面是我简单实现的工具类,tag都是我定义好了的,免得外面再定义一次(麻烦).
public class LogUtil {
public static void logNoTrace(String str) {
Log.d("xfhy888", str);
}
public static void test() {
logNoTrace("大撒大撒大撒");
}
}
2.2 打印调用栈
上面的工具类目前只能打印普通的日志,但是有时我们想在打印日志的同时输出这个地方的调用栈,此时我们再加个方法扩展一下.
public static void log(String str) {
Log.d("xfhy888", str);
Throwable throwable = new Throwable();
StackTraceElement[] stackElements = throwable.getStackTrace();
StringBuilder stringBuilder = new StringBuilder();
if (stackElements != null) {
for (StackTraceElement stackElement : stackElements) {
stringBuilder.append(stackElement.getClassName()).append(" ");
stringBuilder.append(stackElement.getFileName()).append(" ");
stringBuilder.append(stackElement.getMethodName()).append(" ");
stringBuilder.append(stackElement.getLineNumber()).append("\n");
}
}
Log.d("xfhy888", stringBuilder.toString());
}
在log方法中我们手工构建了一个Throwable,然后通过其getStackTrace方法即可得到调用栈信息,通过Log打印出来.效果如下:
12817-12817/com.xfhy.demo D/xfhy888: com.xfhy.LogUtil LogUtil.java log 10
com.xfhy.startactivitydemo.MainActivity$1 MainActivity.java onClick 45
android.view.View View.java performClick 6724
android.view.View View.java performClickInternal 6682
android.view.View View.java access$3400 797
android.view.View$PerformClick View.java run 26472
android.os.Handler Handler.java handleCallback 873
android.os.Handler Handler.java dispatchMessage 99
android.os.Looper Looper.java loop 233
android.app.ActivityThread ActivityThread.java main 7210
java.lang.reflect.Method Method.java invoke -2
com.android.internal.os.RuntimeInit$MethodAndArgsCaller RuntimeInit.java run 499
com.android.internal.os.ZygoteInit ZygoteInit.java main 956
2.3 将工具类转smali
在Android Studio里面写好这个工具类之后,装一个java2smali
插件.然后选中LogUtil文件,再依次点击Build->Compile to Smali
,即可将LogUtil.java转成smali.下面是我转好的
.class public Lcom/xfhy/LogUtil;
.super Ljava/lang/Object;
.source "LogUtil.java"
# direct methods
.method public constructor <init>()V
.registers 1
.prologue
.line 5
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static log(Ljava/lang/String;)V
.registers 9
.param p0, "str" # Ljava/lang/String;
.prologue
.line 8
const-string v4, "xfhy888"
invoke-static {v4, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 10
new-instance v3, Ljava/lang/Throwable;
invoke-direct {v3}, Ljava/lang/Throwable;-><init>()V
.line 11
.local v3, "throwable":Ljava/lang/Throwable;
invoke-virtual {v3}, Ljava/lang/Throwable;->getStackTrace()[Ljava/lang/StackTraceElement;
move-result-object v1
.line 12
.local v1, "stackElements":[Ljava/lang/StackTraceElement;
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
.line 13
.local v2, "stringBuilder":Ljava/lang/StringBuilder;
if-eqz v1, :cond_52
.line 14
array-length v5, v1
const/4 v4, 0x0
:goto_17
if-ge v4, v5, :cond_52
aget-object v0, v1, v4
.line 15
.local v0, "stackElement":Ljava/lang/StackTraceElement;
invoke-virtual {v0}, Ljava/lang/StackTraceElement;->getClassName()Ljava/lang/String;
move-result-object v6
invoke-virtual {v2, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v6
const-string v7, " "
invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 16
invoke-virtual {v0}, Ljava/lang/StackTraceElement;->getFileName()Ljava/lang/String;
move-result-object v6
invoke-virtual {v2, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v6
const-string v7, " "
invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 17
invoke-virtual {v0}, Ljava/lang/StackTraceElement;->getMethodName()Ljava/lang/String;
move-result-object v6
invoke-virtual {v2, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v6
const-string v7, " "
invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 18
invoke-virtual {v0}, Ljava/lang/StackTraceElement;->getLineNumber()I
move-result v6
invoke-virtual {v2, v6}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
move-result-object v6
const-string v7, "\n"
invoke-virtual {v6, v7}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 14
add-int/lit8 v4, v4, 0x1
goto :goto_17
.line 21
.end local v0 # "stackElement":Ljava/lang/StackTraceElement;
:cond_52
const-string v4, "xfhy888"
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v5
invoke-static {v4, v5}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 22
return-void
.end method
.method public static logNoTrace(Ljava/lang/String;)V
.registers 2
.param p0, "str" # Ljava/lang/String;
.prologue
.line 25
const-string v0, "xfhy888"
invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 26
return-void
.end method
有了编译好的smali文件,还需要放到反编译项目的对应包名里面,我这里的是com/xfhy/
,那我就得放到这个目录下.
2.4 使用工具类
这里我随便写个方法测试一下,java代码如下:
public void test() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
它所对应的smali代码如下:
.method public test()V
.registers 3
.prologue
.line 29
const/4 v0, 0x0
.local v0, "i":I
:goto_1
const/16 v1, 0xa
if-ge v0, v1, :cond_d
.line 30
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V
.line 29
add-int/lit8 v0, v0, 0x1
goto :goto_1
.line 32
:cond_d
return-void
.end method
我在方法的一开始就打印一句日志,首先加registers个数+1,因为需要新定义一个变量来存字符串,然后再调用LogUtil的静态方法打印这个字符串.
.method public test()V
.registers 4
const-string v2, "test method"
invoke-static {v2}, Lcom/xfhy/LogUtil;->log(Ljava/lang/String;)V
.prologue
.line 29
const/4 v0, 0x0
.local v0, "i":I
:goto_1
const/16 v1, 0xa
if-ge v0, v1, :cond_d
.line 30
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(I)V
.line 29
add-int/lit8 v0, v0, 0x1
goto :goto_1
.line 32
:cond_d
return-void
.end method
3. 调试smali
我们不能直接调试反编译拿到的java代码,而是只能调试反编译拿到的smali代码.当然,调试的时候,需要懂一些smali的基本语法,这样的话,基本能看懂程序在干嘛.
3.1 让App可以调试
首先是让App可以调试
- 可以修改AndroidManifest.xml中的debuggable改为true(具体操作:先用apktool反编译,再修改AndroidManifest,再打包签名,运行到手机上);
- 也可以使用XDebug 让所有进程处于可以被调试的状态;
3.2 如何调试?
首先是在Android Studio里装一个smalidea的插件,我上面分享的网盘地址里面有.我试了下,smalidea是不支持最新版的Android Studio的.我去查了下,smalidea最后一个版本是0.05,
最后更新时间是2017-03-31。确实有点老了,我看18年年末的时候有人在博客中提到了这个插件,于是我想了下,同时期的Android Studio肯定可以用这个插件. 在Android Studio官网一顿乱串之后发现,
官网提供了历史版本的下载地址.
最后下载了一个2018年10月11日的Android 3.2.1,装上插件试了下->可行->完美.
把apktool反编译好的文件夹导入Android Studio,把所有smali开头的文件夹都标记一下Sources Root(标记方法: 文件夹右键,Mark Directory as -> Sources Root).然后找到你需要调试的类,打好断点.
打开需要调试的App,然后打开Android Device Monitor(在SDK\tools
里面).打开Monitor的时候需要关闭Android Studio.
查看该App对应的端口是多少,记录下来.重新打开Android Studio,编辑Edit Configurations
,点击Add New Configuration
,添加之后再修改一下端口号就行,这里的端口号填上面Monitor看到的那个端口号.
Configuration添加好之后,点击Debug按钮即可进行调试.
熟悉的界面,熟悉的调试方式,开始愉快的调试吧,起飞~
网友评论