Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

random magic value using modified cmd/link alternative #628

Merged
merged 8 commits into from
Jan 8, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ to document the current shortcomings of this tool.
```

* Go plugins are not currently supported; see [#87](https://github.com/burrowers/garble/issues/87).
* 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).

### Contributing

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module mvdan.cc/garble
go 1.19

require (
github.com/bluekeyes/go-gitdiff v0.7.0
github.com/frankban/quicktest v1.14.3
github.com/google/go-cmp v0.5.8
github.com/rogpeppe/go-internal v1.9.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/bluekeyes/go-gitdiff v0.7.0 h1:w4SrRFcufU0/tEpWx3VurDBAnWfpxsmwS7yWr14meQk=
github.com/bluekeyes/go-gitdiff v0.7.0/go.mod h1:QpfYYO1E0fTVHVZAZKiRjtSGY9823iCdvGXBcEzHGbM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE=
github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
Expand Down
14 changes: 14 additions & 0 deletions hash.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"fmt"
"go/token"
"go/types"
Expand Down Expand Up @@ -189,6 +190,19 @@ func isUpper(b byte) bool { return 'A' <= b && b <= 'Z' }
func toLower(b byte) byte { return b + ('a' - 'A') }
func toUpper(b byte) byte { return b - ('a' - 'A') }

// magicValue returns random magic value based
// on user specified seed or the runtime package's GarbleActionID.
func magicValue() uint32 {
hasher.Reset()
if !flagSeed.present() {
hasher.Write(cache.ListedPackages["runtime"].GarbleActionID)
} else {
hasher.Write(flagSeed.bytes)
}
sum := hasher.Sum(sumBuffer[:0])
return binary.LittleEndian.Uint32(sum)
}

func hashWithPackage(pkg *listedPackage, name string) string {
if !flagSeed.present() {
return hashWithCustomSalt(pkg.GarbleActionID, name)
Expand Down
250 changes: 250 additions & 0 deletions internal/linker/linker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
// Copyright (c) 2022, The Garble Authors.
// See LICENSE for licensing information.

package linker

import (
"bytes"
"crypto/sha256"
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/rogpeppe/go-internal/lockedfile"
)

const (
MagicValueEnv = "GARBLE_LINKER_MAGIC"

cacheDirName = "garble"
versionExt = ".version"
garbleCacheDir = "GARBLE_CACHE_DIR"
baseSrcSubdir = "src"
)

var (
//go:embed patches/*.patch
linkerPatchesFS embed.FS
)

func loadLinkerPatches() (string, map[string]string, error) {
versionHash := sha256.New()
patches := make(map[string]string)
err := fs.WalkDir(linkerPatchesFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}

patchBytes, err := linkerPatchesFS.ReadFile(path)
if err != nil {
return err
}

if _, err := versionHash.Write(patchBytes); err != nil {
return err
}

files, _, err := gitdiff.Parse(bytes.NewReader(patchBytes))
if err != nil {
return err
}
for _, file := range files {
if file.IsNew || file.IsDelete || file.IsCopy || file.IsRename {
panic("only modification patch is supported")
}
patches[file.OldName] = string(patchBytes)
}
return nil
})

if err != nil {
return "", nil, err
}
return base64.RawStdEncoding.EncodeToString(versionHash.Sum(nil)), patches, nil
}

// TODO(pagran): Remove git dependency in future
// more information in README.md
func applyPatch(workingDir, patch string) error {
cmd := exec.Command("git", "-C", workingDir, "apply")
cmd.Stdin = strings.NewReader(patch)
return cmd.Run()
}

func copyFile(src, target string) error {
targetDir := filepath.Dir(target)
if err := os.MkdirAll(targetDir, 0o777); err != nil {
return err
}
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()

targetFile, err := os.Create(target)
if err != nil {
return err
}
defer targetFile.Close()
_, err = io.Copy(targetFile, srcFile)
return err
}

func fileExists(path string) bool {
stat, err := os.Stat(path)
if err != nil {
return false
}
return !stat.IsDir()
}

func applyPatches(srcDir, workingDir string, patches map[string]string) (map[string]string, error) {
mod := make(map[string]string)
for fileName, patch := range patches {
oldPath := filepath.Join(srcDir, fileName)
newPath := filepath.Join(workingDir, fileName)
mod[oldPath] = newPath

if err := copyFile(oldPath, newPath); err != nil {
return nil, err
}

if err := applyPatch(workingDir, patch); err != nil {
return nil, fmt.Errorf("apply patch for %s failed: %v", fileName, err)
}
}
return mod, nil
}

func cachePath(goExe string) (string, error) {
var cacheDir string
if val, ok := os.LookupEnv(garbleCacheDir); ok {
cacheDir = val
} else {
userCacheDir, err := os.UserCacheDir()
if err != nil {
panic(fmt.Errorf("cannot retreive user cache directory: %v", err))
}
cacheDir = userCacheDir
}

cacheDir = filepath.Join(cacheDir, cacheDirName)
if err := os.MkdirAll(cacheDir, 0o777); err != nil {
return "", err
}

// Note that we only keep one patched and built linker in the cache.
// If the user switches between Go versions or garble versions often,
// this may result in rebuilds since we don't keep multiple binaries in the cache.
// We can consider keeping multiple versions of the binary in our cache in the future,
// similar to how GOCACHE works with multiple built versions of the same package.
return filepath.Join(cacheDir, "link"+goExe), nil
pagran marked this conversation as resolved.
Show resolved Hide resolved
}

func getCurrentVersion(goVersion, patchesVer string) string {
return goVersion + " " + patchesVer
}

func checkVersion(linkerPath, goVersion, patchesVer string) (bool, error) {
versionPath := linkerPath + versionExt
version, err := os.ReadFile(versionPath)
if os.IsNotExist(err) {
return false, nil
}
if err != nil {
return false, err
}

return string(version) == getCurrentVersion(goVersion, patchesVer), nil
}

func writeVersion(linkerPath, goVersion, patchesVer string) error {
versionPath := linkerPath + versionExt
return os.WriteFile(versionPath, []byte(getCurrentVersion(goVersion, patchesVer)), 0o777)
}

func buildLinker(workingDir string, overlay map[string]string, outputLinkPath string) error {
file, err := json.Marshal(&struct{ Replace map[string]string }{overlay})
if err != nil {
return err
}
overlayPath := filepath.Join(workingDir, "overlay.json")
if err := os.WriteFile(overlayPath, file, 0o777); err != nil {
return err
}

cmd := exec.Command("go", "build", "-overlay", overlayPath, "-o", outputLinkPath, "cmd/link")
// Explicitly setting GOOS and GOARCH variables prevents conflicts during cross-build
cmd.Env = append(os.Environ(), "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)

out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("compiler compile error: %v\n\n%s", err, string(out))
}

return nil
}

func PatchLinker(goRoot, goVersion, goExe, tempDir string) (string, func(), error) {
patchesVer, patches, err := loadLinkerPatches()
if err != nil {
panic(fmt.Errorf("cannot retrieve linker patches: %v", err))
}

outputLinkPath, err := cachePath(goExe)
if err != nil {
return "", nil, err
}

mutex := lockedfile.MutexAt(outputLinkPath + ".lock")
unlock, err := mutex.Lock()
if err != nil {
return "", nil, err
}

// If build is successful, mutex unlocking must be on the caller's side
successBuild := false
defer func() {
if !successBuild {
unlock()
}
}()

isCorrectVer, err := checkVersion(outputLinkPath, goVersion, patchesVer)
if err != nil {
return "", nil, err
}
if isCorrectVer && fileExists(outputLinkPath) {
successBuild = true
return outputLinkPath, unlock, nil
}

srcDir := filepath.Join(goRoot, baseSrcSubdir)
workingDir := filepath.Join(tempDir, "linker-src")

overlay, err := applyPatches(srcDir, workingDir, patches)
if err != nil {
return "", nil, err
}
if err := buildLinker(workingDir, overlay, outputLinkPath); err != nil {
return "", nil, err
}
if err := writeVersion(outputLinkPath, goVersion, patchesVer); err != nil {
return "", nil, err
}
successBuild = true
return outputLinkPath, unlock, nil
}
20 changes: 20 additions & 0 deletions internal/linker/patches/00-dynamic-magic-value.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
diff --git a/link/internal/ld/pcln.go b/link/internal/ld/pcln.go
--- a/cmd/link/internal/ld/pcln.go
+++ b/cmd/link/internal/ld/pcln.go
@@ -239,6 +239,16 @@
if off != size {
panic(fmt.Sprintf("pcHeader size: %d != %d", off, size))
}
+
+ if val, ok := os.LookupEnv("GARBLE_LINKER_MAGIC"); ok {
+ var magicVal uint32
+ // Use fmt package instead of strconv to avoid importing a new package
+ if _, err := fmt.Sscan(val, &magicVal); err != nil {
+ panic(fmt.Errorf("[garble] invalid magic value %s: %v", val, err))
+ }
+
+ header.SetUint32(ctxt.Arch, 0, magicVal)
+ }
}

state.pcheader = state.addGeneratedSym(ctxt, "runtime.pcheader", size, writeHeader)
40 changes: 34 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"golang.org/x/mod/semver"
"golang.org/x/tools/go/ast/astutil"

"mvdan.cc/garble/internal/linker"
"mvdan.cc/garble/internal/literals"
)

Expand Down Expand Up @@ -436,7 +437,29 @@ func mainErr(args []string) error {
} else {
log.Printf("skipping transform on %s with args: %s", tool, strings.Join(transformed, " "))
}
cmd := exec.Command(args[0], transformed...)

var linkUnlock func()
defer func() {
if linkUnlock != nil {
linkUnlock()
}
}()

executablePath := args[0]
if tool == "link" {
modifiedLinkPath, unlock, err := linker.PatchLinker(cache.GoEnv.GOROOT, cache.GoEnv.GOVERSION, cache.GoEnv.GOEXE, sharedTempDir)
if err != nil {
return fmt.Errorf("cannot get modified linker: %v", err)
}

linkUnlock = unlock
mvdan marked this conversation as resolved.
Show resolved Hide resolved
executablePath = modifiedLinkPath
os.Setenv(linker.MagicValueEnv, strconv.FormatUint(uint64(magicValue()), 10))

log.Printf("replaced linker with: %s", executablePath)
}

cmd := exec.Command(executablePath, transformed...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
Expand Down Expand Up @@ -878,10 +901,15 @@ func transformCompile(args []string) ([]string, error) {
for i, file := range files {
basename := filepath.Base(paths[i])
log.Printf("obfuscating %s", basename)
if curPkg.ImportPath == "runtime" && flagTiny {
// strip unneeded runtime code
stripRuntime(basename, file)
tf.removeUnnecessaryImports(file)
if curPkg.ImportPath == "runtime" {
if flagTiny {
// strip unneeded runtime code
stripRuntime(basename, file)
tf.removeUnnecessaryImports(file)
}
if basename == "symtab.go" {
updateMagicValue(file, magicValue())
}
}
tf.handleDirectives(file.Comments)
file = tf.transformGo(file)
Expand Down Expand Up @@ -2211,7 +2239,7 @@ func flagSetValue(flags []string, name, value string) []string {
func fetchGoEnv() error {
out, err := exec.Command("go", "env", "-json",
// Keep in sync with sharedCache.GoEnv.
"GOOS", "GOMOD", "GOVERSION",
"GOOS", "GOMOD", "GOVERSION", "GOROOT", "GOEXE",
).CombinedOutput()
if err != nil {
// TODO: cover this in the tests.
Expand Down
Loading