fix Task1 publication and secret preflight
This commit is contained in:
@@ -20,9 +20,10 @@ const (
|
||||
// Retained input handles deny delete sharing while permitting ordinary reads
|
||||
// and writes by trusted callers.
|
||||
windowsRetainedHandleShareMode uint32 = windows.FILE_SHARE_READ | windows.FILE_SHARE_WRITE
|
||||
// Outputs are opened for exclusive publication: deny write/delete sharing,
|
||||
// but permit the exact-identity read recheck below.
|
||||
windowsOutputHandleShareMode uint32 = windows.FILE_SHARE_READ
|
||||
// A discoverable stage is protected by mandatory zero-share semantics for its
|
||||
// entire lifetime. This denies reads, writes, rename, delete, and hard-link
|
||||
// acquisition by another handle until our final handle-relative rename.
|
||||
windowsOutputHandleShareMode uint32 = 0
|
||||
)
|
||||
|
||||
// ReadCanonicalRegular opens each component with FILE_FLAG_OPEN_REPARSE_POINT and rejects a
|
||||
@@ -139,6 +140,10 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
defer closeWindowsHandles(retainedParents)
|
||||
if len(retainedParents) == 0 {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
parentHandle := retainedParents[len(retainedParents)-1]
|
||||
securityDescriptor, securityAttributes, err := ownerOnlySecurityAttributes()
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
@@ -149,77 +154,86 @@ func writeCanonicalExclusive(path string, contents []byte, mode fs.FileMode) err
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
stagePath := filepath.Join(parent, stageName)
|
||||
stageHandle, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_WRITE, windowsOutputHandleShareMode, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
stageHandle, err := windows.CreateFile(windows.StringToUTF16Ptr(stagePath), windows.GENERIC_WRITE|windows.DELETE, windowsOutputHandleShareMode, securityAttributes, windows.CREATE_NEW, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
if err != nil {
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
stageFile := os.NewFile(uintptr(stageHandle), "thothctl-safeio-stage")
|
||||
if stageFile == nil {
|
||||
windows.CloseHandle(stageHandle)
|
||||
_ = windows.DeleteFile(windows.StringToUTF16Ptr(stagePath))
|
||||
_ = deleteWindowsHandle(stageHandle)
|
||||
_ = windows.CloseHandle(stageHandle)
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
stageCreated := true
|
||||
published := false
|
||||
var staged, publishedIdentity windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &staged); err != nil || staged.NumberOfLinks != 1 {
|
||||
_ = stageFile.Close()
|
||||
_ = windows.DeleteFile(windows.StringToUTF16Ptr(stagePath))
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
cleanup := func() {
|
||||
if published {
|
||||
removeWindowsIfIdentity(filepath.Join(parent, filepath.Base(path)), publishedIdentity)
|
||||
}
|
||||
if stageCreated {
|
||||
removeWindowsIfIdentity(stagePath, staged)
|
||||
closed := false
|
||||
closeStage := func() error {
|
||||
if closed {
|
||||
return nil
|
||||
}
|
||||
closed = true
|
||||
return stageFile.Close()
|
||||
}
|
||||
fail := func() error {
|
||||
_ = stageFile.Close()
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
defer func() { _ = closeStage() }()
|
||||
var staged windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &staged); err != nil || staged.NumberOfLinks != 1 || staged.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || staged.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||
return failWindowsStage(stageHandle, closeStage)
|
||||
}
|
||||
fail := func() error { _ = deleteWindowsHandle(stageHandle); _ = closeStage(); return ErrUnsafeFile }
|
||||
if n, err := stageFile.Write(contents); err != nil || n != len(contents) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Sync(); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// The stage name is visible on Win32. If a same-user actor hard-links it,
|
||||
// the bytes are already public; do not turn that observation into a failure
|
||||
// whose cleanup could not remove the attacker's link.
|
||||
var afterWrite windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &afterWrite); err != nil || afterWrite.NumberOfLinks == 0 || afterWrite.FileSizeHigh != uint32(uint64(len(contents))>>32) || afterWrite.FileSizeLow != uint32(len(contents)) {
|
||||
var after windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(stageHandle, &after); err != nil || after.NumberOfLinks != 1 || after.FileSizeHigh != uint32(uint64(len(contents))>>32) || after.FileSizeLow != uint32(len(contents)) {
|
||||
return fail()
|
||||
}
|
||||
if err := stageFile.Close(); err != nil {
|
||||
cleanup()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
// CreateHardLink is an atomic, same-volume, no-replace publication. The final
|
||||
// pathname can never refer to a partially written candidate.
|
||||
finalPath := filepath.Join(parent, filepath.Base(path))
|
||||
if err := windows.CreateHardLink(windows.StringToUTF16Ptr(finalPath), windows.StringToUTF16Ptr(stagePath), 0); err != nil {
|
||||
return fail()
|
||||
}
|
||||
published = true
|
||||
publishedIdentity = staged
|
||||
check, identityErr := windowsFileIdentity(finalPath)
|
||||
if identityErr != nil || check.NumberOfLinks < 2 || !sameWindowsFile(staged, check) {
|
||||
return fail()
|
||||
}
|
||||
if err := windows.DeleteFile(windows.StringToUTF16Ptr(stagePath)); err != nil {
|
||||
return fail()
|
||||
}
|
||||
stageCreated = false
|
||||
finalIdentity, identityErr := windowsFileIdentity(finalPath)
|
||||
if identityErr != nil || finalIdentity.NumberOfLinks == 0 || !sameWindowsFile(staged, finalIdentity) {
|
||||
// Keep the exact stage handle open with zero sharing through this final check
|
||||
// and atomic no-replace rename. No pathname reopen or cleanup is needed.
|
||||
if err := renameWindowsHandle(stageHandle, parentHandle, filepath.Base(path)); err != nil {
|
||||
return fail()
|
||||
}
|
||||
// Handle-relative rename is the final commit. Handle close is intentionally
|
||||
// ignored after success; no fallible observation or pathname cleanup follows.
|
||||
_ = closeStage()
|
||||
return nil
|
||||
}
|
||||
|
||||
// failWindowsStage disposes an exact handle when initial identity inspection fails.
|
||||
func failWindowsStage(handle windows.Handle, closeStage func() error) error {
|
||||
_ = deleteWindowsHandle(handle)
|
||||
_ = closeStage()
|
||||
return ErrUnsafeFile
|
||||
}
|
||||
|
||||
func deleteWindowsHandle(handle windows.Handle) error {
|
||||
var disposition byte = 1
|
||||
return windows.SetFileInformationByHandle(handle, windows.FileDispositionInfo, &disposition, uint32(unsafe.Sizeof(disposition)))
|
||||
}
|
||||
|
||||
type windowsFileRenameInfo struct {
|
||||
ReplaceIfExists uint8
|
||||
RootDirectory windows.Handle
|
||||
FileNameLength uint32
|
||||
FileName [1]uint16
|
||||
}
|
||||
|
||||
func renameWindowsHandle(handle, parent windows.Handle, leaf string) error {
|
||||
name, err := windows.UTF16FromString(leaf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name = name[:len(name)-1]
|
||||
base := unsafe.Offsetof(windowsFileRenameInfo{}.FileName)
|
||||
buffer := make([]byte, int(base)+len(name)*2)
|
||||
info := (*windowsFileRenameInfo)(unsafe.Pointer(&buffer[0]))
|
||||
info.ReplaceIfExists = 0
|
||||
info.RootDirectory = parent
|
||||
info.FileNameLength = uint32(len(name) * 2)
|
||||
nameBytes := unsafe.Slice((*uint16)(unsafe.Pointer(&buffer[base])), len(name))
|
||||
copy(nameBytes, name)
|
||||
return windows.SetFileInformationByHandle(handle, windows.FileRenameInfo, &buffer[0], uint32(len(buffer)))
|
||||
}
|
||||
func privateWindowsStageName() (string, error) {
|
||||
var random [16]byte
|
||||
if _, err := rand.Read(random[:]); err != nil {
|
||||
@@ -228,26 +242,6 @@ func privateWindowsStageName() (string, error) {
|
||||
return ".thothctl-candidate-" + hex.EncodeToString(random[:]), nil
|
||||
}
|
||||
|
||||
func windowsFileIdentity(path string) (windows.ByHandleFileInformation, error) {
|
||||
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path), windows.GENERIC_READ, windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE, nil, windows.OPEN_EXISTING, windows.FILE_ATTRIBUTE_NORMAL|windows.FILE_FLAG_OPEN_REPARSE_POINT, 0)
|
||||
if err != nil {
|
||||
return windows.ByHandleFileInformation{}, err
|
||||
}
|
||||
defer windows.CloseHandle(h)
|
||||
var information windows.ByHandleFileInformation
|
||||
if err := windows.GetFileInformationByHandle(h, &information); err != nil || information.NumberOfLinks == 0 || information.FileAttributes&windows.FILE_ATTRIBUTE_REPARSE_POINT != 0 || information.FileAttributes&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||
return windows.ByHandleFileInformation{}, ErrUnsafeFile
|
||||
}
|
||||
return information, nil
|
||||
}
|
||||
|
||||
func removeWindowsIfIdentity(path string, expected windows.ByHandleFileInformation) {
|
||||
got, err := windowsFileIdentity(path)
|
||||
if err == nil && sameWindowsFile(got, expected) {
|
||||
_ = windows.DeleteFile(windows.StringToUTF16Ptr(path))
|
||||
}
|
||||
}
|
||||
|
||||
func openWindowsParents(path string) (string, []windows.Handle, error) {
|
||||
volume := filepath.VolumeName(path)
|
||||
root := volume + string(filepath.Separator)
|
||||
@@ -348,7 +342,7 @@ func ownerOnlySecurityAttributes() (*windows.SECURITY_DESCRIPTOR, *windows.Secur
|
||||
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
|
||||
}
|
||||
entries := []windows.EXPLICIT_ACCESS{{
|
||||
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE,
|
||||
AccessPermissions: windows.FILE_GENERIC_READ | windows.FILE_GENERIC_WRITE | windows.DELETE,
|
||||
AccessMode: windows.SET_ACCESS,
|
||||
Inheritance: windows.NO_INHERITANCE,
|
||||
Trustee: trustee,
|
||||
|
||||
Reference in New Issue
Block a user