Android 反编译、修改、重打包与签名

这篇博客主要介绍如何使用 Apktool、dex2jar、jd-gui等工具反编译 Android APP、修改源码、重新打包并签名。

1. 工具安装

1.1. Apktool

Apktool 主要用于反编译和重编译 APK 文件。

Windows 和 Linux 安装方法参照:官方指南

macOS 安装方法如下:

  • 右键保存启动脚本,命名为 apktool
  • 下载最新版本的 Apktool,命名为 apktool.jar
  • apktoolapktool.jar 移动到 /usr/local/bin 目录下
  • 使用 chmod +x apktoolchmod +x apktool.jar 添加运行权限
  • 在命令行直接运行命令 apktool 即可,安装完成

1.2. dex2jar

dex2jar 主要用于将 dex 文件转为 jar 文件

GitHub 下载最新版本,并解压。运行解压后目录下的 d2j-dex2jar.shd2j-dex2jar.bat 即可。

1.3. jd-gui

jd-gui 主要用来可视化 jar 文件。

GitHub 下载最新版本,并解压。运行解压后目录下的 JD-GUI 即可。

macOS 下注意

如果 jd-gui 无法打开,编辑 JD-GUI.app/Contents/MacOS/universalJavaApplicationStub.sh

1
2
3
4
5
6
7
8
9
10
11
12
exec "$JAVACMD" \
-cp "${JVMClassPath}" \
-Xdock:icon="${ResourcesFolder}/${CFBundleIconFile}" \
-Xdock:name="${CFBundleName}" \
## ----- 添加这两行 -----
--add-opens java.base/jdk.internal.loader=ALL-UNNAMED \
--add-opens jdk.zipfs/jdk.nio.zipfs=ALL-UNNAMED \
## --------------------
${JVMOptions:+$JVMOptions }\
${JVMDefaultOptions:+$JVMDefaultOptions }\
${JVMMainClass}\
${JVMArguments:+ $JVMArguments}

详见 issue

2. 查看源码

以小米投屏神器 APP 为例。

运行 d2j-dex2jar.sh 将 APK 中的 dex 文件转为 jar 文件:

1
2
$ ./d2j-dex2jar.sh mi.apk
dex2jar mi.apk -> ./mi-dex2jar.jar

打开 jd-gui,将 mi-dex2jar.jar 拖进去,即可看到反编译后的源码。

image-20190211172815715

3. 修改源码

这里以增加 Log 为修改源码的例子。

jd-gui 只能查看源码,但是无法修改。要修改源码,只能修改 smali 文件,它类似于汇编,但是要简单很多。

首先得使用 apktool 进行反编译:

1
apktool d mi.apk -o mi

这里将 mi.apk 进行反编译并将结果放到 mi 目录中。

首先找到需要添加 Log 的位置,建议在 jd-gui 中寻找,然后在 mi 目录中定位。

例如以下 classsmali 的对应关系

  • mi-dex2jar.jar!/com/xiaomi/mitv/phone/tvassistant/b/a.class
  • mi/smali/com/xiaomi/mitv/phone/tvassistant/b/a.smali

打开 a.classa.smali,首先看 java

1
2
3
4
5
6
7
8
9
public void a(int paramInt)
{
String str1 = String.valueOf(System.currentTimeMillis());
String str2 = a(String.valueOf(paramInt), this.b, str1);
new c(this.d, String.format("http://%s:6095/general?action=setVolum&volum=%d&ts=%s&sign=%s", new Object[] { this.a, Integer.valueOf(paramInt), str1, str2 }), new c.a()
{
public void a(int paramAnonymousInt, String paramAnonymousString) {}
}).d();
}

对应的 smali 文件内容,为了简单,我只提取了部分内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.method public a(I)V
# a 是方法,p0 = this
# a 接收一个参数, p1 = paramInt'

# 定义了 8 个寄存器
.locals 8

.prologue
.line 55
# 获取系统当前时间
invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
# 结果赋值给 v0
move-result-wide v0
# 调用 String.valueOf 方法,将 v0 转为字符串
invoke-static {v0, v1}, Ljava/lang/String;->valueOf(J)Ljava/lang/String;
# 结果赋值给 v0
move-result-object v0
# 到这一步
# String str1 = String.valueOf(System.currentTimeMillis());
# 执行完了

.line 57
# 调用 String.valueOf 方法 p1 转为字符串
invoke-static {p1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
# 结果赋值给 v1
move-result-object v1
# 获取 this.b,也就是从 p1 中获取 b,并赋值给 v2
iget-object v2, p0, Lcom/xiaomi/mitv/phone/tvassistant/b/a;->b:Ljava/lang/String;
# 将 v1,v2,v0 作为参数调用方法 a,并传递 p0 作为 this
invoke-direct {p0, v1, v2, v0}, Lcom/xiaomi/mitv/phone/tvassistant/b/a;->a(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
# 将结果赋值给 v1
move-result-object v1
# 到这一步
# String str2 = a(String.valueOf(paramInt), this.b, str1);
# 执行完了

...

假设我们想要得知其中 this.b 的值,可以这样添加 Log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
.method public a(I)V
# 额外定义 4 个寄存器用于存储 TAG
.locals 12

.prologue
.line 55

# 设置初始化TAG
const-string v8, "AndiedieHack.currentTimeMillis"

const-string v9, "AndiedieHack.param"

const-string v10, "AndiedieHack.b"

const-string v11, "AndiedieHack.result"

invoke-static {}, Ljava/lang/System;->currentTimeMillis()J

move-result-wide v0

invoke-static {v0, v1}, Ljava/lang/String;->valueOf(J)Ljava/lang/String;

move-result-object v0
# 第一个 Log,使用 v8 作为 TAG,输出 v0 的值,即系统当前时间
invoke-static {v8, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

.line 57
invoke-static {p1}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;

move-result-object v1
# 第二个 Log,使用 v9 作为 TAG,输出 v1 的值
# v1 是 p1 的字符串形式,p1 是函数参数
invoke-static {v9, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

iget-object v2, p0, Lcom/xiaomi/mitv/phone/tvassistant/b/a;->b:Ljava/lang/String;
# 第三个 Log,使用 v10 作为 TAG,输出 v2 的值,即 this.b
invoke-static {v10, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

invoke-direct {p0, v1, v2, v0}, Lcom/xiaomi/mitv/phone/tvassistant/b/a;->a(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

move-result-object v1
# 第四个 Log,使用 v11 作为 TAG,输出 v1 的值,即 str2
invoke-static {v11, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

到这一步,源码修改完成。接下来进行重新打包。

3. 重新打包与签名

将修改后的内容重新打包为 APK:

1
apktool b mi -o unsigned.apk

打包之后的 unsigned.apk 是没有签名的,无法安装。

签名需要使用一个 keystore,可以直接使用 Android Studio 为我们生成的 debug 用 keystore。

位置在 用户目录/.android/debug.keystorealiasandroiddebugkey,密码是 android

签名命令:

1
jarsigner -keystore debug.keystore -signedjar signed.apk  unsigned.apk  androiddebugkey

输入密码即可。

4. 测试

在手机上安装 signed.apk,注意如果之前已经安装了投屏神器,需要先卸载,因为两者的签名不一致。

运行 APP,调整音量,可以看到以下 Log:

image-20190211175802805