Step 223. 안드로이드 APK 분석 맛보기 — jadx

Step 223. 안드로이드 APK 분석 맛보기 — jadx

Level 3 — Reversing 심화 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 222(.NET/파이썬 바이너리 리버싱). zip 파일과 XML을 본 적이 있으면 충분합니다.

⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 남의 앱을 디컴파일해 키를 추출하거나 변조해 재배포하는 것은 불법입니다 — 연습은 전용 샘플 앱(InsecureBankv2 같은 취약 앱, CTF 문제 APK)으로만 합니다.

  • 준비물: 파이썬 3 (실측: WSL python3 3.12.3, zipfile 표준 모듈). jadx는 이 환경에 설치되어 있지 않아 화면 예시로 안내합니다.
  • 주의: APK를 다룰 때는 출처를 확인하세요. 실제 앱 마켓 밖의 APK는 악성코드일 수 있고, 실행(에뮬레이터 설치)과 분석(디컴파일)은 위험도가 다릅니다.

리버싱의 마지막 맛보기는 모바일입니다. 안드로이드 앱의 배포 파일 APK는 사실 zip이고, 안의 코드(classes.dex)는 자바 바이트코드 — Step 222에서 배운 "잘 읽히는 세계"의 연장선입니다. 오늘은 APK의 구조를 직접 풀어 보고(실측), 모바일 리버싱의 표준 도구 jadx로 무엇을 찾는지 배웁니다(화면 예시). 목표는 하나: "이 앱, 믿어도 되나?"를 스스로 검증하는 첫걸음입니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • APK의 내부 구조(AndroidManifest.xml, classes.dex, 리소스)를 설명한다
  • APK가 zip임을 직접 풀어 실측한다
  • AndroidManifest.xml에서 권한과 exported 컴포넌트를 읽는다
  • jadx로 디컴파일된 자바 코드에서 하드코딩된 키를 찾는 절차를 안다
  • 자바 코드가 안 읽힐 때의 대안 — smali 뷰의 존재를 안다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 파이썬 3 zipfile (실측: WSL python3 3.12.3) — APK 구조 풀어 보기
오늘의 명령어 zipfile.ZipFile(...).namelist(), file, jadx (화면 예시)
필요한 개념 APK = zip, DEX와 smali, 매니페스트(권한·exported), 디컴파일
오늘의 산출물 APK 구조 실측 기록 + "분석할 때 어디부터 보는가" 체크리스트

2-1. APK = zip — 포장지부터 벗기기

APK(Android Package)는 확장자만 특별할 뿐, 표준 zip 압축 파일입니다. 안에는 정해진 주민들이 살고 있습니다:

파일/폴더 정체
AndroidManifest.xml 앱의 신분증 — 패키지명, 권한, 컴포넌트 선언 (단, 바이너리 XML 형식)
classes.dex 앱의 본체 — 자바/Kotlin 코드가 컴파일된 DEX 바이트코드
resources.arsc 문자열·색상 등 컴파일된 리소스 테이블
res/ 레이아웃, 이미지 등 리소스 원본
META-INF/ 서명 정보 — "누가 만들었고 변조되지 않았는가"

한 가지 정직한 주의: 실제 APK의 AndroidManifest.xml은 텍스트가 아니라 바이너리 XML로 인코딩되어 있어 zip을 풀어도 그대로는 못 읽습니다. 오늘 3단계의 실측은 구조 이해용 모사(텍스트 XML)이고, 실제 매니페스트는 jadx가 디코딩해 보여 줍니다.

2-2. DEX와 smali — 안드로이드의 바이트코드

안드로이드 앱의 코드는 JVM 바이트코드(.class)가 아니라 DEX(Dalvik Executable) 형식으로 컴파일됩니다. ART(안드로이드 런타임)가 해석·실행하는 바이트코드입니다.

DEX를 사람이 읽는 방법은 두 갈래입니다. 위로 올라가면 자바 소스로 디컴파일(jadx), 아래로 내려가면 smali — DEX의 어셈블리에 해당하는 텍스트 표현입니다. Step 222의 "디컴파일이 깨지면 바이트코드를 직독"과 같은 관계입니다: jadx의 자바가 난독화 등으로 깨지면 smali가 최후의 보루입니다.

2-3. jadx — 모바일 리버싱의 첫 도구

jadx는 DEX를 자바 소스로 디컴파일해 보여 주는 오픈소스 도구입니다 (GUI와 CLI 둘 다). APK를 통째로 열면 매니페스트, 리소스, 코드를 한 화면에서 탐색할 수 있습니다.

분석가가 jadx에서 찾는 것은 정해져 있습니다: ① 하드코딩된 비밀(API 키, 비밀번호, 암호화 키), ② 인증 로직(로컬 검증인지 서버 검증인지), ③ 숨겨진 엔드포인트(테스트용 URL, 디버그 기능), ④ 위험한 권한·노출된 컴포넌트. 모바일 CTF와 실무 앱 점검 모두 이 넷에서 시작합니다.

2-4. 매니페스트 읽기 — 권한과 exported

AndroidManifest.xml에서 보는 것은 두 가지입니다.

  1. 권한(permission): <uses-permission> 목록. INTERNET, READ_SMS, ACCESS_FINE_LOCATION 같은 민감 권한이 앱의 기능 설명과 맞는지 — 손전등 앱이 연락처 권한을 달라면 수상합니다.
  2. exported 컴포넌트: android:exported="true"로 선언된 액티비티·서비스는 다른 앱이 직접 깨울 수 있습니다. 로그인을 거치지 않는 내부 화면이 exported면 인증 우회의 실마리가 됩니다.

3. 따라 하기

3-1. 실험실 — 모사 APK 만들기

이 환경에는 실제 APK가 없으므로, 같은 구조의 zip을 파이썬으로 만들어 "APK = zip"을 실측합니다. 진짜 APK와 다른 점은 ① 파일 내용이 더미라는 것 ② 매니페스트가 텍스트 XML이라는 것(2-1 참조)뿐이고, 포장 구조는 동일합니다.

입력 (make_fake_apk.py)

import zipfile

files = {
    "AndroidManifest.xml": (
        '<manifest package="com.example.vault">\n'
        '  <uses-permission android:name="android.permission.INTERNET"/>\n'
        '  <application>\n'
        '    <activity android:name=".LoginActivity" android:exported="true"/>\n'
        '  </application>\n'
        '</manifest>\n'
    ),
    "classes.dex": "dex\n035\x00...(더미 바이트코드)...",
    "resources.arsc": "(더미 리소스 테이블)",
    "res/layout/main.xml": "<LinearLayout><!-- 더미 --></LinearLayout>",
    "META-INF/MANIFEST.MF": "Manifest-Version: 1.0\n",
}

with zipfile.ZipFile("vault_fake.apk", "w", zipfile.ZIP_DEFLATED) as z:
    for name, data in files.items():
        z.writestr(name, data)
print("생성 완료")
cd ~/lab219_223
python3 make_fake_apk.py
ls -l vault_fake.apk
file vault_fake.apk
생성 완료
-rw-r--r-- 1 root root 844 Sep  9 18:42 vault_fake.apk
vault_fake.apk: Android package (APK), with AndroidManifest.xml

(2026-09-09 실측.)

출력 읽는 법: 놀라운 순간입니다 — file 명령이 이 파일을 "Android package (APK)"로 인식했습니다. 내용물의 구성(AndroidManifest.xml + classes.dex + 리소스 구조)만으로 판별한다는 뜻이고, 거꾸로 APK의 정체가 "약속된 구조의 zip"이라는 실측 증거입니다.

3-2. 목록 훑기 — 지도부터 그린다

zip이니까 목록은 공짜로 볼 수 있습니다:

import zipfile
z = zipfile.ZipFile("vault_fake.apk")
for i in z.infolist():
    print(f"{i.file_size:>6}  {i.filename}")
   218  AndroidManifest.xml
    38  classes.dex
    28  resources.arsc
    44  res/layout/main.xml
    22  META-INF/MANIFEST.MF

(2026-09-09 실측.)

읽는 법: 2-1의 표에 있던 주민들이 전부 보입니다. 실전 분석의 첫 동작이 바로 이것입니다 — dex가 여러 개인가(classes2.dex가 있으면 멀티덱스 — 큰 앱), native 라이브러리(lib/ 폴더의 .so)가 있는가, 수상한 에셋이 있는가. 지도를 먼저 그리고 코드로 들어갑니다.

3-3. 매니페스트 추출 — 권한과 exported 읽기

z = zipfile.ZipFile("vault_fake.apk")
print(z.read("AndroidManifest.xml").decode())
<manifest package="com.example.vault">
  <uses-permission android:name="android.permission.INTERNET"/>
  <application>
    <activity android:name=".LoginActivity" android:exported="true"/>
  </application>
</manifest>

(2026-09-09 실측.)

읽는 법: 2-4의 두 렌즈로 읽습니다. 권한은 INTERNET 하나 — 로그인 앱치고 평이합니다. 눈여겨볼 것은 LoginActivityandroid:exported="true" — 다른 앱(또는 adb)이 이 화면을 직접 띄울 수 있다는 선언입니다. 만약 로그인 "후" 화면이 exported였다면? 로그인을 건너뛰고 내부 화면을 직접 깨우는 우회가 성립합니다. 실제 취약 앱 연습(InsecureBankv2)에서 바로 이 패턴을 찾게 됩니다.

참고: 반복하지만 실제 APK의 매니페스트는 바이너리 XML이라 이 방법으로는 깨진 바이트가 나옵니다. 실제 파일에서는 jadx(또는 apktool)가 디코딩한 결과를 읽습니다 — 도구가 하는 일의 의미를 아는 것이 오늘 실측의 목적입니다.

3-4. jadx로 열기 — 화면 예시

이 환경에는 jadx가 없으므로(설치하지 않습니다) 화면 예시로 안내합니다. 실제로 해볼 분은 jadx GitHub 릴리스에서 받으세요 (Java가 필요합니다). 연습용 APK로는 취약점 학습용으로 공개된 InsecureBankv2, DIVA, CTF 문제 APK가 표준입니다.

# 화면 예시 — jadx-gui 사용 흐름
1. jadx-gui 실행 → APK 파일 열기
2. 왼쪽 트리:
   - Resources > AndroidManifest.xml  ← 디코딩된 텍스트로 표시됨
   - Source code > com.example.vault > LoginActivity  ← 자바로 디컴파일
3. LoginActivity.java (디컴파일 결과의 예):

public void onCreate(Bundle bundle) {
    ...
    this.et_password = findViewById(R.id.password);
}

public void login(View v) {
    String u = this.et_username.getText().toString();
    String p = this.et_password.getText().toString();
    if (u.equals("admin") && p.equals("s3cur3P@ss")) {   // ← 하드코딩!
        startActivity(new Intent(this, HomeActivity.class));
    }
}

4. 메뉴 Navigation > Text search 에서 "password", "api_key", "secret" 검색

읽는 법: Step 222의 dnSpy 경험과 같습니다 — 바이트코드라 이름과 구조가 남아 있어 거의 원본 수준으로 읽힙니다. 예시의 login()처럼 자격 증명이 평문 비교로 박혀 있으면 그것이 곧 발견(Finding)입니다. 검색 기능으로 "http://", "BEGIN PRIVATE KEY" 같은 패턴을 훑는 것도 정석입니다.

3-5. smali 맛보기 — 자바가 깨질 때의 대안

난독화된 앱(이름이 a.b.c로 깨진)이나 디컴파일이 어색한 구간에서는 jadx의 smali 뷰로 내려갑니다:

# 화면 예시 — smali (DEX 어셈블리)
.method public login(Landroid/view/View;)V
    .locals 3
    ...
    const-string v1, "admin"
    invoke-virtual {p0, v1}, Ljava/lang/String;.equals(Ljava/lang/Object;)Z
    move-result v2
    if-eqz v2, :cond_0        # 같지 않으면 cond_0으로 점프
    ...
.end method

읽는 법: 장황하지만 구조는 어셈블리와 같습니다 — 상수 적재(const-string), 호출(invoke-*), 분기(if-*). 그리고 보십시오: smali에서도 "admin"은 평문입니다. 하드코딩된 비밀은 어떤 층에서 보든 평문입니다 — 디컴파일이 깨져도 문자열 검색은 항상 먹히는 이유입니다.

3-6. 분석 체크리스트 — 실전 순서

순서 볼 것 도구 찾는 것
1 파일 목록 zip dex 개수, .so, 수상한 에셋
2 매니페스트 jadx (디코딩) 민감 권한, exported 컴포넌트
3 문자열 검색 jadx text search password, api_key, http://, BEGIN PRIVATE KEY
4 인증 로직 jadx 자바 로컬 검증 vs 서버 검증, 우회 가능성
5 깨진 구간 jadx smali 뷰 디컴파일 실패 지점 직독

4. 미션과 연습문제

미션 — "위험한 매니페스트" 감정서 쓰기

  1. 3-1의 스크립트를 변형해, 아래 요소를 일부러 포함한 모사 APK를 만듭니다:
    • 권한: INTERNET, READ_SMS, READ_CONTACTS
    • PostLoginActivity(로그인 후 화면)를 android:exported="true"로 선언
    • assets/ 폴더에 config.txt(내용: api_key = FAKE-KEY-12345) 추가
  2. 만든 APK를 분석가 시점으로 다시 엽니다 — 목록 훑기(3-2), 매니페스트 추출(3-3), assets 읽기
  3. 감정서를 씁니다: 발견한 위험 3가지를 "무엇이 / 왜 위험한가 / 어떻게 발견했는가" 형식으로
  4. 마지막 문단: 같은 요소들을 jadx에서는 어느 화면에서 찾았을지 대응표를 붙입니다

연습문제

문제 1. APK가 "그냥 zip"이라는 사실이 분석에 주는 이점을, 3-2(목록)와 3-3(추출)의 절차에 근거해 설명하세요.

문제 2. android:exported="true"인 컴포넌트가 왜 보안 분석의 관심 대상인가요?

문제 3. 실제 APK의 AndroidManifest.xml을 zip으로 풀어 텍스트 에디터로 열면 깨져 보입니다. 이유와, 그래서 jadx가 대신 해 주는 일을 답하세요.

문제 4. 난독화로 클래스명이 a.b.c로 깨진 앱에서도 유효한 분석 실마리를 두 가지 드세요.


5. 모범 답안과 완료 기준

미션 모범 답안

[제작] files 딕셔너리에 추가:
  권한 2종을 <uses-permission> 줄로 추가
  "assets/config.txt": "api_key = FAKE-KEY-12345"
  PostLoginActivity를 exported="true"로 선언한 매니페스트로 교체

[감정서 — 발견 3종]
1. READ_SMS / READ_CONTACTS 권한 — "금고 앱" 기능 설명과 맞지 않는 과잉 권한.
   발견: 매니페스트의 <uses-permission> 목록.
2. PostLoginActivity가 exported — 로그인을 거치지 않고 외부에서 내부 화면을
   깨울 수 있어 인증 우회 경로가 된다. 발견: exported="true" 선언.
3. assets/config.txt의 하드코딩된 API 키 — apk를 푼 누구나 읽을 수 있다.
   발견: zip 목록에서 수상한 에셋 → z.read("assets/config.txt").

[jadx 대응표] 권한·exported → Resources > AndroidManifest.xml,
             API 키 → Navigation > Text search "api_key",
             코드 → Source code 트리의 클래스 디컴파일 뷰.

검증하는 법: ① 감정서의 각 발견이 "어느 파일/줄에서"를 명시하는가, ② 위험의 이유가 기능 설명과의 대비(과잉 권한) 또는 공격 경로(인증 우회, 키 유출)로 설명되는가, ③ jadx 대응표가 3-6의 체크리스트와 일치하는가. 도구 없이 만든 파일을 도구 있는 척 분석하지 않는 정직함 — 모사임을 명시하는 것도 채점 항목입니다.

연습문제 해답

문제 1 해답. 표준 zip 도구로 설치나 실행 없이 내용을 들여다볼 수 있다는 이점이 있습니다. 3-2처럼 namelist()로 파일 구성(지도)을 공짜로 얻고, 3-3처럼 개별 파일을 read()로 바로 추출할 수 있습니다. 특수한 포맷이 아니라서 첫 정찰의 비용이 0에 가깝고, 실제 분석의 순서(목록 → 매니페스트 → 코드)가 그대로 zip 탐색의 순서가 됩니다.

문제 2 해답. exported 컴포넌트는 앱 바깥(다른 앱, adb)에서 직접 실행할 수 있기 때문입니다. 개발자가 "앱 내부 화면"으로만 쓸 의도였는데 exported로 선언된 화면이 있다면, 로그인 같은 정상 경로를 건너뛰고 그 화면을 바로 깨우는 인증 우회가 성립할 수 있습니다. 그래서 매니페스트 분석에서 exported 선언은 첫 번째로 집어 보는 항목입니다.

문제 3 해답. 실제 APK의 매니페스트는 텍스트 XML이 아니라 안드로이드 전용의 바이너리 XML 형식으로 인코딩되어 있기 때문입니다 — 문자열 테이블과 태그 구조가 압축된 이진 포맷이라 텍스트 에디터로는 깨져 보입니다. jadx(와 apktool)는 이 바이너리 XML을 디코딩해 사람이 읽을 수 있는 텍스트 XML로 복원해 보여 줍니다. 오늘 3-3의 실측이 텍스트로 읽힌 것은 모사 파일이기 때문이라는 점이 "도구가 해 주는 일"의 실체입니다.

문제 4 해답. 첫째, 문자열 — 이름은 깨져도 하드코딩된 비밀번호, URL, API 키는 평문으로 남습니다 (3-5에서 smali에서도 평문임을 확인). 둘째, 리소스와 매니페스트 — 난독화는 보통 코드 이름만 대상이라, 레이아웃·문자열 리소스·권한 선언은 읽힙니다. "이름이 지워지면 문자열과 구조로 읽는다"가 난독화 대응의 첫 감각입니다.

완료 기준 체크리스트

  • [ ] APK의 다섯 구성 요소(매니페스트, dex, arsc, res, META-INF)를 말할 수 있다
  • [ ] 모사 APK를 만들고 file이 APK로 인식하는 것을 실측했다
  • [ ] zip 도구로 목록과 매니페스트를 추출해 읽었다
  • [ ] 권한과 exported 선언을 보는 이유를 설명할 수 있다
  • [ ] 실제 매니페스트가 바이너리 XML이라는 점과 jadx의 역할을 안다
  • [ ] jadx의 화면 구성(트리, 디컴파일 뷰, text search)과 절차를 안다
  • [ ] 미션: 위험 요소 3가지를 포함한 모사 APK를 만들고 감정서를 썼다

6. 흔한 실수와 해결

벽 1. 실제 APK의 매니페스트를 풀었더니 깨진 글자가 나온다

증상: unzip -p app.apk AndroidManifest.xml이 이진 쓰레기를 출력합니다.
원인: 정상입니다 — 실제 매니페스트는 바이너리 XML입니다 (2-1). 오늘 실측의 모사 파일이 텍스트였을 뿐입니다.
해결: jadx 또는 apktool로 디코딩하세요. "zip으로 풀린다"와 "텍스트로 읽힌다"는 다른 문제라는 것이 오늘의 구분선입니다.

벽 2. jadx의 자바 코드가 a.b.c 투성이다

증상: 클래스·메서드 이름이 전부 한 글자입니다.
원인: 난독화(ProGuard/R8 등)입니다. 개발사가 의도적으로 이름을 지운 것입니다.
해결: 이름으로 읽지 말고 문자열과 동작으로 읽으세요 — "password" 문자열을 참조하는 코드가 로그인 로직입니다. jadx의 text search에서 출발해 참조를 거슬러 올라가는 것이 정석입니다. 본격 대응은 Step 224의 주제입니다.

벽 3. 디컴파일된 자바가 이상하다 (빈 메서드, 이상한 분기)

증상: 분명 뭔가 하는 함수인데 자바 결과가 비어 보입니다.
원인: 디컴파일은 복원이지 원본이 아닙니다. 최적화된 DEX는 복원이 어색할 수 있습니다.
해결: 3-5처럼 smali 뷰로 내려가 확인하세요. smali는 DEX를 1:1로 옮긴 것이라 거짓말을 하지 않습니다. 자바가 의심스러우면 smali가 심판입니다.

벽 4. 분석하다 앱을 실행하고 싶어진다

증상: "일단 에뮬레이터에 설치해서 돌려 볼까" 하는 유혹.
원인: 동적 분석의 유혹은 자연스럽지만, 출처 불명 APK의 실행은 위험 수위가 다릅니다.
해결: 정적 분석(jadx)으로 판단이 서기 전에는 실행하지 마세요. 실행이 필요해지면 네트워크를 끊은 전용 에뮬레이터·분석 기기를 씁니다. 실행 중 관찰(Frida 등)은 그 다음 단계의 영역입니다.

벽 5. "연습용 앱"을 구하는 게 고민이다

증상: 어떤 APK로 연습해야 할지 모르겠습니다.
원인: 아무 앱이나 받으면 법적·안전 문제가 있습니다.
해결: 취약점 학습용으로 공개된 앱을 고르세요 — InsecureBankv2, DIVA(Damn Insecure Vulnerable App), 모바일 CTF 문제 APK가 표준 교재입니다. "풀라고 만든 문제"인지 확인하는 습관 자체가 실무 윤리의 일부입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
APK 안드로이드 앱의 배포 파일 — 정체는 약속된 구조의 zip
AndroidManifest.xml 앱의 신분증 — 권한과 컴포넌트 선언 (실제로는 바이너리 XML)
classes.dex 앱 코드 본체 — ART가 실행하는 DEX 바이트코드
jadx DEX → 자바 디컴파일러 — 매니페스트·리소스·코드 통합 뷰
smali DEX의 어셈블리 표현 — 자바가 깨질 때의 최후의 보루
exported 컴포넌트 외부에서 깨울 수 있는 화면/서비스 — 인증 우회의 단서
하드코딩된 비밀 코드에 박힌 키·비밀번호 — 어떤 층에서 보든 평문

오늘의 명령어·도구

명령/도구 하는 일
zipfile.ZipFile(apk).namelist() APK 파일 목록 — 분석의 지도 그리기
z.read("AndroidManifest.xml") 매니페스트 추출 (모사 파일 기준 실측)
file app.apk APK 인식 확인 — 구조로 판별됨을 실측
jadx-gui 매니페스트 디코딩 + DEX 자바 디컴파일 + text search
jadx smali 뷰 디컴파일이 깨진 구간의 1:1 판독
Text search ("password", "api_key") 하드코딩된 비밀 찾기 — 이름이 깨져도 유효

명령어보다 중요한 감각

모바일 리버싱도 결국 같은 문법입니다. 포장을 벗기고(zip), 지도를 그리고(목록), 신분증을 읽고(매니페스트), 코드로 들어간다(jadx). 그리고 어떤 층에서 보든 — 자바든 smali든 — 하드코딩된 비밀은 평문이라는 사실이 오늘의 결론입니다.

Step 178부터 223까지, 리버싱 트랙의 지도가 완성됐습니다: 기계어(crackme) → 방해(안티디버깅) → 포장(패킹) → 이해의 증명(키젠) → 잘 읽히는 세계(.NET/파이썬) → 모바일(APK). 도구는 갈수록 강력해졌지만, 여러분이 기른 것은 도구가 아니라 순서와 감각입니다 — 판별이 먼저, 가벼운 도구부터, 파일이 침묵하면 실행에게 묻는다.


전부 체크되면 Step 223 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.