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 для assessment tracks (ARTA/GRTA/AzRTA) і Linux Hacking Expert (LHE).

Підтримайте HackTricks

Іноді корисно змінити код застосунку, щоб отримати приховану інформацію для себе (наприклад добре обфусцовані паролі або флаги). У такому випадку має сенс декомпілювати apk, змінити код і повторно скомпілювати його.

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

Швидкий спосіб

Використовуючи Visual Studio Code та розширення APKLab, ви можете автоматично декомпілювати, змінити, повторно скомпілювати, підписати й встановити застосунок без виконання жодної команди.

Інший скрипт, який значно полегшує це завдання, — https://github.com/ax/apk.sh

Split APKs / App Bundles

Сучасні цілі зазвичай постачаються як split APKs (base.apk + split_config.*.apk) замість одного монолітного APK. Якщо ви патчитимете лише base.apk, ресурси або нативні бібліотеки можуть розійтися за версіями й встановлення може завершитися невдачею.

Швидка перевірка з пристрою:

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

Якщо ціль — split package, або перебудуйте весь набір, або використайте інструменти, які спочатку об’єднують APK. apk.sh тут корисний, оскільки може поєднати split APKs в один patchable APK і виправити public resource identifiers.\ Для Frida/Objection-орієнтованих repacking workflows також перегляньте Android Anti-Instrumentation & SSL Pinning Bypass.

Декомпіляція APK

Використовуючи APKTool, ви можете отримати доступ до smali code and resources:

apktool d APP.apk

Якщо apktool дає вам будь-яку помилку, спробуйте встановити останню версію

Some interesting files you should look are:

  • res/values/strings.xml (і всі xml-файли всередині res/values/*)
  • AndroidManifest.xml
  • Будь-який файл з розширенням .sqlite або .db

If apktool has problems decoding the application take a look to https://ibotpeaches.github.io/Apktool/documentation/#framework-files or try using the argument -r (Do not decode resources). Then, if the problem was in a resource and not in the source code, you won’t have the problem (you won’t also decompile the resources).

Змінити smali код

You can change instructions, change the value of some variables or add new instructions. I change the Smali code using VS Code, you then install the smalise extension and the editor will tell you if any instruction is incorrect.
Some examples can be found here:

Or you can check below some Smali changes explained.

Перекомпілювати APK

Після модифікації коду ви можете перекомпілювати код використовуючи:

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

Він скомпілює новий APK всередині папки dist.

Якщо 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. АЛЕ ЗВЕРНІТЬ УВАГУ, ЩО ВАМ ПОТРІБНО ПІДПИСАТИ ЗАСТОСУНОК ЛИШЕ ОДИН РАЗ З jarsigner (до zipalign) АБО З aspsigner (після 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

Важливі зауваги:

  • Якщо ви змінюєте APK після підписання його за допомогою apksigner, підпис стає недійсним і вам потрібно підписати його знову.
  • apksigner verify --print-certs корисна для підтвердження, що знову зібраний APK можна встановити, і для перевірки сертифіката, який цільовий додаток розкриє під час виконання.

Модифікація Smali

Для наступного Java-коду Hello World:

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, and move-result-object повинні з’являтися безпосередньо після відповідного invoke-*. Вставляння логування або будь-якої іншої opcode між ними робить метод недійсним.
  • Значення типів long і double є wide значеннями і займають пару регістрів. Якщо ви повторно використовуєте ці регістри пізніше, пам’ятайте, що v10 також займає v11.
  • Якщо потрібно передати багато регістрів або регістри з дуже високими номерами, використовуйте варіанти /range, наприклад invoke-virtual/range.

Логування

#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 до кількості оголошених змінних: Ex: from .locals 10 to .locals 12
  • Нові змінні повинні бути наступними номерами після вже оголошених (в цьому прикладі це повинні бути v10 і v11, пам’ятайте, що нумерація починається з v0).
  • Змініть код функції логування і використайте v10 і v11 замість v5 і v1.

Виправлення поширених перевірок захисту від модифікацій

Коли додаток перепаковано, одна з перших речей, що може зламатися — це in-app signature / installer / integrity check. Корисні рядки для пошуку в JADX або в smali-дереві:

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

Сучасні додатки часто викликають PackageManager.getPackageInfo(…, GET_SIGNING_CERTIFICATES), хешують байти підписувача через MessageDigest і порівнюють результат з жорстко вбудованою константою. На практиці зазвичай простіше пропатчити the 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

If the verification code is noisy, look for the last comparison before the error dialog / finish() / System.exit() / telemetry call and patch there instead of touching the entire routine.

Toasting

Пам’ятайте додати 3 до числа .locals на початку функції.

Цей код підготовлений для вставки в середину функції (змініть кількість змінних за потреби). Він візьме значення 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 бібліотеками (наприклад, щоб увімкнути локальну телеметрію/логування процесу). Ви можете інжектувати виклик System.loadLibrary() у static initializer або на початку Application.onCreate(). Приклад 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 Static Analysis / Rule-Based Hunting

Після декомпіляції за допомогою apktool, ви можете сканувати Smali рядок за рядком за допомогою regex-правил, щоб швидко виявити anti-analysis logic (root/emulator checks) та ймовірні hardcoded secrets. Це є швидкою технікою триажу: розглядайте знаходження як підказки, які потрібно перевірити у прилеглому Smali або реконструйованому Java/Kotlin.

Ключові ідеї:

  • Library filtering: підтиснути або позначати знахідки в загальних неймспейсах сторонніх бібліотек, щоб ви могли зосередитися на шляхах коду, що належать додатку.
  • Context hints: вимагати, щоб підозрілі рядки з’являлися поряд із API, які їх використовують (в межах того самого методу, в межах N рядків).
  • Confidence: використовувати прості рівні (high/medium) для ранжування підказок і зменшення false positives.

Example library prefixes to suppress by default:

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."
}

Додаткові евристики, які добре працюють на практиці:

  • Root package/path checks: вимагати поблизу викликів PackageManager;->getPackageInfo або File;->exists для рядків типу com.topjohnwu.magisk або /data/local/tmp.
  • Emulator checks: поєднувати підозрілі літерали (наприклад, ro.kernel.qemu, generic, goldfish) з поруч розташованими геттерами Build.* та порівняннями рядків (->equals, ->contains, ->startsWith).
  • Hardcoded secrets: позначати const-string лише коли поруч .field або move-result ідентифікатор містить ключові слова, такі як password, token, api_key. Явно ігнорувати маркери тільки для UI, такі як AutofillType, InputType, EditorInfo.

Сканери на основі правил, такі як 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 для assessment tracks (ARTA/GRTA/AzRTA) і Linux Hacking Expert (LHE).

Підтримайте HackTricks