From d740b5bc838e76b34f0f1b155207e5fbc32524ea Mon Sep 17 00:00:00 2001 From: flashlam Date: Wed, 25 Sep 2024 16:58:52 +0200 Subject: [PATCH] added -wordlist option --- README.md | 6 ++-- hash.go | 89 +++++++++++++++++++++++++++++++++++++++---------------- main.go | 17 +++++++++++ 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index e3460f51..78d5bbcc 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ The tool wraps calls to the Go compiler and linker to transform the Go build, in order to: * Replace as many useful identifiers as possible with short base64 hashes -* Replace package paths with short base64 hashes -* Replace filenames and position information with short base64 hashes +* Replace package paths with short base64 hashes or [words](#wordlist) +* Replace filenames and position information with short base64 hashes or [words](#wordlist) * Remove all [build](https://go.dev/pkg/runtime/#Version) and [module](https://go.dev/pkg/runtime/debug/#ReadBuildInfo) information * Strip debugging information and symbol tables via `-ldflags="-w -s"` * [Obfuscate literals](#literal-obfuscation), if the `-literals` flag is given @@ -187,6 +187,8 @@ to document the current shortcomings of this tool. * Garble requires `git` to patch the linker. That can be avoided once go-gitdiff supports [non-strict patches](https://github.com/bluekeyes/go-gitdiff/issues/30). +* Wordlist mode can sometimes crash when the wordlist is too small since it creates collisions between names. + ### Contributing We welcome new contributors. If you would like to contribute, see diff --git a/hash.go b/hash.go index e7def1f1..d9b0df9e 100644 --- a/hash.go +++ b/hash.go @@ -6,6 +6,7 @@ package main import ( "bytes" "crypto/sha256" + "hash/fnv" "encoding/base64" "encoding/binary" "fmt" @@ -18,8 +19,19 @@ import ( "mvdan.cc/garble/internal/literals" ) +// Global variables for tracking assigned replacements and used words. +var Assigned = make(map[string]string) +var Used = make(map[string]bool) + const buildIDSeparator = "/" +func fnvHash(salt []byte, name string) uint64 { + hasher := fnv.New64a() + hasher.Write(salt) + hasher.Write([]byte(name)) + return hasher.Sum64() +} + // splitActionID returns the action ID half of a build ID, the first hash. func splitActionID(buildID string) string { return buildID[:strings.Index(buildID, buildIDSeparator)] @@ -158,6 +170,10 @@ func appendFlags(w io.Writer, forBuildHash bool) { io.WriteString(w, " -seed=") io.WriteString(w, flagSeed.String()) } + if flagWordlist != "" { + io.WriteString(w, " -wordlist=") + io.WriteString(w, flagWordlist) + } if flagControlFlow && forBuildHash { io.WriteString(w, " -ctrlflow") } @@ -363,53 +379,74 @@ func hashWithCustomSalt(salt []byte, name string) string { panic("hashWithCustomSalt: empty name") } - hasher.Reset() - hasher.Write(salt) - hasher.Write(flagSeed.bytes) - io.WriteString(hasher, name) - sum := hasher.Sum(sumBuffer[:0]) - - // The byte after neededSumBytes is never used as part of the name, - // but it is still deterministic and hard to predict, - // so it provides us with useful randomness between 0 and 255. - // We want the number to be between 0 and hashLenthRange-1 as well, - // so we use a remainder operation. - hashLengthRandomness := sum[neededSumBytes] % ((maxHashLength - minHashLength) + 1) - hashLength := minHashLength + hashLengthRandomness + var output []byte - nameBase64.Encode(b64NameBuffer[:], sum[:neededSumBytes]) - b64Name := b64NameBuffer[:hashLength] + if len(Wordlist) == 0 { + hasher.Reset() + hasher.Write(salt) + hasher.Write(flagSeed.bytes) + io.WriteString(hasher, name) + sum := hasher.Sum(sumBuffer[:0]) + + // The byte after neededSumBytes is never used as part of the name, + // but it is still deterministic and hard to predict, + // so it provides us with useful randomness between 0 and 255. + // We want the number to be between 0 and hashLenthRange-1 as well, + // so we use a remainder operation. + hashLengthRandomness := sum[neededSumBytes] % ((maxHashLength - minHashLength) + 1) + hashLength := minHashLength + hashLengthRandomness + + nameBase64.Encode(b64NameBuffer[:], sum[:neededSumBytes]) + output = b64NameBuffer[:hashLength] + + } else{ + // Determine the number of words you want to use + numWords := 3 + + // Get fnv hash value from salt and name + hashValue := fnvHash(salt, name) + + // Select words from the wordlist based on the hash value + selectedWords := make([]string, numWords) + for i := 0; i < numWords; i++ { + // Use parts of the hash to index into the wordlist + index := int((hashValue >> (i * 16)) & 0xFFFF) % len(Wordlist) + selectedWords[i] = strings.Title(strings.ToLower(Wordlist[index])) + } + selectedWords[0] = strings.ToLower(selectedWords[0]) + output = []byte(strings.Join(selectedWords, "")) + } // Even if we are hashing a package path, which is not an identifier, // we still want the result to be a valid identifier, // since we'll use it as the package name too. - if isDigit(b64Name[0]) { + if isDigit(output[0]) { // Turn "3foo" into "Dfoo". // Similar to toLower, since uppercase letters go after digits // in the ASCII table. - b64Name[0] += 'A' - '0' + output[0] += 'A' - '0' } - for i, b := range b64Name { + for i, b := range output { if b == '-' { // URL encoding uses dashes, which aren't valid - b64Name[i] = 'a' + output[i] = 'a' } } // Valid identifiers should stay exported or unexported. if token.IsIdentifier(name) { if token.IsExported(name) { - if b64Name[0] == '_' { + if output[0] == '_' { // Turn "_foo" into "Zfoo". - b64Name[0] = 'Z' - } else if isLower(b64Name[0]) { + output[0] = 'Z' + } else if isLower(output[0]) { // Turn "afoo" into "Afoo". - b64Name[0] = toUpper(b64Name[0]) + output[0] = toUpper(output[0]) } } else { - if isUpper(b64Name[0]) { + if isUpper(output[0]) { // Turn "Afoo" into "afoo". - b64Name[0] = toLower(b64Name[0]) + output[0] = toLower(output[0]) } } } - return string(b64Name) + return string(output) } diff --git a/main.go b/main.go index 45a7f433..9c3953e1 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,7 @@ import ( "time" "unicode" "unicode/utf8" + "io/ioutil" "github.com/rogpeppe/go-internal/cache" "golang.org/x/exp/maps" @@ -50,6 +51,8 @@ import ( "mvdan.cc/garble/internal/literals" ) +var Wordlist []string + var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError) var ( @@ -57,6 +60,7 @@ var ( flagTiny bool flagDebug bool flagDebugDir string + flagWordlist string flagSeed seedFlag // TODO(pagran): in the future, when control flow obfuscation will be stable migrate to flag flagControlFlow = os.Getenv("GARBLE_EXPERIMENTAL_CONTROLFLOW") == "1" @@ -67,6 +71,7 @@ func init() { flagSet.BoolVar(&flagLiterals, "literals", false, "Obfuscate literals such as strings") flagSet.BoolVar(&flagTiny, "tiny", false, "Optimize for binary size, losing some ability to reverse the process") flagSet.BoolVar(&flagDebug, "debug", false, "Print debug logs to stderr") + flagSet.StringVar(&flagWordlist, "wordlist", "", "Wordlist to use for obfuscation, e.g. -wordlist=wordlist.txt") flagSet.StringVar(&flagDebugDir, "debugdir", "", "Write the obfuscated source to a directory, e.g. -debugdir=out") flagSet.Var(&flagSeed, "seed", "Provide a base64-encoded seed, e.g. -seed=o9WDTZ4CN4w\nFor a random seed, provide -seed=random") } @@ -245,6 +250,18 @@ func main1() int { return 2 } + if flagWordlist != "" { + wd, err := os.Getwd() + if !filepath.IsAbs(flagWordlist) { + flagWordlist = filepath.Join(wd, flagWordlist) + } + data, err := ioutil.ReadFile(flagWordlist) + if err != nil { + log.Fatalf("failed to read wordlist: %v", err) + } + Wordlist = strings.Split(string(data), "\n") + } + // If a random seed was used, the user won't be able to reproduce the // same output or failure unless we print the random seed we chose. // If the build failed and a random seed was used,