├── .coveragerc ├── .github └── logo.png ├── .gitignore ├── .gitmodules ├── .vscode └── c_cpp_properties.json ├── LICENSE ├── Makefile ├── README.md ├── assets ├── common │ ├── AdjustStack.s.j2 │ ├── Makefile.j2 │ ├── linker.ld │ ├── loader.j2.c │ └── winlib.j2.c ├── rust │ ├── Cargo.toml │ └── fake_linker.sh └── zig │ ├── build.zig │ └── build.zig.zon ├── payload.c ├── payload.rs ├── payload.zig ├── piclin ├── __init__.py ├── __main__.py ├── config.py ├── util.py └── winlib.py ├── pyproject.toml ├── settings.example.toml └── tests └── test_main.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | __main__.py 4 | config.py 5 | -------------------------------------------------------------------------------- /.github/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JJK96/PIClin/b9998113b4f2d0c532c8b2df751aefd97b17db4b/.github/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | settings.toml 3 | .egg-info 4 | .DS_Store 5 | .coverage 6 | build 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "assets/common/win32-db"] 2 | path = assets/common/win32-db 3 | url = git@github.com:JJK96/win32-db.git 4 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Windows-MinGW", 5 | "includePath": [ 6 | "/usr/share/mingw-w64/include/**" 7 | ], 8 | "defines": [], 9 | "compilerPath": "/usr/bin/x86_64-w64-mingw32-gcc-win32", 10 | "cStandard": "c11", 11 | "cppStandard": "c++17", 12 | "intelliSenseMode": "windows-gcc-x86" 13 | } 14 | ], 15 | "version": 4 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jan-Jaap Korpershoek 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests install run 2 | 3 | run: 4 | python -m piclin --help 5 | 6 | install: 7 | pip install -e . 8 | 9 | tests: 10 | python -m pytest tests --cov=./piclin 11 | 12 | ubuntu_deps: 13 | sudo apt install mingw-w64 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | PIClin, short for Position Independent Code Linker. Allows compiling C, Rust or Zig code to shellcode while allowing the use of Win32 APIs without changes to the source code. In addition, strings and other data can be used without special encoding like stack strings. 6 | 7 | The Win32 APIs and standard library functions that you use are automatically detected at link time, after which a `winlib.c` file is generated containing definitions for these functions. Currently only functions in `kernel32.dll`, `ntdll.dll` and `msvcrt.dll` are supported, because these DLLs can reasonably be expected in every process. However, if you need other DLLs, you need to load your required DLL using `LoadLibrary`, change `piclin/winlib.py` to include functions from your required DLL and extend [win32-db] to generate a function definition database for your DLL. 8 | 9 | ## Install 10 | 11 | ``` 12 | git clone --recurse-submodules https://github.com/jjk96/piclin 13 | cd piclin 14 | ``` 15 | 16 | ``` 17 | pip install -e . 18 | ``` 19 | 20 | or 21 | 22 | ``` 23 | make install 24 | ``` 25 | 26 | Copy `settings.example.toml` to `settings.toml` and fill the necessary values. 27 | You can also install a settings file in ~/.config/piclin/settings.toml 28 | 29 | ## Usage 30 | 31 | ``` 32 | piclin compile payload.c 33 | ``` 34 | 35 | This will compile the payload in the `build` directory. The resulting files are: 36 | * `payload.bin`: The shellcode. 37 | * `loader.exe`: A trivial loader to test the shellcode. 38 | 39 | ### Output format 40 | 41 | The output format is shellcode by default. You can also change the format to PE to generate a PE file that can be executed directly. 42 | 43 | ``` 44 | piclin compile -f PE payload.c 45 | ``` 46 | 47 | ### Rust 48 | 49 | Ensure that you have a Rust toolchain installed. You can use `rustup` to install the toolchain. 50 | 51 | ``` 52 | piclin compile payload.rs 53 | piclin compile -f PE payload.rs 54 | ``` 55 | 56 | ## Caveats 57 | 58 | I have to be honest, I have not tested every edge-case and I might have taken some shortcuts here and there. Below follows a list of caveats that I can think of at the moment: 59 | 60 | * The shellcode (mainly the loader in `winlib.c`) assumes that all used DLLs are already loaded in the process. If your DLL is not loaded, you can load it manually using `LoadLibrary`. You can also resolve this generally by including the logic for loading missing DLLs from [BOF2Shellcode]. The reason I did not do that here is that I want to avoid storing unnecessary strings in the shellcode. 61 | * My parsing logic for C headers is very ad-hoc, it might parse some function headers incorrectly, resulting in incorrect code in `winlib.c`. This might be fixed in the future by including an actual C parser. 62 | * I only tested a very limited set of Windows API and Standard library functions (`WinExec`, `CreateProcessA`, `ExpandEnvironmentStringsA`, `VirtualAlloc`, `VirtualProtect` and `printf`). I'm assuming my code works for any other Windows API function, but I might be wrong. If you encounter anything, it should not be too hard to fix. 63 | * The library uses Windows API functions in a trivial way. For more OPSEC, you probably want to use indirect syscalls or something else. It should not be hard to fork and modify the project to support this. 64 | * I use `Mingw` headers and libraries. These might have subtle differences with Windows SDK headers. If you run into such issues, you can adapt the [win32-db] project to use the Windows SDK headers or generate similar JSON output yourself. 65 | * For compiling rust, you have to ensure that you don't use the standard library, because it will generate linking errors. I did not include the `#![no_std]` attribute because that requires manually implementing a `panic` handler, which results in extra boiler-plate code. 66 | 67 | ## Tests 68 | 69 | Not implemented yet. Test your shellcode before deloyment! 70 | 71 | ``` 72 | make tests 73 | ``` 74 | 75 | ## Credits 76 | 77 | * [c-to-shellcode](https://github.com/Print3M/c-to-shellcode) for the initial idea and framework. 78 | * Michael Schrijver for providing me with a nice linker script that enabled me to use normal strings and data in shellcode. 79 | * [BOF2Shellcode] for the implementation of hash-based dynamic API resolving. 80 | 81 | [BOF2Shellcode]: https://github.com/FalconForceTeam/BOF2shellcode 82 | [win32-db]: https://github.com/JJK96/win32-db 83 | -------------------------------------------------------------------------------- /assets/common/AdjustStack.s.j2: -------------------------------------------------------------------------------- 1 | # Based on: https://github.com/mattifestation/PIC_Bindshell/blob/master/PIC_Bindshell/AdjustStack.asm 2 | .global _start 3 | .section .start 4 | _start: 5 | pushq %rsi # Preserve RSI 6 | movq %rsp, %rsi # Save RSP 7 | andq $-16, %rsp # Align RSP to 16 bytes (0xFFFFFFFFFFFFFFF0) 8 | subq $32, %rsp # Allocate homing space (0x20 bytes) 9 | call {{entry}} # Call the payload entry point 10 | movq %rsi, %rsp # Restore original RSP 11 | popq %rsi # Restore RSI 12 | jmp end 13 | 14 | {% if language != 'zig' %} 15 | .global __main 16 | __main: 17 | ret # Dummy definition of main because this is required by gcc 18 | {% else %} 19 | .global RtlExitUserProcess 20 | RtlExitUserProcess: # Dummy definition for this function which is executed at the end of the Zig main function 21 | ret 22 | {% endif %} 23 | 24 | .global end 25 | .section .end 26 | end: 27 | ret 28 | 29 | -------------------------------------------------------------------------------- /assets/common/Makefile.j2: -------------------------------------------------------------------------------- 1 | CC = {{CC}} 2 | LD = {{LD}} 3 | EXE_PAYLOAD_CFLAGS = -fPIC -mconsole -Os 4 | #-D_UCRT: Do not use mingw's version of the C runtime, but use Microsoft's implementation. 5 | #-DWINBASEAPI: Do not import from DLLs, but statically 6 | BIN_PAYLOAD_CFLAGS = \ 7 | -Os\ 8 | -fPIC\ 9 | -nostdlib\ 10 | -nostartfiles\ 11 | -ffreestanding\ 12 | -fno-asynchronous-unwind-tables\ 13 | -fno-ident\ 14 | -Wl,--no-seh\ 15 | -fno-optimize-sibling-calls\ 16 | -ffunction-sections\ 17 | -D_UCRT= \ 18 | -DWINBASEAPI= 19 | LD_FLAGS = \ 20 | --image-base=0 \ 21 | --gc-sections # Included even though it doesn't seem to work 22 | 23 | loader.exe: loader.c 24 | ${CC} $^ -o $@ 25 | 26 | loader.c: loader.j2.c payload.bin 27 | piclin template $< PAYLOAD=@payload.bin > $@ 28 | 29 | tmp.exe: linker.ld payload.o AdjustStack.o winlib.o 30 | ${LD} -T $^ -o $@ ${LD_FLAGS} 31 | 32 | payload.bin: tmp.exe 33 | x86_64-w64-mingw32-objcopy -O binary -j.text $< $@ 34 | 35 | {% if language == 'c' %} 36 | # Compile payload C code to object file 37 | payload.o: payload.c 38 | ${CC} -c $^ -o $@ ${BIN_PAYLOAD_CFLAGS} 39 | {% elif language == 'rust' %} 40 | # Compile payload Rust code to object file 41 | payload.o: payload.rs 42 | RUSTFLAGS="-C relocation-model=pic -C linker=fake_linker.sh" cargo build --release --target x86_64-pc-windows-gnu 43 | {% elif language == 'zig' %} 44 | # Compile payload Zig code to object file 45 | payload.o: payload.zig 46 | zig build -Dtarget=x86_64-windows -p . -Doptimize=ReleaseSmall 47 | {% endif %} 48 | 49 | # Determine undefined references based on linking errors 50 | functions.txt: linker.ld payload.o AdjustStack.o 51 | ${LD} -T $^ -o /dev/null ${LD_FLAGS} 2>&1 | awk -F\` '/undefined reference/{print substr($$(NF), 1, length($$(NF))-1)}' > $@ 52 | 53 | winlib.c: winlib.j2.c functions.txt 54 | piclin winlib $^ > $@ 55 | 56 | winlib.o: winlib.c 57 | ${CC} -c $^ -o $@ ${BIN_PAYLOAD_CFLAGS} 58 | 59 | AdjustStack.o: AdjustStack.s 60 | ${CC} -c $^ -o $@ ${BIN_PAYLOAD_CFLAGS} 61 | 62 | exe: payload.exe 63 | 64 | {% if language == 'c' %} 65 | payload.exe: winlib.o payload.o 66 | ${CC} $^ -o $@ ${EXE_PAYLOAD_CFLAGS} 67 | {% elif language == 'rust' %} 68 | payload.exe: payload.rs 69 | cargo build --release --target x86_64-pc-windows-gnu 70 | cp target/x86_64-pc-windows-gnu/release/payload.exe ./ 71 | {% endif %} 72 | 73 | -------------------------------------------------------------------------------- /assets/common/linker.ld: -------------------------------------------------------------------------------- 1 | # Credits to Michael Schrijver for the basis of this script 2 | ENTRY(_start) 3 | SECTIONS { 4 | .text (0x0): { *(.start) *(.text*) *(.data) *(.rdata*) *(.xdata*) *(.pdata*) *(.bss) *(.end) } 5 | /DISCARD/ : { *(.eh_frame) *(.note.gnu.property) } 6 | } 7 | -------------------------------------------------------------------------------- /assets/common/loader.j2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | unsigned char payload[] = "{{PAYLOAD}}"; 5 | unsigned int payload_len = sizeof(payload); 6 | 7 | void main() { 8 | void* exec; 9 | BOOL rv; 10 | HANDLE th; 11 | DWORD oldprotect = 0; 12 | 13 | // Shellcode 14 | exec = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); 15 | RtlMoveMemory(exec, payload, payload_len); 16 | rv = VirtualProtect(exec, payload_len, PAGE_EXECUTE_READ, &oldprotect); 17 | 18 | printf("[+] Exec..."); 19 | 20 | th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec, 0, 0, 0); 21 | WaitForSingleObject(th, -1); 22 | 23 | printf("[+] End..."); 24 | } 25 | -------------------------------------------------------------------------------- /assets/common/winlib.j2.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | typedef struct _MY_LDR_DATA_TABLE_ENTRY { 9 | PVOID Reserved1[2]; 10 | LIST_ENTRY InMemoryOrderLinks; 11 | PVOID Reserved2[2]; 12 | PVOID DllBase; 13 | PVOID Reserved3[2]; 14 | UNICODE_STRING FullDllName; 15 | UNICODE_STRING BaseDllName; 16 | PVOID Reserved5[3]; 17 | __C89_NAMELESS union { 18 | ULONG CheckSum; 19 | PVOID Reserved6; 20 | }; 21 | ULONG TimeDateStamp; 22 | } MY_LDR_DATA_TABLE_ENTRY,*PMY_LDR_DATA_TABLE_ENTRY; 23 | 24 | uint64_t getDllBase(unsigned long); 25 | uint64_t loadDll(unsigned long); 26 | uint64_t parseHdrForPtr(uint64_t, unsigned long); 27 | 28 | unsigned long djb2(unsigned char*); 29 | unsigned long unicode_djb2(const wchar_t* str); 30 | WCHAR* toLower(WCHAR* str); 31 | 32 | //Based on https://github.com/FalconForceTeam/BOF2shellcode/blob/master/ApiResolve.c 33 | uint64_t 34 | getFunctionPtr(unsigned long dll_hash, unsigned long function_hash) { 35 | 36 | uint64_t dll_base = 0x00; 37 | uint64_t ptr_function = 0x00; 38 | 39 | dll_base = getDllBase(dll_hash); 40 | // if (dll_base == 0) { 41 | // dll_base = loadDll(dll_hash); 42 | // if (dll_base == 0) 43 | // return 0; 44 | // } 45 | 46 | ptr_function = parseHdrForPtr(dll_base, function_hash); 47 | 48 | return ptr_function; 49 | } 50 | 51 | 52 | uint64_t 53 | parseHdrForPtr(uint64_t dll_base, unsigned long function_hash) { 54 | 55 | PIMAGE_NT_HEADERS nt_hdrs = NULL; 56 | PIMAGE_DATA_DIRECTORY data_dir= NULL; 57 | PIMAGE_EXPORT_DIRECTORY export_dir= NULL; 58 | 59 | uint32_t* ptr_exportadrtable = 0x00; 60 | uint32_t* ptr_namepointertable = 0x00; 61 | uint16_t* ptr_ordinaltable = 0x00; 62 | 63 | uint32_t idx_functions = 0x00; 64 | 65 | unsigned char* ptr_function_name = NULL; 66 | 67 | 68 | nt_hdrs = (PIMAGE_NT_HEADERS)(dll_base + (uint64_t)((PIMAGE_DOS_HEADER)(size_t)dll_base)->e_lfanew); 69 | data_dir = (PIMAGE_DATA_DIRECTORY)&nt_hdrs->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; 70 | export_dir = (PIMAGE_EXPORT_DIRECTORY)(dll_base + (uint64_t)data_dir->VirtualAddress); 71 | 72 | ptr_exportadrtable = (uint32_t*)(dll_base + (uint64_t)export_dir->AddressOfFunctions); 73 | ptr_namepointertable = (uint32_t*)(dll_base + (uint64_t)export_dir->AddressOfNames); 74 | ptr_ordinaltable = (uint16_t*)(dll_base + (uint64_t)export_dir->AddressOfNameOrdinals); 75 | 76 | for(idx_functions = 0; idx_functions < export_dir->NumberOfNames; idx_functions++){ 77 | 78 | ptr_function_name = (unsigned char*)dll_base + (ptr_namepointertable[idx_functions]); 79 | if (djb2(ptr_function_name) == function_hash) { 80 | WORD nameord = ptr_ordinaltable[idx_functions]; 81 | DWORD rva = ptr_exportadrtable[nameord]; 82 | return dll_base + rva; 83 | } 84 | 85 | } 86 | 87 | return 0; 88 | } 89 | 90 | uint64_t 91 | getDllBase(unsigned long dll_hash) { 92 | 93 | PPEB peb = (PPEB)__readgsqword(0x60); 94 | PPEB_LDR_DATA ldr = peb->Ldr; 95 | PLIST_ENTRY item = ldr->InMemoryOrderModuleList.Blink; 96 | PMY_LDR_DATA_TABLE_ENTRY dll = NULL; 97 | 98 | do { 99 | dll = CONTAINING_RECORD(item, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks); 100 | 101 | if (dll->BaseDllName.Buffer == NULL) 102 | return 0; 103 | 104 | if (unicode_djb2(toLower(dll->BaseDllName.Buffer)) == dll_hash) 105 | return (uint64_t)dll->DllBase; 106 | 107 | item = item->Blink; 108 | } while (item != NULL); 109 | 110 | return 0; 111 | } 112 | 113 | unsigned long 114 | djb2(unsigned char* str) 115 | { 116 | unsigned long hash = 5381; 117 | int c; 118 | 119 | while ((c = *str++)) 120 | hash = ((hash << 5) + hash) + c; 121 | 122 | return hash; 123 | } 124 | 125 | unsigned long 126 | unicode_djb2(const wchar_t* str) 127 | { 128 | 129 | unsigned long hash = 5381; 130 | DWORD val; 131 | 132 | while (*str != 0) { 133 | val = (DWORD)*str++; 134 | hash = ((hash << 5) + hash) + val; 135 | } 136 | 137 | return hash; 138 | 139 | } 140 | 141 | WCHAR* 142 | toLower(WCHAR *str) 143 | { 144 | 145 | WCHAR* start = str; 146 | 147 | while (*str) { 148 | 149 | if (*str <= L'Z' && *str >= 'A') { 150 | *str += 32; 151 | } 152 | 153 | str += 1; 154 | 155 | } 156 | 157 | return start; 158 | 159 | } 160 | 161 | {% for dll in dlls %} 162 | #define HASH_{{dll[:-4]}} {{dll | hash}} 163 | {% endfor %} 164 | 165 | {% for entry in entries %} 166 | {{entry}} 167 | {% endfor %} 168 | -------------------------------------------------------------------------------- /assets/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "payload" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | libc = "0.2" 8 | 9 | [dependencies.windows-sys] 10 | version = "0.60" 11 | features = [ 12 | "Win32_Security", 13 | "Win32_System_Threading", 14 | "Win32_UI_WindowsAndMessaging", 15 | ] 16 | 17 | [profile.release] 18 | panic = "abort" 19 | opt-level="z" 20 | 21 | [[bin]] 22 | name = "payload" 23 | path = "payload.rs" 24 | -------------------------------------------------------------------------------- /assets/rust/fake_linker.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # echo $@ > linker-args.txt 3 | for arg in $@; do 4 | if [[ "$arg" = *.payload.*.o ]]; then 5 | echo "Saving $arg to payload.o" 6 | cp "$arg" "./payload.o" 7 | # elif [[ "$arg" = *.o ]]; then 8 | # cp "$arg" "./$(basename "$arg")_saved.o" 9 | fi 10 | done 11 | # done > fake-linker.log 12 | -------------------------------------------------------------------------------- /assets/zig/build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const zigwin32_dep = b.dependency("zigwin32", .{}); 8 | 9 | const mod = b.addModule("payload", .{ 10 | .root_source_file = b.path("payload.zig"), 11 | .target = target, 12 | .optimize = optimize, 13 | .unwind_tables = std.builtin.UnwindTables.none, 14 | .strip = true, 15 | .pic = true, 16 | }); 17 | 18 | mod.addImport("win32", zigwin32_dep.module("win32")); 19 | 20 | const obj = b.addObject(.{ 21 | .name = "payload", 22 | .root_module = mod, 23 | }); 24 | 25 | const write = b.addInstallFile(obj.getEmittedBin(), "payload.o"); 26 | 27 | b.getInstallStep().dependOn(&write.step); 28 | } 29 | -------------------------------------------------------------------------------- /assets/zig/build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .build, 3 | .version = "0.0.0", 4 | .fingerprint = 0xbda0f2db37157eb5, 5 | .minimum_zig_version = "0.15.0-dev.1593+399bace2f", 6 | .dependencies = .{ 7 | .zigwin32 = .{ 8 | .url = "https://github.com/marlersoft/zigwin32/archive/9d399c0895de4746c1fcab366841e2ab031b0429.zip", 9 | .hash = "zigwin32-25.0.28-preview-AAAAANd_5APbcon3QqoYfLKnJ13fiBGJSGq9YrVZsfI-", 10 | }, 11 | }, 12 | .paths = .{ 13 | "build.zig", 14 | "build.zig.zon", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /payload.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | int main(void) { 9 | printf("Test %d %s\n", 4, "str"); 10 | WinExec("calc.exe", SW_SHOWNORMAL); 11 | return 0; 12 | } 13 | -------------------------------------------------------------------------------- /payload.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use windows_sys::{ 3 | core::*, Win32::System::Threading::*, Win32::UI::WindowsAndMessaging::*, 4 | }; 5 | use libc::*; 6 | 7 | #[no_mangle] 8 | fn main() -> () { 9 | unsafe { 10 | WinExec(s!("calc.exe"), SW_SHOWNORMAL.try_into().unwrap()); 11 | printf(s!("Test\n") as *const i8); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /payload.zig: -------------------------------------------------------------------------------- 1 | const win32 = @import("win32"); 2 | extern fn printf(format: [*:0]const u8, ...) void; 3 | 4 | pub fn main() void { 5 | printf("Hello, world!\n"); 6 | _ = win32.system.threading.WinExec("calc.exe", win32.everything.SW_SHOW.SHOWNORMAL); 7 | } 8 | -------------------------------------------------------------------------------- /piclin/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from enum import Enum 3 | from contextlib import contextmanager 4 | import logging 5 | from .util import run_cmd 6 | import jinja2 7 | import shutil 8 | from .config import settings 9 | from itertools import chain 10 | 11 | INPUT_FILE_NAME = "payload.c" 12 | assets = Path(__file__).parent.parent / "assets" 13 | 14 | @contextmanager 15 | def error_handler(): 16 | try: 17 | yield 18 | except Exception as e: 19 | logging.error(e) 20 | 21 | class OutputFormat(Enum): 22 | BINARY = 1 23 | PE = 2 24 | 25 | def suffix_to_language(suffix): 26 | if suffix == ".c": 27 | return "c" 28 | elif suffix == ".rs": 29 | return "rust" 30 | elif suffix == ".zig": 31 | return "zig" 32 | else: 33 | raise ValueError(f"Unsupported file extension: {suffix}") 34 | 35 | def language_to_entrypoint(language): 36 | if language == "zig": 37 | return "wWinMainCRTStartup" 38 | return "main" 39 | 40 | def compile(input_file=INPUT_FILE_NAME, output_dir="build", output_format=OutputFormat.BINARY, rebuild=False): 41 | output_dir = Path(output_dir) 42 | if rebuild and output_dir.exists(): 43 | shutil.rmtree(output_dir) 44 | output_dir.mkdir(exist_ok=True) 45 | input_file = Path(input_file) 46 | language = suffix_to_language(input_file.suffix) 47 | main_file = (output_dir / input_file) 48 | makefile = (output_dir / "Makefile") 49 | adjust_stack_file = (output_dir / "AdjustStack.s") 50 | common_assets = assets/ "common" 51 | asset_dirs = [common_assets] 52 | language_assets = assets / language 53 | if language_assets.exists(): 54 | asset_dirs.append(language_assets) 55 | if not makefile.exists(): 56 | content = template(common_assets / "Makefile.j2", { 57 | "CC": settings.get("CC"), 58 | "LD": settings.get("LD"), 59 | "language": language 60 | }) 61 | with open(makefile, 'w') as f: 62 | f.write(content) 63 | if not adjust_stack_file.exists(): 64 | content = template(common_assets / "AdjustStack.s.j2", { 65 | "entry": language_to_entrypoint(language), 66 | "language": language 67 | }) 68 | with open(adjust_stack_file, 'w') as f: 69 | f.write(content) 70 | if not main_file.exists(): 71 | main_file.symlink_to(input_file.absolute()) 72 | for file in chain(*(d.glob("*") for d in asset_dirs)): 73 | output_file = (output_dir / file.name) 74 | if not output_file.exists(): 75 | output_file.symlink_to(file.absolute()) 76 | if output_format == OutputFormat.BINARY: 77 | with error_handler(): 78 | run_cmd(f"make -C {output_dir}") 79 | logging.info(f"[+] {output_dir}/loader.exe and {output_dir}/payload.bin are ready!") 80 | else: 81 | with error_handler(): 82 | run_cmd(f"make -C {output_dir} exe") 83 | logging.info(f"[+] {output_dir}/payload.exe is ready!") 84 | 85 | 86 | def template(input, data): 87 | env = jinja2.Environment() 88 | 89 | # Inject payload into loader source code 90 | with open(input, "r") as f: 91 | template = env.from_string(f.read()) 92 | 93 | context = {} 94 | for k,v in data.items(): 95 | if isinstance(v, bytes): 96 | payload = "" 97 | for byte in v: 98 | payload += "\\" + hex(byte).lstrip("0") 99 | v = payload 100 | context[k] = v 101 | 102 | return template.render(context) 103 | 104 | def hash_djb2(s): 105 | hash = 5381 106 | for x in s: 107 | hash = (( hash << 5) + hash) + ord(x) 108 | hash = hash & 0xFFFFFFFF 109 | return hash 110 | 111 | -------------------------------------------------------------------------------- /piclin/__main__.py: -------------------------------------------------------------------------------- 1 | import click 2 | import sys 3 | from .config import settings 4 | from . import compile as _compile, template as _template, OutputFormat, hash_djb2 5 | from .winlib import template_winlib 6 | import logging 7 | logging.basicConfig(format="%(message)s", level=logging.INFO) 8 | 9 | @click.group() 10 | def main(): 11 | pass 12 | 13 | @main.command() 14 | @click.argument("input_file") 15 | @click.option("-o", "--output", help="Output directory", default="build") 16 | @click.option("-f", "--output-format", help="Format of the resulting binary", type=click.Choice(OutputFormat), default=OutputFormat.BINARY) 17 | @click.option("--rebuild", help="Delete the output directory before building", is_flag=True) 18 | def compile(input_file, output, output_format, rebuild): 19 | """Compile C code to shellcode while allowing the use of Win32 APIs without changes to the source code. In addition, strings and other data can be used without special encoding like stack strings.""" 20 | _compile(input_file=input_file, output_dir=output, output_format=output_format, rebuild=rebuild) 21 | 22 | 23 | @main.command() 24 | @click.argument("input_file") 25 | @click.argument("data", nargs=-1) 26 | def template(input_file, data): 27 | """Perform templating on the given input file""" 28 | context = {} 29 | for x in data: 30 | k,v = x.split('=') 31 | if v.startswith('@'): 32 | with open(v[1:], 'rb') as f: 33 | v = f.read() 34 | context[k] = v 35 | content = _template(input_file, context) 36 | sys.stdout.write(content) 37 | 38 | 39 | @main.command() 40 | @click.argument("input_file") 41 | @click.argument("functions_file") 42 | def winlib(input_file, functions_file): 43 | functions = [] 44 | with open(functions_file) as f: 45 | for line in f: 46 | functions.append(line[:-1]) 47 | content = template_winlib(input_file, functions) 48 | sys.stdout.write(content) 49 | 50 | 51 | @main.command() 52 | @click.argument("string") 53 | def hash(string): 54 | """ 55 | STRING: Module name or function name to hash 56 | """ 57 | 58 | h = hash_djb2(string) 59 | print(hex(h)) 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /piclin/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from dynaconf import Dynaconf 3 | from pathlib import Path 4 | 5 | dir = Path(__file__).parent 6 | 7 | settings = Dynaconf( 8 | envvar_prefix="PICLIN", 9 | settings_files=[ 10 | dir / '../settings.toml', 11 | Path(os.environ['HOME']) / '.config' / 'piclin' / 'settings.toml' 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /piclin/util.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import logging 3 | 4 | def run_cmd(cmd: str, do_print=True): 5 | if do_print: 6 | logging.info(f"[+] {cmd}") 7 | subprocess.run(cmd, text=True, check=True, shell=True) 8 | 9 | -------------------------------------------------------------------------------- /piclin/winlib.py: -------------------------------------------------------------------------------- 1 | from .config import settings 2 | from . import hash_djb2 3 | from functools import cache 4 | from pathlib import Path 5 | import logging 6 | import json 7 | import jinja2 8 | from textwrap import indent 9 | 10 | 11 | class InvalidDefinition(Exception): 12 | pass 13 | 14 | class Definition: 15 | variable_args = False 16 | parsed = False 17 | def __init__(self, definition_str, dll=None): 18 | self.definition_str = definition_str 19 | self.dll = dll 20 | 21 | def _parse(self): 22 | types, _, args = self.definition_str.rstrip(';').partition('(') 23 | types = list(filter(lambda x:x and x != "WINBASEAPI", types.split(' ', ))) 24 | self.function_name = types[-1] 25 | self.types = types[:-1] 26 | self.retval_types = [t for t in self.types if t not in ["WINAPI", "__cdecl"]] 27 | self.literal_args = args[:-1] 28 | self.variables = [] 29 | self.typedef_args = [] 30 | if self.literal_args.lower() != "void": 31 | for arg in self.literal_args.split(","): 32 | arg = arg.strip() 33 | if arg == '...': 34 | self.variable_args = True 35 | self.typedef_args.append("__builtin_va_list __local_argv") 36 | else: 37 | self.typedef_args.append(arg) 38 | if not ' ' in arg: 39 | continue 40 | var = arg.split(' ')[-1].lstrip('*') 41 | self.variables.append(var) 42 | 43 | def parse(self): 44 | if self.parsed: 45 | return 46 | try: 47 | self._parse() 48 | except Exception as e: 49 | logging.error(f"Could not parse: {self.definition_str}") 50 | raise 51 | 52 | def to_winlib_entry(self): 53 | self.parse() 54 | if not self.dll: 55 | raise Exception("Cannot convert to winlib entry if the DLL is unknown") 56 | hash_name = self.dll[:-4] 57 | if self.variable_args: 58 | dynamic_function_name = "v" + self.function_name 59 | call = f"""\ 60 | __builtin_va_list __local_argv; __builtin_va_start( __local_argv, {', '.join(self.variables)} ); 61 | __retval = _{dynamic_function_name}({', '.join(self.variables)}, __local_argv ); 62 | __builtin_va_end( __local_argv ); 63 | """ 64 | else: 65 | dynamic_function_name = self.function_name 66 | call = f"__retval = _{dynamic_function_name}({', '.join(self.variables)});" 67 | function_hash = hash_djb2(dynamic_function_name) 68 | return f"""\ 69 | #define HASH_{dynamic_function_name} {hex(function_hash)} 70 | typedef {self.types[0]}({self.types[1]} *{dynamic_function_name}_t) ({', '.join(self.typedef_args)}); 71 | {' '.join(self.types)} {self.function_name} ({self.literal_args}) {{ 72 | {dynamic_function_name}_t _{dynamic_function_name} = ({dynamic_function_name}_t) getFunctionPtr(HASH_{hash_name}, HASH_{dynamic_function_name}); 73 | {' '.join(self.retval_types)} __retval; 74 | {indent(call, prefix=" ")} 75 | return __retval; 76 | }} 77 | """ 78 | 79 | def __str__(self): 80 | return self.definition_str 81 | 82 | def lib_to_dll(lib): 83 | if lib.startswith('lib'): 84 | lib = lib[3:] 85 | return lib[:-2] + '.dll' 86 | 87 | def dll_to_lib(dll): 88 | return "lib"+dll[:-3] + "a" 89 | 90 | @cache 91 | def create_database(): 92 | res = {} 93 | win32_db = Path(__file__).parent.parent / "assets" / "common" / "win32-db" 94 | for dll in ["kernel32.dll", "ntdll.dll", "msvcrt.dll"]: 95 | db = win32_db / (dll + ".json") 96 | with open(db) as f: 97 | data = json.load(f) 98 | for function_name, definition in data.items(): 99 | if function_name not in res: 100 | res[function_name] = Definition(definition, dll=dll) 101 | return res 102 | 103 | def get_definition(function_name): 104 | db = create_database() 105 | return db[function_name] 106 | 107 | def template_winlib(input, functions): 108 | env = jinja2.Environment() 109 | env.filters['hash'] = lambda x: hex(hash_djb2(x)) 110 | with open(input) as f: 111 | template = env.from_string(f.read()) 112 | definitions = [get_definition(f) for f in functions] 113 | dlls = {d.dll for d in definitions} 114 | return template.render({ 115 | "dlls": dlls, 116 | "entries": [d.to_winlib_entry() for d in definitions] 117 | }) 118 | 119 | if __name__ == "__main__": 120 | # print(get_definition("WinExec")) 121 | # print(get_definition("VirtualAlloc")) 122 | # print(get_definition("CreateProcessA")) 123 | print(template_winlib("assets/winlib.j2.c", ["WinExec"])) 124 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "PIClin" 3 | version = "0.1" 4 | dependencies = [ 5 | "dynaconf", # Config files 6 | "click", # CLI interface 7 | "jinja2" 8 | ] 9 | authors = [ 10 | { name = "Jan-Jaap Korpershoek" }, 11 | ] 12 | description = """Compile C code to shellcode while allowing the use of Win32 APIs without changes to the source code. In addition, strings and other data can be used without special encoding like stack strings.""" 13 | readme = "README.md" 14 | license = {file = "LICENSE"} 15 | keywords = [] 16 | 17 | [project.optional-dependencies] 18 | Test = [ 19 | "pytest", # Tests 20 | "pytest-cov", # Code coverage 21 | ] 22 | 23 | [tool.setuptools] 24 | packages = ["piclin"] 25 | 26 | [project.scripts] 27 | piclin= "piclin.__main__:main" 28 | -------------------------------------------------------------------------------- /settings.example.toml: -------------------------------------------------------------------------------- 1 | mingw_lib_path = "/usr/x86_64-w64-mingw32/lib" 2 | mingw_headers_path = "/usr/share/mingw-w64/include" 3 | CC = "x86_64-w64-mingw32-gcc-win32" 4 | LD = "x86_64-w64-mingw32-ld" 5 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from piclin import some_action 2 | 3 | def test_some_action(): 4 | assert some_action() == "result" 5 | --------------------------------------------------------------------------------