This article was first published on the Xianzhi Community.

What is Shellcode?

Shellcode is a specially designed, position-independent binary code that is typically used as a payload in exploits to perform specific operations, such as spawning a shell or gaining control over a system.

To write position-independent code, the following points should be taken into account:

The Mechanism Behind DLL Loading on Windows

In Windows, applications cannot directly access system calls. Instead, they use functions from the Windows API (WinAPI), which are stored in DLLs such as kernel32.dll, advapi32.dll, gdi32.dll, etc. ntdll.dll and kernel32.dll are especially important, as every process imports them.

Here is the program I wrote, called nothing_to_do. I used listdlls to list the imported DLLs.

image

DLL Addressing

The TEB (Thread Environment Block) structure contains thread information in user mode. On 32-bit systems, we can use the FS register to find the address of the PEB (Process Environment Block) at offset 0x30.

PEB.ldr points to the PEB_LDR_DATA structure, which contains information about the loaded modules, including the base addresses of kernel32 and ntdll.

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

PEB_LDR_DATA.InMemoryOrderModuleList contains the head of a doubly linked list of the modules loaded in the process. Each entry in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure.

typedef struct _LIST_ENTRY
{
     PLIST_ENTRY Flink;
     PLIST_ENTRY Blink;
} LIST_ENTRY, *PLIST_ENTRY;

Information about the loaded DLL in LDR_DATA_TABLE_ENTRY:

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

Tips:
In Windows versions prior to Vista, the first two DLLs in InInitializationOrderModuleList are ntdll.dll and kernel32.dll, but for Vista and later versions, the second DLL is changed to kernelbase.dll.
In InMemoryOrderModuleList, the first entry is calc.exe (the executable), the second is ntdll.dll, and the third is kernel32.dll. This method currently applies to all versions of Windows and is the preferred approach.

Kernel32.dll Addressing Process:

image

Implementing with Assembly Code:

xor ecx, ecx
mov ebx, fs:[ecx + 0x30]    ; *ebx = PEB base address
mov ebx, [ebx+0x0c]         ; ebx = PEB.Ldr
mov esi, [ebx+0x14]         ; ebx = PEB.Ldr.InMemoryOrderModuleList
lodsd                       ; eax = Second module
xchg eax, esi               ; eax = esi, esi = eax
lodsd                       ; eax = Third(kernel32)
mov ebx, [eax + 0x10]       ; ebx = dll Base address

Function Addressing in Kernel32.dll Export Table

Previously, I studied PE structure-related materials here.

ImageOptionalHeader32.DataDirectory[0].VirtualAddress points to the Export Table RVA. The structure of the Export Table is as follows:

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;      // Timestamp
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;               // Pointer to the export table's filename string
    DWORD   Base;               // Starting ordinal of the export table
    DWORD   NumberOfFunctions;  // Number of exported functions (more accurately, the number of elements in AddressOfFunctions, not the number of functions)
    DWORD   NumberOfNames;      // Number of functions exported by name
    DWORD   AddressOfFunctions;     // Exported function address table RVA: stores the addresses of all exported functions (table element width is 4, total size is NumberOfFunctions * 4)
    DWORD   AddressOfNames;         // Exported function names table RVA: stores the addresses of function name strings (table element width is 4, total size is NumberOfNames * 4)
    DWORD   AddressOfNameOrdinals;  // Exported function ordinal table RVA: stores the function ordinals (table element width is 2, total size is NumberOfNames * 2)
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

image

Implementing with Assembly Code:

;; Find PE export table
mov edx, [ebx + 0x3c]   ; Find the e_lfanew offset in the DOS header
add edx, ebx            ; edx =  pe header
mov edx, [edx + 0x78]   ; edx = offset export table
add edx, ebx            ; edx = export table
mov esi, [edx + 0x20]   ; esi = offset names table
add esi, ebx            ; esi = names table

;; Find the `Winexec` Function Name
xor ecx, ecx
Get_Function:
    inc ecx                         ; ecx++
    lodsd                           ; eax = Next function name string RVA
    add eax, ebx                    ; eax = Function name string pointer
    cmp dword ptr[eax], 0x456E6957  ; eax[0:4] == EniW
    jnz Get_Function
dec ecx;

;;Find the Winexec Function Pointer
mov esi, [edx + 0x24]     ; esi = ordianl table rva
add esi, ebx              ; esi = ordianl table
mov cx, [esi + ecx * 2]   ; ecx = func ordianl
mov esi, [edx + 0x1c]     ; esi = address table rva
add esi, ebx              ; esi = address table
mov edx, [esi + ecx * 4]  ; edx = func address rva
add edx, ebx              ; edx = func address

;; Call the Winexec Function
xor eax, eax
push edx
push eax        ; 0x00
push 0x6578652e
push 0x636c6163
push 0x5c32336d
push 0x65747379
push 0x535c7377
push 0x6f646e69
push 0x575c3a43
mov esi, esp    ; esi = "C:\\Windows\\System32\\calc.exe"
push 10         ; window state SW_SHOWDEFAULT
push esi        ; "C:\\Windows\\System32\\calc.exe"
call edx        ; WinExec(esi, 10)

Final Code:

int main()
{
    __asm {
        ; Find where kernel32.dll is loaded into memory
        xor ecx, ecx
        mov ebx, fs:[ecx + 0x30]    ; *ebx = PEB base address
        mov ebx, [ebx+0x0c]         ; ebx = PEB.Ldr
        mov esi, [ebx+0x14]         ; ebx = PEB.Ldr.InMemoryOrderModuleList
        lodsd                       ; eax = Second module
        xchg eax, esi               ; eax = esi, esi = eax
        lodsd                       ; eax = Third(kernel32)
        mov ebx, [eax + 0x10]       ; ebx = dll Base address

        ;; Find PE export table
        mov edx, [ebx + 0x3c]   ; Find the e_lfanew offset in the DOS header
        add edx, ebx            ; edx =  pe header
        mov edx, [edx + 0x78]   ; edx = offset export table
        add edx, ebx            ; edx = export table
        mov esi, [edx + 0x20]   ; esi = offset names table
        add esi, ebx            ; esi = names table

        ;; Find the `Winexec` Function Name
        xor ecx, ecx
        Get_Function:
            inc ecx                         ; ecx++
            lodsd                           ; eax = Next function name string RVA
            add eax, ebx                    ; eax = Function name string pointer
            cmp dword ptr[eax], 0x456E6957  ; eax[0:4] == EniW
            jnz Get_Function
        dec ecx;

        ;;Find the Winexec Function Pointer
        mov esi, [edx + 0x24]     ; esi = ordianl table rva
        add esi, ebx              ; esi = ordianl table
        mov cx, [esi + ecx * 2]   ; ecx = func ordianl
        mov esi, [edx + 0x1c]     ; esi = address table rva
        add esi, ebx              ; esi = address table
        mov edx, [esi + ecx * 4]  ; edx = func address rva
        add edx, ebx              ; edx = func address

        ;; Call the Winexec Function
        xor eax, eax
        push edx
        push eax        ; 0x00
        push 0x6578652e
        push 0x636c6163
        push 0x5c32336d
        push 0x65747379
        push 0x535c7377
        push 0x6f646e69
        push 0x575c3a43
        mov esi, esp    ; esi = "C:\\Windows\\System32\\calc.exe"
        push 10         ; window state SW_SHOWDEFAULT
        push esi        ; "C:\\Windows\\System32\\calc.exe"
        call edx        ; WinExec(esi, 10)

        ; exit
        add esp, 0x1c
        pop eax
        pop edx
    }
    return 0;
}

It was compiled using Visual Studio, resulting in a very large size. By rewriting it in MASM, I was able to produce a much smaller file: shellcode.asm.

Extract Shellcode

Compile MASM:

F:\\> ml -c -coff .\\shellcode.asm
F:\\> link -subsystem:windows .\\shellcode.obj

There are two methods to extract shellcode:

  1. Use dumpbin.exe: $ dumpbin.exe /ALL .\\shellcode.obj
    image
  2. Extract the PE .text section Data: starting from PointerToRawData, and retrieve data of size VirtualSize:
    image

Shellcode Loader

Write a loader in Golang, code: loader.go. Compile using the Makefile, and the run it:

You did it 🎉

Reference