Maldev Academy Part6

목차


    51. 문자열 해싱

    문자열 해싱

    소개

    해싱은 해시값 또는 해시코드라고 하는 데이터의 고정된 크기 표현을 만드는 데 사용되는 기술입니다. 해시 알고리즘은 단방향 함수로 설계되었기 때문에 해시값을 사용하여 원래 입력 데이터를 확인하는 것은 계산적으로 불가능합니다. 해시 코드는 일반적으로 크기가 더 짧고 작업 속도가 빠릅니다. 문자열을 비교할 때 해싱을 사용하면 문자열 자체를 비교하는 것보다, 특히 문자열이 긴 경우 두 문자열이 동일한지 빠르게 확인할 수 있습니다.

    악성코드 개발의 맥락에서 문자열 해싱은 보안 공급업체가 악성 바이너리를 탐지하는 데 도움이 되는 시그니처로 문자열을 사용할 수 있기 때문에 구현에 사용된 문자열을 숨기는 데 유용한 접근 방식입니다.

    문자열 해싱

    이 모듈에서는 몇 가지 문자열 해싱 알고리즘을 소개합니다. 이러한 알고리즘의 출력은 16진수 형식으로 표현되는 숫자가 더 깔끔하고 간결하다는 점을 이해하는 것이 중요합니다. 이 모듈에서는 다음과 같은 문자열 해싱 알고리즘에 대해 설명합니다.

    • Dbj2
    • JenkinsOneAtATime32Bit
    • LoseLose
    • Rotr32

    이 모듈에서 설명한 것보다 더 많은 문자열 해싱 알고리즘을 사용할 수 있으며, 그 중 일부는 VX-API GitHub 리포지토리에서 찾을 수 있습니다.

    Djb2

    Djb2는 간단하고 빠른 해싱 알고리즘으로, 주로 문자열의 해시값을 생성하는 데 사용되지만 다른 유형의 데이터에도 적용할 수 있습니다. 입력 문자열의 문자를 반복하고 각 문자를 사용하여 아래 스니펫에 설명된 특정 알고리즘에 따라 실행 중인 해시값을 업데이트하는 방식으로 작동합니다.

    hash = ((hash << 5) + hash) + c

    해시는 현재 해시 값, c는 입력 문자열의 현재 문자, <<는 비트 단위 왼쪽 시프트 연산자입니다.

    결과 해시값은 입력 문자열에 고유한 양의 정수입니다. Djb2는 해시 값의 분포가 양호하여 서로 다른 문자열과 각각의 해시 값이 충돌할 확률이 낮은 것으로 알려져 있습니다.

    아래 표시된 Djb2 구현은 VX-API GitHub 리포지토리에서 가져온 것입니다.

    #define INITIAL_HASH	3731  // added to randomize the hash#define INITIAL_SEED	7     // generate Djb2 hashes from Ascii input string
    DWORD HashStringDjb2A(_In_ PCHAR String)
    {
    	ULONG Hash = INITIAL_HASH;
    	INT c;
    
    	while (c = *String++)
    		Hash = ((Hash << INITIAL_SEED) + Hash) + c;
    
    	return Hash;
    }
    
    // generate Djb2 hashes from wide-character input string
    DWORD HashStringDjb2W(_In_ PWCHAR String)
    {
    	ULONG Hash = INITIAL_HASH;
    	INT c;
    
    	while (c = *String++)
    		Hash = ((Hash << INITIAL_SEED) + Hash) + c;
    
    	return Hash;
    }

    JenkinsOneAtATime32Bit

    JenkinsOneAtATime32Bit 알고리즘은 입력 문자열의 문자를 반복하고 각 문자의 값에 따라 실행 중인 해시값을 점진적으로 업데이트하는 방식으로 작동합니다. 해시값을 업데이트하는 알고리즘은 아래 스니펫에 설명되어 있습니다.

    hash += c;
    hash += (hash << 10);
    hash ^= (hash >> 6);

    해시는 현재 해시 값이고 c는 입력 문자열의 현재 문자입니다.

    결과 해시 값은 입력 문자열에 고유한 32비트 정수입니다. JenkinsOneAtATime32Bit은 비교적 양호한 해시 값 분포를 생성하는 것으로 알려져 있어 서로 다른 문자열과 해당 해시 값 간의 충돌 확률이 낮습니다.

    아래 표시된 JenkinsOneAtATime32Bit 구현은 VX-API GitHub 리포지토리에서 가져온 것입니다.

    #define INITIAL_SEED	7	// Generate JenkinsOneAtATime32Bit hashes from Ascii input string
    UINT32 HashStringJenkinsOneAtATime32BitA(_In_ PCHAR String)
    {
    	SIZE_T Index = 0;
    	UINT32 Hash = 0;
    	SIZE_T Length = lstrlenA(String);
    
    	while (Index != Length)
    	{
    		Hash += String[Index++];
    		Hash += Hash << INITIAL_SEED;
    		Hash ^= Hash >> 6;
    	}
    
    	Hash += Hash << 3;
    	Hash ^= Hash >> 11;
    	Hash += Hash << 15;
    
    	return Hash;
    }
    
    // Generate JenkinsOneAtATime32Bit hashes from wide-character input string
    UINT32 HashStringJenkinsOneAtATime32BitW(_In_ PWCHAR String)
    {
    	SIZE_T Index = 0;
    	UINT32 Hash = 0;
    	SIZE_T Length = lstrlenW(String);
    
    	while (Index != Length)
    	{
    		Hash += String[Index++];
    		Hash += Hash << INITIAL_SEED;
    		Hash ^= Hash >> 6;
    	}
    
    	Hash += Hash << 3;
    	Hash ^= Hash >> 11;
    	Hash += Hash << 15;
    
    	return Hash;
    }
    

    LoseLose

    LoseLose 알고리즘은 문자열의 각 문자를 반복하고 각 문자의 ASCII 값을 합산하여 입력 문자열의 해시값을 계산합니다. 해시값을 업데이트하는 알고리즘은 아래 스니펫에 설명되어 있습니다.

    hash = 0;
    hash += c; // For each character c in the input string perform

    LoseLose 알고리즘의 결과인 해시값은 입력 문자열에 고유한 정수입니다. 하지만 해시값의 분포가 균일하지 않아 충돌이 발생할 가능성이 높습니다. 이 문제를 해결하기 위해 아래와 같이 알고리즘의 공식이 업데이트되었습니다.

    hash = 0;
    hash += c; // For each character c in the input string
    hash *= c + 2;  // For more randomization

    그렇다고 해서 좋은 해싱 알고리즘이라고 할 수는 없지만 어느 정도 개선된 알고리즘입니다. 아래 표시된 LoseLose 구현은 VX-API GitHub 리포지토리에서 가져온 것입니다.

    #define INITIAL_SEED	2// Generate LoseLose hashes from ASCII input string
    DWORD HashStringLoseLoseA(_In_ PCHAR String)
    {
    	ULONG Hash = 0;
    	INT c;
    
    	while (c = *String++) {
    		Hash += c;
    		Hash *= c + INITIAL_SEED;	// update
    	}
    	return Hash;
    }
    
    // Generate LoseLose hashes from wide-character input string
    DWORD HashStringLoseLoseW(_In_ PWCHAR String)
    {
    	ULONG Hash = 0;
    	INT c;
    
    	while (c = *String++) {
    		Hash += c;
    		Hash *= c + INITIAL_SEED;	// update
    	}
    
    	return Hash;
    }
    

    Rotr32

    Rotr32 문자열 해시 알고리즘은 입력 문자열에서 반복된 문자를 사용하여 ASCII 값을 합산한 다음 현재 해시 값에 비트 단위 회전을 적용합니다. 입력 값과 카운트(카운트는 INITIAL_SEED)를 사용하여 값에 오른쪽 시프트를 수행한 다음, 원래 값에 카운트의 음수만큼 왼쪽 시프트된 값으로 OR 연산합니다.

    결과 해시값은 입력 문자열에 고유한 32비트 정수입니다. Rotr32는 비교적 양호한 해시값 분포를 생성하는 것으로 알려져 있어 서로 다른 문자열과 각각의 해시값이 충돌할 확률이 낮습니다.

    아래 표시된 Rotr32 구현은 VX-API GitHub 리포지토리에서 가져온 것입니다.

    #define INITIAL_SEED	5	// Helper function that apply the bitwise rotation
    UINT32 HashStringRotr32Sub(UINT32 Value, UINT Count)
    {
    	DWORD Mask = (CHAR_BIT * sizeof(Value) - 1);
    	Count &= Mask;
    #pragma warning( push )#pragma warning( disable : 4146)return (Value >> Count) | (Value << ((-Count) & Mask));
    #pragma warning( pop ) }
    
    // Generate Rotr32 hashes from Ascii input string
    INT HashStringRotr32A(_In_ PCHAR String)
    {
    	INT Value = 0;
    
    	for (INT Index = 0; Index < lstrlenA(String); Index++)
    		Value = String[Index] + HashStringRotr32Sub(Value, INITIAL_SEED);
    
    	return Value;
    }
    
    // Generate Rotr32 hashes from wide-character input string
    INT HashStringRotr32W(_In_ PWCHAR String)
    {
    	INT Value = 0;
    
    	for (INT Index = 0; Index < lstrlenW(String); Index++)
    		Value = String[Index] + HashStringRotr32Sub(Value, INITIAL_SEED);
    
    	return Value;
    }

    문자열 스택

    C/C++ 프로그래밍 언어에서는 문자열을 문자 배열로 표현하여 문자를 서로 구분할 수 있으므로 문자열 기반 탐지를 회피하는 데 도움이 됩니다. 예를 들어, 문자열 “hello world”는 아래의 배열로 표현할 수 있습니다.

    	char string[] = { 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0' };

    HxD 바이너리 편집기를 사용하여 “hello world” 문자열을 검색하면 아무것도 반환되지 않습니다.

    그러나 스택 문자열은 일부 디버거 및 리버스 엔지니어링 도구에서 문자열을 감지하는 플러그인을 포함할 수 있으므로 문자열을 숨기는 데는 충분하지 않습니다.

    데모

    이 모듈에서 언급된 알고리즘을 사용하여 “MaldevAcademy” 문자열이 아래에 해시됩니다. 이 문자열은 ASCII 형식과 와이드 형식 모두에서 해시됩니다. 해시 알고리즘에 따라 ASCII 형식과 와이드 형식이 항상 동일한 해시값을 생성하지 않을 수 있다는 점에 유의하세요.


    52. IAT 숨김 및 난독화 – 소개

    IAT 숨김 및 난독화 – 소개

    소개

    IAT(가져오기 주소 테이블)에는 사용된 함수 및 내보내는 DLL 등 PE 파일에 관한 정보가 포함되어 있습니다. 이러한 유형의 정보는 바이너리를 서명하고 감지하는 데 사용할 수 있습니다.

    예를 들어, 아래 이미지는 프로세스 주입 – 셸코드 모듈에서 바이너리의 가져오기 주소 테이블을 보여줍니다. PE 파일은 매우 의심스러운 것으로 간주되는 함수를 가져옵니다. 그러면 보안 솔루션은 이 정보를 사용하여 구현에 플래그를 지정할 수 있습니다.

    나머지 함수의 대부분은 컴파일러에 의해 추가되었으며 향후 모듈에서 다룰 예정입니다.

    IAT 숨김 및 난독화 – 방법 1

    IAT에서 함수를 숨기려면 런타임 중에 GetProcAddressGetModuleHandle 또는 LoadLibrary를 사용하여 이러한 함수를 동적으로 로드할 수 있습니다. 아래 코드조각은 VirtualAllocEx를 동적으로 로드하므로 검사 시 IAT에 표시되지 않습니다.

    typedef LPVOID (WINAPI* fnVirtualAllocEx)(HANDLE hProcess, LPVOID lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect);
    
    //...
    fnVirtualAllocEx pVirtualAllocEx = GetProcAddress(GetModuleHandleA("KERNEL32.DLL"), "VirtualAllocEx");
    pVirtualAllocEx(...);

    이는 우아한 해결책으로 보일 수 있지만, 몇 가지 이유로 좋은 해결책은 아닙니다:

    • 첫째, 함수 사용을 감지하는 데 사용할 수 있는 VirtualAllocEx 문자열이 바이너리에 존재합니다.
    • GetProcAddress와 GetModuleHandleA는 그 자체로 서명으로 사용되는 IAT에 나타납니다.

    IAT 숨김 및 난독화 – 방법 2

    보다 우아한 해결책은 GetProcAddress 및 GetModuleHandle WinAPI와 동일한 작업을 수행하는 사용자 지정 함수를 만드는 것입니다. 이렇게 하면 이 두 함수를 IAT에 표시하지 않고도 함수를 동적으로 로드할 수 있습니다. 다음 모듈에서는 이 솔루션에 대해 더 자세히 설명하겠습니다.


    53. IAT 숨김 및 난독화 – 사용자 지정 GetProcAddress

    IAT 숨김 및 난독화 – 사용자 지정 GetProcAddress

    소개

    GetProcAddress WinAPI는 지정된 모듈 핸들에서 내보낸 함수의 주소를 검색합니다. 지정된 모듈 핸들에서 함수 이름을 찾을 수 없는 경우 이 함수는 NULL을 반환합니다.

    이 모듈에서는 GetProcAddress를 대체하는 함수가 구현됩니다. 새 함수의 프로토타입은 아래와 같습니다.

    FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {}

    GetProcAddress 작동 방식

    가장 먼저 해결해야 할 사항은 GetProcAddress WinAPI가 함수의 주소를 찾고 검색하는 방법입니다.

    hModule 매개변수는 로드된 DLL의 기본 주소입니다. 이 주소는 프로세스의 주소 공간에서 DLL 모듈을 찾을 수 있는 주소입니다. 따라서 함수 주소 검색은 제공된 DLL 내에서 내보낸 함수를 반복하여 대상 함수의 이름이 존재하는지 확인하는 방식으로 이루어집니다. 유효한 일치 항목이 있으면 주소를 검색합니다.

    내보낸 함수에 액세스하려면 DLL의 내보내기 테이블에 액세스하여 대상 함수 이름을 검색하여 반복해야 합니다.

    불러오기 – 테이블 구조 내보내기

    PE 헤더 구문 분석 모듈에서 내보내기 테이블이 IMAGE_EXPORT_DIRECTORY로 정의된 구조라고 언급한 것을 기억하세요.

    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;

    이 모듈에 대한 이 구조의 관련 멤버는 마지막 세 개입니다.

    • AddressOfFunctions – 내보낸 함수의 주소 배열의 주소를 지정합니다.
    • AddressOfNames – 내보낸 함수 이름의 주소 배열의 주소를 지정합니다.
    • AddressOfNameOrdinals – 내보낸 함수에 대한 서수 배열의 주소를 지정합니다.

    리콜 – 내보내기 테이블에 액세스

    내보내기 디렉터리인 IMAGE_EXPORT_DIRECTORY를 검색하는 방법을 기억해 봅시다. 아래 코드 조각은 PE 헤더 파싱 모듈에서 설명했으므로 익숙할 것입니다.

    함수 시작 부분의 pBase 변수는 코드 조각에 새로 추가된 유일한 변수입니다. 이 변수는 나중에 상대 가상 주소(RVA)를 가상 주소(VA)로 변환할 때 형 변환을 피하기 위해 만들어졌습니다. Visual Studio 컴파일러는 값에 PVOID 데이터 유형을 추가할 때 오류를 발생시키므로 hModule이 대신 PBYTE로 캐스팅되었습니다.

    FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {
    
    	// We do this to avoid casting each time we use 'hModule'
    	PBYTE pBase = (PBYTE) hModule;
    
    	// Getting the DOS header and performing a signature check
    	PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pBase;
    	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    		return NULL;
    
    	// Getting the NT headers and performing a signature check
    	PIMAGE_NT_HEADERS	pImgNtHdrs	= (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    		return NULL;
    
    	// Getting the optional header
    	IMAGE_OPTIONAL_HEADER ImgOptHdr = pImgNtHdrs->OptionalHeader;
    
    	// Getting the image export table
    	// This is the export directory
    	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
        // ...
    }

    내보낸 함수 액세스

    IMAGE_EXPORT_DIRECTORY 구조체에 대한 포인터를 얻은 후에는 내보낸 함수를 반복할 수 있습니다. NumberOfFunctions 멤버는 hModule이 내보낸 함수의 수를 지정합니다. 따라서 루프의 최대 반복 횟수는 NumberOfFunctions와 같아야 합니다.

    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
      // Searching for the target exported function
    }

    검색 로직 구축

    다음 단계는 함수에 대한 검색 로직을 구축하는 것입니다. 검색 로직을 구축하려면 내보내기 테이블에서 하나의 고유한 함수를 참조하는 RVA가 포함된 배열인 AddressOfFunctionsAddressOfNames 및 AddressOfNameOrdinals를 사용해야 합니다.

    typedef struct _IMAGE_EXPORT_DIRECTORY {
        // ...
    	// ...
        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;

    이러한 요소는 RVA이므로 VA를 얻으려면 모듈의 기본 주소인 pBase를 추가해야 합니다. 처음 두 코드 조각은 간단해야 합니다. 각각 함수의 이름과 함수의 주소를 검색합니다. 세 번째 코드 조각은 함수의 서수를 검색하며, 다음 섹션에서 자세히 설명합니다.

    // Getting the function's names array pointer
    PDWORD FunctionNameArray 	= (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    
    // Getting the function's addresses array pointer
    PDWORD FunctionAddressArray 	= (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    
    // Getting the function's ordinal array pointer
    PWORD  FunctionOrdinalArray 	= (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);

    정령 이해

    함수의 서수는 DLL에서 내보낸 함수 테이블 내에서 함수의 위치를 나타내는 정수 값입니다. 내보내기 테이블은 함수 포인터의 목록(배열)으로 구성되며, 각 함수에는 테이블 내 위치에 따라 서수 값이 할당됩니다.

    서수 값은 함수의 이름이 아닌 주소를 식별하는 데 사용된다는 점에 유의하세요. 내보내기 테이블은 함수 이름을 사용할 수 없거나 고유하지 않은 경우를 처리하기 위해 이러한 방식으로 작동합니다. 또한 서수를 사용하여 함수의 주소를 가져오는 것이 이름을 사용하는 것보다 더 빠릅니다. 이러한 이유로 운영 체제에서는 서수를 사용하여 함수의 주소를 검색합니다.

    예를 들어, 가상 할당주소는 FunctionAddressArray [가상 할당 서수]와 같으며, 여기서 FunctionAddressArray는 내보내기 테이블에서 가져온 함수의 주소 배열 포인터입니다.

    이를 염두에 두고 다음 코드 조각은 지정된 모듈의 함수 배열에 있는 각 함수의 서수 값을 인쇄합니다.

    // Getting the function's names array pointer
    PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    
    // Getting the function's addresses array pointer
    PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    
    // Getting the function's ordinal array pointer
    PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
    
    // Looping through all the exported functions
    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
    
    	// Getting the name of the function
    	CHAR* pFunctionName		= (CHAR*)(pBase + FunctionNameArray[i]);
    
    	// Getting the ordinal of the function
    	WORD wFunctionOrdinal = FunctionOrdinalArray[i];
    
    	// Printing
    	printf("[ %0.4d ] NAME: %s -\t ORDINAL: %d\n", i, pFunctionName, wFunctionOrdinal);
    }

    GetProcAddressReplacement 부분 데모

    GetProcAddressReplacement가 아직 완성되지는 않았지만, 이제 함수 이름과 관련 서수를 출력해야 합니다. 지금까지 빌드한 내용을 테스트하려면 다음 매개 변수를 사용하여 함수를 호출하세요:

    GetProcAddressReplacement(GetModuleHandleA("ntdll.dll"), NULL);

    예상대로 함수 이름과 함수의 서수가 콘솔에 인쇄됩니다.

    주소에 서수

    함수의 서수 값을 사용하면 함수의 주소를 얻을 수 있습니다.

    // Getting the function's names array pointer
    PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    
    // Getting the function's addresses array pointer
    PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    
    // Getting the function's ordinal array pointer
    PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
    
    
    // Looping through all the exported functions
    for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
    
    	// Getting the name of the function
    	CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
    
    	// Getting the ordinal of the function
    	WORD wFunctionOrdinal = FunctionOrdinalArray[i];
    
    	// Getting the address of the function through it's ordinal
    	PVOID pFunctionAddress = (PVOID)(pBase + FunctionAddressArray[wFunctionOrdinal]);
    
    	printf("[ %0.4d ] NAME: %s -\t ADDRESS: 0x%p  -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, wFunctionOrdinal);
    }

    기능을 확인하려면 xdbg를 사용하여 메모장 .exe를 열고 ntdll.dll의 내보내기를 확인합니다.

    위의 이미지는 xdbg와 GetProcAddressReplacement 함수를 사용한 경우 모두에서 A_SHAUpdate의 주소가 0x00007FFD384D2D10임을 보여줍니다. Windows 로더가 모든 프로세스에 대해 새로운 서수 배열을 생성하기 때문에 함수에 따라 서수가 다르다는 것을 알 수 있습니다.

    GetProcAddressReplacement 코드

    함수가 완성되기 위해 필요한 마지막 코드는 내보낸 함수 이름을 대상 함수 이름인 lpApiName과 비교하는 방법입니다. 이 작업은 strcmp를 사용하여 쉽게 수행할 수 있습니다. 그런 다음 마지막으로 일치하는 것이 있으면 함수 주소를 반환합니다.

    FARPROC GetProcAddressReplacement(IN HMODULE hModule, IN LPCSTR lpApiName) {
    
    	// We do this to avoid casting at each time we use 'hModule'
    	PBYTE pBase = (PBYTE)hModule;
    
    	// Getting the dos header and doing a signature check
    	PIMAGE_DOS_HEADER	pImgDosHdr		= (PIMAGE_DOS_HEADER)pBase;
    	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    		return NULL;
    
    	// Getting the nt headers and doing a signature check
    	PIMAGE_NT_HEADERS	pImgNtHdrs		= (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    		return NULL;
    
    	// Getting the optional header
    	IMAGE_OPTIONAL_HEADER	ImgOptHdr	= pImgNtHdrs->OptionalHeader;
    
    	// Getting the image export table
    	PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY) (pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
    	// Getting the function's names array pointer
    	PDWORD FunctionNameArray = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    
    	// Getting the function's addresses array pointer
    	PDWORD FunctionAddressArray = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    
    	// Getting the function's ordinal array pointer
    	PWORD  FunctionOrdinalArray = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
    
    
    	// Looping through all the exported functions
    	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++){
    
    		// Getting the name of the function
    		CHAR* pFunctionName = (CHAR*)(pBase + FunctionNameArray[i]);
    
    		// Getting the address of the function through its ordinal
    		PVOID pFunctionAddress	= (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
    
    		// Searching for the function specified
    		if (strcmp(lpApiName, pFunctionName) == 0){
    			printf("[ %0.4d ] FOUND API -\t NAME: %s -\t ADDRESS: 0x%p  -\t ORDINAL: %d\n", i, pFunctionName, pFunctionAddress, FunctionOrdinalArray[i]);
    			return pFunctionAddress;
    		}
    	}
    
    	return NULL;
    }

    GetProcAddressReplacement 최종 데모

    아래 이미지는 NtAllocateVirtualMemory의 주소를 검색하는 GetProcAddress와 GetProcAddressReplacement의 출력을 보여줍니다. 예상대로 두 함수 모두 올바른 함수 주소를 가져왔으며, 따라서 GetProcAddress의 커스텀 구현이 성공적으로 빌드되었습니다.


    54. IAT 숨김 및 난독화 – 사용자 정의 GetModuleHandle

    IAT 숨김 및 난독화 – 사용자 정의 GetModuleHandle

    소개

    GetModuleHandle 함수는 지정된 DLL의 핸들을 검색합니다. 이 함수는 DLL에 대한 핸들을 반환하거나 호출 프로세스에 DLL이 존재하지 않는 경우 NULL을 반환합니다.

    이 모듈에서는 GetModuleHandle을 대체할 함수가 구현됩니다. 새 함수의 프로토타입은 아래와 같습니다.

    HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName){}

    GetModuleHandle 작동 방식

    HMODULE 데이터 유형은 로드된 DLL의 기본 주소로, 프로세스의 주소 공간에서 DLL이 위치한 곳입니다. 따라서 대체 함수의 목표는 지정된 DLL의 기본 주소를 검색하는 것입니다.

    프로세스 환경 블록(PEB)에는 로드된 DLL에 관한 정보, 특히 PEB 구조체의 PEB_LDR_DATA Ldr 멤버가 포함되어 있습니다. 따라서 초기 단계는 PEB 구조를 통해 이 멤버에 액세스하는 것입니다.

    64비트 시스템에서의 PEB

    PEB 구조에 대한 포인터는 스레드 환경 블록(TEB) 구조 내에서 찾을 수 있다는 점을 기억하세요.

    64비트 시스템에서는 TEB 구조체의 포인터에 대한 오프셋이 GS 레지스터에 저장됩니다. 다음 이미지는 x64dbg에서 가져온 것입니다.

    방법 1: 64비트 시스템에서 PEB 검색하기

    PEB를 검색하는 방법에는 두 가지가 있습니다. 첫 번째 방법은 TEB 구조를 검색한 다음 PEB에 대한 포인터를 가져오는 것입니다. 이 접근 방식은 GS 레지스터에서 0x30바이트를 읽어 TEB 구조에 대한 포인터에 도달하는 Visual Studio의 __readgsqword (0x30) 매크로를 사용하여 수행할 수 있습니다.

    // Method 1
    PTEB pTeb = (PTEB)__readgsqword(0x30);
    PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

    방법 2: 64비트 시스템에서 PEB 검색하기

    다음 방법은 GS 레지스터에서 0x60바이트를 읽는 Visual Studio의 __readgsqword (0x60) 매크로를 사용하여 TEB 구조를 건너뛰고 PEB 구조를 직접 검색하는 것입니다.

    // Method 2
    PPEB pPeb2 = (PPEB)(__readgsqword(0x60));

    이는 ProcessEnvironmentBlock 요소가 TEB 구조의 시작 부분에서 0x60 (헥스) 또는 96바이트이기 때문에 가능합니다.

    32비트 시스템에서의 PEB

    32비트 시스템에서는 TEB 구조체의 포인터에 대한 오프셋이 FS 레지스터에 저장됩니다. 다음 이미지는 x32dbg에서 가져온 것입니다.

    그리고 PEB 구조의 포인터가 TEB에 있다는 것을 기억하세요.

    방법 1: 32비트 시스템에서 PEB 검색하기

    64비트 시스템과 마찬가지로 PEB를 검색하는 방법에는 두 가지가 있습니다.

    첫 번째 방법은 FS 레지스터에서 0x18 바이트를 읽는 Visual Studio의 __readfsdword (0x18) 매크로를 사용하여 TEB 구조를 가져온 다음 PEB 구조를 가져오는 것입니다.

    // Method 1
    PTEB pTeb = (PTEB)__readfsdword(0x18);
    PPEB pPeb = (PPEB)pTeb->ProcessEnvironmentBlock;

    방법 2: 32비트 시스템에서 PEB 검색하기

    두 번째 방법은 FS 레지스터에서 0x30바이트를 읽는 Visual Studio의 __readfsdword (0x30) 매크로를 사용하여 TEB 구조를 건너뛰고 PEB를 직접 가져오는 방법입니다.

    // Method 2
    PPEB pPeb2 = (PPEB)(__readfsdword(0x30));

    0x30 (16진수)은 48바이트로, 32비트 TEB 구조체에서 ProcessEnvironmentBlock 요소의 오프셋입니다. 32비트 시스템에서 PVOID 데이터 유형은 4바이트입니다.

    DLL 열거하기

    PEB 구조가 검색되면 다음 단계는 PEB_LDR_DATA Ldr 멤버에 액세스하는 것입니다. 이 멤버에는 프로세스에서 로드된 DLL에 관한 정보가 포함되어 있습니다.

    PEB_LDR_DATA 구조

    PEB_LDR_DATA 구조체는 아래와 같습니다. 이 구조체에서 중요한 멤버는 LIST_ENTRY InMemoryOrderModuleList입니다.

    typedef struct _PEB_LDR_DATA {
      BYTE       Reserved1[8];
      PVOID      Reserved2[3];
      LIST_ENTRY InMemoryOrderModuleList;
    } PEB_LDR_DATA, *PPEB_LDR_DATA;

    LIST_ENTRY 구조

    아래 표시된 리스트_입력 구조는 이중으로 연결된 목록으로, 기본적으로 배열과 동일하지만 인접한 요소에 더 쉽게 액세스할 수 있습니다.

    typedef struct _LIST_ENTRY {
       struct _LIST_ENTRY *Flink;
       struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

    이중 링크 목록은 깜박임 및 깜박임 요소를 각각 머리와 꼬리 포인터로 사용합니다. 즉, 깜박임 요소는 목록의 다음 노드를 가리키고 깜박임 요소는 목록의 이전 노드를 가리킵니다. 이러한 포인터는 연결된 목록을 양방향으로 탐색하는 데 사용됩니다. 이를 알면 이 목록의 열거를 시작하려면 첫 번째 요소인 InMemoryOrderModuleList.Flink에 액세스하는 것부터 시작해야 합니다.

    Microsoft의 InMemoryOrderModuleList 멤버에 대한 정의에 따르면, 목록의 각 항목은 LDR_DATA_TABLE_ENTRY 구조에 대한 포인터라고 명시되어 있습니다.

    LDR_DATA_TABLE_ENTRY 구조체

    LDR_DATA_TABLE_ENTRY 구조체는 프로세스에 로드된 DLL의 링크된 목록 안에 있는 DLL을 나타냅니다. 모든 LDR_DATA_TABLE_ENTRY는 고유한 DLL을 나타냅니다.

    typedef struct _LDR_DATA_TABLE_ENTRY {
        PVOID Reserved1[2];
        LIST_ENTRY InMemoryOrderLinks;	// doubly-linked list that contains the in-memory order of loaded modules
        PVOID Reserved2[2];
        PVOID DllBase;
        PVOID EntryPoint;
        PVOID Reserved3;
        UNICODE_STRING FullDllName;		// 'UNICODE_STRING' structure that contains the filename of the loaded module
        BYTE Reserved4[8];
        PVOID Reserved5[3];
        union {
            ULONG CheckSum;
            PVOID Reserved6;
        };
        ULONG TimeDateStamp;
    } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

    구현 로직

    지금까지 언급된 모든 내용을 바탕으로 필요한 조치는 다음과 같습니다:

    1. PEB 검색
    2. PEB에서 Ldr 멤버를 검색합니다.
    3. 링크된 목록의 첫 번째 요소를 검색합니다.
    HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {
    
    // Getting peb
    #ifdef _WIN64 // if compiling as x64
    	PPEB			pPeb	= (PEB*)(__readgsqword(0x60));
    #elif _WIN32 // if compiling as x32
    	PPEB			pPeb	= (PEB*)(__readfsdword(0x30));
    #endif// Getting the Ldr
    	PPEB_LDR_DATA		    pLdr	= (PPEB_LDR_DATA)(pPeb->Ldr);
    
    	// Getting the first element in the linked list which contains information about the first module
    	PLDR_DATA_TABLE_ENTRY	pDte	= (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
    
    }

    모든 pDte는 링크된 목록 내에서 고유한 DLL을 나타내므로 다음 코드 줄을 사용하여 다음 요소로 이동할 수 있습니다:

    pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);

    위의 코드 줄은 복잡해 보일 수 있지만, pDte가 가리키는 주소에 저장된 값을 역참조한 다음 그 결과를 PLDR_DATA_TABLE_ENTRY 구조체에 대한 포인터로 캐스팅하는 것이 전부입니다. 이것이 바로 링크된 목록이 작동하는 방식이며, 다음 이미지와 같습니다.

    DLL 열거 – 코드

    아래 코드 스니펫은 호출 프로세스 내에 이미 로드된 DLL의 이름을 검색합니다. 이 함수는 대상 모듈인 szModuleName을 검색합니다. 일치하는 항목이 있으면 이 함수는 DLL에 대한 핸들(HMODULE)을 반환하고, 일치하지 않으면 NULL을 반환합니다.

    HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {
    
    // Getting PEB
    #ifdef _WIN64 // if compiling as x64
    	PPEB			pPeb	= (PEB*)(__readgsqword(0x60));
    #elif _WIN32 // if compiling as x32
    	PPEB			pPeb	= (PEB*)(__readfsdword(0x30));
    #endif// Getting Ldr
    	PPEB_LDR_DATA		    pLdr	= (PPEB_LDR_DATA)(pPeb->Ldr);
    
    	// Getting the first element in the linked list which contains information about the first module
    	PLDR_DATA_TABLE_ENTRY	pDte	= (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
    
    	while (pDte) {
    
    		// If not null
    		if (pDte->FullDllName.Length != NULL) {
               	// Print the DLL name
    			wprintf(L"[i] \"%s\" \n", pDte->FullDllName.Buffer);
    		}
    		else {
    			break;
    		}
    
    		// Next element in the linked list
    		pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
    
    	}
    
    	return NULL;
    }

    대소문자를 구분하는 DLL 이름

    이전 이미지의 출력을 살펴보면 일부 DLL 이름은 대문자로 표시되고 다른 이름은 그렇지 않은 것을 쉽게 확인할 수 있으며, 이는 DLL 기본 주소(HMODULE)를 가져오는 기능에 영향을 미칩니다. 예를 들어, KERNEL32. DLL DLL을 검색할 때 Kernel32.DLL을 대신 전달하면 wcscmp 함수는 두 문자열을 서로 다른 문자열로 취급합니다.

    이 문제를 해결하기 위해 두 개의 문자열을 받아 소문자 표현으로 변환한 다음 이 상태에서 비교하는 도우미 함수 IsStringEqual이 만들어졌습니다. 두 문자열이 같으면 참을 반환하고 그렇지 않으면 거짓을 반환합니다.

    BOOL IsStringEqual (IN LPCWSTR Str1, IN LPCWSTR Str2) {
    
    	WCHAR   lStr1	[MAX_PATH],
    			lStr2	[MAX_PATH];
    
    	int		len1	= lstrlenW(Str1),
    			len2	= lstrlenW(Str2);
    
    	int		i		= 0,
    			j		= 0;
    
    	// Checking length. We dont want to overflow the buffers
    	if (len1 >= MAX_PATH || len2 >= MAX_PATH)
    		return FALSE;
    
        // Converting Str1 to lower case string (lStr1)
    	for (i = 0; i < len1; i++){
    		lStr1[i] = (WCHAR)tolower(Str1[i]);
    	}
    	lStr1[i++] = L'\0'; // null terminating
    
        // Converting Str2 to lower case string (lStr2)
    	for (j = 0; j < len2; j++) {
    		lStr2[j] = (WCHAR)tolower(Str2[j]);
    	}
    	lStr2[j++] = L'\0'; // null terminating
    
    	// Comparing the lower-case strings
    	if (lstrcmpiW(lStr1, lStr2) == 0)
    		return TRUE;
    
    	return FALSE;
    }

    DLL 기본 주소

    DLL 기본 주소를 얻으려면 LDR_DATA_TABLE_ENTRY 구조를 참조해야 합니다. 안타깝게도 Microsoft의 공식 문서에는 이 구조체의 상당 부분이 누락되어 있습니다. 따라서 구조를 더 잘 이해하기 위해 Windows Vista 커널 구조에 대한 검색을 수행했습니다. 구조에 대한 결과는 여기에서 확인할 수 있습니다.

    typedef struct _LDR_DATA_TABLE_ENTRY {
        LIST_ENTRY InLoadOrderLinks;
        LIST_ENTRY InMemoryOrderLinks;
        LIST_ENTRY InInitializationOrderLinks;
        PVOID DllBase;
        PVOID EntryPoint;
        ULONG SizeOfImage;
        UNICODE_STRING FullDllName;
        UNICODE_STRING BaseDllName;
        ULONG Flags;
        WORD LoadCount;
        WORD TlsIndex;
        union {
            LIST_ENTRY HashLinks;
            struct {
                PVOID SectionPointer;
                ULONG CheckSum;
            };
        };
        union {
            ULONG TimeDateStamp;
            PVOID LoadedImports;
        };
        PACTIVATION_CONTEXT EntryPointActivationContext;
        PVOID PatchInformation;
        LIST_ENTRY ForwarderLinks;
        LIST_ENTRY ServiceTagLinks;
        LIST_ENTRY StaticLinks;
    } LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

    DLL 기본 주소는 이름에서 알 수 있듯이 InInitializationOrderLinks.Flink이지만, 불행히도 Microsoft는 사람들을 혼동하는 것을 좋아합니다. 이 멤버를 Microsoft의 공식 문서인 LDR_DATA_TABLE_ENTRY와 비교하면 DLL의 기본 주소가 예약된 요소(Reserved2[0])임을 알 수 있습니다.

    이를 염두에 두고 GetModuleHandle 교체 기능을 완료할 수 있습니다.

    GetModuleHandle 교체 함수

    GetModuleHandleReplacement는 GetModuleHandle을 대체하는 함수입니다. 이 함수는 지정된 DLL 이름을 검색하고 프로세스에서 로드된 경우 DLL에 대한 핸들을 반환합니다.

    HMODULE GetModuleHandleReplacement(IN LPCWSTR szModuleName) {
    
    // Getting PEB
    #ifdef _WIN64 // if compiling as x64
    	PPEB					pPeb		= (PEB*)(__readgsqword(0x60));
    #elif _WIN32 // if compiling as x32
    	PPEB					pPeb		= (PEB*)(__readfsdword(0x30));
    #endif// Getting Ldr
    	PPEB_LDR_DATA			pLdr		= (PPEB_LDR_DATA)(pPeb->Ldr);
    	// Getting the first element in the linked list (contains information about the first module)
    	PLDR_DATA_TABLE_ENTRY	pDte		= (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
    
    	while (pDte) {
    
    		// If not null
    		if (pDte->FullDllName.Length != NULL) {
    
    			// Check if both equal
    			if (IsStringEqual(pDte->FullDllName.Buffer, szModuleName)) {
    				wprintf(L"[+] Found Dll \"%s\" \n", pDte->FullDllName.Buffer);
    #ifdef STRUCTSreturn (HMODULE)(pDte->InInitializationOrderLinks.Flink);
    #elsereturn (HMODULE)pDte->Reserved2[0];
    #endif // STRUCTS}
    
    			// wprintf(L"[i] \"%s\" \n", pDte->FullDllName.Buffer);
    		}
    		else {
    			break;
    		}
    
    		// Next element in the linked list
    		pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
    
    	}
    
    	return NULL;
    }

    설명하지 않은 코드의 한 부분이 아래에 나와 있습니다. 코드의 이 부분은 Microsoft의 LDR_DATA_TABLE_ENTRY 구조체 버전이 사용되는지 아니면 Windows Vista 커널 구조체의 구조체가 사용되는지를 결정합니다. 어떤 것을 사용했는지에 따라 멤버의 이름이 변경됩니다.

    #ifdef STRUCTSreturn (HMODULE)(pDte->InInitializationOrderLinks.Flink);
    #elsereturn (HMODULE)pDte->Reserved2[0];
    #endif // STRUCTS

    GetModuleHandleReplacement2

    이 모듈의 코드에서 GetModuleHandleReplacement 함수의 또 다른 구현을 찾을 수 있습니다. GetModuleHandleReplacement2는 이중 링크된 목록 개념을 활용하는 링크된 목록의 요소와 헤드를 사용하여 DLL 열거를 수행합니다. 이 함수는 링크된 목록에 익숙한 사용자를 위해 만들어졌습니다.

    데모


    55. IAT 숨김 및 난독화 – API 해싱

    IAT 숨김 및 난독화 – API 해싱

    소개

    이전 두 모듈에서는 GetProcAddress와 GetModuleHandle을 대체하는 두 개의 사용자 정의 함수 GetProcAddressReplacement와 GetModuleHandleReplacement가 생성되었습니다. 이렇게 하면 IAT에서 가져온 함수를 숨기는 런타임 동적 링크를 수행하는 데 충분했습니다. 그러나 코드 내에서 사용된 문자열은 어떤 함수가 사용되고 있는지 보여줍니다. 예를 들어, 아래 줄은 함수를 사용하여 VirtualAllocEx를 검색합니다.

    GetProcAddressReplacement(GetModuleHandleReplacement("ntdll.dll"),"VirtualAllocEx")

    보안 솔루션은 컴파일된 바이너리 내의 문자열을 쉽게 검색하여 VirtualAllocEx가 사용 중임을 인식할 수 있습니다. 이 문제를 해결하기 위해 문자열 해싱 알고리즘이 GetProcAddressReplacement와 GetModuleHandleReplacement에 모두 적용됩니다. 지정된 모듈 기본 주소 또는 함수 주소를 얻기 위해 문자열 비교를 수행하는 대신 함수는 해시 값으로 대신 작동합니다.

    JenkinsOneAtATime32Bit 구현하기

    이 모듈에서는 GetProcAddressReplacement 및 GetModuleHandleReplacement 함수의 이름이 각각 GetProcAddressH 및 GetModuleHandleH로 변경되었습니다. 이러한 업데이트된 함수는 Jenkins One At A Time 문자열 해싱 알고리즘을 사용하여 함수와 모듈 이름을 해당 함수와 모듈을 나타내는 해시 값으로 대체합니다. 이 알고리즘은 문자열 해싱 모듈에 도입된 JenkinsOneAtATime32Bit 함수를 통해 활용되었다는 점을 기억하세요.

    문자열 해싱

    이 모듈에 표시된 함수를 사용하려면 모듈 이름(예: User32.dll)의 해시값과 함수 이름(예: MessageBoxA)의 해시값을 구해야 합니다. 이 작업은 먼저 해시값을 콘솔에 인쇄하여 수행할 수 있습니다. 해시 알고리즘이 동일한 시드를 사용하는지 확인합니다.

    // ...
    
    int main(){
    	printf("[i] Hash Of \"%s\" Is : 0x%0.8X \n", "USER32.DLL", HASHA("USER32.DLL")); // Capitalized module name
    	printf("[i] Hash Of \"%s\" Is : 0x%0.8X \n", "MessageBoxA", HASHA("MessageBoxA"));
    
      	return 0;
    }

    위의 메인 함수는 다음을 출력합니다:

    [i] Hash Of "USER32.DLL" Is : 0x81E3778E
    [i] Hash Of "MessageBoxA" Is : 0xF10E27CA

    이제 이 해시값을 아래 함수와 함께 사용할 수 있습니다.

    사용법

    함수는 동일한 방식으로 사용되지만 이제는 문자열 값이 아닌 해시 값이 전달됩니다.

    // 0x81E3778E is the hash of USER32.DLL
    // 0xF10E27CA is the hash of MessageBoxA
    fnMessageBoxA pMessageBoxA = GetProcAddressH(GetModuleHandleH(0x81E3778E),0xF10E27CA);

    GetProcAddressH 함수

    GetProcAddressH는 GetProcAddressReplacement와 동일한 함수이지만, 주요 차이점은 내보낸 함수 이름을 입력 해시와 비교하기 위해 JenkinsOneAtATime32Bit 문자열 해싱 알고리즘의 해시 값이 사용된다는 점입니다.

    또한 코드가 두 개의 매크로를 사용하여 코드를 더 깔끔하고 향후 업데이트하기 쉽도록 만들었다는 점도 주목할 만합니다.

    • HASHA – 해시스트링 젠킨스원앳타임32비트A(ASCII) 호출하기
    • HASHW – 해시스트링 젠킨스 원앳타임32비트W 호출(유니코드)
    #define HASHA(API) (HashStringJenkinsOneAtATime32BitA((PCHAR) API))#define HASHW(API) (HashStringJenkinsOneAtATime32BitW((PWCHAR) API))

    이를 염두에 두고 GetProcAddressH 함수는 다음과 같습니다. 이 함수는 두 개의 매개변수를 받습니다:

    • hModule – 함수가 포함된 DLL 모듈에 대한 핸들입니다.
    • dwApiNameHash – 주소를 가져올 함수 이름의 해시값입니다.
    FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {
    
    	if (hModule == NULL || dwApiNameHash == NULL)
    		return NULL;
    
    	PBYTE pBase = (PBYTE)hModule;
    
    	PIMAGE_DOS_HEADER         pImgDosHdr			  = (PIMAGE_DOS_HEADER)pBase;
    	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    		return NULL;
    
    	PIMAGE_NT_HEADERS         pImgNtHdrs			  = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    		return NULL;
    
    	IMAGE_OPTIONAL_HEADER     ImgOptHdr			  = pImgNtHdrs->OptionalHeader;
    
    	PIMAGE_EXPORT_DIRECTORY   pImgExportDir		  = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
    
    	PDWORD  FunctionNameArray	= (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    	PDWORD  FunctionAddressArray	= (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    	PWORD   FunctionOrdinalArray	= (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
    
    	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
    		CHAR*	pFunctionName       = (CHAR*)(pBase + FunctionNameArray[i]);
    		PVOID	pFunctionAddress    = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
    
    		// Hashing every function name pFunctionName
    		// If both hashes are equal then we found the function we want
    		if (dwApiNameHash == HASHA(pFunctionName)) {
    			return pFunctionAddress;
    		}
    	}
    
    	return NULL;
    }

    GetModuleHandleH

    GetModuleHandleH 함수는 GetModuleHandleReplacement와 동일하지만, 주요 차이점은 열거된 DLL 이름을 입력 해시와 비교하는 데 JenkinsOneAtATime32Bit 문자열 해싱 알고리즘의 해시 값이 사용된다는 점입니다. 이 함수는 FullDllName.Buffer의 문자열을 대문자로 처리하므로 dwModuleNameHash 매개변수는 대문자로 된 모듈 이름(예: USER32.DLL)의 해시 값이어야 합니다.

    HMODULE GetModuleHandleH(DWORD dwModuleNameHash) {
    
    	if (dwModuleNameHash == NULL)
    		return NULL;
    
    #ifdef _WIN64
    	PPEB      pPeb = (PEB*)(__readgsqword(0x60));
    #elif _WIN32
    	PPEB      pPeb = (PEB*)(__readfsdword(0x30));
    #endif
    
    	PPEB_LDR_DATA            pLdr  = (PPEB_LDR_DATA)(pPeb->Ldr);
    	PLDR_DATA_TABLE_ENTRY	pDte  = (PLDR_DATA_TABLE_ENTRY)(pLdr->InMemoryOrderModuleList.Flink);
    
    	while (pDte) {
    
    		if (pDte->FullDllName.Length != NULL && pDte->FullDllName.Length < MAX_PATH) {
    
    			// Converting `FullDllName.Buffer` to upper case string
    			CHAR UpperCaseDllName[MAX_PATH];
    
    			DWORD i = 0;
    			while (pDte->FullDllName.Buffer[i]) {
    				UpperCaseDllName[i] = (CHAR)toupper(pDte->FullDllName.Buffer[i]);
    				i++;
    			}
    			UpperCaseDllName[i] = '\0';
    
    			// hashing `UpperCaseDllName` and comparing the hash value to that's of the input `dwModuleNameHash`
    			if (HASHA(UpperCaseDllName) == dwModuleNameHash)
    				return pDte->Reserved2[0];
    
    		}
    		else {
    			break;
    		}
    
    		pDte = *(PLDR_DATA_TABLE_ENTRY*)(pDte);
    	}
    
    	return NULL;
    }

    데모

    이 데모에서는 GetModuleHandleH와 GetProcAddressH를 사용하여 MessageBoxA를 호출합니다.

    #define USER32DLL_HASH      0x81E3778E#define MessageBoxA_HASH    0xF10E27CAint main() {
    
    	// Load User32.dll to the current process so that GetModuleHandleH will work
    	if (LoadLibraryA("USER32.DLL") == NULL) {
    		printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
    		return 0;
    	}
    
    	// Getting the handle of user32.dll using GetModuleHandleH
    	HMODULE hUser32Module = GetModuleHandleH(USER32DLL_HASH);
    	if (hUser32Module == NULL){
    		printf("[!] Cound'nt Get Handle To User32.dll \n");
    		return -1;
    	}
    
    	// Getting the address of MessageBoxA function using GetProcAddressH
    	fnMessageBoxA pMessageBoxA = (fnMessageBoxA)GetProcAddressH(hUser32Module, MessageBoxA_HASH);
    	if (pMessageBoxA == NULL) {
    		printf("[!] Cound'nt Find Address Of Specified Function \n");
    		return -1;
    	}
    
    	// Calling MessageBoxA
    	pMessageBoxA(NULL, "Building Malware With Maldev", "Wow", MB_OK | MB_ICONEXCLAMATION);
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }

    메시지 상자 문자열 검색

    Strings.exe 시스템 내부 도구를 사용하여 “MessageBox” 문자열을 검색합니다.

    바이너리에 해당 문자열이 없는 것을 확인할 수 있습니다. MessageBoxA는 IAT로 가져오거나 바이너리에 문자열로 노출되지 않고 성공적으로 호출되었습니다. 이는 32비트 및 64비트 시스템 모두에 적용됩니다.


    56. IAT 숨김 및 난독화 – 사용자 정의 의사 핸들

    IAT 숨김 및 난독화 – 사용자 정의 의사 핸들

    소개

    앞서 설명한 것처럼 API 해싱을 활용하여 구현의 IAT를 마스킹하는 것은 효과적인 방법입니다. 그러나 가능한 경우 WinAPI 자체를 교체하면 해시 값의 수를 줄여 IAT의 은폐력을 높일 수 있을 뿐만 아니라 API 해싱 알고리즘에 연결된 잠재적인 휴리스틱 서명을 줄일 수 있습니다. 또한 WinAPI 함수에 대한 사용자 지정 코드를 구현하면 다양한 구현에서 사용할 수 있으므로 전체 IAT 숨김 프로세스의 자동화를 간소화할 수 있습니다.

    따라서 이 모듈에서는 디버거를 사용하여 의사 핸들을 검색하는 두 가지 함수를 분석한 다음 사용자 정의 버전을 생성하는 과정을 거칩니다. 다시 말하지만, 목표는 API 해싱을 활용하지 않고 이러한 함수가 IAT에 표시되는 것을 방지하는 것입니다. 분석할 함수는 다음과 같습니다:

    • GetCurrentProcess – 호출 프로세스에 대한 의사 핸들을 가져옵니다.
    • GetCurrentThread – 호출하는 스레드의 의사 핸들을 가져옵니다.

    의사 핸들이란 무엇인가요?

    의사 핸들은 특정 시스템 리소스에 해당하지 않고 대신 현재 프로세스 또는 스레드에 대한 참조로 작동하는 핸들 유형입니다.

    함수 분석

    앞서 언급했듯이 이 두 함수는 프로세스든 스레드든 상대 객체에 대한 의사 핸들을 반환합니다. 이 섹션에서는 xdbg 디버거를 사용하여 이러한 함수를 분석하여 내부 작동 방식을 이해합니다.

    먼저 내보내는 DLL인 kernel32.dll에서 GetCurrentProcess 함수를 검색합니다. 이 함수의 주소는 0x00007FFD9A4A5040입니다.

    이 주소로 이동하여 jmp 명령어를 확인합니다.

    점프를 따라가면 함수 코드에 도달합니다. 명령어 또는 rax, FFFFFFFFFFFFFFFF는 RAX 레지스터를 해당 값으로 설정하고 ret 명령어는 0xFFFFFFFFFFFFFF를 반환합니다. 0xFFFFFFFFFFFFFF의 보수 표현은 -1입니다.

    GetCurrentThread 함수에 대해서도 동일한 단계를 수행합니다. 마찬가지로 이 함수는 0xFFFFFFFFFFFFFE를 반환합니다. 0xFFFFFFFFFFFFFE의 둘의 보수 표현은 -2입니다.

    사용자 지정 구현

    GetCurrentProcess는 -1을 반환하고 GetCurrentThread는 -2를 반환하므로, 이 함수는 다음 매크로로 대체할 수 있습니다. 값은 HANDLE 유형으로 형 변환됩니다.

    #define NtCurrentProcess() ((HANDLE)-1) // Return the pseudo handle for the current process#define NtCurrentThread()  ((HANDLE)-2) // Return the pseudo handle for the current thread

    32비트 시스템

    64비트 버전의 GetCurrentProcess 및 GetCurrentThread 함수는 32비트 버전과 HANDLE 데이터 유형의 크기만 다릅니다. 32비트 시스템에서 HANDLE 데이터 유형은 4바이트입니다. 아래 이미지는 32비트 시스템에서의 GetCurrentProcess를 보여줍니다.

    결론

    이 모듈에서는 API 해싱을 활용하여 구현의 IAT를 숨기는 대신 WinAPI를 대체하는 개념과 로컬 스레드 및 프로세스의 의사 핸들 개념을 소개했습니다. WinAPI 함수는 대부분 이 모듈에서 설명한 것보다 더 복잡한 함수이기 때문에 모든 WinAPI 함수를 사용자 정의 코드로 대체할 수 있는 것은 아니라는 점을 언급할 필요가 있습니다. WinAPI 함수를 추가로 대체하려면 VX-API Github 리포지토리를 방문하세요.


    57. IAT 숨김 및 난독화 – 컴파일 타임 API 해싱

    IAT 숨김 및 난독화 – 컴파일 타임 API 해싱

    소개

    이전 API 해싱 모듈에서는 함수와 모듈을 코드에 추가하기 전에 해당 함수와 모듈의 해시를 생성했습니다. 안타깝게도 시간이 많이 소요될 수 있으며 컴파일 타임 API 해싱을 사용하면 이를 피할 수 있습니다.

    또한, 이전 모듈에서는 해시가 하드 코딩되어 있어 구현할 때마다 업데이트하지 않으면 보안 솔루션이 해시를 IoC로 사용할 수 있었습니다. 하지만 컴파일 타임 API 해싱을 사용하면 바이너리가 컴파일될 때마다 동적 해시가 생성됩니다.

    주의

    이 메서드는 constexpr 키워드를 사용하기 때문에 C++ 프로젝트에서만 작동합니다. C++의 constexpr 연산자는 함수나 변수를 컴파일 시점에 평가할 수 있음을 나타내는 데 사용됩니다. 또한 함수 및 변수에 대한 constexpr 연산자는 컴파일러가 런타임이 아닌 컴파일 타임에 특정 계산을 수행할 수 있도록 하여 애플리케이션의 성능을 향상시킵니다.

    컴파일 시간 해싱 연습

    아래 섹션에서는 컴파일 타임 해싱을 구현하는 데 필요한 단계를 안내합니다.

    컴파일 시간 함수 생성

    첫 번째 단계는 constexpr 연산자를 사용하여 컴파일 시간 함수가 되는 데 사용할 해싱 함수를 변환하는 것입니다. 이 경우, Dbj2 해싱 알고리즘은 constexpr 연산자를 사용하도록 수정됩니다.

    #define        SEED       5// Compile time Djb2 hashing function (WIDE)
    constexpr DWORD HashStringDjb2W(const wchar_t* String) {
    	ULONG Hash = (ULONG)g_KEY;
    	INT c = 0;
    	while ((c = *String++)) {
    		Hash = ((Hash << SEED) + Hash) + c;
    	}
    
    	return Hash;
    }
    
    // Compile time Djb2 hashing function (ASCII)
    constexpr DWORD HashStringDjb2A(const char* String) {
    	ULONG Hash = (ULONG)g_KEY;
    	INT c = 0;
    	while ((c = *String++)) {
    		Hash = ((Hash << SEED) + Hash) + c;
    	}
    
    	return Hash;
    }

    정의되지 않은 변수인 g_KEY는 두 함수 모두에서 초기 해시로 사용됩니다. g_KEY는 전역 constexpr 변수이며, 바이너리를 컴파일할 때마다 RandomCompileTimeSeed라는 함수(아래 설명)에 의해 임의로 생성됩니다.

    랜덤 시드 값 생성

    RandomCompileTimeSeed는 현재 시간을 기준으로 임의의 시드 값을 생성하는 데 사용됩니다. 이 함수는 C++에서 미리 정의된 매크로인 TIME 매크로에서 숫자를 추출하여 현재 시간을 HH:MM:SS 형식으로 확장하는 방식으로 이 작업을 수행합니다. 그런 다음 RandomCompileTimeSeed 함수가 각 숫자에 다른 임의 상수를 곱하고 이를 모두 더하여 최종 시드 값을 생성합니다.

    // Generate a random key at compile time which is used as the initial hash
    constexpr int RandomCompileTimeSeed(void)
    {
    	return '0' * -40271 +
    		__TIME__[7] * 1 +
    		__TIME__[6] * 10 +
    		__TIME__[4] * 60 +
    		__TIME__[3] * 600 +
    		__TIME__[1] * 3600 +
    		__TIME__[0] * 36000;
    };
    
    // The compile time random seed
    constexpr auto g_KEY = RandomCompileTimeSeed() % 0xFF;

    매크로 만들기

    다음으로, 런타임 중에 GetProcAddressH 함수가 해시를 비교하는 데 사용할 두 개의 매크로, RTIME_HASHA와 RTIME_HASHW를 정의합니다. 매크로는 다음과 같이 정의해야 합니다.

    #define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)       // Calling HashStringDjb2A#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)    // Calling HashStringDjb2W

    임의의 컴파일 시간 해시 함수가 설정되면 다음 단계는 컴파일 시간 해시 값을 변수로 선언하는 것입니다. 이 과정을 간소화하기 위해 두 개의 매크로가 구현됩니다.

    #define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);

    문자열화 연산자

    기호는 문자열화 연산자로 알려져 있습니다. 전처리기 매크로 매개변수를 문자열 리터럴로 변환하는 데 사용됩니다.

    예를 들어, HASHA(SomeFunction)과 같이 일부 함수 인수를 사용하여 CTIME_HASHA 매크로를 호출하는 경우 #API 표현식은 문자열 리터럴 "일부 함수“로 대체됩니다.

    연산자 병합

    병합 연산자는 ## 연산자로 알려져 있습니다. 이 연산자는 두 개의 전처리기 매크로를 하나의 매크로로 결합하는 데 사용됩니다. 병합 연산자는 API 매개변수와 문자열 "_Rotr32A ” 또는 "_Rotr32W“를 각각 결합하여 정의되는 변수의 최종 이름을 형성하는 데 사용됩니다.

    예를 들어, HASHA(SomeFunction)처럼 일부 함수 인수를 사용하여 CTIME_HASHA 매크로를 호출하는 경우 ## 연산자는 API와 "_Rotr32A" 를 결합하여 최종 변수 이름 SomeFunction_Rotr32A를 생성합니다.

    매크로 확장 데모

    이전 매크로의 작동 방식을 더 잘 이해할 수 있도록 아래 이미지에서는 컴파일 시간 해시 값을 보유할 MessageBoxA_Rotr32A라는 변수를 생성하여 CTIME_HASHA 매크로를 사용하여 MessageBoxA의 해시를 생성하는 예시를 보여줍니다.

    컴파일 타임 해싱 – 코드

    모든 조각을 조합하면 아래와 같은 코드가 완성됩니다.

    #include <Windows.h>#include <stdio.h>#include <winternl.h>#define        SEED       5// generate a random key (used as initial hash)
    constexpr int RandomCompileTimeSeed(void)
    {
    	return '0' * -40271 +
    		__TIME__[7] * 1 +
    		__TIME__[6] * 10 +
    		__TIME__[4] * 60 +
    		__TIME__[3] * 600 +
    		__TIME__[1] * 3600 +
    		__TIME__[0] * 36000;
    };
    
    constexpr auto g_KEY = RandomCompileTimeSeed() % 0xFF;
    
    
    // Compile time Djb2 hashing function (WIDE)
    constexpr DWORD HashStringDjb2W(const wchar_t* String) {
    	ULONG Hash = (ULONG)g_KEY;
    	INT c = 0;
    	while ((c = *String++)) {
    		Hash = ((Hash << SEED) + Hash) + c;
    	}
    
    	return Hash;
    }
    
    // Compile time Djb2 hashing function (ASCII)
    constexpr DWORD HashStringDjb2A(const char* String) {
    	ULONG Hash = (ULONG)g_KEY;
    	INT c = 0;
    	while ((c = *String++)) {
    		Hash = ((Hash << SEED) + Hash) + c;
    	}
    
    	return Hash;
    }
    
    
    // runtime hashing macros
    #define RTIME_HASHA( API ) HashStringDjb2A((const char*) API)#define RTIME_HASHW( API ) HashStringDjb2W((const wchar_t*) API)// compile time hashing macros (used to create variables)
    #define CTIME_HASHA( API ) constexpr auto API##_Rotr32A = HashStringDjb2A((const char*) #API);#define CTIME_HASHW( API ) constexpr auto API##_Rotr32W = HashStringDjb2W((const wchar_t*) L#API);
    
    
    FARPROC GetProcAddressH(HMODULE hModule, DWORD dwApiNameHash) {
    
    	PBYTE pBase = (PBYTE)hModule;
    
    	PIMAGE_DOS_HEADER           pImgDosHdr        = (PIMAGE_DOS_HEADER)pBase;
    	if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE)
    		return NULL;
    
    	PIMAGE_NT_HEADERS           pImgNtHdrs        = (PIMAGE_NT_HEADERS)(pBase + pImgDosHdr->e_lfanew);
    	if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE)
    		return NULL;
    
    	IMAGE_OPTIONAL_HEADER       ImgOptHdr         = pImgNtHdrs->OptionalHeader;
    
    	PIMAGE_EXPORT_DIRECTORY     pImgExportDir     = (PIMAGE_EXPORT_DIRECTORY)(pBase + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
    
    	PDWORD      FunctionNameArray     = (PDWORD)(pBase + pImgExportDir->AddressOfNames);
    	PDWORD      FunctionAddressArray  = (PDWORD)(pBase + pImgExportDir->AddressOfFunctions);
    	PWORD       FunctionOrdinalArray  = (PWORD)(pBase + pImgExportDir->AddressOfNameOrdinals);
    
    	for (DWORD i = 0; i < pImgExportDir->NumberOfFunctions; i++) {
    		CHAR*	pFunctionName       = (CHAR*)(pBase + FunctionNameArray[i]);
    		PVOID	pFunctionAddress    = (PVOID)(pBase + FunctionAddressArray[FunctionOrdinalArray[i]]);
    
    		if (dwApiNameHash == RTIME_HASHA(pFunctionName)) { // runtime hash value check
    			return (FARPROC)pFunctionAddress;
    		}
    	}
    
    	return NULL;
    }

    데모

    이 데모에서는 컴파일 시간 API 해싱을 사용하여 MessageBoxA_Rotr32A 컴파일 시간 변수를 사용하여 MessageBoxA 및 MessageBoxW를 호출합니다.

    IoC 확인

    시스템 내부 문자열 도구를 사용하여 “메시지 상자”를 검색합니다.

    휴지통 도구를 사용하여 MessageBox와 관련된 모든 항목이 있는지 IAT를 확인합니다.

    바이너리 실행

    바이너리를 실행하여 실제로 MessageBox가 사용되고 있는지 확인합니다.

    동적 해시 값 확인

    코드가 컴파일될 때마다 해시값을 콘솔에 출력하여 코드가 수정되고 있는지 확인합니다.

    Visual Studio 프로젝트를 다시 빌드하고 해시 값을 다시 확인하면 해시 값이 이전 실행과 다른 것을 확인할 수 있습니다.


    58. API 후킹 – 소개

    API 후킹 – 소개

    소개

    API 후킹은 API 함수의 동작을 가로채서 수정하는 데 사용되는 기술입니다. 이는 일반적으로 디버깅, 리버스 엔지니어링 및 게임 치팅에 사용됩니다. API 후킹은 API 함수의 원래 구현을 원래 함수를 호출하기 전이나 후에 몇 가지 추가 작업을 수행하는 사용자 정의 버전으로 대체하는 것입니다. 이를 통해 소스 코드를 수정하지 않고도 프로그램의 동작을 수정할 수 있습니다.

    트램펄린

    API 후킹을 구현하는 고전적인 방법은 트램폴린을 통해 이루어집니다. 트램폴린은 프로세스의 주소 공간 내에서 다른 특정 주소로 점프하여 코드 실행 경로를 변경하는 데 사용되는 셸코드입니다. 트램폴린의 셸코드는 함수의 시작 부분에 삽입되어 함수가 후킹됩니다. 후킹된 함수가 호출되면 트램펄린 셸코드가 대신 트리거되고 실행 흐름이 다른 주소로 전달 및 변경되어 다른 함수가 대신 실행되는 결과를 초래합니다.

    인라인 후킹

    인라인 후킹은 트램펄린 기반 후킹과 유사하게 작동하는 API 후킹을 수행하는 또 다른 접근 방식입니다. 차이점은 인라인 후크는 실행을 정상적인 함수로 반환하여 정상적인 실행을 계속할 수 있다는 점입니다. 구현이 더 복잡하고 유지 관리가 더 어려울 수 있지만 인라인 후크가 더 효율적입니다.

    API 후킹은 보안 솔루션이 일반적으로 악용되는 기능을 보다 철저하게 검사할 수 있도록 하기 위해 수행됩니다. 이에 대해서는 향후 모듈에서 더 자세히 설명하겠습니다. 이 모듈에서는 API 후킹이 멀웨어의 능력을 향상시키는 방법을 살펴봅니다.

    API 후킹이 필요한 이유

    API 후킹은 주로 멀웨어 분석 및 디버깅 목적으로 사용되지만, 다음과 같은 이유로 멀웨어 개발에 활용될 수 있습니다:

    • 민감한 정보 또는 데이터(예: 자격 증명) 수집.
    • 악의적인 목적으로 함수 호출을 수정하거나 가로챌 수 있습니다.
    • 운영 체제 또는 프로그램의 작동 방식을 변경하여 보안 조치를 우회합니다(예: AMSI, ETW).

    후킹 구현하기

    API 후킹을 구현하는 방법에는 여러 가지가 있는데, 한 가지 방법은 Microsoft의 Detours 라이브러리 및 Minhook과 같은 오픈 소스 라이브러리를 이용하는 것입니다. 또 다른 제한적인 방법은 API 후킹을 수행하기 위한 Windows API를 사용하는 것입니다(옵션이 제한적이지만).

    다음 몇 개의 모듈에서는 디투어와 민훅을 모두 시연할 예정입니다. 또한 Windows API를 사용하여 어떤 기능을 제공할 수 있는지 살펴볼 것입니다. 마지막으로, 오픈 소스 라이브러리의 사용을 감지하는 데 일반적으로 사용되는 시그니처와 IoC를 줄이기 위한 사용자 지정 후킹 코드가 만들어집니다.


    59. API 후킹 – 우회 라이브러리

    API 후킹 – 우회 라이브러리

    소개

    우회 후킹 라이브러리는 Microsoft Research에서 개발한 소프트웨어 라이브러리로, Windows에서 함수 호출을 가로채고 리디렉션할 수 있습니다. 이 라이브러리는 특정 함수의 호출을 사용자 정의 대체 함수로 리디렉션하여 추가 작업을 수행하거나 원래 함수의 동작을 수정할 수 있습니다. 디커플링은 일반적으로 C/C++ 프로그램에서 사용되며 32비트 및 64비트 애플리케이션 모두에서 사용할 수 있습니다.

    라이브러리의 위키 페이지는 여기에서 확인할 수 있습니다.

    거래

    우회 라이브러리는 대상 함수의 처음 몇 개의 명령어, 즉 후킹할 함수를 사용자가 제공한 우회 함수, 즉 대신 실행할 함수로 무조건 점프하는 것으로 대체합니다. 무조건 점프라는 용어는 트램펄린이라고도 합니다.

    라이브러리는 트랜잭션을 사용하여 대상 함수에서 후크를 설치 및 제거합니다. 트랜잭션을 사용하면 후킹 루틴에서 여러 함수 후크를 그룹화하여 하나의 단위로 적용할 수 있으므로 프로그램의 동작을 여러 번 변경할 때 유용할 수 있습니다. 또한 필요한 경우 사용자가 모든 변경 사항을 쉽게 취소할 수 있다는 장점도 있습니다. 트랜잭션을 사용할 때는 새 트랜잭션을 시작하고 함수 후크를 추가한 다음 커밋할 수 있습니다. 트랜잭션을 커밋하면 언훅할 때와 마찬가지로 트랜잭션에 추가된 모든 함수 훅이 프로그램에 적용됩니다.

    우회 라이브러리 사용

    디투어 라이브러리의 기능을 사용하려면 디투어 저장소를 다운로드하고 컴파일에 필요한 정적 라이브러리 파일(.lib) 파일을 얻기 위해 디투어 저장소를 컴파일해야 합니다. 또한 detours.h 헤더 파일이 포함되어야 하며, 이에 대한 설명은 디투어 위키의 디투어 사용 섹션에 나와 있습니다.

    프로젝트에 .lib 파일을 추가하는 방법에 대한 추가 도움말은 Microsoft의 설명서를 참조하세요.

    32비트와 64비트 우회 라이브러리

    이 모듈의 공유 코드에는 사용 중인 컴퓨터의 아키텍처에 따라 포함할 Detours .lib 파일의 버전을 결정하는 전처리기 코드가 있습니다. 이를 위해 _M_X64 및 _M_IX86 매크로가 사용됩니다. 이러한 매크로는 컴파일러에서 정의하여 컴퓨터가 64비트 또는 32비트 버전의 Windows를 실행 중인지 여부를 나타냅니다. 전처리기 코드는 다음과 같습니다:

    // If compiling as 64-bit
    #ifdef _M_X64#pragma comment (lib, "detoursx64.lib")#endif // _M_X64// If compiling as 32-bit
    #ifdef _M_IX86#pragma comment (lib, "detoursx86.lib")#endif // _M_IX86

    매크로 _M_X64가 정의되어 있는지 확인하고, 정의되어 있으면 그 뒤에 오는 코드가 컴파일에 포함됩니다. 정의되지 않은 경우 해당 코드는 무시됩니다. 마찬가지로 #ifdef _M_IX86은 _M_IX86 매크로가 정의되어 있는지 확인하고, 정의되어 있으면 그 뒤에 오는 코드가 컴파일에 포함됩니다. 64비트 시스템의 경우 #pragma 주석(lib, "detoursx64.lib" )은 컴파일 중에 detoursx64.lib 라이브러리를 링크하는 데 사용되며, 32비트 시스템의 경우 #pragma 주석(lib, "detoursx86.lib “)은 컴파일 중에 detoursx86.lib 라이브러리를 링크하는 데 사용됩니다.

    detoursx64.lib와 detoursx86.lib 파일은 모두 디튜어스 라이브러리를 컴파일할 때 생성되며, 디튜어스 라이브러리를 64비트 프로젝트로 컴파일할 때 detoursx64.lib가 생성되고, 마찬가지로 32비트 프로젝트로 디튜어스 라이브러리를 컴파일할 때 detoursx86.lib가 만들어집니다.

    우회 API 함수

    후킹 방법을 사용할 때 첫 번째 단계는 항상 후킹할 WinAPI 함수의 주소를 검색하는 것입니다. 함수의 주소는 점프 명령어를 배치할 위치를 결정하는 데 필요합니다. 이 모듈에서는 MessageBoxA 함수가 후킹할 함수로 활용됩니다.

    다음은 우회 라이브러리에서 제공하는 API 함수입니다:

    • DetourTransactionBegin – 우회 연결 또는 분리를 위한 새 트랜잭션을 시작합니다. 이 함수는 연결 및 연결 해제 시 가장 먼저 호출되어야 합니다.
    • DetourUpdateThread – 현재 트랜잭션을 업데이트합니다. 디투어 라이브러리에서 현재 트랜잭션에 스레드를 등록하는 데 사용됩니다.
    • DetourAttach – 현재 트랜잭션의 대상 함수에 훅을 설치합니다. 이것은 DetourTransactionCommit이 호출될 때까지 커밋되지 않습니다.
    • DetourDetach – 현재 트랜잭션의 대상 함수에서 훅을 제거합니다. 이것은 DetourTransactionCommit이 호출될 때까지 커밋되지 않습니다.
    • DetourTransactionCommit – 우회 연결 또는 분리를 위한 현재 트랜잭션을 커밋합니다.

    위의 함수는 함수 실행 결과를 파악하는 데 사용되는 LONG 값을 반환합니다. 우회 API는 성공하면 0인 NO_ERROR를 반환하고 실패하면 0이 아닌 값을 반환합니다. 0이 아닌 값은 디버깅 목적의 오류 코드로 사용할 수 있습니다.

    후킹된 API 교체하기

    다음 단계는 후킹된 API를 대체하는 함수를 만드는 것입니다. 대체 함수는 동일한 데이터 유형이어야 하며, 선택적으로 동일한 매개 변수를 사용해야 합니다. 이렇게 하면 매개변수 값을 검사하거나 수정할 수 있습니다. 예를 들어, 다음 함수는 원래 매개변수 값을 확인할 수 있는 MessageBoxA의 우회 함수로 사용할 수 있습니다.

    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
      // we can check hWnd - lpText - lpCaption - uType parametes
    }

    대체 함수는 더 적은 수의 매개 변수를 받을 수 있지만, 잘못된 주소에 액세스하여 액세스 위반 예외를 발생시키므로 원래 함수보다 더 많은 매개 변수를 받을 수 없다는 점에 유의할 필요가 있습니다.

    무한 루프 문제

    후크 함수가 호출되고 후크가 트리거되면 사용자 정의 함수가 실행되지만 실행 흐름이 계속 유지되려면 사용자 정의 함수가 원래 후크 함수가 반환하려고 했던 유효한 값을 반환해야 합니다. 순진한 접근 방식은 후크 내부의 원래 함수를 호출하여 동일한 값을 반환하는 것입니다. 이렇게 하면 대체 함수가 대신 호출되어 무한 루프가 발생하므로 문제가 발생할 수 있습니다. 이는 일반적인 후킹 문제이며 디투어 라이브러리의 버그가 아닙니다.

    이에 대한 이해를 돕기 위해 아래 코드 스니펫은 대체 함수인 MyMessageBoxA가 MessageBoxA를 호출하는 것을 보여줍니다. 그 결과 무한 루프가 발생합니다. 프로그램이 MyMessageBoxA를 실행하다가 멈추게 되는데, 이는 MyMessageBoxA가 MessageBoxA를 호출하고 MessageBoxA가 다시 MyMessageBoxA 함수로 연결되기 때문입니다.

    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
      // Printing original parameters value
      printf("Original lpText Parameter	: %s\n", lpText);
      printf("Original lpCaption Parameter : %s\n", lpCaption);
    
      // DON'T DO THIS
      // Changing the parameters value
      return MessageBoxA(hWnd, "different lpText", "different lpCaption", uType); // Calling MessageBoxA (this is hooked)
    }

    솔루션 1 – 글로벌 원본 함수 포인터

    디투어 라이브러리는 함수를 후킹하기 전에 원래 함수에 대한 포인터를 저장하여 이 문제를 해결할 수 있습니다. 이 포인터를 전역 변수에 저장하고 우회 함수 내에서 후킹된 함수 대신 호출할 수 있습니다.

    // Used as a unhooked MessageBoxA in `MyMessageBoxA`
    fnMessageBoxA g_pMessageBoxA = MessageBoxA;
    
    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
      // Printing original parameters value
      printf("Original lpText Parameter	: %s\n", lpText);
      printf("Original lpCaption Parameter : %s\n", lpCaption);
    
      // Changing the parameters value
      // Calling an unhooked MessageBoxA
      return g_pMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
    }

    해결 방법 2 – 다른 API 사용

    언급할 만한 또 다른 일반적인 해결책은 후킹된 함수와 동일한 기능을 가진 다른 언후킹된 함수를 호출하는 것입니다. 예를 들어 MessageBoxA와 MessageBoxWVirtualAlloc과 VirtualAllocEx가 있습니다.

    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
      // Printing original parameters value
      printf("Original lpText Parameter	: %s\n", lpText);
      printf("Original lpCaption Parameter : %s\n", lpCaption);
    
      // Changing the parameters value
      return MessageBoxW(hWnd, L"different lpText", L"different lpCaption", uType);
    }

    우회 후킹 루틴

    앞서 설명한 것처럼 Detours 라이브러리는 트랜잭션을 사용하여 작동하므로 API 함수를 후킹하려면 트랜잭션을 생성하고, 트랜잭션에 액션(후킹/언후킹)을 제출한 다음 트랜잭션을 커밋해야 합니다. 아래 코드 스니펫은 이러한 단계를 수행합니다.

    // Used as a unhooked MessageBoxA in `MyMessageBoxA`
    // And used by `DetourAttach` & `DetourDetach`
    fnMessageBoxA g_pMessageBoxA = MessageBoxA;
    
    
    // The function that will run instead MessageBoxA when hooked
    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    
    	printf("[+] Original Parameters : \n");
    	printf("\t - lpText	: %s\n", lpText);
    	printf("\t - lpCaption	: %s\n", lpCaption);
    
    	return g_pMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
    }
    
    
    BOOL InstallHook() {
    
    	DWORD	dwDetoursErr = NULL;
    
      	// Creating the transaction & updating it
    	if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
    		printf("[!] DetourTransactionBegin Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
    	if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
    		printf("[!] DetourUpdateThread Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
      	// Running MyMessageBoxA instead of g_pMessageBoxA that is MessageBoxA
    	if ((dwDetoursErr = DetourAttach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
    		printf("[!] DetourAttach Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
      	// Actual hook installing happen after `DetourTransactionCommit` - commiting the transaction
    	if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
    		printf("[!] DetourTransactionCommit Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
    	return TRUE;
    }

    연결 해제 루틴 우회

    아래 코드 스니펫은 이전 섹션과 동일한 루틴을 보여 주지만 언훅을 위한 것입니다.

    // Used as a unhooked MessageBoxA in `MyMessageBoxA`
    // And used by `DetourAttach` & `DetourDetach`
    fnMessageBoxA g_pMessageBoxA = MessageBoxA;
    
    
    // The function that will run instead MessageBoxA when hooked
    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    
    	printf("[+] Original Parameters : \n");
    	printf("\t - lpText	: %s\n", lpText);
    	printf("\t - lpCaption	: %s\n", lpCaption);
    
    	return g_pMessageBoxA(hWnd, "different lpText", "different lpCaption", uType);
    }
    
    
    BOOL Unhook() {
    
    	DWORD	dwDetoursErr = NULL;
    
      	// Creating the transaction & updating it
    	if ((dwDetoursErr = DetourTransactionBegin()) != NO_ERROR) {
    		printf("[!] DetourTransactionBegin Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
    	if ((dwDetoursErr = DetourUpdateThread(GetCurrentThread())) != NO_ERROR) {
    		printf("[!] DetourUpdateThread Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
      	// Removing the hook from MessageBoxA
    	if ((dwDetoursErr = DetourDetach((PVOID)&g_pMessageBoxA, MyMessageBoxA)) != NO_ERROR) {
    		printf("[!] DetourDetach Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
      	// Actual hook removal happen after `DetourTransactionCommit` - commiting the transaction
    	if ((dwDetoursErr = DetourTransactionCommit()) != NO_ERROR) {
    		printf("[!] DetourTransactionCommit Failed With Error : %d \n", dwDetoursErr);
    		return FALSE;
    	}
    
    	return TRUE;
    }
    

    주요 기능

    이전에 표시된 후킹 및 언후킹 루틴에는 메인 함수가 포함되어 있지 않습니다. 메인 함수는 아래에 표시되어 있으며, 이는 단순히 훅 해제된 버전과 훅된 버전의 MessageBoxA를 호출합니다.

    int main() {
    
        // Will run - not hooked
    	MessageBoxA(NULL, "What Do You Think About Malware Development ?", "Original MsgBox", MB_OK | MB_ICONQUESTION);
    
    
    //------------------------------------------------------------------
        //  Hooking
    	if (!InstallHook())
    	    return -1;
    
    //------------------------------------------------------------------
        // Won't run - will run MyMessageBoxA instead
    	MessageBoxA(NULL, "Malware Development Is Bad", "Original MsgBox", MB_OK | MB_ICONWARNING);
    
    
    //------------------------------------------------------------------
        //  Unhooking
    	if (!Unhook())
    	    return -1;
    
    //------------------------------------------------------------------
        //  Will run - hook removed
    	MessageBoxA(NULL, "Normal MsgBox Again", "Original MsgBox", MB_OK | MB_ICONINFORMATION);
    
      	return 0;
    }
    

    데모

    첫 번째 MessageBoxA 실행(언훅)

    두 번째 MessageBoxA 실행(Hooked)

    세 번째 MessageBoxA 실행(언훅)


    60. API 후킹 – 민훅 라이브러리

    API 후킹 – 민훅 라이브러리

    소개

    민후크는 API 후킹을 구현하는 데 사용할 수 있는 C로 작성된 후킹 라이브러리입니다. Windows의 32비트 및 64비트 애플리케이션과 모두 호환되며, 디투어 라이브러리와 유사하게 인라인 후킹을 위해 x86/x64 어셈블리를 사용합니다. 다른 후킹 라이브러리에 비해 MinHook은 더 간단하고 가벼운 API를 제공하므로 작업하기가 더 쉽습니다.

    민훅 라이브러리 사용

    디투어 라이브러리와 마찬가지로, 민훅 라이브러리도 정적 .lib 파일과 MinHook.h 헤더 파일을 Visual Studio 프로젝트에 포함시켜야 합니다.

    민훅 API 함수

    훅 라이브러리는 훅의 설치 또는 제거에 필요한 정보를 담고 있는 구조를 초기화하는 방식으로 작동합니다. 이 작업은 라이브러리에서 HOOK_ENTRY 구조를 초기화하는 MH_Initialize API를 통해 수행됩니다. 다음으로 MH_CreateHook 함수를 사용하여 훅을 생성하고 MH_EnableHook을 사용하여 훅을 활성화합니다. MH_DisableHook을 사용하여 훅을 제거하고 마지막으로 MH_Uninitialize를 사용하여 초기화 된 구조를 정리합니다. 편의를 위해 아래에 함수를 다시 나열했습니다.

    Minhook API는 Minhook.h에 있는 사용자 정의 열거형인 MH_STATUS 값을 반환합니다. 반환된 MH_STATUS 데이터 유형은 지정된 함수의 오류 코드를 나타냅니다. 함수가 성공하면 0인 MH_OK 값이 반환되고 오류가 발생하면 0이 아닌 값이 반환됩니다.

    MH_Initialize와 MH_Uninitialize 함수는 각각 프로그램의 시작과 끝에서 한 번만 호출해야 한다는 점에 유의할 필요가 있습니다.

    우회 기능

    이 모듈은 이전 모듈의 동일한 MessageBoxA API 예제를 활용하며, 다른 메시지 상자를 실행하도록 후킹 및 변경됩니다.

    fnMessageBoxA g_pMessageBoxA = NULL;
    
    INT WINAPI MyMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) {
    
    	printf("[+] Original Parameters : \n");
    	printf("\t - lpText	: %s\n", lpText);
    	printf("\t - lpCaption	: %s\n", lpCaption);
    
    	return g_pMessageBoxA(hWnd, "Different lpText", "Different lpCaption", uType);
    }

    메시지 상자를 실행하기 위해 g_pMessageBoxA 전역 변수가 사용되는 것을 확인할 수 있는데, 여기서 g_pMessageBoxA는 후크되지 않은 원래의 MessageBoxA API에 대한 포인터입니다. 이 변수가 NULL로 설정된 이유는 g_pMessageBoxA가 수동으로 설정된 디투어 라이브러리와는 달리, 사용할 수 있도록 초기화하는 것은 민훅 MH_CreateHook API 호출이기 때문입니다. 이는 이전 모듈에서 설명한 후킹 루프 문제의 발생을 방지하기 위한 것입니다.

    민훅 후킹 루틴

    앞서 언급했듯이 Minhook을 사용하여 특정 API를 후킹하려면 먼저 MH_Initialize 함수를 실행해야 합니다. 그런 다음 MH_CreateHook으로 후크를 생성하고 MH_EnableHook으로 활성화할 수 있습니다.

    BOOL InstallHook() {
    
    	DWORD 	dwMinHookErr = NULL;
    
    	if ((dwMinHookErr = MH_Initialize()) != MH_OK) {
    		printf("[!] MH_Initialize Failed With Error : %d \n", dwMinHookErr);
    		return FALSE;
    	}
    
    	// Installing the hook on MessageBoxA, to run MyMessageBoxA instead
    	// g_pMessageBoxA will be a pointer to the original MessageBoxA function
    	if ((dwMinHookErr = MH_CreateHook(&MessageBoxA, &MyMessageBoxA, &g_pMessageBoxA)) != MH_OK) {
    		printf("[!] MH_CreateHook Failed With Error : %d \n", dwMinHookErr);
    		return FALSE;
    	}
    
    	// Enabling the hook on MessageBoxA
    	if ((dwMinHookErr = MH_EnableHook(&MessageBoxA)) != MH_OK) {
    		printf("[!] MH_EnableHook Failed With Error : %d \n", dwMinHookErr);
    		return -1;
    	}
    
    	return TRUE;
    }

    민훅 언훅킹 루틴

    디투어 라이브러리와 달리 민훅 라이브러리는 트랜잭션을 사용할 필요가 없습니다. 대신 후크를 제거하려면 후킹된 함수의 주소로 MH_DisableHook API를 실행하기만 하면 됩니다. MH_Uninitialize 호출은 선택 사항이지만 이전 MH_Initialize 호출로 초기화된 구조를 정리합니다.

    BOOL Unhook() {
    
    	DWORD 	dwMinHookErr = NULL;
    
    	if ((dwMinHookErr = MH_DisableHook(&MessageBoxA)) != MH_OK) {
    		printf("[!] MH_DisableHook Failed With Error : %d \n", dwMinHookErr);
    		return -1;
    	}
    
    	if ((dwMinHookErr = MH_Uninitialize()) != MH_OK) {
    		printf("[!] MH_Uninitialize Failed With Error : %d \n", dwMinHookErr);
    		return -1;
    	}
    }

    주요 기능

    이전에 표시된 후킹 및 언후킹 루틴에는 메인 함수가 포함되어 있지 않습니다. 메인 함수는 아래에 표시되어 있으며, 이는 단순히 훅 해제된 버전과 훅된 버전의 MessageBoxA를 호출합니다.

    int main() {
    
    	//  will run
    	MessageBoxA(NULL, "What Do You Think About Malware Development ?", "Original MsgBox", MB_OK | MB_ICONQUESTION);
    
    	//  hooking
    	if (!InstallHook())
    		return -1;
    
    	//  wont run - hooked
    	MessageBoxA(NULL, "Malware Development Is Bad", "Original MsgBox", MB_OK | MB_ICONWARNING);
    
    	//  unhooking
    	if (!Unhook())
    		return -1;
    
    	//  will run - hook disabled
    	MessageBoxA(NULL, "Normal MsgBox Again", "Original MsgBox", MB_OK | MB_ICONINFORMATION);
    
    	return 0;
    }
    

    데모

    첫 번째 MessageBoxA 실행(언훅)

    두 번째 MessageBoxA 실행(Hooked)

    세 번째 MessageBoxA 실행(언훅)

    Share