Research / 리버싱 / UPack은 PE 헤더를 어떻게 겹쳐 써서 분석을 어렵게 만드는가
UPack은 PE 헤더를 어떻게 겹쳐 써서 분석을 어렵게 만드는가
- 유형
- 연구 기록
- 분류
- 리버싱
- 발행
- 2025. 12. 29.
- 분량
- 2분
- 종류
- 연구 기록
파일은 정상으로 실행되는데 디버거와 분석 도구는 헤더를 제대로 못 읽습니다. UPack이 PE 헤더의 어느 값을 바꾸는지 하나씩 정리했습니다.
UPack은 실행 파일을 압축하는 패커입니다. 압축 자체보다 PE 헤더를 비정상적으로 바꿔서 디버거와 분석 도구가 파일을 제대로 읽지 못하게 하는 것이 특징입니다.
예전에는 악성코드에 많이 쓰였고, 지금은 UPack으로 패킹된 파일을 백신이 통째로 삭제 처리하는 방식으로 대응합니다.
이 글은 "리버싱 핵심원리"의 UPack 장을 공부하면서 정리한 내용입니다.
정상 PE 헤더의 순서
평범한 PE 파일의 헤더는 이 순서로 놓입니다.
IMAGE_DOS_HEADER → DOS Stub → IMAGE_NT_HEADER → IMAGE_SECTION_HEADER
UPack으로 패킹한 파일은 이 순서가 불규칙하고, 섹션과 헤더를 서로 겹쳐 씁니다. 그렇게 해도 Windows 로더가 실행하는 데는 문제가 없기 때문입니다.
UPack이 바꾸는 것 여섯 가지
1. DOS 헤더와 NT 헤더를 겹쳐 쓴다
NT 헤더(PE 헤더)가 어디서 시작하는지는 고정이 아니라, DOS 헤더의 e_lfanew 값으로 정해집니다.
- 보통 DOS 헤더는
0x40, DOS Stub는0xA0크기라서e_lfanew는0xE0입니다. - 이 값을 바꾸면 NT 헤더의 위치를 마음대로 옮길 수 있습니다.
UPack은 이걸 이용해 두 헤더를 겹칩니다. 헤더 공간을 아끼는 동시에 구조를 더 복잡하게 만듭니다.
2. SizeOfOptionalHeader를 키운다
IMAGE_FILE_HEADER.SizeOfOptionalHeader는 Optional 헤더의 크기입니다. Optional 헤더는 구조체라서 크기가 사실상 고정입니다.
- PE32:
0xE0 - PE32+:
0xF0
이 필드는 원래 파일 형식마다 다른 구조체를 쓸 수 있게 하려고 만든 것입니다.
섹션 헤더는 "Optional 헤더 시작 위치 + SizeOfOptionalHeader"에서 시작하는데, UPack은 이 값을 더 크게 잡아서 Optional 헤더와 섹션 헤더 사이에 빈 공간을 만들고, 거기에 디코딩 코드를 넣습니다.
3. NumberOfRvaAndSizes를 줄인다
IMAGE_OPTIONAL_HEADER.NumberOfRvaAndSizes는 데이터 디렉터리 배열의 원소 개수입니다.
- 원래:
0x10개 - UPack:
0xA개
구조체 자체는 16개 자리를 그대로 갖고 있지만, 로더는 이 값을 개수로 봅니다. 그래서 뒤의 6개 자리는 존재하지만 실행에 영향을 주지 않는 공간이 되고, UPack은 여기에도 코드를 넣습니다.
4. 섹션 헤더의 안 쓰이는 부분에 데이터를 기록한다
보통 PE 파일은 섹션이 3개이고, 섹션 헤더는 0x170에서 시작합니다.
PE 파일에서는 두 섹션의 파일 시작 오프셋과 크기가 같아도 문제가 없습니다. 로더는 섹션 헤더 값에 따라 파일 영역을 각자 다른 메모리 위치에 매핑하기 때문입니다. 같은 파일 이미지로 위치와 크기가 다른 메모리 이미지를 여러 개 만들 수 있다는 뜻입니다.
UPack 파일의 섹션을 보면 이렇습니다.
| 섹션 | 특징 | 역할 |
|---|---|---|
| 두 개 | 파일 시작 오프셋이 같고 크기가 매우 작음 | — |
| 나머지 하나 | 파일 대부분을 차지 | 원본 파일이 압축돼 들어 있음 |
| 메모리 크기가 원본 SizeOfImage와 같은 섹션 | 실행 중 원본 이미지가 풀리는 자리 | 압축 해제 대상 |
5. RVA → RAW 변환이 맞지 않게 만든다
RAW: 파일 안에서의 위치(실행 전). RVA: 메모리에 올라간 뒤 ImageBase로부터의 상대 주소.
보통은 이 식으로 RVA를 RAW로 바꿉니다.
RAW - PointerToRawData = RVA - VirtualAddress
UPack 파일에서는 이 식으로도 RAW를 찾을 수 없습니다. PointerToRawData는 FileAlignment의 배수여야 하는데, UPack 파일은 그렇지 않기 때문입니다. 분석 도구가 이 식을 그대로 쓰면 엉뚱한 위치를 읽게 됩니다.
6. Import Table을 섹션 끝에 걸쳐 둔다
Import Table의 주소는 데이터 디렉터리에서 얻습니다. IMAGE_DATA_DIRECTORY는 8바이트이고, 앞 4바이트가 주소(RVA), 뒤 4바이트가 크기입니다.
Import Table은 IMAGE_IMPORT_DESCRIPTOR 배열이 이어지다가 NULL 구조체로 끝나야 합니다.
그런데 UPack 파일에서 이 주소를 RAW로 바꿔 보면, 첫 번째 구조체는 있는데 두 번째 구조체도, 끝을 알리는 NULL도 없고 섹션 끝에 걸쳐 있습니다.
그래도 실행이 되는 이유는 로더가 섹션을 매핑하는 방식에 있습니다.
- 파일에는
SizeOfRawData만큼 저장돼 있지만, 메모리에는VirtualSize만큼만 매핑됩니다. - 그래서 세 번째 섹션 끝 이후는 메모리에 올라가지 않고, 그 자리가 NULL로 채워집니다.
- 로더는 RVA 기준으로 매핑하므로 결과적으로 Import Table이 NULL로 끝나는 것처럼 동작합니다.
공격자 관점에서 보면
UPack이 기대는 가정은 하나입니다. "분석 도구는 PE 명세의 일반적인 값을 믿지만, Windows 로더는 그보다 훨씬 관대하다."
- 헤더 크기, 디렉터리 개수, 정렬 값처럼 도구가 "당연히 이 값"이라고 가정하는 필드를 바꿉니다.
- 로더가 실제로 보는 값(
e_lfanew,NumberOfRvaAndSizes,VirtualSize)만 맞춰 두면 실행은 정상입니다.
그래서 분석할 때는 도구가 보여주는 해석보다, 로더가 실제로 그 값을 어떻게 쓰는지를 기준으로 헤더를 다시 읽어야 합니다.
정리
- UPack은 PE 헤더를 겹쳐 쓰고, 안 쓰이는 공간을 만들어 디코딩 코드를 넣습니다.
- 분석 도구가 기대는 값(헤더 크기, 정렬)과 로더가 실제로 쓰는 값의 차이를 이용합니다.
- 참고: "리버싱 핵심원리" UPack 장