[조립하면서 배우는 PE] 일곱번째 이야기 : Pre-Binding Reverse Engineering

리버싱 2017. 1. 10. 16:40

곱번째 이야기입니다. 참 오랜만에 쓰는 글 같습니다. 저도 글을 쓰기 전에 앞에 쓴 글을 다시 읽어봐야 했습니다. 먼소리를 했는지 기억이 안나서... ^^; 암튼 살펴보니, 앞서 이야기에서 ILT에서 임포트할 함수의 이름 또는 ordinal 값을 알아낸 후 이 정보를 이용하여 익스포트 디렉토리(익스포트 테이블)로 부터 함수의 주소를 알아낸다는 사실을 알아보았더군요. 살짝 복습을 해볼까요? Windows XP에 있는 calc.exe 프로그램의 임포트 디렉토리를 찾아  ILT와 IAT를 잠깐 살펴보도록 하죠. 데이터 디렉토리는 그냥 StudPE를 이용하여 확인하도록 하겠습니다. 

사용자 삽입 이미지





그림 1. StudPE로 살펴본 calc.exe의 임포트 테이블(임포트 디렉토리) 주소 

임포트 테이블(임포트 디렉토리)의 주소(RVA)는 0012B80이고 사이즈는 8c입니다. 여섯번째 이야기에서 알아보았듯이 Raw Offset 값은 StudPE가 계산해 준 것이구요, 실제로 데이터 디렉토리에는 존재하지 않는 데이터이죠. 실제 계산 방법은 여섯번째 이야기를 참고하세요. 어쨌든 파일 상에서는 Raw Offset 11F80에서 임포트 테이블을 찾을 수 있겠습니다. WinHex를 이용하여 11F80으로 이동하여 보겠습니다. 

사용자 삽입 이미지


그림 2. WinHex를 이용하여 살펴본 calc.exe의 임포트 테이블 


[그림 2]에서와 같이 두번째 필드인 TimeDateStamp와 세번째 필드인 ForwarderChain은 바인딩 전에는 -1 값을 가집니다. 세번째 필드는 Name은 임포트할 DLL의 이름을 가르키는 포인터(RVA)입니다. 파일 상태에서 읽을 필요가 없으므로 RVA값만 기록되어 있습니다. 기억하시죠? PE 파일에서 어떠한 데이터 구조를 가르킬 때 RawOffset 값이 없다면 로더 입장에서 해당 데이터 구조는 파일 상태에서는 따로 접근하지 않는다는 이야기입니다. 메모리에 로드된 후에나 사용한다는 것이죠. 어쨌든 그래도 파일 상에서 Name을 확인해보도록 하겠습니다. 여섯번째 이야기에서 살펴본 것처럼 데이터 디렉토리내의 데이터구조를 파일 상에서 찾으려면 RVA값과 RawOffset이 동시에 기록되어 있는 섹션 테이블을 참조해야 합니다. StudPE를 이용하여 섹션 테이블을 살펴보면 Name은 .text 섹션에 포함되어 있음을 알 수 있습니다. (RVA값이 00012E42이므로 .text 섹션 범위안에 존재합니다. 아래 그림 참고)

사용자 삽입 이미지

 


 





그림 3. StudPE를 통해 살펴본 calc.exe의 섹션 테이블


.text 섹션의 RVA 값은 00001000이고 RawOffset은 400 이네요. Name필드의 RVA값은 00012E42 이므로 RawOffset은  0x400+(00012E42 -  00001000 ) = 0x12242가 됩니다. WinHex를 이용해서 확인해 볼까요?

사용자 삽입 이미지


그림 4. WinHex를 이용하여 살펴본 calc.exe의 임포트 디렉토리 

이로써 임포트 디렉토리의 첫번째 엔트리는 shell32.dll에 관한 것임을 알 수 있습니다. 그럼 ILT와 IAT를 살펴보도록 하겠습니다. 첫번째 필드인 OriginalFirstChunk로 ILT를 가르키는 RVA 값이며 마지막 필드인 FirstChunk로 IAT를 가르키는 RVA 값이라는 것을 잘 아는 사실일 것입니다. ILT와 IAT 역시 파일 상태에서는 읽을 필요가 없는 데이터들이라 RVA값만 기록되어 있습니다. 휴~ 또 산수시간입니다. ^^; ILT의 위치는 00012CA8 이므로 역시 .text 섹션에 존재함을 알 수 있습니다. 그렇다면 ILT의 파일 상에서의 위치 즉 RawOffset은 0x400 + (12CA8 - 1000) =  0x120A8이군요. WinHex를 이용해서 해당 위치로 이동해 보겠습니다

사용자 삽입 이미지


그림 5. calc.exe의 shell32.dll관련 ILT

 ILT나 IAT는 모두 IMAGE_THUNK_DATA의 배열이라고 했습니다. IMAGE_THUNK_DATA는 4가지 정도의 의미를 가지고 있는데 ILT의 경우 대부분 IMAGE_IMPORT_BY_NAME을 가르키는 RVA값이라는 것도 이미 알아봤구요. 살펴보니 ILT의 첫번째 엔트리는 00012E34 값을 가지고 있네요. 헉헉...
ㅠ.ㅠ;; 또 산수가 필요합니다. 0012E34는 .text 섹션에 위치하므로 파일 상에서의 위치는 0x400 + ( 0012E34 - 1000 ) = 0x12234 입니다. 다시 WinHex를 이용해서 해당 위치를 살펴보도록 하겠습니다. 

사용자 삽입 이미지


그림 6.

위 그림에서 볼 수 있듯이 IMAGE_IMPORT_BY_NAME은 1바이트 사이즈 ordinal값과 이름으로 구성되어 있습니다. 위 그림을 보니 SHELL32.DLL에서 임포트한 첫번째 함수는 ShellAboutW로 오디널 값이 0x94임을 알 수 있습니다. 

자, 이제 IAT를 살펴볼까요? 다시 [그림 2]로 돌아가서 살펴보니 IAT의 RVA는 109C이군요. 역시 .text 섹션에 위치합니다. IAT는 RawOffset은 0x400 + (109c - 1000) = 0x49c 입니다. WinHex를 이용하여 해당 위치를 살펴보면 아래와 같습니다. 

사용자 삽입 이미지

그림 7. calc.exe의 shell32.dll관련 IAT

위 그림에서 볼 수 있듯이 파일 상에서 즉 바인딩 되기 전의 IAT는 ILT와 마찬가지로 보통 IMAGE_IMPORT_BY_NAME 타입의 데이터를 가르킵니다. 물론 동일한 정보를 가르키고 있어야 합니다. 따라서 7744E3DB에 가면 ... 그런데 허걱.. 뭔가 주소가 이상하네요? 7744E3DB라면 왠지 윈도우의 주요 DLL들이 사용하는 주소 중 하나인것 같은데요.. 이럴수가.. 제가 지금까지 거짓말을 한건가요? ㅠ.ㅠ;;  뭐 그런것은 아니구요... ^^ 이건 pre binding(보통 줄여서 binding 또는 bound라고 부릅니다.)이라는 기능때문에 그렇습니다.  

Pre-Binding
 여러개의 DLL을 로딩하는 경우 로딩 타임에 IAT를 완성하는 것은 비교적 오랜 시간이 소요되는 작업이 될 것입니다. 그만큼 프로그램 실행에 많은 지연이 발생하겠죠. 그렇다면, 만약 로딩 타임에 IAT를 완성하지 않고 링킹 타임에 IAT를 미리 완성해 둘 수 있다면 어떻게 될까요? 다시 말해 오브젝트 파일을 링킹하는 단계에서 IAT에 실제 임포트하는 함수의 주소를 채워 넣을 수 있다면? 당연히 성능 향상에 많은 도움이 될 것입니다. 이러한 기능을 pre-binding이라고 합니다. 앞에서 살펴본 calc.exe의 경우 pre-binding이 적용되어 있는 것입니다. 이처럼 pre-binding되어 있는 상태를 일컫을 때 간단히 DLL이 바운드 되었다고 표현합니다. 그렇다면 로더의 입장에서 DLL이 바운드되었는지는 어떻게 알 수 있을까요? 여기에 대한 해답은 bound import table (BIT)에서 찾을 수 있습니다. BIT의 주소는 데이터 디렉토리의 12번째 엔트리인 IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT에서 찾을 수 있습니다. 아래의 그림을 봐주세요. 

사용자 삽입 이미지

그림 8. StudPE를 이용하여 살펴본 calc.exe의 BIT 관련 정보

StudPE를 이용하여 BIT의 위치를 확인해 보았습니다. RVA값이 0x260이고 사이즈가 0x80임을 알 수 있습니다. RawOffset은 우리의 똑똑하신 StudPE가 계산을 못하고 있습니다. ㅠ.ㅠ;; Stupid라고 불러야 할지...  앞의 [그림 3]을 살펴보면 BIT는 섹션에 위치하고 있지 않음을 알 수 있습니다. (RVA값 0x260은 어느 섹션에도 속하지 않습니다.) 이러한 경우 RVA값이나 RawOffset 값은 동일한 값이 됩니다. 따라서 WinHex를 이용하여 파일내 offset 0x260으로 이동해 보면 BIT를 찾을 수 있을 것입니다.  아래의 그림을 봐주세요. 

사용자 삽입 이미지

그림 9. calc.exe의 BIT

BIT의 첫번째 4bytes는 TimeDateStamp 값입니다. 이 값은 매우 중요한데 이에 대해서는 뒤에서 다시 이야기 하도록 하겠습니다. 다음 2bytes는 OffsetModuleName이라는 필드로 BIT의 각 엔트리 시작점에서 모듈의 이름까지의 Offset을 의미합니다. 이로서 우리는 SHELL32.DLL이 이미 바운드 되었음을 알 수 있습니다. 다음 2bytes는 NumberOfModuleForwarderRefs라는 값인데. 이 값은 본 연재에서 다룰 만한 내용이 아니라서 스킵합니다. ^^v 

자 이제 로더는 SHELL32.DLL이 이미 바운드 되었음을 알 수 있으므로 SHELL32.DLL과 관련된 IAT를 채우려고 하지 않을 것입니다. [그림 4]에서 [그림 7]까지 나타난 정보를 살펴보면 shell32.dll의 ShellAboutW의 주소는 7744E3DB임을 알 수 있습니다. 이제 이 주소만 확인해 보면 되겠습니다. 아래의 그림을 봐주세요. 

사용자 삽입 이미지

그림 10. API Address Finder를 이용하여 살펴본 ShellAboutW의 주소. 

허걱.. 이런 IAT 기록된 주소와 일치하지 않습니다. ^^; 또 하나 더 배울게 생겼습니다. 지금의 상황처럼 DLL이 바운딩된 경우 링크 타임에 IAT에 기록된 API의 주소와 실제 주소가 다른 경우가 발생할 수 있습니다. DLL이 업데이트 된 경우이겠죠. 이러한 경우 IAT의 정보가 변경되지 않는다면 당연히 프로그램은 크래쉬됩니다. 따라서 로더는 DLL이 변경되었음을 감지하고 IAT 테이블을 업데이트 할 수 있어야 합니다. 그렇다면 로더는 DLL이 변경되었음을 어떻게 알 수 있을까요? 다시 [그림 9]를 봐주세요. BIT내 엔트리의 처음 4bytes 값은 TimeDateStamp입니다. 로더는 이 값을 DLL의 PE 헤더에 기록된 TimeDateStamp 값과 비교하여 DLL이 변경되었음을 감지할 수 있습니다. 그럼 shell32.dll의 TimeDateStamp값을 확인해 보겠습니다. 

사용자 삽입 이미지


그림 11. Shell32.DLL의 TimeDateStamp

[그림 11]에서 볼 수 있는 것처럼 shell32.dll의 TimeDateStamp값은 [그림 9]에서 확인한 calc.exe의 BIT에 기록된 TimeDateStamp 값보다 큽니다. 로더는 이 정보를 확인하여 shell32.dll이 변경되었음을 알 수 있고 따라서 IAT를 리빌딩하게 됩니다.  이제 마지막으로 calc.exe가 로드된 후 IAT를 살펴보도록 하겠습니다. [그림 2]에 나타난 것처럼 IAT의 RVA가 109c이므로 OllyDbg에서 calc.exe를 실행시킨 후 데이터 덤프 윈도우에서 해당 주소로 이동하면 쉽게 확인할 수 있을 것입니다. 


사용자 삽입 이미지

그림 12. 로드된 후 calc.exe의 IAT 모습 

지금까지 pre-binding에 대해서 알아보았습니다. 이 정도면 임포트에 관해서 기본적으로 알아야 할 것은 어느 정도 공부한 것 같습니다. 물론 지연 로딩이나 API 포워딩 같은 것들을 좀 더 공부해야 하겠지만 본 연재에서는 다루지 않을 생각이구요... 이 글을 읽는 여러분의 숙제로 남겨 두겠습니다. 

다음 이야기는 섹션 테이블에 관한 이야기입니다. 수작업으로 만드는 PE를 좀 더 빨리 완성하기 위해 익스포트 테이블 전에 섹션 테이블을 먼저 알아보도록 할 것입니다. 익스포트 테이블은 우리가 만든 PE 파일이 잘 실행되는 것을 보고 즐기면서 천천히 알아보도록 하겠습니다. ^^ 그럼 오늘도 즐핵하세요. 



출처 - http://zesrever.tistory.com/60

: