Fixed bug where NT hashes were decrypted incorrectly on certain machines. Bumped version of go-smb library to v0.2.6 and golog to v0.3.3, fixing an issue with dumping large DCC2 caches. Added better remote OS detection. Added a Github workflow to build statically compiled binaries.

This commit is contained in:
Jimmy Fjällid
2023-11-22 20:31:24 +01:00
parent 429c7f1fe2
commit fee6676f32
7 changed files with 301 additions and 132 deletions
+39
View File
@@ -0,0 +1,39 @@
# This workflow will build binaries for Linux and Windows x64 architecture
name: release
on:
release:
types: [published]
jobs:
build:
permissions:
contents: write # to allow upload of release assets
runs-on: ubuntu-latest
steps:
- name: checkout repo
uses: actions/checkout@v4
- name: set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
- name: install dependencies
run: go get
- name: build project
run: make
- name: publish binaries to release
run: |
ASSET_NAME_1="go-secdump"
ASSET_NAME_2="go-secdump.exe"
UPLOAD_URL="https://uploads.github.com/repos/${{ github.repository }}/releases/${{ github.event.release.id }}/assets"
# Upload asset 1
curl -sSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @"$ASSET_NAME_1" "$UPLOAD_URL?name=$ASSET_NAME_1"
# Upload asset 2
curl -sSL -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "Content-Type: application/octet-stream" --data-binary @"$ASSET_NAME_2" "$UPLOAD_URL?name=$ASSET_NAME_2"
+2 -2
View File
@@ -1,6 +1,6 @@
all:
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o go-secdump
GOOS=windows GOARCH=386 go build -ldflags "-s -w" -o go-secdump.exe .
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o go-secdump.exe .
clean:
rm -f go-secdump
+2 -2
View File
@@ -3,8 +3,8 @@ module github.com/jfjallid/go-secdump
go 1.19
require (
github.com/jfjallid/go-smb v0.2.5
github.com/jfjallid/golog v0.3.2
github.com/jfjallid/go-smb v0.2.6
github.com/jfjallid/golog v0.3.3
golang.org/x/crypto v0.6.0
golang.org/x/term v0.5.0
)
+4 -4
View File
@@ -1,7 +1,7 @@
github.com/jfjallid/go-smb v0.2.5 h1:J3ubQCBqI0FPczM7Tgn36MAG0H2w+lUCCA3q6b7/5k8=
github.com/jfjallid/go-smb v0.2.5/go.mod h1:Kew0ysf3f+GtnBzpU7jiZmZ4ohp4ZHE7z2PMnXXG7IM=
github.com/jfjallid/golog v0.3.2 h1:i3fKI9AUOnIvPmp/1qtLAbjaS2l3SjdjaE4izzpBwvc=
github.com/jfjallid/golog v0.3.2/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58=
github.com/jfjallid/go-smb v0.2.6 h1:kF0UH7wXjqnqSrISc0vEpLkJFfsz4Bnr/jlXAobrwAM=
github.com/jfjallid/go-smb v0.2.6/go.mod h1:Kew0ysf3f+GtnBzpU7jiZmZ4ohp4ZHE7z2PMnXXG7IM=
github.com/jfjallid/golog v0.3.3 h1:dY6qf8wTxJ9OwBPVTadVRDmt/6MVXSWwXrxaGMMyXsU=
github.com/jfjallid/golog v0.3.3/go.mod h1:19Q/zg5OgPPd0xhFllokPnMzthzhFPZmiAGAokE7k58=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
+1 -1
View File
@@ -39,7 +39,7 @@ import (
)
var log = golog.Get("")
var release string = "0.1.1"
var release string = "0.1.2"
func startRemoteRegistry(session *smb.Connection, share string) (err error) {
f, err := session.OpenFile(share, "svcctl")
+170 -60
View File
@@ -103,6 +103,80 @@ func (self *dpapi_system) unmarshal(data []byte) {
copy(self.UserKey[:], data[24:44])
}
type sam_key_data_aes struct {
Revision uint32
Length uint32
ChecksumLen uint32
DataLen uint32
Salt [16]byte
Data [32]byte
}
type sam_key_data struct {
Revision uint32
Length uint32
Salt [16]byte
Key [16]byte
Checksum [16]byte
_ uint64
}
type domain_account_f struct { // 104 bytes of fixed length fields
Revision uint16
_ uint32 // Unknown
_ uint16 // Unknown
CreationTime uint64
DomainModifiedAccount uint64
MaxPasswordAge uint64
MinPasswordAge uint64
ForceLogoff uint64
LockoutDuration uint64
LockoutObservationWindow uint64
ModifiedCountAtLastPromotion uint64
NextRid uint32
PasswordProperties uint32
MinPasswordLength uint16
PasswordHistoryLength uint16
LockoutThreshold uint16
_ uint16 // Unknown
ServerState uint32
ServerRole uint32
UasCompatibilityRequired uint32
_ uint32 // Unknown
Data []byte
}
func (self *domain_account_f) unmarshal(data []byte) (err error) {
if len(data) < 104 {
err = fmt.Errorf("Not enough data to unmarshal a DOMAIN_ACCOUNT_F")
log.Errorln(err)
return
}
self.Revision = binary.LittleEndian.Uint16(data[:2])
self.CreationTime = binary.LittleEndian.Uint64(data[8:16])
self.DomainModifiedAccount = binary.LittleEndian.Uint64(data[16:24])
self.MaxPasswordAge = binary.LittleEndian.Uint64(data[24:32])
self.MinPasswordAge = binary.LittleEndian.Uint64(data[32:40])
self.ForceLogoff = binary.LittleEndian.Uint64(data[40:48])
self.LockoutDuration = binary.LittleEndian.Uint64(data[48:56])
self.LockoutObservationWindow = binary.LittleEndian.Uint64(data[56:64])
self.ModifiedCountAtLastPromotion = binary.LittleEndian.Uint64(data[64:72])
self.NextRid = binary.LittleEndian.Uint32(data[72:76])
self.PasswordProperties = binary.LittleEndian.Uint32(data[76:80])
self.MinPasswordLength = binary.LittleEndian.Uint16(data[80:82])
self.PasswordHistoryLength = binary.LittleEndian.Uint16(data[82:84])
self.LockoutThreshold = binary.LittleEndian.Uint16(data[84:86])
self.ServerState = binary.LittleEndian.Uint32(data[88:92])
self.ServerRole = binary.LittleEndian.Uint32(data[92:96])
self.UasCompatibilityRequired = binary.LittleEndian.Uint32(data[96:100])
if len(data) > 104 {
self.Data = make([]byte, len(data[104:]))
copy(self.Data, data[104:])
}
return
}
type nl_record struct {
UserLength uint16
DomainNameLength uint16
@@ -418,35 +492,6 @@ func getBootKey(rpccon *msrrp.RPCCon, base []byte) (result []byte, err error) {
return
}
func getEncryptedsysKey(rpccon *msrrp.RPCCon, base, f []byte) (encsysKey []byte, err error) {
val, err := IsWin10After1607(GetOSVersionBuild(rpccon, base))
if err != nil {
return
}
if val {
log.Debugf("Getting AES encrypted sysKey from F[0x88]\n")
encsysKey = f[0x88 : 0x88+16]
} else {
encsysKey = f[0x80 : 0x80+16]
}
return
}
func getEncryptedsysKeyIV(rpccon *msrrp.RPCCon, base, f []byte) (sysKeyIV []byte, err error) {
val, err := IsWin10After1607(GetOSVersionBuild(rpccon, base))
if err != nil {
return
}
if val {
log.Debugf("Getting sysKey IV from F[0x78]\n")
sysKeyIV = f[0x78 : 0x78+16]
} else {
sysKeyIV = f[0x70 : 0x70+16]
}
return
}
func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) {
var tmpSysKey []byte
_, err = getBootKey(rpccon, base)
@@ -459,7 +504,7 @@ func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) {
return
}
f, err := rpccon.QueryValue(hSubKey, "F")
fBytes, err := rpccon.QueryValue(hSubKey, "F")
if err != nil {
log.Errorln(err)
rpccon.CloseKeyHandle(hSubKey)
@@ -468,61 +513,97 @@ func getSysKey(rpccon *msrrp.RPCCon, base []byte) (sysKey []byte, err error) {
rpccon.CloseKeyHandle(hSubKey)
encsysKey, err := getEncryptedsysKey(rpccon, base, f)
if err != nil {
return
}
sysKeyIV, err := getEncryptedsysKeyIV(rpccon, base, f)
f := &domain_account_f{}
err = f.unmarshal(fBytes)
if err != nil {
log.Errorln(err)
rpccon.CloseKeyHandle(hSubKey)
return
}
if f[0] == 0x3 {
tmpSysKey, err = DecryptAESSysKey(BootKey, encsysKey, sysKeyIV)
var encSysKey []byte
var sysKeyIV []byte
sysKey = make([]byte, 16)
if f.Revision == 3 {
// AES
samAesData := sam_key_data_aes{}
err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, &samAesData)
if err != nil {
log.Errorln(err)
rpccon.CloseKeyHandle(hSubKey)
return
}
sysKeyIV = samAesData.Salt[:]
encSysKey = samAesData.Data[:samAesData.DataLen]
tmpSysKey, err = DecryptAESSysKey(BootKey, encSysKey, sysKeyIV)
copy(sysKey, tmpSysKey)
} else if f.Revision == 2 {
// RC4
samData := &sam_key_data{}
err = binary.Read(bytes.NewReader(f.Data), binary.LittleEndian, samData)
if err != nil {
log.Errorln(err)
rpccon.CloseKeyHandle(hSubKey)
return
}
sysKeyIV = samData.Salt[:]
// For RC4, also check the checksum so we should XOR 32 bytes instead of just 16
encSysKey = append(samData.Key[:], samData.Checksum[:]...)
tmpSysKey, err = DecryptRC4SysKey(BootKey, encSysKey, sysKeyIV)
// Verify checksum
input := []byte{}
input = append(input, tmpSysKey[:16]...)
input = append(input, s2...)
input = append(input, tmpSysKey[:16]...)
input = append(input, s1...)
checksum := md5.Sum(input)
if bytes.Compare(checksum[:], tmpSysKey[16:]) != 0 {
err = fmt.Errorf("Syskey checksum failed. Could be that a Syskey startup password is in use.")
log.Errorln(err)
return
}
copy(sysKey, tmpSysKey[:16])
} else {
tmpSysKey, err = DecryptRC4SysKey(BootKey, encsysKey, sysKeyIV)
}
if err != nil {
err = fmt.Errorf("Unknown revision of DOMAIN_ACCOUNT_F")
log.Errorln(err)
return
}
sysKey = make([]byte, len(tmpSysKey))
copy(sysKey, tmpSysKey)
log.Noticef("SysKey: 0x%x\n", sysKey)
return
}
func DecryptRC4SysKey(bootkey, encSyskey, syskeyIV []byte) (syskey []byte, err error) {
func DecryptRC4SysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) {
// Building the decryption key for the Syskey
input := []byte{}
input = append(input, syskeyIV...)
input = append(input, sysKeyIV...)
input = append(input, s1...)
input = append(input, bootkey...)
input = append(input, bootKey...)
input = append(input, s2...)
// Building syskey RC4 enc key with: one byte from F[0x70], String1, Bootkey, String2
rc4key := md5.Sum(input)
log.Debugf("Syskey RC4 enc key: md5(%x %x %x %x)\n", syskeyIV, s1, bootkey, s2)
log.Debugf("Syskey RC4 enc key: md5(%x %q %x %q)\n", sysKeyIV, s1, bootKey, s2)
log.Debugf("Syskey RC4 enc key: %x\n", rc4key)
c1, err := rc4.NewCipher(rc4key[:])
if err != nil {
log.Errorln("Failed to init RC4 key")
return
}
syskey = make([]byte, 16)
c1.XORKeyStream(syskey, encSyskey)
log.Debugf("Syskey: %x\n", syskey)
sysKey = make([]byte, 32)
c1.XORKeyStream(sysKey, encSysKey)
return
}
func DecryptAESSysKey(bootkey, encSyskey, syskeyIV []byte) (syskey []byte, err error) {
syskey = make([]byte, 16)
a1, err := aes.NewCipher(bootkey)
func DecryptAESSysKey(bootKey, encSysKey, sysKeyIV []byte) (sysKey []byte, err error) {
sysKey = make([]byte, len(encSysKey))
a1, err := aes.NewCipher(bootKey)
if err != nil {
log.Errorln("Failed to init AES key")
return
}
c1 := cipher.NewCBCDecrypter(a1, syskeyIV)
c1.CryptBlocks(syskey, encSyskey)
c1 := cipher.NewCBCDecrypter(a1, sysKeyIV)
c1.CryptBlocks(sysKey, encSysKey)
return
}
@@ -532,6 +613,14 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC
// Some entires have empty passwords or hash retrieval fails for some reason.
// In those cases I skip to the next entry. I increment the ctr first thing
// instead of at the end of the loop to make sure it happens
// Determine OS version once
osBuild, osVersion, isServer, err := GetOSVersionBuild(rpccon, base)
if err != nil {
log.Errorln(err)
return
}
cntr := -1
for _, ridStr := range rids {
cntr += 1
@@ -632,7 +721,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC
szNT := binary.LittleEndian.Uint32(v[0xac:])
offsetHashStruct := binary.LittleEndian.Uint32(v[0xa8:]) + 0xcc
//log.Debugf("Size of SAM Hash structure for user %s: %d located at offset: 0x%x\n", name, szNT, offsetHashStruct)
preWin11, err2 := IsBetweenWinXPWin10(GetOSVersionBuild(rpccon, base))
preWin11, err2 := IsBetweenWinXPWin10(osBuild, osVersion, isServer)
if err2 != nil {
log.Errorln(err2)
continue
@@ -644,7 +733,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC
result[cntr].AES = false
result[cntr].Data = v[offsetNTHash : offsetNTHash+16]
} else {
afterAnniversary, err2 := IsWin10After1607(GetOSVersionBuild(rpccon, base))
afterAnniversary, err2 := IsWin10After1607(osBuild, osVersion)
if err2 != nil {
log.Errorln(err2)
continue
@@ -669,7 +758,7 @@ func getNTHash(rpccon *msrrp.RPCCon, base []byte, rids []string) (result []UserC
result[cntr].AES = false
result[cntr].Data = []byte{}
} else {
log.Warningf("Unknown Hash type for %x with length 0x%x with OS: %s\n", rid, szNT, GetOsVersionName(GetOSVersionBuild(rpccon, base)))
log.Warningf("Unknown Hash type for %x with length 0x%x with OS: %s\n", rid, szNT, osNameMap[GetOSVersion(osBuild, osVersion, isServer)])
}
}
}
@@ -1046,13 +1135,15 @@ func GetCachedHashes(rpccon *msrrp.RPCCon, base []byte) (result []string, err er
return
}
func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version float64, err error) {
func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version float64, server bool, err error) {
hSubKey, err := rpccon.OpenSubKey(base, `SOFTWARE\Microsoft\Windows NT\CurrentVersion`)
if err != nil {
log.Noticef("Failed to open registry key CurrentVersion with error: %v\n", err)
return
}
defer rpccon.CloseKeyHandle(hSubKey)
defer func(h []byte) {
rpccon.CloseKeyHandle(h)
}(hSubKey)
value, err := rpccon.QueryValueString(hSubKey, "CurrentBuild")
if err != nil {
@@ -1078,5 +1169,24 @@ func GetOSVersionBuild(rpccon *msrrp.RPCCon, base []byte) (build int, version fl
return
}
hSubKey, err = rpccon.OpenSubKey(base, `SYSTEM\CurrentControlSet\Control\ProductOptions`)
if err != nil {
log.Noticef("Failed to open registry key ProductOptions with error: %v\n", err)
return
}
defer func(h []byte) {
rpccon.CloseKeyHandle(h)
}(hSubKey)
value, err = rpccon.QueryValueString(hSubKey, "ProductType")
if err != nil {
log.Errorln(err)
return
}
if strings.Compare(value, "ServerNT") == 0 {
server = true
}
return
}
+83 -63
View File
@@ -30,79 +30,105 @@ import (
"crypto/sha256"
"encoding/binary"
"math/bits"
"strconv"
)
const (
WIN_UNKNOWN = iota
WINXP
WIN_SERVER_2003
WIN_VISTA
WIN_SERVER_2008
WIN7
WIN_SERVER_2008_R2
WIN8
WIN_SERVER_2012
WIN81
WIN_SERVER_2012_R2
WIN10
WIN_SERVER_2016
WIN_SERVER_2019
WIN_SERVER_2022
WIN11
)
func GetOSVersion(build int, version float64) (os byte, err error) {
var osNameMap = map[byte]string{
WIN_UNKNOWN: "Windows Unknown",
WINXP: "Windows XP",
WIN_VISTA: "Windows Vista",
WIN7: "Windows 7",
WIN8: "Windows 8",
WIN81: "Windows 8.1",
WIN10: "Windows 10",
WIN11: "Windows 11",
WIN_SERVER_2003: "Windows Server 2003",
WIN_SERVER_2008: "Windows Server 2008",
WIN_SERVER_2008_R2: "Windows Server 2008 R2",
WIN_SERVER_2012: "Windows Server 2012",
WIN_SERVER_2012_R2: "Windows Server 2012 R2",
WIN_SERVER_2016: "Windows Server 2016",
WIN_SERVER_2019: "Windows Server 2019",
WIN_SERVER_2022: "Windows Server 2022",
}
if (build >= 7000) && (build < 7999) {
log.Debugf("Windows 7\n")
os = WIN7
} else if (build >= 9000) && (build < 9999) {
if version < 6.3 {
log.Debugf("Windows 8\n")
os = WIN8
} else {
//log.Debugf("Windows 8.1\n")
os = WIN81
}
} else if (build >= 10000) && (build < 18363) {
log.Debugf("Windows 10\n")
os = WIN10
} else {
if (version < 5.2) && (version > 5.0) {
os = WINXP
log.Debugf("Windows XP?\n")
} else {
func GetOSVersion(currentBuild int, currentVersion float64, server bool) (os byte) {
currentVersionStr := strconv.FormatFloat(currentVersion, 'f', 1, 64)
if server {
switch {
case currentBuild >= 3790 && currentBuild < 6001:
os = WIN_SERVER_2003
case currentBuild >= 6001 && currentBuild < 7601:
os = WIN_SERVER_2008
case currentBuild >= 7601 && currentBuild < 9200:
os = WIN_SERVER_2008_R2
case currentBuild >= 9200 && currentBuild < 9600:
os = WIN_SERVER_2012
case currentBuild >= 9200 && currentBuild < 14393:
os = WIN_SERVER_2012_R2
case currentBuild >= 14393 && currentBuild < 17763:
os = WIN_SERVER_2016
case currentBuild >= 17763 && currentBuild < 20348:
os = WIN_SERVER_2019
case currentBuild >= 20348:
os = WIN_SERVER_2022
default:
log.Debugf("Unknown server version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion)
os = WIN_UNKNOWN
}
} else {
switch currentVersionStr {
case "5.1":
os = WINXP
case "6.0":
// Windows Vista but it shares CurrentVersion and CurrentBuild with Windows Server 2008
os = WIN_VISTA
case "6.1":
// Windows 7 but it shares CurrentVersion and CurrentBuild with Windows Server 2008 R2
os = WIN7
case "6.2":
// Windows 8 but it shares CurrentVersion and CurrentBuild with Windows Server 2012
os = WIN8
case "6.3":
// Windows 8.1 but it shares CurrentVersion and CurrentBuild with Windows Server 2012 R2
os = WIN81
case "10.0":
if currentBuild < 22000 {
os = WIN10
} else {
os = WIN11
}
default:
log.Debugf("Unknown version of Windows with CurrentBuild %d and CurrentVersion %f\n", currentBuild, currentVersion)
os = WIN_UNKNOWN
log.Debugf("Unknown OS\n")
}
}
log.Debugf("OS Version: %d\n", os)
log.Debugf("OS Version: %s\n", osNameMap[os])
return
}
func GetOsVersionName(build int, version float64, inErr error) string {
if inErr != nil {
log.Errorln(inErr)
return "Unknown OS"
}
os, err := GetOSVersion(build, version)
if err != nil {
log.Errorln(err)
return "Unknown OS"
}
result := "Unknown OS"
switch os {
case WIN_UNKNOWN:
break
case WINXP:
result = "Windows XP"
case WIN7:
result = "Windows 7"
case WIN8:
result = "Windows 8"
case WIN81:
result = "Windows 8.1"
case WIN10:
result = "Windows 10"
}
return result
}
func IsWin10After1607(build int, version float64, inErr error) (value bool, err error) {
if inErr != nil {
return false, inErr
}
func IsWin10After1607(build int, version float64) (value bool, err error) {
if build >= 14393 {
value = true
} else {
@@ -111,14 +137,8 @@ func IsWin10After1607(build int, version float64, inErr error) (value bool, err
return
}
func IsBetweenWinXPWin10(build int, version float64, inErr error) (value bool, err error) {
if inErr != nil {
return false, inErr
}
os, err := GetOSVersion(build, version)
if err != nil {
return
}
func IsBetweenWinXPWin10(build int, version float64, isServer bool) (value bool, err error) {
os := GetOSVersion(build, version, isServer)
if (WINXP <= os) && (os <= WIN10) {
value = true
} else {