-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide a
systemreg
library that can extract the syskey (aka bootke…
…y) from an offline `SYSTEM` registry hive on Windows. The syskey is required to be able to decrypt/deobfuscate the user hashes from the `SAM` hive. PiperOrigin-RevId: 675443796
- Loading branch information
1 parent
8cedd56
commit 7b55c6b
Showing
2 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
107 changes: 107 additions & 0 deletions
107
detector/weakcredentials/winlocal/systemreg/systemreg.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package systemreg provides a wrapper around the SYSTEM registry. | ||
package systemreg | ||
|
||
import ( | ||
"encoding/hex" | ||
"fmt" | ||
|
||
"github.com/google/osv-scalibr/common/windows/registry" | ||
"golang.org/x/text/encoding/unicode" | ||
) | ||
|
||
var ( | ||
syskeyPaths = []string{"JD", "Skew1", "GBG", "Data"} | ||
|
||
errNoSelectKey = fmt.Errorf("system hive: failed to open `Select` key") | ||
errNoCurrentControlSet = fmt.Errorf("system hive: failed to find CurrentControlSet") | ||
) | ||
|
||
// SystemRegistry is a wrapper around a SYSTEM registry. | ||
type SystemRegistry struct { | ||
registry.Registry | ||
} | ||
|
||
// NewFromFile creates a new SystemRegistry from a file. | ||
func NewFromFile(path string) (*SystemRegistry, error) { | ||
reg, err := registry.NewFromFile(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &SystemRegistry{reg}, nil | ||
} | ||
|
||
// Syskey returns the syskey used to decrypt user hashes. | ||
// The syskey is stored as UTF16-le encoded hexadecimal in the class name of the 4 registry keys | ||
// denoted by `syskeyPaths`. Once the hexadecimal is decoded, the result is still obfuscated and | ||
// the order of the bytes needs to be swapped using the indexes denotated in the `transforms` table. | ||
func (s *SystemRegistry) Syskey() ([]byte, error) { | ||
currentSet, err := s.currentControlSet() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
var syskey string | ||
currentControlSet := fmt.Sprintf(`ControlSet%03d\Control\Lsa\`, currentSet) | ||
for _, k := range syskeyPaths { | ||
key := s.OpenKey(currentControlSet + k) | ||
if key == nil { | ||
return nil, fmt.Errorf("failed to open key: %v", currentControlSet+k) | ||
} | ||
|
||
class, err := key.ClassName() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
syskey += string(class) | ||
} | ||
|
||
decodedKey, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().String(syskey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
unhexKey, err := hex.DecodeString(decodedKey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
transforms := []int{8, 5, 4, 2, 11, 9, 13, 3, 0, 6, 1, 12, 14, 10, 15, 7} | ||
var resultKey []byte | ||
|
||
for i := range len(unhexKey) { | ||
resultKey = append(resultKey, unhexKey[transforms[i]]) | ||
} | ||
|
||
return resultKey, nil | ||
} | ||
|
||
func (s *SystemRegistry) currentControlSet() (uint32, error) { | ||
key := s.OpenKey(`Select`) | ||
if key == nil { | ||
return 0, errNoSelectKey | ||
} | ||
|
||
for _, value := range key.Values() { | ||
if value.Name() == "Current" { | ||
return uint32(value.Data()[0]), nil | ||
} | ||
} | ||
|
||
return 0, errNoCurrentControlSet | ||
} |
209 changes: 209 additions & 0 deletions
209
detector/weakcredentials/winlocal/systemreg/systemreg_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,209 @@ | ||
// Copyright 2024 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package systemreg | ||
|
||
import ( | ||
"slices" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/osv-scalibr/common/windows/registry" | ||
"github.com/google/osv-scalibr/testing/mockregistry" | ||
) | ||
|
||
func TestNewFromFile(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
path string | ||
wantErr bool | ||
}{ | ||
{ | ||
name: "File is missing registry magic", | ||
path: "/dev/null", | ||
wantErr: true, | ||
}, | ||
{ | ||
name: "Fails when file does not exist", | ||
path: "/some/non/existing/file", | ||
wantErr: true, | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
_, err := NewFromFile(tc.path) | ||
if (err != nil) != tc.wantErr { | ||
t.Fatalf("NewFromFile(%q) error: got: %v, want: %v", tc.path, err, tc.wantErr) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestSyskey(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
registry *mockregistry.MockRegistry | ||
want []byte | ||
wantErr bool | ||
wantErrText string | ||
}{ | ||
{ | ||
name: "Parses syskey correctly", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{ | ||
`Select`: &mockregistry.MockKey{ | ||
KValues: []registry.Value{ | ||
&mockregistry.MockValue{ | ||
VName: "Current", | ||
VData: []byte{0x01}, | ||
}, | ||
}, | ||
}, | ||
`ControlSet001\Control\Lsa\JD`: &mockregistry.MockKey{ | ||
KClassName: "\x32\x00\x35\x00\x33\x00\x35\x00\x39\x00\x33\x00\x64\x00\x64\x00", | ||
}, | ||
`ControlSet001\Control\Lsa\Skew1`: &mockregistry.MockKey{ | ||
KClassName: "\x61\x00\x65\x00\x39\x00\x33\x00\x34\x00\x37\x00\x30\x00\x30\x00", | ||
}, | ||
`ControlSet001\Control\Lsa\GBG`: &mockregistry.MockKey{ | ||
KClassName: "\x38\x00\x38\x00\x31\x00\x33\x00\x39\x00\x64\x00\x34\x00\x35\x00", | ||
}, | ||
`ControlSet001\Control\Lsa\Data`: &mockregistry.MockKey{ | ||
KClassName: "\x31\x00\x36\x00\x62\x00\x64\x00\x33\x00\x65\x00\x33\x00\x33\x00", | ||
}, | ||
}, | ||
}, | ||
want: []byte("\x88\x93\xae\x93\x45\x13\xbd\xdd\x25\x47\x35\x16\x3e\x9d\x33\x00"), | ||
}, | ||
{ | ||
name: "Parses syskey correctly with different control set", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{ | ||
`Select`: &mockregistry.MockKey{ | ||
KValues: []registry.Value{ | ||
&mockregistry.MockValue{ | ||
VName: "Current", | ||
VData: []byte{0x02}, | ||
}, | ||
}, | ||
}, | ||
`ControlSet002\Control\Lsa\JD`: &mockregistry.MockKey{ | ||
KClassName: "\x32\x00\x35\x00\x33\x00\x35\x00\x39\x00\x33\x00\x64\x00\x64\x00", | ||
}, | ||
`ControlSet002\Control\Lsa\Skew1`: &mockregistry.MockKey{ | ||
KClassName: "\x61\x00\x65\x00\x39\x00\x33\x00\x34\x00\x37\x00\x30\x00\x30\x00", | ||
}, | ||
`ControlSet002\Control\Lsa\GBG`: &mockregistry.MockKey{ | ||
KClassName: "\x38\x00\x38\x00\x31\x00\x33\x00\x39\x00\x64\x00\x34\x00\x35\x00", | ||
}, | ||
`ControlSet002\Control\Lsa\Data`: &mockregistry.MockKey{ | ||
KClassName: "\x31\x00\x36\x00\x62\x00\x64\x00\x33\x00\x65\x00\x33\x00\x33\x00", | ||
}, | ||
}, | ||
}, | ||
want: []byte("\x88\x93\xae\x93\x45\x13\xbd\xdd\x25\x47\x35\x16\x3e\x9d\x33\x00"), | ||
}, | ||
{ | ||
name: "Parts of the syskey are missing", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{ | ||
`Select`: &mockregistry.MockKey{ | ||
KValues: []registry.Value{ | ||
&mockregistry.MockValue{ | ||
VName: "Current", | ||
VData: []byte{0x01}, | ||
}, | ||
}, | ||
}, | ||
`ControlSet001\Control\Lsa\JD`: &mockregistry.MockKey{ | ||
KClassName: "\x32\x00\x35\x00\x33\x00\x35\x00\x39\x00\x33\x00\x64\x00\x64\x00", | ||
}, | ||
}, | ||
}, | ||
wantErr: true, | ||
wantErrText: `failed to open key: ControlSet001\Control\Lsa\Skew1`, | ||
}, | ||
{ | ||
name: "The key does not decode as hexadecimal", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{ | ||
`Select`: &mockregistry.MockKey{ | ||
KValues: []registry.Value{ | ||
&mockregistry.MockValue{ | ||
VName: "Current", | ||
VData: []byte{0x01}, | ||
}, | ||
}, | ||
}, | ||
`ControlSet001\Control\Lsa\JD`: &mockregistry.MockKey{ | ||
KClassName: "\x32\xFF\x35\xFF\x33\xFF\x35\xFF\x39\xFF\x33\xFF\x64\xFF\x64\xFF", | ||
}, | ||
`ControlSet001\Control\Lsa\Skew1`: &mockregistry.MockKey{ | ||
KClassName: "\x61\x00\x65\x00\x39\x00\x33\x00\x34\x00\x37\x00\x30\x00\x30\x00", | ||
}, | ||
`ControlSet001\Control\Lsa\GBG`: &mockregistry.MockKey{ | ||
KClassName: "\x38\x00\x38\x00\x31\x00\x33\x00\x39\x00\x64\x00\x34\x00\x35\x00", | ||
}, | ||
`ControlSet001\Control\Lsa\Data`: &mockregistry.MockKey{ | ||
KClassName: "\x31\x00\x36\x00\x62\x00\x64\x00\x33\x00\x65\x00\x33\x00\x33\x00", | ||
}, | ||
}, | ||
}, | ||
wantErr: true, | ||
wantErrText: `encoding/hex: invalid byte: U+00EF 'ï'`, | ||
}, | ||
{ | ||
name: "Select registry key not found", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{}, | ||
}, | ||
wantErr: true, | ||
wantErrText: errNoSelectKey.Error(), | ||
}, | ||
{ | ||
name: "Current control set not found", | ||
registry: &mockregistry.MockRegistry{ | ||
Keys: map[string]registry.Key{ | ||
`Select`: &mockregistry.MockKey{}, | ||
}, | ||
}, | ||
wantErr: true, | ||
wantErrText: errNoCurrentControlSet.Error(), | ||
}, | ||
} | ||
|
||
for _, tc := range tests { | ||
t.Run(tc.name, func(t *testing.T) { | ||
sysreg := &SystemRegistry{tc.registry} | ||
got, err := sysreg.Syskey() | ||
|
||
if (err != nil) != tc.wantErr { | ||
t.Errorf("Syskey() unexpected error: %v", err) | ||
} | ||
|
||
if tc.wantErr { | ||
if !strings.Contains(err.Error(), tc.wantErrText) { | ||
t.Errorf("Syskey() unexpected error: got: %v, want: %v", err.Error(), tc.wantErrText) | ||
} | ||
|
||
return | ||
} | ||
|
||
if !slices.Equal(got, tc.want) { | ||
t.Errorf("Syskey() unexpected result: got: %v, want: %v", got, tc.want) | ||
} | ||
}) | ||
} | ||
} |