개요
지난번에 PE 파일의 헤더구조 및 Import 섹션에 대해서 알아보았다. 잘 기억이 안나면 01 PE 파일 분석-헤더분석 문서 및 02 PE 파일 분석-Import 분석 을 다시 보자. 이번에 분석할 부분은 PE 파일에 포함된 Export 영역이다.
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하는 소스코드이다.
- /**
Export 섹션을 표시한다.
*/
void DumpExportSection( CPEAnalyzer* pclAnalyzer )
{
PIMAGE_EXPORT_DIRECTORY pstImageExportDirectory;
PIMAGE_NT_HEADERS pstImageNtHeader;
DWORD dwFileMappedAddress;
DWORD dwRVA;
DWORD dwFileOffset;
DWORD dwNumberOfNameOrdinal;
DWORD dwOrdinalBase;
DWORD* pdwAddressRVATable;
DWORD* pdwNameRVATable;
WORD* pwOrdinalTable;
DWORD i; - pstImageNtHeader = pclAnalyzer->GetImageNtHeaders();
- // Import Section에 대해서 실제 파일 내의 Offset을 얻는다.
dwRVA = pstImageNtHeader->OptionalHeader.
DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ].VirtualAddress;
if( dwRVA == 0 )
{
printf( "\n=== No Export Section ===\n" );
return ;
} - dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA( dwRVA );
dwFileMappedAddress = ( DWORD ) pclAnalyzer->GetMappingAddress();
pstImageExportDirectory = ( PIMAGE_EXPORT_DIRECTORY )(
dwFileMappedAddress + dwFileOffset ); - printf( "\n==== IMAGE_EXPORT_DIRECTORY ====\n" );
- // 실제 Name String을 출력한다.
dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA( pstImageExportDirectory->Name );
printf( "Characteristics 0x%X\n", pstImageExportDirectory->Characteristics );
printf( "TimeDateStamp 0x%X\n", pstImageExportDirectory->TimeDateStamp );
printf( "MajorVersion 0x%X\n", pstImageExportDirectory->MajorVersion );
printf( "MinorVersion 0x%X\n", pstImageExportDirectory->MinorVersion );
printf( "Name 0x%X [%s]\n", pstImageExportDirectory->Name,
dwFileMappedAddress + dwFileOffset );
printf( "Base 0x%X\n", pstImageExportDirectory->Base );
printf( "NumberOfFunctions 0x%X\n", pstImageExportDirectory->NumberOfFunctions );
printf( "NumberOfNames 0x%X\n", pstImageExportDirectory->NumberOfNames );
printf( "AddressOfNameOrdinals 0x%X\n", pstImageExportDirectory->AddressOfNameOrdinals );
// 모두 다 출력할 준비를 한다.
dwNumberOfNameOrdinal = pstImageExportDirectory->NumberOfNames;
dwOrdinalBase = pstImageExportDirectory->Base; - dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA(
pstImageExportDirectory->AddressOfFunctions );
pdwAddressRVATable = ( DWORD* ) ( dwFileMappedAddress + dwFileOffset ); - dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA(
pstImageExportDirectory->AddressOfNameOrdinals );
pwOrdinalTable = ( WORD* ) ( dwFileMappedAddress + dwFileOffset ); - dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA(
pstImageExportDirectory->AddressOfNames );
pdwNameRVATable = ( DWORD* ) ( dwFileMappedAddress + dwFileOffset ); - // Ordinals를 돌면서 실제 함수 이름과 주소를 찍는다.
for( i = 0 ; i < dwNumberOfNameOrdinal ; i++ )
{
printf( "--- Export Function[%d] ---\n", i );
// 함수 이름
if( pdwNameRVATable[ i ] != 0 )
{
dwFileOffset = pclAnalyzer->GetPointerOfRawDataFromRVA( pdwNameRVATable[ i ] ); - printf( "Name 0x%X [%s]\n", pdwNameRVATable[ i ],
( dwFileMappedAddress + dwFileOffset ) );
}
else
{
printf( "Name 0x%X []\n", 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를 출력하는 부분을 조금 수정하여 스트링을 찍도록 하면 된다.
- PEAnalyzer.h : PE 파일을 분석하는 클래스의 헤더 파일
- PEAnalyzer.cpp : PE 파일을 분석하는 클래스의 소스 파일
- main.cpp : 실제 사용하는 예제
- PEAnalyzer.exe : 실행 프로그램
아래는 실행한 결과이다.
마치면서...
이제 Import/Export에 대해서 모두 알아보았다. 지금까지 문서를 통해 DLL이나 EXE가 어떻게 다른 DLL이나 EXE의 함수를 호출하고 어떻게 제공하는지 대략적인 감을 잡을 것이라 생각한다. 이를 잘 생각하면 어떻게 내가 원하는 함수만 노출하고 다른 함수들은 노출을 줄일 수 있는가에 대한 부분도 어느정도 감이 올 것이다.
다음은 DLL에서는 빠지지 않는 재배치(Relocation) 섹션에 대해서 알아보자.
출처(http://kkamagui.tistory.com, http://kkamagui.springnote.com)