goot is a static analysis framework for Go. goot is easy-to-learn, easy-to-use and highly extensible, allowing you to easily develop new analyses on top of it.
Currently, goot provides the following major analysis components (and more analyses are on the way):
- Control/Data-flow analysis framework
- Control-flow graph construction
- Classic data-flow analyses, e.g. taint analysis
- Your dataflow analyses
Intall goot by
go get -u github.com/cokeBeer/goot
Write code below in the project to be analysed, e.g cmd/taint/main.go
package main
import "github.com/cokeBeer/goot/pkg/example/dataflow/taint"
func main() {
// if this file is cmd/taint/main.go
// and you want to analyse package pkg
// the path should be "../../pkg"
// or "../../pkg..." for all packages under pkg
runner := taint.NewRunner("relative/path/to/package")
// for this project, is "github.com/cokeBeer/goot"
runner.ModuleName = "module-name"
runner.PassThroughDstPath = "passthrough.json"
runner.TaintGraphDstPath = "taintgraph.json"
runner.Run()
}
Run the code, and you will get a passthrough.json
in the same directory, which contains taint passthrough information of all functions in your project
You can see key fmt.Sprintf
holds a value object
{
"fmt.Sprintf": {
"Recv": null,
"Results": [
[0, 1]
],
"Params": [
[0, 1],
[1]
]
}
}
This means three things
- the first parameter's taint and the second parameter's taint are passed to the first return value,
- the first parameter receives the first parameter's taint
- the second parameter receives the second parameter's taint
Also, you will get a taintgraph.json
in the same directory
You can see the json file contains taint edges from one call parameter to another call parameter
{
"(*github.com/example/runnner.Runner).RunCmd#0#(*os/exec.Cmd).StdoutPipe#0": {
"From": "(*github.com/example/runnner.Runner).RunCmd",
"FromIndex": 0,
"To": "(*os/exec.Cmd).StdoutPipe",
"ToIndex": 0,
"ToIsMethod": false,
"ToIsSink": true,
"ToIsSignature": false,
"ToIsStatic": true
}
}
This means there is a taint edge from position 0
of RunCmd
(in this case, the parameter is the receiver runner.Runner
itself ) to position 0
of StdoutPipe
(in this case, the parameter is ther recevier exec.Cmd
iteself, too)
To view taint edges better, you can load them to neo4j by set these parameters (for more detailed options, see options of runner)
func main() {
runner := taint.NewRunner("../../internal...")
runner.ModuleName = "gitlab.com/gitlab-org/gitlab-workhorse"
// parameters about neo4j
runner.PersistToNeo4j = true
runner.Neo4jURI = "bolt://localhost:7687"
runner.Neo4jUsername = "neo4j"
runner.Neo4jPassword = "password"
err := runner.Run()
if err != nil {
log.Fatal(err)
}
}
When analysis is end, you can find nodes and taint edges in your neo4j database
For example, we run taint analysis on gitlab.com/gitlab-org/gitlab-workhorse@v13.10.0,which has a RCE vulnerability CVE-2021-22225
Using query below to find taint paths
MATCH (source:Source),(sink:Sink {name:"os/exec.CommandContext"}),p=(source)-[*7]->(sink) RETURN p
We can get a graph like this: (the red nodes are sink, the brown nodes are intra functions and the green nodes are source)
Which reveals two taint paths from source to sink os/exec.CommandContext
, the same as CVE-2021-22225
To use goot as a framework, first create two structs implementing pkg/toolkits/scalar.FlowAnalysis
interface
// FlowAnalysis represents a flow analysis
type FlowAnalysis interface {
GetGraph() *graph.UnitGraph
IsForward() bool
Computations() int
FlowThrougth(inMap *map[any]any, unit ssa.Instruction, outMap *map[any]any)
NewInitalFlow() *map[any]any
EntryInitalFlow() *map[any]any
Copy(srcMap *map[any]any, dstMap *map[any]any)
MergeInto(Unit ssa.Instruction, inout *map[any]any, in *map[any]any)
End(universe []*entry.Entry)
}
and pkg/golang/switcher.Switcher
interface seperately
// Switcher represents a ssa instruction switcher
type Switcher interface {
CaseAlloc(inst *ssa.Alloc)
CasePhi(inst *ssa.Phi)
CaseCall(inst *ssa.Call)
CaseBinOp(inst *ssa.BinOp)
CaseUnOp(inst *ssa.UnOp)
...
CaseGo(inst *ssa.Go)
CaseDefer(inst *ssa.Defer)
CaseSend(inst *ssa.Send)
CaseStore(inst *ssa.Store)
CaseMapUpdate(inst *ssa.MapUpdate)
CaseDebugRef(inst *ssa.DebugRef)
}
Don't worry for these apis. An easy way to implement them is using compose like pkg/toolkits/scalar.BaseFlowAnalysis
// ConstantPropagationAnalysis represents a constant propagtion analysis
type ConstantPropagationAnalysis struct {
scalar.BaseFlowAnalysis
constantPropagationSwitcher *ConstantPropagationSwitcher
}
and pkg/golang/switcher.BaseSwitcher
// ConstantPropagationSwitcher represents a constant propagtion switcher
type ConstantPropagationSwitcher struct {
switcher.BaseSwitcher
constanctPropagationAnalysis *ConstantPropagationAnalysis
inMap *map[any]any
outMap *map[any]any
}
These can make you focus on the core methods you really need to design carefully in specific analyses
You can learn more information about how to use goot as a framework and how to run an analysis from a tiny example I prepared for you in how to use and how to run which demonstrates a constant propagation analysis
- goot's api is similar to soot, so if you wonder how goot's api work, you can learn soot first
- goot uses
*map[any]any
as flow andssa.Instruction
as unit, so please be careful of type assertion