From f5d4d2f50495639f3c22275451f7a163e31977f1 Mon Sep 17 00:00:00 2001 From: pinohans Date: Mon, 31 May 2021 20:00:07 +0800 Subject: [PATCH] v1.0.0 init --- .gitignore | 1 + build.go | 34 ++ config.go | 166 +++++++++ config.json | 11 + go.mod | 5 + go.sum | 2 + internal/dependency/walk.go | 81 +++++ internal/filesystem/filesystem.go | 575 ++++++++++++++++++++++++++++++ main.go | 25 ++ obfuscate.go | 189 ++++++++++ readme.md | 58 +++ setup.go | 35 ++ 12 files changed, 1182 insertions(+) create mode 100644 .gitignore create mode 100644 build.go create mode 100644 config.go create mode 100644 config.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/dependency/walk.go create mode 100644 internal/filesystem/filesystem.go create mode 100644 main.go create mode 100644 obfuscate.go create mode 100644 readme.md create mode 100644 setup.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/build.go b/build.go new file mode 100644 index 0000000..2a3e765 --- /dev/null +++ b/build.go @@ -0,0 +1,34 @@ +package main + +import ( + "go/build" + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func Build() error { + ctx := build.Default + ldflags := `-s -w` + if config.WindowsHide == "1" { + ldflags += " -H=windowsgui" + } + if config.NoStatic == "1" { + ldflags += ` -extldflags '-static'` + } + + ctx.Dir = filepath.Join(newGopath, "src") + arguments := []string{"build", "-trimpath", "-ldflags", ldflags, "-tags", config.Tags, "-o", outputPath, "."} + cmd := exec.Command("go", arguments...) + cmd.Env = environBuild + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = filepath.Join(newGopath, "src") + log.Println("go " + strings.Join(cmd.Args, " ")) + if err := cmd.Run(); err != nil { + log.Fatal("Failed to run BuildSrc: ", err) + } + return nil +} diff --git a/config.go b/config.go new file mode 100644 index 0000000..b8904b8 --- /dev/null +++ b/config.go @@ -0,0 +1,166 @@ +package main + +import ( + "crypto/md5" + "encoding/json" + "fmt" + "go/build" + "io/ioutil" + "log" + "math/rand" + "os" + "path/filepath" + "time" +) + +type Config struct { + MainPkgDir string // dir of main package + OutputPath string // path of dist + NewGopath string // dir of new GOPATH + Tags string // tags are passed to the go compiler + GOOS string // target os + GOARCH string // target arch + CGOENABLED string // cgo enable + WindowsHide string // hide windows GUI + NoStatic string // no static link +} + +var ( + config = Config{ + MainPkgDir: ".", + OutputPath: "dist", + NewGopath: "pkg", + Tags: "", + GOOS: "windows", + GOARCH: "amd64", + CGOENABLED: "0", + WindowsHide: "1", + NoStatic: "1", + } +) + +var ( + mainPkgDir = func() string { + var err error + var ret string + if ret, err = filepath.Abs(config.MainPkgDir); err != nil { + log.Fatalln("Failed to get abs of NewGopath: ", err) + } + if err = os.MkdirAll(ret, 0755); err != nil { + log.Fatalln("Failed to MkdirAll NewGopath: ", err) + } + return ret + }() + + newGopath = func() string { + var err error + var ret string + if ret, err = filepath.Abs(config.NewGopath); err != nil { + log.Fatalln("Failed to get abs of NewGopath: ", err) + } + if err = os.MkdirAll(ret, 0755); err != nil { + log.Fatalln("Failed to MkdirAll NewGopath: ", err) + } + return ret + }() + + outputPath = func() string { + if ret, err := filepath.Abs(config.OutputPath); err != nil { + log.Fatalln("Failed to get abs of OutputDir: ", err) + return "" + } else { + return ret + } + }() + + randMd5 = func() func() string { + rand.Seed(time.Now().Unix()) + return func() string { + buf := make([]byte, 32) + rand.Read(buf) + return fmt.Sprintf("%x", md5.Sum(buf)) + } + }() + + ctxt = func() *build.Context { + ret := build.Default + ret.GOPATH = newGopath + ret.Dir = mainPkgDir + ret.GOOS = config.GOOS + ret.GOARCH = config.GOARCH + return &ret + }() + + environ = func() []string { + return append(os.Environ(), + "GOOS="+ctxt.GOOS, + "GOARCH="+ctxt.GOARCH, + "GOROOT="+ctxt.GOROOT, + "GOPATH="+ctxt.GOPATH, + "CGO_ENABLED="+config.CGOENABLED, + "GO111MODULE=auto", + ) + }() + + environBuild = func() []string { + return append(os.Environ(), + "GOOS="+ctxt.GOOS, + "GOARCH="+ctxt.GOARCH, + "GOROOT="+ctxt.GOROOT, + "GOPATH="+ctxt.GOPATH, + "CGO_ENABLED="+config.CGOENABLED, + "GO111MODULE=off", + ) + }() +) + +func (c *Config) LoadConfig(configFilename string) error { + + configFile, err := os.OpenFile(configFilename, os.O_RDONLY, 0644) + if err != nil { + log.Println("Failed to Open config File: ", err) + return err + } + configJson, err := ioutil.ReadAll(configFile) + if err != nil { + log.Println("Failed to ReadAll config File: ", err) + return err + } + configTemp := Config{} + if err = json.Unmarshal(configJson, &configTemp); err != nil { + log.Println("Failed to Unmarshal config File: ", err) + return err + } + if configTemp.MainPkgDir != "" { + config.MainPkgDir = configTemp.MainPkgDir + } + if configTemp.OutputPath != "" { + config.OutputPath = configTemp.OutputPath + } + if configTemp.NewGopath != "" { + config.NewGopath = configTemp.NewGopath + } + if configTemp.Tags != "" { + config.Tags = configTemp.Tags + } + if configTemp.GOOS != "" { + config.GOOS = configTemp.GOOS + } + if configTemp.GOARCH != "" { + config.GOARCH = configTemp.GOARCH + } + if configTemp.CGOENABLED != "" { + config.CGOENABLED = configTemp.CGOENABLED + } + if configTemp.WindowsHide != "" { + config.WindowsHide = configTemp.WindowsHide + } + if configTemp.NoStatic != "" { + config.NoStatic = configTemp.NoStatic + } + if err = configFile.Close(); err != nil { + log.Println("Failed to Close config File: ", err) + return err + } + return nil +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..8d7a725 --- /dev/null +++ b/config.json @@ -0,0 +1,11 @@ +{ + "MainPkgDir": ".", + "OutputPath": "dist", + "NewGopath": "pkg", + "Tags": "", + "GOOS": "windows", + "GOARCH": "amd64", + "CGOENABLED": "0", + "WindowsHide": "1", + "NoStatic": "1" +} \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4c14881 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module gobfuscator + +go 1.16 + +require github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f29ab35 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/internal/dependency/walk.go b/internal/dependency/walk.go new file mode 100644 index 0000000..551ffdc --- /dev/null +++ b/internal/dependency/walk.go @@ -0,0 +1,81 @@ +package dependency + +import ( + "go/build" + "log" + "path/filepath" + "strings" + "sync" +) + +func Walk(ctx *build.Context, projectDir string, walkFunc func(pkg *build.Package) error) error { + errChan := make(chan error, 0) + done := false + go func() { + defer close(errChan) + wg := sync.WaitGroup{} + mapProcessImports := sync.Map{} + rootPkg, err := ctx.ImportDir(projectDir, 0) + if err != nil { + log.Println("Failed to import projectDir: ", err) + errChan <- err + return + } + wg.Add(1) + go func() { + processImports(ctx, rootPkg, walkFunc, &wg, &mapProcessImports, errChan, &done) + wg.Done() + }() + wg.Wait() + }() + var err error = nil + select { + case err = <-errChan: + if err != nil { + log.Println("Failed to walk: ", err) + done = true + } + } + return err +} + +func processImports(ctx *build.Context, pkg *build.Package, walkFunc func(pkg *build.Package) error, wg *sync.WaitGroup, mapProcessImports *sync.Map, errChan chan error, done *bool) { + value, ok := mapProcessImports.Load(pkg.ImportPath) + if (ok && value.(bool)) || *done { + return + } + mapProcessImports.Store(pkg.ImportPath, true) + log.Println(pkg.ImportPath) + if err := walkFunc(pkg); err != nil { + errChan <- err + return + } + for _, pkgName := range pkg.Imports { + var child *build.Package + var err error + if strings.HasPrefix(pkgName, ".") { + child, err = ctx.Import(pkgName, pkg.Dir, 0) + if err != nil { + log.Println("Failed to Import child start with .: ", err) + errChan <- err + return + } + child.ImportPath = filepath.Join(pkg.ImportPath, child.ImportPath) + } else { + child, err = ctx.Import(pkgName, "", 0) + if err != nil { + log.Println("Failed to Import normal child: ", err) + errChan <- err + return + } + } + if child.Goroot { + continue + } + wg.Add(1) + go func() { + processImports(ctx, child, walkFunc, wg, mapProcessImports, errChan, done) + wg.Done() + }() + } +} diff --git a/internal/filesystem/filesystem.go b/internal/filesystem/filesystem.go new file mode 100644 index 0000000..8bc4e1c --- /dev/null +++ b/internal/filesystem/filesystem.go @@ -0,0 +1,575 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package filesystem + +import ( + "io" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "syscall" + "unicode" + + "github.com/pkg/errors" +) + +// HasFilepathPrefix will determine if "path" starts with "prefix" from +// the point of view of a filesystem. +// +// Unlike filepath.HasPrefix, this function is path-aware, meaning that +// it knows that two directories /foo and /foobar are not the same +// thing, and therefore HasFilepathPrefix("/foobar", "/foo") will return +// false. +// +// This function also handles the case where the involved filesystems +// are case-insensitive, meaning /foo/bar and /Foo/Bar correspond to the +// same file. In that situation HasFilepathPrefix("/Foo/Bar", "/foo") +// will return true. The implementation is *not* OS-specific, so a FAT32 +// filesystem mounted on Linux will be handled correctly. +func HasFilepathPrefix(path, prefix string) (bool, error) { + // this function is more convoluted then ideal due to need for special + // handling of volume name/drive letter on Windows. vnPath and vnPrefix + // are first compared, and then used to initialize initial values of p and + // d which will be appended to for incremental checks using + // IsCaseSensitiveFilesystem and then equality. + + // no need to check IsCaseSensitiveFilesystem because VolumeName return + // empty string on all non-Windows machines + vnPath := strings.ToLower(filepath.VolumeName(path)) + vnPrefix := strings.ToLower(filepath.VolumeName(prefix)) + if vnPath != vnPrefix { + return false, nil + } + + // Because filepath.Join("c:","dir") returns "c:dir", we have to manually + // add path separator to drive letters. Also, we need to set the path root + // on *nix systems, since filepath.Join("", "dir") returns a relative path. + vnPath += string(os.PathSeparator) + vnPrefix += string(os.PathSeparator) + + var dn string + + if isDir, err := IsDir(path); err != nil { + return false, errors.Wrap(err, "failed to check filepath prefix") + } else if isDir { + dn = path + } else { + dn = filepath.Dir(path) + } + + dn = filepath.Clean(dn) + prefix = filepath.Clean(prefix) + + // [1:] in the lines below eliminates empty string on *nix and volume name on Windows + dirs := strings.Split(dn, string(os.PathSeparator))[1:] + prefixes := strings.Split(prefix, string(os.PathSeparator))[1:] + + if len(prefixes) > len(dirs) { + return false, nil + } + + // d,p are initialized with "/" on *nix and volume name on Windows + d := vnPath + p := vnPrefix + + for i := range prefixes { + // need to test each component of the path for + // case-sensitiveness because on Unix we could have + // something like ext4 filesystem mounted on FAT + // mountpoint, mounted on ext4 filesystem, i.e. the + // problematic filesystem is not the last one. + caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(d, dirs[i])) + if err != nil { + return false, errors.Wrap(err, "failed to check filepath prefix") + } + if caseSensitive { + d = filepath.Join(d, dirs[i]) + p = filepath.Join(p, prefixes[i]) + } else { + d = filepath.Join(d, strings.ToLower(dirs[i])) + p = filepath.Join(p, strings.ToLower(prefixes[i])) + } + + if p != d { + return false, nil + } + } + + return true, nil +} + +var ( + errSrcNotDir = errors.New("source is not a directory") + errDstExist = errors.New("destination already exists") +) + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +func CopyDir(src, dst string) error { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + // We use os.Lstat() here to ensure we don't fall in a loop where a symlink + // actually links to a one of its parent directories. + fi, err := os.Lstat(src) + if err != nil { + return err + } + if !fi.IsDir() { + return errSrcNotDir + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return err + } + if err == nil { + err = os.RemoveAll(dst) + if err != nil { + return err + } + } + + if err = os.MkdirAll(dst, fi.Mode()); err != nil { + return errors.Wrapf(err, "cannot mkdir %s", dst) + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return errors.Wrapf(err, "cannot read directory %s", dst) + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + if err = CopyDir(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying directory failed") + } + } else { + // This will include symlinks, which is what we want when + // copying things. + if err = CopyFile(srcPath, dstPath); err != nil { + return errors.Wrap(err, "copying file failed") + } + } + } + + return nil +} + +// EquivalentPaths compares the paths passed to check if they are equivalent. +// It respects the case-sensitivity of the underlying filesysyems. +func EquivalentPaths(p1, p2 string) (bool, error) { + p1 = filepath.Clean(p1) + p2 = filepath.Clean(p2) + + fi1, err := os.Stat(p1) + if err != nil { + return false, errors.Wrapf(err, "could not check for path equivalence") + } + fi2, err := os.Stat(p2) + if err != nil { + return false, errors.Wrapf(err, "could not check for path equivalence") + } + + p1Filename, p2Filename := "", "" + + if !fi1.IsDir() { + p1, p1Filename = filepath.Split(p1) + } + if !fi2.IsDir() { + p2, p2Filename = filepath.Split(p2) + } + + if isPrefix1, err := HasFilepathPrefix(p1, p2); err != nil { + return false, errors.Wrap(err, "failed to check for path equivalence") + } else if isPrefix2, err := HasFilepathPrefix(p2, p1); err != nil { + return false, errors.Wrap(err, "failed to check for path equivalence") + } else if !isPrefix1 || !isPrefix2 { + return false, nil + } + + if p1Filename != "" || p2Filename != "" { + caseSensitive, err := IsCaseSensitiveFilesystem(filepath.Join(p1, p1Filename)) + if err != nil { + return false, errors.Wrap(err, "could not check for filesystem case-sensitivity") + } + if caseSensitive { + if p1Filename != p2Filename { + return false, nil + } + } else { + if !strings.EqualFold(p1Filename, p2Filename) { + return false, nil + } + } + } + + return true, nil +} + +// IsCaseSensitiveFilesystem determines if the filesystem where dir +// exists is case sensitive or not. +// +// CAVEAT: this function works by taking the last component of the given +// path and flipping the case of the first letter for which case +// flipping is a reversible operation (/foo/Bar → /foo/bar), then +// testing for the existence of the new filename. There are two +// possibilities: +// +// 1. The alternate filename does not exist. We can conclude that the +// filesystem is case sensitive. +// +// 2. The filename happens to exist. We have to test if the two files +// are the same file (case insensitive file system) or different ones +// (case sensitive filesystem). +// +// If the input directory is such that the last component is composed +// exclusively of case-less codepoints (e.g. numbers), this function will +// return false. +func IsCaseSensitiveFilesystem(dir string) (bool, error) { + alt := filepath.Join(filepath.Dir(dir), genTestFilename(filepath.Base(dir))) + + dInfo, err := os.Stat(dir) + if err != nil { + return false, errors.Wrap(err, "could not determine the case-sensitivity of the filesystem") + } + + aInfo, err := os.Stat(alt) + if err != nil { + // If the file doesn't exists, assume we are on a case-sensitive filesystem. + if os.IsNotExist(err) { + return true, nil + } + + return false, errors.Wrap(err, "could not determine the case-sensitivity of the filesystem") + } + + return !os.SameFile(dInfo, aInfo), nil +} + +// genTestFilename returns a string with at most one rune case-flipped. +// +// The transformation is applied only to the first rune that can be +// reversibly case-flipped, meaning: +// +// * A lowercase rune for which it's true that lower(upper(r)) == r +// * An uppercase rune for which it's true that upper(lower(r)) == r +// +// All the other runes are left intact. +func genTestFilename(str string) string { + flip := true + return strings.Map(func(r rune) rune { + if flip { + if unicode.IsLower(r) { + u := unicode.ToUpper(r) + if unicode.ToLower(u) == r { + r = u + flip = false + } + } else if unicode.IsUpper(r) { + l := unicode.ToLower(r) + if unicode.ToUpper(l) == r { + r = l + flip = false + } + } + } + return r + }, str) +} + +// CopyFile copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all its contents will be replaced by the contents +// of the source file. The file mode will be copied from the source. +func CopyFile(src, dst string) (err error) { + if sym, err := IsSymlink(src); err != nil { + return errors.Wrap(err, "symlink check failed") + } else if sym { + if err := cloneSymlink(src, dst); err != nil { + if runtime.GOOS == "windows" { + // If cloning the symlink fails on Windows because the user + // does not have the required privileges, ignore the error and + // fall back to copying the file contents. + // + // ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522): + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx + if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) { + return err + } + } else { + return err + } + } else { + return nil + } + } + + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return + } + + if _, err = io.Copy(out, in); err != nil { + out.Close() + return + } + + // Check for write errors on Close + if err = out.Close(); err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + + // Temporary fix for Go < 1.9 + // + // See: https://github.com/golang/dep/issues/774 + // and https://github.com/golang/go/issues/20829 + if runtime.GOOS == "windows" { + dst = fixLongPath(dst) + } + err = os.Chmod(dst, si.Mode()) + + return +} + +// cloneSymlink will create a new symlink that points to the resolved path of sl. +// If sl is a relative symlink, dst will also be a relative symlink. +func cloneSymlink(sl, dst string) error { + resolved, err := os.Readlink(sl) + if err != nil { + return err + } + + return os.Symlink(resolved, dst) +} + +// EnsureDir tries to ensure that a directory is present at the given path. It first +// checks if the directory already exists at the given path. If there isn't one, it tries +// to create it with the given permissions. However, it does not try to create the +// directory recursively. +func EnsureDir(path string, perm os.FileMode) error { + _, err := IsDir(path) + + if os.IsNotExist(err) { + err = os.Mkdir(path, perm) + if err != nil { + return errors.Wrapf(err, "failed to ensure directory at %q", path) + } + } + + return err +} + +// IsDir determines is the path given is a directory or not. +func IsDir(name string) (bool, error) { + fi, err := os.Stat(name) + if err != nil { + return false, err + } + if !fi.IsDir() { + return false, errors.Errorf("%q is not a directory", name) + } + return true, nil +} + +// IsNonEmptyDir determines if the path given is a non-empty directory or not. +func IsNonEmptyDir(name string) (bool, error) { + isDir, err := IsDir(name) + if err != nil && !os.IsNotExist(err) { + return false, err + } else if !isDir { + return false, nil + } + + // Get file descriptor + f, err := os.Open(name) + if err != nil { + return false, err + } + defer f.Close() + + // Query only 1 child. EOF if no children. + _, err = f.Readdirnames(1) + switch err { + case io.EOF: + return false, nil + case nil: + return true, nil + default: + return false, err + } +} + +// IsRegular determines if the path given is a regular file or not. +func IsRegular(name string) (bool, error) { + fi, err := os.Stat(name) + if os.IsNotExist(err) { + return false, nil + } + if err != nil { + return false, err + } + mode := fi.Mode() + if mode&os.ModeType != 0 { + return false, errors.Errorf("%q is a %v, expected a file", name, mode) + } + return true, nil +} + +// IsSymlink determines if the given path is a symbolic link. +func IsSymlink(path string) (bool, error) { + l, err := os.Lstat(path) + if err != nil { + return false, err + } + + return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil +} + +// fixLongPath returns the extended-length (\\?\-prefixed) form of +// path when needed, in order to avoid the default 260 character file +// path limit imposed by Windows. If path is not easily converted to +// the extended-length form (for example, if path is a relative path +// or contains .. elements), or is short enough, fixLongPath returns +// path unmodified. +// +// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath +func fixLongPath(path string) string { + // Do nothing (and don't allocate) if the path is "short". + // Empirically (at least on the Windows Server 2013 builder), + // the kernel is arbitrarily okay with < 248 bytes. That + // matches what the docs above say: + // "When using an API to create a directory, the specified + // path cannot be so long that you cannot append an 8.3 file + // name (that is, the directory name cannot exceed MAX_PATH + // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. + // + // The MSDN docs appear to say that a normal path that is 248 bytes long + // will work; empirically the path must be less then 248 bytes long. + if len(path) < 248 { + // Don't fix. (This is how Go 1.7 and earlier worked, + // not automatically generating the \\?\ form) + return path + } + + // The extended form begins with \\?\, as in + // \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt. + // The extended form disables evaluation of . and .. path + // elements and disables the interpretation of / as equivalent + // to \. The conversion here rewrites / to \ and elides + // . elements as well as trailing or duplicate separators. For + // simplicity it avoids the conversion entirely for relative + // paths or paths containing .. elements. For now, + // \\server\share paths are not converted to + // \\?\UNC\server\share paths because the rules for doing so + // are less well-specified. + if len(path) >= 2 && path[:2] == `\\` { + // Don't canonicalize UNC paths. + return path + } + if !isAbs(path) { + // Relative path + return path + } + + const prefix = `\\?` + + pathbuf := make([]byte, len(prefix)+len(path)+len(`\`)) + copy(pathbuf, prefix) + n := len(path) + r, w := 0, len(prefix) + for r < n { + switch { + case os.IsPathSeparator(path[r]): + // empty block + r++ + case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])): + // /./ + r++ + case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])): + // /../ is currently unhandled + return path + default: + pathbuf[w] = '\\' + w++ + for ; r < n && !os.IsPathSeparator(path[r]); r++ { + pathbuf[w] = path[r] + w++ + } + } + } + // A drive's root directory needs a trailing \ + if w == len(`\\?\c:`) { + pathbuf[w] = '\\' + w++ + } + return string(pathbuf[:w]) +} + +func isAbs(path string) (b bool) { + v := volumeName(path) + if v == "" { + return false + } + path = path[len(v):] + if path == "" { + return false + } + return os.IsPathSeparator(path[0]) +} + +func volumeName(path string) (v string) { + if len(path) < 2 { + return "" + } + // with drive letter + c := path[0] + if path[1] == ':' && + ('0' <= c && c <= '9' || 'a' <= c && c <= 'z' || + 'A' <= c && c <= 'Z') { + return path[:2] + } + // is it UNC + if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) && + !os.IsPathSeparator(path[2]) && path[2] != '.' { + // first, leading `\\` and next shouldn't be `\`. its server name. + for n := 3; n < l-1; n++ { + // second, next '\' shouldn't be repeated. + if os.IsPathSeparator(path[n]) { + n++ + // third, following something characters. its share name. + if !os.IsPathSeparator(path[n]) { + if path[n] == '.' { + break + } + for ; n < l; n++ { + if os.IsPathSeparator(path[n]) { + break + } + } + return path[:n] + } + break + } + } + } + return "" +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..8cbfb33 --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "flag" + "log" +) + +func main() { + configFilename := flag.String("c", "", "config.json") + if *configFilename != "" { + if err := config.LoadConfig(*configFilename); err != nil { + log.Fatalln("Failed to LoadConfig: ", err) + } + } + if err := Setup(); err != nil { + log.Fatalln("Failed to Setup: ", err) + } + + if err := Obfuscate(); err != nil { + log.Fatalln("Failed to obfuscate: ", err) + } + if err := Build(); err != nil { + log.Fatalln("Failed to BuildSrc: ", err) + } +} diff --git a/obfuscate.go b/obfuscate.go new file mode 100644 index 0000000..131f658 --- /dev/null +++ b/obfuscate.go @@ -0,0 +1,189 @@ +package main + +import ( + "errors" + "fmt" + "go/ast" + "go/build" + "go/format" + "go/parser" + "go/token" + "gobfuscator/internal/dependency" + "gobfuscator/internal/filesystem" + "io/fs" + "log" + "os" + "path/filepath" + "strings" + "sync" +) + +func Obfuscate() error { + var mapPkgName sync.Map + if err := preObfuscate(&mapPkgName); err != nil { + log.Println("Failed to preObfuscate: ", err) + return err + } + if err := doObfuscate(mapPkgName); err != nil { + log.Println("Failed to doObfuscate: ", err) + return err + } + return nil +} + +func preObfuscate(mapPkgName *sync.Map) error { + if err := dependency.Walk(ctxt, mainPkgDir, func(pkg *build.Package) error { + // TODO: maybe collision + mapPkgName.Store(pkg.ImportPath, randMd5()) + return nil + }); err != nil { + log.Println("Failed to obfuscate package names: ", err) + return err + } + return nil +} + +type visitor struct { + mapPkgName sync.Map +} + +func (v *visitor) Visit(node ast.Node) ast.Visitor { + switch n := node.(type) { + case *ast.ImportSpec: + oldValue := strings.Trim(n.Path.Value, "\"") + newValue, ok := v.mapPkgName.Load(oldValue) + if ok { + n.Path.Value = fmt.Sprintf("\"%s\"", newValue.(string)) + } + } + return v +} + +func processComment(file *ast.File, src string, dst string) { + astCommentGroups := file.Comments + for _, astCommentGroup := range astCommentGroups { + for _, astComment := range astCommentGroup.List { + text := astComment.Text + text = strings.Trim(text, " ") + if strings.HasPrefix(text, "//go:embed") { + text = strings.TrimPrefix(text, "//go:embed") + text = strings.Trim(text, " ") + for _, dir := range strings.Split(text, " ") { + if dir != "" { + absSrc := filepath.Join(src, dir) + absDst := filepath.Join(dst, dir) + isDir, _ := filesystem.IsDir(absSrc) + if isDir { + if err := os.MkdirAll(absDst, 0755); err != nil { + continue + } + _ = filesystem.CopyDir(absSrc, absDst) + } + } + } + + } else if strings.HasPrefix(text, "//") { + text = strings.TrimPrefix(text, "//") + text = strings.Trim(text, " ") + if strings.HasPrefix(text, "import ") { + astComment.Text = "//" + } + } + } + } + +} + +func writeGoFile(filename string, node ast.Node, set *token.FileSet) error { + log.Println("save to : ", filename) + out, err := os.Create(filename) + if err != nil { + return err + } + if err = format.Node(out, set, node); err != nil { + return err + } + return nil +} + +func doObfuscate(mapPkgName sync.Map) error { + if err := dependency.Walk(ctxt, mainPkgDir, func(pkg *build.Package) error { + var newPath string + if isMainPkg(pkg) { + newPath = filepath.Join(newGopath, "src") + } else { + newImportPath, ok := mapPkgName.Load(pkg.ImportPath) + if !ok { + log.Println("Failed to doObfuscate in Walk when mapPkgName Load: ", pkg.ImportPath) + return errors.New("mapPkgName Load error") + } + newPath = filepath.Join(newGopath, "src", newImportPath.(string)) + } + if err := os.MkdirAll(newPath, 0755); err != nil { + log.Println("Failed to MkdirAll newPath in Walk of CopySrc: ", err) + return err + } + set := token.NewFileSet() + pkgMap, err := parser.ParseDir(set, pkg.Dir, func(info fs.FileInfo) bool { + for _, name := range pkg.IgnoredGoFiles { + if info.Name() == name { + return false + } + } + return true + }, parser.ParseComments) + if err != nil { + log.Println("Failed to parser ParseDir: ", err) + return err + } + for pkgName, astPackage := range pkgMap { + if !isMainPkg(pkg) && pkgName == "main" { + continue + } else if strings.HasSuffix(pkgName, "_test") { + continue + } + ast.Walk(&visitor{mapPkgName: mapPkgName}, astPackage) + for filename, astFile := range astPackage.Files { + dst := filepath.Join(newPath, filepath.Base(filename)) + processComment(astFile, pkg.Dir, newPath) + if err = writeGoFile(dst, astFile, set); err != nil { + log.Println("Failed to writeGoFile: ", err) + return err + } + } + } + srcFiles := [][]string{ + pkg.CgoFiles, + pkg.CFiles, + pkg.CXXFiles, + pkg.MFiles, + pkg.HFiles, + pkg.FFiles, + pkg.SFiles, + pkg.SwigFiles, + pkg.SwigCXXFiles, + pkg.SysoFiles, + } + + for _, list := range srcFiles { + for _, file := range list { + src := filepath.Join(pkg.Dir, file) + dst := filepath.Join(newPath, file) + log.Println(src, dst) + if err = filesystem.CopyFile(src, dst); err != nil { + log.Println("Failed to copyFile in Walk of CopySrc: ", err) + return err + } + } + } + return nil + }); err != nil { + log.Println("Failed to obfuscate package names: ", err) + return err + } + return nil +} + +func isMainPkg(pkg *build.Package) bool { + return pkg.ImportPath == "." && pkg.Name == "main" +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..6f26428 --- /dev/null +++ b/readme.md @@ -0,0 +1,58 @@ +# gobfuscator + +Inspired by [gobfuscate](https://github.com/unixpickle/gobfuscate), but gobfuscator is different in totally. + + +## 1 how to + +### 1.1 create config.json + +> Content of config.json + +```json +{ + "MainPkgDir": ".", + "OutputPath": "dist", + "NewGopath": "pkg", + "Tags": "", + "GOOS": "windows", + "GOARCH": "amd64", + "CGOENABLED": "0", + "WindowsHide": "1", + "NoStatic": "1" +} +``` + +> Explanation of parameter + +```go +type Config struct { + MainPkgDir string // dir of main package + OutputPath string // path of dist + NewGopath string // dir of new GOPATH + Tags string // tags are passed to the go compiler + GOOS string // target os + GOARCH string // target arch + CGOENABLED string // cgo enable + WindowsHide string // hide windows GUI + NoStatic string // no static link +} +``` + +### 1.2 compile and run + +```bash +go mod tidy && go build -o gobfuscator . && ./gobfuscator +``` + +## 2. dependency + +> test with Go 1.16.4 + +## 3. technical + +1. Obfuscate third party package with ast. +2. Process build tags and go:embed. +3. Fast import graph walker. + +And so on. diff --git a/setup.go b/setup.go new file mode 100644 index 0000000..1bf9727 --- /dev/null +++ b/setup.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + "os" + "os/exec" + "path/filepath" +) + +func Setup() error { + if err := GoModTidy(); err != nil { + log.Println("Failed to GoModTidy: ", err) + return err + } + + if err := os.RemoveAll(filepath.Join(newGopath, "src")); err != nil { + log.Println("Failed to RemoveAll in CopySrc: ", err) + return err + } + return nil +} + +func GoModTidy() error { + arguments := []string{"mod", "tidy"} + cmd := exec.Command("go", arguments...) + cmd.Env = environ + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = mainPkgDir + if err := cmd.Run(); err != nil { + log.Println("Failed to run GoModTidy: ", err) + return err + } + return nil +}