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