替换未加固 APK 中的特定字符串

本文纯粹是为了弥补缺憾,而做的技术可行性验证,但如果对你有所启发,老苏会觉得非常欣慰,不感兴趣的朋友可以忽略


前言

2023 的最后一天,老苏发了一篇《将群晖IPTV后台管理套件docker化》,原本是想着通过 docker ,让其他平台的用户也能使用,结果只存活了几个小时就挂了

当时只完成了后台部分,配套的 Android 客户端还需要用户自己用 MT 管理器来修改 apk 中的后台地址。修改方法网上能找到教程,不过对于大部分用户来说,还是有门槛的。

而原本的套件安装完成时,会自动生成对应后台地址的安卓客户端文件。原理老苏大概是明白的,只是因为文章被下架,所以也就没了心情。

最近有点闲,重新研究了一下 Apktool 的编译和反编译,为了不搞乱环境,老苏还是习惯性的封装成了镜像。

原理

其实原理不复杂,第一步将未加固的 apk 文件进行反编译,第二步进行字符串替换,第三步重新编译,第四步重新签名。

思路理顺了,能少走点弯路。一开始老苏忘记了给编译出来的 apk 重新签名,结果导致生成的 apk 不能安装。加入了 Apksigner 之后,Dockerfile 几乎又重新重写了一遍,白白浪费了好几个小时。

构建镜像

如果你不想自己构建,可以跳过,直接阅读下一章节

先要准备三个文件,分别是 Dockerfileentrypoint.shgenerate-keystore.sh,这三个文件放在同一个目录中,用于镜像的构建

文件都放在 Github 上,地址:https://github.com/wbsu2003/Dockerfile/tree/main/apktool

generate-keystore.sh

generate-keystore.sh 是用于生成签名证书的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 生成私钥
openssl genrsa -out my-release-key.pem 2048

# 生成自签名证书
openssl req -new -x509 -key my-release-key.pem -out my-release-cert.pem -days 365 -subj "/CN=MyApp/C=US"

# 将私钥和证书合并为 PKCS#12 文件
openssl pkcs12 -export -out "$KEYSTORE_PATH" -inkey my-release-key.pem -in my-release-cert.pem -name "my-key-alias" -passout pass:"$KEYSTORE_PASSWORD"

# 检查生成的 keystore 文件
if [ -f $KEYSTORE_PATH ]; then
echo "Keystore successfully created at $KEYSTORE_PATH"
else
echo "Failed to create keystore."
fi

# 清理临时文件
rm my-release-key.pem my-release-cert.pem

entrypoint.sh

entrypoint.sh 则完成了整个原理中描述的的 4 步流程

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#!/bin/bash

# 生成证书
if [ "$GENERATE_KEYSTORE" = "true" ]; then
echo "生成 keystore..."
if [ -z "$KEYSTORE_PASSWORD" ]; then
echo "请设置 KEYSTORE_PASSWORD 环境变量。"
exit 1
fi
/usr/local/bin/generate-keystore.sh
fi

# 检查 keystore 文件是否存在
if [ ! -f "$KEYSTORE_PATH" ]; then
echo "Error: Keystore file $KEYSTORE_PATH does not exist."
exit 1
else
echo "Keystore file found: $KEYSTORE_PATH"
fi

# 检查 apksigner 是否可用
if ! command -v apksigner &> /dev/null; then
echo "apksigner 未找到,请检查 Android SDK 是否正确安装。"
exit 1
fi

# 检查环境变量是否设置
if [[ -z "$APP_URL" || -z "$NEW_APP_URL" || -z "$ORIGINAL_APK_NAME" ]]; then
echo "请设置环境变量 APP_URL、NEW_APP_URL 和 ORIGINAL_APK_NAME"
exit 1
fi

# 定义输入和输出路径
INPUT_DIR="/app/input"
OUTPUT_DIR="/app/output"
ORIGINAL_APK="$INPUT_DIR/$ORIGINAL_APK_NAME"

# 创建 decompiled 目录
DECOMPILED_DIR="/app/decompiled"
if [ -d "$DECOMPILED_DIR" ]; then
rm -rf "$DECOMPILED_DIR"
fi

# 提取原 APK 的签名信息
SIGNATURE_INFO=$(apksigner verify --print-signature "$ORIGINAL_APK")
echo "原 APK 签名信息:"
echo "$SIGNATURE_INFO"

# 反编译 APK
apktool d "$ORIGINAL_APK" -o "$DECOMPILED_DIR"

# 替换字符串
find "$DECOMPILED_DIR" -type f -exec sed -i "s|$APP_URL|$NEW_APP_URL|g" {} +

# 重新编译 APK
apktool b "$DECOMPILED_DIR" -o "$OUTPUT_DIR/${ORIGINAL_APK_NAME%.apk}_new.apk"

# 使用 PKCS#12 keystore 对新 APK 进行签名
if apksigner sign --ks "$KEYSTORE_PATH" --ks-type PKCS12 --ks-pass pass:"$KEYSTORE_PASSWORD" --key-pass pass:"$KEYSTORE_PASSWORD" --out "$OUTPUT_DIR/${ORIGINAL_APK_NAME%.apk}_new_signed.apk" "$OUTPUT_DIR/${ORIGINAL_APK_NAME%.apk}_new.apk"; then
echo "新 APK 已生成并输出到 $OUTPUT_DIR/${ORIGINAL_APK_NAME%.apk}_new_signed.apk"
else
echo "签名失败,未生成新 APK。"
exit 1
fi

Dockerfile

Dockerfile 完成整个运行环境的搭建

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
44
45
46
47
48
49
50
51
52
53
54
# 使用基础镜像
FROM openjdk:8-jdk-slim

# 安装必要的工具
RUN apt-get update && apt-get install -y \
wget \
unzip \
openssl \
&& apt-get clean

# 下载并安装 Android SDK
RUN wget -q https://dl.google.com/android/repository/sdk-tools-linux-3859397.zip -O sdk-tools.zip \
&& unzip sdk-tools.zip -d /android-sdk \
&& rm sdk-tools.zip

# 设置环境变量
ENV ANDROID_SDK_ROOT=/android-sdk
ENV PATH=$PATH:$ANDROID_SDK_ROOT/tools/bin:$ANDROID_SDK_ROOT/build-tools/29.0.3
# 设置 keystore 别名和密码
ENV GENERATE_KEYSTORE="true"
ENV KEY_ALIAS="my-key-alias"
ENV KEYSTORE_PASSWORD="123456"
ENV KEYSTORE_PATH="/app/my-release-key.keystore"

# 安装 Android SDK Build Tools 和 Platform Tools
RUN yes | /android-sdk/tools/bin/sdkmanager --licenses \
&& /android-sdk/tools/bin/sdkmanager "build-tools;29.0.3" "platform-tools" \
&& apt-get remove --purge -y wget unzip \
&& apt-get autoremove -y \
&& rm -rf /var/lib/apt/lists/*

# 将 apktool.jar 复制到容器中
# COPY apktool_2.10.0.jar /usr/local/bin/apktool.jar

# 在线下载 apktool
#RUN wget -q https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar -O /usr/local/bin/apktool.jar \
RUN apt-get update && apt-get install -y curl \
&& curl -L -o /usr/local/bin/apktool.jar https://github.com/iBotPeaches/Apktool/releases/download/v2.10.0/apktool_2.10.0.jar \
&& echo -e '#!/bin/sh\nexec java -jar /usr/local/bin/apktool.jar "$@"' > /usr/local/bin/apktool \
&& chmod +x /usr/local/bin/apktool

# 设置工作目录
WORKDIR /app

# 复制证书生成脚本
COPY generate-keystore.sh /usr/local/bin/generate-keystore.sh
RUN chmod +x /usr/local/bin/generate-keystore.sh

# 复制 entrypoint 脚本
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

# 设置默认命令
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

构建镜像和容器运行的基本命令如下👇

1
2
3
4
5
6
7
8
9
10
# 新建文件夹 apktool
mkdir -p apktool

# 进入 apktool 目录
cd apktool

# 将 Dockerfile、entrypoint.sh 和 generate-keystore.sh 放入目录

# 构建镜像
docker build -t wbsu2003/apktool:v1 .

使用

这个镜像是命令行运行的,没有 Web 界面

还是以在群晖上运行为例

docker 文件夹中,创建一个新文件夹 apktool,并在其中建两个子文件夹 inputoutput

文件夹 装载路径 说明
docker/apktool/input /app/input 存放待处理的 apk 文件
docker/apktool/output /app/output 存放处理之后的 apk 文件
1
2
3
4
5
# 新建文件夹 apktool 和 子目录
mkdir -p /volume1/docker/apktool/{input,output}

# 进入 apktool 目录
cd /volume1/docker/apktool

将需要处理的 apk 文件放入到 input 目录中

示例文件老苏放在了:https://github.com/wbsu2003/Dockerfile/raw/refs/heads/main/apktool/QHTV.apk,用手机安装会提示风险,可能是因为签名是 V1 的缘故;

现在可以在当前目录中执行下面的命令了

1
2
3
4
5
6
7
8
9
10
11
# 运行容器(全部参数)
docker run --rm \
-v $(pwd)/input:/app/input \
-v $(pwd)/output:/app/output \
-e APP_URL="http://192.168.0.199/iptv" \
-e NEW_APP_URL="http://192.168.0.197:3332" \
-e ORIGINAL_APK_NAME="QHTV.apk" \
-e GENERATE_KEYSTORE="true" \
-e KEYSTORE_PASSWORD="123456" \
-e KEYSTORE_PATH="/app/my-release-key.keystore" \
wbsu2003/apktool

第一次运行,会需要下载镜像

可变
APP_URL 指程序中原来的后台地址
NEW_APP_URL 指用于替换的新的后台地址
ORIGINAL_APK_NAME 指需要替换的 apk 的文件名,该文件应该被放在 input 目录中
GENERATE_KEYSTORE 是否要生成证书,默认为 true
KEYSTORE_PASSWORD 用于保护 keystore 的密码,脚本会使用这个密码生成和访问 keystore
KEYSTORE_PATH 证书的路径,默认为 /app/my-release-key.keystore,不建议改

3 个为必填项,后 3 个为可选项

当然也可以省掉 KEYSTORE 相关的三个变量,因为老苏在 Dockerfile 中赋了默认值

1
2
3
4
5
6
7
8
# 运行容器(必要参数)
docker run --rm \
-v $(pwd)/input:/app/input \
-v $(pwd)/output:/app/output \
-e APP_URL="http://192.168.0.199/iptv" \
-e NEW_APP_URL="http://192.168.0.197:3332" \
-e ORIGINAL_APK_NAME="QHTV.apk" \
wbsu2003/apktool

GENERATE_KEYSTORE 如果赋值为 false,是不会往下执行的

1
2
3
4
5
6
7
8
9
# 运行容器(错误示例)
docker run --rm \
-v $(pwd)/input:/app/input \
-v $(pwd)/output:/app/output \
-e APP_URL="http://192.168.0.199/iptv" \
-e NEW_APP_URL="http://192.168.0.197:3332" \
-e ORIGINAL_APK_NAME="QHTV.apk" \
-e GENERATE_KEYSTORE="false" \
wbsu2003/apktool

如果没有出错的话

可以在 output 目录看到两个文件

其中 QHTV_new_signed.apk 就是我们用来安装的文件,它的签名状态已经改为了 V1+V2+V3

其他

目前老苏发现的问题:

  1. 如果你先安装了 QHTV.apk 文件,再安装 QHTV_new_signed.apk 会提示安装不了,必须先卸载 QHTV.apk 才行,这是因为签名文件变了。老苏采用的是动态生成签名,所以每次执行,签名都是不一样的;

  2. 生成的 QHTV_new_signed.apkHarmonyOS 2.0.0 和电视盒子上运行正常,但是在另一台 Android 14 上,则卡在首界面,后台授权中查不到上线信息,因此也就无法授权,这个已经超出了本文的范围;

另外请不要问关于 《将群晖IPTV后台管理套件docker化》的问题,当时就是为了不想做技术支持,专门设置成了付费文章,谢谢~

参考文档

Apktool | Apktool
地址:https://apktool.org/

iBotPeaches/Apktool: A tool for reverse engineering Android apk files
地址:https://github.com/iBotPeaches/Apktool

群晖NAS安装IPTV管理系统套件 图文教程 – WANG
地址:https://www.oureiq.top:8812/2023/01/29/群晖nas安装iptv管理系统套件-图文教程/