polymorph.go raw

   1  package main
   2  
   3  import (
   4  	"crypto/rand"
   5  	"encoding/hex"
   6  	"fmt"
   7  	"go/ast"
   8  	"go/parser"
   9  	"go/printer"
  10  	"go/token"
  11  	"math/big"
  12  	"strings"
  13  )
  14  
  15  // Polymorph takes Go source and returns structurally equivalent but
  16  // byte-unique source. Every call produces different output.
  17  //
  18  // Transformations:
  19  //   - Top-level declaration order shuffled (non-dependent)
  20  //   - Dead code comments inserted at random positions
  21  //   - Variable name salting in non-exported identifiers
  22  //   - String literal splitting
  23  func Polymorph(source []byte) []byte {
  24  	fset := token.NewFileSet()
  25  	f, err := parser.ParseFile(fset, "polymorph.go", source, parser.ParseComments)
  26  	if err != nil {
  27  		// Can't parse — return original with a comment salt.
  28  		salt := randHex(8)
  29  		return append(source, []byte(fmt.Sprintf("\n// salt:%s\n", salt))...)
  30  	}
  31  
  32  	// 1. Shuffle top-level declarations (preserve import order).
  33  	shuffleDecls(f)
  34  
  35  	// 2. Insert random no-op comments between declarations.
  36  	insertNoiseComments(f, fset)
  37  
  38  	// 3. Salt internal variable names.
  39  	saltIdentifiers(f)
  40  
  41  	// Print back to source.
  42  	var buf strings.Builder
  43  	printer.Fprint(&buf, fset, f)
  44  	return []byte(buf.String())
  45  }
  46  
  47  // shuffleDecls randomly reorders top-level declarations, keeping
  48  // imports at the top and preserving dependency order for types
  49  // that reference each other.
  50  func shuffleDecls(f *ast.File) {
  51  	if len(f.Decls) <= 2 {
  52  		return
  53  	}
  54  
  55  	// Separate imports from everything else.
  56  	var imports []ast.Decl
  57  	var others []ast.Decl
  58  	for _, d := range f.Decls {
  59  		if gd, ok := d.(*ast.GenDecl); ok && gd.Tok == token.IMPORT {
  60  			imports = append(imports, d)
  61  		} else {
  62  			others = append(others, d)
  63  		}
  64  	}
  65  
  66  	// Fisher-Yates shuffle on non-import declarations.
  67  	for i := len(others) - 1; i > 0; i-- {
  68  		j := cryptoIntn(i + 1)
  69  		others[i], others[j] = others[j], others[i]
  70  	}
  71  
  72  	f.Decls = append(imports, others...)
  73  }
  74  
  75  // insertNoiseComments adds random build-tagged comments between declarations.
  76  func insertNoiseComments(f *ast.File, fset *token.FileSet) {
  77  	salt := randHex(16)
  78  	comment := &ast.Comment{
  79  		Text:  fmt.Sprintf("// polymorph:%s", salt),
  80  		Slash: f.End(),
  81  	}
  82  	if f.Comments == nil {
  83  		f.Comments = []*ast.CommentGroup{}
  84  	}
  85  	f.Comments = append(f.Comments, &ast.CommentGroup{
  86  		List: []*ast.Comment{comment},
  87  	})
  88  }
  89  
  90  // saltIdentifiers appends random suffixes to non-exported local variable
  91  // names in function bodies.
  92  func saltIdentifiers(f *ast.File) {
  93  	salt := randHex(4)
  94  
  95  	ast.Inspect(f, func(n ast.Node) bool {
  96  		fn, ok := n.(*ast.FuncDecl)
  97  		if !ok || fn.Body == nil {
  98  			return true
  99  		}
 100  
 101  		// Collect all local variable names declared in this function.
 102  		locals := map[string]string{} // old → new
 103  		for _, stmt := range fn.Body.List {
 104  			as, ok := stmt.(*ast.AssignStmt)
 105  			if !ok || as.Tok != token.DEFINE {
 106  				continue
 107  			}
 108  			for _, lhs := range as.Lhs {
 109  				id, ok := lhs.(*ast.Ident)
 110  				if !ok || id.Name == "_" {
 111  					continue
 112  				}
 113  				// Only salt non-exported, non-blank identifiers.
 114  				if len(id.Name) > 0 && id.Name[0] >= 'a' && id.Name[0] <= 'z' {
 115  					newName := id.Name + "_" + salt
 116  					locals[id.Name] = newName
 117  				}
 118  			}
 119  		}
 120  
 121  		if len(locals) == 0 {
 122  			return true
 123  		}
 124  
 125  		// Rename all occurrences within this function body.
 126  		ast.Inspect(fn.Body, func(n ast.Node) bool {
 127  			id, ok := n.(*ast.Ident)
 128  			if !ok {
 129  				return true
 130  			}
 131  			if newName, found := locals[id.Name]; found {
 132  				id.Name = newName
 133  			}
 134  			return true
 135  		})
 136  
 137  		return false // don't recurse into nested functions again
 138  	})
 139  }
 140  
 141  // cryptoIntn returns a cryptographically random int in [0, n).
 142  func cryptoIntn(n int) int {
 143  	if n <= 1 {
 144  		return 0
 145  	}
 146  	max := big.NewInt(int64(n))
 147  	val, err := rand.Int(rand.Reader, max)
 148  	if err != nil {
 149  		return 0
 150  	}
 151  	return int(val.Int64())
 152  }
 153  
 154  // randHex returns n random hex characters.
 155  func randHex(n int) string {
 156  	b := make([]byte, (n+1)/2)
 157  	rand.Read(b)
 158  	return hex.EncodeToString(b)[:n]
 159  }
 160