//go:build windows package winpwn import ( "fmt" "io" "sync" "golang.org/x/sys/windows" ) const ( pipeOutBufSize = 64 * 1024 pipeInBufSize = 64 * 1024 ) // pipeConn wraps a duplex named-pipe handle opened with // FILE_FLAG_OVERLAPPED as an io.ReadWriteCloser, the shape Tube needs for // its stdin/stdout fields. The handle is genuinely asynchronous -- matching // how real Windows services hold their pipe ends; a synchronous duplex // pipe is the rarer case in production code -- but every Read/Write below // immediately blocks on GetOverlappedResult, so the type behaves like an // ordinary blocking reader/writer to the rest of the package. That keeps // Tube's synchronous Send/Recv contract intact while still exercising the // same overlapped-completion path a real target uses. type pipeConn struct { h windows.Handle event windows.Handle // manual-reset event reused across overlapped calls closeOnce sync.Once closeErr error } func newPipeConn(h windows.Handle) (*pipeConn, error) { ev, err := windows.CreateEvent(nil, 1 /* manual reset */, 0, nil) if err != nil { windows.CloseHandle(h) return nil, fmt.Errorf("CreateEvent: %w", err) } return &pipeConn{h: h, event: ev}, nil } func (p *pipeConn) Read(b []byte) (int, error) { if len(b) == 0 { return 0, nil } var ov windows.Overlapped ov.HEvent = p.event var n uint32 err := windows.ReadFile(p.h, b, &n, &ov) if err != nil && err != windows.ERROR_IO_PENDING { if err == windows.ERROR_BROKEN_PIPE || err == windows.ERROR_HANDLE_EOF { return 0, io.EOF } return 0, err } var transferred uint32 if err := windows.GetOverlappedResult(p.h, &ov, &transferred, true); err != nil { if err == windows.ERROR_BROKEN_PIPE || err == windows.ERROR_HANDLE_EOF { return int(transferred), io.EOF } return int(transferred), err } if transferred == 0 { return 0, io.EOF } return int(transferred), nil } func (p *pipeConn) Write(b []byte) (int, error) { if len(b) == 0 { return 0, nil } var ov windows.Overlapped ov.HEvent = p.event var n uint32 err := windows.WriteFile(p.h, b, &n, &ov) if err != nil && err != windows.ERROR_IO_PENDING { return 0, err } var transferred uint32 if err := windows.GetOverlappedResult(p.h, &ov, &transferred, true); err != nil { return int(transferred), err } return int(transferred), nil } // Close is idempotent (sync.Once) deliberately: Tube.Close calls Close once // via its stdin field and once via its stdout field, and stdin/stdout are // the same *pipeConn here -- unlike net.Conn, a raw Windows HANDLE is not // safe to pass to CloseHandle twice (the numeric value can be reused by an // unrelated object in between). func (p *pipeConn) Close() error { p.closeOnce.Do(func() { _ = windows.CloseHandle(p.event) p.closeErr = windows.CloseHandle(p.h) }) return p.closeErr } // ServePipe creates a duplex named pipe at \\.\pipe\ // (PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED -- the pattern real Windows // services use, not the rarer synchronous one) and blocks until exactly // one client connects -- the named-pipe analogue of Spawn/Remote for the // IPC-flavored challenges Windows uses far more than Linux pwn does. // Returns a *Tube wired to the connected pipe end, so Send/Recv*/ // Interactive/SetTimeout all work exactly as they do over a process or TCP // socket. func ServePipe(name string) (*Tube, error) { fullName, err := windows.UTF16PtrFromString(`\\.\pipe\` + name) if err != nil { return nil, fmt.Errorf("invalid pipe name %q: %w", name, err) } h, err := windows.CreateNamedPipe( fullName, windows.PIPE_ACCESS_DUPLEX|windows.FILE_FLAG_OVERLAPPED, windows.PIPE_TYPE_BYTE|windows.PIPE_READMODE_BYTE|windows.PIPE_WAIT, 1, // maxInstances: one client, matching "spawn one challenge instance" pipeOutBufSize, pipeInBufSize, 0, // default timeout nil, ) if err != nil { return nil, fmt.Errorf("CreateNamedPipe: %w", err) } conn, err := newPipeConn(h) if err != nil { return nil, err } Info(`Waiting for a client on \\.\pipe\%s`, name) var ov windows.Overlapped ov.HEvent = conn.event err = windows.ConnectNamedPipe(conn.h, &ov) if err != nil && err != windows.ERROR_IO_PENDING && err != windows.ERROR_PIPE_CONNECTED { conn.Close() return nil, fmt.Errorf("ConnectNamedPipe: %w", err) } if err != windows.ERROR_PIPE_CONNECTED { var transferred uint32 if err := windows.GetOverlappedResult(conn.h, &ov, &transferred, true); err != nil { conn.Close() return nil, fmt.Errorf("waiting for client connection: %w", err) } } Success(`Client connected to \\.\pipe\%s`, name) return newTube(nil, nil, conn, conn), nil } // DialPipe connects to a named pipe at \\.\pipe\ as a client (the // CreateFile-based counterpart to ServePipe), returning a *Tube wired to // it. func DialPipe(name string) (*Tube, error) { fullName, err := windows.UTF16PtrFromString(`\\.\pipe\` + name) if err != nil { return nil, fmt.Errorf("invalid pipe name %q: %w", name, err) } h, err := windows.CreateFile( fullName, windows.GENERIC_READ|windows.GENERIC_WRITE, 0, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_OVERLAPPED, 0, ) if err != nil { return nil, fmt.Errorf(`CreateFile(\\.\pipe\%s): %w`, name, err) } conn, err := newPipeConn(h) if err != nil { return nil, err } return newTube(nil, nil, conn, conn), nil }