Files
Jacob Paullus d6089390bd registry: harden hive parser against malformed inputs
Four parser-panic / silent-corruption bugs in pkg/registry/hive.go,
all reachable from attacker-controlled hive bytes:

1. GetValueData resident branch: VKRecord.DataLen lower 31 bits are
   read verbatim and used to slice a 4-byte buffer at [:dataLen]. A
   DataLen of 0x80000005 panics with "slice bounds out of range".
   Found by kajaaz using Zorya (issue #25); fix matches her suggested
   one-line bounds check.

2. SetValueData resident branch (structurally identical to #1): the
   existing len(newData) == dataLen check doesn't enforce the 4-byte
   cap, so a hostile dataLen=5 with a matching 5-byte newData slices
   one byte past the DataOffset field into the adjacent cell. Same
   guard.

3. SetValueData non-resident branch: vk.DataOffset is attacker-
   controlled and the code does copy(h.data[dataPos:dataPos+dataLen],
   newData) without ever validating the destination. Hostile offsets
   either panic on out-of-bounds or silently scribble over arbitrary
   hive bytes (a value of 0xFFFFF000 lands near the regf header). Route
   through readCell (which validates the cell header and bounds) and
   verify dataLen fits before mutating.

4. enumSubKeys riSig branch: readCell can return a slice shorter than
   the 4-byte cell header (minimum-size cell with no usable bytes), so
   the immediate subCell[0:2] / subCell[2:4] reads can panic on a
   malformed sub-list. The sibling code at line 397/437 already guards
   the analogous index; mirror it here.

Fixes #25 plus three structurally similar bugs surfaced while patching.
2026-05-11 23:48:10 -05:00

682 lines
17 KiB
Go

// Copyright 2026 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
//
// https://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 registry
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"unicode/utf16"
)
const (
regfMagic = 0x66676572 // "regf"
hbinMagic = 0x6e696268 // "hbin"
nkSig = 0x6b6e // "nk"
vkSig = 0x6b76 // "vk"
lfSig = 0x666c // "lf"
lhSig = 0x686c // "lh"
liSig = 0x696c // "li"
riSig = 0x6972 // "ri"
skSig = 0x6b73 // "sk"
)
// NK flags
const (
KEY_HIVE_ENTRY = 0x0004
KEY_NO_DELETE = 0x0008
KEY_SYM_LINK = 0x0010
KEY_COMP_NAME = 0x0020
)
// Hive represents a parsed registry hive
type Hive struct {
data []byte
rootOffset int32
}
// Open parses a registry hive from bytes
func Open(data []byte) (*Hive, error) {
if len(data) < 4096 {
return nil, fmt.Errorf("hive too small: %d bytes", len(data))
}
// Verify magic
magic := binary.LittleEndian.Uint32(data[0:4])
if magic != regfMagic {
return nil, fmt.Errorf("invalid hive magic: 0x%08x", magic)
}
// Root cell offset is at offset 36
rootOffset := int32(binary.LittleEndian.Uint32(data[36:40]))
return &Hive{
data: data,
rootOffset: rootOffset,
}, nil
}
// cellOffset converts a cell offset to actual data offset
// Cell offsets are relative to the first hbin (offset 4096)
func (h *Hive) cellOffset(offset int32) int {
return int(offset) + 4096
}
// readCell reads a cell at the given offset and returns its data (the bytes
// after the 4-byte cell header). A valid allocated cell has a negative size
// field; a positive size marks a free cell, and zero is malformed.
func (h *Hive) readCell(offset int32) ([]byte, error) {
pos := h.cellOffset(offset)
if pos < 4096 || pos >= len(h.data)-4 {
return nil, fmt.Errorf("invalid cell offset: %d", offset)
}
rawSize := int32(binary.LittleEndian.Uint32(h.data[pos : pos+4]))
if rawSize >= 0 {
// Positive = free cell, zero = malformed. Either way, no data to read.
return nil, fmt.Errorf("cell at offset %d is free or zero-sized (raw size %d)", offset, rawSize)
}
size := -rawSize
if size < 4 {
// Minimum cell is the 4-byte header; anything smaller would produce
// an inverted slice (panic in the original code).
return nil, fmt.Errorf("cell at offset %d has invalid size %d (min 4)", offset, size)
}
if pos+int(size) > len(h.data) {
return nil, fmt.Errorf("cell extends beyond hive: %d + %d > %d", pos, size, len(h.data))
}
return h.data[pos+4 : pos+int(size)], nil
}
// NKRecord represents a key node
type NKRecord struct {
Signature uint16
Flags uint16
LastModified uint64
Access uint32
ParentOffset int32
SubKeyCount uint32
SubKeyCountVol uint32
SubKeyListOffset int32
SubKeyListOffsetVol int32
ValueCount uint32
ValueListOffset int32
SecurityOffset int32
ClassOffset int32
MaxSubKeyNameLen uint32
MaxSubKeyClassLen uint32
MaxValueNameLen uint32
MaxValueDataLen uint32
WorkVar uint32
NameLen uint16
ClassLen uint16
Name string
}
// parseNK parses an NK (key node) record
func (h *Hive) parseNK(offset int32) (*NKRecord, error) {
cell, err := h.readCell(offset)
if err != nil {
return nil, err
}
if len(cell) < 76 {
return nil, fmt.Errorf("NK cell too small: %d", len(cell))
}
nk := &NKRecord{}
r := bytes.NewReader(cell)
binary.Read(r, binary.LittleEndian, &nk.Signature)
if nk.Signature != nkSig {
return nil, fmt.Errorf("invalid NK signature: 0x%04x", nk.Signature)
}
binary.Read(r, binary.LittleEndian, &nk.Flags)
binary.Read(r, binary.LittleEndian, &nk.LastModified)
binary.Read(r, binary.LittleEndian, &nk.Access)
binary.Read(r, binary.LittleEndian, &nk.ParentOffset)
binary.Read(r, binary.LittleEndian, &nk.SubKeyCount)
binary.Read(r, binary.LittleEndian, &nk.SubKeyCountVol)
binary.Read(r, binary.LittleEndian, &nk.SubKeyListOffset)
binary.Read(r, binary.LittleEndian, &nk.SubKeyListOffsetVol)
binary.Read(r, binary.LittleEndian, &nk.ValueCount)
binary.Read(r, binary.LittleEndian, &nk.ValueListOffset)
binary.Read(r, binary.LittleEndian, &nk.SecurityOffset)
binary.Read(r, binary.LittleEndian, &nk.ClassOffset)
binary.Read(r, binary.LittleEndian, &nk.MaxSubKeyNameLen)
binary.Read(r, binary.LittleEndian, &nk.MaxSubKeyClassLen)
binary.Read(r, binary.LittleEndian, &nk.MaxValueNameLen)
binary.Read(r, binary.LittleEndian, &nk.MaxValueDataLen)
binary.Read(r, binary.LittleEndian, &nk.WorkVar)
binary.Read(r, binary.LittleEndian, &nk.NameLen)
binary.Read(r, binary.LittleEndian, &nk.ClassLen)
// Read name
if nk.NameLen > 0 {
nameBytes := make([]byte, nk.NameLen)
r.Read(nameBytes)
if nk.Flags&KEY_COMP_NAME != 0 {
// ASCII name
nk.Name = string(nameBytes)
} else {
// UTF-16LE name
chars := make([]uint16, nk.NameLen/2)
for i := 0; i < len(chars); i++ {
chars[i] = binary.LittleEndian.Uint16(nameBytes[i*2:])
}
nk.Name = string(utf16.Decode(chars))
}
}
return nk, nil
}
// VKRecord represents a value node
type VKRecord struct {
Signature uint16
NameLen uint16
DataLen uint32
DataOffset uint32
DataType uint32
Flags uint16
Spare uint16
Name string
}
// parseVK parses a VK (value node) record
func (h *Hive) parseVK(offset int32) (*VKRecord, error) {
cell, err := h.readCell(offset)
if err != nil {
return nil, err
}
if len(cell) < 20 {
return nil, fmt.Errorf("VK cell too small: %d", len(cell))
}
vk := &VKRecord{}
r := bytes.NewReader(cell)
binary.Read(r, binary.LittleEndian, &vk.Signature)
if vk.Signature != vkSig {
return nil, fmt.Errorf("invalid VK signature: 0x%04x", vk.Signature)
}
binary.Read(r, binary.LittleEndian, &vk.NameLen)
binary.Read(r, binary.LittleEndian, &vk.DataLen)
binary.Read(r, binary.LittleEndian, &vk.DataOffset)
binary.Read(r, binary.LittleEndian, &vk.DataType)
binary.Read(r, binary.LittleEndian, &vk.Flags)
binary.Read(r, binary.LittleEndian, &vk.Spare)
// Read name
if vk.NameLen > 0 {
nameBytes := make([]byte, vk.NameLen)
r.Read(nameBytes)
if vk.Flags&0x0001 != 0 {
// ASCII name
vk.Name = string(nameBytes)
} else {
// UTF-16LE name
chars := make([]uint16, vk.NameLen/2)
for i := 0; i < len(chars); i++ {
chars[i] = binary.LittleEndian.Uint16(nameBytes[i*2:])
}
vk.Name = string(utf16.Decode(chars))
}
}
return vk, nil
}
// GetValueData retrieves the data for a value record
func (h *Hive) GetValueData(vk *VKRecord) ([]byte, error) {
dataLen := vk.DataLen & 0x7FFFFFFF
isResident := vk.DataLen&0x80000000 != 0
if isResident {
// Data is stored in the DataOffset field itself (up to 4 bytes).
// dataLen is attacker-controlled (lower 31 bits of vk.DataLen, read
// verbatim from hive bytes); reject lengths beyond the 4-byte cap
// before slicing to prevent a panic on malformed hives.
if dataLen > 4 {
return nil, fmt.Errorf("resident data length %d exceeds maximum of 4 bytes", dataLen)
}
data := make([]byte, 4)
binary.LittleEndian.PutUint32(data, vk.DataOffset)
return data[:dataLen], nil
}
// Data is in a separate cell
cell, err := h.readCell(int32(vk.DataOffset))
if err != nil {
return nil, err
}
if int(dataLen) > len(cell) {
dataLen = uint32(len(cell))
}
return cell[:dataLen], nil
}
// FindKey locates a subkey by path (e.g., "SAM\\Domains\\Account")
func (h *Hive) FindKey(path string) (int32, error) {
parts := strings.Split(path, "\\")
currentOffset := h.rootOffset
for _, part := range parts {
if part == "" {
continue
}
nk, err := h.parseNK(currentOffset)
if err != nil {
return 0, fmt.Errorf("failed to parse key: %v", err)
}
found := false
subKeys, err := h.enumSubKeys(nk)
if err != nil {
return 0, fmt.Errorf("failed to enum subkeys: %v", err)
}
for _, sk := range subKeys {
if strings.EqualFold(sk.name, part) {
currentOffset = sk.offset
found = true
break
}
}
if !found {
return 0, fmt.Errorf("key not found: %s", part)
}
}
return currentOffset, nil
}
type subKeyInfo struct {
name string
offset int32
}
// enumSubKeys enumerates subkeys of a key
func (h *Hive) enumSubKeys(nk *NKRecord) ([]subKeyInfo, error) {
if nk.SubKeyCount == 0 || nk.SubKeyListOffset == -1 {
return nil, nil
}
cell, err := h.readCell(nk.SubKeyListOffset)
if err != nil {
return nil, err
}
if len(cell) < 4 {
return nil, fmt.Errorf("subkey list too small")
}
sig := binary.LittleEndian.Uint16(cell[0:2])
count := binary.LittleEndian.Uint16(cell[2:4])
var subKeys []subKeyInfo
switch sig {
case lfSig, lhSig:
// LF/LH list: entries are [offset, hash] pairs
for i := uint16(0); i < count; i++ {
entryOff := 4 + int(i)*8
if entryOff+4 > len(cell) {
break
}
offset := int32(binary.LittleEndian.Uint32(cell[entryOff : entryOff+4]))
subNK, err := h.parseNK(offset)
if err != nil {
continue
}
subKeys = append(subKeys, subKeyInfo{name: subNK.Name, offset: offset})
}
case liSig:
// LI list: entries are just offsets
for i := uint16(0); i < count; i++ {
entryOff := 4 + int(i)*4
if entryOff+4 > len(cell) {
break
}
offset := int32(binary.LittleEndian.Uint32(cell[entryOff : entryOff+4]))
subNK, err := h.parseNK(offset)
if err != nil {
continue
}
subKeys = append(subKeys, subKeyInfo{name: subNK.Name, offset: offset})
}
case riSig:
// RI list: entries are offsets to sub-lists
for i := uint16(0); i < count; i++ {
entryOff := 4 + int(i)*4
if entryOff+4 > len(cell) {
break
}
subListOffset := int32(binary.LittleEndian.Uint32(cell[entryOff : entryOff+4]))
// Recursively process sub-list. readCell can return a slice
// shorter than the 4-byte cell header (e.g. minimum-size cell with
// no usable bytes), so guard against an indexing panic before
// reading the sub-list signature and count.
subCell, err := h.readCell(subListOffset)
if err != nil {
continue
}
if len(subCell) < 4 {
continue
}
subSig := binary.LittleEndian.Uint16(subCell[0:2])
subCount := binary.LittleEndian.Uint16(subCell[2:4])
if subSig == lfSig || subSig == lhSig {
for j := uint16(0); j < subCount; j++ {
subEntryOff := 4 + int(j)*8
if subEntryOff+4 > len(subCell) {
break
}
offset := int32(binary.LittleEndian.Uint32(subCell[subEntryOff : subEntryOff+4]))
subNK, err := h.parseNK(offset)
if err != nil {
continue
}
subKeys = append(subKeys, subKeyInfo{name: subNK.Name, offset: offset})
}
}
}
default:
return nil, fmt.Errorf("unknown subkey list signature: 0x%04x", sig)
}
return subKeys, nil
}
// GetValue retrieves a value from a key
func (h *Hive) GetValue(keyOffset int32, valueName string) (uint32, []byte, error) {
nk, err := h.parseNK(keyOffset)
if err != nil {
return 0, nil, err
}
if nk.ValueCount == 0 || nk.ValueListOffset == -1 {
return 0, nil, fmt.Errorf("key has no values")
}
// Read value list
cell, err := h.readCell(nk.ValueListOffset)
if err != nil {
return 0, nil, err
}
// Value list is an array of offsets
for i := uint32(0); i < nk.ValueCount; i++ {
if int(i*4+4) > len(cell) {
break
}
vkOffset := int32(binary.LittleEndian.Uint32(cell[i*4 : i*4+4]))
vk, err := h.parseVK(vkOffset)
if err != nil {
continue
}
// Check for default value (empty name)
if valueName == "" && vk.NameLen == 0 {
data, err := h.GetValueData(vk)
return vk.DataType, data, err
}
if strings.EqualFold(vk.Name, valueName) {
data, err := h.GetValueData(vk)
return vk.DataType, data, err
}
}
return 0, nil, fmt.Errorf("value not found: %s", valueName)
}
// GetClassName retrieves the class name of a key
func (h *Hive) GetClassName(keyOffset int32) (string, error) {
nk, err := h.parseNK(keyOffset)
if err != nil {
return "", err
}
if nk.ClassOffset == -1 || nk.ClassLen == 0 {
return "", nil
}
cell, err := h.readCell(nk.ClassOffset)
if err != nil {
return "", err
}
if int(nk.ClassLen) > len(cell) {
return "", fmt.Errorf("class name extends beyond cell")
}
// Class name is UTF-16LE
chars := make([]uint16, nk.ClassLen/2)
for i := 0; i < len(chars); i++ {
chars[i] = binary.LittleEndian.Uint16(cell[i*2:])
}
return string(utf16.Decode(chars)), nil
}
// FindSubKey locates a direct child subkey by name and returns its offset
func (h *Hive) FindSubKey(parentOffset int32, name string) (int32, error) {
nk, err := h.parseNK(parentOffset)
if err != nil {
return 0, err
}
subKeys, err := h.enumSubKeys(nk)
if err != nil {
return 0, err
}
for _, sk := range subKeys {
if strings.EqualFold(sk.name, name) {
return sk.offset, nil
}
}
return 0, fmt.Errorf("subkey not found: %s", name)
}
// GetClassNameRaw retrieves the raw class name bytes of a key without UTF-16 decoding
func (h *Hive) GetClassNameRaw(keyOffset int32) ([]byte, error) {
nk, err := h.parseNK(keyOffset)
if err != nil {
return nil, err
}
if nk.ClassOffset == -1 || nk.ClassLen == 0 {
return nil, nil
}
cell, err := h.readCell(nk.ClassOffset)
if err != nil {
return nil, err
}
return cell, nil
}
// EnumSubKeys lists subkey names of a key
func (h *Hive) EnumSubKeys(keyOffset int32) ([]string, error) {
nk, err := h.parseNK(keyOffset)
if err != nil {
return nil, err
}
subKeys, err := h.enumSubKeys(nk)
if err != nil {
return nil, err
}
names := make([]string, len(subKeys))
for i, sk := range subKeys {
names[i] = sk.name
}
return names, nil
}
// EnumValues lists value names of a key
func (h *Hive) EnumValues(keyOffset int32) ([]string, error) {
nk, err := h.parseNK(keyOffset)
if err != nil {
return nil, err
}
if nk.ValueCount == 0 || nk.ValueListOffset == -1 {
return nil, nil
}
cell, err := h.readCell(nk.ValueListOffset)
if err != nil {
return nil, err
}
var names []string
for i := uint32(0); i < nk.ValueCount; i++ {
if int(i*4+4) > len(cell) {
break
}
vkOffset := int32(binary.LittleEndian.Uint32(cell[i*4 : i*4+4]))
vk, err := h.parseVK(vkOffset)
if err != nil {
continue
}
names = append(names, vk.Name)
}
return names, nil
}
// RootOffset returns the root key offset
func (h *Hive) RootOffset() int32 {
return h.rootOffset
}
// Data returns the raw hive bytes for saving
func (h *Hive) Data() []byte {
return h.data
}
// SetValueData overwrites the data of a named value under the given key.
// The new data must be exactly the same length as the existing data.
func (h *Hive) SetValueData(keyOffset int32, valueName string, newData []byte) error {
nk, err := h.parseNK(keyOffset)
if err != nil {
return err
}
if nk.ValueCount == 0 || nk.ValueListOffset == -1 {
return fmt.Errorf("key has no values")
}
// Read value list
cell, err := h.readCell(nk.ValueListOffset)
if err != nil {
return err
}
for i := uint32(0); i < nk.ValueCount; i++ {
if int(i*4+4) > len(cell) {
break
}
vkOffset := int32(binary.LittleEndian.Uint32(cell[i*4 : i*4+4]))
vk, err := h.parseVK(vkOffset)
if err != nil {
continue
}
match := false
if valueName == "" && vk.NameLen == 0 {
match = true
} else if strings.EqualFold(vk.Name, valueName) {
match = true
}
if !match {
continue
}
dataLen := vk.DataLen & 0x7FFFFFFF
isResident := vk.DataLen&0x80000000 != 0
if uint32(len(newData)) != dataLen {
return fmt.Errorf("data length mismatch: existing %d, new %d", dataLen, len(newData))
}
if isResident {
// Data stored inline in the VK record's DataOffset field
// VK cell starts at cellOffset(vkOffset)+4 (skip cell size)
// DataOffset is at bytes 8..12 of the VK record (after sig:2 + nameLen:2 + dataLen:4).
// Same 4-byte cap as the read path in GetValueData; without this
// guard a length-matching newData buffer of 5+ bytes would silently
// scribble into the adjacent hive cell.
if dataLen > 4 {
return fmt.Errorf("resident data length %d exceeds maximum of 4 bytes", dataLen)
}
vkPos := h.cellOffset(vkOffset) + 4 // skip cell size dword
dataFieldPos := vkPos + 8 // offset of DataOffset field
copy(h.data[dataFieldPos:dataFieldPos+int(dataLen)], newData)
return nil
}
// Data is in a separate cell; overwrite in place. vk.DataOffset is
// attacker-controlled in malformed hives, so route through readCell
// (which validates the cell header and bounds) and verify dataLen
// fits before mutating. Without this guard a hostile offset can
// scribble over arbitrary hive bytes including the regf header.
cell, err := h.readCell(int32(vk.DataOffset))
if err != nil {
return err
}
if int(dataLen) > len(cell) {
return fmt.Errorf("value data length %d exceeds cell size %d", dataLen, len(cell))
}
copy(cell[:dataLen], newData)
return nil
}
return fmt.Errorf("value not found: %s", valueName)
}