Step 220. 패킹과 언패킹 — UPX와 OEP 찾기
Level 3 — Reversing 심화 | 난이도 ★★★★☆ | 예상 소요 시간 4시간
전제: Step 219(안티디버깅 기법과 우회), Step 178(Reversing 입문).
strings,objdump, gdb의 기본 사용을 압니다.
⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 패킹과 언패킹은 악성코드 분석의 기본 기술이지만, 남의 소프트웨어를 언패킹해 유통하면 법률 문제가 됩니다.
- 준비물: WSL 우분투 (실측: Ubuntu 24.04). 이 환경에는 UPX가 설치되어 있지 않아 UPX 명령 자체는 출력 예시로 안내하고, 패킹의 핵심 원리(압축 = 문자열 은폐, 스텁 = 실행 복원 장치)는 gzip으로 실측합니다.
- 주의: 인터넷에서 임의의 패킹된 파일을 받아 실행하지 마세요. 패킹은 악성코드의 기본 위장술이기도 합니다.
Step 219의 샘플은 strings에 메시지가 그대로 보였습니다. 그런데 실전 바이너리 — 특히 악성코드 — 는 strings에 아무것도 안 걸리는 경우가 많습니다. 원본 코드를 통째로 압축·암호화해 싸 버렸기 때문입니다. 이것이 패킹(packing)이고, 싸인 것을 다시 벗겨 내는 것이 언패킹(unpacking)입니다. 오늘은 패킹이 왜 문자열을 숨기는지를 실측으로 확인하고, 업계 표준 패커 UPX의 언패킹 절차와 OEP 개념을 배웁니다.
1. 학습 목표
이 챕터를 끝내면 다음을 할 수 있습니다:
- 패커(packer)의 구조 — 압축된 페이로드 + 풀어 주는 스텁(stub) — 을 설명한다
- 압축이 문자열 분석을 무력화한다는 것을 실측으로 보인다
- OEP(Original Entry Point, 원래 진입점)가 무엇이고 왜 언패킹의 목표인지 설명한다
- UPX 자동 언패킹(
upx -d)과 수동 언패킹(메모리 덤프)의 차이를 안다 - "덤프했는데 실행이 안 되는" 상황 — IAT 복구 문제를 개념으로 설명한다
2. 배경 지식 — 오늘의 도구와 개념
오늘의 도구 한눈에 보기
| 구분 | 내용 |
|---|---|
| 언어·환경 | WSL 우분투 bash (실측: Ubuntu 24.04, gzip 1.12, binutils strings/file) |
| 오늘의 명령어 | gzip -c, strings, file, upx(미설치 → 출력 예시) |
| 필요한 개념 | 패커와 스텁, OEP, 꼬리 점프(tail jump), 메모리 덤프, IAT 복구 |
| 오늘의 산출물 | 압축 전후 strings 비교 기록 + OEP 개념 정리 |
2-1. 패커의 구조 — 압축 덩어리와 포장지
패커(packer)는 실행 파일의 원본 코드·데이터를 압축(또는 암호화)해서 감싸는 도구입니다. 결과물의 구조는 두 조각입니다.
- 페이로드: 압축된 원본. 이 상태로는 실행도, 문자열 추출도 안 됩니다.
- 스텁(stub): 파일 맨 앞에 붙는 작은 복원 코드. 실행되면 메모리에서 페이로드를 풀고, 풀린 원본 코드로 점프합니다.
겉에서 보면 실행 파일은 스텁뿐입니다. 정적 분석 도구(strings, 디스어셈블러)가 보는 것은 스텁과 알 수 없는 압축 덩어리 — 원본 코드는 파일 어디에도 평문으로 존재하지 않습니다. Step 178의 XOR 감춤을 프로그램 전체로 확장한 것이라고 생각하면 됩니다.
2-2. UPX — 가장 유명한 "정직한" 패커
UPX(Ultimate Packer for eXecutables)는 오픈소스 압축 패커로, 원래 용도는 파일 크기 절약입니다. 악의적 목적이 아니라서 자기 이름과 버전을 파일에 남기고, upx -d 한 줄로 원상 복구됩니다.
실전 감각: UPX로 패킹된 파일에는 UPX0, UPX1 같은 섹션 이름과 "UPX!" 매직이 남습니다. 그런데 바로 그 "정직함" 때문에 악성코드 제작자들은 UPX를 변형해 씁니다 — 매직을 지우거나 압축 방식을 바꾼 변형 UPX는 upx -d가 거부합니다. "자동이 안 먹히는 상황"이 실전의 기본값이고, 그때 필요한 것이 수동 언패킹입니다.
2-3. OEP — 언패킹의 목표 지점
패킹된 프로그램의 실행 순서는 이렇습니다: 운영체제가 스텁을 시작 → 스텁이 메모리에 원본을 복원 → 원본의 시작점으로 점프. 이 마지막 점프의 목적지가 OEP(Original Entry Point, 원래 진입점)입니다.
수동 언패킹의 정의는 한 문장입니다: 스텁이 복원을 끝내고 OEP로 점프하는 바로 그 순간에 실행을 멈추고, 메모리에서 풀린 원본을 덤프한다. 파일 안에는 없던 원본 코드가 실행 중 메모리에는 평문으로 존재하기 때문입니다 — "감추고 싶은 것은 실행 중에는 반드시 드러난다" (Step 178, 219에서 계속된 그 법칙입니다).
스텁의 마지막 점프는 보통 아주 먼 주소로 뛰는 한 번의 큰 jmp라서 꼬리 점프(tail jump)라고 부릅니다. 수동 언패킹 실습은 곧 "꼬리 점프 찾기"입니다.
2-4. 덤프 이후의 숙제 — IAT 복구
OEP에서 덤프했다고 끝이 아닙니다. 프로그램이 외부 라이브러리 함수(printf 같은)를 부를 때 거치는 주소표를 IAT(Import Address Table)라고 하는데, 덤프된 시점의 주소는 그 실행의 메모리 배치 기준이라 그대로 저장하면 다른 환경에서 깨집니다.
그래서 덤프 도구(Windows의 Scylla 등)는 "IAT Autosearch → Get Imports → Fix Dump" 순서로 주소표를 재구성합니다. "덤프는 떴는데 실행이 안 된다"는 언패킹 실패의 90%가 IAT 복구 누락입니다. 오늘은 개념만 잡고, 도구 실습은 Windows 분석 환경을 갖췄을 때의 과제로 남깁니다.
3. 따라 하기
3-1. 원리 실측 — 압축하면 strings가 침묵한다
UPX가 없는 이 환경에서도 패킹의 첫 번째 효과(문자열 은폐)는 실측할 수 있습니다. gzip은 실행 파일을 만들어 주지는 않지만 "압축 = 정보 은폐"의 원리는 동일합니다. Step 219의 guard219를 재료로 씁니다.
cd ~/lab219_223
strings ./guard219 | wc -l
gzip -c -9 ./guard219 > guard219.gz
stat -c "%s %n" guard219 guard219.gz
strings ./guard219.gz | wc -l
101
16312 guard219
2931 guard219.gz
34
(2026-09-09 실측.)
출력 읽는 법: 원본에는 문자열이 101개 있었는데 압축본에는 34개뿐이고, 그마저 대부분 잡음입니다. 크기는 16312 → 2931바이트로 줄었습니다. 결정적으로 비밀 문구가 사라졌는지 확인합니다:
strings ./guard219.gz | grep FLAG
strings -e S ./guard219.gz | grep "감지"
(두 명령 모두 아무것도 출력하지 않음)
(2026-09-09 실측.)
읽는 법: FLAG도, 한글 감지 메시지도 통째로 증발했습니다. 파일 형식 확인까지 보면 그림이 완성됩니다:
file guard219 guard219.gz
guard219: ELF 64-bit LSB pie executable, x86-64, ... not stripped
guard219.gz: gzip compressed data, was "guard219", ... original size modulo 2^32 16312
(2026-09-09 실측.)
읽는 법: 압축본은 더 이상 ELF가 아닙니다. 실행 파일의 정체(형식, 문자열, 구조)가 전부 압축 뒤에 숨었습니다. 패커가 하는 일의 절반이 이것입니다 — 나머지 절반(실행 가능하게 만드는 스텁)이 다를 뿐입니다.
3-2. 스텁이 없으면 실행도 안 된다 — 실측
압축본을 직접 실행해 봅니다:
chmod +x guard219.gz
./guard219.gz
bash: line 1: ./guard219.gz: cannot execute binary file: Exec format error
(2026-09-09 실측.)
읽는 법: 당연한 결과이지만 중요합니다. 압축만으로는 실행 파일이 아닙니다. 실행하려면 "풀어 주는 코드"(스텁)가 파일 앞에 붙어 있어야 하고, 운영체제는 스텁을 실행하고, 스텁이 메모리에서 원본을 복원합니다. 이것이 gzip과 패커의 차이이자, 패킹된 파일이 "겉은 실행 파일, 속은 압축 덩어리"인 이유입니다.
3-3. UPX 자동 언패킹 — 출력 예시
이 환경에는 UPX가 없으므로(설치도 하지 않습니다) 표준 절차를 출력 예시로 안내합니다. 실제로 해볼 분은 패키지 관리자(sudo apt install upx-ucl)로 설치하세요.
# 출력 예시 — UPX 설치 환경 기준
upx -9 -o guard219_packed ./guard219
# 출력 예시
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.4 Markus Oberhumer, Laszlo Molnar & John Reiser May 9th 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
16312 -> 6420 39.36% linux/amd64 guard219_packed
(출력 예시 — 미실측. 실제 수치는 버전과 파일에 따라 다릅니다.)
패킹된 파일은 겉모습이 달라집니다:
# 출력 예시
strings ./guard219_packed | head
# 출력 예시
UPX!
UPX!
$Info: This file is packed with the UPX executable packer http://upx.sf.net $
...
(출력 예시 — 미실측.)
읽는 법: 원본 문자열 대신 UPX의 자기 소개가 보입니다. 정직한 패커라서 이름표를 달고 있죠. 자동 해제는 한 줄입니다:
# 출력 예시
upx -d guard219_packed
# 출력 예시
16312 <- 6420 39.36% linux/amd64 guard219_packed
(출력 예시 — 미실측.)
읽는 법: 화살표 방향이 패킹과 반대(작은 것 → 큰 것)이고, 원래 크기로 돌아왔습니다. UPX는 자기 압축 방식을 스스로 알기에 완벽 복원됩니다. 자동 언패킹이 가능한 것은 "패커를 알기 때문"입니다 — 모르는 패커, 변형된 패커가 오면 이 버튼은 없습니다.
3-4. 수동 언패킹의 지도 — OEP 찾기 (개념 절차)
upx -d가 거부하는 바이너리를 만났을 때의 표준 절차를 정리합니다. x64dbg/Scylla(Windows) 기준 화면 예시이며, 개념은 리눅스 gdb에서도 동일합니다.
# 화면 예시 — 수동 언패킹 5단계
1. 디버거에 패킹된 바이너리 로드 → 시작 지점은 "스텁"이다
(단서: pushad / pusha — 레지스터 일괄 저장. 복원 후 되돌리려고)
2. 스텁 코드를 따라가며 복원 루프를 지나친다
(반복문이 길게 이어지다가 갑자기 아주 먼 주소로의 jmp 하나)
3. 그 꼬리 점프(tail jump)에 브레이크포인트 → 도착지가 OEP
4. OEP에서 실행을 멈추고 Scylla로 메모리 덤프 (Dump)
5. IAT Autosearch → Get Imports → Fix Dump로 주소표 복구
→ 이제 디스어셈블러/디컴파일러에서 원본 코드가 읽힌다
OEP 판별 감각: OEP에 도달하면 코드의 "표정"이 바뀝니다. 스텁의 기계스러운 복원 루프가 끝나고, 프로그램답게 생긴 시작 코드(표준 프롤로그 — 스택 프레임을 잡고 라이브러리 초기화를 부르는)가 나타납니다. 경험으로 익히는 감각이지만, 첫 단서는 언제나 "복원 루프 직후의 먼 점프"입니다.
3-5. 오늘 실측한 것과 못 한 것의 경계
| 항목 | 방법 | 상태 |
|---|---|---|
| 압축 = 문자열·구조 은폐 | gzip + strings 비교 | 2026-09-09 실측 |
| 압축만으로는 실행 불가 | 압축본 직접 실행 → Exec format error | 2026-09-09 실측 |
| UPX 패킹/자동 해제 | upx 명령 | 출력 예시 (UPX 미설치) |
| 수동 언패킹 5단계 | x64dbg + Scylla | 화면 예시 (Windows 도구) |
| IAT 복구 | Scylla Fix Dump | 개념 설명 |
정직한 경계를 아는 것도 분석 실력입니다. 원리(은폐와 복원)는 실측으로 체감했고, 도구 절차는 실전 환경에서 그대로 재현할 수 있는 지도를 가졌습니다.
4. 미션과 연습문제
미션 — "패킹 전후 리포트" 작성
- Step 219의 guard219(또는 아무 실습 바이너리)를 골라, 패킹 전 상태를 기록합니다 —
file결과,strings개수, FLAG 검색 결과 - gzip으로 압축해 같은 항목을 다시 기록합니다
- 압축본 실행 시도 결과(
Exec format error)를 기록하고, "왜 패커에는 스텁이 필요한가"를 두 문장으로 씁니다 - 마지막 문단: "내가 수동 언패킹을 해야 하는 변형 패커를 만났다면, 어떤 순서(5단계)로 접근하겠는가"를 정리합니다
연습문제
문제 1. 패킹된 파일에서 strings가 침묵하는 이유를, 3-1의 실측 수치(101개 → 34개)를 들어 설명하세요.
문제 2. 3-2에서 압축본은 실행이 안 됐는데, UPX로 패킹한 파일은 실행됩니다. 이 차이를 만드는 부품은 무엇이며, 그 부품의 임무는 무엇인가요?
문제 3. OEP를 "언패킹의 목표 지점"이라고 부르는 이유를, "풀린 원본 코드가 존재하는 장소"의 관점에서 설명하세요.
문제 4. 덤프를 떴는데 실행이 안 되는 가장 흔한 원인은 무엇이며, 왜 그런 일이 생기나요?
5. 모범 답안과 완료 기준
미션 모범 답안
[패킹 전] file: ELF 64-bit LSB pie executable, not stripped
strings: 101개 / FLAG 검색: FLAG{ant1_d3bug_byp4ss3d} 발견
[패킹 후] file: gzip compressed data (ELF 아님)
strings: 34개 / FLAG 검색: 없음
[실행 시도] ./guard219.gz → cannot execute binary file: Exec format error
스텁이 필요한 이유: 압축된 데이터는 CPU가 직접 실행할 수 없다.
누군가가 먼저 메모리에서 풀어 주고 풀린 코드로 점프해야 하며,
그 "누군가"가 바로 파일 앞에 붙는 스텁이다.
[수동 언패킹 접근 순서] ① 디버거로 스텁 진입 → ② pushad와 복원 루프 식별
→ ③ 꼬리 점프에 BP를 걸어 OEP 포착 → ④ OEP에서 덤프
→ ⑤ IAT 복구(Autosearch → Get Imports → Fix Dump) 후 정적 분석.
검증하는 법: 전후 수치가 실제 명령 출력과 일치하는가(재실행해 대조), 스텁 설명에 "메모리에서 복원"과 "점프"가 둘 다 들어갔는가, 5단계 순서가 빠짐없이 있는가. 특히 ⑤를 빼먹으면 안 됩니다 — 벽 2의 주인공입니다.
연습문제 해답
문제 1 해답. 원본의 문자열은 ELF 파일 안에 평문으로 저장되어 있어 strings가 101개를 건졌습니다. 압축은 데이터를 다른 비트 패턴으로 바꿔 버리므로, 압축본에서는 같은 바이트 열이 존재하지 않고 결과적으로 34개의 잡음만 남았습니다. FLAG와 한글 메시지가 모두 검색에 걸리지 않은 것이 증거입니다. 정적 분석의 첫 도구가 파일의 "겉"만 읽기 때문입니다.
문제 2 해답. 스텁(stub)입니다. 스텁은 패킹된 파일 맨 앞에 붙는 복원 코드로, 운영체제가 실행하는 실제 진입점입니다. 임무는 두 가지 — 메모리에서 압축된 페이로드를 원본 코드로 풀고, 풀린 원본의 진입점(OEP)으로 점프하는 것입니다. gzip본에는 이 부품이 없어서 운영체제가 실행을 거부한 것입니다.
문제 3 해답. 파일 안에는 원본 코드가 평문으로 존재하지 않지만, 스텁이 복원을 끝낸 순간 메모리에는 평문 원본이 있습니다. OEP는 바로 그 "복원 완료"의 끝이자 원본 실행의 시작점입니다. 거기서 멈춰 메모리를 덤프하면 파일에서 읽을 수 없던 원본 코드를 얻습니다. 그래서 수동 언패킹의 모든 절차가 "OEP에 도달하기"를 향합니다.
문제 4 해답. IAT(Import Address Table) 복구 누락입니다. 프로그램이 외부 라이브러리 함수를 부를 때는 IAT라는 주소표를 거치는데, 덤프 시점의 표에는 그 실행에서만 유효한 메모리 주소가 들어 있습니다. 다른 실행/환경에서는 라이브러리가 다른 주소에 올라가므로 표가 무효가 되고, 잘못된 주소를 부르는 순간 프로그램이 깨집니다. 그래서 덤프 도구는 주소표를 "함수 이름" 기준으로 다시 채우는 IAT 복구를 수행합니다.
완료 기준 체크리스트
- [ ] 패커의 두 부품(압축 페이로드 + 스텁)을 설명할 수 있다
- [ ] 압축 전후의 strings 개수와 FLAG 검색 결과를 실측으로 비교했다
- [ ] 압축본이 실행되지 않는 것을 확인하고 이유를 안다
- [ ] OEP와 꼬리 점프가 무엇인지 설명할 수 있다
- [ ]
upx -d가 먹히는 조건(패커를 아는 경우)과 안 먹히는 경우를 안다 - [ ] 수동 언패킹 5단계를 순서대로 말할 수 있다
- [ ] IAT 복구가 왜 필요한지 한 문장으로 말할 수 있다
6. 흔한 실수와 해결
벽 1. upx -d가 거부한다
증상(출력 예시): NotPackedException: not packed by UPX 또는 CantUnpackException.
원인: 파일이 UPX가 아닌 패커로 싸였거나, UPX 매직/섹션 이름을 지운 변형 UPX일 가능성이 큽니다. 자동 해제는 "패커를 아는 경우"에만 동작합니다.
해결: 수동 언패킹(3-4의 5단계)으로 전환하세요. 거부 자체가 정보입니다 — "이 파일은 숨길 의도가 있다"는 뜻이니까요.
벽 2. 덤프는 떴는데 실행이 안 되거나 바로 죽는다
증상: 덤프한 exe를 실행하면 즉시 종료하거나 주소 오류가 납니다.
원인: IAT 복구 누락 — 덤프에 그 실행에서만 유효한 주소가 박혀 있습니다 (2-4).
해결: Scylla 기준 "IAT Autosearch" → "Get Imports" → "Fix Dump" 순서를 빠뜨리지 마세요. Get Imports 목록에 빨간(실패) 항목이 남아 있으면 수동으로 올바른 라이브러리 함수를 지정해 줍니다.
벽 3. OEP인지 아닌지 판별이 안 된다
증상: 점프를 따라갔는데 또 기계스러운 코드만 나옵니다.
원인: 꼬리 점프를 잘못 골랐거나(복원 루프 안의 점프), 패커가 복원을 여러 겹으로 합니다.
해결: 진짜 OEP의 표정은 "프로그램다운 시작"입니다 — 스택 프레임 프롤로그와 라이브러리 초기화 호출. 층이 여러 겹이면 각 층에서 같은 절차를 반복하면 됩니다. 인내심이 도구입니다.
벽 4. 압축본을 실행해 보니 Exec format error가 나와 당황했다
증상: ./guard219.gz: cannot execute binary file: Exec format error (2026-09-09 실측).
원인: 오류가 아니라 정상입니다. gzip본은 실행 파일이 아니라 데이터 파일이라 운영체제가 거부합니다.
해결: 이 결과가 곧 오늘의 실측 결론입니다 — "압축만으로는 실행이 안 되고, 스텁이 필요하다". 당황하지 말고 기록하세요.
벽 5. 패킹된 파일이 악성코드일까 봐 실행이 두렵다
증상: 수동 언패킹은 "실행해 가며" 하는 작업이라 주저됩니다.
원인: 합리적인 두려움입니다. 패킹은 악성코드의 상복입니다.
해결: 수동 언패킹은 반드시 격리된 분석 VM(스냅샷 있는 가상머신, 네트워크 차단)에서 하세요. 오늘 실습한 것처럼 내가 만든 파일로 연습하고, 출처 불명 파일은 처음부터 정적 분석만으로 판단합니다.
7. 정리
오늘의 개념
| 개념 | 한 줄 설명 |
|---|---|
| 패커 | 원본 코드를 압축·암호화해 감싸는 도구 — 실행 파일을 데이터로 바꾼다 |
| 스텁 | 파일 앞의 복원 코드 — 메모리에서 풀고 OEP로 점프한다 |
| UPX | 가장 유명한 오픈소스 패커 — 정직해서 자동 해제(upx -d)가 된다 |
| 변형 패커 | 매직을 지우거나 방식을 바꾼 패커 — 자동 해제 불가, 수동 언패킹 필요 |
| OEP | 원래 진입점 — 스텁의 복원이 끝나고 점프하는 원본의 시작 주소 |
| 꼬리 점프 | 스텁 끝에서 OEP로 뛰는 한 번의 먼 jmp — 수동 언패킹의 표적 |
| IAT 복구 | 덤프의 주소표를 함수 이름 기준으로 재구성 — 안 하면 덤프가 안 돌아간다 |
오늘의 명령어
| 명령 | 하는 일 |
|---|---|
gzip -c -9 ./바이너리 > out.gz |
원리 실습용 압축 — 문자열 은폐 효과 확인 |
strings ./파일 | wc -l |
패킹 전후 문자열 개수 비교 |
file ./파일 |
ELF인지, 압축 데이터인지 정체 판별 |
upx -9 -o 출력 입력 |
UPX 패킹 (설치 환경에서, 오늘은 출력 예시) |
upx -d 패킹본 |
UPX 자동 언패킹 — 패커를 알 때만 가능 |
| (x64dbg+Scylla) 꼬리 점프에 BP → Dump → Fix Dump | 수동 언패킹 표준 절차 |
명령어보다 중요한 감각
패킹은 마술이 아니라 포장입니다. 오늘 실측이 보여 준 것처럼, 압축 한 번이면 문자열 101개가 34개로 줄고 FLAG가 증발합니다 — 하지만 실행하려면 언젠가는 풀어야 하고, 풀린 순간은 메모리에 있습니다. "파일이 숨기면 실행에게 묻는다"는 리버싱의 대원칙이 언패킹에서도 그대로입니다.
그리고 도구의 경계를 아는 것이 실력입니다. upx -d가 되면 버튼을 누르고, 안 되면 5단계 지도를 펼치면 됩니다. 중요한 것은 어느 쪽이든 "지금 어느 단계에 있는가"를 스스로 말할 수 있는 것 — 그 감각이 있으면 처음 보는 패커도 겁낼 이유가 없습니다.
전부 체크되면 Step 220 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.