293 lines
7.8 KiB
Go
293 lines
7.8 KiB
Go
package winpwn
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"regexp"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// Tube is the core abstraction for talking to a target: a spawned local
|
|
// process or a remote TCP connection. It mirrors pwntools' tube class and
|
|
// is the type every transport (Spawn, Remote, ...) returns.
|
|
//
|
|
// Every blocking call returns an error instead of killing the process --
|
|
// callers decide what a timeout/EOF/closed-connection means for their
|
|
// script, the same way pwntools raises (and lets you catch) EOFError /
|
|
// PwnlibException instead of the library calling sys.exit().
|
|
type Tube struct {
|
|
cmd *exec.Cmd
|
|
conn net.Conn
|
|
stdin io.WriteCloser
|
|
stdout io.ReadCloser
|
|
reader *bufio.Reader
|
|
|
|
// timeout overrides Context.Timeout for this tube specifically; zero
|
|
// means "fall back to Context.Timeout" (which is itself zero/forever
|
|
// by default). Set via SetTimeout.
|
|
timeout time.Duration
|
|
|
|
closeOnce sync.Once
|
|
closeErr error
|
|
}
|
|
|
|
// newTube wraps a writer/reader pair into a Tube with a buffered reader.
|
|
func newTube(cmd *exec.Cmd, conn net.Conn, stdin io.WriteCloser, stdout io.ReadCloser) *Tube {
|
|
return &Tube{
|
|
cmd: cmd,
|
|
conn: conn,
|
|
stdin: stdin,
|
|
stdout: stdout,
|
|
reader: bufio.NewReader(stdout),
|
|
}
|
|
}
|
|
|
|
// SetTimeout overrides Context.Timeout for this tube's Recv*/Send* calls.
|
|
// Zero means block forever (the default), matching pwntools' per-tube
|
|
// timeout= override of the global context.timeout.
|
|
func (t *Tube) SetTimeout(d time.Duration) {
|
|
t.timeout = d
|
|
}
|
|
|
|
func (t *Tube) effectiveTimeout() time.Duration {
|
|
if t.timeout > 0 {
|
|
return t.timeout
|
|
}
|
|
return Context.Timeout
|
|
}
|
|
|
|
// PID returns the OS process ID for a locally spawned process, or 0 for
|
|
// remote tubes. Use it to feed winpwn.NewProcessSymbols or
|
|
// winpwn.ListProcessHeaps without having to track the PID separately.
|
|
func (t *Tube) PID() uint32 {
|
|
if t.cmd != nil && t.cmd.Process != nil {
|
|
return uint32(t.cmd.Process.Pid)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Close tears down the underlying process/connection. Safe to call more
|
|
// than once (idempotent) -- Interactive relies on this to make a pending
|
|
// stdin write fail fast instead of dangling.
|
|
func (t *Tube) Close() error {
|
|
t.closeOnce.Do(func() {
|
|
if t.stdin != nil {
|
|
_ = t.stdin.Close()
|
|
}
|
|
if t.stdout != nil {
|
|
_ = t.stdout.Close()
|
|
}
|
|
if t.conn != nil {
|
|
t.closeErr = t.conn.Close()
|
|
return
|
|
}
|
|
if t.cmd != nil && t.cmd.Process != nil {
|
|
t.closeErr = t.cmd.Process.Kill()
|
|
}
|
|
})
|
|
return t.closeErr
|
|
}
|
|
|
|
// withTimeout runs fn on its own goroutine and races it against this tube's
|
|
// effective timeout. If fn doesn't return in time, withTimeout returns a
|
|
// timeout error immediately -- but fn's goroutine is *not* killed (the
|
|
// underlying pipe/socket reader has no native per-call deadline), so it
|
|
// keeps running in the background until the blocking I/O it's stuck in
|
|
// eventually completes or errors. That's the standard, and only portable,
|
|
// way to bolt a deadline onto an arbitrary io.Reader/Writer in Go.
|
|
func withTimeout[T any](t *Tube, fn func() (T, error)) (T, error) {
|
|
timeout := t.effectiveTimeout()
|
|
if timeout <= 0 {
|
|
return fn()
|
|
}
|
|
type result struct {
|
|
v T
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
go func() {
|
|
v, err := fn()
|
|
ch <- result{v, err}
|
|
}()
|
|
select {
|
|
case r := <-ch:
|
|
return r.v, r.err
|
|
case <-time.After(timeout):
|
|
var zero T
|
|
return zero, fmt.Errorf("winpwn: operation timed out after %s", timeout)
|
|
}
|
|
}
|
|
|
|
// Recv reads up to n bytes from the tube, blocking until n bytes have
|
|
// arrived. On a clean EOF after at least one byte it returns the partial
|
|
// read with a nil error (mirroring the old behavior); on EOF with nothing
|
|
// read yet, it returns io.EOF.
|
|
func (t *Tube) Recv(n int) ([]byte, error) {
|
|
return withTimeout(t, func() ([]byte, error) {
|
|
buf := make([]byte, n)
|
|
read, err := io.ReadFull(t.reader, buf)
|
|
if err != nil {
|
|
if err == io.ErrUnexpectedEOF || err == io.EOF {
|
|
if read > 0 {
|
|
return buf[:read], nil
|
|
}
|
|
return nil, io.EOF
|
|
}
|
|
return buf[:read], err
|
|
}
|
|
return buf, nil
|
|
})
|
|
}
|
|
|
|
// RecvUntil reads from the tube until delim is seen (inclusive of delim).
|
|
func (t *Tube) RecvUntil(delim []byte) ([]byte, error) {
|
|
return withTimeout(t, func() ([]byte, error) {
|
|
var out []byte
|
|
for {
|
|
b, err := t.reader.ReadByte()
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, b)
|
|
if bytes.HasSuffix(out, delim) {
|
|
return out, nil
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// RecvLine reads a single line, including the trailing newline
|
|
// (Context.Newline, "\n" by default).
|
|
func (t *Tube) RecvLine() ([]byte, error) {
|
|
return t.RecvUntil(Context.Newline)
|
|
}
|
|
|
|
// RecvPred reads one byte at a time until pred(accumulated) reports true,
|
|
// the analogue of pwntools' recvpred.
|
|
func (t *Tube) RecvPred(pred func([]byte) bool) ([]byte, error) {
|
|
return withTimeout(t, func() ([]byte, error) {
|
|
var out []byte
|
|
for {
|
|
b, err := t.reader.ReadByte()
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, b)
|
|
if pred(out) {
|
|
return out, nil
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// RecvRegex reads one byte at a time until the accumulated buffer matches
|
|
// re, the analogue of pwntools' recvregex.
|
|
func (t *Tube) RecvRegex(re *regexp.Regexp) ([]byte, error) {
|
|
return t.RecvPred(func(buf []byte) bool {
|
|
return re.Match(buf)
|
|
})
|
|
}
|
|
|
|
// Send writes raw bytes to the tube.
|
|
func (t *Tube) Send(data []byte) error {
|
|
_, err := withTimeout(t, func() (int, error) {
|
|
return t.stdin.Write(data)
|
|
})
|
|
return err
|
|
}
|
|
|
|
// SendLine writes data followed by Context.Newline.
|
|
func (t *Tube) SendLine(data []byte) error {
|
|
return t.Send(append(append([]byte{}, data...), Context.Newline...))
|
|
}
|
|
|
|
// SendAfter waits for delim, then sends data (no trailing newline).
|
|
func (t *Tube) SendAfter(delim []byte, data []byte) error {
|
|
if _, err := t.RecvUntil(delim); err != nil {
|
|
return err
|
|
}
|
|
return t.Send(data)
|
|
}
|
|
|
|
// SendLineAfter waits for delim, then sends data followed by a newline.
|
|
func (t *Tube) SendLineAfter(delim []byte, data []byte) error {
|
|
if _, err := t.RecvUntil(delim); err != nil {
|
|
return err
|
|
}
|
|
return t.SendLine(data)
|
|
}
|
|
|
|
// Interactive hands the tube's stdin/stdout over to the user's terminal,
|
|
// the Go analogue of pwntools' tube.interactive(). Ctrl+C cleanly tears
|
|
// down the local process or remote connection.
|
|
//
|
|
// The stdin-forwarding goroutine below has no portable way to be cancelled
|
|
// in Go (os.Stdin.Read blocks with no deadline support), so it keeps
|
|
// running until the next keystroke/EOF even after Interactive returns;
|
|
// the deferred Close() at least makes its next Write fail fast instead of
|
|
// leaving the target side dangling. This is a known, deliberate limitation,
|
|
// not an oversight -- don't call Interactive() in a tight loop expecting
|
|
// the goroutine to be gone before the next iteration.
|
|
func (t *Tube) Interactive() {
|
|
defer t.Close()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
|
defer signal.Stop(sigChan)
|
|
|
|
stdoutDone := make(chan struct{})
|
|
go func() {
|
|
_, _ = io.Copy(os.Stdout, t.reader)
|
|
close(stdoutDone)
|
|
}()
|
|
|
|
go func() {
|
|
_, _ = io.Copy(t.stdin, os.Stdin)
|
|
}()
|
|
|
|
Info("Switching to interactive mode")
|
|
|
|
if t.cmd != nil {
|
|
// Local process branch.
|
|
waitCh := make(chan error, 1)
|
|
go func() {
|
|
waitCh <- t.cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case err := <-waitCh:
|
|
if err == nil {
|
|
Info("Process exited normally (code 0)")
|
|
} else {
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
code := exitErr.ExitCode()
|
|
// 0xC0000005 (Access Violation) is the Windows analogue of SIGSEGV.
|
|
Error("Process crashed/terminated with code: 0x%X", uint32(code))
|
|
} else {
|
|
Error("Process execution error: %v", err)
|
|
}
|
|
}
|
|
case <-sigChan:
|
|
Info("Interrupted by user, killing process...")
|
|
}
|
|
} else {
|
|
// Remote connection branch.
|
|
select {
|
|
case <-stdoutDone:
|
|
Info("Connection closed by foreign host")
|
|
case <-sigChan:
|
|
Info("Interrupted by user, closing connection...")
|
|
}
|
|
}
|
|
}
|