Files
sing-box/experimental/boxdd/security_windows.go
T
2026-07-13 15:08:24 +08:00

516 lines
15 KiB
Go

//go:build windows
package main
import (
"bytes"
"crypto/sha1"
"encoding/binary"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"unicode/utf16"
"unsafe"
E "github.com/sagernet/sing/common/exceptions"
"github.com/tailscale/go-winio"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc/mgr"
)
const (
trustedInstallerUserID = "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"
fileDeleteChildAccess = 0x00000040
)
func secureWindowsInstallation(executablePath string, allowUnsafeInstallation bool) (string, error) {
daemonExecutable, err := openLockedExecutable(executablePath)
if err != nil {
return "", err
}
defer windows.CloseHandle(daemonExecutable)
daemonPath, err := finalWindowsPath(daemonExecutable)
if err != nil {
return "", err
}
installationDirectory, applicationPath, err := installedApplicationPath(daemonPath)
if err != nil {
return "", err
}
applicationExecutable, err := openLockedExecutable(applicationPath)
if err != nil {
return "", E.Cause(err, "open installed application")
}
defer windows.CloseHandle(applicationExecutable)
daemonSigner, err := authenticodeSigner(daemonPath, daemonExecutable)
if err != nil {
return "", E.Cause(err, "authenticate installed daemon")
}
applicationFinalPath, err := finalWindowsPath(applicationExecutable)
if err != nil {
return "", err
}
applicationSigner, err := authenticodeSigner(applicationFinalPath, applicationExecutable)
if err != nil {
return "", E.Cause(err, "authenticate installed application")
}
if !bytes.Equal(daemonSigner, applicationSigner) {
return "", E.New("installed application and daemon have different signing certificates")
}
volumeRoot, err := validateFixedNTFSVolume(installationDirectory)
if err != nil {
return "", err
}
err = validateInstallationAncestors(filepath.Dir(installationDirectory), volumeRoot, !allowUnsafeInstallation)
if err != nil {
return "", err
}
err = validateTreeHasNoReparsePoints(installationDirectory)
if err != nil {
return "", err
}
err = applyProtectedTree(
installationDirectory,
"O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;GRGX;;;AU)",
"O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;GRGX;;;AU)",
)
if err != nil {
return "", err
}
return daemonPath, nil
}
func installedApplicationPath(daemonPath string) (string, string, error) {
daemonDirectory := filepath.Dir(daemonPath)
resourcesDirectory := filepath.Dir(daemonDirectory)
installationDirectory := filepath.Dir(resourcesDirectory)
if !strings.EqualFold(filepath.Base(daemonPath), daemonExecutableName) ||
!strings.EqualFold(filepath.Base(daemonDirectory), "daemon") ||
!strings.EqualFold(filepath.Base(resourcesDirectory), "resources") {
return "", "", E.New("daemon executable is outside the installed sing-box layout")
}
return installationDirectory, filepath.Join(installationDirectory, applicationExecutableName), nil
}
func secureWindowsWorkingDirectory(path string) error {
serviceUserID, err := windowsServiceSID()
if err != nil {
return E.Cause(err, "create daemon service SID")
}
serviceUserIDString := serviceUserID.String()
if serviceUserIDString == "" {
return E.New("daemon service has an invalid SID")
}
err = validateTreeHasNoReparsePoints(path)
if err != nil {
return err
}
directoryDescriptor := fmt.Sprintf(
"O:SYG:SYD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;%s)",
serviceUserIDString,
)
fileDescriptor := fmt.Sprintf(
"O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;%s)",
serviceUserIDString,
)
return applyProtectedTree(path, directoryDescriptor, fileDescriptor)
}
func windowsServiceSID() (*windows.SID, error) {
serviceNameUTF16 := utf16.Encode([]rune(strings.ToUpper(serviceName)))
serviceNameContent := make([]byte, len(serviceNameUTF16)*2)
for index, codeUnit := range serviceNameUTF16 {
binary.LittleEndian.PutUint16(serviceNameContent[index*2:], codeUnit)
}
serviceNameHash := sha1.Sum(serviceNameContent)
return windows.StringToSid(fmt.Sprintf(
"S-1-5-80-%d-%d-%d-%d-%d",
binary.LittleEndian.Uint32(serviceNameHash[0:4]),
binary.LittleEndian.Uint32(serviceNameHash[4:8]),
binary.LittleEndian.Uint32(serviceNameHash[8:12]),
binary.LittleEndian.Uint32(serviceNameHash[12:16]),
binary.LittleEndian.Uint32(serviceNameHash[16:20]),
))
}
func validateProtectedWindowsWorkingDirectory(path string, serviceUserID *windows.SID) error {
attributes, err := windowsFileAttributes(path)
if err != nil {
return err
}
if attributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
return E.New("daemon working directory path is not a directory")
}
if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return E.New("daemon working directory is a reparse point")
}
descriptor, err := windows.GetNamedSecurityInfo(
path,
windows.SE_FILE_OBJECT,
windows.OWNER_SECURITY_INFORMATION|windows.GROUP_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
)
if err != nil {
return err
}
owner, _, err := descriptor.Owner()
if err != nil {
return err
}
systemUserID, err := windows.CreateWellKnownSid(windows.WinLocalSystemSid)
if err != nil {
return err
}
if !windows.EqualSid(owner, systemUserID) {
return E.New("daemon working directory is not owned by SYSTEM")
}
control, _, err := descriptor.Control()
if err != nil {
return err
}
if control&windows.SE_DACL_PROTECTED == 0 {
return E.New("daemon working directory access control is inherited")
}
discretionaryAccessControlList, _, err := descriptor.DACL()
if err != nil {
return err
}
if discretionaryAccessControlList == nil || discretionaryAccessControlList.AceCount != 3 {
return E.New("daemon working directory has unexpected access control entries")
}
administratorsUserID, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
if err != nil {
return err
}
expectedUsers := map[string]bool{
systemUserID.String(): false,
administratorsUserID.String(): false,
serviceUserID.String(): false,
}
for index := uint32(0); index < uint32(discretionaryAccessControlList.AceCount); index++ {
var accessControlEntry *windows.ACCESS_ALLOWED_ACE
err = windows.GetAce(discretionaryAccessControlList, index, &accessControlEntry)
if err != nil {
return err
}
if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE ||
accessControlEntry.Header.AceFlags != windows.OBJECT_INHERIT_ACE|windows.CONTAINER_INHERIT_ACE ||
uint32(accessControlEntry.Mask) != 0x001F01FF {
return E.New("daemon working directory has an unsafe access control entry")
}
userID := (*windows.SID)(unsafe.Pointer(&accessControlEntry.SidStart)).String()
seen, exists := expectedUsers[userID]
if !exists || seen {
return E.New("daemon working directory grants access to an unexpected principal")
}
expectedUsers[userID] = true
}
for _, seen := range expectedUsers {
if !seen {
return E.New("daemon working directory is missing a required access control entry")
}
}
return nil
}
func applyProtectedServiceSecurity(service *mgr.Service) error {
descriptor, err := windows.SecurityDescriptorFromString(
"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x2008d;;;AU)",
)
if err != nil {
return err
}
discretionaryAccessControlList, _, err := descriptor.DACL()
if err != nil {
return err
}
return windows.SetSecurityInfo(
service.Handle,
windows.SE_SERVICE,
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
nil,
nil,
discretionaryAccessControlList,
nil,
)
}
func allowAuthenticatedUsersToQueryCurrentProcess() error {
descriptor, err := windows.SecurityDescriptorFromString(
"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x101000;;;AU)",
)
if err != nil {
return err
}
discretionaryAccessControlList, _, err := descriptor.DACL()
if err != nil {
return err
}
return windows.SetSecurityInfo(
windows.CurrentProcess(),
windows.SE_KERNEL_OBJECT,
windows.DACL_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION,
nil,
nil,
discretionaryAccessControlList,
nil,
)
}
func validateFixedNTFSVolume(path string) (string, error) {
pathPointer, err := windows.UTF16PtrFromString(path)
if err != nil {
return "", err
}
volumePathBuffer := make([]uint16, windows.MAX_LONG_PATH)
err = windows.GetVolumePathName(pathPointer, &volumePathBuffer[0], uint32(len(volumePathBuffer)))
if err != nil {
return "", E.Cause(err, "resolve installation volume")
}
volumePath := windows.UTF16ToString(volumePathBuffer)
volumePathPointer, err := windows.UTF16PtrFromString(volumePath)
if err != nil {
return "", err
}
if windows.GetDriveType(volumePathPointer) != windows.DRIVE_FIXED {
return "", E.New("sing-box must be installed on a fixed local drive")
}
fileSystemNameBuffer := make([]uint16, 32)
err = windows.GetVolumeInformation(
volumePathPointer,
nil,
0,
nil,
nil,
nil,
&fileSystemNameBuffer[0],
uint32(len(fileSystemNameBuffer)),
)
if err != nil {
return "", E.Cause(err, "query installation file system")
}
if !strings.EqualFold(windows.UTF16ToString(fileSystemNameBuffer), "NTFS") {
return "", E.New("sing-box must be installed on NTFS")
}
return filepath.Clean(volumePath), nil
}
func validateInstallationAncestors(path string, volumeRoot string, validatePermissions bool) error {
currentPath := filepath.Clean(path)
cleanVolumeRoot := filepath.Clean(volumeRoot)
for {
err := validateInstallationAncestor(currentPath, validatePermissions)
if err != nil {
return err
}
if strings.EqualFold(currentPath, cleanVolumeRoot) {
return nil
}
parentPath := filepath.Dir(currentPath)
if parentPath == currentPath {
return E.New("installation path is outside its resolved volume")
}
currentPath = parentPath
}
}
func validateInstallationAncestor(path string, validatePermissions bool) error {
attributes, err := windowsFileAttributes(path)
if err != nil {
return err
}
if attributes&windows.FILE_ATTRIBUTE_DIRECTORY == 0 {
return E.New("installation ancestor is not a directory: ", path)
}
if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return E.New("installation ancestor is a reparse point: ", path)
}
if !validatePermissions {
return nil
}
descriptor, err := windows.GetNamedSecurityInfo(
path,
windows.SE_FILE_OBJECT,
windows.OWNER_SECURITY_INFORMATION|windows.DACL_SECURITY_INFORMATION,
)
if err != nil {
return E.Cause(err, "query installation ancestor security")
}
owner, _, err := descriptor.Owner()
if err != nil {
return err
}
if !trustedAdministrativeUser(owner) {
return E.New("installation ancestor is owned by an unprivileged principal: ", path)
}
discretionaryAccessControlList, _, err := descriptor.DACL()
if err != nil {
return err
}
if discretionaryAccessControlList == nil {
return E.New("installation ancestor has an empty access control list: ", path)
}
for index := uint32(0); index < uint32(discretionaryAccessControlList.AceCount); index++ {
var accessControlEntry *windows.ACCESS_ALLOWED_ACE
err = windows.GetAce(discretionaryAccessControlList, index, &accessControlEntry)
if err != nil {
return err
}
if accessControlEntry.Header.AceFlags&windows.INHERIT_ONLY_ACE != 0 {
continue
}
if accessControlEntry.Header.AceType != windows.ACCESS_ALLOWED_ACE_TYPE {
continue
}
mask := uint32(accessControlEntry.Mask)
dangerousAccess := uint32(windows.DELETE | windows.WRITE_DAC | windows.WRITE_OWNER | windows.GENERIC_WRITE | windows.GENERIC_ALL | fileDeleteChildAccess)
if mask&dangerousAccess == 0 {
continue
}
principal := (*windows.SID)(unsafe.Pointer(&accessControlEntry.SidStart))
if !trustedAdministrativeUser(principal) {
return E.New("installation ancestor is replaceable by an unprivileged principal: ", path)
}
}
return nil
}
func trustedAdministrativeUser(userID *windows.SID) bool {
if userID == nil {
return false
}
return userID.IsWellKnown(windows.WinLocalSystemSid) ||
userID.IsWellKnown(windows.WinBuiltinAdministratorsSid) ||
userID.String() == trustedInstallerUserID
}
func validateTreeHasNoReparsePoints(root string) error {
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkError error) error {
if walkError != nil {
return walkError
}
attributes, err := windowsFileAttributes(path)
if err != nil {
return err
}
if attributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return E.New("protected tree contains a reparse point: ", path)
}
return nil
})
}
func applyProtectedTree(root string, directorySecurityDescriptor string, fileSecurityDescriptor string) error {
return winio.RunWithPrivilege(winio.SeRestorePrivilege, func() error {
directoryDescriptor, err := windows.SecurityDescriptorFromString(directorySecurityDescriptor)
if err != nil {
return err
}
fileDescriptor, err := windows.SecurityDescriptorFromString(fileSecurityDescriptor)
if err != nil {
return err
}
return filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkError error) error {
if walkError != nil {
return walkError
}
descriptor := fileDescriptor
if entry.IsDir() {
descriptor = directoryDescriptor
}
err = applyProtectedFileSecurity(path, descriptor)
if err != nil {
return E.Cause(err, "secure ", path)
}
return nil
})
})
}
func applyProtectedFileSecurity(path string, descriptor *windows.SECURITY_DESCRIPTOR) error {
owner, _, err := descriptor.Owner()
if err != nil {
return err
}
group, _, err := descriptor.Group()
if err != nil {
return err
}
discretionaryAccessControlList, _, err := descriptor.DACL()
if err != nil {
return err
}
return windows.SetNamedSecurityInfo(
path,
windows.SE_FILE_OBJECT,
windows.OWNER_SECURITY_INFORMATION|
windows.GROUP_SECURITY_INFORMATION|
windows.DACL_SECURITY_INFORMATION|
windows.PROTECTED_DACL_SECURITY_INFORMATION,
owner,
group,
discretionaryAccessControlList,
nil,
)
}
func windowsFileAttributes(path string) (uint32, error) {
pathPointer, err := windows.UTF16PtrFromString(path)
if err != nil {
return 0, err
}
return windows.GetFileAttributes(pathPointer)
}
func ensureWindowsWorkingDirectory(path string) error {
serviceUserID, err := windowsServiceSID()
if err != nil {
return E.Cause(err, "create daemon service SID")
}
created := false
_, err = os.Lstat(path)
if os.IsNotExist(err) {
serviceUserIDString := serviceUserID.String()
if serviceUserIDString == "" {
return E.New("daemon service has an invalid SID")
}
descriptor, descriptorError := windows.SecurityDescriptorFromString(
fmt.Sprintf("D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;%s)", serviceUserIDString),
)
if descriptorError != nil {
return descriptorError
}
pathPointer, pathError := windows.UTF16PtrFromString(path)
if pathError != nil {
return pathError
}
securityAttributes := &windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(windows.SecurityAttributes{})),
SecurityDescriptor: descriptor,
}
err = windows.CreateDirectory(pathPointer, securityAttributes)
if err != nil {
return E.Cause(err, "create protected daemon working directory")
}
created = true
} else if err != nil {
return err
}
if created {
err = secureWindowsWorkingDirectory(path)
if err != nil {
return err
}
}
err = validateProtectedWindowsWorkingDirectory(path, serviceUserID)
if err != nil {
return err
}
if created {
return nil
}
return secureWindowsWorkingDirectory(path)
}