Smali - Decompiling/[Modifying]/Compiling

Tip

学习并实践 AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
学习并实践 GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
学习并实践 Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) 浏览用于评估路线的 完整 HackTricks Training 目录ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)

支持 HackTricks

有时修改应用程序代码以获取隐藏信息(例如高度混淆的密码或 flags)是有意义的。此时,反编译 apk、修改代码并重新编译可能会很有用。

Opcodes reference: http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

快速方法

使用 Visual Studio CodeAPKLab 扩展,你可以 automatically decompile、修改、recompile、签名并安装该应用,而无需执行任何命令。

另一个极大简化此任务的脚本是 https://github.com/ax/apk.sh

Split APKs / App Bundles

现代目标通常以 split APKsbase.apk + split_config.*.apk)的形式发布,而不是单个单体 APK。如果你只修改 base.apk,资源或本机库可能会不同步,导致安装失败。

Quick triage from a device:

adb shell pm path com.example.app
adb pull /data/app/.../base.apk
adb pull /data/app/.../split_config.arm64_v8a.apk
adb pull /data/app/.../split_config.en.apk

如果目标是拆分包,要么重建整个套件,要么使用可以 先合并 APKs 的工具。 apk.sh 在这里很有用,因为它可以将拆分的 APK 合并成单个可打补丁的 APK 并修复公共资源标识符。
对于面向 Frida/Objection 的重新打包工作流,也可查看 Android Anti-Instrumentation & SSL Pinning Bypass.

反编译 APK

使用 APKTool 你可以访问 smali code and resources

apktool d APP.apk

如果 apktool 出现任何错误,尝试安装 最新版本

一些有趣且你应该查看的文件包括

  • res/values/strings.xml(以及 res/values/* 中的所有 xml)
  • AndroidManifest.xml
  • 任何扩展名为 .sqlite.db 的文件

如果 apktool解码应用时有问题,请查看 https://ibotpeaches.github.io/Apktool/documentation/#framework-files 或尝试使用参数 -r(不要解码资源)。这样,如果问题出在资源而不是源代码,你就不会遇到该问题(不过资源也不会被反编译)。

Change smali code

你可以更改****指令、更改某些变量的添加新的指令。我使用 VS Code 修改 Smali 代码,安装 smalise extension 后编辑器会告诉你是否有任何指令不正确
一些示例可以在这里找到:

或者你可以查看下面的一些 Smali 修改说明

Recompile the APK

修改代码后你可以重新编译代码使用:

apktool b . #In the folder generated when you decompiled the application

它会在 dist 文件夹 编译 新的 APK。

如果 apktool 出现 错误,试试 installing the latest version

为新的 APK 签名

然后,你需要 生成一个密钥(系统会要求你输入一个密码和一些可以随机填写的信息):

keytool -genkey -v -keystore key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias <your-alias>

最后,签名新的 APK:

jarsigner -keystore key.jks path/to/dist/* <your-alias>

jarsigner 仍然适用于一些快速测试,但对于现代 Android 构建,apksigner 更为推荐,因为它能够处理较新的 APK 签名方案。

优化新应用

zipalign 是一个归档对齐工具,为 Android 应用 (APK) 文件提供重要的优化。 More information here.

zipalign [-f] [-v] <alignment> infile.apk outfile.apk
zipalign -v 4 infile.apk

如果 APK 包含捆绑的本机库(lib/*.so),Android 现在建议使用 -P 16,以便 .so 文件对 16 KiB 和 4 KiB 页面大小设备都进行对齐:

zipalign -P 16 -f -v 4 infile.apk outfile.apk

为新的 APK 签名(又一次?)

如果你更喜欢使用apksigner而不是 jarsigner,你应该为 apk 签名,在应用zipaling 的优化之后。BUT NOTICE THAT YOU ONLY HAVE TO 只对应用签名一次 WITH jarsigner (before zipalign) OR WITH aspsigner (after zipaling).

apksigner sign --ks key.jks ./dist/mycompiled.apk

更实用的现代流程是:

apktool b . -o dist/app-unsigned.apk
zipalign -P 16 -f -v 4 dist/app-unsigned.apk dist/app-aligned.apk
apksigner sign --ks key.jks --out dist/app-signed.apk dist/app-aligned.apk
apksigner verify --verbose --print-certs dist/app-signed.apk

重要说明:

  • 如果你在用 apksigner 签名之后修改了 APK,签名会被作废,你必须重新签名。
  • apksigner verify --print-certs 可用于确认重建后的 APK 是否可安装,并检查目标在运行时会暴露的证书。

修改 Smali

对于下面的 Hello World Java 代码:

public static void printHelloWorld() {
System.out.println("Hello World")
}

Smali 代码如下:

.method public static printHelloWorld()V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World"
invoke-virtual {v0,v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method

Smali 指令集可在 here.

轻微更改

在函数内部修改变量的初始值

有些变量在函数开始处使用操作码 const 定义,你可以修改它们的值,或者定义新的变量:

#Number
const v9, 0xf4240
const/4 v8, 0x1
#Strings
const-string v5, "wins"

基本操作

#Math
add-int/lit8 v0, v2, 0x1 #v2 + 0x1 and save it in v0
mul-int v0,v2,0x2 #v2*0x2 and save in v0

#Move the value of one object into another
move v1,v2

#Condtions
if-ge #Greater or equals
if-le #Less or equals
if-eq #Equals

#Get/Save attributes of an object
iget v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Save this.o inside v0
iput v0, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Save v0 inside this.o

#goto
:goto_6 #Declare this where you want to start a loop
if-ne v0, v9, :goto_6 #If not equals, go to: :goto_6
goto :goto_6 #Always go to: :goto_6

更大变更

Smali 中通常会导致重建失败的陷阱

  • 当你只需要在现有方法体内使用临时寄存器时,优先增加 .locals。参数寄存器 (p0, p1…) 映射到方法的 最高 寄存器,因此盲目改用 .registers 经常会破坏参数布局。
  • move-result, move-result-wide, 和 move-result-object 必须紧跟在 对应的 invoke-* 之后。在它们之间插入 logging 或任何其他 opcode 会使方法无效。
  • longdouble 值是 wide 值,会占用一对寄存器。如果你在后面重用这些寄存器,记住 v10 也占用 v11
  • 如果你需要传递大量寄存器,或编号很高的寄存器,使用带 /range 的变体,例如 invoke-virtual/range

Logging

#Log win: <number>
iget v5, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I #Get this.o inside v5
invoke-static {v5}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; #Transform number to String
move-result-object v1 #Move to v1
const-string v5, "wins" #Save "win" inside v5
invoke-static {v5, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I #Logging "Wins: <num>"

建议:

  • 如果你打算在函数内使用已声明的变量(declared v0,v1,v2…),请将这些行放在 .local 和变量声明 (const v0, 0x1) 之间
  • 如果你想把日志代码插入函数代码的中间:
  • 将已声明变量的数量增加 2:例如从 .locals 10.locals 12
  • 新变量应为已声明变量之后的编号(在此例中应为 v10v11,记住编号从 v0 开始)。
  • 修改日志函数的代码,使用 v10v11 来替换 v5v1

Patching common anti-tamper checks

当应用被重新打包时,首先可能出问题的之一是应用内的 签名 / 安装来源 / 完整性 检查。 在 JADX 或 smali 树中好的搜索字符串包括:

  • GET_SIGNATURES
  • GET_SIGNING_CERTIFICATES
  • apkContentsSigners
  • MessageDigest
  • SHA-256
  • Base64
  • getInstallerPackageName
  • com.android.vending

现代应用通常会调用 PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES),使用 MessageDigest 对签名字节进行哈希,并将结果与硬编码常量比较。 在实践中,通常更容易修补 final boolean / branch,而不是重写所有的签名处理代码。

Example patterns:

# Force a boolean result to "valid"
const/4 v0, 0x1

# Or invert the branch that sends execution to the tamper handler
if-eqz v0, :tamper_detected   # original
if-nez v0, :tamper_detected   # patched

如果验证代码很嘈杂,寻找在错误对话框 / finish() / System.exit() / telemetry 调用之前的最后一次比较,并在那里打补丁,而不是修改整个例程。

Toasting

记得在函数开头将 .locals 的数量增加 3。

这段代码准备插入函数的中间(必要时更改 变量的数量)。它会取得 this.o 的值,将其转换String,然后用该值显示一个toast

const/4 v10, 0x1
const/4 v11, 0x1
const/4 v12, 0x1
iget v10, p0, Lcom/google/ctf/shallweplayagame/GameActivity;->o:I
invoke-static {v10}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v11
invoke-static {p0, v11, v12}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;
move-result-object v12
invoke-virtual {v12}, Landroid/widget/Toast;->show()V

在启动时加载本地库 (System.loadLibrary)

有时你需要预加载本地库,以便在其他 JNI 库之前完成初始化(例如,为了启用进程本地的 telemetry/logging)。你可以在静态初始化器或在 Application.onCreate() 的早期注入对 System.loadLibrary() 的调用。下面是用于静态类初始化器 () 的示例 smali:

.class public Lcom/example/App;
.super Landroid/app/Application;

.method static constructor <clinit>()V
.registers 1
const-string v0, "sotap"         # library name without lib...so prefix
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V
return-void
.end method

或者,将相同的两条指令放在 Application.onCreate() 的开头,以确保库尽可能早地加载:

.method public onCreate()V
.locals 1

const-string v0, "sotap"
invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

invoke-super {p0}, Landroid/app/Application;->onCreate()V
return-void
.end method

注意:

  • 确保库的正确 ABI 变体存在于 lib//(例如 arm64-v8a/armeabi-v7a)以避免 UnsatisfiedLinkError。
  • 尽早加载(class static initializer)可保证 native logger 能观察到随后的 JNI 活动。

Smali 静态分析 / 基于规则的检测

使用 apktool 反编译后,你可以逐行扫描 Smali,使用 regex 规则快速发现反分析逻辑(root/emulator checks)和可能的硬编码秘密。这是一种快速筛查技术:将命中视为线索,必须在周围的 Smali 或重建的 Java/Kotlin 中验证。

关键点:

  • Library filtering:在常见第三方命名空间下抑制或标记发现,以便你专注于应用自有的代码路径。
  • Context hints:要求可疑字符串出现在消费它们的 API 附近(在同一方法内或在 N 行之内)。
  • Confidence:使用简单的级别(高/中)对线索排序并减少误报。

默认应抑制的示例库前缀:

Landroidx/
Lkotlin/
Lkotlinx/
Lcom/google/
Lcom/squareup/
Lokhttp3/
Lokio/
Lretrofit2/

示例检测规则 (regex + 上下文启发式):

{
"category": "root_check",
"regex_patterns": [
"(?i)invoke-static .*Runtime;->getRuntime\\(\\).*->exec\\(.*\\"(su|magisk|busybox)\\"",
"(?i)const-string [vp0-9, ]+\\"(/system/xbin/su|/system/bin/su|/sbin/su)\\""
],
"context_hint": "Only report when the same method also calls File;->exists/canExecute or Runtime;->exec."
}

在实践中效果良好的额外启发式方法:

  • 根包/路径检查:要求在附近存在针对像 com.topjohnwu.magisk/data/local/tmp 这样的字符串的 PackageManager;->getPackageInfoFile;->exists 调用。
  • 模拟器检查:将可疑字面量(例如 ro.kernel.qemugenericgoldfish)与附近的 Build.* getters 和字符串比较(->equals->contains->startsWith)配对。
  • 硬编码的敏感信息:仅当附近的 .fieldmove-result 标识符包含像 passwordtokenapi_key 这样的关键字时,才标记 const-string。明确忽略仅与 UI 相关的标记,如 AutofillTypeInputTypeEditorInfo

像 PulseAPK Core 这样的规则驱动扫描器实现了该模型,以便在 Smali 中快速发现反分析逻辑和潜在的敏感信息。

参考资料

Tip

学习并实践 AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
学习并实践 GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
学习并实践 Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) 浏览用于评估路线的 完整 HackTricks Training 目录ARTA/GRTA/AzRTA)以及 Linux Hacking Expert (LHE)

支持 HackTricks