Skip to content

Commit

Permalink
Provide a systemreg library that can extract the syskey (aka bootke…
Browse files Browse the repository at this point in the history
…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
tooryx authored and copybara-github committed Oct 3, 2024
1 parent 8cedd56 commit 7b55c6b
Show file tree
Hide file tree
Showing 2 changed files with 316 additions and 0 deletions.
107 changes: 107 additions & 0 deletions detector/weakcredentials/winlocal/systemreg/systemreg.go
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 detector/weakcredentials/winlocal/systemreg/systemreg_test.go
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)
}
})
}
}

0 comments on commit 7b55c6b

Please sign in to comment.