Microsoft MVP성태의 닷넷 이야기
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 4개 있습니다.)
(시리즈 글이 5개 있습니다.)
.NET Framework: 431. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요?
; https://www.sysnet.pe.kr/2/0/1659

.NET Framework: 461. .NET EXE 파일을 닷넷 프레임워크 버전에 상관없이 실행할 수 있을까요? - 두 번째 이야기
; https://www.sysnet.pe.kr/2/0/1746

VC++: 129. EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법
; https://www.sysnet.pe.kr/2/0/11858

디버깅 기술: 126. windbg - .NET x86 CLR2/CLR4 EXE의 EntryPoint
; https://www.sysnet.pe.kr/2/0/11861

디버깅 기술: 127. windbg - .NET x64 EXE의 EntryPoint
; https://www.sysnet.pe.kr/2/0/11863




EXE를 LoadLibrary로 로딩해 PE 헤더에 있는 EntryPoint를 직접 호출하는 방법

EXE 파일에는 DLL과는 다르게 운영체제가 로딩 후 호출을 하는 진입점(EntryPoint)이 있습니다. Visual C++의 경우, 컴파일하면 응용 프로그램의 종류에 따라 진입점 위치를 CRT(msvcr dll)에 포함하고 있는 다음의 함수로 설정해 둡니다.

  • 콘솔 프로그램: mainCRTStartup 또는 wmainCRTStartup
  • 윈도우 프로그램: WinMainCRTStartup 또는 wWinMainCRTStartup
  • DLL: _DllMainCRTStartup

그렇다면, 우리도 EXE 프로그램을 로딩해 EntryPoint 함수를 직접 부르는 것이 가능하지 않을까요? ^^




이것을 테스트하기 위해 C/C++ 콘솔 프로젝트 2개를 준비합니다.

  • ConsoleApplication1: exe_entry EXE를 로드해 EntryPoint를 직접 호출하는 콘솔 프로젝트
  • exe_entry: DLL처럼 로드되는 EXE 콘솔 프로젝트

그리고 exe_entry의 경우 다음과 같이 간단하게 코딩을 하고,

int main()
{
    return 5;
}

아래의 글에서 설명한 방법에 따라 CRT 의존성을 제거합니다.

CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거
; https://www.sysnet.pe.kr/2/0/1437

Configuration Properties
    C/C++
        Code Generation
            Basic Runtime Checks: Default
            Security Check: No (/GS-)

Configuration Properties
    Linker
        Advanced
            Entry Point: main

그다음, ConsoleApplication1에서는 다음과 같은 식으로 LoadLibrary를 이용해 exe_entry.exe를 로드하면 PE 헤더 정보를 이용해 EntryPoint를 찾아 실행할 수 있습니다.

#include <Windows.h>
#include <stdio.h>
#include <DbgHelp.h>

#pragma comment(lib, "dbghelp.lib")

typedef int(*MainFunc)(void);

int main()
{
    LPVOID hInstance = ::LoadLibrary(L"exe_entry.exe");

    PIMAGE_NT_HEADERS ntHeader = ImageNtHeader(hInstance);
    MainFunc pfnMain = (MainFunc)(DWORD_PTR)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)hInstance);
    printf("result %d\n", pfnMain());
}

/*
출력 결과
result: 5
*/




일단, CRT가 없는 exe_entry 실행 파일에 대해서는 성공했으니 이제 일반적인 CRT 의존성이 있는 EXE의 EntryPoint에 대고 호출해 보면 어떨까요? 아쉽게도 이번에는 실행하는 경우 다음과 같은 식의 AV 예외가 발생합니다.

Exception thrown at 0x0001B7AC in ConsoleApplication1.exe: 0xC0000005: Access violation executing location 0x0001B7AC. occurred

Disassembly 창을 이용해 디버깅으로 추적해 들어가 보면,

_mainCRTStartup:
009C132F E9 FC 0B 00 00       jmp         mainCRTStartup (09C1F30h)

// d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp
extern "C" int mainCRTStartup()
{
009C1F30 55                   push        ebp  
009C1F31 8B EC                mov         ebp,esp  
    return __scrt_common_main();
009C1F33 E8 58 FC FF FF       call        __scrt_common_main (09C1B90h)  
}

// d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl ----

static __forceinline int __cdecl __scrt_common_main()
{
009C1B90 55                   push        ebp  
009C1B91 8B EC                mov         ebp,esp  
    // The /GS security cookie must be initialized before any exception handling
    // targeting the current image is registered.  No function using exception
    // handling can be called in the current image until after this call:
    __security_init_cookie();
009C1B93 E8 24 F7 FF FF       call        ___security_init_cookie (09C12BCh)  

    return __scrt_common_main_seh();
009C1B98 E8 13 00 00 00       call        __scrt_common_main_seh (09C1BB0h)  
}

___security_init_cookie:
009C12BC E9 AF 1E 00 00       jmp         __security_init_cookie (09C3170h)  

// d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\gs\gs_support.c

void __cdecl __security_init_cookie(void)
{
009C3170 55                   push        ebp  
009C3171 8B EC                mov         ebp,esp  
009C3173 51                   push        ecx  
    UINT_PTR cookie;

	// ...[생략]...

    /*
     * Initialize the global cookie with an unpredictable value which is
     * different for each module in a process.
     */
    cookie = __get_entropy();
009C319C E8 3F FF FF FF       call        __get_entropy (09C30E0h)  
009C31A1 89 45 FC             mov         dword ptr [cookie],eax  

	// ...[생략]...
}

//  d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\gs\gs_support.c

static UINT_PTR __get_entropy(void)
{
009C30E0 55                   push        ebp  
009C30E1 8B EC                mov         ebp,esp  
009C30E3 83 EC 14             sub         esp,14h  
    UINT_PTR cookie;
    FT systime = { 0 };
009C30E6 33 C0                xor         eax,eax  
009C30E8 89 45 F4             mov         dword ptr [systime],eax  
009C30EB 89 45 F8             mov         dword ptr [ebp-8],eax  
    LARGE_INTEGER perfctr;

    GetSystemTimeAsFileTime(&systime.ft_struct);
009C30EE 8D 4D F4             lea         ecx,[systime]  
009C30F1 51                   push        ecx  
009C30F2 FF 15 3C B0 9C 00    call        dword ptr [__imp__GetSystemTimeAsFileTime@4 (09CB03Ch)]  // <== 예외 발생
#if defined (_WIN64)
    cookie = systime.ft_scalar;
	// ...[생략]...
}

AV 예외가 Win32 API인 GetSystemTimeAsFileTime 함수를 호출하면서 발생하는 것을 확인할 수 있습니다. 이때의 IAT에서 __imp__GetSystemTimeAsFileTime의 주소(09CB03Ch)에 있는 값은,

0x009CB03C  ac b7 01 00  ....

0x0001b7ac이고, 바로 이 값이 AV 예외 발생 메시지 창에 나온 그 주소입니다.

Exception thrown at 0x0001B7AC in ConsoleApplication1.exe: 0xC0000005: Access violation executing location 0x0001B7AC. occurred

실제로 Disassembly 창을 이용해 0x0001B7AC 위치의 코드를 보면

0001B7AC ??                   ?? ?? 
0001B7AD ??                   ?? ?? 
0001B7AE ??                   ?? ?? 
0001B7AF ??                   ?? ?? 
0001B7B0 ??                   ?? ?? 

해당 가상 주소는 commit된 적이 없는 빈 주소 공간입니다. 결국, Win32 API 함수의 IAT 주솟값이 엉뚱한 값을 가지고 있다는 것이 문제입니다.




이런 현상이 발생하는 원인은 간단합니다. 윈도우는 DLL 파일에 대해서만 해당 PE 파일의 IAT 주솟값을 패치시켜주기 때문에 EXE 모듈을 로드했을 때는 (DONT_RESOLVE_DLL_REFERENCES 옵션을 적용해 DLL을 LoadLibary한 것처럼) 쓰레기로 채워져 있던 것입니다. 이 문제를 간단하게 해결하고 싶다면 EXE 파일의 PE 헤더에 Characteristics 값을,

// winnt.h

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

"DLL" 속성도 함께 추가하면 됩니다. 이를 위해 dnSpy.exe 도구를 이용하면 다음과 같이 간단하게 "DLL" 속성을 추가하고 저장할 수 있습니다.

call_exe_entry_point_1.png

그림에 보이는 "Characteristics" 값이 0x2102인데 다음의 상수 조합에 해당합니다.

#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved 
#define IMAGE_FILE_32BIT_MACHINE             0x0100  // 32 bit word machine.
#define IMAGE_FILE_DLL                       0x2000  // File is a DLL.

이제 다시 CRT 링크된 EXE의 EntryPoint 함수를 LoadLibrary를 이용해 호출하면 DLL을 로드하는 경우와 동일하게 IAT 패칭을 운영체제가 대신 수행해 주기 때문에 EntryPoint 함수 호출이 정상적으로 실행되는 것을 확인할 수 있습니다.




물론 dnSpy.exe를 이용하지 않고 직접 IMAGE_FILE_DLL 속성을 PE 포맷에 따라 설정하도록 만드는 프로그램을 제작하는 것도 가능합니다.

// 아래의 소스 코드를 살짝 변경시켰습니다.
// setdllcharacteristics 
// https://blog.didierstevens.com/2010/10/17/setdllcharacteristics/

#include <stdio.h>
#include <string.h>

#define PEINDEX_POSITION 0x3C
#define CHARACTERISTICS_OFFSET 0x16

int main(int argc, char *argv[])
{
    FILE *fPEFile;
    char *pszFilename;
    unsigned int uiPEHEADERIndex;
    unsigned short int wDLLCHARACTERISTICS;
    int iIter;

    if (1 == argc)
    {
        fprintf(stderr, "Usage: setcharacteristics file\n");
        return -1;
    }

    pszFilename = argv[argc-1];
    if ((fPEFile = fopen(pszFilename, "r+b")) == NULL)
    {
        fprintf(stderr, "Error: opening file %s\n", pszFilename);
        return -1;
    }

    if (fseek(fPEFile, PEINDEX_POSITION, 0) != 0)
    {
        fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, PEINDEX_POSITION);
        fclose(fPEFile);
        return -1;
    }

    if (fread(&uiPEHEADERIndex, sizeof(uiPEHEADERIndex), 1, fPEFile) != 1)
    {
        fprintf(stderr, "Error: reading file %s\n", pszFilename);
        fclose(fPEFile);
        return -1;
    }

    if (fseek(fPEFile, uiPEHEADERIndex + CHARACTERISTICS_OFFSET, 0) != 0)
    {
        fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, uiPEHEADERIndex + CHARACTERISTICS_OFFSET);
        fclose(fPEFile);
        return -1;
    }

    if (fread(&wDLLCHARACTERISTICS, sizeof(wDLLCHARACTERISTICS), 1, fPEFile) != 1)
    {
        fprintf(stderr, "Error: reading file %s\n", pszFilename);
        fclose(fPEFile);
        return -1;
    }

    printf("Original CHARACTERISTICS = 0x%04X\n", wDLLCHARACTERISTICS);

    wDLLCHARACTERISTICS |= 0x2000;

    if (fseek(fPEFile, uiPEHEADERIndex + CHARACTERISTICS_OFFSET, 0) != 0)
    {
        fprintf(stderr, "Error: fseek file %s %04X\n", pszFilename, uiPEHEADERIndex + CHARACTERISTICS_OFFSET);
        fclose(fPEFile);
        return -1;
    }

    if (fwrite(&wDLLCHARACTERISTICS, sizeof(wDLLCHARACTERISTICS), 1, fPEFile) != 1)
    {
        fprintf(stderr, "Error: writing file %s\n", pszFilename);
        fclose(fPEFile);
        return -1;
    }

    printf("Updated  CHARACTERISTICS = 0x%04X\n", wDLLCHARACTERISTICS);

    fclose(fPEFile);

    return 0;
}

Visual C++로 컴파일한 후,

C:\temp\setcharacteristics> cl setcharacteristics.c
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27027.1 for x86
Copyright (C) Microsoft Corporation.  All rights reserved.

setcharacteristics.c
Microsoft (R) Incremental Linker Version 14.16.27027.1
Copyright (C) Microsoft Corporation.  All rights reserved.

/out:setcharacteristics.exe
setcharacteristics.obj

EXE 프로젝트의 "Build Events" / "Post-Build Event"에 다음과 같은 식으로 실행해 두도록 만들면 됩니다.

$(SolutionDir)setcharacteristics.exe $(TargetPath)




IMAGE_FILE_DLL 속성을 추가하고 싶지 않다면, 반대로 LoadLibrary를 호출한 측에서 IAT 주소를 패치하는 작업을 직접 수행해도 됩니다. 이 작업 역시 다음의 글에서 ^^ 친절하게 이미 다 설명하고 있습니다.

Load EXE as DLL: Mission Possible
; https://www.codeproject.com/Articles/1045674/Load-EXE-as-DLL-Mission-Possible

따라서 다음과 같이 코드를 추가해 주면,

#include <Windows.h>
#include <stdio.h>
#include <DbgHelp.h>

#pragma comment(lib, "dbghelp.lib")

typedef int(*MainFunc)(void);

void PatchIAT(HINSTANCE h);

int main()
{
    // LPVOID hInstance = ::LoadLibrary(L"exe_entry.exe"); // EXE without CRT

    LPVOID hInstance = ::LoadLibrary(L"exe_dll_entry.exe"); // EXE with CRT

    PatchIAT((HINSTANCE)hInstance);

    PIMAGE_NT_HEADERS ntHeader = ImageNtHeader(hInstance);

    MainFunc pfnMain = (MainFunc)(DWORD_PTR)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)hInstance);
    printf("result %d\n", pfnMain());
}

// https://www.codeproject.com/Articles/1045674/Load-EXE-as-DLL-Mission-Possible
void PatchIAT(HINSTANCE h)
{
    // Find the IAT size
    DWORD ulsize = 0;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(h, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulsize);
    if (!pImportDesc)
        return;

    // Loop names
    for (; pImportDesc->Name; pImportDesc++)
    {
        PSTR pszModName = (PSTR)((PBYTE)h + pImportDesc->Name);
        if (!pszModName)
            break;

        HINSTANCE hImportDLL = LoadLibraryA(pszModName);
        if (!hImportDLL)
        {
            // ... (error)
        }

        // Get caller's import address table (IAT) for the callee's functions
        PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)h + pImportDesc->FirstThunk);

        // Replace current function address with new function address
        for (; pThunk->u1.Function; pThunk++)
        {
            FARPROC pfnNew = 0;
            size_t rva = 0;
#ifdef _WIN64
            if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG64)
#else
            if (pThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG32)
#endif
            {
                // Ordinal
#ifdef _WIN64
                size_t ord = IMAGE_ORDINAL64(pThunk->u1.Ordinal);
#else
                size_t ord = IMAGE_ORDINAL32(pThunk->u1.Ordinal);
#endif

                PROC* ppfn = (PROC*)& pThunk->u1.Function;
                if (!ppfn)
                {
                    // ... (error)
                }
                rva = (size_t)pThunk;

                char fe[100] = { 0 };
                sprintf_s(fe, 100, "#%u", ord);
                pfnNew = GetProcAddress(hImportDLL, (LPCSTR)ord);
                if (!pfnNew)
                {
                    // ... (error)
                }
            }
            else
            {
                // Get the address of the function address
                PROC* ppfn = (PROC*)& pThunk->u1.Function;
                if (!ppfn)
                {
                    // ... (error)
                }
                rva = (size_t)pThunk;
                PSTR fName = (PSTR)h;
                fName += pThunk->u1.Function;
                fName += 2;
                if (!fName)
                    break;
                pfnNew = GetProcAddress(hImportDLL, fName);
                if (!pfnNew)
                {
                    // ... (error)
                }
            }

            // Patch it now...
            auto hp = GetCurrentProcess();
            if (!WriteProcessMemory(hp, (LPVOID*)rva, &pfnNew, sizeof(pfnNew), NULL) && (ERROR_NOACCESS == GetLastError()))
            {
                DWORD dwOldProtect;
                if (VirtualProtect((LPVOID)rva, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect))
                {
                    if (!WriteProcessMemory(GetCurrentProcess(), (LPVOID*)rva, &pfnNew, sizeof(pfnNew), NULL))
                    {
                        // ... (error)
                    }
                    if (!VirtualProtect((LPVOID)rva, sizeof(pfnNew), dwOldProtect, &dwOldProtect))
                    {
                        // ... (error)
                    }
                }
            }
        }
    }
}

역시 아무런 문제없이 EntryPoint 함수가 실행됩니다.

(첨부 파일은 이 글의 예제 프로젝트를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 2/11/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13497정성태12/22/20232276오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232279Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232271Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232469Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232520닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232247개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232200Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232296개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232056개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20231992오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232277개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232089개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20231986오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232058개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232193닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232725닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232173개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/20232477개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/20232206개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/20232409닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/20232140닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/20232200닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/20232068개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/20232261닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/20232080C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/20232129Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...