burrowers / garble

Obfuscate Go builds
BSD 3-Clause "New" or "Revised" License
3.92k stars 248 forks source link

Condition obfuscation [idea] #61

Open pagran opened 4 years ago

pagran commented 4 years ago

I propose to actively use lambdas and channels since they generate a lot of boilerplate code in the output binary. For example, you can obfuscate any if stmt:

Original code:

if os.Getpid()%2 == 0 || os.Getpid()%3 == 0 {
    println("lucky pid", os.Getpid())
} else {
    println("unlucky pid", os.Getpid())
}

Step 1:

if (func() bool {
    return os.Getpid()%2 == 0 || os.Getpid()%3 == 0
})() {
    println("lucky pid", os.Getpid())
} else {
    println("unlucky pid", os.Getpid())
}

Step 2:

if (func() bool {
    c := make(chan bool, 2)
    go func() {
        c <- os.Getpid()%2 == 0
        c <- os.Getpid()%3 == 0
    }()
    return <-c || <-c
})() {
    println("lucky pid", os.Getpid())
} else {
    println("unlucky pid", os.Getpid())
}

Just try load output binaru into ida pro ;)

mvdan commented 4 years ago

How would you implement this in a way that doesn't blow up the binary size, compile time, and run-time speed of the program? We definitely can't do this for all statements. Would you just do a small portion of statements at random? Or would you depend on some sort of annotation like "please make this statement harder to deobfuscate"?

pagran commented 4 years ago

I like the idea of ​​explicitly indicating which expressions to obfuscate. For example, we can obfuscate the logic of checking a license without regard to speed and size, but do not touch the recursive calculation of the number of fibonacci

he-he, goroutines quite expensive

BenchmarkOriginal
BenchmarkOriginal-24            13812631            77.4 ns/op
BenchmarkStep1
BenchmarkStep1-24               14489564            80.0 ns/op
BenchmarkStep2
BenchmarkStep2-24                 526202          2803 ns/op
BenchmarkStep2Optimized
BenchmarkStep2Optimized-24       2433956           504 ns/op

https://gist.github.com/pagran/29cfbd713119e7395b12ed134145be91

mvdan commented 4 years ago

I'd be fine with some form of annotation. Here are two ideas:

1) special comments, like:

//go:garble obfuscate-statement
someHiddenValue = ...

2) special no-op APIs, like:

garble.ObfuscateBlock(func() {
    someHiddenValue = ...
}())

With option 2, we would replace the func call with the logic to hide the code inside the anonymous func.

pagran commented 4 years ago

In theory, no-op APIs should be more flexible. They allow you to wrap multi-line logic.

garble.ObfuscateBlock(func() {
// open license file
// check rsa signature
//  something else
}) // I think () are not needed, let api guarantee a lambda call
pagran commented 4 years ago

I can continue the idea:

  1. Add protection levels
  2. Add special methods for string (and other default types)
  3. Add obfuscated resources (not sure)

Something like:

garble.ObfuscateBlock{Low,...,Ultra}(func() {
// logic
})

gargble.ObfuscateString{Low,Hight,Ultra}("Some string") // return string type

gargble.LoadFile("assets/logo.png")

On ultra we can generate megabytes trash code :)

mvdan commented 4 years ago

I wonder if options like -literals could be replaced by such annotations too. It certainly feels more powerful, given that you can narrow down your needs. It seems less useful if one wants to obfuscate all strings, but I wonder if that's something we should encourage to begin with.

An option in the code is the route we took with hiding panics with @capnspacehook, though that is just a single global setting.

pagran commented 4 years ago

I think the function "obfuscate all lines" may be convenient for typical functions where it makes no sense to manually manage obfuscation (error handling, logs, etc.), but it should be as lightweight as possible

capnspacehook commented 4 years ago

The only problem I see with no-op APIs, is that it won't allow wrapping top-level declarations. For example, if I wanted to obfuscate several function declarations in a file, wrapping it in another function call is illegal syntax. I know that garble would replace the calls etc and the code could compile cleanly, but this would disallow any other tools from operating on the source code, such as gofmt for example.

Example code:


package main

import (
    "fmt"
)

garble.ObfuscateBlock{
    func foo() { fmt.Println("foo") }

    func bar() { fmt.Println("bar") }

    func baz() { fmt.Println("baz") }
}

func main() {
    fmt.Println("Hello, playground")
}
mvdan commented 4 years ago

Well, you could do one of:

var secretVar

func init() { garble.ObfuscateBlock(func() { secretVar = ... }) }

func foo() { garble.ObfuscateBlock(func() { body ... }) }

Not ideal, but still better than using comments everywhere, I think.

capnspacehook commented 4 years ago

Why do you prefer that syntax over using comments? To me, comments could be more flexible. We could allow block comments to annotate blocks of code without changing the source code much:


//garble:blockobfuscate
var secretVar ...

func init() { secretVar = ... }

func foo() { ... }
//garble:end
mvdan commented 4 years ago

@capnspacehook because that feels like inventing our own syntax on top of Go's syntax, almost like a meta-syntax that the user has to learn and remember. I would like to avoid that. Comment directives that just affect the following node/statement are OK-ish, but anything more than that feels too icky.

Go already has ways to group syntax: top-level declarations can use (, and statements can use {. So your example above could be like:

//garble:whatever
var (
    ...
    ...
)

//garble:whatever
func init() { secretVar = ... }

The alternative is pure Go syntax like garble.ObfuscateBlock, but then we force the user to move global variable assignments to init statements, which isn't super nice. Perhaps we could instead have them do:

var secretVar = garble.ObfuscateValue(expression...)

I'm thinking outloud here. I don't have a strong opinion overall, but I would like to stick to "plain Go" syntax as much as possible, i.e. ideally without magic comments.