├── .gitignore ├── Dockerfile ├── src ├── shellcode.c ├── runtime.h ├── payload_msgbox.c └── runtime.c ├── LICENSE ├── tools ├── encoder.py ├── clean_asm.py ├── pe_extract.py └── handle_asm.py ├── Makefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | tools/__pycache__ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:stable-slim 2 | 3 | RUN apt update && apt install -y --no-install-recommends \ 4 | mingw-w64 \ 5 | make \ 6 | python3 \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | WORKDIR /workspace -------------------------------------------------------------------------------- /src/shellcode.c: -------------------------------------------------------------------------------- 1 | #include "runtime.h" 2 | 3 | void payload_main(SC_ENV *env); 4 | 5 | int main(void) { 6 | SC_ENV env; 7 | sc_init_env(&env); 8 | payload_main(&env); 9 | return 0; 10 | } 11 | 12 | #include "runtime.c" 13 | #include "payload_msgbox.c" -------------------------------------------------------------------------------- /src/runtime.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | typedef struct _SC_ENV { 5 | HMODULE kernel32; 6 | HMODULE (WINAPI *pLoadLibraryA)(LPCSTR); 7 | FARPROC (WINAPI *pGetProcAddress)(HMODULE, LPCSTR); 8 | } SC_ENV; 9 | 10 | void sc_init_env(SC_ENV *env); -------------------------------------------------------------------------------- /src/payload_msgbox.c: -------------------------------------------------------------------------------- 1 | #include "runtime.h" 2 | 3 | void payload_main(SC_ENV *env) { 4 | if (!env->pLoadLibraryA || !env->pGetProcAddress) { 5 | return; 6 | } 7 | 8 | char user32_dll_name[] = { 'u','s','e','r','3','2','.','d','l','l', 0 }; 9 | char message_box_name[] = { 'M','e','s','s','a','g','e','B','o','x','W', 0 }; 10 | 11 | wchar_t msg_content[] = { 'H','e','l','l','o',' ','W','o','r','l','d','!', 0 }; 12 | wchar_t msg_title[] = { 'D','e','m','o','!', 0 }; 13 | 14 | HMODULE u32 = env->pLoadLibraryA(user32_dll_name); 15 | if (!u32) { 16 | return; 17 | } 18 | 19 | int (WINAPI *pMessageBoxW)( 20 | HWND, 21 | LPCWSTR, 22 | LPCWSTR, 23 | UINT 24 | ) = (int (WINAPI*)(HWND, LPCWSTR, LPCWSTR, UINT)) 25 | env->pGetProcAddress(u32, message_box_name); 26 | 27 | if (!pMessageBoxW) { 28 | return; 29 | } 30 | 31 | pMessageBoxW(0, msg_content, msg_title, MB_OK); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Matt Kiely 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tools/encoder.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | XOR_KEY = 0x5A 4 | 5 | 6 | def build_xor_stub(payload_len: int, key: int) -> bytes: 7 | if payload_len <= 0 or payload_len > 0xFFFFFFFF: 8 | raise ValueError("Payload length must be in 1..0xFFFFFFFF") 9 | 10 | stub = bytearray([ 11 | # 0: lea rsi, [rip+0x17] ; payload starts 30 bytes from stub start 12 | 0x48, 0x8D, 0x35, 0x17, 0x00, 0x00, 0x00, 13 | # 7: mov ecx, ; using placeholder bytes 14 | 0xB9, 0x00, 0x00, 0x00, 0x00, 15 | # 12: mov al, ; using placeholder bytes 16 | 0xB0, 0x00, 17 | # 14: xor byte ptr [rsi], al 18 | 0x30, 0x06, 19 | # 16: inc rsi 20 | 0x48, 0xFF, 0xC6, 21 | # 19: loop decode_loop (back -7 bytes) 22 | 0xE2, 0xF9, 23 | # 21: lea rax, [rip+0x2] ; payload again 24 | 0x48, 0x8D, 0x05, 0x02, 0x00, 0x00, 0x00, 25 | # 28: jmp rax 26 | 0xFF, 0xE0, 27 | ]) 28 | 29 | # Patch length (little-endian) at offset 8 30 | stub[8:12] = payload_len.to_bytes(4, byteorder="little") 31 | # Patch XOR key at offset 13 32 | stub[13] = key & 0xFF 33 | 34 | return bytes(stub) 35 | 36 | 37 | def xor_encode(payload: bytes, key: int) -> bytes: 38 | return bytes(b ^ (key & 0xFF) for b in payload) 39 | -------------------------------------------------------------------------------- /tools/clean_asm.py: -------------------------------------------------------------------------------- 1 | # Ref: https://github.com/mattifestation/PIC_Bindshell/blob/master/PIC_Bindshell/AdjustStack.asm#L24 2 | ALIGN_STUB = r""" 3 | .globl AlignRSP 4 | AlignRSP: 5 | push rsi 6 | mov rsi, rsp 7 | and rsp, -16 8 | sub rsp, 0x20 9 | call main 10 | mov rsp, rsi 11 | pop rsi 12 | ret 13 | """ 14 | 15 | _META_PREFIXES = ( 16 | ".file", 17 | ".ident", 18 | ".cfi_", 19 | ".loc", 20 | ".def", 21 | ".scl", 22 | ".type", 23 | ".endef", 24 | ".seh_", 25 | ".linkonce", 26 | ) 27 | 28 | _ALIGN_PREFIXES = ( 29 | ".p2align", 30 | ".align", 31 | ".balign", 32 | ) 33 | 34 | 35 | def _should_drop_line(stripped: str) -> bool: 36 | if stripped.startswith(_META_PREFIXES): 37 | return True 38 | if stripped.startswith(".extern") or stripped.startswith("EXTERN"): 39 | return True 40 | if "__main" in stripped: 41 | return True 42 | if stripped.startswith(_ALIGN_PREFIXES): 43 | return True 44 | return False 45 | 46 | 47 | def clean_asm_source(text: str) -> str: 48 | lines = text.splitlines() 49 | cleaned = [] 50 | align_inserted = False 51 | 52 | for line in lines: 53 | stripped = line.lstrip() 54 | 55 | if _should_drop_line(stripped): 56 | continue 57 | 58 | if stripped.startswith(".section"): 59 | if ".rdata" in stripped or ".data" in stripped: 60 | line = " .text" 61 | elif ".text$" in stripped or ".text.startup" in stripped: 62 | line = " .text" 63 | 64 | if stripped.startswith(".data") or stripped.startswith(".rdata"): 65 | line = " .text" 66 | 67 | if not align_inserted and ( 68 | stripped.startswith(".text") 69 | or (stripped.startswith(".section") and ".text" in stripped) 70 | ): 71 | cleaned.append(line) 72 | cleaned.append(ALIGN_STUB) 73 | align_inserted = True 74 | continue 75 | 76 | cleaned.append(line) 77 | 78 | if not align_inserted: 79 | cleaned.append(ALIGN_STUB) 80 | 81 | return "\n".join(cleaned) 82 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME = shellcode-build-pipeline:latest 2 | 3 | SRC_DIR = src 4 | BUILD_DIR = build 5 | TOOLS_DIR = tools 6 | 7 | CC = x86_64-w64-mingw32-gcc 8 | PYTHON = python3 9 | 10 | C_SRC = $(SRC_DIR)/shellcode.c 11 | ASM_OUT = $(BUILD_DIR)/c-shellcode.s 12 | CLEAN_ASM_OUT = $(BUILD_DIR)/c-shellcode_cleaned.asm 13 | SHELL_OBJ = $(BUILD_DIR)/c-shellcode.o 14 | SHELL_EXE = $(BUILD_DIR)/c-shellcode.exe 15 | SHELL_BIN = $(BUILD_DIR)/c-shellcode_64_encoded.bin 16 | 17 | CFLAGS_ASM = -std=c11 -O2 -Wall \ 18 | -S -masm=intel \ 19 | -fno-asynchronous-unwind-tables \ 20 | -fno-stack-protector \ 21 | -ffreestanding 22 | 23 | ASFLAGS_CLEAN = -c -x assembler-with-cpp 24 | LDFLAGS_PE = -O2 -Wall -nostdlib -nodefaultlibs -Wl,--entry=AlignRSP 25 | 26 | .PHONY: all asm cleaned_asm pe shellcode clean \ 27 | docker-build docker-asm docker-cleaned-asm docker-pe docker-shellcode docker-clean 28 | 29 | all: docker-shellcode 30 | 31 | asm: $(ASM_OUT) 32 | 33 | cleaned_asm: $(CLEAN_ASM_OUT) 34 | 35 | pe: $(SHELL_EXE) 36 | 37 | shellcode: $(SHELL_BIN) 38 | 39 | $(BUILD_DIR): 40 | mkdir -p $(BUILD_DIR) 41 | 42 | $(ASM_OUT): $(C_SRC) | $(BUILD_DIR) 43 | $(CC) $(CFLAGS_ASM) -o $@ $(C_SRC) 44 | 45 | $(CLEAN_ASM_OUT): $(ASM_OUT) $(TOOLS_DIR)/handle_asm.py | $(BUILD_DIR) 46 | $(PYTHON) $(TOOLS_DIR)/handle_asm.py clean $(ASM_OUT) $(CLEAN_ASM_OUT) 47 | 48 | $(SHELL_OBJ): $(CLEAN_ASM_OUT) | $(BUILD_DIR) 49 | $(CC) $(ASFLAGS_CLEAN) -o $@ $(CLEAN_ASM_OUT) 50 | 51 | $(SHELL_EXE): $(SHELL_OBJ) | $(BUILD_DIR) 52 | $(CC) $(LDFLAGS_PE) -o $@ $(SHELL_OBJ) 53 | 54 | $(SHELL_BIN): $(SHELL_EXE) $(TOOLS_DIR)/handle_asm.py | $(BUILD_DIR) 55 | $(PYTHON) $(TOOLS_DIR)/handle_asm.py extract $(SHELL_EXE) $(SHELL_BIN) 56 | 57 | clean: 58 | rm -rf $(BUILD_DIR) 59 | 60 | docker-build: 61 | docker build -t $(IMAGE_NAME) . 62 | 63 | docker-asm: docker-build 64 | docker run --rm -v $(PWD):/workspace -w /workspace $(IMAGE_NAME) \ 65 | make asm 66 | 67 | docker-cleaned-asm: docker-build 68 | docker run --rm -v $(PWD):/workspace -w /workspace $(IMAGE_NAME) \ 69 | make cleaned_asm 70 | 71 | docker-pe: docker-build 72 | docker run --rm -v $(PWD):/workspace -w /workspace $(IMAGE_NAME) \ 73 | make pe 74 | 75 | docker-shellcode: docker-build 76 | docker run --rm -v $(PWD):/workspace -w /workspace $(IMAGE_NAME) \ 77 | make shellcode 78 | 79 | docker-clean: 80 | docker rmi $(IMAGE_NAME) || true 81 | -------------------------------------------------------------------------------- /tools/pe_extract.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import sys 3 | from typing import Tuple 4 | 5 | 6 | def extract_text_section(pe_bytes: bytes) -> Tuple[bytes, int]: 7 | if len(pe_bytes) < 0x100: 8 | raise ValueError("PE file too small") 9 | 10 | e_lfanew = struct.unpack_from(" len(pe_bytes): 49 | raise ValueError(".text raw data out of range") 50 | 51 | text_section = pe_bytes[raw_ptr:raw_ptr + raw_size] 52 | print( 53 | f"[*] .text section: VirtualAddr=0x{text_virtual_addr:x}, " 54 | f"RawPtr=0x{text_raw_ptr:x}, RawSize=0x{text_raw_size:x}", 55 | file=sys.stderr, 56 | ) 57 | break 58 | 59 | if text_section is None: 60 | raise ValueError("No .text section found") 61 | 62 | if text_virtual_addr is None or text_raw_size is None: 63 | raise ValueError("Invalid .text section layout") 64 | 65 | if text_virtual_addr <= entry_point_rva < text_virtual_addr + text_raw_size: 66 | entry_offset = entry_point_rva - text_virtual_addr 67 | print(f"[*] Entry point offset in .text: 0x{entry_offset:x}", file=sys.stderr) 68 | else: 69 | print( 70 | f"[!] WARNING: Entry point RVA (0x{entry_point_rva:x}) is outside .text!", 71 | file=sys.stderr, 72 | ) 73 | entry_offset = 0 74 | 75 | return text_section, entry_offset 76 | -------------------------------------------------------------------------------- /tools/handle_asm.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from clean_asm import clean_asm_source 5 | from pe_extract import extract_text_section 6 | from encoder import XOR_KEY, build_xor_stub, xor_encode 7 | 8 | def print_c_array(shellcode: bytes, varname: str = "shellcode"): 9 | print("") 10 | print("/* ================== C SHELLCODE ARRAY ================== */") 11 | print(f"unsigned char {varname}[] = {{") 12 | 13 | line = " " 14 | for i, b in enumerate(shellcode): 15 | line += f"0x{b:02x}, " 16 | if (i + 1) % 16 == 0: 17 | print(line) 18 | line = " " 19 | if line.strip(): 20 | print(line) 21 | 22 | print("};") 23 | print(f"unsigned int {varname}_len = {len(shellcode)};") 24 | print("/* ======================================================== */") 25 | print("") 26 | 27 | def do_clean(input_path: Path, output_path: Path) -> None: 28 | src = input_path.read_text(encoding="utf-8") 29 | cleaned = clean_asm_source(src) 30 | output_path.write_text(cleaned, encoding="utf-8") 31 | print(f"[+] Cleaned assembly written to {output_path}", file=sys.stderr) 32 | 33 | 34 | def do_extract(input_exe: Path, output_bin: Path) -> None: 35 | pe_bytes = input_exe.read_bytes() 36 | text_bytes, entry_offset = extract_text_section(pe_bytes) 37 | payload = text_bytes[entry_offset:] 38 | payload = payload.rstrip(b"\x00\x90\xcc") 39 | print(f"[+] Raw payload length: {len(payload)} bytes", file=sys.stderr) 40 | encoded = xor_encode(payload, XOR_KEY) 41 | stub = build_xor_stub(len(encoded), XOR_KEY) 42 | final_shellcode = stub + encoded 43 | 44 | print( 45 | f"[+] Final shellcode (stub + encoded payload): {len(final_shellcode)} bytes", 46 | file=sys.stderr, 47 | ) 48 | 49 | output_bin.write_bytes(final_shellcode) 50 | print_c_array(final_shellcode, varname="shellcode_64") 51 | 52 | 53 | def main(): 54 | if len(sys.argv) < 2: 55 | print("Usage:", file=sys.stderr) 56 | print(" handle_asm.py clean ", file=sys.stderr) 57 | print(" handle_asm.py extract ", file=sys.stderr) 58 | sys.exit(1) 59 | 60 | mode = sys.argv[1] 61 | 62 | if mode == "clean": 63 | if len(sys.argv) != 4: 64 | print("Usage: handle_asm.py clean ", file=sys.stderr) 65 | sys.exit(1) 66 | inp = Path(sys.argv[2]) 67 | outp = Path(sys.argv[3]) 68 | do_clean(inp, outp) 69 | return 70 | 71 | if mode == "extract": 72 | if len(sys.argv) != 4: 73 | print("Usage: handle_asm.py extract ", file=sys.stderr) 74 | sys.exit(1) 75 | exe_path = Path(sys.argv[2]) 76 | bin_path = Path(sys.argv[3]) 77 | do_extract(exe_path, bin_path) 78 | return 79 | 80 | print(f"Unknown mode: {mode}", file=sys.stderr) 81 | sys.exit(1) 82 | 83 | 84 | if __name__ == "__main__": 85 | main() 86 | -------------------------------------------------------------------------------- /src/runtime.c: -------------------------------------------------------------------------------- 1 | // Adapted from "From a C project, through assembly, to shellcode" 2 | // by hasherezade for @vxunderground 3 | // Ref: https://raw.githubusercontent.com/hasherezade/masm_shc/master/docs/FromaCprojectthroughassemblytoshellcode.pdf 4 | 5 | #include "runtime.h" 6 | 7 | #ifndef TO_LOWERCASE 8 | #define TO_LOWERCASE(out, c1) (out = (c1 <= 'Z' && c1 >= 'A') ? c1 = (c1 - 'A') + 'a' : c1) 9 | #endif 10 | 11 | typedef struct _UNICODE_STRING { 12 | USHORT Length; 13 | USHORT MaximumLength; 14 | PWSTR Buffer; 15 | } UNICODE_STRING, *PUNICODE_STRING; 16 | 17 | typedef struct _PEB_LDR_DATA { 18 | ULONG Length; 19 | BOOLEAN Initialized; 20 | HANDLE SsHandle; 21 | LIST_ENTRY InLoadOrderModuleList; 22 | LIST_ENTRY InMemoryOrderModuleList; 23 | LIST_ENTRY InInitializationOrderModuleList; 24 | PVOID EntryInProgress; 25 | } PEB_LDR_DATA, *PPEB_LDR_DATA; 26 | 27 | typedef struct _LDR_DATA_TABLE_ENTRY { 28 | LIST_ENTRY InLoadOrderModuleList; 29 | LIST_ENTRY InMemoryOrderModuleList; 30 | LIST_ENTRY InInitializationOrderModuleList; 31 | void* BaseAddress; 32 | void* EntryPoint; 33 | ULONG SizeOfImage; 34 | UNICODE_STRING FullDllName; 35 | UNICODE_STRING BaseDllName; 36 | ULONG Flags; 37 | SHORT LoadCount; 38 | SHORT TlsIndex; 39 | HANDLE SectionHandle; 40 | ULONG CheckSum; 41 | ULONG TimeDateStamp; 42 | } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; 43 | 44 | typedef struct _PEB { 45 | BOOLEAN InheritedAddressSpace; 46 | BOOLEAN ReadImageFileExecOptions; 47 | BOOLEAN BeingDebugged; 48 | BOOLEAN SpareBool; 49 | HANDLE Mutant; 50 | PVOID ImageBaseAddress; 51 | PPEB_LDR_DATA Ldr; 52 | } PEB, *PPEB; 53 | 54 | static LPVOID get_module_by_name(WCHAR* module_name) { 55 | PPEB peb = NULL; 56 | #if defined(_WIN64) 57 | peb = (PPEB)__readgsqword(0x60); 58 | #else 59 | peb = (PPEB)__readfsdword(0x30); 60 | #endif 61 | PPEB_LDR_DATA ldr = peb->Ldr; 62 | LIST_ENTRY *head = &ldr->InLoadOrderModuleList; 63 | LIST_ENTRY *curr = head->Flink; 64 | 65 | while (curr && curr != head) { 66 | PLDR_DATA_TABLE_ENTRY mod = (PLDR_DATA_TABLE_ENTRY)curr; 67 | if (mod->BaseDllName.Buffer != NULL) { 68 | WCHAR *curr_name = mod->BaseDllName.Buffer; 69 | size_t i = 0; 70 | for (i = 0; module_name[i] != 0 && curr_name[i] != 0; i++) { 71 | WCHAR c1, c2; 72 | TO_LOWERCASE(c1, module_name[i]); 73 | TO_LOWERCASE(c2, curr_name[i]); 74 | if (c1 != c2) { 75 | break; 76 | } 77 | } 78 | if (module_name[i] == 0 && curr_name[i] == 0) { 79 | return mod->BaseAddress; 80 | } 81 | } 82 | curr = curr->Flink; 83 | } 84 | return NULL; 85 | } 86 | 87 | static LPVOID get_func_by_name(LPVOID module, char* func_name) { 88 | IMAGE_DOS_HEADER* idh = (IMAGE_DOS_HEADER*)module; 89 | if (idh->e_magic != IMAGE_DOS_SIGNATURE) { 90 | return NULL; 91 | } 92 | IMAGE_NT_HEADERS* nt_headers = (IMAGE_NT_HEADERS*)((BYTE*)module + idh->e_lfanew); 93 | IMAGE_DATA_DIRECTORY* exportsDir = &(nt_headers->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]); 94 | if (exportsDir->VirtualAddress == 0) { 95 | return NULL; 96 | } 97 | 98 | DWORD expAddr = exportsDir->VirtualAddress; 99 | IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)(expAddr + (ULONG_PTR)module); 100 | SIZE_T namesCount = exp->NumberOfNames; 101 | 102 | DWORD funcsListRVA = exp->AddressOfFunctions; 103 | DWORD funcNamesListRVA = exp->AddressOfNames; 104 | DWORD namesOrdsListRVA = exp->AddressOfNameOrdinals; 105 | 106 | for (SIZE_T i = 0; i < namesCount; i++) { 107 | DWORD* nameRVA = (DWORD*)((BYTE*)module + funcNamesListRVA + i * sizeof(DWORD)); 108 | WORD* nameIndex = (WORD*)((BYTE*)module + namesOrdsListRVA + i * sizeof(WORD)); 109 | DWORD* funcRVA = (DWORD*)((BYTE*)module + funcsListRVA + (*nameIndex) * sizeof(DWORD)); 110 | 111 | LPSTR curr_name = (LPSTR)((BYTE*)module + *nameRVA); 112 | size_t k = 0; 113 | for (k = 0; func_name[k] != 0 && curr_name[k] != 0; k++) { 114 | if (func_name[k] != curr_name[k]) { 115 | break; 116 | } 117 | } 118 | if (func_name[k] == 0 && curr_name[k] == 0) { 119 | return (BYTE*)module + (*funcRVA); 120 | } 121 | } 122 | return NULL; 123 | } 124 | 125 | void sc_init_env(SC_ENV *env) { 126 | WCHAR kernel32_dll_name[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0 }; 127 | char load_lib_name[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 }; 128 | char get_proc_name[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s', 0 }; 129 | 130 | LPVOID base = get_module_by_name(kernel32_dll_name); 131 | if (!base) { 132 | env->kernel32 = NULL; 133 | env->pLoadLibraryA = NULL; 134 | env->pGetProcAddress = NULL; 135 | return; 136 | } 137 | 138 | env->kernel32 = (HMODULE)base; 139 | env->pLoadLibraryA = (HMODULE (WINAPI*)(LPCSTR))get_func_by_name(base, load_lib_name); 140 | env->pGetProcAddress = (FARPROC (WINAPI*)(HMODULE, LPCSTR))get_func_by_name(base, get_proc_name); 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # windows-x64-shellcode-pipeline 2 | 3 | A Dockerized build pipeline for custom Windows x64 shellcode, including a custom encoder stub and fully automated payload preparation. 4 | 5 | ## Background 6 | 7 | I highly recommend trying to build Windows shellcode from scratch at least once. It's a checkpoint of understanding malware development and Windows internals. But authoring shellcode by writing assembly directly is something I wouldn't wish on my worst ops. 8 | 9 | Inspired by [@hasherezade's](https://github.com/hasherezade) excellent paper [From a C project, through assembly, to shellcode](https://raw.githubusercontent.com/hasherezade/masm_shc/master/docs/FromaCprojectthroughassemblytoshellcode.pdf), this is a full build pipeline for customized x64 Windows shellcode that allows users to author payloads in C rather than assembly. Writing shellcode directly in assembly is time consuming, tedious, and error prone. Compiling a C program tailored for position-independent execution and subsequentally extracting the code from that PE is a much better option, which is what her paper covers. 10 | 11 | Her paper details how to perform the process entirely within the Visual Studio dev environment, which usually means developing within Windows. I wanted to port this process over to a Dockerized build pipeline that supports the entire process and cross-compiles the source into the final product. This project also implements the automated post-processing that her paper mentions. 12 | 13 | ### Features 14 | 15 | - Allows you to author the final payloads as modular, ergonomic C source rather than error-prone assembly. 16 | - Builds the shellcode in stages: first as the assembly of a scaffolded PE built for PIC execution, then with automated Python post-processing to clean the resulting assembly, and finally as a PIC `.bin` file ready for execution 17 | - Handles potential stack alignment errors by implementing Matt Graeber's tried-and-true [AlignRSP stub](https://github.com/mattifestation/PIC_Bindshell/blob/master/PIC_Bindshell/AdjustStack.asm#L24) 18 | - Includes an example of a simple encoder/decoder stub (single byte XOR) to show the process of generating polymorphic shellcode that is built in an encoded state and decoded at execution (think Shikata Ga Nai). 19 | 20 | The point here is to demonstrate the build pipeline, not necessarily provide you with field-grade shellcode. 21 | 22 | ## Building 23 | 24 | The Makefile supports building the shellcode to each individual phase for testing and evaluation. Calling `make` with no arguments will run the entire build pipeline all the way to the final shellcode `.bin` file. 25 | 26 | You may encounter warnings when running make, but the pipeline will still work: 27 | 28 | ``` 29 | λ make 30 | docker build -t shellcode-build-pipeline:latest . 31 | 32 | ...[snip]... 33 | 34 | python3 tools/handle_asm.py clean build/c-shellcode.s build/c-shellcode_cleaned.asm 35 | [+] Cleaned assembly written to build/c-shellcode_cleaned.asm 36 | x86_64-w64-mingw32-gcc -c -x assembler-with-cpp -o 37 | 38 | ...[snip]... 39 | 40 | python3 tools/handle_asm.py extract build/c-shellcode.exe build/c-shellcode_64_encoded.bin 41 | [*] Entry point RVA: 0x1000 42 | [*] .text section: VirtualAddr=0x1000, RawPtr=0x400, RawSize=0x400 43 | [*] Entry point offset in .text: 0x0 44 | [+] Raw payload length: 892 bytes 45 | [+] Final shellcode (stub + encoded payload): 922 bytes 46 | 47 | /* ================== C SHELLCODE ARRAY ================== */ 48 | unsigned char shellcode_64[] = { 49 | 0x48, 0x8d, /*... snip */ 50 | }; 51 | unsigned int shellcode_64_len = 922; 52 | ``` 53 | 54 | This prints the shellcode to the terminal formatted similarly to how `msfvenom` would output it and also writes the `.bin` file to the build dir. 55 | 56 | image 57 | 58 | ## Custom Payloads 59 | 60 | The source comes with an example payload file (`src/payload_msgbox.c`) which should give you a good starting point on how to develop a custom payload. The other source files handle the rest of the work required to bootstrap and execute position-independent code, including the runtime and calling the shellcode entrypoint after aligning the stack to the 16-byte boundary. With that in mind, to write a custom payload, you need to: 61 | 62 | - Write a new file that implements the payload of your shellcode in C 63 | - Add the new payload to the end of the `shellcode.c` orchestrator: 64 | 65 | ``` 66 | ... 67 | #include "runtime.c" 68 | // #include "payload_msgbox.c" 69 | #include "your_payload_source_here.c" 70 | ``` 71 | 72 | The shellcode entrypoint is `payload_main(SC_ENV *env)`. The SC_ENV struct is initialized by `runtime.c` after locating the PEB, resolving the base of required modules, and resolving API addresses. This is effectively a miniature loader that reconstructs enough of an IAT for position-independent execution (shoutout to @hasherezade and her paper, which covers all of this and provided excellent implementation of how to do the bootstrapping). 73 | 74 | ```c 75 | void payload_main(SC_ENV *env) { 76 | if (!env->pLoadLibraryA || !env->pGetProcAddress) { 77 | return; 78 | } 79 | 80 | // Stack strings so we can store our strings inline of the shellcode rather than in an external section 81 | char user32_dll_name[] = { 'u','s','e','r','3','2','.','d','l','l', 0 }; 82 | char message_box_name[] = { 'M','e','s','s','a','g','e','B','o','x','W', 0 }; 83 | 84 | wchar_t msg_content[] = { 'H','e','l','l','o',' ','W','o','r','l','d','!', 0 }; 85 | wchar_t msg_title[] = { 'D','e','m','o','!', 0 }; 86 | 87 | HMODULE u32 = env->pLoadLibraryA(user32_dll_name); 88 | if (!u32) { 89 | return; 90 | } 91 | 92 | int (WINAPI *pMessageBoxW)( 93 | HWND, 94 | LPCWSTR, 95 | LPCWSTR, 96 | UINT 97 | ) = (int (WINAPI*)(HWND, LPCWSTR, LPCWSTR, UINT)) 98 | env->pGetProcAddress(u32, message_box_name); 99 | 100 | if (!pMessageBoxW) { 101 | return; 102 | } 103 | 104 | pMessageBoxW(0, msg_content, msg_title, MB_OK); 105 | } 106 | ``` 107 | 108 | You would need to implement the C code to perform the actual payload within the entrypoint function, assemble the stack strings to call the APIs, define custom implementation of the dynamically loaded APIs, and then execute the actual payload. Be careful: even though the build pipeline handles most of the transformation to get this payload into PIC-ready form, certain C patterns will not work well as shellcode. In general, the guidelines are: 109 | 110 | - no global data, 111 | - no absolute addressing, 112 | - no reliance on compiler-inserted CRT initialization. 113 | - try not to use JMP tables if you can help it 114 | 115 | ## Custom Encoder 116 | 117 | The build pipeline includes a simple, naive example of obfuscating shellcode. The example implements the encoder during the build process and the decoder stub that reverses the encoding schema at runtime. The example is a single byte XOR, which probably won't make it past a basic EDR tbh, but that's not the point. 118 | 119 | Implementing a more sophisticated, custom encoder is a matter of building out the two corresponding functions in `encoder.py`: the encoding routine (Pyton) and the decoder stub (implemented directly in Assembly and appended with Python in the example). The important thing is that your implementation must match, of course, so adding a new kind of encoding/encrypting schema is a matter of implementing the encoder as a Python function against the payload bytes and writing an assembly stub that unwinds the encoding. 120 | 121 | ## Credits 122 | 123 | - @hasherezade for the excellent paper and implementation of bootstrapping API calls from shellcode 124 | - Matt Graeber (@mattifestation) for the AlignRSP stub 125 | - Maldev Academy for the inspiration 126 | 127 | --------------------------------------------------------------------------------