Skip to content

Commit

Permalink
feat: add TUI
Browse files Browse the repository at this point in the history
  • Loading branch information
NSEcho committed Jan 6, 2024
1 parent e7de1ca commit 8edba8b
Show file tree
Hide file tree
Showing 10 changed files with 496 additions and 103 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# furlzz

![Running against Telegram](telegram.png)

furlzz is a small fuzzer written to test out iOS URL schemes.
It does so by attaching to the application using Frida and based on the input/seed it mutates the data
and tries to open the mutated URL. furlzz works in-process, meaning you aren't actually opening
Expand Down Expand Up @@ -57,7 +59,7 @@ encode on the mutated input while the second one generates base64 from it.
6. Adjust timeout if you would like to go with slower fuzzing
7. If the crash happen, replay it with `furlzz crash` passing created session and crash files
![Running against Telegram](telegram.png)
# Mutations
Expand Down
235 changes: 138 additions & 97 deletions cmd/fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package cmd

import (
"context"
"errors"
"fmt"
"github.com/fatih/color"
tea "github.com/charmbracelet/bubbletea"
"github.com/frida/frida-go/frida"
"github.com/nsecho/furlzz/internal/tui"
"github.com/nsecho/furlzz/mutator"
"github.com/spf13/cobra"
"os"
Expand All @@ -24,19 +24,12 @@ var fuzzCmd = &cobra.Command{
if err != nil {
return err
}
if base == "" {
return errors.New("base URL cannot be empty")
}

input, err := cmd.Flags().GetString("input")
if err != nil {
return err
}

if input == "" && strings.Contains(base, "FUZZ") {
return errors.New("input directory cannot be empty when using FUZZ keyword")
}

if strings.Contains(base, "FUZZ") {
validInputs, err = readInputs(input)
if err != nil {
Expand Down Expand Up @@ -84,118 +77,166 @@ var fuzzCmd = &cobra.Command{
return err
}

l.Infof("Fuzzing base URL \"%s\"", base)
if strings.Contains(base, "FUZZ") {
l.Infof("Read %d inputs from %s directory",
len(validInputs), input)
} else {
l.Infof("Fuzzing base URL")
}

if runs == 0 {
l.Infof("Fuzzing indefinitely")
} else {
l.Infof("Fuzzing with %d mutated inputs", runs)
}

if timeout != 0 {
l.Infof("Sleeping %d seconds between each fuzz case", timeout)
}

app, err := cmd.Flags().GetString("app")
if err != nil {
return err
}

if app == "" {
return errors.New("error: app cannot be empty")
}

dev := frida.USBDevice()
if dev == nil {
return errors.New("no USB device detected")
}
defer dev.Clean()
m := tui.NewModel()
m.Crash = crash
m.Runs = runs
m.Timeout = timeout
m.App = app
m.Device = "usb"
m.Function = fn
m.Method = method
m.Delegate = delegate
m.UIApp = uiapp
m.Scene = scene
m.Base = base
m.Input = input
m.ValidInputs = validInputs

p := tea.NewProgram(m)

var sess *frida.Session = nil
var script *frida.Script = nil
hasCrashed := false

go func() {
<-m.DetachCH
sendStats(p, "Unloading script")
if script != nil {
script.Unload()
}
sendStats(p, "Detaching session")
if sess != nil {
sess.Detach()
}

sess, err := dev.Attach(app, nil)
if err != nil {
return err
}
}()

l.Infof("Attached to %s", app)
go func() {
if base == "" {
sendErr(p, "Base cannot be empty")
return
}

var lastInput string
if input == "" && strings.Contains(base, "FUZZ") {
sendErr(p, "Input directory cannot be empty")
return
}

sess.On("detached", func(reason frida.SessionDetachReason, crash *frida.Crash) {
l.Infof("Session detached; reason=%s", reason.String())
out := fmt.Sprintf("fcrash_%s_%s", app, crashSHA256(lastInput))
err := func() error {
f, err := os.Create(out)
if err != nil {
return err
}
f.WriteString(lastInput)
return nil
}()
if err != nil {
l.Errorf("Error writing crash file: %v", err)
} else {
l.Infof("Written crash to: %s", out)
if app == "" {
sendErr(p, "App cannot be empty")
return
}
s := Session{
App: app,
Base: base,
Delegate: delegate,
Function: fn,
Method: method,
Scene: scene,
UIApp: uiapp,

dev := frida.USBDevice()
if dev == nil {
sendErr(p, "No USB device detected")
return
}
if err := s.WriteToFile(); err != nil {
l.Errorf("Error writing session file: %v", err)
} else {
l.Infof("Written session file")
defer dev.Clean()

sendStats(p, "Attached to USB device")
sendStats(p, fmt.Sprintf("Reading inputs from %s", input))

sess, err = dev.Attach(app, nil)
if err != nil {
sendErr(p, err.Error())
return
}
os.Exit(1)
})

script, err := sess.CreateScript(scriptContent)
if err != nil {
return err
}
sendStats(p, fmt.Sprintf("Attached to %s", app))

var lastInput string

sess.On("detached", func(reason frida.SessionDetachReason, crash *frida.Crash) {
if hasCrashed {
sendStats(p, fmt.Sprintf("Session detached; reason=%s", reason.String()))
out := fmt.Sprintf("fcrash_%s_%s", app, crashSHA256(lastInput))
err := func() error {
f, err := os.Create(out)
if err != nil {
return err
}
f.WriteString(lastInput)
return nil
}()
if err != nil {
sendErr(p, fmt.Sprintf("Could not write crash file: %s", err.Error()))
} else {
sendStats(p, fmt.Sprintf("Written crash to: %s", out))
}
s := Session{
App: app,
Base: base,
Delegate: delegate,
Function: fn,
Method: method,
Scene: scene,
UIApp: uiapp,
}
if err := s.WriteToFile(); err != nil {
sendErr(p, fmt.Sprintf("Could not write session file: %s", err.Error()))
} else {
sendStats(p, "Written session file")
}
}
})

script, err = sess.CreateScript(scriptContent)
if err != nil {
sendErr(p, fmt.Sprintf("Could not create script: %s", err.Error()))
return
}

script.On("message", func(message string) {
l.Infof("script output: %s", message)
})
script.On("message", func(message string) {
sendStats(p, fmt.Sprintf("Script output: %s", message))
})

if err := script.Load(); err != nil {
return err
}
if err := script.Load(); err != nil {
sendErr(p, fmt.Sprintf("Could not load script: %s", err.Error()))
return
}

l.Infof("Loaded script")
sendStats(p, "Script loaded")

_ = script.ExportsCall("setup", method, uiapp, delegate, scene)
l.Infof("Finished setup")
_ = script.ExportsCall("setup_fuzz", method, uiapp, delegate, scene)
sendStats(p, "Finished fuzz setup")

m := mutator.NewMutator(base, app, runs, fn, crash, validInputs...)
ch := m.Mutate()
mut := mutator.NewMutator(base, app, runs, fn, crash, validInputs...)
ch := mut.Mutate()

for mutated := range ch {
lastInput = mutated.Input
l.Infof("[%s] %s\n", color.New(color.FgCyan).Sprintf("%s", mutated.Mutation), mutated.Input)
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
if err := script.ExportsCallWithContext(ctx, "fuzz", method, mutated.Input); err == frida.ErrContextCancelled {
sess.Detach()
break
}
if timeout > 0 {
time.Sleep(time.Duration(timeout) * time.Second)
for mutated := range ch {
lastInput = mutated.Input
p.Send(tui.MutatedMsg(mutated))
ctx, _ := context.WithTimeout(context.Background(), 1*time.Second)
if err := script.ExportsCallWithContext(ctx, "fuzz", method, mutated.Input); err == frida.ErrContextCancelled {
sess.Detach()
break
}
if timeout > 0 {
time.Sleep(time.Duration(timeout) * time.Second)
}
}
}
}()

p.Run()

return nil
},
}

func sendStats(p *tea.Program, msg string) {
p.Send(tui.StatsMsg(msg))
}

func sendErr(p *tea.Program, msg string) {
p.Send(tui.ErrMsg(msg))
}

func init() {
fuzzCmd.Flags().StringP("app", "a", "Gadget", "Application name to attach to")
fuzzCmd.Flags().StringP("base", "b", "", "base URL to fuzz")
Expand Down
34 changes: 32 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,41 @@ require (
)

require (
github.com/alecthomas/chroma v0.10.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.17.1 // indirect
github.com/charmbracelet/bubbletea v0.25.0 // indirect
github.com/charmbracelet/glamour v0.6.0 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/microcosm-cc/bluemonday v1.0.21 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sahilm/fuzzy v0.1.1-0.20230530133925-c48e322e2a8f // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.6.0 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
github.com/yuin/goldmark-emoji v1.0.1 // indirect
golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

0 comments on commit 8edba8b

Please sign in to comment.