From 6d9989b53a9669c54ae080d6121ae61f08e8a6de Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Sat, 5 Apr 2025 18:54:27 -0400 Subject: [PATCH] Incomplete Windows daemon support --- daemon/1shared.go | 4 +- daemon/1sharedUNIXGeneric.go | 9 +++ daemon/1sharedWindows.go | 7 ++ daemon/2server-pidToPath-Darwin.go | 1 + daemon/2server-pidToPath-Windows.go | 29 +++++++ daemon/2server.go | 74 ------------------ daemon/2serverUNIXGeneric.go | 81 ++++++++++++++++++++ daemon/{3client.go => 3clientUNIXGeneric.go} | 2 + daemon/3clientWindows.go | 34 ++++++++ go.mod | 11 ++- go.sum | 10 ++- 11 files changed, 179 insertions(+), 83 deletions(-) create mode 100644 daemon/1sharedUNIXGeneric.go create mode 100644 daemon/1sharedWindows.go create mode 100644 daemon/2server-pidToPath-Windows.go create mode 100644 daemon/2serverUNIXGeneric.go rename daemon/{3client.go => 3clientUNIXGeneric.go} (96%) create mode 100644 daemon/3clientWindows.go diff --git a/daemon/1shared.go b/daemon/1shared.go index 166e153..1e73190 100644 --- a/daemon/1shared.go +++ b/daemon/1shared.go @@ -2,8 +2,6 @@ package daemon import ( "os" - "path/filepath" ) -var binPath, _ = os.Executable() // store binary path -var socketPath = "/tmp/" + filepath.Base(binPath) + "-rcwd.sock" // store UNIX socket path +var binPath, _ = os.Executable() // store binary path diff --git a/daemon/1sharedUNIXGeneric.go b/daemon/1sharedUNIXGeneric.go new file mode 100644 index 0000000..2278654 --- /dev/null +++ b/daemon/1sharedUNIXGeneric.go @@ -0,0 +1,9 @@ +//go:build !windows + +package daemon + +import ( + "path/filepath" +) + +var socketPath = "/tmp/" + filepath.Base(binPath) + "-rcwd.sock" // store UNIX socket path diff --git a/daemon/1sharedWindows.go b/daemon/1sharedWindows.go new file mode 100644 index 0000000..889d3dd --- /dev/null +++ b/daemon/1sharedWindows.go @@ -0,0 +1,7 @@ +//go:build windows + +package daemon + +import "path/filepath" + +var socketPath = `\\.\pipe\` + filepath.Base(binPath) + `-rcwd` // store Windows named pipe path diff --git a/daemon/2server-pidToPath-Darwin.go b/daemon/2server-pidToPath-Darwin.go index b52e9e1..69cd09f 100644 --- a/daemon/2server-pidToPath-Darwin.go +++ b/daemon/2server-pidToPath-Darwin.go @@ -9,6 +9,7 @@ import ( ) // pidToPath returns the path of the executable that has the given PID. +// TODO remove reliance on system command OR verify authenticity of "lsof" binary func pidToPath(pid int) string { pidString := strconv.Itoa(pid) cmd := exec.Command("lsof", "-a", "-dtxt", "-p"+pidString) diff --git a/daemon/2server-pidToPath-Windows.go b/daemon/2server-pidToPath-Windows.go new file mode 100644 index 0000000..6af1b01 --- /dev/null +++ b/daemon/2server-pidToPath-Windows.go @@ -0,0 +1,29 @@ +//go:build windows + +package daemon + +import ( + "log" + "syscall" + + "golang.org/x/sys/windows" +) + +// pidToPath returns the path of the executable that has the given PID. +func pidToPath(pid uint32) string { + // get a handle to the process + const PROCESS_QUERY_INFORMATION = 0x0400 + const PROCESS_VM_READ = 0x0010 + hProcess, err := windows.OpenProcess(PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, false, pid) + if err != nil { + log.Fatalf("Failed to open process %d: %v", pid, err) + } + defer windows.CloseHandle(hProcess) + + // query the process executable path + var pathBuf [syscall.MAX_PATH]uint16 + pathLen := uint32(len(pathBuf)) + windows.QueryFullProcessImageName(hProcess, 0, &pathBuf[0], &pathLen) + + return syscall.UTF16ToString(pathBuf[:pathLen]) +} diff --git a/daemon/2server.go b/daemon/2server.go index ba94a03..dd7b077 100644 --- a/daemon/2server.go +++ b/daemon/2server.go @@ -1,18 +1,10 @@ package daemon import ( - "bytes" "crypto/sha256" "errors" "io" - "log" - "net" - "net/rpc" "os" - "strconv" - "time" - - peercred "github.com/rwinkhart/peercred-mini" ) var daemonHash []byte @@ -20,49 +12,6 @@ var daemonHash []byte // RCWService provides an RPC method. type RCWService struct{} -// Run should be called to start an RPC server. -func Run() { - // store the hash of the daemon binary - daemonHash = getFileHash(binPath) - - // remove the socket file if it already exists - if _, err := os.Stat(socketPath); err == nil { - if err := os.Remove(socketPath); err != nil { - log.Fatalf("Failed to remove existing socket: %v", err) - } - } - - // register RCWService with the RPC package - if err := rpc.Register(&RCWService{}); err != nil { - log.Fatalf("Error registering RPC service: %v", err) - } - - // listen on the Unix domain socket - listener, err := net.Listen("unix", socketPath) - if err != nil { - log.Fatalf("Failed to listen on UNIX socket %s: %v", socketPath, err) - } - defer listener.Close() - log.Printf("RPC daemon listening on unix://%s", socketPath) - - // Accept connections (timeout after 3 minutes of inactivity) - for { - listener.(*net.UnixListener).SetDeadline(time.Now().Add(3 * time.Minute)) - - conn, err := listener.Accept() - if err != nil { - if err.(net.Error).Timeout() { - log.Println("Three minutes have passed without any connections. Exiting...") - os.Exit(0) - } - log.Printf("Accept error: %v", err) - continue - } - // use a goroutine to check the client's identity - go handleConn(conn) - } -} - // GetPass is the RPC method. // For now (as a test/example), it returns "hello" if the input is "hi". func (h *RCWService) GetPass(request string, reply *string) error { @@ -73,29 +22,6 @@ func (h *RCWService) GetPass(request string, reply *string) error { return errors.New("unexpected input, expected \"hi\"") } -// handleConn verifies the identity of the client. -// It uses the file descriptor of the connection to get the PID of the client, -// which is then used to get the path of the client's executable and calculate its hash. -// The passphrase is only returned if the client's executable hash matches the daemon's hash -// and if the request is coming from the same user. -// This ensures that only the binary the daemon is embedded in can retrieve the passphrase. -func handleConn(conn net.Conn) { - ucred := peercred.Get(conn) - - // check if the RPC call is coming from an identical binary and from the same user - callingBinPath := pidToPath(ucred.PID) - if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(getFileHash(callingBinPath), daemonHash) { - // valid client; hand off the connection to the RPC server - rpc.ServeConn(conn) - } else { - // invalid client; close the connection w/o a response, - // log the client's path, and kill the daemon - conn.Close() - log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file - os.Exit(2) - } -} - // getFileHash returns the SHA256 hash of the file at the given path. func getFileHash(path string) []byte { file, _ := os.Open(path) diff --git a/daemon/2serverUNIXGeneric.go b/daemon/2serverUNIXGeneric.go new file mode 100644 index 0000000..055c7ce --- /dev/null +++ b/daemon/2serverUNIXGeneric.go @@ -0,0 +1,81 @@ +//go:build !windows + +package daemon + +import ( + "bytes" + "log" + "net" + "net/rpc" + "os" + "strconv" + "time" + + peercred "github.com/rwinkhart/peercred-mini" +) + +// Run should be called to start an RPC server. +func Run() { + // register RCWService with the RPC package + if err := rpc.Register(&RCWService{}); err != nil { + log.Fatalf("Error registering RPC service: %v", err) + } + + // store the hash of the daemon binary + daemonHash = getFileHash(binPath) + + // remove the socket file if it already exists + if _, err := os.Stat(socketPath); err == nil { + if err := os.Remove(socketPath); err != nil { + log.Fatalf("Failed to remove existing socket: %v", err) + } + } + + // listen on the Unix domain socket + listener, err := net.Listen("unix", socketPath) + if err != nil { + log.Fatalf("Failed to listen on UNIX socket %s: %v", socketPath, err) + } + defer listener.Close() + log.Printf("RPC daemon listening on unix://%s", socketPath) + + // accept connections (timeout after 3 minutes of inactivity) + for { + listener.(*net.UnixListener).SetDeadline(time.Now().Add(3 * time.Minute)) + + conn, err := listener.Accept() + if err != nil { + if err.(net.Error).Timeout() { + log.Println("Three minutes have passed without any connections. Exiting...") + os.Exit(0) + } + log.Printf("Accept error: %v", err) + continue + } + // use a goroutine to check the client's identity + go handleConn(conn) + } +} + +// handleConn verifies the identity of the client. +// It uses the file descriptor of the connection to get the PID of the client, +// which is then used to get the path of the client's executable and calculate its hash. +// The passphrase is only returned if the client's executable hash matches the daemon's hash +// and if the request is coming from the same user. +// This ensures that only the binary the daemon is embedded in can retrieve the passphrase. +func handleConn(conn net.Conn) { + ucred := peercred.Get(conn) + + // check if the RPC call is coming from an identical binary and from the same user + callingBinPath := pidToPath(ucred.PID) + if ucred.UID == strconv.Itoa(os.Getuid()) && bytes.Equal(getFileHash(callingBinPath), daemonHash) { + // valid client; hand off the connection to the RPC server + rpc.ServeConn(conn) + } else { + // invalid client; close the connection w/o a response, + // log the client's path, and kill the daemon + conn.Close() + log.Printf("Request received from invalid client: PID(%d), UID(%s), Path(%s)", ucred.PID, ucred.UID, callingBinPath) // TODO log to file + os.Exit(2) + } +} diff --git a/daemon/3client.go b/daemon/3clientUNIXGeneric.go similarity index 96% rename from daemon/3client.go rename to daemon/3clientUNIXGeneric.go index 92db596..e7b46f6 100644 --- a/daemon/3client.go +++ b/daemon/3clientUNIXGeneric.go @@ -1,3 +1,5 @@ +//go:build !windows + package daemon import ( diff --git a/daemon/3clientWindows.go b/daemon/3clientWindows.go new file mode 100644 index 0000000..91c3076 --- /dev/null +++ b/daemon/3clientWindows.go @@ -0,0 +1,34 @@ +//go:build windows + +package daemon + +import ( + "log" + "net/rpc" + + "github.com/Microsoft/go-winio" +) + +// Call connects to the RPC server and requests the passphrase. +func Call() string { + // connect to the Windows named pipe + conn, err := winio.DialPipe(socketPath, nil) + if err != nil { + log.Fatalf("Dial error: %v", err) + } + defer conn.Close() + + // create an RPC client using the connection + client := rpc.NewClient(conn) + defer client.Close() + + // request the passphrase from the RPC server + var reply string + err = client.Call("RCWService.GetPass", "hi", &reply) + if err != nil { + log.Fatalf("Error calling RCWService.GetPass: %v", err) + } + + // return the passphrase + return reply +} diff --git a/go.mod b/go.mod index 065378a..613d5bf 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,13 @@ module rcw go 1.24.2 -require github.com/rwinkhart/peercred-mini v0.0.0-20250405191724-d116525c2b41 +require github.com/rwinkhart/peercred-mini v0.0.0-20250405214620-93ccab17290b -require github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405011819-d304d362d032 // indirect +require ( + github.com/Microsoft/go-winio v0.6.2 + golang.org/x/sys v0.32.0 +) + +replace golang.org/x/sys => github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405010723-99a5f0732c0e + +replace github.com/rwinkhart/peercred-mini => ../peercred diff --git a/go.sum b/go.sum index a316f87..a8911c0 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ -github.com/rwinkhart/peercred-mini v0.0.0-20250405191724-d116525c2b41 h1:wUMbTEwPIf6bqLCAUmJ58uuYu6OX0iGa6S6NjYUXGPA= -github.com/rwinkhart/peercred-mini v0.0.0-20250405191724-d116525c2b41/go.mod h1:cH3916WNofkve4MeeK56H0whuUUVf57iYht3nsUyEh8= -github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405011819-d304d362d032 h1:4qzc09ysQhkyI2oN7pLbZMQP+qHq2TlVRCkMSmbl8m8= -github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405011819-d304d362d032/go.mod h1:U89aT1yaUAbs+9Dp9i1lZ2+5GsRNYk0l42+d1wykDBA= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/rwinkhart/peercred-mini v0.0.0-20250405214620-93ccab17290b h1:AEeo+zR+ZupGNvUwvu0YZcIX2X2sN1/S5Qw0nPyns3k= +github.com/rwinkhart/peercred-mini v0.0.0-20250405214620-93ccab17290b/go.mod h1:JNvJiNItSyk3JXfeDvdt1sPmY5NHqj7TJ2faTobtQHA= +github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405010723-99a5f0732c0e h1:YRpZcbGU/LVLwK87IURH4Sfye6Fv0ri1IRZ27loWpHs= +github.com/rwinkhart/sys-freebsd-13-xucred v0.0.0-20250405010723-99a5f0732c0e/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=