github.com/google/osv-scalibr@v0.4.1/detector/weakcredentials/winlocal/samreg/crypto.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package samreg
    16  
    17  import (
    18  	"crypto/aes"
    19  	"crypto/cipher"
    20  	"crypto/des"
    21  	"crypto/md5"
    22  	"crypto/rc4"
    23  	"errors"
    24  	"slices"
    25  )
    26  
    27  /*
    28  ** Note: This file contains the code to perform the final step of decrypting the user's hashes.
    29  **
    30  ** You can read more about hash encryption in the following article:
    31  ** https://web.archive.org/web/20190717124313/http://www.beginningtoseethelight.org/ntsecurity/index.htm#D3BC3F5643A17823
    32  **
    33  ** The hashes are encrypted several times with different encryption algorithms and keys.
    34  ** First, the hashes can be encrypted with RC4 or AES-256.
    35  **   - For RC4, the key is the MD5 hash of the syskey, the RID and statically defined constants
    36  **     (different for NTLM or LM).
    37  **   - For AES-256, the key is the syskey and the IV is found in the UserV structure directly.
    38  **
    39  ** Whether RC4 or AES-256 is to be used depends on a combination of the current OS version, the time
    40  ** the hash were generated and the current OS configuration. But the hash data stored in the UserV
    41  ** structure contains bits indicating which encryption algorithm was used.
    42  **
    43  ** Once the first decryption has been performed, the hash is always encrypted with a final layer of
    44  ** DES. The encrypted is split in two halves and each half is decrypted with a different key.
    45  ** The two keys are derived from the user's RID. They are derived from a set of statically
    46  ** defined permutations and bitwise operations.
    47   */
    48  
    49  const (
    50  	// LMHashConstant is used to decrypt LM hashes when RC4 is used.
    51  	LMHashConstant = "LMPASSWORD\x00"
    52  	// NTLMHashConstant is used to decrypt NTLM hashes when RC4 is used.
    53  	NTLMHashConstant = "NTPASSWORD\x00"
    54  )
    55  
    56  var (
    57  	errInvalidRIDSize = errors.New("RID cannot be derived: is not 4 bytes")
    58  )
    59  
    60  // transformRID performs a set of bitwise operations on the provided key to derive one of the two
    61  // 8-byte keys that will be used in the final step of hash decryption (DES).
    62  // These bitwise operations are hardcoded on the Windows side, so we do the same here.
    63  func transformRID(key []byte) []byte {
    64  	var outputKey []byte
    65  	outputKey = append(outputKey, key[0]>>0x1)
    66  	outputKey = append(outputKey, ((key[0]&0x01)<<6)|(key[1]>>2))
    67  	outputKey = append(outputKey, ((key[1]&0x03)<<5)|(key[2]>>3))
    68  	outputKey = append(outputKey, ((key[2]&0x07)<<4)|(key[3]>>4))
    69  	outputKey = append(outputKey, ((key[3]&0x0F)<<3)|(key[4]>>5))
    70  	outputKey = append(outputKey, ((key[4]&0x1F)<<2)|(key[5]>>6))
    71  	outputKey = append(outputKey, ((key[5]&0x3F)<<1)|(key[6]>>7))
    72  	outputKey = append(outputKey, key[6]&0x7F)
    73  
    74  	for i := range 8 {
    75  		outputKey[i] = (outputKey[i] << 1) & 0xfe
    76  	}
    77  
    78  	return outputKey
    79  }
    80  
    81  // deriveRID derives two 8-byte keys from the provided RID. It performs a set of predefined
    82  // circular permutations on the RID before using transformRID to perform bitwise operations that
    83  // will result in the final keys.
    84  func deriveRID(rid []byte) ([]byte, []byte, error) {
    85  	if len(rid) != 4 {
    86  		return nil, nil, errInvalidRIDSize
    87  	}
    88  
    89  	rid1 := []byte{rid[0], rid[1], rid[2], rid[3], rid[0], rid[1], rid[2]}
    90  	rid2 := []byte{rid[3], rid[0], rid[1], rid[2], rid[3], rid[0], rid[1]}
    91  	return transformRID(rid1), transformRID(rid2), nil
    92  }
    93  
    94  // Whichever encryption algorithm is used, the last round of decryption is always DES. It involves:
    95  //   - Taking the RID of the user and deriving it into two 8-byte DES keys
    96  //   - Each of these two keys is then used to decrypt a half of the encrypted hash
    97  func decryptDES(rid []byte, encryptedHash []byte) ([]byte, error) {
    98  	key1, key2, err := deriveRID(rid)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	block1, err := des.NewCipher(key1)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	block2, err := des.NewCipher(key2)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	decryptedHash := make([]byte, 16)
   114  	block1.Decrypt(decryptedHash[:8], encryptedHash[:8])
   115  	block2.Decrypt(decryptedHash[8:], encryptedHash[8:])
   116  	return decryptedHash, nil
   117  }
   118  
   119  // decryptRC4Hash decrypts the RC4 encrypted user's hash back to LM/NTLM format using the derived
   120  // syskey. Note that the syskey is expected to be already derived.
   121  func decryptRC4Hash(rid []byte, syskey, hash []byte, hashConstant []byte) ([]byte, error) {
   122  	rc4Key := md5.Sum(slices.Concat(syskey, rid, hashConstant))
   123  	c, err := rc4.NewCipher(rc4Key[:])
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	rc4Decrypted := make([]byte, 16)
   129  	c.XORKeyStream(rc4Decrypted, hash)
   130  	return decryptDES(rid, rc4Decrypted[:16])
   131  }
   132  
   133  // decryptAESHash decrypts the AES encrypted user's hash back to LM/NTLM format using the derived
   134  // syskey. Note that the syskey is expected to be already derived.
   135  func decryptAESHash(rid []byte, syskey, hash []byte, iv []byte) ([]byte, error) {
   136  	if len(hash) == 0 {
   137  		return nil, nil
   138  	}
   139  
   140  	if len(hash)%aes.BlockSize != 0 {
   141  		return nil, errors.New("hash length not aligned with AES block size")
   142  	}
   143  
   144  	block, err := aes.NewCipher(syskey)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	mode := cipher.NewCBCDecrypter(block, iv)
   150  	aesDecrypted := make([]byte, 32)
   151  	mode.CryptBlocks(aesDecrypted, hash)
   152  	return decryptDES(rid, aesDecrypted)
   153  }