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)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.
때때로 애플리케이션 코드를 수정해서 숨겨진 정보(예: 잘 난독화된 비밀번호나 flags)에 접근하는 것이 흥미로울 수 있습니다. 그런 경우 apk를 디컴파일하고 코드를 수정한 뒤 다시 컴파일하는 것이 유용합니다.
Opcodes 참조: http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html
빠른 방법
Visual Studio Code와 APKLab 확장으로, 어떤 명령도 실행하지 않고 애플리케이션을 자동으로 디컴파일하고, 수정하고, 다시 컴파일하여 서명 후 설치할 수 있습니다.
이 작업을 크게 도와주는 또 다른 스크립트는 https://github.com/ax/apk.sh입니다.
Split APKs / App Bundles
최신 대상은 일반적으로 단일 모놀리식 APK 대신 split APKs (base.apk + split_config.*.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는 분할 APK들을 단일 패치 가능한 APK로 결합하고 공개 리소스 식별자(public resource identifiers)를 수정해주기 때문에 유용합니다.
Frida/Objection 중심의 리패킹 워크플로우의 경우, Android Anti-Instrumentation & SSL Pinning Bypass도 확인하세요.
APK 디컴파일
APKTool을 사용하면 smali 코드 및 리소스에 접근할 수 있습니다:
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 (리소스를 디코딩하지 않음)을 사용해 보세요. 그러면 문제가 소스 코드가 아닌 리소스에 있었다면 문제가 발생하지 않습니다(리소스도 디컴파일되지 않습니다).
smali 코드 변경
명령을 변경하거나, 일부 변수의 값을 변경하거나 새 명령을 추가할 수 있습니다. 저는 Smali 코드를 VS Code로 수정하며, 그 후 smalise extension 을 설치하면 에디터가 어떤 instruction이 잘못되었는지 알려줍니다.
다음과 같은 예시들을 확인할 수 있습니다:
APK 재컴파일
코드를 수정한 후 다음을 사용해 코드를 다시 컴파일할 수 있습니다:
apktool b . #In the folder generated when you decompiled the application
새 APK를 dist 폴더 안에서 compile 합니다.
만약 apktool이 error를 발생시키면, installing the latest version을 시도해 보세요
새 APK에 서명하기
그런 다음, generate a key를 해야 합니다 (비밀번호와 임의로 입력할 수 있는 몇 가지 정보를 요청받습니다):
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에 서명 (다시?)
만약 jarsigner 대신 apksigner을 선호한다면, zipaling으로 최적화를 적용한 후에 APK에 서명해야 합니다. 하지만 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
중요 사항:
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에서 확인할 수 있습니다.
간단한 변경
함수 내부 변수의 초기값 수정
일부 변수는 함수의 시작 부분에서 opcode _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가 보통 재빌드를 깨는 주의사항
- 기존 메서드 본문에서 임시 registers만 필요한 경우에는
.locals값을 늘리는 것을 권장합니다. 파라미터 registers (p0,p1…)는 메서드의 최상위 registers에 매핑되므로, 무턱대고.registers로 전환하면 인수 layout이 종종 깨집니다. move-result,move-result-wide,move-result-object는 일치하는invoke-*바로 다음에 나타나야 합니다. 그 사이에 logging이나 다른 opcode를 삽입하면 메서드가 무효화됩니다.long과double값은 wide 값으로 register pair를 소비합니다. 이후에 해당 registers를 재사용할 경우,v10이v11도 함께 차지한다는 점을 기억하세요.- 많은 registers를 전달해야 하거나 번호가 매우 높은 registers를 전달해야 할 경우,
invoke-virtual/range와 같은/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>"
Recommendations:
- If you are going to use declared variables inside the function (declared v0,v1,v2…) put these lines between the .local
and the declarations of the variables (const v0, 0x1) - If you want to put the logging code in the middle of the code of a function:
- Add 2 to the number of declared variables: Ex: from .locals 10 to .locals 12
- The new variables should be the next numbers of the already declared variables (in this example should be v10 and v11, remember that it starts in v0).
- Change the code of the logging function and use v10 and v11 instead of v5 and v1.
Patching common anti-tamper checks
앱을 재패키징(repacked)할 때 가장 먼저 깨질 수 있는 것 중 하나는 앱 내의 signature / installer / integrity 검사입니다. JADX나 smali tree에서 검색하기 좋은 문자열은 다음과 같습니다:
GET_SIGNATURESGET_SIGNING_CERTIFICATESapkContentsSignersMessageDigestSHA-256Base64getInstallerPackageNamecom.android.vending
Modern apps often call PackageManager.getPackageInfo(..., GET_SIGNING_CERTIFICATES), hash the signer bytes with MessageDigest, and compare the result with a hardcoded constant. In practice, it is usually easier to patch the final boolean / branch than to rewrite all the signature-handling code.
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() / 텔레메트리 호출 이전의 마지막 비교를 찾아 전체 루틴을 건드리지 말고 그곳에 패치하라.
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을 활성화하기 위해). static initializer 또는 Application.onCreate() 초기에 System.loadLibrary() 호출을 삽입할 수 있습니다. static 클래스 초기화자 (
.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
참고:
- UnsatisfiedLinkError를 피하려면 라이브러리의 올바른 ABI 변형이 lib/
/ 아래에 존재하는지 확인하세요 (예: arm64-v8a/armeabi-v7a). - 매우 일찍 로드하면 (class static initializer) 네이티브 로거가 이후의 JNI 활동을 관찰할 수 있습니다.
Smali Static Analysis / Rule-Based Hunting
apktool로 디컴파일한 후, regex 규칙으로 Smali를 한 줄씩 스캔하여 anti-analysis 로직 (root/emulator checks)과 가능성 있는 hardcoded secrets를 빠르게 찾아낼 수 있습니다. 이는 fast triage 기법입니다: 발견된 항목은 주변 Smali 또는 재구성된 Java/Kotlin에서 반드시 검증해야 할 리드로 취급하세요.
Key ideas:
- Library filtering: 공통 서드파티 네임스페이스 아래의 발견 항목을 억제하거나 태그하여 앱 소유 코드 경로에 집중할 수 있게 합니다.
- Context hints: 의심스러운 문자열이 그것을 소비하는 APIs 근처(같은 메서드 내, 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: 문자열 예:
com.topjohnwu.magisk또는/data/local/tmp에 대해 근처에PackageManager;->getPackageInfo또는File;->exists호출이 있는지 요구합니다. - Emulator checks: 의심스러운 리터럴(예:
ro.kernel.qemu,generic,goldfish)을 근처의Build.*getter 및 문자열 비교(->equals,->contains,->startsWith)와 짝지으세요. - Hardcoded secrets: 근처의
.field또는move-result식별자에password,token,api_key같은 키워드가 포함된 경우에만const-string을 플래그합니다.AutofillType,InputType,EditorInfo같은 UI 전용 마커는 명시적으로 무시하세요.
PulseAPK Core와 같은 규칙 기반 스캐너는 이 모델을 구현하여 Smali에서 안티-분석 로직과 잠재적 비밀을 빠르게 드러냅니다.
References
- PulseAPK Core
- PulseAPK Smali Detection Rules
- SoTap: 경량 인앱 JNI (.so) 동작 로거 – github.com/RezaArbabBot/SoTap
- Android Developers: apksigner and zipalign
- apk.sh: github.com/ax/apk.sh
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)
평가 트랙 (ARTA/GRTA/AzRTA)과 Linux Hacking Expert (LHE)를 보려면 전체 HackTricks Training 카탈로그를 둘러보세요.
HackTricks 지원하기
- subscription plans를 확인하세요!
- 💬 Discord group, telegram group에 참여하고, X/Twitter에서 @hacktricks_live를 팔로우하거나, LinkedIn page와 YouTube channel을 확인하세요.
- HackTricks 및 HackTricks Cloud github repos에 PR을 제출해 hacking tricks를 공유하세요.


