feat(binana): change tool to deposit generated files into an 'artifacts' folder that isn't retained by repository history

This commit is contained in:
phaneron 2026-02-28 02:34:20 -05:00
parent 68f52b8efd
commit 47b08df145
44 changed files with 904 additions and 622 deletions

42
.github/workflows/push.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Push
on:
push:
paths:
- "Makefile"
- "go.mod"
- "go.sum"
- "go/**"
- "profile/**"
branches:
- artifacts
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version:
"stable" # Uses the latest stable Go version
# checkout repository
- name: Checkout code
uses: actions/checkout@v6
# build binana tool
- name: Make tool
run: make dependencies
# run binana tool
- name: Make artifacts
run: make artifacts
# upload artifacts
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: artifacts
path: artifacts/

2
.gitignore vendored
View file

@ -1,3 +1,5 @@
jest
.vscode
bin
artifacts
profile/*/symbol/main.sym

View file

@ -3,14 +3,11 @@ all: generate
ce-lua:
./script/build-cheatengine-scripts cheatengine profile/3.3.5a-windows-386/cheatengine
compile-symbols:
./script/compile-symbols profile/3.3.5a-windows-386
artifacts:
./bin/bna mk 3.3.5a-windows-386
profile-gen:
./bin/binana generate --profile profile/3.3.5a-windows-386
generate: compile-symbols profile-gen
generate: artifacts
dependencies:
mkdir -p bin
go build -v -o bin/binana github.com/thunderbrewhq/binana/go/cmd/binana
go build -v -o bin/bna github.com/thunderbrewhq/binana/go/cmd/bna

8
go.mod
View file

@ -1,20 +1,18 @@
module github.com/thunderbrewhq/binana
go 1.22.0
go 1.25.5
require (
github.com/fatih/color v1.18.0
github.com/pierrec/lz4/v4 v4.1.21
github.com/spf13/cobra v1.8.1
modernc.org/cc/v3 v3.41.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.25.0 // indirect

6
go.sum
View file

@ -1,14 +1,10 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd h1:EVX1s+XNss9jkRW9K6XGJn2jL2lB1h5H804oKPsxOec=
github.com/ianlancetaylor/demangle v0.0.0-20240912202439-0a2b6291aafd/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@ -16,8 +12,6 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=

11
go/app/app.go Normal file
View file

@ -0,0 +1,11 @@
package app
import (
"fmt"
"os"
)
func Fatal(args ...any) {
fmt.Fprintln(os.Stderr, args...)
os.Exit(1)
}

17
go/app/cmd/execute.go Normal file
View file

@ -0,0 +1,17 @@
package cmd
import (
_ "github.com/thunderbrewhq/binana/go/app/cmd/lint"
_ "github.com/thunderbrewhq/binana/go/app/cmd/make"
_ "github.com/thunderbrewhq/binana/go/app/cmd/tidy"
"github.com/thunderbrewhq/binana/go/app"
"github.com/thunderbrewhq/binana/go/app/cmd/root"
)
func Execute() {
err := root.RootCmd.Execute()
if err != nil {
app.Fatal(err)
}
}

43
go/app/cmd/lint/lint.go Normal file
View file

@ -0,0 +1,43 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/app"
"github.com/thunderbrewhq/binana/go/app/profile"
)
var lint_cmd = cobra.Command{
Use: "lint profile",
Short: "show warnings and coverage for a profile",
Run: lint_func,
}
func init() {
f := lint_cmd.Flags()
f.Bool("bounds", false, "check for bad function boundaries")
f.Bool("constructors", false, "check for outdated class constructor names")
}
func lint_func(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Help()
return
}
f := cmd.Flags()
var params profile.LintParams
params.Profile = args[0]
var err error
params.Constructors, err = f.GetBool("constructors")
if err != nil {
app.Fatal(err)
}
params.Bounds, err = f.GetBool("bounds")
if err != nil {
app.Fatal(err)
}
profile.Lint(&params)
}

32
go/app/cmd/make/make.go Normal file
View file

@ -0,0 +1,32 @@
package make
import (
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/app/cmd/root"
"github.com/thunderbrewhq/binana/go/app/profile"
)
func mk_func(cmd *cobra.Command, args []string) {
compress, err := cmd.Flags().GetBool("compress")
if err != nil {
panic(err)
}
var params profile.MakeParams
params.Profile = args[0]
params.CompressX64dbgDatabase = compress
profile.Make(&params)
}
var mk_cmd = cobra.Command{
Use: "mk profile",
Args: cobra.MinimumNArgs(1),
Short: "Convert source files into various tool formats",
Run: mk_func,
}
func init() {
f := mk_cmd.Flags()
f.BoolP("compress", "c", true, "enable/disable compression of the x64dbg database file")
root.RootCmd.AddCommand(&mk_cmd)
}

8
go/app/cmd/root/root.go Normal file
View file

@ -0,0 +1,8 @@
package root
import "github.com/spf13/cobra"
var RootCmd = cobra.Command{
Use: "bna",
Short: "Binana helper tool",
}

25
go/app/cmd/tidy/tidy.go Normal file
View file

@ -0,0 +1,25 @@
package tidy
import (
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/app/cmd/root"
"github.com/thunderbrewhq/binana/go/app/profile"
)
var tidy_cmd = cobra.Command{
Use: "tidy profile",
Short: "sort all symbol files in a profile",
Args: cobra.MinimumNArgs(1),
Run: tidy_func,
}
func init() {
//f := tidy_cmd.Flags()
root.RootCmd.AddCommand(&tidy_cmd)
}
func tidy_func(cmd *cobra.Command, args []string) {
var params profile.TidyParams
params.Profile = args[0]
profile.Tidy(&params)
}

65
go/app/profile/lint.go Normal file
View file

@ -0,0 +1,65 @@
package profile
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/thunderbrewhq/binana/go/symbols"
)
type linter struct {
warnings uint64
named_functions_count uint64
}
func (linter *linter) warn(s *symbols.TableEntry, f string, args ...any) {
linter.warnings++
color.Set(color.FgRed)
fmt.Printf(" warning: ")
color.Unset()
fmt.Printf(" in %s:%d: %s", s.Filename, s.Linenumber, s.Symbol.Name)
fmt.Printf(f, args...)
}
type LintParams struct {
Profile string
Constructors bool
Bounds bool
}
func Lint(params *LintParams) {
Open(params.Profile)
defer Close()
var linter linter
for entry := range Profile.Symbols.Entries() {
sn := entry.Symbol.Name
if entry.Symbol.Kind == symbols.Function {
linter.named_functions_count++
if params.Constructors {
//
b, a, found := strings.Cut(sn, "__")
if found {
if b == a {
linter.warn(entry, "this style of naming a constructor function is preferred: ClassName__constructor\n")
}
}
}
if params.Bounds {
if entry.Symbol.EndAddress == 0 {
linter.warn(entry, "does not have an end address\n")
}
}
}
}
if Profile.Info.FunctionCount != 0 {
ratio := float64(linter.named_functions_count) / float64(Profile.Info.FunctionCount)
fmt.Printf("%d out of %d functions named (%f%%)\n", linter.named_functions_count, Profile.Info.FunctionCount, ratio*100.0)
fmt.Printf("%d warnings generated\n", linter.warnings)
}
}

20
go/app/profile/make.go Normal file
View file

@ -0,0 +1,20 @@
package profile
import (
"github.com/thunderbrewhq/binana/go/app"
"github.com/thunderbrewhq/binana/go/profile"
)
type MakeParams struct {
Profile string
profile.CompileArtifactsParams
}
func Make(params *MakeParams) {
Open(params.Profile)
defer Close()
if err := Profile.CompileArtifacts(&params.CompileArtifactsParams); err != nil {
app.Fatal(err)
}
}

26
go/app/profile/profile.go Normal file
View file

@ -0,0 +1,26 @@
package profile
import (
"path/filepath"
"github.com/thunderbrewhq/binana/go/app"
"github.com/thunderbrewhq/binana/go/profile"
)
var Profile profile.Profile
func Open(profile_name string) {
if profile_name == "" {
app.Fatal("no profile selected")
}
if err := Profile.Open(
filepath.Join("profile", profile_name),
filepath.Join("artifacts", profile_name),
); err != nil {
app.Fatal(err)
}
}
func Close() {
//Profile.Close()
}

61
go/app/profile/tidy.go Normal file
View file

@ -0,0 +1,61 @@
package profile
import (
"os"
"path/filepath"
"github.com/thunderbrewhq/binana/go/app"
"github.com/thunderbrewhq/binana/go/symbols"
)
type TidyParams struct {
Profile string
}
func tidy_symbol_file(name string, params *TidyParams) (err error) {
var symbol_table symbols.Table
symbol_table.Init()
if err = symbol_table.Load(name); err != nil {
return
}
var f *os.File
f, err = os.Create(name)
if err != nil {
return
}
if _, err = symbol_table.WriteTo(f); err != nil {
f.Close()
return
}
err = f.Close()
return
}
func tidy_symbol_directory(name string, params *TidyParams) (err error) {
var contents []os.DirEntry
contents, err = os.ReadDir(name)
if err != nil {
return
}
for _, content := range contents {
if content.IsDir() {
if err = tidy_symbol_directory(filepath.Join(name, content.Name()), params); err != nil {
return
}
} else {
if err = tidy_symbol_file(filepath.Join(name, content.Name()), params); err != nil {
return
}
}
}
return
}
func Tidy(params *TidyParams) {
// tidy symbols
profile_symbols := filepath.Join("profile", params.Profile, "symbol")
if err := tidy_symbol_directory(profile_symbols, params); err != nil {
app.Fatal(err)
}
}

View file

@ -1,97 +0,0 @@
package cmd
import (
"fmt"
"strings"
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/profile"
"github.com/thunderbrewhq/binana/go/symfile"
)
type check_params struct {
ProfileName string
Constructors bool
Bounds bool
}
func check(params *check_params) {
p, err := profile.Open(params.ProfileName)
if err != nil {
panic(err)
}
named_function_count := uint64(0)
warnings := uint64(0)
warn := func(s *symfile.Entry, f string, args ...any) {
warnings++
color.Set(color.FgRed)
fmt.Printf(" warning: ")
color.Unset()
fmt.Printf("in line %d: (%s): ", s.LineNumber, s.Name)
fmt.Printf(f, args...)
}
for _, s := range p.SymbolTable.Entries {
sn := s.Name
if s.Kind == symfile.Function {
named_function_count++
if params.Constructors {
//
b, a, found := strings.Cut(sn, "__")
if found {
if b == a {
warn(&s, "this style of naming a constructor function is preferred: ClassName__constructor\n")
}
}
}
if params.Bounds {
if s.EndAddress == 0 {
warn(&s, "does not have an end address\n")
}
}
}
}
if p.Info.FunctionCount != 0 {
ratio := float64(named_function_count) / float64(p.Info.FunctionCount)
fmt.Printf("%d out of %d functions named (%f%%)\n", named_function_count, p.Info.FunctionCount, ratio*100.0)
fmt.Printf("%d warnings generated\n", warnings)
}
}
func check_run(cmd *cobra.Command, args []string) {
if len(args) < 1 {
cmd.Help()
return
}
f := cmd.Flags()
var params check_params
params.ProfileName = args[0]
var err error
params.Constructors, err = f.GetBool("constructors")
if err != nil {
panic(err)
}
params.Bounds, err = f.GetBool("bounds")
if err != nil {
panic(err)
}
check(&params)
}
var check_cmd = cobra.Command{
Use: "check profile",
Short: "check a profile for correctness",
Run: check_run,
}

View file

@ -1,92 +0,0 @@
package cmd
import (
"bufio"
"fmt"
"io"
"os"
"strings"
"github.com/ianlancetaylor/demangle"
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/symfile"
)
func demangle_symbol_name(name string) (bn_name string, dm_name string, type_str string, err error) {
// var ast demangle.AST
// ast, err = demangle.ToAST(name)
// if err != nil {
// bn_name = name
// err = nil
// return
// }
// var name []string
// var args []string
// ast.Traverse(func(a demangle.AST) bool {
// if fn_type, ok := a.(*demangle.FunctionType); ok {
// args = append(args, fn_)
// }
// })
var dm_err error
dm_name, dm_err = demangle.ToString(name)
if dm_err != nil {
bn_name = name
return
}
bn_name = dm_name
if strings.Contains(bn_name, "(") {
var arg string
bn_name, arg, _ = strings.Cut(bn_name, "(")
type_str = fmt.Sprintf("uint32_t func(%s", arg)
}
bn_name = strings.ReplaceAll(bn_name, "::", "__")
bn_name = strings.ReplaceAll(bn_name, "<", "_")
bn_name = strings.ReplaceAll(bn_name, ">", "")
return
}
func format_symbol_entry(entry *symfile.Entry, out io.Writer) {
outentry := *entry
if outentry.Name[0] == '_' && outentry.Name[1] != 'Z' {
outentry.Name = outentry.Name[1:]
}
bn_name, dm_name, _, err := demangle_symbol_name(outentry.Name)
if err == nil {
outentry.Name = bn_name
outentry.Comment = dm_name
// if outentry.DataType == "" {
// outentry.DataType = dm_type
// }
}
out.Write([]byte(outentry.String()))
out.Write([]byte("\n"))
}
func format_symbols(in io.Reader, out io.Writer) {
scanner := bufio.NewScanner(in)
for scanner.Scan() {
var entry symfile.Entry
if err := entry.Parse(scanner.Text()); err != nil {
fmt.Println(err)
os.Exit(1)
}
format_symbol_entry(&entry, out)
}
}
func format_symbols_run(cmd *cobra.Command, args []string) {
format_symbols(os.Stdin, os.Stdout)
}
var format_symbols_cmd = cobra.Command{
Use: "format-symbols",
Short: "format symbols from stdin",
Run: format_symbols_run,
}

View file

@ -1,37 +0,0 @@
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/profile"
)
func generate_run(cmd *cobra.Command, args []string) {
compress, err := cmd.Flags().GetBool("compress")
if err != nil {
panic(err)
}
game_profile_directory, err := cmd.Flags().GetString("profile")
if err != nil {
panic(err)
}
game_profile, err := profile.Open(game_profile_directory)
if err != nil {
panic(err)
}
if err = game_profile.Generate(compress); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
var generate_cmd = cobra.Command{
Use: "generate",
Short: "Convert source files into various tool formats",
Run: generate_run,
}

View file

@ -1,38 +0,0 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// rootCmd represents the base command when called without any subcommands
var root = cobra.Command{
Use: "binana",
Short: "Binana helper tool",
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := root.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
generate_cmd.Flags().StringP("profile", "p", "3.3.5a", "the game profile")
generate_cmd.Flags().BoolP("compress", "c", true, "enable/disable compression of the x64dbg database file")
check_cmd.Flags().Bool("bounds", false, "check for bad function boundaries")
check_cmd.Flags().Bool("constructors", false, "check for outdated class constructor names")
root.AddCommand(&generate_cmd)
root.AddCommand(&format_symbols_cmd)
root.AddCommand(&x64dbg_typesort_cmd)
root.AddCommand(&check_cmd)
}

View file

@ -1,32 +0,0 @@
package cmd
import (
"encoding/json"
"os"
"github.com/spf13/cobra"
"github.com/thunderbrewhq/binana/go/x64dbg"
)
var x64dbg_typesort_cmd = cobra.Command{
Use: "x64dbg-typesort [types.json file]",
Short: "sort a x64dbg types file",
Run: x64dbg_typesort_run,
}
func x64dbg_typesort_run(cmd *cobra.Command, args []string) {
types, err := x64dbg.LoadTypes(args[0])
if err != nil {
panic(err)
}
if err = x64dbg.SortTypes(types); err != nil {
panic(err)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err = encoder.Encode(types); err != nil {
panic(err)
}
}

View file

@ -1,7 +0,0 @@
package main
import "github.com/thunderbrewhq/binana/go/cmd/binana/cmd"
func main() {
cmd.Execute()
}

7
go/cmd/bna/main.go Normal file
View file

@ -0,0 +1,7 @@
package main
import "github.com/thunderbrewhq/binana/go/app/cmd"
func main() {
cmd.Execute()
}

View file

@ -0,0 +1,35 @@
package profile
import (
"fmt"
"slices"
)
type CompileArtifactsParams struct {
CompressX64dbgDatabase bool
}
func (profile *Profile) CompileArtifacts(params *CompileArtifactsParams) (err error) {
if !profile.loaded {
panic(profile.loaded)
}
compilers := []struct {
OS []string
Name string
Fn func(*Profile, *CompileArtifactsParams) error
}{
{[]string{"windows", "linux", "darwin"}, "Ghidra", compile_ghidra_artifacts},
{[]string{"windows", "linux", "darwin"}, "IDA Pro", compile_idapro_artifacts},
{[]string{"windows"}, "x64dbg", compile_x64dbg_artifacts},
}
for _, compiler := range compilers {
if slices.Contains(compiler.OS, profile.Info.OS) {
fmt.Println("compiling artifacts for", compiler.Name)
if err = compiler.Fn(profile, params); err != nil {
return
}
}
}
return
}

View file

@ -0,0 +1,21 @@
package profile
import (
"os"
"path/filepath"
)
func compile_ghidra_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) {
ghidra_path := filepath.Join(profile.ArtifactsDirectory, "ghidra")
if err = os.MkdirAll(ghidra_path, 0755); err != nil {
return
}
var symbol_file *os.File
symbol_file, err = os.Create(filepath.Join(ghidra_path, "all.sym"))
if err != nil {
return
}
_, err = profile.Symbols.WriteTo(symbol_file)
symbol_file.Close()
return
}

View file

@ -1,12 +1,27 @@
package profile
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/thunderbrewhq/binana/go/symfile"
"github.com/thunderbrewhq/binana/go/symbols"
)
var (
//go:embed ida/import_all.idc
import_all_script []byte
//go:embed ida/import_data_types.idc
import_data_types_script []byte
//go:embed ida/import_functions.idc
import_functions_script []byte
//go:embed ida/import_symbols.idc
import_symbols_script []byte
)
type idac_import_batch struct {
@ -64,8 +79,12 @@ func (b *idac_import_batch) Close() (err error) {
}
func (profile *Profile) create_idac_import_batch(name string) (b *idac_import_batch, err error) {
batch_path := filepath.Join(profile.ArtifactsDirectory, "ida", "batch")
if err = os.MkdirAll(batch_path, 0755); err != nil {
return
}
b = new(idac_import_batch)
b.fd, err = os.Create(filepath.Join(profile.Directory, "ida", "batch", name+".idc"))
b.fd, err = os.Create(filepath.Join(batch_path, name+".idc"))
if err != nil {
return
}
@ -73,8 +92,7 @@ func (profile *Profile) create_idac_import_batch(name string) (b *idac_import_ba
return
}
func (profile *Profile) generate_symbols_idc() (err error) {
// symbols
func compile_idapro_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) {
var b *idac_import_batch
b, err = profile.create_idac_import_batch("import_symbols")
if err != nil {
@ -88,8 +106,8 @@ func (profile *Profile) generate_symbols_idc() (err error) {
name_instances := make(map[string]int)
for _, symbol := range profile.SymbolTable.Entries {
name := symbol.Name
for symbol := range profile.Symbols.Entries() {
name := symbol.Symbol.Name
instances := name_instances[name]
name_instances[name] = instances + 1
@ -98,7 +116,7 @@ func (profile *Profile) generate_symbols_idc() (err error) {
}
quoted_name := strconv.Quote(name)
address := fmt.Sprintf("0x%08X", symbol.StartAddress)
address := fmt.Sprintf("0x%08X", symbol.Symbol.StartAddress)
b.P("set_name(%s, %s);", address, quoted_name)
}
b.T(0)
@ -117,10 +135,10 @@ func (profile *Profile) generate_symbols_idc() (err error) {
b.P("static import_data_types() {")
b.T(1)
b.P("// Give types to data labels")
for _, symbol := range profile.SymbolTable.Entries {
if symbol.DataType != "" {
quoted_data_type := strconv.Quote(symbol.DataType)
address := fmt.Sprintf("0x%08X", symbol.StartAddress)
for symbol := range profile.Symbols.Entries() {
if symbol.Symbol.DataType != "" {
quoted_data_type := strconv.Quote(symbol.Symbol.DataType)
address := fmt.Sprintf("0x%08X", symbol.Symbol.StartAddress)
b.P("apply_type(%s, %s);", address, quoted_data_type)
}
}
@ -141,16 +159,16 @@ func (profile *Profile) generate_symbols_idc() (err error) {
b.T(1)
b.P("// Import function addresses and comments")
b.P(`msg("Importing function addresses and comments");`)
for _, function_symbol := range profile.SymbolTable.Entries {
if function_symbol.Kind == symfile.Function {
address := fmt.Sprintf("0x%08X", function_symbol.StartAddress)
for function_symbol := range profile.Symbols.Entries() {
if function_symbol.Symbol.Kind == symbols.Function {
address := fmt.Sprintf("0x%08X", function_symbol.Symbol.StartAddress)
// b.P("set_func_start(%s, %s);", address, address)
// if function_symbol.EndAddress != 0 {
// end_address := fmt.Sprintf("0x%08X", function_symbol.EndAddress)
// b.P("set_func_end(%s, %s);", address, end_address)
// }
if function_symbol.Comment != "" {
b.P("set_func_cmt(%s, %s, 0);", address, strconv.Quote(function_symbol.Comment))
if function_symbol.Symbol.Comment != "" {
b.P("set_func_cmt(%s, %s, 0);", address, strconv.Quote(function_symbol.Symbol.Comment))
}
}
}
@ -159,10 +177,23 @@ func (profile *Profile) generate_symbols_idc() (err error) {
b.Close()
return
}
// include stock scripts
ida_path := filepath.Join(profile.ArtifactsDirectory, "ida")
if err = os.MkdirAll(ida_path, 0755); err != nil {
return
}
if err = os.WriteFile(filepath.Join(ida_path, "import_all.idc"), import_all_script, 0755); err != nil {
return
}
if err = os.WriteFile(filepath.Join(ida_path, "import_data_types.idc"), import_data_types_script, 0755); err != nil {
return
}
if err = os.WriteFile(filepath.Join(ida_path, "import_functions.idc"), import_functions_script, 0755); err != nil {
return
}
if err = os.WriteFile(filepath.Join(ida_path, "import_symbols.idc"), import_symbols_script, 0755); err != nil {
return
}
func (profile *Profile) CreateIDAFiles() (err error) {
err = profile.generate_symbols_idc()
return
}

View file

@ -0,0 +1,9 @@
package profile
func compile_x64dbg_artifacts(profile *Profile, params *CompileArtifactsParams) (err error) {
if err = compile_x64dbg_types(profile); err != nil {
return
}
err = compile_x64dbg_database(profile, params.CompressX64dbgDatabase)
return
}

View file

@ -2,9 +2,10 @@ package profile
import (
"fmt"
"os"
"path/filepath"
"github.com/thunderbrewhq/binana/go/symfile"
"github.com/thunderbrewhq/binana/go/symbols"
"github.com/thunderbrewhq/binana/go/x64dbg"
)
@ -12,7 +13,7 @@ func hex_address(u uint64) string {
return fmt.Sprintf("0x%x", u)
}
func (profile *Profile) generate_x64dbg_database(compress bool) (err error) {
func compile_x64dbg_database(profile *Profile, compress bool) (err error) {
// Convert symbol table into x64dbg database
is_64bit := profile.Info.Arch == "amd64"
@ -22,30 +23,30 @@ func (profile *Profile) generate_x64dbg_database(compress bool) (err error) {
var dd x64dbg.Database
for _, entry := range profile.SymbolTable.Entries {
relative_start_address := entry.StartAddress - base_address
for entry := range profile.Symbols.Entries() {
relative_start_address := entry.Symbol.StartAddress - base_address
relative_end_address := relative_start_address
if entry.EndAddress != 0 {
relative_end_address = entry.EndAddress - base_address
if entry.Symbol.EndAddress != 0 {
relative_end_address = entry.Symbol.EndAddress - base_address
// for x64dbg, the end address is the last instruction.
// for us, the end address is the address immediately after the last instruction.
relative_end_address -= 1
}
if relative_end_address < relative_start_address || relative_end_address-relative_start_address >= 50000 {
fmt.Printf("Strange symbol %s %08x %08x (offset %d)\n", entry.Name, relative_start_address, relative_end_address, relative_end_address-relative_start_address)
fmt.Printf("Strange symbol %s %08x %08x (offset %d)\n", entry.Symbol.Name, relative_start_address, relative_end_address, relative_end_address-relative_start_address)
}
// create label
var label x64dbg.Label
label.Manual = true
label.Address = hex_address(relative_start_address)
label.Text = entry.Name
label.Text = entry.Symbol.Name
label.Module = module_name
dd.Labels = append(dd.Labels, label)
if entry.Kind == symfile.Function {
if entry.Symbol.Kind == symbols.Function {
var fn x64dbg.Function
fn.Manual = true
fn.Start = hex_address(relative_start_address)
@ -66,8 +67,11 @@ func (profile *Profile) generate_x64dbg_database(compress bool) (err error) {
}
// save database
dd_path := filepath.Join(profile.Directory, "x64dbg", filename)
if err = x64dbg.SaveDatabase(dd_path, &dd, compress); err != nil {
dd_path := filepath.Join(profile.ArtifactsDirectory, "x64dbg")
if err = os.MkdirAll(dd_path, 0755); err != nil {
return
}
if err = x64dbg.SaveDatabase(filepath.Join(dd_path, filename), &dd, compress); err != nil {
return
}

View file

@ -2,6 +2,7 @@ package profile
import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
@ -98,7 +99,7 @@ loop:
}
// parses the C headers and generates a matching x64dbg types.json file
func (profile *Profile) generate_x64dbg_types() (err error) {
func compile_x64dbg_types(profile *Profile) (err error) {
// parse C headers
var cc_config cc.Config
cc_config.ABI, err = cc.NewABI("windows", profile.Info.Arch)
@ -231,7 +232,11 @@ func (profile *Profile) generate_x64dbg_types() (err error) {
x64_types.Structs = append(x64_types.Structs, x64_struct)
}
types_file_path := filepath.Join(profile.Directory, "x64dbg", "types.json")
dd_path := filepath.Join(profile.ArtifactsDirectory, "x64dbg")
if err = os.MkdirAll(dd_path, 0755); err != nil {
return
}
types_file_path := filepath.Join(dd_path, "types.json")
err = x64dbg.SortTypes(&x64_types)
if err != nil {

View file

@ -1,15 +0,0 @@
package profile
func (profile *Profile) Generate(compress_db bool) (err error) {
if err = profile.CreateIDAFiles(); err != nil {
return
}
if profile.Info.OS == "windows" {
if err = profile.CreateX64dbgFiles(compress_db); err != nil {
return
}
}
return
}

View file

@ -0,0 +1,13 @@
#include <idc.idc>
#include "batch/import_symbols.idc"
#include "batch/import_data_types.idc"
#include "batch/import_functions.idc"
static main() {
// Import all
import_symbols();
import_data_types();
import_functions();
// Wait for auto-analysis to be finished
auto_wait();
}

View file

@ -0,0 +1,6 @@
#include "batch/import_data_types.idc"
static main() {
import_data_types();
auto_wait();
}

View file

@ -0,0 +1,5 @@
#include "batch/import_functions.idc"
static main() {
import_functions();
}

View file

@ -0,0 +1,5 @@
#include "batch/import_symbols.idc"
static main() {
import_symbols();
}

View file

@ -6,16 +6,18 @@ import (
"os"
"path/filepath"
"github.com/thunderbrewhq/binana/go/symfile"
"github.com/thunderbrewhq/binana/go/symbols"
)
type Profile struct {
Info Info
Directory string
SymbolTable *symfile.InMemoryTable
loaded bool
Info Info
Directory string
ArtifactsDirectory string
Symbols symbols.Table
}
func Open(profile_directory string) (profile *Profile, err error) {
func (profile *Profile) Open(profile_directory, artifacts_directory string) (err error) {
var dir fs.FileInfo
dir, err = os.Stat(profile_directory)
if err != nil {
@ -27,31 +29,23 @@ func Open(profile_directory string) (profile *Profile, err error) {
return
}
fmt.Println("Opening profile", profile_directory)
fmt.Println("opening profile", profile_directory)
profile = new(Profile)
profile.Symbols.Init()
profile.Directory = profile_directory
profile.ArtifactsDirectory = artifacts_directory
// read profile meta info
if err = read_info(filepath.Join(profile_directory, "info.json"), &profile.Info); err != nil {
return
}
profile.Directory = profile_directory
path_to_symbols_file := filepath.Join(profile_directory, "symbol", "main.sym")
var symbols_file *os.File
symbols_file, err = os.Open(path_to_symbols_file)
if err != nil {
// read symbols directory
if err = profile.Symbols.Load(filepath.Join(profile_directory, "symbol")); err != nil {
return
}
profile.SymbolTable = new(symfile.InMemoryTable)
profile.loaded = true
if err = symfile.Load(profile.SymbolTable, symbols_file); err != nil {
return
}
symbols_file.Close()
//
return
}

View file

@ -1,13 +0,0 @@
package profile
func (profile *Profile) CreateX64dbgFiles(compress_db bool) (err error) {
if err = profile.generate_x64dbg_database(compress_db); err != nil {
return
}
if err = profile.generate_x64dbg_types(); err != nil {
return
}
return
}

8
go/symbols/error.go Normal file
View file

@ -0,0 +1,8 @@
package symbols
import "fmt"
var (
ErrDuplicateSymbol = fmt.Errorf("symbols: a symbol already exists at that address")
ErrLineLeftEmpty = fmt.Errorf("symbols: line was left empty")
)

79
go/symbols/loader.go Normal file
View file

@ -0,0 +1,79 @@
package symbols
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
)
const min_columns = 3
type loader struct {
filename string
input *bufio.Reader
table *Table
line_number int
}
func (l *loader) read_line() (line string, err error) {
line, err = l.input.ReadString('\n')
if err != nil {
return
}
line = strings.TrimRight(line, "\r\n")
l.line_number++
return
}
func (l *loader) parse_line(line string) (err error) {
var table_entry TableEntry
table_entry.Filename = l.filename
table_entry.Linenumber = l.line_number
if line == "" {
//err = fmt.Errorf("%w: %s:%d", ErrLineLeftEmpty, table_entry.Filename, table_entry.Linenumber)
return
}
if err = table_entry.Symbol.Parse(line); err != nil {
err = fmt.Errorf("%w: %s:%d", err, table_entry.Filename, table_entry.Linenumber)
return
}
err = l.table.Insert(&table_entry)
return
}
func load(filename string, file io.Reader, table *Table) (err error) {
l := new(loader)
l.input = bufio.NewReader(file)
l.filename = filename
l.table = table
var (
line string
)
for {
line, err = l.read_line()
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
} else {
err = fmt.Errorf("symbols: error reading at line %d: %w", l.line_number, err)
return
}
}
if err = l.parse_line(line); err != nil {
err = fmt.Errorf("symbols: error parsing at line %d: %w", l.line_number, err)
return
}
}
return
}

View file

@ -1,4 +1,4 @@
package symfile
package symbols
import (
"fmt"
@ -69,7 +69,7 @@ func parse_attributes(attribute_columns []string) (attributes map[string]string,
return
}
func (entry *Entry) Parse(line string) (err error) {
func (symbol *Symbol) Parse(line string) (err error) {
// trim extraneous whitespace
line = strings.Trim(line, " \t")
@ -89,7 +89,7 @@ func (entry *Entry) Parse(line string) (err error) {
// get name of symbol
name_column := columns[0]
if name_column == "" {
return fmt.Errorf("symfile: (*entry).Parse: entry has invalid name '%s", name_column)
return fmt.Errorf("symbols: (*Symbol).Parse: entry has invalid name '%s", name_column)
}
start_address, err = strconv.ParseUint(columns[1], 16, 64)
@ -99,10 +99,10 @@ func (entry *Entry) Parse(line string) (err error) {
kind_column := columns[2]
if len(kind_column) != 1 {
return fmt.Errorf("symfile: (*entry).Parse: entry has invalid kind")
return fmt.Errorf("symbols: (*Symbol).Parse: entry has invalid kind")
}
kind := EntryKind(kind_column[0])
kind := SymbolKind(kind_column[0])
if !slices.Contains(valid_kinds, kind) {
return fmt.Errorf("symfile: (*entry).Parse: entry has invalid kind")
@ -122,10 +122,10 @@ func (entry *Entry) Parse(line string) (err error) {
}
// Start to build entry
entry.Name = name_column
entry.StartAddress = start_address
entry.Kind = kind
entry.Comment = comment_text
symbol.Name = name_column
symbol.StartAddress = start_address
symbol.Kind = kind
symbol.Comment = comment_text
// build attributes
if num_semantic_columns > 3 {
@ -138,11 +138,11 @@ func (entry *Entry) Parse(line string) (err error) {
}
if data_type, found := attributes["type"]; found {
entry.DataType = data_type
symbol.DataType = data_type
}
if end_address, found := attributes["end"]; found {
entry.EndAddress, err = strconv.ParseUint(end_address, 16, 64)
symbol.EndAddress, err = strconv.ParseUint(end_address, 16, 64)
if err != nil {
return
}

77
go/symbols/symbol.go Normal file
View file

@ -0,0 +1,77 @@
package symbols
import (
"fmt"
"io"
"strings"
)
// What kind of Symbol is this?
type SymbolKind uint8
const (
// Something that can be executed
Function SymbolKind = 'f'
// Something that is read or written to
DataLabel SymbolKind = 'l'
)
var (
valid_kinds = []SymbolKind{Function, DataLabel}
)
// An entry in the table
type Symbol struct {
// Undecorated, raw name
Name string
// Offset to the start of the function or data
StartAddress uint64
// What kind of Symbol is this?
Kind SymbolKind
// Any table entry can have a comment after a ';' column
Comment string
// Attributes
// end=AABBCCEEDD
EndAddress uint64
// type=void*
// The C syntax type of the data
DataType string
}
func (entry *Symbol) String() string {
var b strings.Builder
entry.WriteTo(&b)
return b.String()
}
func (entry *Symbol) WriteTo(w io.Writer) (n int64, err error) {
var b int
b, err = fmt.Fprintf(w, "%s %08X %c", entry.Name, entry.StartAddress, entry.Kind)
if err != nil {
return
}
n += int64(b)
if entry.EndAddress != 0 {
b, err = fmt.Fprintf(w, " end=%08X", entry.EndAddress)
if err != nil {
return
}
n += int64(b)
}
if entry.DataType != "" {
b, err = fmt.Fprintf(w, " type=\"%s\"", entry.DataType)
if err != nil {
return
}
n += int64(b)
}
if entry.Comment != "" {
b, err = fmt.Fprintf(w, " ; %s", entry.Comment)
if err != nil {
return
}
n += int64(b)
}
return
}

145
go/symbols/table.go Normal file
View file

@ -0,0 +1,145 @@
package symbols
import (
"fmt"
"io"
"iter"
"os"
"path/filepath"
"slices"
"strings"
)
type TableEntry struct {
// the file where the symbol appears
Filename string
// the line number where the symbol appears in the file
Linenumber int
Symbol Symbol
}
// Most tables are reasonably sized and can be kept in memory
type Table struct {
entries map[uint64]*TableEntry
}
func (table *Table) Init() {
table.entries = make(map[uint64]*TableEntry)
}
// Open opens a symbol file or a tree of symbol files.
func (table *Table) Load(name string) (err error) {
var (
root_info os.FileInfo
)
root_info, err = os.Stat(name)
if err != nil {
return
}
if root_info.IsDir() {
var tree_entries []os.DirEntry
tree_entries, err = os.ReadDir(name)
if err != nil {
return
}
slices.SortFunc(tree_entries, func(a, b os.DirEntry) int {
return strings.Compare(a.Name(), b.Name())
})
for _, tree_entry := range tree_entries {
tree_child_name := filepath.Join(name, tree_entry.Name())
if err = table.Load(tree_child_name); err != nil {
return
}
}
} else {
var symbol_file *os.File
symbol_file, err = os.Open(name)
if err != nil {
return
}
err = table.LoadFile(name, symbol_file)
symbol_file.Close()
if err != nil {
return
}
}
return
}
func (table *Table) Entries() iter.Seq[*TableEntry] {
sorted_entries := make([]*TableEntry, len(table.entries))
var i int
for _, entry := range table.entries {
sorted_entries[i] = entry
i++
}
slices.SortFunc(sorted_entries, func(a, b *TableEntry) int {
if a.Symbol.StartAddress < b.Symbol.StartAddress {
return -1
} else if a.Symbol.StartAddress == b.Symbol.StartAddress {
return 0
} else {
return 1
}
})
return func(yield func(*TableEntry) bool) {
for _, entry := range sorted_entries {
if !yield(entry) {
break
}
}
}
}
// LoadFile reads all symbols from the input stream
func (table *Table) LoadFile(name string, file io.Reader) (err error) {
err = load(name, file, table)
return
}
func (table *Table) Insert(entry *TableEntry) (err error) {
conflicting_symbol, ok := table.entries[entry.Symbol.StartAddress]
if ok {
err = fmt.Errorf(
"%w: cannot insert symbol '%s' (%s:%d) because another symbol '%s' (%s:%d) already exists at that address '%08x'",
ErrDuplicateSymbol,
entry.Symbol.Name,
entry.Filename,
entry.Linenumber,
conflicting_symbol.Symbol.Name,
conflicting_symbol.Filename,
conflicting_symbol.Linenumber,
conflicting_symbol.Symbol.StartAddress,
)
return
}
table.entries[entry.Symbol.StartAddress] = entry
return
}
func (table *Table) WriteTo(w io.Writer) (n int64, err error) {
for entry := range table.Entries() {
var b int64
b, err = entry.Symbol.WriteTo(w)
if err != nil {
return
}
n += b
_, err = w.Write([]byte("\n"))
if err != nil {
return
}
n++
}
return
}
func (table *Table) Len() int {
return len(table.entries)
}

View file

@ -1,70 +0,0 @@
package symfile
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
)
const min_columns = 3
type loader struct {
input *bufio.Reader
table Table
line_number uint64
}
func (l *loader) read_line() (line string, err error) {
l.line_number++
line, err = l.input.ReadString('\n')
if err != nil {
return
}
line = strings.TrimRight(line, "\r\n")
return
}
func (l *loader) parse_line(line string) (err error) {
var entry Entry
if err = entry.Parse(line); err != nil {
err = fmt.Errorf("%w: line %d", err, l.line_number)
return
}
entry.LineNumber = l.line_number
err = l.table.Insert(&entry)
return
}
func load(text io.Reader, table Table) (err error) {
l := new(loader)
l.input = bufio.NewReader(text)
l.table = table
var (
line string
)
for {
line, err = l.read_line()
if err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
} else {
err = fmt.Errorf("symfile: error reading at line %d: %w", l.line_number, err)
return
}
}
if err = l.parse_line(line); err != nil {
err = fmt.Errorf("symfile: error parsing at line %d: %w", l.line_number, err)
return
}
}
return
}

View file

@ -1,38 +0,0 @@
package symfile
import (
"fmt"
"slices"
"sort"
)
// Most tables are reasonably-sized and can be kept in memory
type InMemoryTable struct {
Entries []Entry
}
func (t *InMemoryTable) Insert(entry *Entry) (err error) {
i := sort.Search(len(t.Entries), func(i int) bool {
return t.Entries[i].StartAddress >= entry.StartAddress
})
if i < len(t.Entries) {
if t.Entries[i].StartAddress == entry.StartAddress {
err = fmt.Errorf("symfile: (*InMemoryTable).Insert() failed: duplicate entry: %s", entry.Name)
return
}
t.Entries = slices.Insert(t.Entries, i, *entry)
} else {
t.Entries = append(t.Entries, *entry)
}
return
}
func (t *InMemoryTable) Len() int {
return len(t.Entries)
}
func NewInMemoryTable() *InMemoryTable {
return new(InMemoryTable)
}

View file

@ -1,64 +0,0 @@
package symfile
import (
"fmt"
"io"
"strings"
)
// What kind of Entry is this?
type EntryKind uint8
const (
// Something that can be executed
Function EntryKind = 'f'
// Something that is read or written to
DataLabel EntryKind = 'l'
)
var (
valid_kinds = []EntryKind{Function, DataLabel}
)
// An entry in the table
type Entry struct {
LineNumber uint64
// Undecorated, raw name
Name string
// Offset to the start of the function or data
StartAddress uint64
// What kind of Entry is this?
Kind EntryKind
// Any table entry can have a comment after a ';' column
Comment string
// Attributes
// end=AABBCCEEDD
EndAddress uint64
// type=void*
// The C syntax type of the data
DataType string
}
func (entry *Entry) String() string {
var b strings.Builder
fmt.Fprintf(&b, "%s %08X %c", entry.Name, entry.StartAddress, entry.Kind)
if entry.EndAddress != 0 {
fmt.Fprintf(&b, " end=%08X", entry.EndAddress)
}
if entry.DataType != "" {
fmt.Fprintf(&b, " type=\"%s\"", entry.DataType)
}
if entry.Comment != "" {
fmt.Fprintf(&b, " ; %s", entry.Comment)
}
return b.String()
}
type Table interface {
Insert(entry *Entry) (err error)
}
func Load(table Table, text io.Reader) (err error) {
err = load(text, table)
return
}