wg-quicker/vendor/golang.zx2c4.com/wireguard/wgctrl/internal/wguser/conn_windows.go

251 lines
6.5 KiB
Go

//+build windows
package wguser
import (
"errors"
"net"
"os"
"runtime"
"strings"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/ipc/winpipe"
)
// Expected prefixes when dealing with named pipes.
const (
pipePrefix = `\\.\pipe\`
wgPrefix = `ProtectedPrefix\Administrators\WireGuard\`
)
// dial is the default implementation of Client.dial.
func dial(device string) (net.Conn, error) {
// Thanks to @zx2c4 for the sample code that makes this possible:
// https://github.com/WireGuard/wgctrl-go/issues/36#issuecomment-491912143.
//
// See also:
// https://docs.microsoft.com/en-us/windows/desktop/secauthz/impersonation-tokens
// https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-reverttoself
//
// All of these operations require a locked OS thread for the duration of
// this function. Once the pipe is opened successfully, RevertToSelf
// terminates the impersonation of a client application.
runtime.LockOSThread()
defer func() {
// Terminate the token impersonation operation. Per the Microsoft
// documentation, the process should shut down if RevertToSelf fails.
if err := windows.RevertToSelf(); err != nil {
panicf("wguser: failed to terminate token impersonation, panicking per Microsoft recommendation: %v", err)
}
runtime.UnlockOSThread()
}()
privileges := windows.Tokenprivileges{
PrivilegeCount: 1,
Privileges: [1]windows.LUIDAndAttributes{
{
Attributes: windows.SE_PRIVILEGE_ENABLED,
},
},
}
err := windows.LookupPrivilegeValue(
nil,
windows.StringToUTF16Ptr("SeDebugPrivilege"),
&privileges.Privileges[0].Luid,
)
if err != nil {
return nil, err
}
processes, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if err != nil {
return nil, err
}
defer windows.CloseHandle(processes)
e := windows.ProcessEntry32{
Size: uint32(unsafe.Sizeof(windows.ProcessEntry32{})),
}
// Iterate the process list looking for any processes named winlogon.exe.
//
// It is possible for an attacker to attempt a denial of service of this
// application by creating bogus processes with that name, so we must
// attempt dialing a connection for each matching process until we either
// succeed or run out of processes to try.
//
// It is unlikely that an attacker's process could appear before the true
// winlogon.exe in this list, but better safe than sorry.
for err := windows.Process32First(processes, &e); ; err = windows.Process32Next(processes, &e) {
// Handle any errors from process list iteration.
switch err {
case nil:
// Keep iterating processes.
case windows.ERROR_NO_MORE_FILES:
// No more files to check.
return nil, errors.New("wguser: unable to find suitable winlogon.exe process to communicate with WireGuard")
default:
return nil, err
}
if strings.ToLower(windows.UTF16ToString(e.ExeFile[:])) != "winlogon.exe" {
continue
}
// Can we communicate with the device by impersonating this process?
c, err := tryDial(device, e.ProcessID, privileges)
switch {
case err == nil:
// Success, use this connection.
return c, nil
case os.IsPermission(err):
// We found a process named winlogon.exe that doesn't have permission
// to open a handle to the WireGuard device. Skip it and keep trying.
default:
return nil, err
}
}
}
// tryDial attempts to impersonate the security token of pid to dial device.
// tryDial _must_ only be invoked by dial.
func tryDial(device string, pid uint32, privileges windows.Tokenprivileges) (net.Conn, error) {
// Revert to normal thread state before attempting any further manipulation.
// See comment in dial about the panic.
if err := windows.RevertToSelf(); err != nil {
panicf("wguser: failed to terminate token impersonation, panicking per Microsoft recommendation: %v", err)
}
if err := windows.ImpersonateSelf(windows.SecurityImpersonation); err != nil {
return nil, err
}
thread, err := windows.GetCurrentThread()
if err != nil {
return nil, err
}
defer windows.CloseHandle(thread)
var ttok windows.Token
err = windows.OpenThreadToken(
thread,
windows.TOKEN_ADJUST_PRIVILEGES,
false,
&ttok,
)
if err != nil {
return nil, err
}
defer ttok.Close()
err = windows.AdjustTokenPrivileges(
ttok,
false,
&privileges,
uint32(unsafe.Sizeof(privileges)),
nil,
nil,
)
if err != nil {
return nil, err
}
proc, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, pid)
if err != nil {
return nil, err
}
defer windows.CloseHandle(proc)
var ptok windows.Token
err = windows.OpenProcessToken(
proc,
windows.TOKEN_IMPERSONATE|windows.TOKEN_DUPLICATE,
&ptok,
)
if err != nil {
return nil, err
}
defer ptok.Close()
var dup windows.Token
err = windows.DuplicateTokenEx(
ptok,
0,
nil,
windows.SecurityImpersonation,
windows.TokenImpersonation,
&dup,
)
if err != nil {
return nil, err
}
defer dup.Close()
if err := windows.SetThreadToken(nil, dup); err != nil {
return nil, err
}
localSystem, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
if err != nil {
return nil, err
}
return winpipe.DialPipe(device, nil, (*syscall.SID)(localSystem))
}
// find is the default implementation of Client.find.
func find() ([]string, error) {
return findNamedPipes(wgPrefix)
}
// findNamedPipes looks for Windows named pipes that match the specified
// search string prefix.
func findNamedPipes(search string) ([]string, error) {
var (
pipes []string
data windows.Win32finddata
)
// Thanks @zx2c4 for the tips on the appropriate Windows APIs here:
// https://א.cc/dHGpnhxX/c.
h, err := windows.FindFirstFile(
// Append * to find all named pipes.
windows.StringToUTF16Ptr(pipePrefix+"*"),
&data,
)
if err != nil {
return nil, err
}
// FindClose is used to close file search handles instead of the typical
// CloseHandle used elsewhere, see:
// https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-findclose.
defer windows.FindClose(h)
// Check the first file's name for a match, but also keep searching for
// WireGuard named pipes until no more files can be iterated.
for {
name := windows.UTF16ToString(data.FileName[:])
if strings.HasPrefix(name, search) {
// Concatenate strings directly as filepath.Join appears to break the
// named pipe prefix convention.
pipes = append(pipes, pipePrefix+name)
}
if err := windows.FindNextFile(h, &data); err != nil {
if err == windows.ERROR_NO_MORE_FILES {
break
}
return nil, err
}
}
return pipes, nil
}