mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
464 lines
12 KiB
Go
464 lines
12 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 dpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
)
|
|
|
|
// BKRP GUIDs for backup key retrieval
|
|
var (
|
|
// BACKUPKEY_BACKUP_GUID - 7F752B10-178E-11D1-AB8F-00805F14DB40 - backup a secret (ServerWrap)
|
|
BACKUPKEY_BACKUP_GUID = [16]byte{
|
|
0x10, 0x2B, 0x75, 0x7F, 0x8E, 0x17, 0xD1, 0x11,
|
|
0xAB, 0x8F, 0x00, 0x80, 0x5F, 0x14, 0xDB, 0x40,
|
|
}
|
|
|
|
// BACKUPKEY_RETRIEVE_BACKUP_KEY_GUID - 018FF48A-EABA-40C6-8F6D-72370240E967 - retrieve backup key
|
|
BACKUPKEY_RETRIEVE_BACKUP_KEY_GUID = [16]byte{
|
|
0x8A, 0xF4, 0x8F, 0x01, 0xBA, 0xEA, 0xC6, 0x40,
|
|
0x8F, 0x6D, 0x72, 0x37, 0x02, 0x40, 0xE9, 0x67,
|
|
}
|
|
|
|
// BACKUPKEY_RESTORE_GUID_WIN2K - 7FE94D50-178E-11D1-AB8F-00805F14DB40 - legacy restore (ServerWrap)
|
|
BACKUPKEY_RESTORE_GUID_WIN2K = [16]byte{
|
|
0x50, 0x4D, 0xE9, 0x7F, 0x8E, 0x17, 0xD1, 0x11,
|
|
0xAB, 0x8F, 0x00, 0x80, 0x5F, 0x14, 0xDB, 0x40,
|
|
}
|
|
)
|
|
|
|
// BackupKey represents a domain backup key
|
|
type BackupKey struct {
|
|
Version uint32
|
|
Magic uint32
|
|
KeyLength uint32
|
|
Certificate []byte
|
|
PrivateKey *rsa.PrivateKey
|
|
PVKData []byte
|
|
}
|
|
|
|
// PVKFileHeader represents the PVK file header
|
|
type PVKFileHeader struct {
|
|
Magic uint32 // 0xb0b5f11e
|
|
Reserved uint32
|
|
KeyType uint32
|
|
Encrypted uint32
|
|
SaltLength uint32
|
|
KeyLength uint32
|
|
}
|
|
|
|
// PreferredBackupKey represents the GUID pointing to the preferred backup key
|
|
type PreferredBackupKey struct {
|
|
GUID [16]byte
|
|
}
|
|
|
|
// PrivateKeyBlob represents a PRIVATEKEYBLOB structure
|
|
type PrivateKeyBlob struct {
|
|
PublicKeyStruc struct {
|
|
Type byte
|
|
Version byte
|
|
Reserved uint16
|
|
AlgID uint32
|
|
}
|
|
RSAPubKey struct {
|
|
Magic uint32 // "RSA2" = 0x32415352
|
|
BitLen uint32
|
|
PubExp uint32
|
|
}
|
|
Modulus []byte
|
|
Prime1 []byte // p
|
|
Prime2 []byte // q
|
|
Exponent1 []byte // d mod (p-1)
|
|
Exponent2 []byte // d mod (q-1)
|
|
Coefficient []byte // q^-1 mod p
|
|
PrivateExponent []byte // d
|
|
}
|
|
|
|
// ParseBackupKeyResponse parses the response from BKRP
|
|
func ParseBackupKeyResponse(data []byte) (*BackupKey, error) {
|
|
if len(data) < 8 {
|
|
return nil, fmt.Errorf("data too short for backup key")
|
|
}
|
|
|
|
bk := &BackupKey{}
|
|
|
|
// Check first byte to determine format
|
|
if data[0] == 0x30 {
|
|
// DER-encoded X.509 certificate (starts with SEQUENCE)
|
|
// This is the response from BACKUPKEY_RETRIEVE_BACKUP_KEY_GUID
|
|
bk.Certificate = data
|
|
bk.Version = 1
|
|
|
|
// Try to extract the public key from the certificate
|
|
cert, err := x509.ParseCertificate(data)
|
|
if err == nil {
|
|
if rsaKey, ok := cert.PublicKey.(*rsa.PublicKey); ok {
|
|
// Store as a partial private key (just the public part)
|
|
bk.PrivateKey = &rsa.PrivateKey{
|
|
PublicKey: *rsaKey,
|
|
}
|
|
}
|
|
}
|
|
return bk, nil
|
|
}
|
|
|
|
r := bytes.NewReader(data)
|
|
binary.Read(r, binary.LittleEndian, &bk.Version)
|
|
|
|
// Check for PVK magic first
|
|
if binary.LittleEndian.Uint32(data[0:4]) == 0xb0b5f11e {
|
|
return parsePVKBackupKey(data)
|
|
}
|
|
|
|
// Version 2 BACKUP_KEY structure from LSA secret
|
|
if bk.Version == 2 {
|
|
return ParsePrivateKeyData(data)
|
|
}
|
|
|
|
// Version 1 - certificate with length prefix
|
|
r.Seek(0, 0)
|
|
var certLen uint32
|
|
binary.Read(r, binary.LittleEndian, &certLen)
|
|
|
|
if certLen > uint32(len(data)-4) {
|
|
// Try parsing as raw PRIVATEKEYBLOB
|
|
return parsePrivateKeyBlob(data)
|
|
}
|
|
|
|
bk.Certificate = make([]byte, certLen)
|
|
r.Read(bk.Certificate)
|
|
|
|
return bk, nil
|
|
}
|
|
|
|
// parsePVKBackupKey parses a PVK-format backup key
|
|
func parsePVKBackupKey(data []byte) (*BackupKey, error) {
|
|
bk := &BackupKey{}
|
|
bk.PVKData = data
|
|
|
|
// Check PVK magic
|
|
if len(data) < 24 {
|
|
return nil, fmt.Errorf("data too short for PVK header")
|
|
}
|
|
|
|
// PVK header
|
|
magic := binary.LittleEndian.Uint32(data[0:4])
|
|
if magic != 0xb0b5f11e {
|
|
// Not a PVK file, might be raw PRIVATEKEYBLOB
|
|
return parsePrivateKeyBlob(data)
|
|
}
|
|
|
|
// Parse PVK header
|
|
hdr := &PVKFileHeader{
|
|
Magic: magic,
|
|
Reserved: binary.LittleEndian.Uint32(data[4:8]),
|
|
KeyType: binary.LittleEndian.Uint32(data[8:12]),
|
|
Encrypted: binary.LittleEndian.Uint32(data[12:16]),
|
|
SaltLength: binary.LittleEndian.Uint32(data[16:20]),
|
|
KeyLength: binary.LittleEndian.Uint32(data[20:24]),
|
|
}
|
|
|
|
if hdr.Encrypted != 0 {
|
|
return nil, fmt.Errorf("encrypted PVK files not supported")
|
|
}
|
|
|
|
// Key data starts after header and salt
|
|
keyData := data[24+hdr.SaltLength:]
|
|
return parsePrivateKeyBlob(keyData)
|
|
}
|
|
|
|
// parsePrivateKeyBlob parses a Windows PRIVATEKEYBLOB structure
|
|
func parsePrivateKeyBlob(data []byte) (*BackupKey, error) {
|
|
if len(data) < 20 {
|
|
return nil, fmt.Errorf("data too short for PRIVATEKEYBLOB")
|
|
}
|
|
|
|
bk := &BackupKey{}
|
|
pkb := &PrivateKeyBlob{}
|
|
|
|
r := bytes.NewReader(data)
|
|
|
|
// PUBLICKEYSTRUC
|
|
binary.Read(r, binary.LittleEndian, &pkb.PublicKeyStruc.Type)
|
|
binary.Read(r, binary.LittleEndian, &pkb.PublicKeyStruc.Version)
|
|
binary.Read(r, binary.LittleEndian, &pkb.PublicKeyStruc.Reserved)
|
|
binary.Read(r, binary.LittleEndian, &pkb.PublicKeyStruc.AlgID)
|
|
|
|
// RSAPUBKEY
|
|
binary.Read(r, binary.LittleEndian, &pkb.RSAPubKey.Magic)
|
|
binary.Read(r, binary.LittleEndian, &pkb.RSAPubKey.BitLen)
|
|
binary.Read(r, binary.LittleEndian, &pkb.RSAPubKey.PubExp)
|
|
|
|
// Check magic
|
|
if pkb.RSAPubKey.Magic != 0x32415352 { // "RSA2"
|
|
return nil, fmt.Errorf("invalid RSA key magic: 0x%x", pkb.RSAPubKey.Magic)
|
|
}
|
|
|
|
byteLen := int(pkb.RSAPubKey.BitLen / 8)
|
|
halfLen := byteLen / 2
|
|
|
|
// Read key components
|
|
pkb.Modulus = make([]byte, byteLen)
|
|
r.Read(pkb.Modulus)
|
|
|
|
pkb.Prime1 = make([]byte, halfLen)
|
|
r.Read(pkb.Prime1)
|
|
|
|
pkb.Prime2 = make([]byte, halfLen)
|
|
r.Read(pkb.Prime2)
|
|
|
|
pkb.Exponent1 = make([]byte, halfLen)
|
|
r.Read(pkb.Exponent1)
|
|
|
|
pkb.Exponent2 = make([]byte, halfLen)
|
|
r.Read(pkb.Exponent2)
|
|
|
|
pkb.Coefficient = make([]byte, halfLen)
|
|
r.Read(pkb.Coefficient)
|
|
|
|
pkb.PrivateExponent = make([]byte, byteLen)
|
|
r.Read(pkb.PrivateExponent)
|
|
|
|
// Convert to Go RSA key (need to reverse byte order - Windows uses little-endian)
|
|
bk.PrivateKey = &rsa.PrivateKey{
|
|
PublicKey: rsa.PublicKey{
|
|
N: new(big.Int).SetBytes(reverseBytes(pkb.Modulus)),
|
|
E: int(pkb.RSAPubKey.PubExp),
|
|
},
|
|
D: new(big.Int).SetBytes(reverseBytes(pkb.PrivateExponent)),
|
|
Primes: []*big.Int{
|
|
new(big.Int).SetBytes(reverseBytes(pkb.Prime1)),
|
|
new(big.Int).SetBytes(reverseBytes(pkb.Prime2)),
|
|
},
|
|
}
|
|
|
|
// Precompute for better performance
|
|
bk.PrivateKey.Precompute()
|
|
|
|
return bk, nil
|
|
}
|
|
|
|
// ToPEM converts the backup key to PEM format
|
|
func (bk *BackupKey) ToPEM() ([]byte, error) {
|
|
if bk.PrivateKey == nil {
|
|
return nil, fmt.Errorf("no private key available")
|
|
}
|
|
|
|
derBytes := x509.MarshalPKCS1PrivateKey(bk.PrivateKey)
|
|
block := &pem.Block{
|
|
Type: "RSA PRIVATE KEY",
|
|
Bytes: derBytes,
|
|
}
|
|
|
|
return pem.EncodeToMemory(block), nil
|
|
}
|
|
|
|
// ToPVK converts the backup key to PVK format
|
|
func (bk *BackupKey) ToPVK() []byte {
|
|
if bk.PVKData != nil {
|
|
return bk.PVKData
|
|
}
|
|
// Future enhancement: Implement conversion from RSA key to PVK
|
|
return nil
|
|
}
|
|
|
|
// Dump prints backup key information
|
|
func (bk *BackupKey) Dump() {
|
|
fmt.Println("[BACKUP KEY]")
|
|
fmt.Printf("Version : %d\n", bk.Version)
|
|
if bk.PrivateKey != nil {
|
|
fmt.Printf("Key Size : %d bits\n", bk.PrivateKey.N.BitLen())
|
|
fmt.Printf("Public Exp : %d\n", bk.PrivateKey.E)
|
|
fmt.Printf("Modulus : %s...\n", hex.EncodeToString(bk.PrivateKey.N.Bytes()[:32]))
|
|
}
|
|
if len(bk.Certificate) > 0 {
|
|
fmt.Printf("Certificate : %d bytes\n", len(bk.Certificate))
|
|
}
|
|
fmt.Println()
|
|
}
|
|
|
|
// reverseBytes reverses a byte slice (for little-endian to big-endian conversion)
|
|
func reverseBytes(b []byte) []byte {
|
|
result := make([]byte, len(b))
|
|
for i := 0; i < len(b); i++ {
|
|
result[i] = b[len(b)-1-i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ParsePrivateKeyData parses raw backup key data from LSA secrets
|
|
// The format depends on the version stored in LSA
|
|
func ParsePrivateKeyData(data []byte) (*BackupKey, error) {
|
|
if len(data) < 8 {
|
|
return nil, fmt.Errorf("data too short")
|
|
}
|
|
|
|
// Check for PVK magic (0xb0b5f11e)
|
|
magic := binary.LittleEndian.Uint32(data[0:4])
|
|
if magic == 0xb0b5f11e {
|
|
return parsePVKBackupKey(data)
|
|
}
|
|
|
|
// Check for PRIVATEKEYBLOB header (bType=0x07, bVersion=0x02)
|
|
if data[0] == 0x07 && data[1] == 0x02 {
|
|
return parsePrivateKeyBlob(data)
|
|
}
|
|
|
|
// Check version field (version 2 = BACKUP_KEY structure from LSA secret)
|
|
version := binary.LittleEndian.Uint32(data[0:4])
|
|
if version == 2 {
|
|
// BACKUP_KEY structure (from G$BCKUPKEY_<GUID>):
|
|
// Version (4) + PrivKeyLength (4) + CertificateLength (4) + PrivKey + Cert
|
|
if len(data) < 12 {
|
|
return nil, fmt.Errorf("data too short for v2 structure")
|
|
}
|
|
|
|
privKeyLen := binary.LittleEndian.Uint32(data[4:8])
|
|
certLen := binary.LittleEndian.Uint32(data[8:12])
|
|
|
|
offset := uint32(12)
|
|
if uint32(len(data)) < offset+privKeyLen+certLen {
|
|
return nil, fmt.Errorf("data too short for v2 content: have %d, need %d", len(data), offset+privKeyLen+certLen)
|
|
}
|
|
|
|
// Parse private key blob (starts at offset 12)
|
|
bk, err := parsePrivateKeyBlob(data[offset : offset+privKeyLen])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %v", err)
|
|
}
|
|
|
|
// Store certificate
|
|
if certLen > 0 {
|
|
bk.Certificate = make([]byte, certLen)
|
|
copy(bk.Certificate, data[offset+privKeyLen:])
|
|
}
|
|
bk.Version = 2
|
|
|
|
return bk, nil
|
|
}
|
|
|
|
// Try version 1 (P_BACKUP_KEY structure)
|
|
if version == 1 {
|
|
// P_BACKUP_KEY: Version(4) + KeyLength(4) + CertLen(4) + Key(KeyLen) + Cert(CertLen)
|
|
if len(data) < 12 {
|
|
return nil, fmt.Errorf("data too short for v1 structure")
|
|
}
|
|
|
|
keyLen := binary.LittleEndian.Uint32(data[4:8])
|
|
certLen := binary.LittleEndian.Uint32(data[8:12])
|
|
|
|
offset := 12
|
|
if uint32(len(data)) < uint32(offset)+keyLen+certLen {
|
|
return nil, fmt.Errorf("data too short for v1 content")
|
|
}
|
|
|
|
// The key data is a PRIVATEKEYBLOB
|
|
bk, err := parsePrivateKeyBlob(data[offset : uint32(offset)+keyLen])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %v", err)
|
|
}
|
|
|
|
bk.Certificate = make([]byte, certLen)
|
|
copy(bk.Certificate, data[uint32(offset)+keyLen:])
|
|
bk.Version = 1
|
|
|
|
return bk, nil
|
|
}
|
|
|
|
// Try as raw PRIVATEKEYBLOB
|
|
return parsePrivateKeyBlob(data)
|
|
}
|
|
|
|
// DecryptWithBackupKey decrypts a domain key using the domain backup key
|
|
func DecryptWithBackupKey(domainKey *DomainKey, backupKey *BackupKey) ([]byte, error) {
|
|
if backupKey.PrivateKey == nil {
|
|
return nil, fmt.Errorf("backup key has no private key")
|
|
}
|
|
|
|
// The domain key secret is RSA encrypted
|
|
// We need to use PKCS1v15 decryption
|
|
plaintext, err := rsa.DecryptPKCS1v15(nil, backupKey.PrivateKey, domainKey.Secret)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("RSA decryption failed: %v", err)
|
|
}
|
|
|
|
return plaintext, nil
|
|
}
|
|
|
|
// LoadPVKFile loads a backup key from a PVK file
|
|
func LoadPVKFile(data []byte) (*BackupKey, error) {
|
|
return parsePVKBackupKey(data)
|
|
}
|
|
|
|
// LoadPEMFile loads a backup key from a PEM file
|
|
func LoadPEMFile(data []byte) (*BackupKey, error) {
|
|
block, _ := pem.Decode(data)
|
|
if block == nil {
|
|
return nil, fmt.Errorf("no PEM block found")
|
|
}
|
|
|
|
var privateKey *rsa.PrivateKey
|
|
var err error
|
|
|
|
switch block.Type {
|
|
case "RSA PRIVATE KEY":
|
|
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
|
case "PRIVATE KEY":
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse PKCS8 key: %v", err)
|
|
}
|
|
var ok bool
|
|
privateKey, ok = key.(*rsa.PrivateKey)
|
|
if !ok {
|
|
return nil, fmt.Errorf("not an RSA private key")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse private key: %v", err)
|
|
}
|
|
|
|
return &BackupKey{
|
|
PrivateKey: privateKey,
|
|
Version: 1,
|
|
}, nil
|
|
}
|
|
|
|
// LoadBackupKeyFile loads a backup key from either PVK or PEM format
|
|
func LoadBackupKeyFile(data []byte) (*BackupKey, error) {
|
|
// Check for PEM format
|
|
if bytes.HasPrefix(data, []byte("-----BEGIN")) {
|
|
return LoadPEMFile(data)
|
|
}
|
|
|
|
// Check for PVK magic
|
|
if len(data) >= 4 && binary.LittleEndian.Uint32(data[0:4]) == 0xb0b5f11e {
|
|
return LoadPVKFile(data)
|
|
}
|
|
|
|
// Try parsing as raw PRIVATEKEYBLOB
|
|
return parsePrivateKeyBlob(data)
|
|
}
|