Skip to content

Commit 4630f9e

Browse files
v1.2.0-dev; 2025-09-14.1600
1 parent de3f26c commit 4630f9e

File tree

2 files changed

+149
-85
lines changed

2 files changed

+149
-85
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### v1.2.0-dev; 2025-09-13.2100
1+
### v1.2.0-dev; 2025-09-14.1600
22
```
33
addressed raw base-16 issue https://github.com/cyclone-github/hashgen/issues/8
44
added feature: "keep-order" from https://github.com/cyclone-github/hashgen/issues/7
@@ -7,6 +7,10 @@ add modes: mysql5 (300), phpass (400), md5crypt (500), sha256crypt (7400), sha51
77
added hashcat salted modes: -m 10, 20, 110, 120, 1410, 1420, 1310, 1320, 1710, 1720, 10810, 10820
88
added hashcat modes: -m 2600, 4500
99
cleaned up hashFunc aliases, algo typo, and hex mode
10+
fixed ntlm encoding issue
11+
added sanity check to not print blank / invalid hashes (part of ntlm fix, but applies to all hash modes)
12+
converted checkForHex from string to byte
13+
updated yescrypt defaults to match debian 12 (libxcrypt)
1014
```
1115
### v1.1.4; 2025-08-23
1216
```

hashgen.go

Lines changed: 144 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@ import (
1818
"hash/crc64"
1919
"io"
2020
"log"
21+
"math/bits"
2122
"os"
2223
"runtime"
23-
"strings"
2424
"sync"
2525
"sync/atomic"
2626
"time"
2727
"unicode/utf16"
28+
"unicode/utf8"
2829

2930
"github.com/cyclone-github/base58"
3031
"github.com/ebfe/keccak" // keccak 224/384
@@ -79,24 +80,28 @@ v1.1.2; 2025-04-08
7980
v1.1.3; 2025-06-30
8081
added mode "hex" for $HEX[] formatted output
8182
added alias "dehex" to "plaintext" mode
82-
improved "plaintext/dehex" logic to decode both $HEX[] --> and raw base-16 input <-- (removed decoding raw base 16, see changes for v1.1.5)
83+
improved "plaintext/dehex" logic to decode both $HEX[] --> and raw base-16 input <-- (removed decoding raw base 16, see changes for v1.2.0)
8384
v1.1.4; 2025-08-23
8485
added modes: keccak-224, keccak-384, blake2b-256, blake2b-384, blake2b-512, blake2c-256
8586
added benchmark flag, -b (to benchmark current mode, disables output)
8687
compiled with Go v1.25.0 which gives a small performance boost to multiple algos
8788
added notes concerning some NTLM hashes not being crackable with certain hash cracking tools due to encoding gremlins
88-
v1.2.0-dev; 2025-09-13.2245
89+
v1.2.0-dev; 2025-09-14.1600
8990
addressed raw base-16 issue https://github.com/cyclone-github/hashgen/issues/8
9091
added feature: "keep-order" from https://github.com/cyclone-github/hashgen/issues/7
9192
added dynamic lines/sec from https://github.com/cyclone-github/hashgen/issues/11
9293
add modes: mysql5 (300), phpass (400), md5crypt (500), sha256crypt (7400), sha512crypt (1800), Wordpress bcrypt-HMAC-SHA384 (wpbcrypt)
9394
added hashcat salted modes: -m 10, 20, 110, 120, 1410, 1420, 1310, 1320, 1710, 1720, 10810, 10820
9495
added hashcat modes: -m 2600, 4500
9596
cleaned up hashFunc aliases, algo typo, and hex mode
97+
fixed ntlm encoding issue
98+
added sanity check to not print blank / invalid hashes (part of ntlm fix, but applies to all hash modes)
99+
converted checkForHex from string to byte
100+
updated yescrypt defaults to match debian 12 (libxcrypt)
96101
*/
97102

98103
func versionFunc() {
99-
fmt.Fprintln(os.Stderr, "hashgen v1.2.0-dev; 2025-09-13.2245\nhttps://github.com/cyclone-github/hashgen")
104+
fmt.Fprintln(os.Stderr, "hashgen v1.2.0-dev; 2025-09-14.1600\nhttps://github.com/cyclone-github/hashgen")
100105
}
101106

102107
// help function
@@ -184,54 +189,78 @@ if your wordlist contains HEX strings that resemble alphabet soup, don't be surp
184189
the best way to fix HEX decoding issues is to correctly parse your wordlists so you don't end up with foobar HEX strings
185190
if you have suggestions on how to better handle HEX decoding errors, contact me on github
186191
*/
187-
func checkForHex(line string) ([]byte, string, int) {
192+
func checkForHex(line []byte) ([]byte, []byte, int) {
188193
// check if line is in $HEX[] format
189-
if strings.HasPrefix(line, "$HEX[") {
194+
const prefix = "$HEX["
195+
if len(line) >= len(prefix) && bytes.HasPrefix(line, []byte(prefix)) {
190196
// attempt to correct improperly formatted $HEX[] entries
191197
// if it doesn't end with "]", add the missing bracket
192198
var hexErrorDetected int
193-
if !strings.HasSuffix(line, "]") {
194-
line += "]" // add missing trailing "]"
199+
hasClose := bytes.HasSuffix(line, []byte("]"))
200+
if !hasClose {
195201
hexErrorDetected = 1 // mark as error since the format was corrected
196202
}
197203

198204
// find first '[' and last ']'
199-
startIdx := strings.Index(line, "[")
200-
endIdx := strings.LastIndex(line, "]")
205+
startIdx := bytes.IndexByte(line, '[')
206+
endIdx := bytes.LastIndexByte(line, ']')
207+
if endIdx == -1 {
208+
endIdx = len(line) // pretend ']' is at end
209+
}
201210
hexContent := line[startIdx+1 : endIdx]
202211

203212
// decode hex content into bytes
204-
decodedBytes, err := hex.DecodeString(hexContent)
205-
// error handling
206-
if err != nil {
207-
hexErrorDetected = 1 // mark as error since there was an issue decoding
208-
209-
// remove blank spaces and invalid hex characters
210-
cleanedHexContent := strings.Map(func(r rune) rune {
211-
if strings.ContainsRune("0123456789abcdefABCDEF", r) {
212-
return r
213-
}
214-
return -1 // remove invalid hex character
215-
}, hexContent)
216-
217-
// if hex has an odd length, add a zero nibble to make it even
218-
if len(cleanedHexContent)%2 != 0 {
219-
cleanedHexContent = "0" + cleanedHexContent
213+
var decodedBytes []byte
214+
if n := len(hexContent); n > 0 && (n&1) == 0 {
215+
decodedBytes = make([]byte, n/2)
216+
if _, err := hex.Decode(decodedBytes, hexContent); err == nil {
217+
disp := make([]byte, 5+len(hexContent)+1) // "$HEX[" + hex + "]"
218+
copy(disp, prefix)
219+
copy(disp[5:], hexContent)
220+
disp[len(disp)-1] = ']'
221+
return decodedBytes, disp, hexErrorDetected
220222
}
223+
hexErrorDetected = 1
224+
} else {
225+
hexErrorDetected = 1
226+
}
221227

222-
decodedBytes, err = hex.DecodeString(cleanedHexContent)
223-
if err != nil {
224-
log.Printf("Error decoding $HEX[] content: %v", err)
225-
// if decoding still fails, return original line as bytes
226-
return []byte(line), line, hexErrorDetected
228+
// error handling: remove invalid hex chars
229+
clean := make([]byte, 0, len(hexContent))
230+
for _, c := range hexContent {
231+
lc := c | 0x20
232+
if (c >= '0' && c <= '9') || (lc >= 'a' && lc <= 'f') {
233+
clean = append(clean, c)
227234
}
228235
}
236+
// if hex has an odd length, add a zero nibble to make it even
237+
if len(clean)%2 != 0 {
238+
clean = append([]byte{'0'}, clean...)
239+
}
240+
241+
decodedBytes = make([]byte, len(clean)/2)
242+
if len(clean) == 0 || func() bool {
243+
_, err := hex.Decode(decodedBytes, clean)
244+
return err != nil
245+
}() {
246+
log.Printf("Error decoding $HEX[] content")
247+
// if decoding still fails, return original line as bytes
248+
disp := make([]byte, 5+len(hexContent)+1)
249+
copy(disp, prefix)
250+
copy(disp[5:], hexContent)
251+
disp[len(disp)-1] = ']'
252+
return line, disp, hexErrorDetected
253+
}
229254

230255
// return decoded bytes and formatted hex content
231-
return decodedBytes, "$HEX[" + hexContent + "]", hexErrorDetected
256+
disp := make([]byte, 5+len(hexContent)+1)
257+
copy(disp, prefix)
258+
copy(disp[5:], hexContent)
259+
disp[len(disp)-1] = ']'
260+
return decodedBytes, disp, hexErrorDetected
232261
}
233262
// return original line as bytes if not in $HEX[] format
234-
return []byte(line), line, 0
263+
return line, line, 0
235264
}
236265

237266
// ITU-R M.1677-1 standard morse code mapping
@@ -742,6 +771,62 @@ func wpbcrypt(password []byte, cost int) string {
742771
return wpPrefix + s[1:]
743772
}
744773

774+
// yescrypt, using debian/libxcrypt defaults
775+
func yescryptHash(pass []byte) string {
776+
// debian/libxcrypt defaults: N=4096, r=32, p=1, keyLen=32, 128-bit salt
777+
const N = 4096
778+
const r = 32
779+
const p = 1
780+
const keyLen = 32
781+
const saltLen = 16
782+
783+
// salt
784+
salt := make([]byte, saltLen)
785+
if _, err := rand.Read(salt); err != nil {
786+
fmt.Fprintln(os.Stderr, "yescrypt: salt error:", err)
787+
return ""
788+
}
789+
790+
// derive
791+
key, err := yescrypt.Key(pass, salt, N, r, p, keyLen)
792+
if err != nil {
793+
fmt.Fprintln(os.Stderr, "yescrypt:", err)
794+
return ""
795+
}
796+
797+
// crypt-base64 encoder (./0-9A-Za-z)
798+
encode64 := func(src []byte) string {
799+
var dst []byte
800+
var v uint32
801+
bitsAcc := 0
802+
for i := 0; i < len(src); i++ {
803+
v |= uint32(src[i]) << bitsAcc
804+
bitsAcc += 8
805+
for bitsAcc >= 6 {
806+
dst = append(dst, cryptBase64[v&0x3f])
807+
v >>= 6
808+
bitsAcc -= 6
809+
}
810+
}
811+
if bitsAcc > 0 {
812+
dst = append(dst, cryptBase64[v&0x3f])
813+
}
814+
return string(dst)
815+
}
816+
817+
// params field:
818+
// flags 'j' (YESCRYPT_DEFAULTS), then log2(N) and r, both encoded 1-based in crypt-base64
819+
ln := bits.TrailingZeros(uint(N)) // N must be power of two
820+
if 1<<ln != N || r <= 0 {
821+
fmt.Fprintln(os.Stderr, "yescrypt: invalid N/r")
822+
return ""
823+
}
824+
params := []byte{'j', cryptBase64[(ln-1)&0x3f], cryptBase64[(r-1)&0x3f]}
825+
826+
// assemble
827+
return "$y$" + string(params) + "$" + encode64(salt) + "$" + encode64(key)
828+
}
829+
745830
// supported hash algos / modes
746831
func hashBytes(hashFunc string, data []byte, cost int) string {
747832
// random salt gen
@@ -852,37 +937,7 @@ func hashBytes(hashFunc string, data []byte, cost int) string {
852937
return string(buf)
853938
// yescrypt
854939
case "yescrypt":
855-
salt := make([]byte, 8) // random 8-byte salt
856-
if _, err := rand.Read(salt); err != nil {
857-
fmt.Fprintln(os.Stderr, "Error generating salt:", err)
858-
return ""
859-
}
860-
key, err := yescrypt.Key(data, salt, 32768, 8, 1, 32) // use default yescrypt parameters: N=32768, r=8, p=1, keyLen=32
861-
if err != nil {
862-
fmt.Fprintln(os.Stderr, "yescrypt error:", err)
863-
return ""
864-
}
865-
encode64 := func(src []byte) string {
866-
var dst []byte
867-
var value uint32
868-
bits := 0
869-
for i := 0; i < len(src); i++ {
870-
value |= uint32(src[i]) << bits
871-
bits += 8
872-
for bits >= 6 {
873-
dst = append(dst, cryptBase64[value&0x3f])
874-
value >>= 6
875-
bits -= 6
876-
}
877-
}
878-
if bits > 0 {
879-
dst = append(dst, cryptBase64[value&0x3f])
880-
}
881-
return string(dst)
882-
}
883-
encodedSalt := encode64(salt)
884-
encodedKey := encode64(key)
885-
return fmt.Sprintf("$y$jC5$%s$%s", encodedSalt, encodedKey)
940+
return yescryptHash(data)
886941
// argon2id
887942
case "argon2id", "34000":
888943
salt := make([]byte, 16) // random 16-byte salt
@@ -1102,23 +1157,24 @@ func hashBytes(hashFunc string, data []byte, cost int) string {
11021157
// md5crypt -m 500
11031158
case "md5crypt", "500":
11041159
return md5crypt(data)
1105-
// ntlm -m 1000
1160+
// ntlm -m 1000 (strict: skip invalid UTF-8 / UTF-16)
11061161
case "ntlm", "1000":
1162+
var rs []rune
1163+
for i := 0; i < len(data); {
1164+
r, sz := utf8.DecodeRune(data[i:])
1165+
if r == utf8.RuneError && sz == 1 {
1166+
return ""
1167+
}
1168+
if r >= 0xD800 && r <= 0xDFFF {
1169+
return ""
1170+
}
1171+
rs = append(rs, r)
1172+
i += sz
1173+
}
1174+
u16 := utf16.Encode(rs)
11071175
h := md4.New()
1108-
// convert byte slice to string assuming UTF-8, then encode as UTF-16LE
1109-
// this may not work as expected if plaintext contains non-ASCII/UTF-8 encoding
1110-
// due to encoding gremlins, not all NTLM hashes generated with hashgen are recoverable
1111-
// recovery test results on rockyou.txt (14,344,391 lines):
1112-
// mdxfind recovered: 99.998% missed: 218 / 14,344,391
1113-
// hashpwn recovered: 99.993% missed: 1,025 / 14,344,391
1114-
// jtr recovered: 99.961% missed: 5,631 / 14,344,391
1115-
// hashcat recovered: 99.862% missed: 19,824 / 14,344,391
1116-
input := utf16.Encode([]rune(strings.ToValidUTF8(string(data), ""))) // convert byte slice to string, then to rune slice
1117-
if err := binary.Write(h, binary.LittleEndian, input); err != nil {
1118-
panic("Failed NTLM hashing")
1119-
}
1120-
hashBytes := h.Sum(nil)
1121-
return hex.EncodeToString(hashBytes)
1176+
_ = binary.Write(h, binary.LittleEndian, u16)
1177+
return hex.EncodeToString(h.Sum(nil))
11221178
// blake2b-256 (raw hex)
11231179
case "blake2b-256", "blake2b256":
11241180
h := blake2b.Sum256(data)
@@ -1193,18 +1249,22 @@ func processChunk(chunk []byte, count *int64, hexErrorCount *int64, hashFunc str
11931249
reader := bytes.NewReader(chunk)
11941250
scanner := bufio.NewScanner(reader)
11951251
for scanner.Scan() {
1196-
line := scanner.Text()
1197-
decodedBytes, hexContent, hexErrCount := checkForHex(line)
1252+
lineBytes := scanner.Bytes()
1253+
decodedBytes, hexContent, hexErrCount := checkForHex(lineBytes)
11981254
hash := hashBytes(hashFunc, decodedBytes, cost)
1255+
if hash == "" {
1256+
continue
1257+
} // skip empty lines
11991258
writer.WriteString(hash)
12001259
if hashPlainOutput {
1201-
writer.WriteString(":" + hexContent)
1260+
_ = writer.WriteByte(':')
1261+
_, _ = writer.Write(hexContent)
12021262
}
1203-
writer.WriteString("\n")
1263+
_ = writer.WriteByte('\n')
12041264
atomic.AddInt64(count, 1) // line count
12051265
atomic.AddInt64(hexErrorCount, int64(hexErrCount)) // hex error count
12061266
}
1207-
writer.Flush()
1267+
_ = writer.Flush()
12081268
}
12091269

12101270
// process logic

0 commit comments

Comments
 (0)