[SHEHZADE//ABDULLAH ANSARI]
Red Team Consultant @ Google | MS in Cybersecurity @ NYU
● Notice //GitHub (@shehzade) is temporarily suspended pending an automated false-positive review. All tools are hosted on this Code Mirror and available on request (contact); GitHub links will return once reinstated.
PUBLIC MIRRORGo v1.0.0
Offline Active Directory database (ntds.dit) parser and BloodHound graph reconstructor in pure Go. Extracts directory objects, group memberships, domain trusts, and binary DACLs from dead-disk ESE database pages and registry hives.
$
git clone https://shehzade.io/repos/ntds-to-bloodhound.git
Copy
Source ZIP (246.9 KB)
Prebuilt Binaries
269 lines · 6.4 KB
| 1 | package crypto |
| 2 | |
| 3 | import ( |
| 4 | "crypto/aes" |
| 5 | "crypto/cipher" |
| 6 | "crypto/des" |
| 7 | "crypto/md5" |
| 8 | "crypto/rc4" |
| 9 | "encoding/binary" |
| 10 | "encoding/hex" |
| 11 | "errors" |
| 12 | "fmt" |
| 13 | "strings" |
| 14 | ) |
| 15 | |
| 16 | const ( |
| 17 | EmptyLMHash = "aad3b435b51404eeaad3b435b51404ee" |
| 18 | EmptyNTHash = "31d6cfe0d16ae931b73c59d7e0c089c0" |
| 19 | ) |
| 20 | |
| 21 | type CryptoEngine struct { |
| 22 | BootKey []byte |
| 23 | PEKList [][]byte |
| 24 | } |
| 25 | |
| 26 | func NewCryptoEngine(bootKey []byte) *CryptoEngine { |
| 27 | return &CryptoEngine{ |
| 28 | BootKey: bootKey, |
| 29 | PEKList: make([][]byte, 0), |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func (c *CryptoEngine) ParsePekList(pekListRaw []byte) error { |
| 34 | if len(pekListRaw) < 24 { |
| 35 | return errors.New("pekList blob too short") |
| 36 | } |
| 37 | |
| 38 | header := pekListRaw[:8] |
| 39 | keyMaterial := pekListRaw[8:24] |
| 40 | encryptedPek := pekListRaw[24:] |
| 41 | |
| 42 | var decryptedPekList []byte |
| 43 | |
| 44 | if binary.LittleEndian.Uint32(header[:4]) == 0x02 { |
| 45 | h := md5.New() |
| 46 | h.Write(c.BootKey) |
| 47 | for i := 0; i < 1000; i++ { |
| 48 | h.Write(keyMaterial) |
| 49 | } |
| 50 | tmpKey := h.Sum(nil) |
| 51 | rc4Cipher, err := rc4.NewCipher(tmpKey) |
| 52 | if err != nil { |
| 53 | return fmt.Errorf("failed to create RC4 cipher for PEK: %w", err) |
| 54 | } |
| 55 | decryptedPekList = make([]byte, len(encryptedPek)) |
| 56 | rc4Cipher.XORKeyStream(decryptedPekList, encryptedPek) |
| 57 | } else if binary.LittleEndian.Uint32(header[:4]) == 0x03 { |
| 58 | block, err := aes.NewCipher(c.BootKey) |
| 59 | if err != nil { |
| 60 | return fmt.Errorf("failed to create AES cipher for PEK: %w", err) |
| 61 | } |
| 62 | if len(encryptedPek)%aes.BlockSize != 0 { |
| 63 | return fmt.Errorf("encrypted PEK size %d is not a multiple of block size", len(encryptedPek)) |
| 64 | } |
| 65 | mode := cipher.NewCBCDecrypter(block, keyMaterial) |
| 66 | decryptedPekList = make([]byte, len(encryptedPek)) |
| 67 | mode.CryptBlocks(decryptedPekList, encryptedPek) |
| 68 | } else { |
| 69 | return fmt.Errorf("unsupported PEK header version: %x", header[:4]) |
| 70 | } |
| 71 | |
| 72 | if len(decryptedPekList) < 32 { |
| 73 | return errors.New("decrypted PEK list too short") |
| 74 | } |
| 75 | |
| 76 | plainEntries := decryptedPekList[32:] |
| 77 | pos := 0 |
| 78 | curIndex := uint32(0) |
| 79 | |
| 80 | for pos+20 <= len(plainEntries) { |
| 81 | index := binary.LittleEndian.Uint32(plainEntries[pos : pos+4]) |
| 82 | pekKey := make([]byte, 16) |
| 83 | copy(pekKey, plainEntries[pos+4:pos+20]) |
| 84 | |
| 85 | if index != curIndex { |
| 86 | break |
| 87 | } |
| 88 | c.PEKList = append(c.PEKList, pekKey) |
| 89 | curIndex++ |
| 90 | pos += 20 |
| 91 | } |
| 92 | |
| 93 | if len(c.PEKList) == 0 { |
| 94 | return errors.New("no valid PEK keys found in pekList") |
| 95 | } |
| 96 | |
| 97 | return nil |
| 98 | } |
| 99 | |
| 100 | func transformKey(in []byte) []byte { |
| 101 | out := make([]byte, 8) |
| 102 | out[0] = in[0] >> 1 |
| 103 | out[1] = ((in[0] & 0x01) << 6) | (in[1] >> 2) |
| 104 | out[2] = ((in[1] & 0x03) << 5) | (in[2] >> 3) |
| 105 | out[3] = ((in[2] & 0x07) << 4) | (in[3] >> 4) |
| 106 | out[4] = ((in[3] & 0x0F) << 3) | (in[4] >> 5) |
| 107 | out[5] = ((in[4] & 0x1F) << 2) | (in[5] >> 6) |
| 108 | out[6] = ((in[5] & 0x3F) << 1) | (in[6] >> 7) |
| 109 | out[7] = in[6] & 0x7F |
| 110 | |
| 111 | for i := 0; i < 8; i++ { |
| 112 | out[i] = (out[i] << 1) & 0xFE |
| 113 | } |
| 114 | return out |
| 115 | } |
| 116 | |
| 117 | func deriveDesKeys(rid uint32) ([]byte, []byte) { |
| 118 | ridBytes := make([]byte, 4) |
| 119 | binary.LittleEndian.PutUint32(ridBytes, rid) |
| 120 | |
| 121 | k1 := []byte{ridBytes[0], ridBytes[1], ridBytes[2], ridBytes[3], ridBytes[0], ridBytes[1], ridBytes[2]} |
| 122 | k2 := []byte{ridBytes[3], ridBytes[0], ridBytes[1], ridBytes[2], ridBytes[3], ridBytes[0], ridBytes[1]} |
| 123 | |
| 124 | return transformKey(k1), transformKey(k2) |
| 125 | } |
| 126 | |
| 127 | func removeDESLayer(crypted []byte, rid uint32) ([]byte, error) { |
| 128 | if len(crypted) < 16 { |
| 129 | return nil, errors.New("crypted hash must be at least 16 bytes") |
| 130 | } |
| 131 | |
| 132 | k1, k2 := deriveDesKeys(rid) |
| 133 | c1, err := des.NewCipher(k1) |
| 134 | if err != nil { |
| 135 | return nil, err |
| 136 | } |
| 137 | c2, err := des.NewCipher(k2) |
| 138 | if err != nil { |
| 139 | return nil, err |
| 140 | } |
| 141 | |
| 142 | out := make([]byte, 16) |
| 143 | c1.Decrypt(out[:8], crypted[:8]) |
| 144 | c2.Decrypt(out[8:16], crypted[8:16]) |
| 145 | return out, nil |
| 146 | } |
| 147 | |
| 148 | func (c *CryptoEngine) DecryptHash(encBlob []byte, rid uint32) ([]byte, error) { |
| 149 | if len(c.PEKList) == 0 { |
| 150 | return nil, errors.New("no PEK keys loaded") |
| 151 | } |
| 152 | if len(encBlob) < 24 { |
| 153 | return nil, errors.New("encrypted hash blob too short") |
| 154 | } |
| 155 | |
| 156 | header := encBlob[:8] |
| 157 | keyMaterial := encBlob[8:24] |
| 158 | |
| 159 | pekIndexHex := hex.EncodeToString(header) |
| 160 | if len(pekIndexHex) < 10 { |
| 161 | return nil, errors.New("invalid header format") |
| 162 | } |
| 163 | pekIndex := int(header[4]) |
| 164 | if pekIndex >= len(c.PEKList) { |
| 165 | pekIndex = 0 |
| 166 | } |
| 167 | pek := c.PEKList[pekIndex] |
| 168 | |
| 169 | var tmpHash []byte |
| 170 | |
| 171 | if binary.LittleEndian.Uint32(header[:4]) == 0x13 { |
| 172 | if len(encBlob) < 28+16 { |
| 173 | return nil, errors.New("AES hash blob too short") |
| 174 | } |
| 175 | ciphertext := encBlob[28 : 28+16] |
| 176 | |
| 177 | block, err := aes.NewCipher(pek) |
| 178 | if err != nil { |
| 179 | return nil, fmt.Errorf("failed to create AES cipher: %w", err) |
| 180 | } |
| 181 | mode := cipher.NewCBCDecrypter(block, keyMaterial) |
| 182 | tmpHash = make([]byte, 16) |
| 183 | mode.CryptBlocks(tmpHash, ciphertext) |
| 184 | } else { |
| 185 | if len(encBlob) < 24+16 { |
| 186 | return nil, errors.New("RC4 hash blob too short") |
| 187 | } |
| 188 | ciphertext := encBlob[24 : 24+16] |
| 189 | |
| 190 | h := md5.New() |
| 191 | h.Write(pek) |
| 192 | h.Write(keyMaterial) |
| 193 | tmpKey := h.Sum(nil) |
| 194 | |
| 195 | rc4Cipher, err := rc4.NewCipher(tmpKey) |
| 196 | if err != nil { |
| 197 | return nil, fmt.Errorf("failed to create RC4 cipher: %w", err) |
| 198 | } |
| 199 | tmpHash = make([]byte, 16) |
| 200 | rc4Cipher.XORKeyStream(tmpHash, ciphertext) |
| 201 | } |
| 202 | |
| 203 | return removeDESLayer(tmpHash, rid) |
| 204 | } |
| 205 | |
| 206 | func ParseSID(b []byte) (string, uint32, error) { |
| 207 | if len(b) < 8 { |
| 208 | return "", 0, errors.New("SID buffer too short") |
| 209 | } |
| 210 | |
| 211 | revision := b[0] |
| 212 | subAuthCount := int(b[1]) |
| 213 | if len(b) < 8+subAuthCount*4 { |
| 214 | return "", 0, errors.New("SID buffer truncated for subauthorities") |
| 215 | } |
| 216 | |
| 217 | var authority uint64 |
| 218 | for i := 2; i < 8; i++ { |
| 219 | authority = (authority << 8) | uint64(b[i]) |
| 220 | } |
| 221 | |
| 222 | parts := []string{fmt.Sprintf("S-%d-%d", revision, authority)} |
| 223 | var rid uint32 |
| 224 | |
| 225 | for i := 0; i < subAuthCount; i++ { |
| 226 | offset := 8 + i*4 |
| 227 | var subAuth uint32 |
| 228 | if i == subAuthCount-1 && subAuthCount > 1 { |
| 229 | subAuth = binary.BigEndian.Uint32(b[offset : offset+4]) |
| 230 | rid = subAuth |
| 231 | } else { |
| 232 | subAuth = binary.LittleEndian.Uint32(b[offset : offset+4]) |
| 233 | } |
| 234 | parts = append(parts, fmt.Sprintf("%d", subAuth)) |
| 235 | } |
| 236 | |
| 237 | return strings.Join(parts, "-"), rid, nil |
| 238 | } |
| 239 | |
| 240 | func ParseBinarySID(b []byte) (string, uint32, error) { |
| 241 | if len(b) < 8 { |
| 242 | return "", 0, errors.New("SID buffer too short") |
| 243 | } |
| 244 | |
| 245 | revision := b[0] |
| 246 | subAuthCount := int(b[1]) |
| 247 | if revision != 1 || subAuthCount > 15 || len(b) < 8+subAuthCount*4 { |
| 248 | return "", 0, errors.New("SID buffer invalid or truncated") |
| 249 | } |
| 250 | |
| 251 | var authority uint64 |
| 252 | for i := 2; i < 8; i++ { |
| 253 | authority = (authority << 8) | uint64(b[i]) |
| 254 | } |
| 255 | |
| 256 | parts := []string{fmt.Sprintf("S-%d-%d", revision, authority)} |
| 257 | var rid uint32 |
| 258 | |
| 259 | for i := 0; i < subAuthCount; i++ { |
| 260 | offset := 8 + i*4 |
| 261 | subAuth := binary.LittleEndian.Uint32(b[offset : offset+4]) |
| 262 | if i == subAuthCount-1 { |
| 263 | rid = subAuth |
| 264 | } |
| 265 | parts = append(parts, fmt.Sprintf("%d", subAuth)) |
| 266 | } |
| 267 | |
| 268 | return strings.Join(parts, "-"), rid, nil |
| 269 | } |