188 lines
5.3 KiB
Go
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
|
|
}
|