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에서 보는 것은 두 가지입니다.
- 권한(permission):
<uses-permission>목록. INTERNET, READ_SMS, ACCESS_FINE_LOCATION 같은 민감 권한이 앱의 기능 설명과 맞는지 — 손전등 앱이 연락처 권한을 달라면 수상합니다. - 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 하나 — 로그인 앱치고 평이합니다. 눈여겨볼 것은 LoginActivity의 android: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. 미션과 연습문제
미션 — "위험한 매니페스트" 감정서 쓰기
- 3-1의 스크립트를 변형해, 아래 요소를 일부러 포함한 모사 APK를 만듭니다:
- 권한: INTERNET, READ_SMS, READ_CONTACTS
PostLoginActivity(로그인 후 화면)를android:exported="true"로 선언- assets/ 폴더에
config.txt(내용:api_key = FAKE-KEY-12345) 추가
- 만든 APK를 분석가 시점으로 다시 엽니다 — 목록 훑기(3-2), 매니페스트 추출(3-3), assets 읽기
- 감정서를 씁니다: 발견한 위험 3가지를 "무엇이 / 왜 위험한가 / 어떻게 발견했는가" 형식으로
- 마지막 문단: 같은 요소들을 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 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.