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

188 lines
5.3 KiB
Go

//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\<name>
// (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\<name> 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
}