Post

Writing custom Shellcode - C++ to ASM to Shellcode

Learn how to easily write custom Shellcode by converting C++ to ASM to Shellcode.

Writing custom shellcode can make the process of evading AVs much easier. However, writing Assembly code line by line can be quite labour-intensive.

A solution proposed in Hasherezade’s paper is to first create a C/C++ program, then convert it to assembly code. After generating a .exe from this ASM code, you can extract the shellcode from the .text section.

This approach allows for easier development of custom shellcode, and we can directly use the shellcode with shellcode injection techniques.

In this post, I will use Maldev academy’s Local Payload execution technique.

Table of Contents

  1. Preparing Dev Environment
  2. Converting C++ to Assembly
  3. Fixing the .asm file
    1. Remove dependencies from external libraries
    2. Fix Stack Alignment
    3. Remove PDATA and XDATA Segments
    4. Fix Syntax Issues
  4. Linking the ASM code to an EXE
  5. Testing the EXE
  6. Copying and Running the Shellcode from the EXE
  7. References

1. Preparing Dev Environment

1. Search for VsDevCmd.bat. This file should be located at:

1
%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\Tools

2. You can start by running the following command:

1
2
cmd /k "C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat"

  • e17df2f1d0167b81c9c97606d4e43a3a.png

2. Converting C++ to Assembly

We will convert this 2 C++ files to Assembly:

  • file.cpp - the program that pops a message box.

  • peb-lookup.h - header file required by the file.cpp, which contains functions for resolving addresses for LoadLibraryA and GetProcAddress

Using the Dev Environment from before, you can convert the C++ code to Assembly:

  • Don’t forget to use the x64 version of cl.exe.
1
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.40.33807\bin\Hostx64\x64\cl.exe" /c /FAs /GS- file.cpp
  • c1097bb23a149697c2974341d09c805a.png

This should generate a .asm file:

  • 3aabccf10419ebc335ba5b08f14081cd.png

file.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <Windows.h>
#include "peb-lookup.h"

// It's worth noting that strings can be defined inside the .text section:
#pragma code_seg(".text")
int main()
{
    // Stack based strings for libraries and functions the shellcode needs
    wchar_t kernel32_dll_name[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0 };
    char load_lib_name[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 };
    char get_proc_name[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s', 0 };
    char user32_dll_name[] = { 'u','s','e','r','3','2','.','d','l','l', 0 };
    char message_box_name[] = { 'M','e','s','s','a','g','e','B','o','x','W', 0 };

    // stack based strings to be passed to the messagebox win api
    wchar_t msg_content[] = { 'H','e','l','l','o', ' ', 'W','o','r','l','d','!', 0 };
    wchar_t msg_title[] = { 'D','e','m','o','!', 0 };

    // resolve kernel32 image base
    LPVOID base = get_module_by_name((const LPWSTR)kernel32_dll_name);
    if (!base) {
        return 1;
    }

    // resolve loadlibraryA() address
    LPVOID load_lib = get_func_by_name((HMODULE)base, (LPSTR)load_lib_name);
    if (!load_lib) {
        return 2;
    }

    // resolve getprocaddress() address
    LPVOID get_proc = get_func_by_name((HMODULE)base, (LPSTR)get_proc_name);
    if (!get_proc) {
        return 3;
    }

    // loadlibrarya and getprocaddress function definitions
    HMODULE(WINAPI * _LoadLibraryA)(LPCSTR lpLibFileName) = (HMODULE(WINAPI*)(LPCSTR))load_lib;
    FARPROC(WINAPI * _GetProcAddress)(HMODULE hModule, LPCSTR lpProcName)
        = (FARPROC(WINAPI*)(HMODULE, LPCSTR)) get_proc;

    // load user32.dll
    LPVOID u32_dll = _LoadLibraryA(user32_dll_name);

    // messageboxw function definition
    int (WINAPI * _MessageBoxW)(
        _In_opt_ HWND hWnd,
        _In_opt_ LPCWSTR lpText,
        _In_opt_ LPCWSTR lpCaption,
        _In_ UINT uType) = (int (WINAPI*)(
            _In_opt_ HWND,
            _In_opt_ LPCWSTR,
            _In_opt_ LPCWSTR,
            _In_ UINT)) _GetProcAddress((HMODULE)u32_dll, message_box_name);

    if (_MessageBoxW == NULL) return 4;


    // invoke the message box winapi
    _MessageBoxW(0, msg_content, msg_title, MB_OK);

    return 0;
}

PEB-lookup header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#pragma once
#include <Windows.h>

#ifndef __NTDLL_H__

#ifndef TO_LOWERCASE
#define TO_LOWERCASE(out, c1) (out = (c1 <= 'Z' && c1 >= 'A') ? c1 = (c1 - 'A') + 'a': c1)
#endif


typedef struct _UNICODE_STRING
{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;

} UNICODE_STRING, * PUNICODE_STRING;

typedef struct _PEB_LDR_DATA
{
    ULONG Length;
    BOOLEAN Initialized;
    HANDLE SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
    PVOID      EntryInProgress;

} PEB_LDR_DATA, * PPEB_LDR_DATA;

//here we don't want to use any functions imported form extenal modules

typedef struct _LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY  InLoadOrderModuleList;
    LIST_ENTRY  InMemoryOrderModuleList;
    LIST_ENTRY  InInitializationOrderModuleList;
    void* BaseAddress;
    void* EntryPoint;
    ULONG   SizeOfImage;
    UNICODE_STRING FullDllName;
    UNICODE_STRING BaseDllName;
    ULONG   Flags;
    SHORT   LoadCount;
    SHORT   TlsIndex;
    HANDLE  SectionHandle;
    ULONG   CheckSum;
    ULONG   TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;


typedef struct _PEB
{
    BOOLEAN InheritedAddressSpace;
    BOOLEAN ReadImageFileExecOptions;
    BOOLEAN BeingDebugged;
    BOOLEAN SpareBool;
    HANDLE Mutant;

    PVOID ImageBaseAddress;
    PPEB_LDR_DATA Ldr;

    // [...] this is a fragment, more elements follow here

} PEB, * PPEB;

#endif //__NTDLL_H__

inline LPVOID get_module_by_name(WCHAR* module_name)
{
    PPEB peb = NULL;
#if defined(_WIN64)
    peb = (PPEB)__readgsqword(0x60);
#else
    peb = (PPEB)__readfsdword(0x30);
#endif
    PPEB_LDR_DATA ldr = peb->Ldr;
    LIST_ENTRY list = ldr->InLoadOrderModuleList;

    PLDR_DATA_TABLE_ENTRY Flink = *((PLDR_DATA_TABLE_ENTRY*)(&list));
    PLDR_DATA_TABLE_ENTRY curr_module = Flink;

    while (curr_module != NULL && curr_module->BaseAddress != NULL) {
        if (curr_module->BaseDllName.Buffer == NULL) continue;
        WCHAR* curr_name = curr_module->BaseDllName.Buffer;

        size_t i = 0;
        for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) {
            WCHAR c1, c2;
            TO_LOWERCASE(c1, module_name[i]);
            TO_LOWERCASE(c2, curr_name[i]);
            if (c1 != c2) break;
        }
        if (module_name[i] == 0 && curr_name[i] == 0) {
            //found
            return curr_module->BaseAddress;
        }
        // not found, try next:
        curr_module = (PLDR_DATA_TABLE_ENTRY)curr_module->InLoadOrderModuleList.Flink;
    }
    return NULL;
}

inline LPVOID get_func_by_name(LPVOID module, char* func_name)
{
    IMAGE_DOS_HEADER* idh = (IMAGE_DOS_HEADER*)module;
    if (idh->e_magic != IMAGE_DOS_SIGNATURE) {
        return NULL;
    }
    IMAGE_NT_HEADERS* nt_headers = (IMAGE_NT_HEADERS*)((BYTE*)module + idh->e_lfanew);
    IMAGE_DATA_DIRECTORY* exportsDir = &(nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
    if (exportsDir->VirtualAddress == NULL) {
        return NULL;
    }

    DWORD expAddr = exportsDir->VirtualAddress;
    IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(expAddr + (ULONG_PTR)module);
    SIZE_T namesCount = exp->NumberOfNames;

    DWORD funcsListRVA = exp->AddressOfFunctions;
    DWORD funcNamesListRVA = exp->AddressOfNames;
    DWORD namesOrdsListRVA = exp->AddressOfNameOrdinals;

    //go through names:
    for (SIZE_T i = 0; i < namesCount; i++) {
        DWORD* nameRVA = (DWORD*)(funcNamesListRVA + (BYTE*)module + i * sizeof(DWORD));
        WORD* nameIndex = (WORD*)(namesOrdsListRVA + (BYTE*)module + i * sizeof(WORD));
        DWORD* funcRVA = (DWORD*)(funcsListRVA + (BYTE*)module + (*nameIndex) * sizeof(DWORD));

        LPSTR curr_name = (LPSTR)(*nameRVA + (BYTE*)module);
        size_t k = 0;
        for (k = 0; func_name[k] != 0 && curr_name[k] != 0; k++) {
            if (func_name[k] != curr_name[k]) break;
        }
        if (func_name[k] == 0 && curr_name[k] == 0) {
            //found
            return (BYTE*)module + (*funcRVA);
        }
    }
    return NULL;
}

3. Fixing the .asm file

After converting the C++ code to assembly, we need to clean up the file a bit, so we can link it to an .exe without errors and to avoid the shellcode from crashing.

Specifically, we need to:

  1. Remove dependencies from external libraries
  2. Fix Stack Alignment
  3. Remove PDATA and XDATA Segments
  4. Fix Syntax Issues

3.1 Remove dependencies from external libraries

Remove or comment out the libraries LIBCMT and OLDNAMES.

  • 78a72023f71401504817b08850f75281.png

3.2 Fix Stack Alignment

Add procedure AlignRSP right at the top of the first _TEXT segment in our c-shellcode.asm:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; https://github.com/mattifestation/PIC_Bindshell/blob/master/PIC_Bindshell/AdjustStack.asm

; AlignRSP is a simple call stub that ensures that the stack is 16-byte aligned prior
; to calling the entry point of the payload. This is necessary because 64-bit functions
; in Windows assume that they were called with 16-byte stack alignment. When amd64
; shellcode is executed, you can't be assured that you stack is 16-byte aligned. For example,
; if your shellcode lands with 8-byte stack alignment, any call to a Win32 function will likely
; crash upon calling any ASM instruction that utilizes XMM registers (which require 16-byte)
; alignment.

AlignRSP PROC
    push rsi ; Preserve RSI since we're stomping on it
    mov rsi, rsp ; Save the value of RSP so it can be restored
    and rsp, 0FFFFFFFFFFFFFFF0h ; Align RSP to 16 bytes
    sub rsp, 020h ; Allocate homing space for ExecutePayload
    call main ; Call the entry point of the payload
    mov rsp, rsi ; Restore the original value of RSP
    pop rsi ; Restore RSI
    ret ; Return to caller
AlignRSP ENDP

3.3 Remove PDATA and XDATA Segments

Remove or comment out PDATA and XDATA segments as shown below:

  • bfa537cf43a3812de7f5f4882809f9d4.png

3.4 Fix Syntax Issues

Now, we just need to fix the syntax issues.

We can use the error we get while linking the MASM file to an EXE to assist in fixing syntax.

Possible errors:

  • error A2027: operand must be a memory expression
    • Cause: mov rax, QWORD PTR gs:96
    • Solution: Add square brackets []
    • mov rax, QWORD PTR gs:[96]
  • error A2006: undefined symbol : FLAT
    • Cause: lea r8, OFFSET FLAT:$SG93137
    • Solution: Remove :FLAT
    • lea r8, OFFSET $SG93137
  • error LNK2019: unresolved external symbol __imp_MessageBoxA referenced in function main file.exe : fatal error LNK1120: 1 unresolved externals
    • Cause: missing user32.lib
    • Solution: Add the following line to the ASM code includelib user32.lib.
    • Note: This should happen because we loaded the library in the C++ code.
    • Ensure the libpath is pointing to x64 version. So, add this flag to the cl.exe command:
    • /LIBPATH:"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.22621.0\um\x64"

4. Linking the ASM code to an EXE

Now, you need to link the assembly listings inside file.asm to get an executable file.exe:

1
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.40.33807\bin\Hostx64\x64\ml64.exe" file.asm /link /entry:AlignRSP
  • c919fdc18fb391df6c4146918bd590fc.png

5. Testing the EXE

To check if the last step was successful, try running the file.exe that was created from the file.asm.

  • c28b6e89743b8d1ce7337f9057f3d7bc.png

6. Copying and Running the Shellcode from the EXE

Now that we have a working exe, we can extract the shellcode and execute it using any Payload execution techniques.

We can use an hex editor to extract the shellcode from the .text section.

I will use CFF Explorer in this case.

  1. Find the Raw Address of .text and .rdata section:
    • For me, the start of .text was at 0x200 and for .rdata at 0xA00
    • 7b52718224179a7e4137ab1a3bb64879.png
  2. Go to Hex editor, and scroll down until the offset corresponds to the Raw Address you found earlier.
    • This will be the start of your shellcode:
    • df11b9fe1532a12a8c3cb43c42170f9b.png
  3. Copy the shellcode starting at the raw address of .text and stopping before the raw address of .rdata.
    • For me, it was from 0x200 to 0x9F0.
    • Note: 0x9F0 is the address before 0xA00, .rdata address.
    • 348c88458956786d792937260878ce5f.png
  4. Since I’m going to use this shellcode on a C/C++ script, I can copy it to C/C++ array:
    • a091074a5b951420fdfdf07c72d178e8.png
  5. You can use this shellcode with a shellcode injection techniques.
  6. In this case, I used the Local Payload Execution technique from Maldev academy.
    • 5e307e12025fc94dd6bdff1af77d84d4.png

References

  1. https://medium.com/@shaddy43/the-epitome-of-evasion-a-custom-shellcode-c751a1a17e5b
  2. https://www.ired.team/offensive-security/code-injection-process-injection/writing-and-compiling-shellcode-in-c
This post is licensed under CC BY 4.0 by the author.