PE파일이 실제 저장장치에서 프로세스 가상 메모리로 로드되면 PE헤더에 적힌 ImageBase를 읽고 메모리에 그 주소에 로딩된다. 하지만 생각해보면 같은 exe 파일이면 기본적으로 같은 ImageBase를 가진다. DLL이나 SYS파일도 같은 ImageBase를 가진단 말이지?
파일 형식 | 기본 Imagebase |
---|---|
exe | 00400000 |
dll | 10000000 |
sys | 10000 |
즉 여러 pe파일을 로딩하게 되면 내가 로드되어야 할 메모리 주소에 이미 다른 pe가 로드되어 있을 수 있다. 그럴 경우 메모리의 다른 비어있는 곳에 로드되어야 하는데 이 과정을 relocation이라고 한다.
예시로 보자면 이런 감성이다. B라는 dll이 로드를 시도할 때 B의 Imagebase인 10000000에 로드를 하려고 할 것. 그치만 이게 웬걸. A가 이미 그 주소를 차지하고 있다. 이럴 경우 빈 메모리 주소 3C000000에 로드되는 것이다.
ASLR은 윈도우 vista 이후부터 생긴 보안 강화용 기능이다. 기존에 프로세스가 실행될 경우엔 여러 pe 중에서 exe 파일이 가장 먼저 메모리에 로딩되기 때문에 exe는 재배치를 고려할 필요가 없었다.
하지만 보안을 위해서 exe를 실행할 때마다 매번 이 exe가 로드되는 메모리 주소를 랜덤하게 지정하는 것이다.
이게 Address Space Layout Randomization, ASLR이다.
ASLR은 dll이나 sys 파일에도 적용된다. Microsoft는 각 운영체제마다 주요한 시스템 dll들은 각자가 고유한 imagebase를 가지도록 했다. 그래서 한 시스템에서 kernel32.dll, user32.dll 등과 같은 것은 자기 imagebase에 로드되기 때문에 얘네들끼리는 Relocation을 걱정할 필요가 없어진 것이다.
실습을 통해서 어떻게 pe가 재배치 되나 확인해보자.
그 전에 우린 하드코딩이란 개념을 알아야 한다. 사실 우리가 아니라 내가 모른다.
Hard Coding은 데이터(상수, 변수, 주소 등등)이 소스 코드에 다이렉트로 적혀있단 것이다. 즉 하드코딩하게 되면 데이터의 값이 고정된다는 것이다. (입력에 따라서 data가 달라지는 개념이 아니니까.)
Ollydbg를 이용해서 notepad.exe를 실행하면 이런 사진처럼 ASLR으로 인해 주소가 280000대에서 놀아야 한다고 한다. 그리고 이런 명령어들에선 메모리주소가 하드코딩된 것들이 보여야 한다.
하지만 우리의 x96dbg에선 어째서 이렇게 깔끔하게 01000000으로 imagebase를 잡으셨을까..
ASLR은 이 notepad.exe가 실행될 때마다 계속하여 프로세스 메모리 주소가 달라지게 만든다.
프로세스 메모리 주소가 하드코딩된 부분이 존재할 거야. (재배치 없는 이론적 주소)
그 주소에서 Imagebase를 빼줘
(VA를 RVA로 바꾸는 거지)
이제 실제로 로딩된 주소에 그 RVA만큼 더해줘.
그렇다면 핵심은 바로 하드코딩된 주소 위치를 찾는 것이다. 이 하드코딩된 주소들의 offset 위치를 모아놓은 목록이 바로 Relocation Table이다. PE 파일 내부에 존재하는 이 목록은 PE가 컴파일되고 링크되는 일련의 빌드 과정에서 주어진다. PE 헤더의 Base Relocation Table를 따라가면 이 relocation table을 찾아갈 수 있다.
PE 헤더 배울때 DataDirectory[0]~[15] 있었던 것 기억나는가. DataDirectory[5]에 Base Relocation Table의 주소가 RVA로 담겨있다.
PE View로 보면 이렇게 확인할 수 있다. 지금 0이라고 되어있네. 그래서 지금 실행할 때 relocation이 딱히 안됐던거구만.
그래서 리버싱핵심원리 깃헙가서 새로 16장 파트의 notepad.exe를 다운받아서 PE View로 열어봤다.
봐봐. 지금은 Base Relocation Table이 rva 2F000이잖아. 그럼 얜 x96dbg로 실행해보면 이번엔 ASLR이 될거라고.
그렇지 주소가 이상하잖아. 15장까진 실습을 위해 이론적인 주소를 제대로 알아야 하니까 일부로 Base relocation을 0으로 만든거였네.
peview로 .reloc 섹션에 가보자.
잘보면 첫 두칸 빼고는 전부 다 3으로 시작하는게 보이니?
이게 뭔지를 알려면 Base Relocation Table의 구조를 알아야 한다.
Base Relocation table은 결국 IMAGE_BASE_RELOCATION이라는 구조체가 여러개 배열된 꼴이다. 이 구조체의 정의를 간단하게 알려줄게.
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress // 기준 주소인데 실제로는 RVA 값이다.
DWORD SizeOfBlock // 각 단위 블록의 크기다.
}
그리고 멤버는 아니지만 WORD짜리 TypeOffset[1]이라는 배열이 주우욱 나열되어있다.
2바이트짜리 TypeOffset은 4비트짜리 Type (숫자 하나)랑 12비트짜리 Offset (세자리 숫자)의 조합이다.
즉 아까 이 사진에서 구조체를 분석하면 기준주소는 RVA 1000이고 단위블록의 크기는 150, 그리고 342D, 3420, 3461, 3436...이 나열된 것이다.
Type은 구조체에서 정의하느는데 PE에서 일반적인 값은 3이다. (IMAGE_REL_BASED_HIGHLOW=3) 64비트 아키텍쳐용 PE+에선 A이다. (IMAGE_REL_BASED_DIR64=A)
이 구조체는 TypeOffset에 채워진 값이 0000이면 끝난다.
그래서 이 Offset이 중요하다.
따라서 하드코딩된 주소가 어디에 적혀있느냐?
기준 주소 Virtual Address(RVA) + Offset = 하드코딩된 주소 위치
예를 들자면
1000 + 420 = 1420 (rva)이다.
rva 1420에 가보자.
지금의 내 x96dbg에선 4D0000부터 로딩되어있다. 따라서 rva 1420은 VA 4D1420이다.
명령어를 보면 CALL 004D10C4,
즉 정리하면 VA 4D1420 주소엔 4D10C4라는 IAT 주소가 저장되어있다. 4D10C4주소가 4D1420으로 재배치된 것이다.
이 4D10C4를 PE View에서 찾을 수도 있다.
PE View의 .text 섹션에 가서 rva 1420을 가니까 010010C4가 있다. 이건 하드코딩된 주소다.
아까 찾은 4D10C4랑 비슷해보이네.
Image Base 01000000을 뺀다.
010010C4 - 01000000 = 10C4
실제 로딩되는 주소에다 더해서 VA를 구한다.
4D0000 + 10C4 = 4D10C4
PE 로더는 프로그램 내에 하드코딩된 10010C4 같은 주소를 전부 다 4D10C4처럼 재배치한다. 이걸 IMGAE_BASE_RELOCATION 구조체가 끝날 때까지 반복한다.