[cpp] API 세트 DLL 이름 바인딩하기

@codemaru · December 17, 2015 · 7 min read

윈도우 7이 등장하면서 kernel32.dll을 살펴보면 이상한 부분이 하나 생겼습니다. 바로 IAT 부분인데요. api-ms-win-core-console-l1-1-0.dll 같은 이상한 DLL로 부터 우리가 원래 익히 알고 있는 API들을 임포트 하고 있는 것이죠. 그런데 api-ms-win-core-console-l1-1-0.dll 같은 것에 실제 코드가 포함돼 있냐하면 그것도 아닙니다. 헐~ 멍밍하게 되는데요. 그 당시에 발견한 문서에서 기본적인 메커니즘을 알게 되어서 해당 문서에 나와 있는 DLL 이름들만 하드 코딩해서 사용을 했었습니다.

이후에 윈도우 8이 나왔고, 8.1이 출시됐고, 윈도우 10이 나오고, 이제는 윈도우 10 TH2까지 나왔습니다. 저 문서에 있는 내용 만으로 뭔가가 정상 동작하기에는 뭔가 한참 모자랄만큼 운영체제가 많이 나왔죠. 어제 해킹툴을 테스트하다가 코드가 정상적으로 동작하지 않는 것을 발견했습니다. 바로 위에서 우리가 하드 코딩한 DLL 말고도 온갖 요상한 DLL이 많이 생긴게 원인이었습니다. 그래서 이 많은 DLL 이름을 어쩌지 하는 찰나에 관련 설명을 담고 있는 MSDN 문서를 발견하게 되었습니다. 목록이 엄청 길죠. API 세트에 사용되는 온갖 DLL 이름이 다 나와있습니다. 이 테이블을 기초로 DLL 이름을 매핑해주는 함수를 확장시켰더니 마법같이 잘 동작했습니다. 역시 MSDN ㅋㅋ~ 그런데 뭔가 좀 아쉽죠. 모두 입력 시켜서 동작하게는 만들었지만 도대체 이 이름들은 어디서 오며 운영체제는 어떻게 그걸 매핑시키는지 궁금한거죠~ 은막 뒤의 비밀이 궁금하다랄까요.

자동으로 될 일은 없고, 운영체제도 어디에선가는 매핑을 시킬텐데 그 자료를 알게 되면 굳이 이걸 일일이 입력하고 있지 않아도 되지 않을까라는 생각이었습니다. 그 생각으로 오늘 LoadLibrary 코드를 좀 살펴봤는데요. 비밀을 알아냈습니다. LoadLibrary 내부에서 PEB에 저장된 ApiSetMap이란 자료 구조를 참조해서 DLL 이름을 리다이렉트 시키고 있었습니다. 풉. 역시 마법은 없었습니다. ApiSetMap이란 걸 알게 되었으니 관련 키워드로 검색을 해봤습니다. 자료가 많지는 않은데 친절하게 리버싱해 둔 자료가 있었습니다.

전체적인 메커니즘은 런타임 DLL 이름 해석 Part 1, Part 2에 잘 나와 있습니다. 관련 자료를 토대로 만든 실제 코드는 위키 글을 참고하면 됩니다.

간략하게 전체 구조를 리뷰해보면 모든 데이터의 근간은 apisetschema.dll에 있습니다. 윈도우는 부팅할 때 그 DLL의 .apiset 섹션 데이터를 토대로 메모리에 API 세트 이름 매핑 자료를 로드합니다. 그리고 이후 프로세스가 생성되면 PEB의 ApiSetMap에다가 해당 자료를 참조할 수 있도록 데이터를 매핑시켜줍니다. 이제 LoadLibrary를 호출하면 PEB의 ApiSetMap을 뒤져서 매칭되는 DLL이면 이름을 리다이렉트 시켜주는 것이죠. 결국 하드코딩한 테이블을 사용할 것이 아니라면 ApiSetMap을 어떻게 뒤지는지 알아야 합니다.

앞선 위키 글의 경우 윈도우 7을 기초로 작성해서 제가 사용하는 윈도우 8.1에서는 동작하지 않는 문제가 있었습니다. 그래서 윈도우 8.1에서 동작하도록 코드를 한 번 만들어 보았습니다. 런타임 DLL 이름과 매핑되는 실제 DLL 이름을 출력해 줍니다. 위키 글에 있는 코드는 ApiSetMap 구조체 버전이 2인 환경입니다. 아래는 버전이 4인 환경일 때의 구조체 정보랍니다. 윈도우 10은 또 업그레이드가 되었겠죠?

덧) 현재까지 알려진 ApiSetMap 자료 구조 버전은 총 세 개입니다. 윈도우 7에 포함된 구조가 2버전이고, 윈도우 8에 포함된 구조가 4버전입니다. 윈도우 10에는 6버전 구조로 변경이 되었네요. 아래 코드는 윈도우 8의 ApiSetMap 버전 4를 출력하는 기능을 합니다. 당연히 2나 6에서는 정상 동작하지 않습니다.

#include <windows.h>
#include <stdio.h>

#define ADD_PTR(a, b) ((PVOID)((ULONG_PTR)(a) + (ULONG_PTR)(b)))

#pragma pack(1)

typedef struct _API_SET_MAP_DATA_ENTRY
{
	ULONG flags;
	ULONG name_offset;
	ULONG name_length;
	ULONG value_offset;
	ULONG value_length;
} API_SET_MAP_DATA_ENTRY, *PAPI_SET_MAP_DATA_ENTRY;


typedef struct _API_SET_MAP_DATA
{
	ULONG flags;
	ULONG count;
	API_SET_MAP_DATA_ENTRY entries[ANYSIZE_ARRAY];
} API_SET_MAP_DATA, *PAPI_SET_MAP_DATA;


typedef struct _API_SET_MAP_ENTRY
{
	ULONG flags;
	ULONG name_offset;
	ULONG name_length;
	ULONG alias_offset;
	ULONG alias_length;
	ULONG data_offset;
} API_SET_MAP_ENTRY, *PAPI_SET_MAP_ENTRY;



typedef struct _API_SET_MAP
{
	ULONG version;
	ULONG size;
	ULONG flags;
	ULONG count;
	API_SET_MAP_ENTRY entries[ANYSIZE_ARRAY];
} API_SET_MAP, *PAPI_SET_MAP;


#pragma pack()

int main()
{
	PVOID name_ptr;
	ULONG_PTR api_set_map;

	PAPI_SET_MAP pasm;
	PAPI_SET_MAP_DATA pasmd;

	WCHAR name[MAX_PATH];

	api_set_map = __readfsdword(0x30);
	api_set_map = *(ULONG_PTR *)(api_set_map + 0x38);


	pasm = (PAPI_SET_MAP) api_set_map;

	for(ULONG i = 0; i < pasm->count; ++i)
	{
		name_ptr = ADD_PTR(pasm, pasm->entries[i].name_offset);

		memset(name, 0, sizeof(name));
		memcpy(name, name_ptr, pasm->entries[i].name_length);

		pasmd = (PAPI_SET_MAP_DATA) ADD_PTR(pasm, pasm->entries[i].data_offset);

		printf("%ls => [%d]", name, pasmd->count);

		for(ULONG j = 0; j < pasmd->count; ++j)
		{
			name_ptr = ADD_PTR(pasm,  pasmd->entries[j].value_offset);

			memset(name, 0, sizeof(name));
			memcpy(name, name_ptr, pasmd->entries[j].value_length);

			printf(" %ls", name);
		}

		printf("\n");

	}

	return 0;
}
@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중