aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2020-11-10 17:17:34 +0100
committerJason A. Donenfeld <Jason@zx2c4.com>2020-11-16 19:03:37 +0100
commita07fb45f3672167dda329f64618f93ad69851a35 (patch)
tree2b60fae6cd8d7715829e57da758056589e591e9d
parentfetcher: introduce downloader utility (diff)
downloadwireguard-windows-a07fb45f3672167dda329f64618f93ad69851a35.tar.xz
wireguard-windows-a07fb45f3672167dda329f64618f93ad69851a35.zip
conf: move configuration to C:\Program Files\WireGuard\Data
It doesn't get wiped out on Windows upgrades. Signed-off-by: Jason A. Donenfeld <Jason@zx2c4.com>
-rw-r--r--attacksurface.md2
-rw-r--r--conf/migration_windows.go62
-rw-r--r--conf/path_windows.go135
-rw-r--r--ringlogger/dump.go30
-rw-r--r--ringlogger/global.go2
-rw-r--r--updater/msirunner_windows.go8
6 files changed, 175 insertions, 64 deletions
diff --git a/attacksurface.md b/attacksurface.md
index 24fdbe02..7a4ff7d1 100644
--- a/attacksurface.md
+++ b/attacksurface.md
@@ -29,7 +29,7 @@ The manager service is a userspace service running as Local System, responsible
- Extensive IPC using unnamed pipes, inherited by the UI process.
- A readable `CreateFileMapping` handle to a binary ringlog shared by all services, inherited by the UI process.
- It listens for service changes in tunnel services according to the string prefix "WireGuardTunnel$".
- - It manages DPAPI-encrypted configuration files in Local System's local appdata directory, and makes some effort to enforce good configuration filenames.
+ - It manages DPAPI-encrypted configuration files in `C:\Program Files\WireGuard\Data`, which is created with `O:SYG:SYD:PAI(A;OICI;FA;;;SY)(A;OICI;FR;;;BA)`, and makes some effort to enforce good configuration filenames.
- It uses `WTSEnumerateSessions` and `WTSSESSION_NOTIFICATION` to walk through each available session. It then uses `WTSQueryUserToken`, and then calls `GetTokenInformation(TokenGroups)` on it. If one of the returned group's SIDs matches `IsWellKnownSid(WinBuiltinAdministratorsSid)`, and has attributes of either `SE_GROUP_ENABLED` or `SE_GROUP_USE_FOR_DENY_ONLY` and calling `GetTokenInformation(TokenElevation)` on it or its `TokenLinkedToken` indicates that either is elevated, then it spawns the UI process as that the elevated user token, passing it three unnamed pipe handles for IPC and the log mapping handle, as described above.
### UI
diff --git a/conf/migration_windows.go b/conf/migration_windows.go
index 72b298b6..9e5e1eb9 100644
--- a/conf/migration_windows.go
+++ b/conf/migration_windows.go
@@ -6,46 +6,58 @@
package conf
import (
+ "io/ioutil"
"log"
+ "os"
"path/filepath"
- "strings"
"golang.org/x/sys/windows"
)
-func maybeMigrate(c string) {
+func maybeMigrateConfiguration(c string) {
if disableAutoMigration {
return
}
-
- vol := filepath.VolumeName(c)
- withoutVol := strings.TrimPrefix(c, vol)
- oldRoot := filepath.Join(vol, "\\windows.old")
- oldC := filepath.Join(oldRoot, withoutVol)
-
- sd, err := windows.GetNamedSecurityInfo(oldRoot, windows.SE_FILE_OBJECT, windows.OWNER_SECURITY_INFORMATION)
- if err == windows.ERROR_PATH_NOT_FOUND || err == windows.ERROR_FILE_NOT_FOUND {
- return
- }
+ oldRoot, err := windows.KnownFolderPath(windows.FOLDERID_LocalAppData, windows.KF_FLAG_DEFAULT)
if err != nil {
- log.Printf("Not migrating configuration from ‘%s’ due to GetNamedSecurityInfo error: %v", oldRoot, err)
return
}
- owner, defaulted, err := sd.Owner()
+ oldC := filepath.Join(oldRoot, "WireGuard", "Configurations")
+ files, err := ioutil.ReadDir(oldC)
if err != nil {
- log.Printf("Not migrating configuration from ‘%s’ due to GetSecurityDescriptorOwner error: %v", oldRoot, err)
- return
- }
- if defaulted || (!owner.IsWellKnown(windows.WinLocalSystemSid) && !owner.IsWellKnown(windows.WinBuiltinAdministratorsSid)) {
- log.Printf("Not migrating configuration from ‘%s’, as it is not explicitly owned by SYSTEM or Built-in Administrators, but rather ‘%v’", oldRoot, owner)
return
}
- err = windows.MoveFileEx(windows.StringToUTF16Ptr(oldC), windows.StringToUTF16Ptr(c), windows.MOVEFILE_COPY_ALLOWED)
- if err != nil {
- if err != windows.ERROR_FILE_NOT_FOUND && err != windows.ERROR_ALREADY_EXISTS {
- log.Printf("Not migrating configuration from ‘%s’ due to error when moving files: %v", oldRoot, err)
+ for i := range files {
+ if files[i].IsDir() {
+ continue
}
- return
+ fileName := files[i].Name()
+ newPath := filepath.Join(c, fileName)
+ newFile, err := os.OpenFile(newPath, os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0600)
+ if err != nil {
+ continue
+ }
+ oldPath := filepath.Join(oldC, fileName)
+ oldConfig, err := ioutil.ReadFile(oldPath)
+ if err != nil {
+ newFile.Close()
+ os.Remove(newPath)
+ continue
+ }
+ _, err = newFile.Write(oldConfig)
+ if err != nil {
+ newFile.Close()
+ os.Remove(newPath)
+ continue
+ }
+ newFile.Close()
+ os.Remove(oldPath)
+ log.Printf("Migrated configuration from ‘%s’ to ‘%s’", oldPath, newPath)
+ }
+ if os.Remove(oldC) == nil {
+ oldLog := filepath.Join(oldRoot, "WireGuard", "log.bin")
+ oldRoot := filepath.Join(oldRoot, "WireGuard")
+ os.Remove(oldLog)
+ os.Remove(oldRoot)
}
- log.Printf("Migrated configuration from ‘%s’", oldRoot)
}
diff --git a/conf/path_windows.go b/conf/path_windows.go
index a53968c5..0d7e09b3 100644
--- a/conf/path_windows.go
+++ b/conf/path_windows.go
@@ -6,10 +6,17 @@
package conf
import (
+ "debug/pe"
+ "errors"
+ "fmt"
"os"
"path/filepath"
+ "strings"
+ "syscall"
+ "unsafe"
"golang.org/x/sys/windows"
+ "golang.org/x/sys/windows/registry"
)
var cachedConfigFileDir string
@@ -20,16 +27,16 @@ func tunnelConfigurationsDirectory() (string, error) {
if cachedConfigFileDir != "" {
return cachedConfigFileDir, nil
}
- root, err := RootDirectory()
+ root, err := RootDirectory(true)
if err != nil {
return "", err
}
c := filepath.Join(root, "Configurations")
- maybeMigrate(c)
- err = os.MkdirAll(c, os.ModeDir|0700)
- if err != nil {
+ err = os.Mkdir(c, os.ModeDir|0700)
+ if err != nil && !os.IsExist(err) {
return "", err
}
+ maybeMigrateConfiguration(c)
cachedConfigFileDir = c
return cachedConfigFileDir, nil
}
@@ -42,19 +49,129 @@ func PresetRootDirectory(root string) {
disableAutoMigration = true
}
-func RootDirectory() (string, error) {
+// TODO: replace with x/sys/windows upstreamed function
+func setKernelObjectSecurity(handle windows.Handle, securityInformation windows.SECURITY_INFORMATION, securityDescriptor *windows.SECURITY_DESCRIPTOR) (err error) {
+ r1, _, e1 := syscall.Syscall(windows.NewLazySystemDLL("advapi32.dll").NewProc("SetKernelObjectSecurity").Addr(), 3, uintptr(handle), uintptr(securityInformation), uintptr(unsafe.Pointer(securityDescriptor)))
+ if r1 == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+func getFinalPathNameByHandle(file windows.Handle, filePath *uint16, filePathSize uint32, flags uint32) (n uint32, err error) {
+ r0, _, e1 := syscall.Syscall6(windows.NewLazySystemDLL("kernel32.dll").NewProc("GetFinalPathNameByHandleW").Addr(), 4, uintptr(file), uintptr(unsafe.Pointer(filePath)), uintptr(filePathSize), uintptr(flags), 0, 0)
+ n = uint32(r0)
+ if n == 0 {
+ err = errnoErr(e1)
+ }
+ return
+}
+
+func RootDirectory(create bool) (string, error) {
if cachedRootDir != "" {
return cachedRootDir, nil
}
- root, err := windows.KnownFolderPath(windows.FOLDERID_LocalAppData, windows.KF_FLAG_CREATE)
+ var isWow bool
+ var processMachine, nativeMachine uint16
+ err := windows.IsWow64Process2(windows.CurrentProcess(), &processMachine, &nativeMachine)
+ if err == nil {
+ isWow = processMachine != pe.IMAGE_FILE_MACHINE_UNKNOWN
+ } else {
+ if !errors.Is(err, windows.ERROR_PROC_NOT_FOUND) {
+ return "", err
+ }
+ err = windows.IsWow64Process(windows.CurrentProcess(), &isWow)
+ if err != nil {
+ return "", err
+ }
+ }
+ var root string
+ if !isWow {
+ root, err = windows.KnownFolderPath(windows.FOLDERID_ProgramFiles, windows.KF_FLAG_CREATE)
+ if err != nil {
+ return "", err
+ }
+ } else {
+ key, err := registry.OpenKey(windows.HKEY_LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion`, registry.READ|registry.WOW64_64KEY)
+ if err != nil {
+ return "", err
+ }
+ var typ uint32
+ root, typ, err = key.GetStringValue("ProgramFilesDir")
+ key.Close()
+ if err != nil {
+ return "", err
+ }
+ if typ != registry.SZ {
+ return "", registry.ErrUnexpectedType
+ }
+ }
+ root = filepath.Join(root, "WireGuard")
+ if !create {
+ return filepath.Join(root, "Data"), nil
+ }
+ root16, err := windows.UTF16PtrFromString(root)
+ if err != nil {
+ return "", err
+ }
+
+ // The root directory inherits its ACL from Program Files; we don't want to mess with that
+ err = windows.CreateDirectory(root16, nil)
+ if err != nil && err != windows.ERROR_ALREADY_EXISTS {
+ return "", err
+ }
+
+ dataDirectorySd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:PAI(A;OICI;FA;;;SY)(A;OICI;FR;;;BA)")
if err != nil {
return "", err
}
- c := filepath.Join(root, "WireGuard")
- err = os.MkdirAll(c, os.ModeDir|0700)
+ dataDirectorySa := &windows.SecurityAttributes{
+ Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
+ SecurityDescriptor: dataDirectorySd,
+ }
+
+ data := filepath.Join(root, "Data")
+ data16, err := windows.UTF16PtrFromString(data)
+ if err != nil {
+ return "", err
+ }
+ var dataHandle windows.Handle
+ for {
+ err = windows.CreateDirectory(data16, dataDirectorySa)
+ if err != nil && err != windows.ERROR_ALREADY_EXISTS {
+ return "", err
+ }
+ dataHandle, err = windows.CreateFile(data16, windows.READ_CONTROL|windows.WRITE_OWNER|windows.WRITE_DAC, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE, nil, windows.OPEN_EXISTING, windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
+ if err != nil && err != windows.ERROR_FILE_NOT_FOUND {
+ return "", err
+ }
+ if err == nil {
+ break
+ }
+ }
+ defer windows.CloseHandle(dataHandle)
+ var fileInfo windows.ByHandleFileInformation
+ err = windows.GetFileInformationByHandle(dataHandle, &fileInfo)
+ if err != nil {
+ return "", err
+ }
+ if fileInfo.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
+ return "", errors.New("Data directory is actually a file")
+ }
+ if fileInfo.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
+ return "", errors.New("Data directory is reparse point")
+ }
+ var buf [windows.MAX_PATH * 4]uint16
+ _, err = getFinalPathNameByHandle(dataHandle, &buf[0], uint32(len(buf)), 0)
+ if err != nil {
+ return "", err
+ }
+ if !strings.EqualFold(`\\?\`+data, windows.UTF16ToString(buf[:])) {
+ return "", fmt.Errorf("Data directory jumped to unexpected location: got %q; want %q", windows.UTF16ToString(buf[:]), `\\?\`+data)
+ }
+ err = setKernelObjectSecurity(dataHandle, windows.DACL_SECURITY_INFORMATION|windows.GROUP_SECURITY_INFORMATION|windows.OWNER_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION, dataDirectorySd)
if err != nil {
return "", err
}
- cachedRootDir = c
+ cachedRootDir = data
return cachedRootDir, nil
}
diff --git a/ringlogger/dump.go b/ringlogger/dump.go
index 05a9b27f..495f08bd 100644
--- a/ringlogger/dump.go
+++ b/ringlogger/dump.go
@@ -11,36 +11,16 @@ import (
"path/filepath"
"golang.org/x/sys/windows"
- "golang.org/x/sys/windows/registry"
"golang.zx2c4.com/wireguard/windows/conf"
)
-func DumpTo(out io.Writer, localSystem bool) error {
- var path string
- if !localSystem {
- root, err := conf.RootDirectory()
- if err != nil {
- return err
- }
- path = filepath.Join(root, "log.bin")
- } else {
- k, err := registry.OpenKey(registry.LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\S-1-5-18", registry.QUERY_VALUE)
- if err != nil {
- return err
- }
- defer k.Close()
-
- systemprofile, _, err := k.GetStringValue("ProfileImagePath")
- if err != nil {
- return err
- }
- systemprofile, err = registry.ExpandString(systemprofile)
- if err != nil {
- return err
- }
- path = filepath.Join(systemprofile, "AppData", "Local", "WireGuard", "log.bin")
+func DumpTo(out io.Writer, notSystem bool) error {
+ root, err := conf.RootDirectory(!notSystem)
+ if err != nil {
+ return err
}
+ path := filepath.Join(root, "log.bin")
file, err := os.Open(path)
if err != nil {
return err
diff --git a/ringlogger/global.go b/ringlogger/global.go
index 60e750a5..06d3840e 100644
--- a/ringlogger/global.go
+++ b/ringlogger/global.go
@@ -18,7 +18,7 @@ func InitGlobalLogger(tag string) error {
if Global != nil {
return nil
}
- root, err := conf.RootDirectory()
+ root, err := conf.RootDirectory(true)
if err != nil {
return err
}
diff --git a/updater/msirunner_windows.go b/updater/msirunner_windows.go
index c11d4db9..f7ee35a2 100644
--- a/updater/msirunner_windows.go
+++ b/updater/msirunner_windows.go
@@ -68,9 +68,11 @@ func msiTempFile() (*os.File, error) {
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: sd,
}
- // TODO: os.TempDir() returns C:\windows\temp when calling from this context. Supposedly this is mostly secure
- // against TOCTOU, but who knows! Look into this!
- name := filepath.Join(os.TempDir(), hex.EncodeToString(randBytes[:]))
+ windir, err := windows.GetWindowsDirectory()
+ if err != nil {
+ return nil, err
+ }
+ name := filepath.Join(windir, "Temp", hex.EncodeToString(randBytes[:]))
name16 := windows.StringToUTF16Ptr(name)
// TODO: it would be nice to specify delete_on_close, but msiexec.exe doesn't open its files with read sharing.
fileHandle, err := windows.CreateFile(name16, windows.GENERIC_WRITE, windows.FILE_SHARE_READ, sa, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL, 0)