Skip to content

Commit

Permalink
added -wordlist option
Browse files Browse the repository at this point in the history
  • Loading branch information
flashlam committed Sep 25, 2024
1 parent 48dd226 commit d740b5b
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 28 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
89 changes: 63 additions & 26 deletions hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package main
import (
"bytes"
"crypto/sha256"
"hash/fnv"
"encoding/base64"
"encoding/binary"
"fmt"
Expand All @@ -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)]
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
}
17 changes: 17 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import (
"time"
"unicode"
"unicode/utf8"
"io/ioutil"

"github.com/rogpeppe/go-internal/cache"
"golang.org/x/exp/maps"
Expand All @@ -50,13 +51,16 @@ import (
"mvdan.cc/garble/internal/literals"
)

var Wordlist []string

var flagSet = flag.NewFlagSet("garble", flag.ContinueOnError)

var (
flagLiterals bool
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"
Expand All @@ -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")
}
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit d740b5b

Please sign in to comment.