From 5a932ccc460f91bda18868a0cd809f597442b171 Mon Sep 17 00:00:00 2001 From: Gabriel Adrian Samfira Date: Tue, 1 Nov 2022 02:55:33 -0700 Subject: [PATCH] Implement readUser on Windows This change allows us to parse the SAM registry hive to fetch the user information associated with the user we set in the USER stanza. This is the Windows equivalent of parsing the /etc/passwd file. Unfortunately the format of that hive is obscure. Signed-off-by: Gabriel Adrian Samfira --- internal/winapi/offline_registry_windows.go | 17 ++ internal/winapi/sam_windows.go | 206 ++++++++++++++++++++ internal/winapi/winapi_windows.go | 3 + internal/winapi/zsyscall_windows.go | 108 ++++++++++ solver/llbsolver/file/backend.go | 46 +---- solver/llbsolver/file/backend_unix.go | 49 +++++ solver/llbsolver/file/backend_windows.go | 19 ++ solver/llbsolver/file/user_nolinux.go | 4 +- solver/llbsolver/file/user_windows.go | 65 ++++++ 9 files changed, 475 insertions(+), 42 deletions(-) create mode 100644 internal/winapi/offline_registry_windows.go create mode 100644 internal/winapi/sam_windows.go create mode 100644 internal/winapi/winapi_windows.go create mode 100644 internal/winapi/zsyscall_windows.go create mode 100644 solver/llbsolver/file/backend_unix.go create mode 100644 solver/llbsolver/file/backend_windows.go create mode 100644 solver/llbsolver/file/user_windows.go diff --git a/internal/winapi/offline_registry_windows.go b/internal/winapi/offline_registry_windows.go new file mode 100644 index 000000000000..a0ca1aa9ad22 --- /dev/null +++ b/internal/winapi/offline_registry_windows.go @@ -0,0 +1,17 @@ +package winapi + +const ( + REG_OPTION_CREATE_LINK uint32 = 0x00000002 + REG_OPTION_NON_VOLATILE uint32 = 0x00000000 + MAX_KEY_NAME uint32 = 255 + MAX_VALUE_NAME uint32 = 16383 +) + +//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go +//sys OROpenHive(file *uint16, key *syscall.Handle) (regerrno error) = offreg.OROpenHive +//sys ORCloseHive(key *syscall.Handle) (regerrno error) = offreg.ORCloseHive +//sys OROpenKey(rootKey syscall.Handle, lpSubKeyName *uint16, key *syscall.Handle) (regerrno error) = offreg.OROpenKey +//sys ORCloseKey(key *syscall.Handle) (regerrno error) = offreg.ORCloseKey +//sys OREnumKey(key syscall.Handle, index uint32, name *uint16, nameSize *uint32, class *uint16, classSize *uint32, ftLastWriteTime uintptr) (regerrno error) = offreg.OREnumKey +//sys OREnumValue(key syscall.Handle, index uint32, valueName *uint16, valueNameSize *uint32, valueType *uint32, data *byte, bufferSize *uint32) (regerrno error) = offreg.OREnumValue +//sys ORQueryInfoKey(key syscall.Handle, lpClass *uint16, lpcClass *uint32, lpcSubKeys *uint32, lpcMaxSubKeyLen *uint32, lpcMaxClassLen *uint32, lpcValues *uint32, lpcMaxValueNameLen *uint32, lpcMaxValueLen *uint32, lpcbSecurityDescriptor *uint32, lpftLastWriteTime uintptr) (regerrno error) = offreg.ORQueryInfoKey diff --git a/internal/winapi/sam_windows.go b/internal/winapi/sam_windows.go new file mode 100644 index 000000000000..e658c0f2a4ea --- /dev/null +++ b/internal/winapi/sam_windows.go @@ -0,0 +1,206 @@ +package winapi + +import ( + "bytes" + "encoding/binary" + "fmt" + "strconv" + "strings" + "syscall" + "unicode/utf16" + + "golang.org/x/sys/windows" +) + +func decodeEntry(buffer []byte) (string, error) { + name := make([]uint16, len(buffer)/2) + err := binary.Read(bytes.NewReader(buffer), binary.LittleEndian, &name) + if err != nil { + return "", fmt.Errorf("decoding name: %w", err) + } + return string(utf16.Decode(name)), nil +} + +type samValue struct { + Name string + Data []byte +} + +type SAMUser struct { + Username string + SIDString string + RID int64 +} + +func reverse(s []byte) { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + s[i], s[j] = s[j], s[i] + } +} + +func (s *SAMUser) SID() (*windows.SID, error) { + if s.SIDString == "" { + return nil, fmt.Errorf("no sid available") + } + utfPtr, err := syscall.UTF16PtrFromString(s.SIDString) + if err != nil { + return nil, fmt.Errorf("converting string to utf-16 ptr: %w", err) + } + var sid *windows.SID + if err := windows.ConvertStringSidToSid(utfPtr, &sid); err != nil { + return nil, fmt.Errorf("fetching SID: %w", err) + } + return sid, nil +} + +func getValues(key syscall.Handle) ([]samValue, error) { + var subKeys uint32 + var nValues uint32 + if err := ORQueryInfoKey(key, nil, nil, &subKeys, nil, nil, &nValues, nil, nil, nil, 0); err != nil { + return nil, err + } + + ret := make([]samValue, nValues) + + var j uint32 = 0 + for j = 0; j < nValues; j++ { + nSize := MAX_VALUE_NAME + var dwType uint32 = 0 + var cbData uint32 = 0 + valueName := make([]uint16, nSize) + if err := OREnumValue(key, j, &valueName[0], &nSize, &dwType, nil, &cbData); err != syscall.ERROR_MORE_DATA { + continue + } + buffer := make([]byte, cbData) + + if err := OREnumValue(key, j, &valueName[0], &nSize, &dwType, &buffer[0], &cbData); err != nil { + return nil, err + } + valueNameString := syscall.UTF16ToString(valueName[:nSize]) + ret[j] = samValue{ + Name: valueNameString, + Data: buffer, + } + } + return ret, nil +} + +func parseUserInfo(data samValue, rid int64) (SAMUser, error) { + usernameOffset := binary.LittleEndian.Uint32(data.Data[12:16]) + usernameLen := binary.LittleEndian.Uint32(data.Data[16:20]) + ret, err := decodeEntry(data.Data[usernameOffset+0xCC : (usernameOffset+0xCC)+usernameLen]) + if err != nil { + return SAMUser{}, fmt.Errorf("decoding username: %w", err) + } + // Before the username offset, we have the Administrators group SID, twice. + // The user full SID starts 16 bytes before the two Administrator group SIDs. + // Skip 32 bytes, then fetch preceding 16 bytes we care about. + // See https://web.archive.org/web/20190423074618/http://www.beginningtoseethelight.org:80/ntsecurity/index.htm + // Sadly, the archived link is the best description of the format of the SAM hive + // I have been able to find. + sidBytes := data.Data[(usernameOffset+0xcc)-48 : (usernameOffset+0xcc)-32] + first := sidBytes[0:4] + second := sidBytes[4:8] + third := sidBytes[8:12] + fourth := sidBytes[12:16] + // fmt.Println(first, second, third, fourth) + reverse(first) + reverse(second) + reverse(third) + reverse(fourth) + foundRid := binary.BigEndian.Uint32(fourth) + if foundRid != uint32(rid) { + return SAMUser{}, nil + } + sid := fmt.Sprintf("S-1-5-21-%d-%d-%d-%d", binary.BigEndian.Uint32(first), binary.BigEndian.Uint32(second), binary.BigEndian.Uint32(third), binary.BigEndian.Uint32(fourth)) + return SAMUser{ + Username: ret, + SIDString: sid, + RID: rid, + }, nil +} + +func walkSAMUsers(rootKey syscall.Handle) ([]SAMUser, error) { + subkeyName, err := syscall.UTF16PtrFromString("sam\\Domains\\Account\\Users") + if err != nil { + return nil, fmt.Errorf("fetching utf16 pointer: %w", err) + } + + var users syscall.Handle + if err := OROpenKey(rootKey, subkeyName, &users); err != nil { + return nil, fmt.Errorf("opening users key: %w", err) + } + defer ORCloseKey(&users) + + var subkeys uint32 + var nvalues uint32 + if err := ORQueryInfoKey(users, nil, nil, &subkeys, nil, nil, &nvalues, nil, nil, nil, 0); err != nil { + return nil, fmt.Errorf("querying key info: %w", err) + } + + SAMUsers := []SAMUser{} + + var key uint32 + for key = 0; key < subkeys; key++ { + nSize := MAX_KEY_NAME + name := make([]uint16, nSize) + if err := OREnumKey(users, key, &name[0], &nSize, nil, nil, 0); err != nil { + return nil, fmt.Errorf("enumerating key: %w", err) + } + keyName := syscall.UTF16ToString(name[:nSize]) + if strings.EqualFold(keyName, "names") { + continue + } + + rid, err := strconv.ParseInt(keyName, 16, 64) + if err != nil { + return nil, fmt.Errorf("parsing RID: %w", err) + } + + data := name[:nSize] + var userKey syscall.Handle + if err := OROpenKey(users, &data[0], &userKey); err != nil { + return nil, fmt.Errorf("opening key: %w", err) + } + + values, err := getValues(userKey) + if err != nil { + return nil, fmt.Errorf("fetching values for key: %w", err) + } + + for _, val := range values { + if val.Name == "V" { + parsed, err := parseUserInfo(val, rid) + if err != nil { + return nil, fmt.Errorf("parsing user data: %w", err) + } + if parsed.SIDString == "" { + continue + } + SAMUsers = append(SAMUsers, parsed) + } + } + + } + + return SAMUsers, nil +} + +func GetUserInfoFromOfflineSAMHive(samPath string) ([]SAMUser, error) { + hivePath, err := syscall.UTF16PtrFromString("C:\\Users\\Administrator\\work\\getuser\\sam2") + if err != nil { + return nil, fmt.Errorf("getting path: %v", err) + } + + var key syscall.Handle + if err := OROpenHive(hivePath, &key); err != nil { + return nil, fmt.Errorf("opening hive: %v", err) + } + defer ORCloseHive(&key) + + users, err := walkSAMUsers(key) + if err != nil { + return nil, fmt.Errorf("parsing hive: %v", err) + } + return users, nil +} diff --git a/internal/winapi/winapi_windows.go b/internal/winapi/winapi_windows.go new file mode 100644 index 000000000000..6a90e3a69ac9 --- /dev/null +++ b/internal/winapi/winapi_windows.go @@ -0,0 +1,3 @@ +package winapi + +//go:generate go run github.com/Microsoft/go-winio/tools/mkwinsyscall -output zsyscall_windows.go ./*.go diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go new file mode 100644 index 000000000000..d73ddd60c26e --- /dev/null +++ b/internal/winapi/zsyscall_windows.go @@ -0,0 +1,108 @@ +//go:build windows + +// Code generated by 'go generate' using "github.com/Microsoft/go-winio/tools/mkwinsyscall"; DO NOT EDIT. + +package winapi + +import ( + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var _ unsafe.Pointer + +// Do the interface allocations only once for common +// Errno values. +const ( + errnoERROR_IO_PENDING = 997 +) + +var ( + errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING) + errERROR_EINVAL error = syscall.EINVAL +) + +// errnoErr returns common boxed Errno values, to prevent +// allocations at runtime. +func errnoErr(e syscall.Errno) error { + switch e { + case 0: + return errERROR_EINVAL + case errnoERROR_IO_PENDING: + return errERROR_IO_PENDING + } + // TODO: add more here, after collecting data on the common + // error values see on Windows. (perhaps when running + // all.bat?) + return e +} + +var ( + modoffreg = windows.NewLazySystemDLL("offreg.dll") + + procORCloseHive = modoffreg.NewProc("ORCloseHive") + procORCloseKey = modoffreg.NewProc("ORCloseKey") + procOREnumKey = modoffreg.NewProc("OREnumKey") + procOREnumValue = modoffreg.NewProc("OREnumValue") + procOROpenHive = modoffreg.NewProc("OROpenHive") + procOROpenKey = modoffreg.NewProc("OROpenKey") + procORQueryInfoKey = modoffreg.NewProc("ORQueryInfoKey") +) + +func ORCloseHive(key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procORCloseHive.Addr(), 1, uintptr(unsafe.Pointer(key)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func ORCloseKey(key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procORCloseKey.Addr(), 1, uintptr(unsafe.Pointer(key)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OREnumKey(key syscall.Handle, index uint32, name *uint16, nameSize *uint32, class *uint16, classSize *uint32, ftLastWriteTime uintptr) (regerrno error) { + r0, _, _ := syscall.Syscall9(procOREnumKey.Addr(), 7, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(nameSize)), uintptr(unsafe.Pointer(class)), uintptr(unsafe.Pointer(classSize)), uintptr(ftLastWriteTime), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OREnumValue(key syscall.Handle, index uint32, valueName *uint16, valueNameSize *uint32, valueType *uint32, data *byte, bufferSize *uint32) (regerrno error) { + r0, _, _ := syscall.Syscall9(procOREnumValue.Addr(), 7, uintptr(key), uintptr(index), uintptr(unsafe.Pointer(valueName)), uintptr(unsafe.Pointer(valueNameSize)), uintptr(unsafe.Pointer(valueType)), uintptr(unsafe.Pointer(data)), uintptr(unsafe.Pointer(bufferSize)), 0, 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OROpenHive(file *uint16, key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procOROpenHive.Addr(), 2, uintptr(unsafe.Pointer(file)), uintptr(unsafe.Pointer(key)), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func OROpenKey(rootKey syscall.Handle, lpSubKeyName *uint16, key *syscall.Handle) (regerrno error) { + r0, _, _ := syscall.Syscall(procOROpenKey.Addr(), 3, uintptr(rootKey), uintptr(unsafe.Pointer(lpSubKeyName)), uintptr(unsafe.Pointer(key))) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} + +func ORQueryInfoKey(key syscall.Handle, lpClass *uint16, lpcClass *uint32, lpcSubKeys *uint32, lpcMaxSubKeyLen *uint32, lpcMaxClassLen *uint32, lpcValues *uint32, lpcMaxValueNameLen *uint32, lpcMaxValueLen *uint32, lpcbSecurityDescriptor *uint32, lpftLastWriteTime uintptr) (regerrno error) { + r0, _, _ := syscall.Syscall12(procORQueryInfoKey.Addr(), 11, uintptr(key), uintptr(unsafe.Pointer(lpClass)), uintptr(unsafe.Pointer(lpcClass)), uintptr(unsafe.Pointer(lpcSubKeys)), uintptr(unsafe.Pointer(lpcMaxSubKeyLen)), uintptr(unsafe.Pointer(lpcMaxClassLen)), uintptr(unsafe.Pointer(lpcValues)), uintptr(unsafe.Pointer(lpcMaxValueNameLen)), uintptr(unsafe.Pointer(lpcMaxValueLen)), uintptr(unsafe.Pointer(lpcbSecurityDescriptor)), uintptr(lpftLastWriteTime), 0) + if r0 != 0 { + regerrno = syscall.Errno(r0) + } + return +} diff --git a/solver/llbsolver/file/backend.go b/solver/llbsolver/file/backend.go index 974c2e04e877..bc02d0d10f57 100644 --- a/solver/llbsolver/file/backend.go +++ b/solver/llbsolver/file/backend.go @@ -5,6 +5,7 @@ import ( "log" "os" "path/filepath" + "runtime" "strings" "time" @@ -25,48 +26,13 @@ func timestampToTime(ts int64) *time.Time { return &tm } -func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { - if user == nil { - return func(old *copy.User) (*copy.User, error) { - if old == nil { - if idmap == nil { - return nil, nil - } - old = ©.User{} // root - // non-nil old is already mapped - if idmap != nil { - identity, err := idmap.ToHost(idtools.Identity{ - UID: old.UID, - GID: old.GID, - }) - if err != nil { - return nil, err - } - return ©.User{UID: identity.UID, GID: identity.GID}, nil - } - } - return old, nil - }, nil - } - u := *user - if idmap != nil { - identity, err := idmap.ToHost(idtools.Identity{ - UID: user.UID, - GID: user.GID, - }) - if err != nil { - return nil, err - } - u.UID = identity.UID - u.GID = identity.GID +func mkdir(ctx context.Context, d string, action pb.FileActionMkDir, user *copy.User, idmap *idtools.IdentityMapping) error { + actionPath := action.Path + if runtime.GOOS == "windows" { + actionPath = strings.Split(actionPath, ":")[1] } - return func(*copy.User) (*copy.User, error) { - return &u, nil - }, nil -} -func mkdir(ctx context.Context, d string, action pb.FileActionMkDir, user *copy.User, idmap *idtools.IdentityMapping) error { - p, err := fs.RootPath(d, filepath.Join("/", action.Path)) + p, err := fs.RootPath(d, filepath.Join("/", actionPath)) if err != nil { return err } diff --git a/solver/llbsolver/file/backend_unix.go b/solver/llbsolver/file/backend_unix.go new file mode 100644 index 000000000000..d01290f300ac --- /dev/null +++ b/solver/llbsolver/file/backend_unix.go @@ -0,0 +1,49 @@ +//go:build !windows +// +build !windows + +package file + +import ( + "github.com/docker/docker/pkg/idtools" + copy "github.com/tonistiigi/fsutil/copy" +) + +func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { + if user == nil { + return func(old *copy.User) (*copy.User, error) { + if old == nil { + if idmap == nil { + return nil, nil + } + old = ©.User{} // root + // non-nil old is already mapped + if idmap != nil { + identity, err := idmap.ToHost(idtools.Identity{ + UID: old.UID, + GID: old.GID, + }) + if err != nil { + return nil, err + } + return ©.User{UID: identity.UID, GID: identity.GID}, nil + } + } + return old, nil + }, nil + } + u := *user + if idmap != nil { + identity, err := idmap.ToHost(idtools.Identity{ + UID: user.UID, + GID: user.GID, + }) + if err != nil { + return nil, err + } + u.UID = identity.UID + u.GID = identity.GID + } + return func(*copy.User) (*copy.User, error) { + return &u, nil + }, nil +} diff --git a/solver/llbsolver/file/backend_windows.go b/solver/llbsolver/file/backend_windows.go new file mode 100644 index 000000000000..e7b96aed338d --- /dev/null +++ b/solver/llbsolver/file/backend_windows.go @@ -0,0 +1,19 @@ +package file + +import ( + "github.com/docker/docker/pkg/idtools" + copy "github.com/tonistiigi/fsutil/copy" +) + +func mapUserToChowner(user *copy.User, idmap *idtools.IdentityMapping) (copy.Chowner, error) { + var copyUser copy.User + if user == nil || user.SID == "" { + copyUser.SID = idtools.ContainerAdministratorSidString + } else { + copyUser.SID = user.SID + } + + return func(*copy.User) (*copy.User, error) { + return ©User, nil + }, nil +} diff --git a/solver/llbsolver/file/user_nolinux.go b/solver/llbsolver/file/user_nolinux.go index 80652fd4abe4..111ec4c70f0a 100644 --- a/solver/llbsolver/file/user_nolinux.go +++ b/solver/llbsolver/file/user_nolinux.go @@ -1,5 +1,5 @@ -//go:build !linux -// +build !linux +//go:build !windows && !linux +// +build !windows,!linux package file diff --git a/solver/llbsolver/file/user_windows.go b/solver/llbsolver/file/user_windows.go new file mode 100644 index 000000000000..f6bb3383fe9e --- /dev/null +++ b/solver/llbsolver/file/user_windows.go @@ -0,0 +1,65 @@ +package file + +import ( + "fmt" + "path/filepath" + + "github.com/docker/docker/pkg/idtools" + "github.com/moby/buildkit/internal/winapi" + "github.com/moby/buildkit/snapshot" + "github.com/moby/buildkit/solver/llbsolver/ops/fileoptypes" + "github.com/moby/buildkit/solver/pb" + "github.com/pkg/errors" + copy "github.com/tonistiigi/fsutil/copy" +) + +func readUser(chopt *pb.ChownOpt, mu, mg fileoptypes.Mount) (*copy.User, error) { + if chopt == nil { + return nil, nil + } + var us copy.User + if chopt.User != nil { + switch u := chopt.User.User.(type) { + case *pb.UserOpt_ByName: + if u.ByName.Name == "ContainerAdministrator" { + us.SID = idtools.ContainerAdministratorSidString + return &us, nil + } + if u.ByName.Name == "ContainerUser" { + us.SID = idtools.ContainerUserSidString + return &us, nil + } + + if mu == nil { + return nil, errors.Errorf("invalid missing user mount") + } + mmu, ok := mu.(*Mount) + if !ok { + return nil, errors.Errorf("invalid mount type %T", mu) + } + lm := snapshot.LocalMounter(mmu.m) + dir, err := lm.Mount() + if err != nil { + return nil, err + } + defer lm.Unmount() + + passwdPath := filepath.Join(dir, "Windows/system32/config/SAM") + users, err := winapi.GetUserInfoFromOfflineSAMHive(passwdPath) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", passwdPath, err) + } + + for _, usr := range users { + if usr.Username == u.ByName.Name { + us.SID = usr.SIDString + break + } + } + case *pb.UserOpt_ByID: + return nil, fmt.Errorf("UserOpt_ByID not supported on Windows") + } + } + + return &us, nil +}