Smali - Dekompilacja/[Modyfikacja]/Kompilacja

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks

Czasami warto zmodyfikować kod aplikacji, aby uzyskać dostęp do ukrytych informacji (może dobrze obfuskowane hasła lub flags). Wtedy opłaca się zdekompilować apk, zmienić kod i ponownie go skompilować.

Referencja opcode’ów: http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html

Szybki sposób

Używając Visual Studio Code i rozszerzenia APKLab, możesz automatycznie zdekompilować, zmodyfikować, ponownie skompilować, podpisać i zainstalować aplikację bez wykonywania żadnego polecenia.

Inny skrypt, który bardzo ułatwia to zadanie, to https://github.com/ax/apk.sh

Split APKs / App Bundles

Nowoczesne cele są zwykle dostarczane jako split APKs (base.apk + split_config.*.apk) zamiast jednego monolitycznego APK. Jeśli załatasz tylko base.apk, zasoby lub biblioteki natywne mogą się rozjechać i instalacja może się nie powieść.

Szybkie sprawdzenie z urządzenia:

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

Jeśli docelowy pakiet jest split package, przebuduj cały zestaw albo użyj narzędzia, które najpierw łączy APKs. apk.sh jest tu przydatne, ponieważ potrafi połączyć split APKs w pojedynczy APK możliwy do patchowania i naprawić public resource identifiers.
Dla workflowów repackingu ukierunkowanych na Frida/Objection sprawdź także Android Anti-Instrumentation & SSL Pinning Bypass.

Dekompilacja APK

Używając APKTool możesz uzyskać dostęp do kodu smali i zasobów:

apktool d APP.apk

Jeśli apktool zwróci jakiś błąd, spróbuj installing the latest version

Some interesting files you should look are:

  • res/values/strings.xml (i wszystkie pliki xml w res/values/*)
  • AndroidManifest.xml
  • Każdy plik z rozszerzeniem .sqlite lub .db

If apktool has problems decoding the application zajrzyj na https://ibotpeaches.github.io/Apktool/documentation/#framework-files lub spróbuj użyć argumentu -r (Nie dekoduj zasobów). Wtedy, jeśli problem był w zasobie, a nie w kodzie źródłowym, nie będziesz mieć problemu (nie zdekompilujesz też zasobów).

Zmień kod smali

Możesz zmieniać instrukcje, zmieniać wartość niektórych zmiennych lub dodawać nowe instrukcje. Zmieniam kod Smali używając VS Code, następnie instalujesz smalise extension i edytor powie ci, jeśli jakaś instrukcja jest niepoprawna.
Niektóre przykłady można znaleźć tutaj:

Lub możesz check below some Smali changes explained.

Przekompiluj APK

Po zmodyfikowaniu kodu możesz przekompilować kod używając:

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

Skompiluje nowy APK wewnątrz folderu dist.

Jeśli apktool zgłasza błąd, spróbuj zainstalować najnowszą wersję

Podpisz nowy APK

Następnie musisz wygenerować klucz (zostaniesz poproszony o hasło oraz o pewne informacje, które możesz wypełnić losowo):

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

Na koniec, podpisz nowy APK:

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

jarsigner wciąż działa dla szybkich testów, ale dla nowoczesnych buildów Androida apksigner jest preferowany, ponieważ obsługuje nowsze schematy podpisywania APK.

Optymalizuj nową aplikację

zipalign to narzędzie do wyrównywania archiwów, które zapewnia istotną optymalizację plików aplikacji Android (APK). Więcej informacji tutaj.

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

Jeśli APK zawiera dołączone biblioteki natywne (lib/*.so), Android teraz zaleca użycie -P 16, aby pliki .so były wyrównane zarówno dla urządzeń z rozmiarem strony 16 KiB, jak i 4 KiB:

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

Podpisz nowe APK (znowu?)

Jeśli wolisz użyć apksigner zamiast jarsigner, powinieneś podpisać APK po zastosowaniu optymalizacji za pomocą zipaling. ALE ZWRÓĆ UWAGĘ, ŻE MUSISZ PODPISAĆ APLIKACJĘ TYLKO RAZ przy użyciu jarsigner (przed zipalign) LUB przy użyciu aspsigner (po zipaling).

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

Bardziej praktyczny, nowoczesny przebieg wygląda następująco:

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

Ważne uwagi:

  • Jeśli zmodyfikujesz APK po podpisaniu go za pomocą apksigner, podpis zostanie unieważniony i musisz podpisać go ponownie.
  • apksigner verify --print-certs jest przydatne do potwierdzenia, że przebudowane APK jest możliwe do zainstalowania oraz do sprawdzenia certyfikatu, który aplikacja docelowa ujawni w czasie działania.

Modyfikowanie Smali

Dla następującego kodu Java ‘Hello World’:

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

Kod Smali wyglądałby następująco:

.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

Zestaw instrukcji Smali jest dostępny tutaj.

Niewielkie zmiany

Modyfikacja wartości początkowych zmiennej wewnątrz funkcji

Niektóre zmienne są zdefiniowane na początku funkcji przy użyciu opcode const; możesz zmodyfikować ich wartości lub zdefiniować nowe:

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

Podstawowe operacje

#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

Większe zmiany

Pułapki Smali, które zwykle psują przebudowy

  • Prefer increasing .locals gdy potrzebujesz tylko tymczasowych rejestrów w ciele istniejącej metody. Rejestry parametrów (p0, p1…) są mapowane na najwyższe rejestry metody, więc bezmyślne przejście na .registers często psuje układ argumentów.
  • move-result, move-result-wide, i move-result-object muszą występować bezpośrednio po odpowiadającym invoke-*. Wstawienie logowania lub dowolnego innego opcode pomiędzy nimi sprawia, że metoda jest nieprawidłowa.
  • Wartości long i double są wartościami wide i zajmują parę rejestrów. Jeśli ponownie używasz tych rejestrów później, pamiętaj, że v10 zajmuje też v11.
  • Jeśli musisz przekazać wiele rejestrów lub bardzo wysoko ponumerowanych, użyj wariantów /range, takich jak invoke-virtual/range.

Logowanie

#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>"

Recommendations:

  • Jeśli zamierzasz użyć zadeklarowanych zmiennych wewnątrz funkcji (declared v0,v1,v2…) umieść te linie pomiędzy .local i deklaracjami zmiennych (const v0, 0x1)
  • Jeśli chcesz umieścić kod logowania w środku ciała funkcji:
  • Dodaj 2 do liczby zadeklarowanych zmiennych: Ex: from .locals 10 to .locals 12
  • Nowe zmienne powinny być kolejnymi numerami po już zadeklarowanych zmiennych (w tym przykładzie powinny to być v10 i v11, pamiętaj, że numeracja zaczyna się od v0).
  • Zmień kod funkcji logującej i użyj v10 i v11 zamiast v5 i v1.

Modyfikowanie typowych kontroli antymanipulacyjnych

Gdy aplikacja jest repakowana, jedną z pierwszych rzeczy, które mogą przestać działać, jest wbudowana kontrola podpisu / instalatora / integralności. Dobre ciągi do wyszukania w JADX lub w drzewie smali to:

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

Nowoczesne aplikacje często wywołują PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES), haszują bajty podpisującego przy użyciu MessageDigest i porównują wynik ze stałą zakodowaną w kodzie. W praktyce zwykle łatwiej jest załatać final boolean / branch niż przepisać cały kod obsługi podpisów.

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

Jeśli kod weryfikacji jest noisy (generuje dużo sprawdzeń), poszukaj ostatniego porównania przed oknem błędu / finish() / System.exit() / wywołaniem telemetry i zapatchuj tam zamiast modyfikować całą rutynę.

Toasting

Pamiętaj, aby dodać 3 do liczby .locals na początku funkcji.

Ten kod jest przygotowany do wstawienia w środek funkcji (zmień liczbę zmiennych w razie potrzeby). Pobierze wartość this.o, konwertuje ją do String i następnie wyświetli toast z jej wartością.

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

Ładowanie natywnej biblioteki przy uruchamianiu (System.loadLibrary)

Czasami trzeba wstępnie załadować natywną bibliotekę, aby została zainicjalizowana przed innymi bibliotekami JNI (np. aby włączyć telemetryę/logowanie lokalne dla procesu). Możesz wstrzyknąć wywołanie System.loadLibrary() w statycznym inicjalizatorze lub wcześnie w Application.onCreate(). Przykładowy smali dla statycznego inicjalizatora klasy ():

.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

Alternatywnie, umieść te same dwie instrukcje na początku Application.onCreate(), aby zapewnić, że biblioteka załaduje się jak najwcześniej:

.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

Uwagi:

  • Upewnij się, że poprawny wariant ABI biblioteki znajduje się w lib// (np. arm64-v8a/armeabi-v7a), aby uniknąć UnsatisfiedLinkError.
  • Ładowanie bardzo wcześnie (class static initializer) gwarantuje, że native logger może obserwować następującą aktywność JNI.

Analiza statyczna Smali / wykrywanie oparte na regułach

Po dekompilacji za pomocą apktool, możesz skanować Smali linia po linii przy użyciu reguł regex, aby szybko wyłapać logikę anty-analizy (sprawdzenia root/emulator) i potencjalnie hardcoded secrets. To technika szybkiej triage: traktuj trafienia jako wskazówki, które musisz zweryfikować w otaczającym Smali lub zrekonstruowanym Java/Kotlin.

Kluczowe pomysły:

  • Library filtering: tłumić lub oznaczać wyniki w obrębie powszechnych third-party namespace’ów, żeby skupić się na ścieżkach kodu należących do aplikacji.
  • Context hints: wymagać, aby podejrzane ciągi pojawiały się blisko API, które ich konsumuje (w tej samej metodzie, w obrębie N linii).
  • Confidence: stosować proste poziomy (wysoki/średni) do priorytetyzacji wskazówek i zmniejszenia fałszywych pozytywów.

Przykładowe prefiksy bibliotek do domyślnego tłumienia:

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

Przykładowe reguły wykrywania (regex + heurystyka kontekstowa):

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

Dodatkowe heurystyki, które dobrze działają w praktyce:

  • Root package/path checks: wymagaj pobliskich wywołań PackageManager;->getPackageInfo lub File;->exists dla stringów takich jak com.topjohnwu.magisk lub /data/local/tmp.
  • Emulator checks: paruj podejrzane literały (np. ro.kernel.qemu, generic, goldfish) z pobliskimi getterami Build.* i porównaniami stringów (->equals, ->contains, ->startsWith).
  • Hardcoded secrets: oznacz const-string tylko gdy pobliski .field lub identyfikator move-result zawiera słowa kluczowe takie jak password, token, api_key. Jawnie ignoruj markery tylko UI, takie jak AutofillType, InputType, EditorInfo.

Skannery sterowane regułami, takie jak PulseAPK Core, implementują ten model, aby szybko uwidocznić anti-analysis logic i potencjalne secrets w Smali.

Referencje

Tip

Ucz się i ćwicz AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Ucz się i ćwicz GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Ucz się i ćwicz Az Hacking: HackTricks Training Azure Red Team Expert (AzRTE) Przeglądaj pełny katalog HackTricks Training dla ścieżek assessment (ARTA/GRTA/AzRTA) oraz Linux Hacking Expert (LHE).

Wsparcie HackTricks