Files
burrowers-garble/testdata/script/reflect.txtar
Paul Scheduikat 926f3de60d obfuscate all names used in reflection
Go code can retrieve and use field and method names via the `reflect` package.
For that reason, historically we did not obfuscate names of fields and methods
underneath types that we detected as used for reflection, via e.g. `reflect.TypeOf`.

However, that caused a number of issues. Since we obfuscate and build one package
at a time, we could only detect when types were used for reflection in their own package
or in upstream packages. Use of reflection in downstream packages would be detected
too late, causing one package to obfuscate the names and the other not to, leading to a build failure.

A different approach is implemented here. All names are obfuscated now, but we collect
those types used for reflection, and at the end of a build in `package main`,
we inject a function into the runtime's `internal/abi` package to reverse the obfuscation
for those names which can be used for reflection.

This does mean that the obfuscation for these names is very weak, as the binary
contains a one-to-one mapping to their original names, but they cannot be obfuscated
without breaking too many Go packages out in the wild. There is also some amount
of overhead in `internal/abi` due to this, but we aim to make the overhead insignificant.

Fixes #884, #799, #817, #881, #858, #843, #842

Closes #406
2024-11-27 22:38:43 +01:00

646 lines
14 KiB
Plaintext

exec garble build
exec ./main
cmp stdout main.stdout
! binsubstr main$exe 'garble_main.go' 'test/main' 'importedpkg.' 'DownstreamObfuscated' 'SiblingObfuscated' 'IndirectObfuscated' 'IndirectNamedWithoutReflect' 'AliasIndirectNamedWithReflect' 'AliasIndirectNamedWithoutReflect' 'FmtTypeField' 'LocalObfuscated'
binsubstr main$exe 'ReflectInDefined' 'ExportedField2' 'unexportedField2' 'IndirectUnobfuscated' 'IndirectNamedWithReflect'
[short] stop # no need to verify this with -short
# Check that the program works as expected without garble.
go build
exec ./main
cmp stdout main.stdout
-- go.mod --
module test/main
go 1.23
-- garble_main.go --
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/gob"
"encoding/json"
"fmt"
"math/big"
"os"
"reflect"
"unsafe"
"strings"
"sync"
"text/template"
"test/main/importedpkg"
"test/main/importedpkg2"
)
var Sink interface{}
func main() {
// Fields still work fine when they are not obfuscated.
fmt.Println(importedpkg.ReflectInDefinedVar.ExportedField2)
fmt.Println(importedpkg.ReflectInDefined{ExportedField2: 5})
// Type names are not obfuscated either, when reflection is used.
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOf(2))
printfWithoutPackage("%T\n", importedpkg.ReflectTypeOfIndirect(4))
// More complex use of reflect.
v := importedpkg.ReflectValueOfVar
printfWithoutPackage("%#v\n", v)
method := reflect.ValueOf(&v).MethodByName("ExportedMethodName")
if method.IsValid() {
fmt.Println(method.Call(nil))
} else {
fmt.Println("method not found")
}
// Use of a common library using reflect, encoding/json.
enc, _ := json.Marshal(EncodingT{Foo: 3})
fmt.Println(string(enc))
// Another common library, text/template.
tmpl := template.Must(template.New("").Parse("Hello {{.Name}}."))
_ = tmpl.Execute(os.Stdout, struct{Name string}{Name: "Dave"})
fmt.Println() // Always print a newline.
// Another complex case, involving embedding and another package.
outer := &importedpkg.EmbeddingOuter{}
outer.InnerField = 3
enc, _ = json.Marshal(outer)
fmt.Println(string(enc))
// An edge case; the struct type is defined in a different package.
// Note that the struct type is unnamed, but it still has named fields.
// We only use reflection on it here, not the declaring package.
// As such, we should obfuscate the field name.
// Simply using the field name here used to cause build failures.
_ = reflect.TypeOf(importedpkg.UnnamedWithDownstreamReflect{})
fmt.Printf("%v\n", importedpkg.UnnamedWithDownstreamReflect{
DownstreamObfuscated: "downstream",
})
// An edge case; the struct type is defined in package importedpkg2.
// importedpkg2 does not use reflection on it, so it's not obfuscated there.
// importedpkg uses reflection on a type containing ReflectInSiblingImport.
// If our logic is incorrect, we might inconsistently obfuscate the type.
// We should not obfuscate it when building any package.
fmt.Printf("%v\n", importedpkg2.ReflectInSiblingImport{
SiblingObfuscated: "sibling",
})
// Using type aliases as both regular fields, and embedded fields.
var emb EmbeddingIndirect
emb.With.IndirectUnobfuscated = "indirect-with"
emb.With.DuplicateFieldName = 3
emb.Without.IndirectObfuscated = "indirect-without"
emb.Without.DuplicateFieldName = 4
fmt.Printf("%v\n", emb)
printfWithoutPackage("%#v\n", emb.With)
// TODO: don't obfuscate the embedded field name here
// printfWithoutPackage("%#v\n", importedpkg.ReflectEmbeddingAlias{})
indirectReflection(IndirectReflection{})
fmt.Println(FmtType{})
// Variadic functions are a bit tricky as the number of parameters is variable.
// We want to notice indirect uses of reflection via all variadic arguments.
_ = importedpkg.VariadicReflect(0, 1, 2, 3)
_ = importedpkg.VariadicReflect(0)
variadic := VariadicReflection{ReflectionField: "variadic"}
_ = importedpkg.VariadicReflect("foo", 1, variadic, false)
printfWithoutPackage("%#v\n", variadic)
testx509()
testGoSpew()
// Very complex reflection used by gorm
user := StatUser{}
find(&user)
// Similar to gorm with composite literals instead of direct assignments
userComp := StatCompUser{}
findComp(&userComp)
x := UnnamedStructInterface(importedpkg.ReflectUnnamedStruct(0))
x.UnnamedStructMethod(struct{ UnnamedStructField string }{UnnamedStructField: "field value"})
// Local names not used in reflection should not be in the final binary,
// even if they are embedded in a struct and become a field name.
type unexportedLocalObfuscated struct { LocalObfuscatedA int }
type ExportedLocalObfuscated struct { LocalObfuscatedB int }
type EmbeddingObfuscated struct {
unexportedLocalObfuscated
ExportedLocalObfuscated
}
// Ensure the types are kept in the binary. Use an anonymous type too.
_ = fmt.Sprintf("%#v", EmbeddingObfuscated{})
_ = fmt.Sprintf("%#v", struct{ExportedLocalObfuscated}{})
// reflection can see all type names, even local ones, so they cannot be obfuscated.
{
type TypeOfNamedField struct { NamedReflectionField int }
type TypeOfEmbeddedField struct { EmbeddedReflectionField int }
type TypeOfParent struct {
ReflectionField TypeOfNamedField
TypeOfEmbeddedField
}
t := reflect.TypeOf(TypeOfParent{})
fmt.Println("TypeOfParent's own name:", t.Name())
namedField, _ := t.FieldByName("ReflectionField")
namedFieldField, _ := namedField.Type.FieldByName("NamedReflectionField")
fmt.Println("TypeOfParent named:",
namedField.Type.Name(),
namedFieldField.Name,
)
embedField, _ := t.FieldByName("TypeOfEmbeddedField")
embedFieldField, _ := embedField.Type.FieldByName("EmbeddedReflectionField")
fmt.Println("TypeOfParent embedded:",
embedField.Type.Name(),
embedFieldField.Name,
)
}
y := UnnamedStructFields{}
y.unexportedGoGoProto = new(struct {
mu sync.Mutex
extensionMap map[int32]EncodingT
})
fmt.Println(reflect.TypeOf(Connection{}))
}
type EmbeddingIndirect struct {
// With field names, to test selectors above.
With importedpkg.AliasIndirectNamedWithReflect
Without importedpkg.AliasIndirectNamedWithoutReflect
// Embedding used to crash garble, too.
importedpkg.AliasIndirectNamedWithReflect
}
func printfWithoutPackage(format string, v any) {
s := fmt.Sprintf(format, v)
if _, without, found := strings.Cut(s, "."); found {
s = without
}
fmt.Print(s)
}
type EncodingT struct {
Foo int
}
type RecursiveStruct struct {
*RecursiveStruct
list []RecursiveStruct
}
// This could crash or hang if we don't deal with loops.
var _ = reflect.TypeOf(RecursiveStruct{})
type IndirectReflection struct {
ReflectionField string
}
func indirectReflection(v any) {
fmt.Println(reflect.TypeOf(v).Field(0).Name)
}
type VariadicReflection struct {
ReflectionField string
}
type FmtType struct {
FmtTypeField int
}
// copied from github.com/davecgh/go-spew, which reaches into reflect's internals
func testGoSpew() {
flagValOffset := func() uintptr {
field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
if !ok {
panic("reflect.Value has no flag field")
}
return field.Offset
}()
type flag uintptr
flagField := func(v *reflect.Value) *flag {
return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
}
type t0 int
var t struct {
A t0
t0
a t0
}
vA := reflect.ValueOf(t).FieldByName("A")
va := reflect.ValueOf(t).FieldByName("a")
vt0 := reflect.ValueOf(t).FieldByName("t0")
flagvA := *flagField(&vA)
flagva := *flagField(&va)
flagvt0 := *flagField(&vt0)
if flagvA&flagva&flagvt0 == 0 {
panic("reflect.Value read-only flag has changed semantics")
}
type T0 int
var T struct {
A T0
T0
a T0
}
vA = reflect.ValueOf(T).FieldByName("A")
va = reflect.ValueOf(T).FieldByName("a")
vt0 = reflect.ValueOf(T).FieldByName("T0")
flagvA = *flagField(&vA)
flagva = *flagField(&va)
flagvt0 = *flagField(&vt0)
if flagvA&flagva&flagvt0 == 0 {
panic("reflect.Value read-only flag has changed semantics")
}
}
// encoding/x509 uses encoding/asn1, which uses reflect.
// In one place it depends on field names; that used to be broken by garble.
func testx509() {
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
panic(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{Organization: []string{"Acme Co"}},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
panic(err)
}
_, err = x509.ParseCertificate(derBytes)
if err != nil {
panic(err)
}
}
type pingMsg struct {
Data string `sshtype:"192"`
}
type pongMsg struct {
Data string `sshtype:"193"`
}
// golang.org/x/crypto/ssh converts a reflected type to another type
func testSSH() {
var msg = pingMsg{
Data: "data",
}
json.Marshal(msg)
_ = pongMsg(msg)
}
// variations similar to ssh
type reflectedMsg struct {
Data string
}
type convertedMsg struct {
Data string
}
func reflectConvert() {
msg := reflectedMsg(convertedMsg{})
json.Marshal(msg)
}
type reflectedMsg2 struct {
Data string
}
type convertedMsg2 struct {
Data string
}
func unrelatedConvert() {
// only discoverable by rechecking the package
_ = convertedMsg2(reflectedMsg2{})
}
func reflectUnrelatedConv() {
var msg = reflectedMsg2{
Data: "data",
}
json.Marshal(msg)
}
type StatUser struct {
Id int64 `gorm:"primaryKey"`
User_Id int64
}
type StatCompUser struct {
Id int64 `gorm:"primaryKey"`
User_Id int64
}
type Transaction struct {
Statement Statement
}
type Statement struct {
Dest interface{}
Model string
}
func find(dest interface{}) {
tx := Transaction{}
tx.Statement.Dest = dest
execute(tx)
}
func findComp(dest interface{}) {
tx := Transaction{
Statement: Statement{
Dest: dest,
},
}
execute(tx)
}
func execute(db Transaction) {
stmt := db.Statement
v := reflect.TypeOf(stmt.Dest)
fmt.Println(v)
}
type UnnamedStructInterface interface {
UnnamedStructMethod(struct{ UnnamedStructField string })
}
// Some projects declare types with unnamed struct fields,
// and the entire type is used via reflection and cannot be obfuscated.
// However, when assigning to these fields, the use of inline anonymous struct types
// confused garble, and it did not obfuscate those inline structs as well.
// That resulted in "cannot use X as Y value in assignment" build errors.
var _ = reflect.TypeOf(UnnamedStructFields{})
type UnnamedStructFields struct {
// As seen in github.com/gogo/protobuf/proto.
unexportedGoGoProto *struct {
mu sync.Mutex
extensionMap map[int32]EncodingT
}
}
func gobStruct() {
type gobAlias struct {
Security []map[string]struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make([]map[string]struct {
List []string
Pad bool
}, 0, len([]string{}))
}
func gobMap() {
type gobAlias struct {
Security map[string]struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make(map[string]struct {
List []string
Pad bool
}, len([]string{}))
}
func gobChan() {
type gobAlias struct {
Security chan struct {
List []string
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
alias.Security = make(chan struct {
List []string
Pad bool
}, len([]string{}))
}
type Connection struct {
MaxLen struct {
Varchar int
}
}
// NewConnection create a new connection from databaseURL string
func NewConnection() *Connection {
return &Connection{
MaxLen: struct {
Varchar int
}{
Varchar: 0x7FFF,
},
}
}
func closure() {
type gobAlias struct {
Security func() struct {
Pad bool
}
}
alias := gobAlias{}
gob.NewEncoder(os.Stdout).Encode(alias)
outer := func() func() struct{ Pad bool } {
return func() struct{ Pad bool } {
return struct{ Pad bool }{Pad: true}
}
}
alias.Security = outer()
}
-- importedpkg/imported.go --
package importedpkg
import (
"fmt"
"reflect"
"test/main/importedpkg/indirect"
"test/main/importedpkg2"
)
type ReflectTypeOf int
var _ = reflect.TypeOf(ReflectTypeOf(0))
type ReflectTypeOfIndirect int
var _ = reflect.TypeOf(new([]*ReflectTypeOfIndirect))
type ReflectValueOf struct {
ExportedField string
unexportedField string
}
func (r *ReflectValueOf) ExportedMethodName() string { return "method: " + r.ExportedField }
var ReflectValueOfVar = ReflectValueOf{ExportedField: "abc"}
var _ = reflect.TypeOf(ReflectValueOfVar)
type ReflectInDefined struct {
ExportedField2 int
unexportedField2 int
importedpkg2.ReflectInSiblingImport
}
var ReflectInDefinedVar = ReflectInDefined{ExportedField2: 9000}
var _ = reflect.TypeOf(ReflectInDefinedVar)
var _ = reflect.TypeOf([]*struct{EmbeddingOuter}{})
type EmbeddingOuter struct {
EmbeddingInner
Anon struct {
AnonField int
}
}
type EmbeddingInner struct {
InnerField int
}
type UnnamedWithDownstreamReflect = struct {
DownstreamObfuscated string
}
type (
AliasIndirectNamedWithReflect = indirect.IndirectNamedWithReflect
AliasIndirectNamedWithoutReflect = indirect.IndirectNamedWithoutReflect
)
var _ = reflect.TypeOf(ReflectEmbeddingAlias{})
type ReflectEmbeddingAlias struct {
ReflectEmbeddedAlias
}
type ReflectEmbeddedAlias = ReflectEmbeddingNamed
type ReflectEmbeddingNamed struct{}
func VariadicReflect(x any, ys ...any) int {
_ = reflect.TypeOf(x)
for _, y := range ys {
_ = reflect.TypeOf(y)
}
return len(ys)
}
type ReflectUnnamedStruct int
func (ReflectUnnamedStruct) UnnamedStructMethod(s struct{ UnnamedStructField string }) {
fmt.Println(reflect.TypeOf(s))
}
-- importedpkg2/imported2.go --
package importedpkg2
type ReflectInSiblingImport struct {
SiblingObfuscated string
}
-- importedpkg/indirect/indirect.go --
package indirect
import "reflect"
var _ = reflect.TypeOf(IndirectNamedWithReflect{})
type IndirectNamedWithReflect struct {
IndirectUnobfuscated string
DuplicateFieldName int
}
type IndirectNamedWithoutReflect struct {
IndirectObfuscated string
DuplicateFieldName int
}
-- main.stdout --
9000
{5 0 {}}
ReflectTypeOf
ReflectTypeOfIndirect
ReflectValueOf{ExportedField:"abc", unexportedField:""}
[method: abc]
{"Foo":3}
Hello Dave.
{"InnerField":3,"Anon":{"AnonField":0}}
{downstream}
{sibling}
{{indirect-with 3} {indirect-without 4} { 0}}
IndirectNamedWithReflect{IndirectUnobfuscated:"indirect-with", DuplicateFieldName:3}
ReflectionField
{0}
VariadicReflection{ReflectionField:"variadic"}
*main.StatUser
*main.StatCompUser
struct { UnnamedStructField string }
TypeOfParent's own name: TypeOfParent
TypeOfParent named: TypeOfNamedField NamedReflectionField
TypeOfParent embedded: TypeOfEmbeddedField EmbeddedReflectionField
main.Connection