오늘 간만에 드라이버 코드를 좀 만졌습니다. 예전에 만들어 놓은 동적 로딩 가능한 드라이버를 테스트할 일이 생겼거든요. ㅎㅎ 반응 속도가 굉장히 빨라야 한다고 해서 허접한 드라이버에 큐도 집어넣고, 나름 동기화도 처리하려고 뮤텍스를 사용했습니다.
그러다 보니 어느새 드라이버가 재부팅 되기 시작했습니다. 한참을 디버깅한 후에 알아내긴 했는데, 내가 이러고도 멀티코어 OS를 만든다고 할 수 있나 하는 생각이 들더군요. 코어 2개가 동작하니 싱글코어 일때 보다 더 Lock/Unlock 범위를 신경썼어야 하는데… ㅠㅠ 크윽... 역시 아직 멀었나 봅니다. 아주 기본적인 것 조차 헷갈리고 있으니... 이거 원 ㅎㅎ
오래 간만에 필터 드라이버를 사용할 일이 생겼습니다. 구석에 짱박힌 소스를 꺼내서 설치한 후 테스트해보니... 얼래... 또 키보드 마우스에 아무런 반응이 없습니다. 이런 경우 눈 앞이 캄캄해지고 정신적으로 공허해져서 믿고 싶지 않은 상태가 되는데... 그래도 오늘은 비교적 일찍 정신이 들더군요.
사실 어제부터 분석하기 시작했지만... ㅡ_ㅡa... 답이 쉽게 안나왔습니다. 그래서 다시 처음부터 하나하나 드라이버 상태를 관찰해가면서 진행한 결과... 빙고~!!!! 다시 잘 동작하는 키보드 마우스 필터 드라이버를 완성했습니다. >ㅁ<)-b 이거 쓸일은 별로 없는데... 괜시리 시간만 투자하는게 아닌가 싶기도 하군요. ^^;;;;
필터 드라이버 수정하기가 점점 더 힘들어지는 것 같습니다. 그래도 이렇게 나마 감을 잃지 않고 있으니 다행이네요. ㅎㅎ 요즘은 64bit OS의 늪에 빠져서 헤어나오지 못하는지라 ㅎㅎ
아웅~ 오늘은 이만자고, 내일은 64Bit OS에 어플리케이션을 올리는 방법에 대해 생각해봐야겠습니다. 사실 이 정도만 하면 거의 끝난거나 마찬가니지... 언능하고 끝내야겠습니다. ;)
흑흑... 이게 얼마만인지 모르겠습니다. ㅜ_ㅜ)-b 쌩판 모르는 상태로 HID 드라이버를 손보기 시작해서 키보드와 마우스를 생성하기까지 한참 걸렸고, 보안 프로그램에 걸려서 이걸 피해 가는데 또 한참 걸렸네요.
ㅜ_ㅜ 아아~ 진짜 ㅡ_ㅡa... 간단히 쓸 수 있는 API를 막아놔서 다른 방법을 찾는다고 고생했습니다. ㅜ_ㅜ 제가 아는 범위에서 테스트 안해본 케이스가 거의 없을 정도로 오만가지를 다 썼는데... 결국 간단한 아이디어로 해결... ^^;;; 그것도 놀다가 깜짝 떠올라서... ㅎㅎㅎ(역시 인생은 타이밍~!!!). 아이디어는 어쩔 수 없이 비공개... 왠지 하면 큰일날듯해서 ^^;;;;;
가상 HID 드라이버가 HID miniport 드라이버이다 보니 이것저것 제약사항이 많더군요. 뭐 하나 제대로 할 수 있는 것도 없고... 툭하면 재부팅되기 일수이고... 평일 저녁시간과 주말 일부를 올인해서 겨우 해결했습니다. 방금 테스트했는데 큰 문제가 없는 것 같네요. ^^)/~~~
이제 더러워진 드라이버 코드를 좀 정리하고, 약간 테스트만 더하면 끝날 것 같습니다. 어휴~ 이번 주는 따로 스케줄이 있어서 내일 말고는 시간이 없을 듯한데, 내일 저녁에 완전히 끝내 놓야겠군요. ^^;;;; 어휴... 죽는 줄 알았습니다. 어휴~ 진짜... @0@)/~!!!
덕분에 HID에 대해서 공부 한번 제대로 했습니다(정말? ㅡ_ㅡa..). 역시 DDK만한게 없군요. 최고입니다. ㅎㅎ DDK 만세~~ 그럼 다들 좋은 밤 되세요 ;)
ps) 이런... ㅡ_ㅡa... 한쪽은 또 다른 수를 써놓았군요. ㅡ_ㅡ;;; 좀 더 파봐야 할 것 같습니다. ㅜ_ㅜ 아우~ 진짜... ㅜ_ㅜ)/~
간단히 시작했던 작업이 어느새 필터 드라이버를 동적 로딩하는 단계까지 발전했습니다. ㅜ_ㅜ)/~ 이게 어찌된 영문인가 모르겠습니다. ^^;;;;
기존의 드라이버가 DDK의 소스를 기반으로 만들어졌는데, 정적으로 로딩되서 실행되는 방식이었습니다. 그런데 갑자기 동적 로딩으로 할 수 있게 해달라는 요청이 있어서 소스를 손보기 시작했습니다. 참고로 동적 로딩과 정적 로딩은 아래와 같은 차이가 있습니다. ^^
정적 로딩 : 윈도우 레지스트리에 등록하여 윈도우 시작 시에 자동으로 로드되는 방식
동적 로딩 : 사용자가 요청하면 로딩하는 방식, 일반적으로 시스템 초기화가 완료된 이후에 로딩됨
처음에는 간단히 동적 로드만 되면 되겠거니 생각했는데, 이게 일이 점점 많아지는 겁니다. ㅜ_ㅜ 키를 입력하게 하는 주요 루틴을 다른 쪽으로 옮겨야 하더군요. 이때부터 작업 로드가 심하게 걸리기 시작했습니다. 잠도 못자고 분석하고 테스트하고... 후덜덜덜...
정적 로딩하는 버전은 PNP 메시지를 받아서 드라이버가 등록될 때 상위 Driver의 Callback 함수를 가지고 있다가 그걸 강제로 호출해서 값을 넣도록 하는 방식이었는데, 동적 로딩할 때는 PNP 메시지를 받지 못한 상태로 로딩되기 때문에 MJ_READ 에서 이를 처리하도록 해야했습니다. 삽질 끝에 알았습니다. 이런... ㅜ_ㅜ 더불어 IRP를 Cancel해야 다시 읽는 다는 것두요. ^^;;;
노트북이 없었더라면 작업 시간이 두배 정도 더 걸릴뻔 했습니다. 어휴... 끔찍하네요. ^^;;;;
방금 테스트를 해봤는데, 정상적으로 동작하는 것 같습니다.
신나는 주말입니다. ^^)/~
ps. Path가 등록이된 파일을 찾고 싶다면, SearchPath를 이용하면 FullPath가 넘어 온다.
Qus) 클라이언트 컴퓨터에 설치된 firefox의 경로를 찾고 싶은데 어떻게 하지? Firefox가 써 놓은 레지스터리값를 뒤져볼까? firefox의 기본 설치 경로에 exe 파일을 createfile해서 판단할까?
Ans) SearchPath를 이용하셔서 FullPath를 얻으시고, 얻은 FullPath를 PathFileExists로 검증하시면 됩니다.
Microsoft Support Professionals Toolkit for Windows
The User Mode Process Dumper (userdump) dumps any running Win32 processes memory image on the fly, without attaching a debugger, or terminating target processes.
Dial-up (56K)DSL/Cable (256K)DSL/Cable (768K)T1 (1.5M) 9 min
Overview
The User Mode Process Dumper (userdump) dumps any running Win32 processes memory image (including system processes such as csrss.exe, winlogon.exe, services.exe, etc) on the fly, without attaching a debugger, or terminating target processes. Generated dump file can be analyzed or debugged by using the standard debugging tools.
The userdump generates dump file by several triggers;
Dump by specifying PID or process name from command line
Dump automatically when process being monitored caused exceptions
Dump automatically when process being monitored exited
Dump by pressing hot key sequence
Updates in April 4,2007 (Build 8.1.2929.5)
Userdum is now fully compatible with Windows Server 2003 SP2 and Windows XP x64 Edition SP2. Previously, Process Monitoring did not function on SP2 of these operating systems. The same problem also occurred if a hotfix for KB919341 or KB909613 was applied to SP1 of these operating systems. This problem has been fixed.
System crash problem on Windows 2000 SP4 has been fixed. Bugcheck 0x1E (BucketID = userdump!ExtractImageFileName+26) could happen when a process monitored by Exit Monitor went to zombie state (the process is not alive but still remains in the system process list) and another process attempted to terminate the process in zombie state. Exit Monitor no longer dumps processes in zombie state in this case as they don’t have any meaningful memory image.
Updates in August 7,2006 (Build 8.1.2929.4)
Thread time information is added to the dump file by default so that debugger extension !runaway works.
Added all other meaningful MiniDumpWriteDump() options available in dbghelp.dll V6.4.7.1
Comment stream is added to the dump file indicating that the dump file was generated by userdump.exe. Comment includes Computer Name and how userdump.exe was launched
New userdump.exe -W option is added to add Window handle information. udext.dll debugger extension DLL is provided to see this information by debugger to debug the dump file.
EXEs and DLLs are now installed to %windir%\system32\kktools\ folder and this location is added to system path.
Userdump.exe is linked with dbghelp.dll dynamically for x86, too. You now need userdump.exe and dbghelp.dll provided with userdump.exe even in command line mode. The same dbghelp.dll is also installed for full-featured mode.
Userdump.exe no longer uses system provided dbghelp.dll on x64 and IPF. Instead, dbghelp.dll provided with userdump is always used on all platforms – x86, x64, and IPF.
Process Monitoring and Hot Key snapshot support long process names up to 32 bytes.
Process Monitoring supports "Switch the dumper" option to specify an alternative dumper such as sqldumper.exe.
Process Exit Monitoring supports dumping both a process being killed and a process who called NtTerminateProcess() in the cross-process termination scenario.
Process Exit Monitoring allows to specify either Complete minidump, Small minidump, or No dump .
Process Exception Monitoring allows to specify Complete minidump or Small minidump.
Process Exception Monitoring can catch exceptions raised by calling RaiseException() in WOW64 processes.
Process Exception Monitoring always catches exceptions raised by RaiseException() regardless of "Ignore exceptions that occur inside Kernel32.dll" switch.
The control panel applet was refined for better GUI.
Non-privileged users can no longer launch the control panel applet.
Improved event logging to log at the beginning and the end of dumping and indicates process names/PIDs.
Supported Operating Systems: Windows 2000 Service Pack 3; Windows 2000 Service Pack 4; Windows Server 2003; Windows Server 2003 Service Pack 1; Windows Server 2003 Service Pack 2; Windows XP Embedded Service Pack 1; Windows XP Embedded Service Pack 2
You need a debugger tool which support dump file analysis like "Debugging Tools for Windows"
If the previous version of the User Mode Process Dumper is installed, you need to uninstall first.
Click the Download button on this page to start the download. Do one of the following:
To start the installation immediately, click Open or Run this program from its current location
To copy the download to your computer for installation at a later time, click Save or Save this program to disk.
To install the User Mode Process Dumper, run the UserModeProcessDumper8_1_2929_5.exe package. After you accept the Software License Terms, all necessary files are copied to the C:\kktools\userdump8.1 folder.
Go to C:\kktools\userdump8.1\Architecture folder or the folder you specified in the previous step, and run setup.exe.
Prior to starting and using the User Mode Process Dumper, please be sure to read the readme.htm file, which is located in the C:\kktools\userdump8.1 folder.
Microsoft and partners are jointly developing tools to improve Windows supportability. This joint-development project started from 1998 and has counted 8th phase already. At phase 8 project, the following partners are participating in the project.
Fujitsu Limited.
Hitachi, Ltd.
Nihon Unisys, Ltd.
NTT Data Corporation
Toshiba Corporation
Tools are owned and released by Microsoft Corporation under the name of "Microsoft Support Professionals Toolkit for Windows".
지인의 블로그에서 오래전에 Cheating in On-line Games에 대한 링크와 자료들을 봤었는데, 나름 바쁜 일이 있어서 그냥 지나쳤습니다. 오늘 문득 생각이나서 다시 들어가보니 내용이 괜찮다는 덧글이 많이 붙어있더군요. 그래서 한번 읽어볼 겸 올려봅니다.
온라인 게임용 보안 프로그램(nProtect, XTrap, 헥쉴드, 가드캣 등등)에 관심이 많으신 분들은 읽어보시면 방어 프로그램의 원리를 알 수 있을 겁니다. 창과 방패와 같다는 느낌이 드는군요. 우수한 방패의 약점을 분석해서 더 우수한 창을 만들고 반대로 우수한 창의 약점을 분석해서 더 우수한 방패를 만들 수 있는 것처럼 해킹과 보안 역시 비슷한 관계가 있는 것이 아닌가 생각해봅니다. ^^;;;
윈도우 시스템 프로그래밍한다는 사람치고 PE 파일에 대해서 모르는 사람은 아마 거의 없을 것이다. 윈도우 실행 파일 및 DLL, 그리고 드라이버 파일까지도 PE 파일 형태를 따르고 있으니 뭘 해도 따라다니는게 이 PE(Portable Executable) 파일 포맷이니까 말이다. PE 파일 포맷은 크게 헤더, 섹션, 데이터의 세부분으로 나뉘는데 기존 DOS 시절 사용하던 COFF(Common Object File Format)과 거의 비슷한 구조를 가지며 기본 뼈대에서 확장된 듯한 형태를 가진다.
PE 실행파일이 가지고 있는 헤더를 분석함으로써 실제 데이터가 있는 위치를 파일에서 찾고 해당 영역을 분석할 수 있다.
PE 파일 구조에 대해서 자세히 알아보기 전에 참고할 좋은 프로그램 몇가지를 소개한다.
PE Explorer : 유료다. ㅡ,.ㅡ;;; 공짜 버전도 있는데 30일 한정이라서... 그렇지만 강력하다 @0@)/~
PE Browser : 공짜다. 하지만 역시 뭔가 부족하다는 거... 그냥 쓰기에는 괜찮다.
앞으로 Relative Virtual Address(RVA)라는 용어가 많이 나올텐데, 잠깐 알아보자.
RVA는 실행파일이 메모리에 로드되었을 때, 그 시작 주소를 0으로 생각하고 계산하는 주소이다. 즉 RVA의 값이 0x40 이고 실행파일이 로드되었을 때, 그 시작위치가 0x1000 이라면 실제 그 영역이 메모리에 로드되었을 때 위치는 0x1040이 된다. 실행파일 시작 위치를 0으로 하는 상대적 주소라는 것만 알면 같단하다. 뒤에 설명하면서 계속 사용될 용어이므로 알아두자.
PE 파일 포맷 전체 구조
PE 파일 포맷은 크게 아래와 같이 구성된다.
위에서 보는 것과 같이 크게는 붉은 색 부분과 푸른색 부분으로 나눌 수 있다. 붉은 색 부분은 헤더나 데이터가 위치하는 영역의 속성과 크기 등등을 나타내는 정보이고, 푸른 색 부분은 실제 데이터들이 위치하는 영역을 나타낸다.
IMAGE_DOS_HEADER : PE 파일의 처음에 위치하며 뒷부분에 DOS에서 실행했을 때, 에러 메시지(This program cannot be run in DOS mode)를 표시하는 스텁(Stub) 코드를 포함하고 있음. MAGIC Number와 다음에 오는 IMAGE_NT_HEADER의 위치를 표시
IMAGE_NT_HEADER : PE 파일 포맷에 대한 정보를 포함. 아래의 두 부분으로 구성
IMAGE_FILE_HEADER : Section의 수 및 속성과 같은 정보 포함
IMAGE_OPTIONAL_HEADER : PE 파일에 대한 속성 또는 이미지 베이스와 같은 정보 포함
Data Directory : 어떤 영역의 Virtual Address와 Size 정보를 포함
IMAGE_SECTION_HEADER : 섹션에 대한 실질적인 정보를 포함
Section(섹션) : 실제 데이터가 위치하는 영역
각 영역에 대해 세부적으로 알아보자.
IMAGE_DOS_HEADER
IMAGE_DOS_HEADER는 PE 실행파일 첫부분에 위치하며 아래와 같이 WinNT.h에 정의되어 있다.
#define IMAGE_DOS_SIGNATURE 0x4D5A // MZ
#define IMAGE_OS2_SIGNATURE 0x4E45 // NE
#define IMAGE_OS2_SIGNATURE_LE 0x4C45 // LE
#define IMAGE_NT_SIGNATURE 0x50450000 // PE00
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // Magic number <MZ>
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
크게 주의해서 볼 부분은 실행파일인지 판단하는데 사용되는 e_magic 부분과 다음에 오는 IMAGE_NT_HEADER의 위치를 표시해 주는 e_lfanew 부분이다. 다른 부분은 크게 중요한 정보를 가지고 있지 않으니 일단 패스~
IMAGE_NT_HEADER
IMAGE_NT_HEADER는 실제 PE 파일 포맷에 대한 정보를 포함하는 헤더로써 IMAGE_FILE_HEADER와 IMAGE_OPTIONAL_HEADER로 구성된다.
다음은 Characteristics에 대한 부분인데 WinNT.h에 아래와 같이 정의되어있다. 역시나 파란색 부분만 보면 될 것 같다.
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
IMAGE_OPTIONAL_HEADER
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Optional Header는 꽤나 중요한 정보를 가지고 있다. 위에서 보면 알 수 있듯이 PE 파일의 전반적이 내용들에 대한 정보를 포함한다. 항목이 꽤나 많은데 중요한 정보만 추리면 아래와 같다.
Magic : Signature로 32Bit의 경우 0x10b를 가짐
SizeOfCode : 섹션 중에 IMAGE_SCN_CNT_CODE 속성을 가진 섹션들 전체의 합
SizeOfInitializedData : 섹션 중에 IMAGE_SCN_CNT_INITIALIZED_DATA 속성을 가진 섹션들 전체의 합
SizeOfUninitializedData : 섹션 중에 IMAGE_SCN_CNT_UNINITIALIZED_DATA 속성을 가진 섹션들 전체의 합
AddressOfEntryPoint : Entry Point의 주소. 실제 로더가 제일 먼저 실행할 코드의 시작점
BaseOfCode : 코드가 시작되는 상대 주소(RVA)
BaseOfData : 데이터가 시작되는 상대 주소(RVA)
ImageBase : 이미지가 로딩되는 메모리의 Base 주소. 일반적으로 실행파일의 경우 0x400000(4Mbyte) 위치에 로딩
SectionAlignment : 섹션이 정렬되는 크기. PE 파일 자체가 메모리 맵 파일이기 때문에 0x1000(4Kbyte) 보다 크거나 같아야 함
SizeOfImage : 모든 섹션들의 합. 이미지 실행을 위해 메모리를 할당해야 하는 총 크기
NumberOfRvaAndSizes : 뒤에 오는 DataDirectory의 개수. 무조건 16개
Data Directory : 총 16개가 있으며 각 항목은 특정 데이터에 대한 정보를 가지고 있음. 뒤에서 설명
이미지 디렉토리 정보는 굉장히 중요하다. 경우에 따라서 섹션이 합쳐질 수 있기 때문에 통합된 섹션에서 원하는 정보를 찾는 방법은 이미지 디렉토리에 포함된 정보를 이용하는 방법 밖에는 없다. 여러모로 많이 쓰이는 인덱스는 아래와 같은 역할을 한다.
IMAGE_DIRECTORY_ENTRY_EXPORT : Export 함수들에 대한 Export Table의 시작 위치와 크기를 나타냄
IMAGE_DIRECTORY_ENTRY_IMPORT : Import 함수들에 대한 Import Table의 시작 위치와 크기를 나타냄
IMAGE_DIRECTORY_ENTRY_RESOURCE : IMAGE_RESOURCE_DIRECTORY 구조체의 시작 위치를 나타냄
IMAGE_DIRECTORY_ENTRY_TLS : Thread Local Storage에 대한 포인터
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG : IMAGE_LOAD_CONFIG_DIRECTORY 구조체애 대한 포인터
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT : IMAGE_BOUND_IMPORT_DESCRIPTOR 구조체의 배열을 가리키는 포인터
IMAGE_DIRECTORY_ENTRY_IAT : Import Address Table의 시작 위치를 나타냄
실제 위의 값중에서 변경하면 OS의 로더에 의해 로딩이 되지 않는 부분이 있는데, 붉은 색으로 표시된 IMAGE_DIRECTORY_ENTRY_TLS와 IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 부분이다. 이 부분은 미리 로더가 읽어서 초기 작업을 실행하는 부분이라 이부분이 포함된 영역을 이상하게 조작하게되면 로더가 로딩에 실패하게 된다(PE파일 암호화 과제를 하면서 온몸으로 느꼈다.. ㅡ_ㅡ;;;). 조작을 하려면 신중히 해야될 듯 싶다.
여기까지 IMAGE_NT_HEADER에 대해서 알아보았다. 일단 지금은 특정영역의 크기와 위치를 표시한다는 정도만 알아놓고 다음으로 넘어가자.
IMAGE_SECTION_HEADER
PE 헤더의 뒷부분에 연속해서 IMAGE_SECTION_HEADER가 위치하게 된다. 섹션은 뒤에 올 코드나 데이터가 위치하는 영역에 대한 구체적인 정보를 포함하고 있으므로 굉장히 중요하다. 섹션의 개수는 앞서 IMAGE_FILE_HEADER에 포함된 NumberOfSections에서 얻을 수 있으며 해당 개수만큼 얻어오면 된다. IMAGE_SECTION_HEADER는 WinNT.h에 아래와 같이 정의되어있다.
SizeOfRawData : VirtualSize의 크기를 IMAGE_OPTIONAL_HEADER에 포함된 FileAlignment의 단위로 올림한 크기
PointerToRawData : 실제 섹션 데이터가 파일 내에 존재하는 오프셋. Virtual Address와 같을 수도 있고 다를 수도 있음
Characteristics : 섹션의 속성 표시. 자세한 것은 뒤를 참조
위의 VirtualSize와 SizeOfRawData는 영역의 크기를 나타낸다는 공통점이 있으나 라운드 업된 크기와 실제 크기를 나타낸다는 차이가 있다. 만약 섹션의 크기를 조작했다면 위의 두부분 모두 손을 봐야 한다.
Virtual Address와 Pointer To Raw Data의 값이 다를 수 있다고 했는데, 왜그럴까? 이것은 실행 파일의 크기를 줄이기 위해서이다. 만약 로드 되었을 때 크기가 0x2000 정도인 섹션이 있다고 하자. 그런데 이 섹션은 메모리의 값이 초기화 될 필요도 없고 값도 들어있지 않다면? 실행 시에 영역만 할당해주면 끝이라면? 이런 경우라면 굳이 이 섹션이 실행파일에서 영역을 가지고 있을 필요가 없다. 따라서 Virtual Address는 0이 아닌 값을 갖겠지만 파일 내에 위치를 의미하는 Pointer To Raw Data의 값은 0이 된다.
즉 실제 파일 내에는 존재하지 않는 영역이 생김으로써 Virtual Address와 Pointer To Raw Data의 값이 달라질 수 있으며, 기타 다른 이유로도 충분히 다를 수 있다. 따라서 실행파일을 조작하기위해서는 Pointer To Raw Data의 값을 위주로 작업을 해야 한다.
Characteristics는 해당 영역의 속성을 나타내는데, WinNT.h에 정의되어있고 아주 흥미로운 값을 가지고 있다.
// IMAGE_SCN_TYPE_REG 0x00000000 // Reserved.
// IMAGE_SCN_TYPE_DSECT 0x00000001 // Reserved.
// IMAGE_SCN_TYPE_NOLOAD 0x00000002 // Reserved.
// IMAGE_SCN_TYPE_GROUP 0x00000004 // Reserved.
#define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved.
// IMAGE_SCN_TYPE_COPY 0x00000010 // Reserved.
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments or some other type of information.
// IMAGE_SCN_TYPE_OVER 0x00000400 // Reserved.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // Section contents comdat.
// 0x00002000 // Reserved.
// IMAGE_SCN_MEM_PROTECTED - Obsolete 0x00004000
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL 0x00008000 // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA 0x00008000
// IMAGE_SCN_MEM_SYSHEAP - Obsolete 0x00010000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
// Unused 0x00F00000
#define IMAGE_SCN_ALIGN_MASK 0x00F00000
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
중요한 플래그 별로 의미를 보면 아래와 같다.
IMAGE_SCN_CNT_CODE : 섹션에 코드가 포함되어있음. IMAGE_SCN_MEM_EXECUTE와 보통 같이 지정됨
IMAGE_SCN_CNT_INITIALIZED_DATA : 섹션이 초기화된 데이터를 포함하고 있음
IMAGE_SCN_CNT_UNINITIALIZED_DATA : 섹션이 초기화 되지 않은 데이터를 포함하고 있음
IMAGE_SCN_MEM_DISCARDABLE : 섹션이 버려질 수 있음. 한번 사용되고 필요없는 섹션들(relocation 데이터 같은 경우)이 이 속성을 가짐
IMAGE_SCN_MEM_SHARED : 섹션이 이 모듈을 사용하는 모든 프로세스에 의해서 공유될 수 있음을 의미
IMAGE_SCN_MEM_EXECUTE : 섹션이 실행 가능함
IMAGE_SCN_MEM_READ : 섹션이 읽기 가능함
IMAGE_SCN_MEM_WRITE : 섹션이 쓰기 가능함
위의 값을 보면 섹션에 대한 속성이 미리 정의되어있다는 것을 알 수 있다. 즉 데이터 섹션 같은 경우 IMAGE_SCN_MEM_READ/WRITE 속성을 가지고 있으리라 유추할 수 있고, 코드가 포함된 섹션의 경우 IMAGE_SCN_MEM_EXECUTE 속성을 가지고 있다고 유추할 수 있다.
섹션의 경우 섹션 이름을 가지고 있는데, VC로 실행파일을 만들면 .text, .data, .idata와 같은 이름의 섹션들이 생긴다. 이름 그대로 코드, 데이터와 같은 정보가 포함된 섹션이라는 것을 알 수 있는데, 여기서 속지 말아야 할 것은 섹션 이름은 권장값이므로 섹션 이름으로 섹션이 포함하는 내용을 판단하면 안된다는 것이다. 특히 파일의 크기를 줄이는 릴리즈 옵션 같은 경우는 섹션들이 합쳐져서 하나의 섹션으로 존재하는 경우도 있기 때문에 섹션 이름을 이용해서 찾아서는 안되며 IMAGE_NT_HEADER에 있는 Data Directory의 값을 참조해서 찾도록 해야 한다.
실제 구현
설명이 굉장히 길었다. 이제 실제로 이 헤더 정보를 분석하는 간단한 코드를 작성해 보자. RVA와 PointerOfRawData의 관계를 생각하면 약간 복잡한데, 이것은 추후에 다시 보도록 하고 헤더 정보만 표시해 보자.
분석하는 클래스를 작성하여 간단히 헤더 정보를 추출하고 이를 화면에 표시하는 테스트 프로그램을 작성하였다.
Most Kernel Mode API functions return NTSTATUS values. To translate these status values to messages by using the FormatMessage API function, you must reference the NtDLL.dll module in the parameter list.
MORE INFORMATION
The following code sample demonstrates how to obtain the system message string.
void DisplayError(DWORD NTStatusMessage) {
LPVOID lpMessageBuffer;
HMODULE Hand = LoadLibrary("NTDLL.DLL");
일단 들어가기 전에 재배치(Relocation)이 무엇인지 알아보자. 재배치는 코드에 특정 값을 더해줘서 다른 메모리 주소에서 실행 가능하게 해주는 것을 말한다. 말 그대로 코드를 다시 배치하는 과정인데, 왜 이런걸 해야 하는 걸까?
EXE 파일의 경우 윈도우에서는 굳이 재배치를 할 필요가 없다. 왜냐하면 로더가 프로그램을 로딩할때 제일 먼저 EXE 파일을 위한 메모리를 할당해주기 때문이다. IMAGE_OPTIONAL_HEADER에 ImageBase라는 필드를 기억할지 모르겠다. 이것이 바로 PE 파일이 로딩될 Base 주소를 의미한다. EXE 파일의 경우 가장 먼저 메모리를 할당 받으므로 ImageBase(일반적으로 0x400000)에 로딩 가능하다.
DLL의 경우는 어떨까? 실행파일이 사용하는 DLL이 어디 한두개일까? 여러개가 로딩이 되면 당연히 그중에 몇몇 DLL은 ImageBase에 로딩하지 못하는 경우도 발생한다. 이때 어쩔 수 없이 다른 메모리 주소에서 실행해야하는데 이 과정을 재배치 과정이라고 하고 재배치 섹션의 정보가 사용되는 것이다.
이제 세부 구조에 대해서 알아보자
재배치 섹션의 시작
재배치 정보는 다른 정보와 마찬가지로 IMAGE_OPTIONAL_HEADER의 Data Directory에서 찾을 수 있고 0x05 인덱스(IMAGE_DIRECTORY_ENTRY_BASERELOC)에서 그 RVA를 구할 수 있다.
재배치 섹션의 처음 시작은 IMAGE_BASE_RELOCATION 구조체로 시작하며 WinNT.h에 아래와 같이 정의되어있다.
SizeOfBlock : 재배치 영역의 크기. IMAGE_BASE_RELOCATION 자신 크기를 포함한 전체 크기
Export 섹션보다 더 간단한 구조를 가진다. 이후에 보면 알겠지만 재배치 데이터 같은 경우 IMAGE_BASE_RELOCATION 구조체 다음에 n개의 WORD 형태로 반복해서 나타나고 그중 하위 0xFFF는 Offset으로 사용된다.
따라서 재배치할 영역이 크고 넓은 경우 IMAGE_BASE_RELOCATION + n개의 WORD의 형태가 반복되어서 나타나게 된다. 마지막은 역시 데이터가 0인가를 이용하여 판단한다.
재배치 정보
그럼 IMAGE_BASE_RELOCATION 이후에 존재하는 n개의 WORD는 어떤식으로 구성될까?
상위 4Bit는 재배치 Type으로 사용되며 하위 12Bit는 Offset으로 사용된다. 따라서 코드를 작성한다면 아래와 같이 쓸 수 있을 것이다.
WORD wData;
printf( "Type %X\n", ( wData & 0xF000 ) >> 12 );
printf( "Offset %X\n", wData & 0xFFF );
Type과 Offset은 아래와 같은 의미를 가진다.
Type : 재배치 정보의 타입. 사실 거의 의미가 없고 0일 경우 패딩 데이터
Offset : 실제 코드가 재배치 될 영역. VirtualAddress와 더해져서 수정해야할 RVA 값이 됨
위에서 보듯 실제 코드가 수정되어야 하는 위치는 IMAGE_BASE_RELOCATION의 VirtualAddress와 Offset을 더한 값이 된다. 정말 간단하다.
여기서 알아두어야 할 점은 Offset이 최대 0xFFF라는 것이다. 즉 4Kbyte까지 커버가 가능하므로 4Kbyte 이상이 되면 다시 IMAGE_BASE_RELOCATION을 만들어서 접근해야 한다.
이것을 그림으로 보면 아래와 같다.
재배치 수행
위에서 메모리에 어디를 재배치 해야하는가에 대한 정보를 알아보았다. 그럼 과연 그 위치에서 무엇을 해야 정상적으로 동작될까? 개요에서 재배치를 수행하는 이유가 모듈의 시작 위치가 이동되기 때문이라고 했다. 그럼 당연히 이동한 거리만큼 값을 더해줘야 정상적으로 수행이 될 것이라는 것을 알 수 있다.
재배치를 수행하는 과정은 아주 간단하다. 아래의 순서대로 수행하면 된다.
재배치가 수행되야 할 곳의 DWORD 값을 읽는다.
읽은 DWORD 값에서 현재 IMAGE_OPTIONAL_HEADER의 ImageBase를 뺀다. 빼는 순간 코드는 0을 기본 Base 주소로 하는 코드로 변한다.
뺀 값에 실제로 모듈이 로딩된 메모리 주소를 더한다. 더하는 순간 코드는 실제 모듈이 로딩된 위치에서 정상적으로 수행가능한 코드로 변한다.
단순한 뺄셈과 덧셈만으로 재배치가 가능하다. 자료 구조가 간단한 만큼 처리 또한 간단하다.
실제 구현
구조가 아주 간단한 만큼 코드도 아주 간단하다. IMAGE_BASE_RELOCATION의 정보가 0일때까지 모든 재배치 정보를 표시한다.
간단히 재배치 섹션에 대해서 알아보았다. 아주 심플한 자료구조와 반복된 작업을 수행하면 코드를 어디서든 실행가능하게 할 수 있다는 놀라운 사실을 알게 됬으니, 이제 실행 코드를 임의의 영역에 메모리를 할당 시키고 실행 시키는 것도 가능 할 것이다(왜 자꾸 말만하면 어둠(??)의 세계 이야기가 나오는지 모르겠다... ㅡ_ㅡ;;;; 이러면 안되는데...).
다음은 기회가 되면 실행파일을 조작해서 특정한 일을 수행하는 내용을 하려 하는데, 약간 민감한 부분이 잇어서 언제가 될지는 모르겠다.
Export 영역은 내가 다른 프로그램을 위한 기능을 제공하기위해 노출한 함수 목록이 들어있다. Import 영역과 비슷한 방법으로 IMAGE_NT_HEADER의 Data Directory를 찾아서 0번째 인덱스(IMAGE_DIRECTORY_ENTRY_EXPORT)를 찾으면 Export 섹션을 구할 수 있다. 역시 RVA를 파일 내의 오프셋(Pointer Of Raw Data)로 바꾸는 작업이 필요하다.
Export 섹션의 시작
Export 섹션의 첫번째는 IMAGE_EXPORT_DIRECTORY 구조체로 되어있으며 WinNT.h에 아래와 같이 정의되어있다.
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
각 항목의 역할은 아래와 같다.
Name : DLL의 이름을 나타내는 ASCII 문자열. RVA 값
Base : 아래에 오는 Address Of Name Ordinals의 시작 서수. AddressOfNameOridianals의 값은 Base의 값을 뺀 형태로 저장.
NumberOfFunctions : AddressOfFunctions가 가리키는 RVA 배열의 수
NumberOfNames : AddressOfNames가 가리키는 RVA 배열의 수
AddressOfFunctions : 함수의 실제 주소가 담긴 RVA 값의 배열. 배열의 인덱스는 아래 AddressOfNameOrdinals에서 구한 값이거나 서수에서 Base 값을 뺀 값
AddressOfNames : 함수의 실제 Ascii 문자열이 담긴 RVA 위치값의 배열
AddressOfNameOrdinals : 함수의 서수(Ordinal)값의 배열 위치. RVA 값이며 배열은 WORD크기. 실제 해당 함수의 서수는 AddressOfNameOrdinals에서 WORD의 값을 얻은 것에 Base의 값을 더해야 함
Import 섹션보다 비교적 직관적이고 간단한 구조로 되어있음을 알 수 있다. 이것을 그림으로 보면 아래와 같다.
Export된 함수 주소 찾기
그럼 함수 이름과 서수를 가지고 어떻게 함수 주소를 찾을 수 있는지 알아보자. 함수의 실제 주소는 AddressOfFunctions 에 저장되어있다. 그럼 해당 함수가 AddressOfFunctions의 어디에 위치하는지 알아야 하는데 함수 이름을 이용해서 찾는 경우와 서수를 이용해서 찾는 경우가 다르다.
1. 함수 이름으로 함수 주소 찾기
함수 이름은 AddressOfNames 필드를 이용해서 검색하면 함수 이름이 어느 인덱스에 위치하는지 알 수 있다. 이 인덱스로 서수가 들어있는 AddressOfNameOrdinals 에 접근하면 해당 함수 이름의 서수가 얼마인지 알 수 있다( 여기서 서수는 실제 서수값에서 Base를 뺀 상대 서수 값이다). 이 상대 서수값을 가지고 AddressOfFunctions 에 접근하면 함수가 존재하는 실 주소를 알 수 있다.
2. 서수로 함수 주소 찾기
서수로 함수 이름을 찾는 방법은 아주 간단하다. AddressOfFunction의 함수 주소 인덱스는 서수에서 Base를 뺀 상대 서수값으로 되어있으므로 그냥 Base를 빼서 AddressOfFunction에 접근하면 된다.
3. 참고 : 모든 함수 주소 찾기
함수 이름과 함수 주소를 모두 찾을려면? NumberOfNames 필드 값을 이용해서 해당 Name의 스트링과 서수값을 구하고 그것으로 AddressOfFunctions에 접근하면 모두 구할 수 있다.
아래는 위 3번 방식을 이용해서 간단히 Export된 함수를 Enumeration하는 소스코드이다.
// Ordinals를 돌면서 실제 함수 이름과 주소를 찍는다.
for( i = 0 ; i < dwNumberOfNameOrdinal ; i++ )
{
printf( "--- Export Function[%d] ---\n", i );
// 함수 이름
if( pdwNameRVATable[ i ] != 0 )
{
dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA( pdwNameRVATable[ i ] );
// 함수의 서수
printf( "Ordinal 0x%X [0x%X]\n", pwOrdinalTable[ i ],
pwOrdinalTable[ i ] + dwOrdinalBase );
// 함수의 주소
printf( "Function Address 0x%X\n", pdwAddressRVATable[
pwOrdinalTable[ i ] ] );
}
}
실제 구현
이제 Export 섹션을 출력해 보자. Import 섹션보다 간단하므로 출력하는데 큰 어려움이 없을 것이다.
이 코드를 가지고 DLL을 출력해 보면 실제 함수가 있는 Address의 주소가 다른 것들이 보일지도 모른다. 이것은 해당 역할을 하는 함수가 다른 DLL이나 EXE의 함수로 포워드(Forward) 된 것으로 함수의 주소가 text 섹션이 아닌 특정 다른 섹션을 가리킨다. 이 값은 Forward된 dll.Function 형태의 스트링 주소를 가리키고 실제 표시를 해보면 그렇다는 것을 알 수 있을 것이다. 이러한 경우가 발생하면 위의 Function Address를 출력하는 부분을 조금 수정하여 스트링을 찍도록 하면 된다.
이제 Import/Export에 대해서 모두 알아보았다. 지금까지 문서를 통해 DLL이나 EXE가 어떻게 다른 DLL이나 EXE의 함수를 호출하고 어떻게 제공하는지 대략적인 감을 잡을 것이라 생각한다. 이를 잘 생각하면 어떻게 내가 원하는 함수만 노출하고 다른 함수들은 노출을 줄일 수 있는가에 대한 부분도 어느정도 감이 올 것이다.
지난번에 PE 파일의 헤더구조에 대해서 알아보았다. 잘 기억이 안나면 01 PE 파일 분석-헤더분석 문서를 다시 보자. 이번에 분석할 부분은 PE 파일에 포함된 Import 영역이다.
Import 영역은 PE 파일이 실행될 때 외부로부터 당겨와서 사용하는 함수들의 목록이 포함되어있으며, Export 영역은 다른 모듈이 사용할 수 있게 노출해 놓은 함수들이 들어있다.
일단 Import 영역부터 살펴보자.
Import 영역 분석
Import 영역은 PE 파일내의 특정 섹션에 자리잡고 있으며, IMAGE_OPTIONAL_HEADER의 DataDirectory에서 위치를 찾을 수 있다. DataDirectory내의 인덱스는 0x01이며 IMAGE_DIRECTORY_ENTRY_IMPORT 매크로로 WinNT.h에 정의되어있다. Data Directory가 포함하는 정보는 해당 위치의 RVA 값과 크기 정보 이다.
분석하는 데이터는 실제 파일의 형태로 되어있으므로, 여기서 부터는 섹션의 RVA와 PointerOfRawData의 값을 이용해서 찾아야 한다. RVA 값을 이용해서 어떻게 실제 섹션 데이터가 있는 파일 내의 위치를 찾을까? 답은 섹션 정보에있는 RVA값과 Virtual Size 그리고 PointerOfRawData의 값을 이용하는 것이다.
그럼 함수를 하나 만들자. 함수의 원형은 아래와 같이 RVA를 넣어주면 실제 파일에서 존재하는 위치를 계산해 주는 것이다.
DWORD GetPointerOfRawDataFromRVA( DWORD dwRVA );
저 함수를 이용해서 실제 섹션내에 존재하는 Import 섹션의 위치를 찾아보자. 어떻게 해야 할까? Import 섹션은 IMAGE_OPTIONAL_HEADER의 Data Directory[ IMAGE_DIRECTORY_ENTRY_IMPORT ]에서 찾을 수 있다고 했다. Virtual Address를 위 함수에 넣으면 실제 파일내의 Offset을 구할 수 있다.
Import 섹션은 IMAGE_IMPORT_DESCRIPTOR 구조체로 시작한다. 해당 구조체의 배열 형태로 구성되며 마지막은 NULL 구조체로 되어있어 끝을 표시한다. 아래는 WinNT.h에 정의된 내용이다.
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) };
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date\time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
중요 항목에 대한 설명은 아래와 같다
OriginalFirstThunk : IMAGE_THUNK_DATA 구조체 RVA 주소를 가리킴. 이때 IMAGE_THUNK_DATA는 Import하는 함수 이름이나 서수(Ordinal)를 포함하는 구조체 IMAGE_IMPORT_BY_NAME을 가리킴
Name : Import한 DLL의 이름을 담고 있는 ASCII 문자열의 RVA 주소
FirstThunk : OriginalFirstThunk와 같이 IMAGE_THUNK_DATA 구조체의 RVA 주소를 가리키지만 PE 파일이 메모리에 맵핑되고 나면 Import 한 DLL 내의 함수 주소가지고 있는 IMAGE_THUNK_DATA를 가리킴
위의 항목을 보면 Import 섹션에 외부로 부터 당겨쓰는 DLL이나 함수 정보를 모두 포함하고 있음을 알 수 있다. 그럼 OriginalFirstThunk 및 FirstThunk가 가리키는 구조체인 IMAGE_THUNK_DATA의 구조는 어떻게 되어있을까?
WinNT.h에서 IMAGE_THUNK_DATA에 대한 구조체를 보면 아래와 같이 단순한 공용체임을 알 수 있다.
Hint : Import시 서수를 사용하지 않으면 임의로 설정되는 0 base의 숫자, Import 한 함수가 추가될때마다 1씩 증가. 별로 중요한 값은 아닌듯...
Name : NULL로 끝나는 Ascii 문자열. 실제 함수 이름을 포함
굉장히 복잡한 구조체들이 얼기설기 엮여있는데, 이것을 메모리 로딩 전의 PE 파일과 메모리 로딩 후의 PE 파일을 비교해서 그림으로 보면 좀 간단하다.
<메모리에 로딩 전 PE 파일 구조>
<메모리 로딩 후 PE 파일의 구조>
Import 섹션을 찾는 순서는 아래와 같다(위 그림을 참조하면서 같이보자).
Data Directory 또는 섹션 헤더에서 Import 섹션의 시작을 찾는다.
시작부분은 IMAGE_IMPORT_DESCRIPTOR의 구조체로 시작하고 n개가 존재한다. 끝은 구조체의 모든 데이터가 0이면 끝이다.
Name의 RVA를 따라가서 DLL 이름을 뽑는다.
Original First Thunk 또는 First Thunk의 RVA를 따라가서 IMAGE_THUNK_DATA의 구조체 배열에 접근한다. IMAGE_IMPORT_DESCRIPTOR와 마찬가지로 n개가 존재하며 마지막은 구조체의 모든 데이터가 0인지로 판단한다.
IMAGE_THUNK_DATA의 필드 값에 따라 파일명으로 함수를 Import 했는지 서수(Ordinal)로 Import 했는지 판단하여 해당 정보를 뽑는다.
실제 구현
자 이로써 Import 섹션을 분석할 수 있는 모든 조건이 갖추어졌다. 실제 위에서 나온 구조체를 바탕으로 Import 섹션을 한번 분석해 보자.
주의 할 것은 Borland 사의 링커의 경우 Original First Thunk를 아예 사용하지 않는 다는 것이다. 다행이도 First Thunk 항목은 다 연결되어있으니 First Thunk를 이용해서 덤프하는 것으로 하자. 사실 정확하게 구현을 하려면 Forwarder String까지 해야 하나 예가 드물어서 서수 아니면 함수명이라 가정하고 프로그램을 작성하였다. 혹 문제가 발생하면 ForwarderString 쪽을 처리하면 될 것 같다.
지금까지 PE 파일에서 Import 한 함수들을 분석하는 방법을 알아보았다. Import 섹션은 보안 분야에서 상당히 중요하게 다뤄지고 있는 부분이다. 고전적인 루트킷 같은 경우, 실행 중인 프로세스에 DLL을 삽입하여 Import 섹션의 함수를 후킹해 정보를 빼내는 기능을 하였고, 실행 중인 프로세스에 Import 섹션의 함수를 모니터링 함으로써 후킹을 감시하는 기능도 할 수 있다.
물론 명시적 로딩(Explicit Loading) 즉 LoadLibrary() 같은 함수를 이용하여 DLL을 로딩하고 함수를 사용하는 경우는 Import 섹션에 데이터가 그리 많이 존재하지 않겠지만 명시적 로딩을 사용하더라도 반드시 2개의 함수 즉 LoadLibrary(), GetProcAddress() 와 같은 함수는 필요하기 때문에 Import 섹션을 분석하는 것은 의미가 있다.