diff --git a/Dockerfile b/Dockerfile index 5ffdaac7..a1696565 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ RUN git clone https://github.com/bettercap/caplets /usr/local/share/bettercap/ca # final stage FROM alpine +RUN apk add --update ca-certificates RUN apk add --no-cache --update bash iproute2 libpcap libusb-dev libnetfilter_queue wireless-tools COPY --from=build-env /go/src/github.com/bettercap/bettercap/bettercap /app/ COPY --from=build-env /go/src/github.com/bettercap/bettercap/caplets /app/ diff --git a/Gopkg.lock b/Gopkg.lock index bd28eebe..3ec72fc5 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -27,7 +27,7 @@ [[projects]] branch = "master" - digest = "1:a2c142e6c2aa1c71796c748bbe42d224e23d6638fd5b3ae153e70a4b08a8da4e" + digest = "1:881bb9d751b9408f038b83e9331ce3c57603710f3546f16e7d43b5c24e974f6d" name = "github.com/bettercap/gatt" packages = [ ".", @@ -40,7 +40,7 @@ "xpc", ] pruneopts = "UT" - revision = "277ee0d0ef94d26e3190252c59fa34dde0df4f26" + revision = "d1a17475747afe7c0d78813596d4e95801a5d592" [[projects]] branch = "master" @@ -83,7 +83,7 @@ revision = "2ce16c963a8ac5bd6af851d4877e38701346983f" [[projects]] - digest = "1:da1be9af4c3f262bd385cc722b08d98d4a47ddea57731e98b85c7ba21b35bc31" + digest = "1:5247f5757ba31623c464db149dc272a37604516d8fbae1561b36e0d7cee070a5" name = "github.com/evilsocket/islazy" packages = [ "data", @@ -96,8 +96,8 @@ "zip", ] pruneopts = "UT" - revision = "6ef79e84ded205e48f296d21e3bc65d1cf4f5c78" - version = "v1.10.3" + revision = "c5c7a41bb1c20e6df409825ed24af8de5fb7fb70" + version = "v1.10.4" [[projects]] branch = "master" @@ -184,6 +184,14 @@ pruneopts = "UT" revision = "f16ca3b7b383d3f0373109cac19147de3e8ae2d1" +[[projects]] + digest = "1:7ad278b575635babef38e4ad4219500c299a58ea14b30eb21383d0efca00b369" + name = "github.com/kr/binarydist" + packages = ["."] + pruneopts = "UT" + revision = "88f551ae580780cc79d12ab4c218ba1ca346b83a" + version = "v0.1.0" + [[projects]] digest = "1:4701b2acabe16722ecb1e387d39741a29269386bfc4ba6283ecda362d289eff1" name = "github.com/malfunkt/iprange" @@ -333,6 +341,7 @@ "github.com/gorilla/websocket", "github.com/inconshreveable/go-vhost", "github.com/jpillora/go-tld", + "github.com/kr/binarydist", "github.com/malfunkt/iprange", "github.com/mdlayher/dhcp6", "github.com/mdlayher/dhcp6/dhcp6opts", diff --git a/Gopkg.toml b/Gopkg.toml index b2f07827..55c4e5b9 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -73,3 +73,7 @@ [prune] go-tests = true unused-packages = true + +[[constraint]] + name = "github.com/kr/binarydist" + version = "0.1.0" diff --git a/caplets/caplet.go b/caplets/caplet.go index b36c1a8a..f4a6613a 100644 --- a/caplets/caplet.go +++ b/caplets/caplet.go @@ -8,11 +8,32 @@ import ( "github.com/evilsocket/islazy/fs" ) +type Script struct { + Path string `json:"path"` + Size int64 `json:"size"` + Code []string `json:"code"` +} + +func newScript(path string, size int64) Script { + return Script{ + Path: path, + Size: size, + Code: make([]string, 0), + } +} + type Caplet struct { - Name string - Path string - Size int64 - Code []string + Script + Name string `json:"name"` + Scripts []Script `json:"scripts"` +} + +func NewCaplet(name string, path string, size int64) Caplet { + return Caplet{ + Script: newScript(path, size), + Name: name, + Scripts: make([]Script, 0), + } } func (cap *Caplet) Eval(argv []string, lineCb func(line string) error) error { @@ -23,6 +44,10 @@ func (cap *Caplet) Eval(argv []string, lineCb func(line string) error) error { // temporarily change the working directory return fs.Chdir(filepath.Dir(cap.Path), func() error { for _, line := range cap.Code { + // skip empty lines and comments + if line == "" || line[0] == '#' { + continue + } // replace $0 with argv[0], $1 with argv[1] and so on for i, arg := range argv { what := fmt.Sprintf("$%d", i) diff --git a/caplets/manager.go b/caplets/manager.go index 88620c61..cb004d0c 100644 --- a/caplets/manager.go +++ b/caplets/manager.go @@ -2,6 +2,7 @@ package caplets import ( "fmt" + "io/ioutil" "os" "path/filepath" "sort" @@ -16,22 +17,22 @@ var ( cacheLock = sync.Mutex{} ) -func List() []Caplet { - caplets := make([]Caplet, 0) +func List() []*Caplet { + caplets := make([]*Caplet, 0) for _, searchPath := range LoadPaths { files, _ := filepath.Glob(searchPath + "/*" + Suffix) files2, _ := filepath.Glob(searchPath + "/*/*" + Suffix) for _, fileName := range append(files, files2...) { - if stats, err := os.Stat(fileName); err == nil { + if _, err := os.Stat(fileName); err == nil { base := strings.Replace(fileName, searchPath+"/", "", -1) base = strings.Replace(base, Suffix, "", -1) - caplets = append(caplets, Caplet{ - Name: base, - Path: fileName, - Size: stats.Size(), - }) + if err, caplet := Load(base); err != nil { + fmt.Fprintf(os.Stderr, "wtf: %v\n", err) + } else { + caplets = append(caplets, caplet) + } } } } @@ -51,6 +52,7 @@ func Load(name string) (error, *Caplet) { return nil, caplet } + baseName := name names := []string{} if !strings.HasSuffix(name, Suffix) { name += Suffix @@ -64,23 +66,41 @@ func Load(name string) (error, *Caplet) { names = append(names, name) } - for _, filename := range names { - if fs.Exists(filename) { + for _, fileName := range names { + if stats, err := os.Stat(fileName); err == nil { cap := &Caplet{ - Path: filename, - Code: make([]string, 0), + Script: newScript(fileName, stats.Size()), + Name: baseName, + Scripts: make([]Script, 0), } cache[name] = cap - if reader, err := fs.LineReader(filename); err != nil { - return fmt.Errorf("error reading caplet %s: %v", filename, err), nil + if reader, err := fs.LineReader(fileName); err != nil { + return fmt.Errorf("error reading caplet %s: %v", fileName, err), nil } else { for line := range reader { - if line == "" || line[0] == '#' { - continue - } cap.Code = append(cap.Code, line) } + + // the caplet has a dedicated folder + if strings.Contains(baseName, "/") || strings.Contains(baseName, "\\") { + dir := filepath.Dir(fileName) + // get all secondary .cap and .js files + if files, err := ioutil.ReadDir(dir); err == nil && len(files) > 0 { + for _, f := range files { + subFileName := filepath.Join(dir, f.Name()) + if subFileName != fileName && (strings.HasSuffix(subFileName, ".cap") || strings.HasSuffix(subFileName, ".js")) { + if reader, err := fs.LineReader(subFileName); err == nil { + script := newScript(subFileName, f.Size()) + for line := range reader { + script.Code = append(script.Code, line) + } + cap.Scripts = append(cap.Scripts, script) + } + } + } + } + } } return nil, cap diff --git a/core/banner.go b/core/banner.go index 825af8e2..67ec0563 100644 --- a/core/banner.go +++ b/core/banner.go @@ -2,7 +2,7 @@ package core const ( Name = "bettercap" - Version = "2.19" + Version = "2.22" Author = "Simone 'evilsocket' Margaritelli" Website = "https://bettercap.org/" ) diff --git a/core/core.go b/core/core.go index f79e6ffc..3761a6c7 100644 --- a/core/core.go +++ b/core/core.go @@ -1,7 +1,6 @@ package core import ( - "fmt" "os/exec" "sort" @@ -34,7 +33,7 @@ func HasBinary(executable string) bool { return true } -func ExecSilent(executable string, args []string) (string, error) { +func Exec(executable string, args []string) (string, error) { path, err := exec.LookPath(executable) if err != nil { return "", err @@ -47,11 +46,3 @@ func ExecSilent(executable string, args []string) (string, error) { return str.Trim(string(raw)), nil } } - -func Exec(executable string, args []string) (string, error) { - out, err := ExecSilent(executable, args) - if err != nil { - fmt.Printf("ERROR for '%s %s': %s\n", executable, args, err) - } - return out, err -} diff --git a/core/options.go b/core/options.go index e1a478e2..62d4e33d 100644 --- a/core/options.go +++ b/core/options.go @@ -22,7 +22,7 @@ func ParseOptions() (Options, error) { o := Options{ InterfaceName: flag.String("iface", "", "Network interface to bind to, if empty the default interface will be auto selected."), Gateway: flag.String("gateway-override", "", "Use the provided IP address instead of the default gateway. If not specified or invalid, the default gateway will be used."), - AutoStart: flag.String("autostart", "events.stream, net.recon", "Comma separated list of modules to auto start."), + AutoStart: flag.String("autostart", "events.stream", "Comma separated list of modules to auto start."), Caplet: flag.String("caplet", "", "Read commands from this file and execute them in the interactive session."), Debug: flag.Bool("debug", false, "Print debug messages."), PrintVersion: flag.Bool("version", false, "Print the version and exit."), diff --git a/firewall/firewall_darwin.go b/firewall/firewall_darwin.go index ac91b370..8a83255e 100644 --- a/firewall/firewall_darwin.go +++ b/firewall/firewall_darwin.go @@ -41,7 +41,7 @@ func Make(iface *network.Endpoint) FirewallManager { } func (f PfFirewall) sysCtlRead(param string) (string, error) { - if out, err := core.ExecSilent("sysctl", []string{param}); err != nil { + if out, err := core.Exec("sysctl", []string{param}); err != nil { return "", err } else if m := sysCtlParser.FindStringSubmatch(out); len(m) == 3 && m[1] == param { return m[2], nil @@ -52,7 +52,7 @@ func (f PfFirewall) sysCtlRead(param string) (string, error) { func (f PfFirewall) sysCtlWrite(param string, value string) (string, error) { args := []string{"-w", fmt.Sprintf("%s=%s", param, value)} - _, err := core.ExecSilent("sysctl", args) + _, err := core.Exec("sysctl", args) if err != nil { return "", err } @@ -115,9 +115,9 @@ func (f PfFirewall) generateRule(r *Redirection) string { func (f *PfFirewall) enable(enabled bool) { f.enabled = enabled if enabled { - core.ExecSilent("pfctl", []string{"-e"}) + core.Exec("pfctl", []string{"-e"}) } else { - core.ExecSilent("pfctl", []string{"-d"}) + core.Exec("pfctl", []string{"-d"}) } } @@ -139,7 +139,7 @@ func (f PfFirewall) EnableRedirection(r *Redirection, enabled bool) error { f.enable(true) // load the rule - if _, err := core.ExecSilent("pfctl", []string{"-f", f.filename}); err != nil { + if _, err := core.Exec("pfctl", []string{"-f", f.filename}); err != nil { return err } } else { diff --git a/modules/any_proxy/any_proxy.go b/modules/any_proxy/any_proxy.go index 427c0499..a8977d27 100644 --- a/modules/any_proxy/any_proxy.go +++ b/modules/any_proxy/any_proxy.go @@ -80,7 +80,7 @@ func (mod *AnyProxy) Configure() error { var dstAddress string if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, iface = mod.StringParam("any.proxy.iface"); err != nil { return err } else if err, protocol = mod.StringParam("any.proxy.protocol"); err != nil { diff --git a/modules/api_rest/api_rest.go b/modules/api_rest/api_rest.go index 11da44aa..c3b6f082 100644 --- a/modules/api_rest/api_rest.go +++ b/modules/api_rest/api_rest.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "sync" "time" "github.com/bettercap/bettercap/session" @@ -26,6 +27,17 @@ type RestAPI struct { useWebsocket bool upgrader websocket.Upgrader quit chan bool + + recClock int + recording bool + recTime int + loading bool + replaying bool + recordFileName string + recordWait *sync.WaitGroup + record *Record + recStarted time.Time + recStopped time.Time } func NewRestAPI(s *session.Session) *RestAPI { @@ -39,10 +51,30 @@ func NewRestAPI(s *session.Session) *RestAPI { ReadBufferSize: 1024, WriteBufferSize: 1024, }, + recClock: 1, + recording: false, + recTime: 0, + loading: false, + replaying: false, + recordFileName: "", + recordWait: &sync.WaitGroup{}, + record: nil, } + mod.State.Store("recording", &mod.recording) + mod.State.Store("rec_clock", &mod.recClock) + mod.State.Store("replaying", &mod.replaying) + mod.State.Store("loading", &mod.loading) + mod.State.Store("load_progress", 0) + mod.State.Store("rec_time", &mod.recTime) + mod.State.Store("rec_filename", &mod.recordFileName) + mod.State.Store("rec_frames", 0) + mod.State.Store("rec_cur_frame", 0) + mod.State.Store("rec_started", &mod.recStarted) + mod.State.Store("rec_stopped", &mod.recStopped) + mod.AddParam(session.NewStringParameter("api.rest.address", - session.ParamIfaceAddress, + "127.0.0.1", session.IPv4Validator, "Address to bind the API REST server to.")) @@ -93,6 +125,34 @@ func NewRestAPI(s *session.Session) *RestAPI { return mod.Stop() })) + mod.AddParam(session.NewIntParameter("api.rest.record.clock", + "1", + "Number of seconds to wait while recording with api.rest.record between one sample and the next one.")) + + mod.AddHandler(session.NewModuleHandler("api.rest.record off", "", + "Stop recording the session.", + func(args []string) error { + return mod.stopRecording() + })) + + mod.AddHandler(session.NewModuleHandler("api.rest.record FILENAME", `api\.rest\.record (.+)`, + "Start polling the rest API periodically recording each sample in a compressed file that can be later replayed.", + func(args []string) error { + return mod.startRecording(args[0]) + })) + + mod.AddHandler(session.NewModuleHandler("api.rest.replay off", "", + "Stop replaying the recorded session.", + func(args []string) error { + return mod.stopReplay() + })) + + mod.AddHandler(session.NewModuleHandler("api.rest.replay FILENAME", `api\.rest\.replay (.+)`, + "Start the rest API module in replay mode using FILENAME as the recorded session file, will revert to normal mode once the replay is over.", + func(args []string) error { + return mod.startReplay(args[0]) + })) + return mod } @@ -126,7 +186,7 @@ func (mod *RestAPI) Configure() error { var port int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, ip = mod.StringParam("api.rest.address"); err != nil { return err } else if err, port = mod.IntParam("api.rest.port"); err != nil { @@ -174,7 +234,10 @@ func (mod *RestAPI) Configure() error { router.Methods("OPTIONS").HandlerFunc(mod.corsRoute) + router.HandleFunc("/api/file", mod.fileRoute) + router.HandleFunc("/api/events", mod.eventsRoute) + router.HandleFunc("/api/session", mod.sessionRoute) router.HandleFunc("/api/session/ble", mod.sessionRoute) router.HandleFunc("/api/session/ble/{mac}", mod.sessionRoute) @@ -202,7 +265,9 @@ func (mod *RestAPI) Configure() error { } func (mod *RestAPI) Start() error { - if err := mod.Configure(); err != nil { + if mod.replaying { + return fmt.Errorf("the api is currently in replay mode, run api.rest.replay off before starting it") + } else if err := mod.Configure(); err != nil { return err } @@ -226,6 +291,12 @@ func (mod *RestAPI) Start() error { } func (mod *RestAPI) Stop() error { + if mod.recording { + mod.stopRecording() + } else if mod.replaying { + mod.stopReplay() + } + return mod.SetRunning(false, func() { go func() { mod.quit <- true diff --git a/modules/api_rest/api_rest_controller.go b/modules/api_rest/api_rest_controller.go index e9765f19..da9b4d63 100644 --- a/modules/api_rest/api_rest_controller.go +++ b/modules/api_rest/api_rest_controller.go @@ -3,7 +3,11 @@ package api_rest import ( "crypto/subtle" "encoding/json" + "fmt" + "io" + "io/ioutil" "net/http" + "os" "strconv" "strings" @@ -22,7 +26,7 @@ type APIResponse struct { } func (mod *RestAPI) setAuthFailed(w http.ResponseWriter, r *http.Request) { - mod.Warning("Unauthorized authentication attempt from %s", r.RemoteAddr) + mod.Warning("Unauthorized authentication attempt from %s to %s", r.RemoteAddr, r.URL.String()) w.Header().Set("WWW-Authenticate", `Basic realm="auth"`) w.WriteHeader(401) @@ -32,7 +36,7 @@ func (mod *RestAPI) setAuthFailed(w http.ResponseWriter, r *http.Request) { func (mod *RestAPI) toJSON(w http.ResponseWriter, o interface{}) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(o); err != nil { - mod.Error("error while encoding object to JSON: %v", err) + mod.Warning("error while encoding object to JSON: %v", err) } } @@ -60,8 +64,68 @@ func (mod *RestAPI) checkAuth(r *http.Request) bool { return true } +func (mod *RestAPI) patchFrame(buf []byte) (frame map[string]interface{}, err error) { + // this is ugly but necessary: since we're replaying, the + // api.rest state object is filled with *old* values (the + // recorded ones), but the UI needs updated values at least + // of that in order to understand that a replay is going on + // and where we are at it. So we need to parse the record + // back into a session object and update only the api.rest.state + frame = make(map[string]interface{}) + + if err = json.Unmarshal(buf, &frame); err != nil { + return + } + + for _, i := range frame["modules"].([]interface{}) { + m := i.(map[string]interface{}) + if m["name"] == "api.rest" { + state := m["state"].(map[string]interface{}) + mod.State.Range(func(key interface{}, value interface{}) bool { + state[key.(string)] = value + return true + }) + break + } + } + + return +} + func (mod *RestAPI) showSession(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I) + if mod.replaying { + if !mod.record.Session.Over() { + from := mod.record.Session.CurFrame() - 1 + q := r.URL.Query() + vals := q["from"] + if len(vals) > 0 { + if n, err := strconv.Atoi(vals[0]); err == nil { + from = n + } + } + mod.record.Session.SetFrom(from) + + mod.Debug("replaying session %d of %d from %s", + mod.record.Session.CurFrame(), + mod.record.Session.Frames(), + mod.recordFileName) + + mod.State.Store("rec_frames", mod.record.Session.Frames()) + mod.State.Store("rec_cur_frame", mod.record.Session.CurFrame()) + + buf := mod.record.Session.Next() + if frame, err := mod.patchFrame(buf); err != nil { + mod.Error("%v", err) + } else { + mod.toJSON(w, frame) + return + } + } else { + mod.stopReplay() + } + } + + mod.toJSON(w, mod.Session) } func (mod *RestAPI) showBLE(w http.ResponseWriter, r *http.Request) { @@ -69,8 +133,8 @@ func (mod *RestAPI) showBLE(w http.ResponseWriter, r *http.Request) { mac := strings.ToLower(params["mac"]) if mac == "" { - mod.toJSON(w, session.I.BLE) - } else if dev, found := session.I.BLE.Get(mac); found { + mod.toJSON(w, mod.Session.BLE) + } else if dev, found := mod.Session.BLE.Get(mac); found { mod.toJSON(w, dev) } else { http.Error(w, "Not Found", 404) @@ -82,8 +146,8 @@ func (mod *RestAPI) showHID(w http.ResponseWriter, r *http.Request) { mac := strings.ToLower(params["mac"]) if mac == "" { - mod.toJSON(w, session.I.HID) - } else if dev, found := session.I.HID.Get(mac); found { + mod.toJSON(w, mod.Session.HID) + } else if dev, found := mod.Session.HID.Get(mac); found { mod.toJSON(w, dev) } else { http.Error(w, "Not Found", 404) @@ -91,19 +155,19 @@ func (mod *RestAPI) showHID(w http.ResponseWriter, r *http.Request) { } func (mod *RestAPI) showEnv(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Env) + mod.toJSON(w, mod.Session.Env) } func (mod *RestAPI) showGateway(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Gateway) + mod.toJSON(w, mod.Session.Gateway) } func (mod *RestAPI) showInterface(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Interface) + mod.toJSON(w, mod.Session.Interface) } func (mod *RestAPI) showModules(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Modules) + mod.toJSON(w, mod.Session.Modules) } func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) { @@ -111,8 +175,8 @@ func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) { mac := strings.ToLower(params["mac"]) if mac == "" { - mod.toJSON(w, session.I.Lan) - } else if host, found := session.I.Lan.Get(mac); found { + mod.toJSON(w, mod.Session.Lan) + } else if host, found := mod.Session.Lan.Get(mac); found { mod.toJSON(w, host) } else { http.Error(w, "Not Found", 404) @@ -120,15 +184,15 @@ func (mod *RestAPI) showLAN(w http.ResponseWriter, r *http.Request) { } func (mod *RestAPI) showOptions(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Options) + mod.toJSON(w, mod.Session.Options) } func (mod *RestAPI) showPackets(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.Queue) + mod.toJSON(w, mod.Session.Queue) } func (mod *RestAPI) showStartedAt(w http.ResponseWriter, r *http.Request) { - mod.toJSON(w, session.I.StartedAt) + mod.toJSON(w, mod.Session.StartedAt) } func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) { @@ -136,10 +200,10 @@ func (mod *RestAPI) showWiFi(w http.ResponseWriter, r *http.Request) { mac := strings.ToLower(params["mac"]) if mac == "" { - mod.toJSON(w, session.I.WiFi) - } else if station, found := session.I.WiFi.Get(mac); found { + mod.toJSON(w, mod.Session.WiFi) + } else if station, found := mod.Session.WiFi.Get(mac); found { mod.toJSON(w, station) - } else if client, found := session.I.WiFi.GetClient(mac); found { + } else if client, found := mod.Session.WiFi.GetClient(mac); found { mod.toJSON(w, client) } else { http.Error(w, "Not Found", 404) @@ -154,43 +218,84 @@ func (mod *RestAPI) runSessionCommand(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad Request", 400) } else if err = json.NewDecoder(r.Body).Decode(&cmd); err != nil { http.Error(w, "Bad Request", 400) - } else if err = session.I.Run(cmd.Command); err != nil { - http.Error(w, err.Error(), 400) - } else { - mod.toJSON(w, APIResponse{Success: true}) } + + for _, aCommand := range session.ParseCommands(cmd.Command) { + if err = mod.Session.Run(aCommand); err != nil { + http.Error(w, err.Error(), 400) + return + } + } + + mod.toJSON(w, APIResponse{Success: true}) +} + +func (mod *RestAPI) getEvents(limit int) []session.Event { + events := make([]session.Event, 0) + for _, e := range mod.Session.Events.Sorted() { + if mod.Session.EventsIgnoreList.Ignored(e) == false { + events = append(events, e) + } + } + + nevents := len(events) + nmax := nevents + n := nmax + + if limit > 0 && limit < nmax { + n = limit + } + + return events[nevents-n:] } func (mod *RestAPI) showEvents(w http.ResponseWriter, r *http.Request) { - var err error + q := r.URL.Query() + + if mod.replaying { + if !mod.record.Events.Over() { + from := mod.record.Events.CurFrame() - 1 + vals := q["from"] + if len(vals) > 0 { + if n, err := strconv.Atoi(vals[0]); err == nil { + from = n + } + } + mod.record.Events.SetFrom(from) + + mod.Debug("replaying events %d of %d from %s", + mod.record.Events.CurFrame(), + mod.record.Events.Frames(), + mod.recordFileName) + + buf := mod.record.Events.Next() + if _, err := w.Write(buf); err != nil { + mod.Error("%v", err) + } else { + return + } + } else { + mod.stopReplay() + } + } if mod.useWebsocket { mod.startStreamingEvents(w, r) } else { - events := session.I.Events.Sorted() - nevents := len(events) - nmax := nevents - n := nmax - - q := r.URL.Query() vals := q["n"] + limit := 0 if len(vals) > 0 { - n, err = strconv.Atoi(q["n"][0]) - if err == nil { - if n > nmax { - n = nmax - } - } else { - n = nmax + if n, err := strconv.Atoi(q["n"][0]); err == nil { + limit = n } } - mod.toJSON(w, events[nevents-n:]) + mod.toJSON(w, mod.getEvents(limit)) } } func (mod *RestAPI) clearEvents(w http.ResponseWriter, r *http.Request) { - session.I.Events.Clear() + mod.Session.Events.Clear() } func (mod *RestAPI) corsRoute(w http.ResponseWriter, r *http.Request) { @@ -212,10 +317,10 @@ func (mod *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) { return } - session.I.Lock() - defer session.I.Unlock() + mod.Session.Lock() + defer mod.Session.Unlock() - path := r.URL.String() + path := r.URL.Path switch { case path == "/api/session": mod.showSession(w, r) @@ -258,6 +363,44 @@ func (mod *RestAPI) sessionRoute(w http.ResponseWriter, r *http.Request) { } } +func (mod *RestAPI) readFile(fileName string, w http.ResponseWriter, r *http.Request) { + fp, err := os.Open(fileName) + if err != nil { + msg := fmt.Sprintf("could not open %s for reading: %s", fileName, err) + mod.Debug(msg) + http.Error(w, msg, 404) + return + } + defer fp.Close() + + w.Header().Set("Content-type", "application/octet-stream") + + io.Copy(w, fp) +} + +func (mod *RestAPI) writeFile(fileName string, w http.ResponseWriter, r *http.Request) { + data, err := ioutil.ReadAll(r.Body) + if err != nil { + msg := fmt.Sprintf("invalid file upload: %s", err) + mod.Warning(msg) + http.Error(w, msg, 404) + return + } + + err = ioutil.WriteFile(fileName, data, 0666) + if err != nil { + msg := fmt.Sprintf("can't write to %s: %s", fileName, err) + mod.Warning(msg) + http.Error(w, msg, 404) + return + } + + mod.toJSON(w, APIResponse{ + Success: true, + Message: fmt.Sprintf("%s created", fileName), + }) +} + func (mod *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) { mod.setSecurityHeaders(w) @@ -274,3 +417,22 @@ func (mod *RestAPI) eventsRoute(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad Request", 400) } } + +func (mod *RestAPI) fileRoute(w http.ResponseWriter, r *http.Request) { + mod.setSecurityHeaders(w) + + if !mod.checkAuth(r) { + mod.setAuthFailed(w, r) + return + } + + fileName := r.URL.Query().Get("name") + + if fileName != "" && r.Method == "GET" { + mod.readFile(fileName, w, r) + } else if fileName != "" && r.Method == "POST" { + mod.writeFile(fileName, w, r) + } else { + http.Error(w, "Bad Request", 400) + } +} diff --git a/modules/api_rest/api_rest_record.go b/modules/api_rest/api_rest_record.go new file mode 100644 index 00000000..fd5d7c09 --- /dev/null +++ b/modules/api_rest/api_rest_record.go @@ -0,0 +1,116 @@ +package api_rest + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/evilsocket/islazy/fs" +) + +var ( + errNotRecording = errors.New("not recording") +) + +func (mod *RestAPI) errAlreadyRecording() error { + return fmt.Errorf("the module is already recording to %s", mod.recordFileName) +} + +func (mod *RestAPI) recordState() error { + mod.Session.Lock() + defer mod.Session.Unlock() + + session := new(bytes.Buffer) + encoder := json.NewEncoder(session) + + if err := encoder.Encode(mod.Session); err != nil { + return err + } + + events := new(bytes.Buffer) + encoder = json.NewEncoder(events) + + if err := encoder.Encode(mod.getEvents(0)); err != nil { + return err + } + + return mod.record.NewState(session.Bytes(), events.Bytes()) +} + +func (mod *RestAPI) recorder() { + clock := time.Duration(mod.recClock) * time.Second + + mod.recTime = 0 + mod.recording = true + mod.replaying = false + mod.record = NewRecord(mod.recordFileName, &mod.SessionModule) + + mod.Info("started recording to %s (clock %s) ...", mod.recordFileName, clock) + + mod.recordWait.Add(1) + defer mod.recordWait.Done() + + tick := time.NewTicker(1 * time.Second) + lastSampled := time.Time{} + + for range tick.C { + if !mod.recording { + break + } + + mod.recTime++ + + if time.Since(lastSampled) >= clock { + lastSampled = time.Now() + if err := mod.recordState(); err != nil { + mod.Error("error while recording: %s", err) + mod.recording = false + break + } + } + } + + mod.Info("stopped recording to %s ...", mod.recordFileName) +} + +func (mod *RestAPI) startRecording(filename string) (err error) { + if mod.recording { + return mod.errAlreadyRecording() + } else if mod.replaying { + return mod.errAlreadyReplaying() + } else if err, mod.recClock = mod.IntParam("api.rest.record.clock"); err != nil { + return err + } else if mod.recordFileName, err = fs.Expand(filename); err != nil { + return err + } + + // we need the api itself up and running + if !mod.Running() { + if err = mod.Start(); err != nil { + return err + } + } + + go mod.recorder() + + return nil +} + +func (mod *RestAPI) stopRecording() error { + if !mod.recording { + return errNotRecording + } + + mod.recording = false + + mod.recordWait.Wait() + + err := mod.record.Flush() + + mod.recordFileName = "" + mod.record = nil + + return err +} diff --git a/modules/api_rest/api_rest_replay.go b/modules/api_rest/api_rest_replay.go new file mode 100644 index 00000000..b04c9ef4 --- /dev/null +++ b/modules/api_rest/api_rest_replay.go @@ -0,0 +1,81 @@ +package api_rest + +import ( + "errors" + "fmt" + "time" + + "github.com/evilsocket/islazy/fs" +) + +var ( + errNotReplaying = errors.New("not replaying") +) + +func (mod *RestAPI) errAlreadyReplaying() error { + return fmt.Errorf("the module is already replaying a session from %s", mod.recordFileName) +} + +func (mod *RestAPI) startReplay(filename string) (err error) { + if mod.replaying { + return mod.errAlreadyReplaying() + } else if mod.recording { + return mod.errAlreadyRecording() + } else if mod.recordFileName, err = fs.Expand(filename); err != nil { + return err + } + + mod.State.Store("load_progress", 0) + defer func() { + mod.State.Store("load_progress", 100.0) + }() + + mod.loading = true + defer func() { + mod.loading = false + }() + + mod.Info("loading %s ...", mod.recordFileName) + + start := time.Now() + if mod.record, err = LoadRecord(mod.recordFileName, &mod.SessionModule); err != nil { + return err + } + loadedIn := time.Since(start) + + // we need the api itself up and running + if !mod.Running() { + if err := mod.Start(); err != nil { + return err + } + } + + mod.recStarted = mod.record.Session.StartedAt() + mod.recStopped = mod.record.Session.StoppedAt() + duration := mod.recStopped.Sub(mod.recStarted) + mod.recTime = int(duration.Seconds()) + mod.replaying = true + mod.recording = false + + mod.Info("loaded %s of recording (%d frames) started at %s in %s, started replaying ...", + duration, + mod.record.Session.Frames(), + mod.recStarted, + loadedIn) + + return nil +} + +func (mod *RestAPI) stopReplay() error { + if !mod.replaying { + return errNotReplaying + } + + mod.replaying = false + + mod.Info("stopped replaying from %s ...", mod.recordFileName) + + mod.recordFileName = "" + + return nil +} diff --git a/modules/api_rest/record.go b/modules/api_rest/record.go new file mode 100644 index 00000000..cd52765f --- /dev/null +++ b/modules/api_rest/record.go @@ -0,0 +1,298 @@ +package api_rest + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "sync" + "time" + + "github.com/bettercap/bettercap/session" + + "github.com/evilsocket/islazy/fs" + "github.com/kr/binarydist" +) + +type patch []byte +type frame []byte + +type progressCallback func(done int) + +type RecordEntry struct { + sync.Mutex + + Data []byte `json:"data"` + Cur []byte `json:"-"` + States []patch `json:"states"` + NumStates int `json:"-"` + CurState int `json:"-"` + + frames []frame + progress progressCallback +} + +func NewRecordEntry(progress progressCallback) *RecordEntry { + return &RecordEntry{ + Data: nil, + Cur: nil, + States: make([]patch, 0), + NumStates: 0, + CurState: 0, + frames: nil, + progress: progress, + } +} + +func (e *RecordEntry) AddState(state []byte) error { + e.Lock() + defer e.Unlock() + + // set reference state + if e.Data == nil { + e.Data = state + } else { + // create a patch + oldReader := bytes.NewReader(e.Cur) + newReader := bytes.NewReader(state) + writer := new(bytes.Buffer) + + if err := binarydist.Diff(oldReader, newReader, writer); err != nil { + return err + } + + e.States = append(e.States, patch(writer.Bytes())) + e.NumStates++ + e.CurState = 0 + } + e.Cur = state + + return nil +} + +func (e *RecordEntry) Reset() { + e.Lock() + defer e.Unlock() + e.Cur = e.Data + e.NumStates = len(e.States) + e.CurState = 0 +} + +func (e *RecordEntry) Compile() error { + e.Lock() + defer e.Unlock() + + // reset the state + e.Cur = e.Data + e.NumStates = len(e.States) + e.CurState = 0 + e.frames = make([]frame, e.NumStates+1) + + // first is the master frame + e.frames[0] = frame(e.Data) + // precompute frames so they can be accessed by index + for i := 0; i < e.NumStates; i++ { + patch := e.States[i] + oldReader := bytes.NewReader(e.Cur) + patchReader := bytes.NewReader(patch) + newWriter := new(bytes.Buffer) + + if err := binarydist.Patch(oldReader, newWriter, patchReader); err != nil { + return err + } + + e.Cur = newWriter.Bytes() + e.frames[i+1] = e.Cur + + e.progress(1) + } + + e.progress(1) + + return nil +} + +func (e *RecordEntry) Frames() int { + e.Lock() + defer e.Unlock() + // master + sub states + return e.NumStates + 1 +} + +func (e *RecordEntry) CurFrame() int { + e.Lock() + defer e.Unlock() + return e.CurState + 1 +} + +func (e *RecordEntry) SetFrom(from int) { + e.Lock() + defer e.Unlock() + e.CurState = from +} + +func (e *RecordEntry) Over() bool { + e.Lock() + defer e.Unlock() + return e.CurState > e.NumStates +} + +func (e *RecordEntry) Next() []byte { + e.Lock() + defer e.Unlock() + cur := e.CurState + e.CurState++ + return e.frames[cur] +} + +func (e *RecordEntry) TimeOf(idx int) time.Time { + e.Lock() + defer e.Unlock() + + buf := e.frames[idx] + frame := make(map[string]interface{}) + + if err := json.Unmarshal(buf, &frame); err != nil { + fmt.Printf("%v\n", err) + return time.Time{} + } else if tm, err := time.Parse(time.RFC3339, frame["polled_at"].(string)); err != nil { + fmt.Printf("%v\n", err) + return time.Time{} + } else { + return tm + } +} + +func (e *RecordEntry) StartedAt() time.Time { + return e.TimeOf(0) +} + +func (e *RecordEntry) StoppedAt() time.Time { + return e.TimeOf(e.NumStates) +} + +func (e *RecordEntry) Duration() time.Duration { + return e.StoppedAt().Sub(e.StartedAt()) +} + +// the Record object represents a recorded session +type Record struct { + sync.Mutex + + mod *session.SessionModule `json:"-"` + fileName string `json:"-"` + done int `json:"-"` + total int `json:"-"` + progress float64 `json:"-"` + Session *RecordEntry `json:"session"` + Events *RecordEntry `json:"events"` +} + +func NewRecord(fileName string, mod *session.SessionModule) *Record { + r := &Record{ + fileName: fileName, + mod: mod, + } + + r.Session = NewRecordEntry(r.onProgress) + r.Events = NewRecordEntry(r.onProgress) + + return r +} + +func (r *Record) onProgress(done int) { + r.done += done + r.progress = float64(r.done) / float64(r.total) * 100.0 + r.mod.State.Store("load_progress", r.progress) +} + +func LoadRecord(fileName string, mod *session.SessionModule) (*Record, error) { + if !fs.Exists(fileName) { + return nil, fmt.Errorf("%s does not exist", fileName) + } + + compressed, err := ioutil.ReadFile(fileName) + if err != nil { + return nil, fmt.Errorf("error while reading %s: %s", fileName, err) + } + + decompress, err := gzip.NewReader(bytes.NewReader(compressed)) + if err != nil { + return nil, fmt.Errorf("error while reading gzip file %s: %s", fileName, err) + } + defer decompress.Close() + + raw, err := ioutil.ReadAll(decompress) + if err != nil { + return nil, fmt.Errorf("error while decompressing %s: %s", fileName, err) + } + + rec := &Record{} + + decoder := json.NewDecoder(bytes.NewReader(raw)) + if err = decoder.Decode(rec); err != nil { + return nil, fmt.Errorf("error while parsing %s: %s", fileName, err) + } + + rec.fileName = fileName + rec.mod = mod + + rec.Session.NumStates = len(rec.Session.States) + rec.Session.progress = rec.onProgress + rec.Events.NumStates = len(rec.Events.States) + rec.Events.progress = rec.onProgress + + rec.done = 0 + rec.total = rec.Session.NumStates + rec.Events.NumStates + 2 + rec.progress = 0.0 + + // reset state and precompute frames + if err = rec.Session.Compile(); err != nil { + return nil, err + } else if err = rec.Events.Compile(); err != nil { + return nil, err + } + + return rec, nil +} + +func (r *Record) NewState(session []byte, events []byte) error { + if err := r.Session.AddState(session); err != nil { + return err + } else if err := r.Events.AddState(events); err != nil { + return err + } + return r.Flush() +} + +func (r *Record) save() error { + buf := new(bytes.Buffer) + encoder := json.NewEncoder(buf) + + if err := encoder.Encode(r); err != nil { + return err + } + + data := buf.Bytes() + + compressed := new(bytes.Buffer) + compress := gzip.NewWriter(compressed) + + if _, err := compress.Write(data); err != nil { + return err + } else if err = compress.Flush(); err != nil { + return err + } else if err = compress.Close(); err != nil { + return err + } + + return ioutil.WriteFile(r.fileName, compressed.Bytes(), os.ModePerm) +} + +func (r *Record) Flush() error { + r.Lock() + defer r.Unlock() + return r.save() +} diff --git a/modules/arp_spoof/arp_spoof.go b/modules/arp_spoof/arp_spoof.go index 6dc5a573..507d38af 100644 --- a/modules/arp_spoof/arp_spoof.go +++ b/modules/arp_spoof/arp_spoof.go @@ -127,9 +127,14 @@ func (mod *ArpSpoofer) Start() error { return err } + nTargets := len(mod.addresses) + len(mod.macs) + if nTargets == 0 { + mod.Warning("list of targets is empty, module not starting.") + return nil + } + return mod.SetRunning(true, func() { neighbours := []net.IP{} - nTargets := len(mod.addresses) + len(mod.macs) if mod.internal { list, _ := iprange.ParseList(mod.Session.Interface.CIDR()) diff --git a/modules/ble/ble_recon.go b/modules/ble/ble_recon.go index 1bea0d99..d30bf03d 100644 --- a/modules/ble/ble_recon.go +++ b/modules/ble/ble_recon.go @@ -20,6 +20,7 @@ import ( type BLERecon struct { session.SessionModule + deviceId int gattDevice gatt.Device currDevice *network.BLEDevice writeUUID *gatt.UUID @@ -34,6 +35,7 @@ type BLERecon struct { func NewBLERecon(s *session.Session) *BLERecon { mod := &BLERecon{ SessionModule: session.NewSessionModule("ble.recon", s), + deviceId: -1, gattDevice: nil, quit: make(chan bool), done: make(chan bool), @@ -42,6 +44,8 @@ func NewBLERecon(s *session.Session) *BLERecon { connected: false, } + mod.InitState("scanning") + mod.selector = utils.ViewSelectorFor(&mod.SessionModule, "ble.show", []string{"rssi", "mac", "seen"}, "rssi asc") @@ -108,6 +112,10 @@ func NewBLERecon(s *session.Session) *BLERecon { mod.AddHandler(write) + mod.AddParam(session.NewIntParameter("ble.device", + fmt.Sprintf("%d", mod.deviceId), + "Index of the HCI device to use, -1 to autodetect.")) + return mod } @@ -138,13 +146,23 @@ func (w dummyWriter) Write(p []byte) (n int, err error) { func (mod *BLERecon) Configure() (err error) { if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if mod.gattDevice == nil { - mod.Debug("initializing device ...") + if err, mod.deviceId = mod.IntParam("ble.device"); err != nil { + return err + } + + mod.Debug("initializing device (id:%d) ...", mod.deviceId) golog.SetFlags(0) golog.SetOutput(dummyWriter{mod}) - if mod.gattDevice, err = gatt.NewDevice(defaultBLEClientOptions...); err != nil { + + options := []gatt.Option{ + gatt.LnxMaxConnections(255), + gatt.LnxDeviceID(mod.deviceId, true), + } + + if mod.gattDevice, err = gatt.NewDevice(options...); err != nil { mod.Debug("error while creating new gatt device: %v", err) return err } @@ -171,18 +189,20 @@ func (mod *BLERecon) Start() error { <-mod.quit - mod.Info("stopping scan ...") + if mod.gattDevice != nil { + mod.Info("stopping scan ...") - if mod.currDevice != nil && mod.currDevice.Device != nil && mod.gattDevice != nil { - mod.Debug("resetting connection with %v", mod.currDevice.Device) - mod.gattDevice.CancelConnection(mod.currDevice.Device) - } + if mod.currDevice != nil && mod.currDevice.Device != nil { + mod.Debug("resetting connection with %v", mod.currDevice.Device) + mod.gattDevice.CancelConnection(mod.currDevice.Device) + } - mod.Debug("stopping device") - if err := mod.gattDevice.Stop(); err != nil { - mod.Warning("error while stopping device: %v", err) - } else { - mod.Debug("gatt device closed") + mod.Debug("stopping device") + if err := mod.gattDevice.Stop(); err != nil { + mod.Warning("error while stopping device: %v", err) + } else { + mod.Debug("gatt device closed") + } } mod.done <- true @@ -196,6 +216,7 @@ func (mod *BLERecon) Stop() error { mod.Debug("module stopped, cleaning state") mod.gattDevice = nil mod.setCurrentDevice(nil) + mod.ResetState() }) } @@ -216,6 +237,7 @@ func (mod *BLERecon) pruner() { func (mod *BLERecon) setCurrentDevice(dev *network.BLEDevice) { mod.connected = false mod.currDevice = dev + mod.State.Store("scanning", dev) } func (mod *BLERecon) writeBuffer(mac string, uuid gatt.UUID, data []byte) error { @@ -233,7 +255,7 @@ func (mod *BLERecon) enumAllTheThings(mac string) error { } mod.setCurrentDevice(dev) - if err := mod.Configure(); err != nil && err != session.ErrAlreadyStarted { + if err := mod.Configure(); err != nil && err.Error() != session.ErrAlreadyStarted("ble.recon").Error() { return err } diff --git a/modules/ble/ble_recon_events.go b/modules/ble/ble_recon_events.go index a19dc310..fed2b0a8 100644 --- a/modules/ble/ble_recon_events.go +++ b/modules/ble/ble_recon_events.go @@ -67,7 +67,8 @@ func (mod *BLERecon) onPeriphConnected(p gatt.Peripheral, err error) { mod.Info("connected, enumerating all the things for %s!", p.ID()) services, err := p.DiscoverServices(nil) - if err != nil { + // https://github.com/bettercap/bettercap/issues/498 + if err != nil && err.Error() != "success" { mod.Error("error discovering services: %s", err) return } diff --git a/modules/dhcp6_spoof/dhcp6_spoof.go b/modules/dhcp6_spoof/dhcp6_spoof.go index f7a7a325..2b0f729b 100644 --- a/modules/dhcp6_spoof/dhcp6_spoof.go +++ b/modules/dhcp6_spoof/dhcp6_spoof.go @@ -78,7 +78,7 @@ func (mod *DHCP6Spoofer) Configure() error { var err error if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } if mod.Handle, err = pcap.OpenLive(mod.Session.Interface.Name(), 65536, true, pcap.BlockForever); err != nil { diff --git a/modules/dns_spoof/dns_spoof.go b/modules/dns_spoof/dns_spoof.go index f4d5c9ce..dcf65db1 100644 --- a/modules/dns_spoof/dns_spoof.go +++ b/modules/dns_spoof/dns_spoof.go @@ -133,7 +133,7 @@ func (mod *DNSSpoofer) Configure() error { var proxyip string if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if mod.Handle, err = pcap.OpenLive(mod.Session.Interface.Name(), 65536, true, pcap.BlockForever); err != nil { return err } else if err = mod.Handle.SetBPFFilter("udp and port 53"); err != nil { diff --git a/modules/dns_spoof/dns_spoof_hosts.go b/modules/dns_spoof/dns_spoof_hosts.go index adb6c3c3..efc0f12d 100644 --- a/modules/dns_spoof/dns_spoof_hosts.go +++ b/modules/dns_spoof/dns_spoof_hosts.go @@ -46,7 +46,7 @@ func NewHostEntry(host string, address net.IP) HostEntry { return entry } -func HostsFromFile(filename string,defaultAddress net.IP) (err error, entries []HostEntry) { +func HostsFromFile(filename string, defaultAddress net.IP) (err error, entries []HostEntry) { input, err := os.Open(filename) if err != nil { return diff --git a/modules/events_stream/events_stream.go b/modules/events_stream/events_stream.go index 517e4db1..58b0e00b 100644 --- a/modules/events_stream/events_stream.go +++ b/modules/events_stream/events_stream.go @@ -29,7 +29,6 @@ type EventsStream struct { outputName string output *os.File rotation rotation - ignoreList *IgnoreList triggerList *TriggerList waitFor string waitChan chan *session.Event @@ -47,10 +46,11 @@ func NewEventsStream(s *session.Session) *EventsStream { quit: make(chan bool), waitChan: make(chan *session.Event), waitFor: "", - ignoreList: NewIgnoreList(), triggerList: NewTriggerList(), } + mod.State.Store("ignoring", &mod.Session.EventsIgnoreList) + mod.AddHandler(session.NewModuleHandler("events.stream on", "", "Start events stream.", func(args []string) error { @@ -127,7 +127,7 @@ func NewEventsStream(s *session.Session) *EventsStream { ignore := session.NewModuleHandler("events.ignore FILTER", "events.ignore ([^\\s]+)", "Events with an identifier matching this filter will not be shown (use multiple times to add more filters).", func(args []string) error { - return mod.ignoreList.Add(args[0]) + return mod.Session.EventsIgnoreList.Add(args[0]) }) ignore.Complete("events.ignore", s.EventsCompleter) @@ -137,7 +137,7 @@ func NewEventsStream(s *session.Session) *EventsStream { include := session.NewModuleHandler("events.include FILTER", "events.include ([^\\s]+)", "Used to remove filters passed with the events.ignore command.", func(args []string) error { - return mod.ignoreList.Remove(args[0]) + return mod.Session.EventsIgnoreList.Remove(args[0]) }) include.Complete("events.include", s.EventsCompleter) @@ -147,13 +147,13 @@ func NewEventsStream(s *session.Session) *EventsStream { mod.AddHandler(session.NewModuleHandler("events.filters", "", "Print the list of filters used to ignore events.", func(args []string) error { - if mod.ignoreList.Empty() { + if mod.Session.EventsIgnoreList.Empty() { fmt.Printf("Ignore filters list is empty.\n") } else { - mod.ignoreList.RLock() - defer mod.ignoreList.RUnlock() + mod.Session.EventsIgnoreList.RLock() + defer mod.Session.EventsIgnoreList.RUnlock() - for _, filter := range mod.ignoreList.Filters() { + for _, filter := range mod.Session.EventsIgnoreList.Filters() { fmt.Printf(" '%s'\n", string(filter)) } } @@ -163,7 +163,7 @@ func NewEventsStream(s *session.Session) *EventsStream { mod.AddHandler(session.NewModuleHandler("events.filters.clear", "", "Clear the list of filters passed with the events.ignore command.", func(args []string) error { - mod.ignoreList = NewIgnoreList() + mod.Session.EventsIgnoreList.Clear() return nil })) @@ -281,7 +281,7 @@ func (mod *EventsStream) Start() error { mod.waitChan <- &e } - if !mod.ignoreList.Ignored(e) { + if !mod.Session.EventsIgnoreList.Ignored(e) { mod.View(e, true) } @@ -303,7 +303,7 @@ func (mod *EventsStream) Show(limit int) error { selected := []session.Event{} for i := range events { e := events[num-1-i] - if !mod.ignoreList.Ignored(e) { + if !mod.Session.EventsIgnoreList.Ignored(e) { selected = append(selected, e) if len(selected) == limit { break diff --git a/modules/gps/gps.go b/modules/gps/gps.go index 65c92596..b71b67fd 100644 --- a/modules/gps/gps.go +++ b/modules/gps/gps.go @@ -72,7 +72,7 @@ func (mod *GPS) Author() string { func (mod *GPS) Configure() (err error) { if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, mod.serialPort = mod.StringParam("gps.device"); err != nil { return err } else if err, mod.baudRate = mod.IntParam("gps.baudrate"); err != nil { @@ -126,11 +126,14 @@ func (mod *GPS) Start() error { return mod.SetRunning(true, func() { defer mod.serial.Close() + mod.Info("started on port %s ...", mod.serialPort) + for mod.Running() { if line, err := mod.readLine(); err == nil { if s, err := nmea.Parse(line); err == nil { // http://aprs.gids.nl/nmea/#gga if m, ok := s.(nmea.GNGGA); ok { + mod.Session.GPS.Updated = time.Now() mod.Session.GPS.Latitude = m.Latitude mod.Session.GPS.Longitude = m.Longitude mod.Session.GPS.FixQuality = m.FixQuality @@ -139,6 +142,7 @@ func (mod *GPS) Start() error { mod.Session.GPS.Altitude = m.Altitude mod.Session.GPS.Separation = m.Separation } else if m, ok := s.(nmea.GPGGA); ok { + mod.Session.GPS.Updated = time.Now() mod.Session.GPS.Latitude = m.Latitude mod.Session.GPS.Longitude = m.Longitude mod.Session.GPS.FixQuality = m.FixQuality diff --git a/modules/hid/hid.go b/modules/hid/hid.go index 74897f95..1ae5b3b4 100644 --- a/modules/hid/hid.go +++ b/modules/hid/hid.go @@ -63,6 +63,10 @@ func NewHIDRecon(s *session.Session) *HIDRecon { scriptPath: "", } + mod.State.Store("sniffing", &mod.sniffAddr) + mod.State.Store("injecting", &mod.inInjectMode) + mod.State.Store("layouts", SupportedLayouts()) + mod.AddHandler(session.NewModuleHandler("hid.recon on", "", "Start scanning for HID devices on the 2.4Ghz spectrum.", func(args []string) error { @@ -159,7 +163,7 @@ func (mod *HIDRecon) Configure() error { var n int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } if err, mod.useLNA = mod.BoolParam("hid.lna"); err != nil { @@ -200,10 +204,20 @@ func (mod *HIDRecon) Configure() error { return nil } +func (mod *HIDRecon) forceStop() error { + return mod.SetRunning(false, func() { + if mod.dongle != nil { + mod.dongle.Close() + mod.Debug("device closed") + } + }) +} func (mod *HIDRecon) Stop() error { return mod.SetRunning(false, func() { mod.waitGroup.Wait() - mod.dongle.Close() - mod.Debug("device closed") + if mod.dongle != nil { + mod.dongle.Close() + mod.Debug("device closed") + } }) } diff --git a/modules/hid/hid_inject.go b/modules/hid/hid_inject.go index f5e10c3a..bcba94c5 100644 --- a/modules/hid/hid_inject.go +++ b/modules/hid/hid_inject.go @@ -52,16 +52,15 @@ func (mod *HIDRecon) prepInjection() (error, *network.HIDDevice, []*Command) { dev, found := mod.Session.HID.Get(mod.sniffAddr) if found == false { mod.Warning("device %s is not visible, will use HID type %s", mod.sniffAddr, tui.Yellow(mod.sniffType)) + } else if dev.Type == network.HIDTypeUnknown { + mod.Warning("device %s type has not been detected yet, falling back to '%s'", mod.sniffAddr, tui.Yellow(mod.sniffType)) } var builder FrameBuilder - if found { + if found && dev.Type != network.HIDTypeUnknown { // get the device specific protocol handler builder, found = FrameBuilders[dev.Type] if found == false { - if dev.Type == network.HIDTypeUnknown { - return errNoType(mod.sniffAddr), nil, nil - } return errNotSupported(dev), nil, nil } } else { diff --git a/modules/hid/hid_recon.go b/modules/hid/hid_recon.go index fee44273..13df827a 100644 --- a/modules/hid/hid_recon.go +++ b/modules/hid/hid_recon.go @@ -4,6 +4,7 @@ import ( "time" "github.com/bettercap/nrf24" + "github.com/google/gousb" ) func (mod *HIDRecon) doHopping() { @@ -26,7 +27,13 @@ func (mod *HIDRecon) doHopping() { mod.channel = 1 } if err := mod.dongle.SetChannel(mod.channel); err != nil { - mod.Warning("error hopping on channel %d: %v", mod.channel, err) + if err == gousb.ErrorNoDevice || err == gousb.TransferStall { + mod.Error("device disconnected, stopping module") + mod.forceStop() + return + } else { + mod.Warning("error hopping on channel %d: %v", mod.channel, err) + } } else { mod.lastHop = time.Now() } @@ -61,6 +68,25 @@ func (mod *HIDRecon) onDeviceDetected(buf []byte) { } } +var maxDeviceTTL = 20 * time.Minute + +func (mod *HIDRecon) devPruner() { + mod.waitGroup.Add(1) + defer mod.waitGroup.Done() + + mod.Debug("devices pruner started.") + for mod.Running() { + for _, dev := range mod.Session.HID.Devices() { + sinceLastSeen := time.Since(dev.LastSeen) + if sinceLastSeen > maxDeviceTTL { + mod.Debug("device %s not seen in %s, removing.", dev.Address, sinceLastSeen) + mod.Session.HID.Remove(dev.Address) + } + } + time.Sleep(30 * time.Second) + } +} + func (mod *HIDRecon) Start() error { if err := mod.Configure(); err != nil { return err @@ -70,6 +96,8 @@ func (mod *HIDRecon) Start() error { mod.waitGroup.Add(1) defer mod.waitGroup.Done() + go mod.devPruner() + mod.Info("hopping on %d channels every %s", nrf24.TopChannel, mod.hopPeriod) for mod.Running() { if mod.isSniffing() { @@ -86,6 +114,11 @@ func (mod *HIDRecon) Start() error { buf, err := mod.dongle.ReceivePayload() if err != nil { + if err == gousb.ErrorNoDevice || err == gousb.TransferStall { + mod.Error("device disconnected, stopping module") + mod.forceStop() + return + } mod.Warning("error receiving payload from channel %d: %v", mod.channel, err) continue } diff --git a/modules/hid/hid_sniff.go b/modules/hid/hid_sniff.go index 98198359..a6fd7ec4 100644 --- a/modules/hid/hid_sniff.go +++ b/modules/hid/hid_sniff.go @@ -41,6 +41,7 @@ func (mod *HIDRecon) setSniffMode(mode string, silent bool) error { mod.sniffAddrRaw = raw } } + return nil } diff --git a/modules/http_proxy/http_proxy.go b/modules/http_proxy/http_proxy.go index 90007b9f..9cec358d 100644 --- a/modules/http_proxy/http_proxy.go +++ b/modules/http_proxy/http_proxy.go @@ -2,6 +2,8 @@ package http_proxy import ( "github.com/bettercap/bettercap/session" + + "github.com/evilsocket/islazy/str" ) type HttpProxy struct { @@ -38,6 +40,12 @@ func NewHttpProxy(s *session.Session) *HttpProxy { "", "URL, path or javascript code to inject into every HTML page.")) + mod.AddParam(session.NewStringParameter("http.proxy.blacklist", "", "", + "Comma separated list of hostnames to skip while proxying (wildcard expressions can be used).")) + + mod.AddParam(session.NewStringParameter("http.proxy.whitelist", "", "", + "Comma separated list of hostnames to proxy if the blacklist is used (wildcard expressions can be used).")) + mod.AddParam(session.NewBoolParameter("http.proxy.sslstrip", "false", "Enable or disable SSL stripping.")) @@ -77,9 +85,11 @@ func (mod *HttpProxy) Configure() error { var scriptPath string var stripSSL bool var jsToInject string + var blacklist string + var whitelist string if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, address = mod.StringParam("http.proxy.address"); err != nil { return err } else if err, proxyPort = mod.IntParam("http.proxy.port"); err != nil { @@ -92,8 +102,15 @@ func (mod *HttpProxy) Configure() error { return err } else if err, jsToInject = mod.StringParam("http.proxy.injectjs"); err != nil { return err + } else if err, blacklist = mod.StringParam("http.proxy.blacklist"); err != nil { + return err + } else if err, whitelist = mod.StringParam("http.proxy.whitelist"); err != nil { + return err } + mod.proxy.Blacklist = str.Comma(blacklist) + mod.proxy.Whitelist = str.Comma(whitelist) + return mod.proxy.Configure(address, proxyPort, httpPort, scriptPath, jsToInject, stripSSL) } diff --git a/modules/http_proxy/http_proxy_base.go b/modules/http_proxy/http_proxy_base.go index 8acaf564..7d77b7ba 100644 --- a/modules/http_proxy/http_proxy_base.go +++ b/modules/http_proxy/http_proxy_base.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "net/url" + "path/filepath" "strconv" "strings" "time" @@ -41,6 +42,8 @@ type HTTPProxy struct { Script *HttpProxyScript CertFile string KeyFile string + Blacklist []string + Whitelist []string jsHook string isTLS bool @@ -61,13 +64,15 @@ func stripPort(s string) string { func NewHTTPProxy(s *session.Session) *HTTPProxy { p := &HTTPProxy{ - Name: "http.proxy", - Proxy: goproxy.NewProxyHttpServer(), - sess: s, - stripper: NewSSLStripper(s, false), - isTLS: false, - Server: nil, - tag: session.AsTag("http.proxy"), + Name: "http.proxy", + Proxy: goproxy.NewProxyHttpServer(), + sess: s, + stripper: NewSSLStripper(s, false), + isTLS: false, + Server: nil, + Blacklist: make([]string, 0), + Whitelist: make([]string, 0), + tag: session.AsTag("http.proxy"), } p.Proxy.Verbose = false @@ -111,20 +116,41 @@ func (p *HTTPProxy) Fatal(format string, args ...interface{}) { } func (p *HTTPProxy) doProxy(req *http.Request) bool { - blacklist := []string{ - "localhost", - "127.0.0.1", - } - if req.Host == "" { p.Error("got request with empty host: %v", req) return false } - host := strings.Split(req.Host, ":")[0] - for _, blacklisted := range blacklist { - if host == blacklisted { - p.Error("got request with blacklisted host: %s", req.Host) + hostname := strings.Split(req.Host, ":")[0] + for _, local := range []string{"localhost", "127.0.0.1"} { + if hostname == local { + p.Error("got request with localed host: %s", req.Host) + return false + } + } + + return true +} + +func (p *HTTPProxy) shouldProxy(req *http.Request) bool { + hostname := strings.Split(req.Host, ":")[0] + + // check for the whitelist + for _, expr := range p.Whitelist { + if matched, err := filepath.Match(expr, hostname); err != nil { + p.Error("error while using proxy whitelist expression '%s': %v", expr, err) + } else if matched { + p.Debug("hostname '%s' matched whitelisted element '%s'", hostname, expr) + return true + } + } + + // then the blacklist + for _, expr := range p.Blacklist { + if matched, err := filepath.Match(expr, hostname); err != nil { + p.Error("error while using proxy blacklist expression '%s': %v", expr, err) + } else if matched { + p.Debug("hostname '%s' matched blacklisted element '%s'", hostname, expr) return false } } diff --git a/modules/http_proxy/http_proxy_base_filters.go b/modules/http_proxy/http_proxy_base_filters.go index d8ad23ce..52bd6e69 100644 --- a/modules/http_proxy/http_proxy_base_filters.go +++ b/modules/http_proxy/http_proxy_base_filters.go @@ -19,32 +19,34 @@ func (p *HTTPProxy) fixRequestHeaders(req *http.Request) { } func (p *HTTPProxy) onRequestFilter(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { - p.Debug("< %s %s %s%s", req.RemoteAddr, req.Method, req.Host, req.URL.Path) + if p.shouldProxy(req) { + p.Debug("< %s %s %s%s", req.RemoteAddr, req.Method, req.Host, req.URL.Path) - p.fixRequestHeaders(req) + p.fixRequestHeaders(req) - redir := p.stripper.Preprocess(req, ctx) - if redir != nil { - // we need to redirect the user in order to make - // some session cookie expire - return req, redir - } + redir := p.stripper.Preprocess(req, ctx) + if redir != nil { + // we need to redirect the user in order to make + // some session cookie expire + return req, redir + } - // do we have a proxy script? - if p.Script == nil { - return req, nil - } + // do we have a proxy script? + if p.Script == nil { + return req, nil + } - // run the module OnRequest callback if defined - jsreq, jsres := p.Script.OnRequest(req) - if jsreq != nil { - // the request has been changed by the script - p.logRequestAction(req, jsreq) - return jsreq.ToRequest(), nil - } else if jsres != nil { - // a fake response has been returned by the script - p.logResponseAction(req, jsres) - return req, jsres.ToResponse(req) + // run the module OnRequest callback if defined + jsreq, jsres := p.Script.OnRequest(req) + if jsreq != nil { + // the request has been changed by the script + p.logRequestAction(req, jsreq) + return jsreq.ToRequest(), nil + } else if jsres != nil { + // a fake response has been returned by the script + p.logResponseAction(req, jsres) + return req, jsres.ToResponse(req) + } } return req, nil @@ -123,28 +125,30 @@ func (p *HTTPProxy) onResponseFilter(res *http.Response, ctx *goproxy.ProxyCtx) return nil } - p.Debug("> %s %s %s%s", res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path) + if p.shouldProxy(res.Request) { + p.Debug("> %s %s %s%s", res.Request.RemoteAddr, res.Request.Method, res.Request.Host, res.Request.URL.Path) - p.fixResponseHeaders(res) + p.fixResponseHeaders(res) - p.stripper.Process(res, ctx) + p.stripper.Process(res, ctx) - // do we have a proxy script? - if p.Script != nil { - _, jsres := p.Script.OnResponse(res) - if jsres != nil { - // the response has been changed by the script - p.logResponseAction(res.Request, jsres) - return jsres.ToResponse(res.Request) + // do we have a proxy script? + if p.Script != nil { + _, jsres := p.Script.OnResponse(res) + if jsres != nil { + // the response has been changed by the script + p.logResponseAction(res.Request, jsres) + return jsres.ToResponse(res.Request) + } } - } - // inject javascript code if specified and needed - if doInject, cType := p.isScriptInjectable(res); doInject { - if err, injectedResponse := p.doScriptInjection(res, cType); err != nil { - p.Error("error while injecting javascript: %s", err) - } else if injectedResponse != nil { - return injectedResponse + // inject javascript code if specified and needed + if doInject, cType := p.isScriptInjectable(res); doInject { + if err, injectedResponse := p.doScriptInjection(res, cType); err != nil { + p.Error("error while injecting javascript: %s", err) + } else if injectedResponse != nil { + return injectedResponse + } } } diff --git a/modules/http_server/http_server.go b/modules/http_server/http_server.go index f03aff73..d5ad1845 100644 --- a/modules/http_server/http_server.go +++ b/modules/http_server/http_server.go @@ -71,7 +71,7 @@ func (mod *HttpServer) Configure() error { var port int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } if err, path = mod.StringParam("http.server.path"); err != nil { @@ -82,7 +82,7 @@ func (mod *HttpServer) Configure() error { fileServer := http.FileServer(http.Dir(path)) router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mod.Info("%s %s %s%s", tui.Bold(strings.Split(r.RemoteAddr, ":")[0]), r.Method, r.Host, r.URL.Path) + mod.Debug("%s %s %s%s", tui.Bold(strings.Split(r.RemoteAddr, ":")[0]), r.Method, r.Host, r.URL.Path) fileServer.ServeHTTP(w, r) })) @@ -110,7 +110,8 @@ func (mod *HttpServer) Start() error { var err error mod.Info("starting on http://%s", mod.server.Addr) if err = mod.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - panic(err) + mod.Error("%v", err) + mod.Stop() } }) } diff --git a/modules/https_proxy/https_proxy.go b/modules/https_proxy/https_proxy.go index 25a74d08..10bfaca1 100644 --- a/modules/https_proxy/https_proxy.go +++ b/modules/https_proxy/https_proxy.go @@ -6,6 +6,7 @@ import ( "github.com/bettercap/bettercap/tls" "github.com/evilsocket/islazy/fs" + "github.com/evilsocket/islazy/str" ) type HttpsProxy struct { @@ -58,6 +59,12 @@ func NewHttpsProxy(s *session.Session) *HttpsProxy { "", "Path of a proxy JS script.")) + mod.AddParam(session.NewStringParameter("https.proxy.blacklist", "", "", + "Comma separated list of hostnames to skip while proxying (wildcard expressions can be used).")) + + mod.AddParam(session.NewStringParameter("https.proxy.whitelist", "", "", + "Comma separated list of hostnames to proxy if the blacklist is used (wildcard expressions can be used).")) + mod.AddHandler(session.NewModuleHandler("https.proxy on", "", "Start HTTPS proxy.", func(args []string) error { @@ -95,9 +102,11 @@ func (mod *HttpsProxy) Configure() error { var keyFile string var stripSSL bool var jsToInject string + var whitelist string + var blacklist string if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, address = mod.StringParam("https.proxy.address"); err != nil { return err } else if err, proxyPort = mod.IntParam("https.proxy.port"); err != nil { @@ -118,8 +127,15 @@ func (mod *HttpsProxy) Configure() error { return err } else if err, jsToInject = mod.StringParam("https.proxy.injectjs"); err != nil { return err + } else if err, blacklist = mod.StringParam("https.proxy.blacklist"); err != nil { + return err + } else if err, whitelist = mod.StringParam("https.proxy.whitelist"); err != nil { + return err } + mod.proxy.Blacklist = str.Comma(blacklist) + mod.proxy.Whitelist = str.Comma(whitelist) + if !fs.Exists(certFile) || !fs.Exists(keyFile) { err, cfg := tls.CertConfigFromModule("https.proxy", mod.SessionModule) if err != nil { diff --git a/modules/https_server/https_server.go b/modules/https_server/https_server.go index 0ed5e96a..6d4a4813 100644 --- a/modules/https_server/https_server.go +++ b/modules/https_server/https_server.go @@ -89,7 +89,7 @@ func (mod *HttpsServer) Configure() error { var keyFile string if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } if err, path = mod.StringParam("https.server.path"); err != nil { @@ -100,7 +100,7 @@ func (mod *HttpsServer) Configure() error { fileServer := http.FileServer(http.Dir(path)) router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - mod.Info("%s %s %s%s", tui.Bold(strings.Split(r.RemoteAddr, ":")[0]), r.Method, r.Host, r.URL.Path) + mod.Debug("%s %s %s%s", tui.Bold(strings.Split(r.RemoteAddr, ":")[0]), r.Method, r.Host, r.URL.Path) fileServer.ServeHTTP(w, r) })) @@ -159,7 +159,8 @@ func (mod *HttpsServer) Start() error { return mod.SetRunning(true, func() { mod.Info("starting on https://%s", mod.server.Addr) if err := mod.server.ListenAndServeTLS(mod.certFile, mod.keyFile); err != nil && err != http.ErrServerClosed { - panic(err) + mod.Error("%v", err) + mod.Stop() } }) } diff --git a/modules/mac_changer/mac_changer.go b/modules/mac_changer/mac_changer.go index b0a80acf..a991cfb1 100644 --- a/modules/mac_changer/mac_changer.go +++ b/modules/mac_changer/mac_changer.go @@ -103,7 +103,7 @@ func (mod *MacChanger) setMac(mac net.HardwareAddr) error { func (mod *MacChanger) Start() error { if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err := mod.Configure(); err != nil { return err } else if err := mod.setMac(mod.fakeMac); err != nil { diff --git a/modules/modules.go b/modules/modules.go index 15e15c4b..fed48f25 100644 --- a/modules/modules.go +++ b/modules/modules.go @@ -24,6 +24,7 @@ import ( "github.com/bettercap/bettercap/modules/syn_scan" "github.com/bettercap/bettercap/modules/tcp_proxy" "github.com/bettercap/bettercap/modules/ticker" + "github.com/bettercap/bettercap/modules/ui" "github.com/bettercap/bettercap/modules/update" "github.com/bettercap/bettercap/modules/wifi" "github.com/bettercap/bettercap/modules/wol" @@ -36,7 +37,6 @@ func LoadModules(sess *session.Session) { sess.Register(arp_spoof.NewArpSpoofer(sess)) sess.Register(api_rest.NewRestAPI(sess)) sess.Register(ble.NewBLERecon(sess)) - sess.Register(caplets.NewCapletsModule(sess)) sess.Register(dhcp6_spoof.NewDHCP6Spoofer(sess)) sess.Register(net_recon.NewDiscovery(sess)) sess.Register(dns_spoof.NewDNSSpoofer(sess)) @@ -54,8 +54,11 @@ func LoadModules(sess *session.Session) { sess.Register(syn_scan.NewSynScanner(sess)) sess.Register(tcp_proxy.NewTcpProxy(sess)) sess.Register(ticker.NewTicker(sess)) - sess.Register(update.NewUpdateModule(sess)) sess.Register(wifi.NewWiFiModule(sess)) sess.Register(wol.NewWOL(sess)) sess.Register(hid.NewHIDRecon(sess)) + + sess.Register(caplets.NewCapletsModule(sess)) + sess.Register(update.NewUpdateModule(sess)) + sess.Register(ui.NewUIModule(sess)) } diff --git a/modules/mysql_server/mysql_server.go b/modules/mysql_server/mysql_server.go index eb145c9b..3ddb1d97 100644 --- a/modules/mysql_server/mysql_server.go +++ b/modules/mysql_server/mysql_server.go @@ -79,7 +79,7 @@ func (mod *MySQLServer) Configure() error { var port int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, mod.infile = mod.StringParam("mysql.server.infile"); err != nil { return err } else if err, mod.outfile = mod.StringParam("mysql.server.outfile"); err != nil { diff --git a/modules/net_recon/net_recon.go b/modules/net_recon/net_recon.go index 3ec71168..5208a010 100644 --- a/modules/net_recon/net_recon.go +++ b/modules/net_recon/net_recon.go @@ -48,13 +48,13 @@ func NewDiscovery(s *session.Session) *Discovery { })) mod.AddHandler(session.NewModuleHandler("net.show ADDRESS1, ADDRESS2", `net.show (.+)`, - "Show information about a specific list of addresses (by IP or MAC).", + "Show information about a specific comma separated list of addresses (by IP or MAC).", func(args []string) error { return mod.Show(args[0]) })) mod.AddHandler(session.NewModuleHandler("net.show.meta ADDRESS1, ADDRESS2", `net\.show\.meta (.+)`, - "Show meta information about a specific list of addresses (by IP or MAC).", + "Show meta information about a specific comma separated list of addresses (by IP or MAC).", func(args []string) error { return mod.showMeta(args[0]) })) diff --git a/modules/net_sniff/net_sniff.go b/modules/net_sniff/net_sniff.go index 97dc6082..d97780c2 100644 --- a/modules/net_sniff/net_sniff.go +++ b/modules/net_sniff/net_sniff.go @@ -146,7 +146,7 @@ func (mod *Sniffer) Configure() error { var err error if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, mod.Ctx = mod.GetContext(); err != nil { if mod.Ctx != nil { mod.Ctx.Close() diff --git a/modules/packet_proxy/packet_proxy_linux_amd64.go b/modules/packet_proxy/packet_proxy_linux_amd64.go index f934c13b..0f4f7243 100644 --- a/modules/packet_proxy/packet_proxy_linux_amd64.go +++ b/modules/packet_proxy/packet_proxy_linux_amd64.go @@ -190,7 +190,7 @@ func dummyCallback(payload *nfqueue.Payload) int { func (mod *PacketProxy) Start() error { if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err := mod.Configure(); err != nil { return err } diff --git a/modules/syn_scan/syn_scan.go b/modules/syn_scan/syn_scan.go index 76d36dda..95e683b5 100644 --- a/modules/syn_scan/syn_scan.go +++ b/modules/syn_scan/syn_scan.go @@ -45,6 +45,9 @@ func NewSynScanner(s *session.Session) *SynScanner { progressEvery: time.Duration(1) * time.Second, } + mod.State.Store("scanning", &mod.addresses) + mod.State.Store("progress", 0.0) + mod.AddParam(session.NewIntParameter("syn.scan.show-progress-every", "1", "Period in seconds for the scanning progress reporting.")) @@ -58,7 +61,7 @@ func NewSynScanner(s *session.Session) *SynScanner { return mod.Stop() })) - mod.AddHandler(session.NewModuleHandler("syn.scan IP-RANGE [START-PORT] [END-PORT]", "syn.scan ([^\\s]+) ?(\\d+)?([\\s\\d]*)?", + mod.AddHandler(session.NewModuleHandler("syn.scan IP-RANGE START-PORT END-PORT", "syn.scan ([^\\s]+) ?(\\d+)?([\\s\\d]*)?", "Perform a syn port scanning against an IP address within the provided ports range.", func(args []string) error { period := 0 @@ -155,6 +158,7 @@ func plural(n uint64) string { func (mod *SynScanner) showProgress() error { progress := 100.0 * (float64(mod.stats.doneProbes) / float64(mod.stats.totProbes)) + mod.State.Store("progress", progress) mod.Info("[%.2f%%] found %d open port%s for %d address%s, sent %d/%d packets in %s", progress, mod.stats.openPorts, @@ -172,12 +176,17 @@ func (mod *SynScanner) Stop() error { return mod.SetRunning(false, func() { mod.waitGroup.Wait() mod.showProgress() + mod.addresses = []net.IP{} + mod.State.Store("progress", 0.0) }) } func (mod *SynScanner) synScan() error { mod.SetRunning(true, func() { - defer mod.SetRunning(false, nil) + defer mod.SetRunning(false, func() { + mod.addresses = []net.IP{} + mod.State.Store("progress", 0.0) + }) mod.waitGroup.Add(1) defer mod.waitGroup.Done() @@ -199,6 +208,8 @@ func (mod *SynScanner) synScan() error { mod.Info("scanning %d address%s on port %d ...", mod.stats.numAddresses, plural, mod.startPort) } + mod.State.Store("progress", 0.0) + // set the collector mod.Session.Queue.OnPacket(mod.onPacket) defer mod.Session.Queue.OnPacket(nil) diff --git a/modules/tcp_proxy/tcp_proxy.go b/modules/tcp_proxy/tcp_proxy.go index 1a13e3bb..23ac6041 100644 --- a/modules/tcp_proxy/tcp_proxy.go +++ b/modules/tcp_proxy/tcp_proxy.go @@ -95,7 +95,7 @@ func (mod *TcpProxy) Configure() error { var tunnelPort int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, address = mod.StringParam("tcp.address"); err != nil { return err } else if err, proxyAddress = mod.StringParam("tcp.proxy.address"); err != nil { diff --git a/modules/ticker/ticker.go b/modules/ticker/ticker.go index 5e62139c..3edaafc3 100644 --- a/modules/ticker/ticker.go +++ b/modules/ticker/ticker.go @@ -59,7 +59,7 @@ func (mod *Ticker) Configure() error { var period int if mod.Running() { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } else if err, commands = mod.StringParam("ticker.commands"); err != nil { return err } else if err, period = mod.IntParam("ticker.period"); err != nil { diff --git a/modules/ui/ui.go b/modules/ui/ui.go new file mode 100644 index 00000000..f5d6950e --- /dev/null +++ b/modules/ui/ui.go @@ -0,0 +1,190 @@ +package ui + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "regexp" + + "github.com/bettercap/bettercap/session" + + "github.com/google/go-github/github" + + "github.com/evilsocket/islazy/fs" + "github.com/evilsocket/islazy/tui" + "github.com/evilsocket/islazy/zip" +) + +var versionParser = regexp.MustCompile(`name:"ui",version:"([^"]+)"`) + +type UIModule struct { + session.SessionModule + client *github.Client + tmpFile string + basePath string + uiPath string +} + +func NewUIModule(s *session.Session) *UIModule { + mod := &UIModule{ + SessionModule: session.NewSessionModule("ui", s), + client: github.NewClient(nil), + } + + mod.AddParam(session.NewStringParameter("ui.basepath", + "/usr/local/share/bettercap/", + "", + "UI base installation path.")) + + mod.AddParam(session.NewStringParameter("ui.tmpfile", + filepath.Join(os.TempDir(), "ui.zip"), + "", + "Temporary file to use while downloading UI updates.")) + + mod.AddHandler(session.NewModuleHandler("ui.version", "", + "Print the currently installed UI version.", + func(args []string) error { + return mod.showVersion() + })) + + mod.AddHandler(session.NewModuleHandler("ui.update", "", + "Download the latest available version of the UI and install it.", + func(args []string) error { + return mod.Start() + })) + + return mod +} + +func (mod *UIModule) Name() string { + return "ui" +} + +func (mod *UIModule) Description() string { + return "A module to manage bettercap's UI updates and installed version." +} + +func (mod *UIModule) Author() string { + return "Simone Margaritelli " +} + +func (mod *UIModule) Configure() (err error) { + if err, mod.basePath = mod.StringParam("ui.basepath"); err != nil { + return err + } else { + mod.uiPath = filepath.Join(mod.basePath, "ui") + } + + if err, mod.tmpFile = mod.StringParam("ui.tmpfile"); err != nil { + return err + } + + return nil +} + +func (mod *UIModule) Stop() error { + return nil +} + +func (mod *UIModule) download(version, url string) error { + if !fs.Exists(mod.basePath) { + mod.Warning("creating ui install path %s ...", mod.basePath) + if err := os.MkdirAll(mod.basePath, os.ModePerm); err != nil { + return err + } + } + + out, err := os.Create(mod.tmpFile) + if err != nil { + return err + } + defer out.Close() + defer os.Remove(mod.tmpFile) + + mod.Info("downloading ui %s from %s ...", tui.Bold(version), url) + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if _, err := io.Copy(out, resp.Body); err != nil { + return err + } + + if fs.Exists(mod.uiPath) { + mod.Warning("removing previously installed UI from %s ...", mod.uiPath) + if err := os.RemoveAll(mod.uiPath); err != nil { + return err + } + } + + mod.Info("installing to %s ...", mod.uiPath) + + if _, err = zip.Unzip(mod.tmpFile, mod.basePath); err != nil { + return err + } + + mod.Info("installation complete, you can now run the %s (or https-ui) caplet to start the UI.", tui.Bold("http-ui")) + + return nil +} + +func (mod *UIModule) showVersion() error { + if err := mod.Configure(); err != nil { + return err + } + + if !fs.Exists(mod.uiPath) { + return fmt.Errorf("path %s does not exist, ui not installed", mod.uiPath) + } + + search := filepath.Join(mod.uiPath, "/main.*.js") + matches, err := filepath.Glob(search) + if err != nil { + return err + } else if len(matches) == 0 { + return fmt.Errorf("can't find any main.*.js files in %s", mod.uiPath) + } + + for _, filename := range matches { + if raw, err := ioutil.ReadFile(filename); err != nil { + return err + } else if m := versionParser.FindStringSubmatch(string(raw)); m != nil { + version := m[1] + mod.Info("v%s", version) + return nil + } + } + + return fmt.Errorf("can't parse version from %s", search) +} + +func (mod *UIModule) Start() error { + if err := mod.Configure(); err != nil { + return err + } else if err := mod.SetRunning(true, nil); err != nil { + return err + } + defer mod.SetRunning(false, nil) + + mod.Info("checking latest stable release ...") + + if releases, _, err := mod.client.Repositories.ListReleases(context.Background(), "bettercap", "ui", nil); err == nil { + latest := releases[0] + for _, a := range latest.Assets { + if *a.Name == "ui.zip" { + return mod.download(*latest.TagName, *a.BrowserDownloadURL) + } + } + } else { + mod.Error("error while fetching latest release info from GitHub: %s", err) + } + + return nil +} diff --git a/modules/utils/script_builtins.go b/modules/utils/script_builtins.go index 3e4e7426..cc49ec30 100644 --- a/modules/utils/script_builtins.go +++ b/modules/utils/script_builtins.go @@ -38,7 +38,7 @@ func init() { entry_list := []string{} for _, file := range dir { - entry_list = append( entry_list, file.Name() ) + entry_list = append(entry_list, file.Name()) } v, err := otto.Otto.ToValue(*call.Otto, entry_list) @@ -167,21 +167,21 @@ func init() { return errOtto("gzipCompress: expected 1 argument, %d given instead.", argc) } - uncompressed_bytes := []byte( argv[0].String() ) + uncompressed_bytes := []byte(argv[0].String()) var writer_buffer bytes.Buffer gzip_writer := gzip.NewWriter(&writer_buffer) _, err := gzip_writer.Write(uncompressed_bytes) if err != nil { - return errOtto( "gzipCompress: could not compress data: %s", err.Error() ) + return errOtto("gzipCompress: could not compress data: %s", err.Error()) } gzip_writer.Close() compressed_bytes := writer_buffer.Bytes() - v, err := otto.ToValue( string(compressed_bytes) ) + v, err := otto.ToValue(string(compressed_bytes)) if err != nil { - return errOtto( "Could not convert to string: %s", err.Error() ) + return errOtto("Could not convert to string: %s", err.Error()) } return v @@ -195,24 +195,24 @@ func init() { return errOtto("gzipDecompress: expected 1 argument, %d given instead.", argc) } - compressed_bytes := []byte( argv[0].String() ) + compressed_bytes := []byte(argv[0].String()) reader_buffer := bytes.NewBuffer(compressed_bytes) gzip_reader, err := gzip.NewReader(reader_buffer) if err != nil { - return errOtto( "gzipDecompress: could not create gzip reader: %s", err.Error() ) + return errOtto("gzipDecompress: could not create gzip reader: %s", err.Error()) } var decompressed_buffer bytes.Buffer _, err = decompressed_buffer.ReadFrom(gzip_reader) if err != nil { - return errOtto( "gzipDecompress: could not decompress data: %s", err.Error() ) + return errOtto("gzipDecompress: could not decompress data: %s", err.Error()) } decompressed_bytes := decompressed_buffer.Bytes() - v, err := otto.ToValue( string(decompressed_bytes) ) + v, err := otto.ToValue(string(decompressed_bytes)) if err != nil { - return errOtto( "Could not convert to string: %s", err.Error() ) + return errOtto("Could not convert to string: %s", err.Error()) } return v diff --git a/modules/wifi/wifi.go b/modules/wifi/wifi.go index bdd513d5..2a48b4a1 100644 --- a/modules/wifi/wifi.go +++ b/modules/wifi/wifi.go @@ -38,6 +38,7 @@ type WiFiModule struct { frequencies []int ap *network.AccessPoint stickChan int + shakesFile string skipBroken bool pktSourceChan chan gopacket.Packet pktSourceChanClosed bool @@ -47,7 +48,6 @@ type WiFiModule struct { assocSkip []net.HardwareAddr assocSilent bool assocOpen bool - shakesFile string apRunning bool showManuf bool apConfig packets.Dot11ApConfig @@ -81,6 +81,8 @@ func NewWiFiModule(s *session.Session) *WiFiModule { chanLock: &sync.Mutex{}, } + mod.InitState("channels") + mod.AddParam(session.NewStringParameter("wifi.interface", "", "", @@ -124,7 +126,8 @@ func NewWiFiModule(s *session.Session) *WiFiModule { func(args []string) (err error) { mod.ap = nil mod.stickChan = 0 - mod.frequencies, err = network.GetSupportedFrequencies(mod.iface.Name()) + freqs, err := network.GetSupportedFrequencies(mod.iface.Name()) + mod.setFrequencies(freqs) mod.hopChanges <- true return err })) @@ -258,7 +261,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule { "false", "If true, wifi.show will also show the devices manufacturers.")) - mod.AddHandler(session.NewModuleHandler("wifi.recon.channel", `wifi\.recon\.channel[\s]+([0-9]+(?:[, ]+[0-9]+)*|clear)`, + mod.AddHandler(session.NewModuleHandler("wifi.recon.channel CHANNEL", `wifi\.recon\.channel[\s]+([0-9]+(?:[, ]+[0-9]+)*|clear)`, "WiFi channels (comma separated) or 'clear' for channel hopping.", func(args []string) (err error) { freqs := []int{} @@ -285,8 +288,7 @@ func NewWiFiModule(s *session.Session) *WiFiModule { } } - mod.Debug("new frequencies: %v", freqs) - mod.frequencies = freqs + mod.setFrequencies(freqs) // if wifi.recon is not running, this would block forever if mod.Running() { @@ -330,6 +332,17 @@ const ( ErrIfaceNotUp = "Interface Not Up" ) +func (mod *WiFiModule) setFrequencies(freqs []int) { + mod.Debug("new frequencies: %v", freqs) + + mod.frequencies = freqs + channels := []int{} + for _, freq := range freqs { + channels = append(channels, network.Dot11Freq2Chan(freq)) + } + mod.State.Store("channels", channels) +} + func (mod *WiFiModule) Configure() error { var ifName string var hopPeriod int @@ -428,21 +441,21 @@ func (mod *WiFiModule) Configure() error { mod.hopPeriod = time.Duration(hopPeriod) * time.Millisecond if mod.source == "" { - // No channels setted, retrieve frequencies supported by the card - if len(mod.frequencies) == 0 { - if mod.frequencies, err = network.GetSupportedFrequencies(ifName); err != nil { - return fmt.Errorf("error while getting supported frequencies of %s: %s", ifName, err) - } - - mod.Debug("wifi supported frequencies: %v", mod.frequencies) - - // we need to start somewhere, this is just to check if - // this OS supports switching channel programmatically. - if err = network.SetInterfaceChannel(ifName, 1); err != nil { - return fmt.Errorf("error while initializing %s to channel 1: %s", ifName, err) - } - mod.Info("started (min rssi: %d dBm)", mod.minRSSI) + if freqs, err := network.GetSupportedFrequencies(ifName); err != nil { + return fmt.Errorf("error while getting supported frequencies of %s: %s", ifName, err) + } else { + mod.setFrequencies(freqs) } + + mod.Debug("wifi supported frequencies: %v", mod.frequencies) + + // we need to start somewhere, this is just to check if + // this OS supports switching channel programmatically. + if err = network.SetInterfaceChannel(ifName, 1); err != nil { + return fmt.Errorf("error while initializing %s to channel 1: %s", ifName, err) + } + + mod.Info("started (min rssi: %d dBm)", mod.minRSSI) } return nil @@ -480,13 +493,17 @@ func (mod *WiFiModule) updateStats(dot11 *layers.Dot11, packet gopacket.Packet) bytes := uint64(len(packet.Data())) dst := dot11.Address1.String() - if station, found := mod.Session.WiFi.Get(dst); found { - station.Received += bytes + if ap, found := mod.Session.WiFi.Get(dst); found { + ap.Received += bytes + } else if sta, found := mod.Session.WiFi.GetClient(dst); found { + sta.Received += bytes } src := dot11.Address2.String() - if station, found := mod.Session.WiFi.Get(src); found { - station.Sent += bytes + if ap, found := mod.Session.WiFi.Get(src); found { + ap.Sent += bytes + } else if sta, found := mod.Session.WiFi.GetClient(src); found { + sta.Sent += bytes } } } @@ -544,6 +561,17 @@ func (mod *WiFiModule) Start() error { return nil } +func (mod *WiFiModule) forcedStop() error { + return mod.SetRunning(false, func() { + // signal the main for loop we want to exit + if !mod.pktSourceChanClosed { + mod.pktSourceChan <- nil + } + // close the pcap handle to make the main for exit + mod.handle.Close() + }) +} + func (mod *WiFiModule) Stop() error { return mod.SetRunning(false, func() { // wait any pending write operation diff --git a/modules/wifi/wifi_ap.go b/modules/wifi/wifi_ap.go index fcc4c29a..7a5ed0b7 100644 --- a/modules/wifi/wifi_ap.go +++ b/modules/wifi/wifi_ap.go @@ -35,7 +35,7 @@ func (mod *WiFiModule) startAp() error { if !mod.Running() { return errNoRecon } else if mod.apRunning { - return session.ErrAlreadyStarted + return session.ErrAlreadyStarted(mod.Name()) } go func() { diff --git a/modules/wifi/wifi_hopping.go b/modules/wifi/wifi_hopping.go index 79fea79e..99fdbf07 100644 --- a/modules/wifi/wifi_hopping.go +++ b/modules/wifi/wifi_hopping.go @@ -1,11 +1,47 @@ package wifi import ( + "net" "time" "github.com/bettercap/bettercap/network" ) +func (mod *WiFiModule) isInterfaceConnected() bool { + ifaces, err := net.Interfaces() + if err != nil { + mod.Error("error while enumerating interfaces: %s", err) + return false + } + + for _, iface := range ifaces { + if mod.iface.HwAddress == network.NormalizeMac(iface.HardwareAddr.String()) { + return true + } + } + + return false +} + +func (mod *WiFiModule) hop(channel int) (mustStop bool) { + mod.chanLock.Lock() + defer mod.chanLock.Unlock() + + mod.Debug("hopping on channel %d", channel) + + if err := network.SetInterfaceChannel(mod.iface.Name(), channel); err != nil { + // check if the device has been disconnected + if mod.isInterfaceConnected() == false { + mod.Error("interface %s disconnected, stopping module", mod.iface.Name()) + mustStop = true + } else { + mod.Warning("error while hopping to channel %d: %s", channel, err) + } + } + + return +} + func (mod *WiFiModule) onChannel(channel int, cb func()) { mod.chanLock.Lock() defer mod.chanLock.Unlock() @@ -13,11 +49,7 @@ func (mod *WiFiModule) onChannel(channel int, cb func()) { prev := mod.stickChan mod.stickChan = channel - if err := network.SetInterfaceChannel(mod.iface.Name(), channel); err != nil { - mod.Warning("error while hopping to channel %d: %s", channel, err) - } else { - mod.Debug("hopped on channel %d", channel) - } + mod.hop(channel) cb() @@ -50,13 +82,10 @@ func (mod *WiFiModule) channelHopper() { channel = mod.stickChan } - mod.Debug("hopping on channel %d", channel) - - mod.chanLock.Lock() - if err := network.SetInterfaceChannel(mod.iface.Name(), channel); err != nil { - mod.Warning("error while hopping to channel %d: %s", channel, err) + if stop := mod.hop(channel); stop { + mod.forcedStop() + return } - mod.chanLock.Unlock() select { case _ = <-mod.hopChanges: diff --git a/modules/wifi/wifi_recon.go b/modules/wifi/wifi_recon.go index dab0360b..6e62d0d9 100644 --- a/modules/wifi/wifi_recon.go +++ b/modules/wifi/wifi_recon.go @@ -124,7 +124,7 @@ func (mod *WiFiModule) discoverClients(radiotap *layers.RadioTap, dot11 *layers. freq := int(radiotap.ChannelFrequency) rssi := radiotap.DBMAntennaSignal - if station, isNew := ap.AddClientIfNew(bssid, freq, rssi, mod.Session.Lan.Aliases()); isNew { + if station, isNew := ap.AddClientIfNew(bssid, freq, rssi); isNew { mod.Session.Events.Add("wifi.client.new", ClientEvent{ AP: ap, Client: station, diff --git a/modules/wifi/wifi_recon_handshakes.go b/modules/wifi/wifi_recon_handshakes.go index 7c5045a5..599eec3e 100644 --- a/modules/wifi/wifi_recon_handshakes.go +++ b/modules/wifi/wifi_recon_handshakes.go @@ -35,8 +35,9 @@ func (mod *WiFiModule) discoverHandshakes(radiotap *layers.RadioTap, dot11 *laye // in order to have a consistent association of AP, client and handshakes. staIsUs := bytes.Equal(staMac, mod.iface.HW) station, found := ap.Get(staMac.String()) + staAdded := false if !found { - station, _ = ap.AddClientIfNew(staMac.String(), ap.Frequency, ap.RSSI, mod.Session.Lan.Aliases()) + station, staAdded = ap.AddClientIfNew(staMac.String(), ap.Frequency, ap.RSSI) } rawPMKID := []byte(nil) @@ -96,10 +97,9 @@ func (mod *WiFiModule) discoverHandshakes(radiotap *layers.RadioTap, dot11 *laye // is persisted even after stations are pruned due to inactivity ap.WithKeyMaterial(true) } - // if we added ourselves as a client station but we didn't get any // PMKID, just remove it from the list of clients of this AP. - if staIsUs && rawPMKID == nil { + if staAdded || (staIsUs && rawPMKID == nil) { ap.RemoveClient(staMac.String()) } } diff --git a/network/ble.go b/network/ble.go index c167ae17..0b492194 100644 --- a/network/ble.go +++ b/network/ble.go @@ -9,6 +9,8 @@ import ( "time" "github.com/bettercap/gatt" + + "github.com/evilsocket/islazy/data" ) const BLEMacValidator = "([a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2}:[a-fA-F0-9]{1,2})" @@ -18,6 +20,7 @@ type BLEDevLostCallback func(dev *BLEDevice) type BLE struct { sync.RWMutex + aliases *data.UnsortedKV devices map[string]*BLEDevice newCb BLEDevNewCallback lostCb BLEDevLostCallback @@ -27,9 +30,10 @@ type bleJSON struct { Devices []*BLEDevice `json:"devices"` } -func NewBLE(newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { +func NewBLE(aliases *data.UnsortedKV, newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { return &BLE{ devices: make(map[string]*BLEDevice), + aliases: aliases, newCb: newcb, lostCb: lostcb, } @@ -55,14 +59,19 @@ func (b *BLE) AddIfNew(id string, p gatt.Peripheral, a *gatt.Advertisement, rssi defer b.Unlock() id = NormalizeMac(id) + alias := b.aliases.GetOr(id, "") if dev, found := b.devices[id]; found { dev.LastSeen = time.Now() dev.RSSI = rssi dev.Advertisement = a + if alias != "" { + dev.Alias = alias + } return dev } newDev := NewBLEDevice(p, a, rssi) + newDev.Alias = alias b.devices[id] = newDev if b.newCb != nil { diff --git a/network/ble_device.go b/network/ble_device.go index 3cb115aa..0b1ef7bb 100644 --- a/network/ble_device.go +++ b/network/ble_device.go @@ -27,6 +27,7 @@ type BLEService struct { } type BLEDevice struct { + Alias string LastSeen time.Time DeviceName string Vendor string @@ -40,6 +41,7 @@ type bleDeviceJSON struct { LastSeen time.Time `json:"last_seen"` Name string `json:"name"` MAC string `json:"mac"` + Alias string `json:"alias"` Vendor string `json:"vendor"` RSSI int `json:"rssi"` Connectable bool `json:"connectable"` @@ -81,6 +83,7 @@ func (d *BLEDevice) MarshalJSON() ([]byte, error) { LastSeen: d.LastSeen, Name: d.Name(), MAC: d.Device.ID(), + Alias: d.Alias, Vendor: d.Vendor, RSSI: d.RSSI, Connectable: d.Advertisement.Connectable, diff --git a/network/ble_unsupported.go b/network/ble_unsupported.go index 9765fe9d..b2a523f6 100644 --- a/network/ble_unsupported.go +++ b/network/ble_unsupported.go @@ -5,10 +5,13 @@ package network import ( "encoding/json" "time" + + "github.com/evilsocket/islazy/data" ) type BLEDevice struct { LastSeen time.Time + Alias string } func NewBLEDevice() *BLEDevice { @@ -21,6 +24,7 @@ type BLEDevNewCallback func(dev *BLEDevice) type BLEDevLostCallback func(dev *BLEDevice) type BLE struct { + aliases *data.UnsortedKV devices map[string]*BLEDevice newCb BLEDevNewCallback lostCb BLEDevLostCallback @@ -30,8 +34,9 @@ type bleJSON struct { Devices []*BLEDevice `json:"devices"` } -func NewBLE(newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { +func NewBLE(aliases *data.UnsortedKV, newcb BLEDevNewCallback, lostcb BLEDevLostCallback) *BLE { return &BLE{ + aliases: aliases, devices: make(map[string]*BLEDevice), newCb: newcb, lostCb: lostcb, diff --git a/network/hid.go b/network/hid.go index bb8e3605..ed332381 100644 --- a/network/hid.go +++ b/network/hid.go @@ -4,6 +4,8 @@ import ( "encoding/json" "sync" "time" + + "github.com/evilsocket/islazy/data" ) type HIDDevNewCallback func(dev *HIDDevice) @@ -11,6 +13,7 @@ type HIDDevLostCallback func(dev *HIDDevice) type HID struct { sync.RWMutex + aliases *data.UnsortedKV devices map[string]*HIDDevice newCb HIDDevNewCallback lostCb HIDDevLostCallback @@ -20,9 +23,10 @@ type hidJSON struct { Devices []*HIDDevice `json:"devices"` } -func NewHID(newcb HIDDevNewCallback, lostcb HIDDevLostCallback) *HID { +func NewHID(aliases *data.UnsortedKV, newcb HIDDevNewCallback, lostcb HIDDevLostCallback) *HID { return &HID{ devices: make(map[string]*HIDDevice), + aliases: aliases, newCb: newcb, lostCb: lostcb, } @@ -52,14 +56,20 @@ func (b *HID) AddIfNew(address []byte, channel int, payload []byte) (bool, *HIDD defer b.Unlock() id := HIDAddress(address) + alias := b.aliases.GetOr(id, "") + if dev, found := b.devices[id]; found { dev.LastSeen = time.Now() dev.AddChannel(channel) dev.AddPayload(payload) + if alias != "" { + dev.Alias = alias + } return false, dev } newDev := NewHIDDevice(address, channel, payload) + newDev.Alias = alias b.devices[id] = newDev if b.newCb != nil { diff --git a/network/hid_device.go b/network/hid_device.go index 1562f9e1..16dac6f1 100644 --- a/network/hid_device.go +++ b/network/hid_device.go @@ -1,12 +1,15 @@ package network import ( + "encoding/hex" "encoding/json" "fmt" "sort" "strings" "sync" "time" + + "github.com/evilsocket/islazy/str" ) type HIDType int @@ -39,6 +42,7 @@ type HIDDevice struct { sync.Mutex LastSeen time.Time Type HIDType + Alias string Address string RawAddress []byte channels map[int]bool @@ -47,10 +51,13 @@ type HIDDevice struct { } type hidDeviceJSON struct { - LastSeen time.Time `json:"last_seen"` - Type string `json:"type"` - Address string `json:"address"` - Channels []string `json:"channels"` + LastSeen time.Time `json:"last_seen"` + Type string `json:"type"` + Address string `json:"address"` + Alias string `json:"alias"` + Channels []string `json:"channels"` + Payloads []string `json:"payloads"` + PayloadsSize uint64 `json:"payloads_size"` } func NormalizeHIDAddress(address string) string { @@ -90,12 +97,28 @@ func NewHIDDevice(address []byte, channel int, payload []byte) *HIDDevice { } func (dev *HIDDevice) MarshalJSON() ([]byte, error) { + dev.Lock() + defer dev.Unlock() + doc := hidDeviceJSON{ - LastSeen: dev.LastSeen, - Type: dev.Type.String(), - Address: dev.Address, - Channels: dev.ChannelsList(), + LastSeen: dev.LastSeen, + Type: dev.Type.String(), + Address: dev.Address, + Alias: dev.Alias, + Channels: dev.channelsListUnlocked(), + Payloads: make([]string, 0), + PayloadsSize: dev.payloadsSz, } + + // get the latest 50 payloads + for i := len(dev.payloads) - 1; i >= 0; i-- { + data := str.Trim(hex.Dump(dev.payloads[i])) + doc.Payloads = append([]string{data}, doc.Payloads...) + if len(doc.Payloads) == 50 { + break + } + } + return json.Marshal(doc) } @@ -106,10 +129,7 @@ func (dev *HIDDevice) AddChannel(ch int) { dev.channels[ch] = true } -func (dev *HIDDevice) ChannelsList() []string { - dev.Lock() - defer dev.Unlock() - +func (dev *HIDDevice) channelsListUnlocked() []string { chans := []string{} for ch := range dev.channels { chans = append(chans, fmt.Sprintf("%d", ch)) @@ -119,6 +139,11 @@ func (dev *HIDDevice) ChannelsList() []string { return chans } +func (dev *HIDDevice) ChannelsList() []string { + dev.Lock() + defer dev.Unlock() + return dev.channelsListUnlocked() +} func (dev *HIDDevice) Channels() string { return strings.Join(dev.ChannelsList(), ",") diff --git a/network/lan.go b/network/lan.go index 9363c0ba..0c2bef8f 100644 --- a/network/lan.go +++ b/network/lan.go @@ -2,23 +2,18 @@ package network import ( "encoding/json" - "fmt" "net" "strings" "sync" "github.com/evilsocket/islazy/data" - "github.com/evilsocket/islazy/fs" ) const LANDefaultttl = 10 -const LANAliasesFile = "~/bettercap.aliases" type EndpointNewCallback func(e *Endpoint) type EndpointLostCallback func(e *Endpoint) -var aliasesFileName, _ = fs.Expand(LANAliasesFile) - type LAN struct { sync.Mutex hosts map[string]*Endpoint @@ -34,12 +29,7 @@ type lanJSON struct { Hosts []*Endpoint `json:"hosts"` } -func NewLAN(iface, gateway *Endpoint, newcb EndpointNewCallback, lostcb EndpointLostCallback) *LAN { - aliases, err := data.NewUnsortedKV(aliasesFileName, data.FlushOnEdit) - if err != nil { - fmt.Printf("error loading %s: %s", aliasesFileName, err) - } - +func NewLAN(iface, gateway *Endpoint, aliases *data.UnsortedKV, newcb EndpointNewCallback, lostcb EndpointLostCallback) *LAN { return &LAN{ iface: iface, gateway: gateway, @@ -63,19 +53,6 @@ func (l *LAN) MarshalJSON() ([]byte, error) { return json.Marshal(doc) } -func (lan *LAN) SetAliasFor(mac, alias string) bool { - lan.Lock() - defer lan.Unlock() - - mac = NormalizeMac(mac) - lan.aliases.Set(mac, alias) - if e, found := lan.hosts[mac]; found { - e.Alias = alias - return true - } - return false -} - func (lan *LAN) Get(mac string) (*Endpoint, bool) { lan.Lock() defer lan.Unlock() diff --git a/network/net.go b/network/net.go index 37123537..47be68d8 100644 --- a/network/net.go +++ b/network/net.go @@ -286,7 +286,7 @@ func ActivateInterface(name string) error { func SetInterfaceTxPower(name string, txpower int) error { if core.HasBinary("iwconfig") { - if out, err := core.ExecSilent("iwconfig", []string{name, "txpower", fmt.Sprintf("%d", txpower)}); err != nil { + if out, err := core.Exec("iwconfig", []string{name, "txpower", fmt.Sprintf("%d", txpower)}); err != nil { return err } else if out != "" { return fmt.Errorf("unexpected output while setting txpower to %d for interface %s: %s", txpower, name, out) diff --git a/network/wifi.go b/network/wifi.go index 9d991652..db416c32 100644 --- a/network/wifi.go +++ b/network/wifi.go @@ -11,6 +11,7 @@ import ( "github.com/google/gopacket/layers" "github.com/google/gopacket/pcapgo" + "github.com/evilsocket/islazy/data" "github.com/evilsocket/islazy/fs" ) @@ -43,22 +44,24 @@ type APLostCallback func(ap *AccessPoint) type WiFi struct { sync.Mutex - aps map[string]*AccessPoint - iface *Endpoint - newCb APNewCallback - lostCb APLostCallback + aliases *data.UnsortedKV + aps map[string]*AccessPoint + iface *Endpoint + newCb APNewCallback + lostCb APLostCallback } type wifiJSON struct { AccessPoints []*AccessPoint `json:"aps"` } -func NewWiFi(iface *Endpoint, newcb APNewCallback, lostcb APLostCallback) *WiFi { +func NewWiFi(iface *Endpoint, aliases *data.UnsortedKV, newcb APNewCallback, lostcb APLostCallback) *WiFi { return &WiFi{ - aps: make(map[string]*AccessPoint), - iface: iface, - newCb: newcb, - lostCb: lostcb, + aps: make(map[string]*AccessPoint), + aliases: aliases, + iface: iface, + newCb: newcb, + lostCb: lostcb, } } @@ -134,6 +137,7 @@ func (w *WiFi) AddIfNew(ssid, mac string, frequency int, rssi int8) (*AccessPoin defer w.Unlock() mac = NormalizeMac(mac) + alias := w.aliases.GetOr(mac, "") if ap, found := w.aps[mac]; found { ap.LastSeen = time.Now() if rssi != 0 { @@ -143,10 +147,15 @@ func (w *WiFi) AddIfNew(ssid, mac string, frequency int, rssi int8) (*AccessPoin if !isBogusMacESSID(ssid) { ap.Hostname = ssid } + + if alias != "" { + ap.Alias = alias + } return ap, false } - newAp := NewAccessPoint(ssid, mac, frequency, rssi) + newAp := NewAccessPoint(ssid, mac, frequency, rssi, w.aliases) + newAp.Alias = alias w.aps[mac] = newAp if w.newCb != nil { diff --git a/network/wifi_ap.go b/network/wifi_ap.go index ec864916..58143cc0 100644 --- a/network/wifi_ap.go +++ b/network/wifi_ap.go @@ -12,6 +12,7 @@ type AccessPoint struct { *Station sync.Mutex + aliases *data.UnsortedKV clients map[string]*Station withKeyMaterial bool } @@ -22,9 +23,10 @@ type apJSON struct { Handshake bool `json:"handshake"` } -func NewAccessPoint(essid, bssid string, frequency int, rssi int8) *AccessPoint { +func NewAccessPoint(essid, bssid string, frequency int, rssi int8, aliases *data.UnsortedKV) *AccessPoint { return &AccessPoint{ Station: NewStation(essid, bssid, frequency, rssi), + aliases: aliases, clients: make(map[string]*Station), } } @@ -67,11 +69,12 @@ func (ap *AccessPoint) RemoveClient(mac string) { } } -func (ap *AccessPoint) AddClientIfNew(bssid string, frequency int, rssi int8, aliases *data.UnsortedKV) (*Station, bool) { +func (ap *AccessPoint) AddClientIfNew(bssid string, frequency int, rssi int8) (*Station, bool) { ap.Lock() defer ap.Unlock() bssid = NormalizeMac(bssid) + alias := ap.aliases.GetOr(bssid, "") if s, found := ap.clients[bssid]; found { // update @@ -79,17 +82,15 @@ func (ap *AccessPoint) AddClientIfNew(bssid string, frequency int, rssi int8, al s.RSSI = rssi s.LastSeen = time.Now() - if aliases != nil { - s.Alias = aliases.GetOr(bssid, "") + if alias != "" { + s.Alias = alias } return s, false } s := NewStation("", bssid, frequency, rssi) - if aliases != nil { - s.Alias = aliases.GetOr(bssid, "") - } + s.Alias = alias ap.clients[bssid] = s return s, true diff --git a/packets/queue.go b/packets/queue.go index 9546f738..1a7eeaf0 100644 --- a/packets/queue.go +++ b/packets/queue.go @@ -38,10 +38,11 @@ type PacketCallback func(pkt gopacket.Packet) type Queue struct { sync.RWMutex - Activities chan Activity + // keep on top because of https://github.com/bettercap/bettercap/issues/500 Stats Stats Protos sync.Map Traffic sync.Map + Activities chan Activity iface *network.Endpoint handle *pcap.Handle @@ -62,6 +63,7 @@ func NewQueue(iface *network.Endpoint) (q *Queue, err error) { q = &Queue{ Protos: sync.Map{}, Traffic: sync.Map{}, + Stats: Stats{}, Activities: make(chan Activity), writes: &sync.WaitGroup{}, @@ -165,6 +167,10 @@ func (q *Queue) trackActivity(eth *layers.Ethernet, ip4 *layers.IPv4, address ne } func (q *Queue) TrackPacket(size uint64) { + // https://github.com/bettercap/bettercap/issues/500 + if q == nil { + panic("track packet on nil queue!") + } atomic.AddUint64(&q.Stats.PktReceived, 1) atomic.AddUint64(&q.Stats.Received, size) } diff --git a/session.record b/session.record new file mode 100755 index 00000000..55ef2d96 Binary files /dev/null and b/session.record differ diff --git a/session/events.go b/session/events.go index c5b1c97f..d029b4cc 100644 --- a/session/events.go +++ b/session/events.go @@ -111,7 +111,16 @@ func (p *EventPool) Add(tag string, data interface{}) { // broadcast the event to every listener for _, l := range p.listeners { - l <- e + // do not block! + go func(ch *chan Event) { + // channel might be closed + defer func() { + if recover() != nil { + + } + }() + *ch <- e + }(&l) } } diff --git a/modules/events_stream/ignore_list.go b/session/events_ignore_list.go similarity index 59% rename from modules/events_stream/ignore_list.go rename to session/events_ignore_list.go index 036142e2..711a57cb 100644 --- a/modules/events_stream/ignore_list.go +++ b/session/events_ignore_list.go @@ -1,13 +1,12 @@ -package events_stream +package session import ( + "encoding/json" "errors" "fmt" "strings" "sync" - "github.com/bettercap/bettercap/session" - "github.com/evilsocket/islazy/str" ) @@ -15,24 +14,30 @@ var ( ErrEmptyExpression = errors.New("expression can not be empty") ) -type IgnoreFilter string +type filter string -func (f IgnoreFilter) Matches(s string) bool { +func (f filter) Matches(s string) bool { return string(f) == s || strings.HasPrefix(s, string(f)) } -type IgnoreList struct { +type EventsIgnoreList struct { sync.RWMutex - filters []IgnoreFilter + filters []filter } -func NewIgnoreList() *IgnoreList { - return &IgnoreList{ - filters: make([]IgnoreFilter, 0), +func NewEventsIgnoreList() *EventsIgnoreList { + return &EventsIgnoreList{ + filters: make([]filter, 0), } } -func (l *IgnoreList) checkExpression(expr string) (string, error) { +func (l *EventsIgnoreList) MarshalJSON() ([]byte, error) { + l.RLock() + defer l.RUnlock() + return json.Marshal(l.filters) +} + +func (l *EventsIgnoreList) checkExpression(expr string) (string, error) { expr = str.Trim(expr) if expr == "" { return "", ErrEmptyExpression @@ -41,7 +46,7 @@ func (l *IgnoreList) checkExpression(expr string) (string, error) { return expr, nil } -func (l *IgnoreList) Add(expr string) (err error) { +func (l *EventsIgnoreList) Add(expr string) (err error) { if expr, err = l.checkExpression(expr); err != nil { return err } @@ -57,12 +62,12 @@ func (l *IgnoreList) Add(expr string) (err error) { } // all good - l.filters = append(l.filters, IgnoreFilter(expr)) + l.filters = append(l.filters, filter(expr)) return nil } -func (l *IgnoreList) Remove(expr string) (err error) { +func (l *EventsIgnoreList) Remove(expr string) (err error) { if expr, err = l.checkExpression(expr); err != nil { return err } @@ -71,8 +76,8 @@ func (l *IgnoreList) Remove(expr string) (err error) { defer l.Unlock() // build a new list with everything that does not match - toRemove := IgnoreFilter(expr) - newList := make([]IgnoreFilter, 0) + toRemove := filter(expr) + newList := make([]filter, 0) for _, filter := range l.filters { if !toRemove.Matches(string(filter)) { newList = append(newList, filter) @@ -89,7 +94,13 @@ func (l *IgnoreList) Remove(expr string) (err error) { return nil } -func (l *IgnoreList) Ignored(e session.Event) bool { +func (l *EventsIgnoreList) Clear() { + l.Lock() + defer l.Unlock() + l.filters = make([]filter, 0) +} + +func (l *EventsIgnoreList) Ignored(e Event) bool { l.RLock() defer l.RUnlock() @@ -102,12 +113,12 @@ func (l *IgnoreList) Ignored(e session.Event) bool { return false } -func (l *IgnoreList) Empty() bool { +func (l *EventsIgnoreList) Empty() bool { l.RLock() defer l.RUnlock() return len(l.filters) == 0 } -func (l *IgnoreList) Filters() []IgnoreFilter { +func (l *EventsIgnoreList) Filters() []filter { return l.filters } diff --git a/session/module.go b/session/module.go index 5accba3d..f8ba5e75 100644 --- a/session/module.go +++ b/session/module.go @@ -1,6 +1,7 @@ package session import ( + "encoding/json" "fmt" "net" "strings" @@ -19,16 +20,47 @@ type Module interface { Handlers() []ModuleHandler Parameters() map[string]*ModuleParam + Extra() map[string]interface{} Running() bool Start() error Stop() error } +type ModuleList []Module + +type moduleJSON struct { + Name string `json:"name"` + Description string `json:"description"` + Author string `json:"author"` + Parameters map[string]*ModuleParam `json:"parameters"` + Handlers []ModuleHandler `json:"handlers"` + Running bool `json:"running"` + State map[string]interface{} `json:"state"` +} + +func (mm ModuleList) MarshalJSON() ([]byte, error) { + mods := []moduleJSON{} + for _, m := range mm { + mJSON := moduleJSON{ + Name: m.Name(), + Description: m.Description(), + Author: m.Author(), + Parameters: m.Parameters(), + Handlers: m.Handlers(), + Running: m.Running(), + State: m.Extra(), + } + mods = append(mods, mJSON) + } + return json.Marshal(mods) +} + type SessionModule struct { - Name string `json:"name"` - Session *Session `json:"-"` - Started bool `json:"started"` - StatusLock *sync.RWMutex `json:"-"` + Name string + Session *Session + Started bool + StatusLock *sync.RWMutex + State *sync.Map handlers []ModuleHandler params map[string]*ModuleParam @@ -45,6 +77,7 @@ func NewSessionModule(name string, s *Session) SessionModule { Session: s, Started: false, StatusLock: &sync.RWMutex{}, + State: &sync.Map{}, handlers: make([]ModuleHandler, 0), params: make(map[string]*ModuleParam), @@ -54,6 +87,28 @@ func NewSessionModule(name string, s *Session) SessionModule { return m } +func (m *SessionModule) Extra() map[string]interface{} { + extra := make(map[string]interface{}) + m.State.Range(func(k, v interface{}) bool { + extra[k.(string)] = v + return true + }) + return extra +} + +func (m *SessionModule) InitState(keys ...string) { + for _, key := range keys { + m.State.Store(key, nil) + } +} + +func (m *SessionModule) ResetState() { + m.State.Range(func(k, v interface{}) bool { + m.State.Store(k, nil) + return true + }) +} + func (m *SessionModule) Debug(format string, args ...interface{}) { m.Session.Events.Log(log.DEBUG, m.tag+format, args...) } @@ -176,9 +231,9 @@ func (m *SessionModule) Running() bool { func (m *SessionModule) SetRunning(running bool, cb func()) error { if running == m.Running() { if m.Started { - return ErrAlreadyStarted + return ErrAlreadyStarted(m.Name) } else { - return ErrAlreadyStopped + return ErrAlreadyStopped(m.Name) } } diff --git a/session/session.go b/session/session.go index aa5e96c0..c9aeedc8 100644 --- a/session/session.go +++ b/session/session.go @@ -1,7 +1,6 @@ package session import ( - "encoding/json" "errors" "fmt" "net" @@ -21,6 +20,7 @@ import ( "github.com/bettercap/bettercap/network" "github.com/bettercap/bettercap/packets" + "github.com/evilsocket/islazy/data" "github.com/evilsocket/islazy/fs" "github.com/evilsocket/islazy/log" "github.com/evilsocket/islazy/ops" @@ -35,43 +35,24 @@ const ( var ( I = (*Session)(nil) - ErrAlreadyStarted = errors.New("module is already running") - ErrAlreadyStopped = errors.New("module is not running") - ErrNotSupported = errors.New("this component is not supported on this OS") + ErrNotSupported = errors.New("this component is not supported on this OS") reCmdSpaceCleaner = regexp.MustCompile(`^([^\s]+)\s+(.+)$`) reEnvVarCapture = regexp.MustCompile(`{env\.([^}]+)}`) ) +func ErrAlreadyStarted(name string) error { + return fmt.Errorf("module %s is already running", name) +} + +func ErrAlreadyStopped(name string) error { + return fmt.Errorf("module %s is not running", name) +} + type UnknownCommandCallback func(cmd string) bool -type ModuleList []Module - -type JSONModule struct { - Name string `json:"name"` - Description string `json:"description"` - Author string `json:"author"` - Parameters map[string]*ModuleParam `json:"parameters"` - Handlers []ModuleHandler `json:"handlers"` - Running bool `json:"running"` -} - -func (mm ModuleList) MarshalJSON() ([]byte, error) { - mods := []JSONModule{} - for _, m := range mm { - mods = append(mods, JSONModule{ - Name: m.Name(), - Description: m.Description(), - Author: m.Author(), - Parameters: m.Parameters(), - Handlers: m.Handlers(), - Running: m.Running(), - }) - } - return json.Marshal(mods) -} - type GPS struct { + Updated time.Time Latitude float64 // Latitude. Longitude float64 // Longitude. FixQuality string // Quality of fix. @@ -81,27 +62,33 @@ type GPS struct { Separation float64 // Geoidal separation } -type Session struct { - Options core.Options `json:"options"` - Interface *network.Endpoint `json:"interface"` - Gateway *network.Endpoint `json:"gateway"` - Env *Environment `json:"env"` - Lan *network.LAN `json:"lan"` - WiFi *network.WiFi `json:"wifi"` - BLE *network.BLE `json:"ble"` - HID *network.HID `json:"hid"` - Queue *packets.Queue `json:"packets"` - StartedAt time.Time `json:"started_at"` - Active bool `json:"active"` - GPS GPS `json:"gps"` - Modules ModuleList `json:"modules"` +const AliasesFile = "~/bettercap.aliases" - Input *readline.Instance `json:"-"` - Prompt Prompt `json:"-"` - CoreHandlers []CommandHandler `json:"-"` - Events *EventPool `json:"-"` - UnkCmdCallback UnknownCommandCallback `json:"-"` - Firewall firewall.FirewallManager `json:"-"` +var aliasesFileName, _ = fs.Expand(AliasesFile) + +type Session struct { + Options core.Options + Interface *network.Endpoint + Gateway *network.Endpoint + Env *Environment + Lan *network.LAN + WiFi *network.WiFi + BLE *network.BLE + HID *network.HID + Queue *packets.Queue + StartedAt time.Time + Active bool + GPS GPS + Modules ModuleList + Aliases *data.UnsortedKV + + Input *readline.Instance + Prompt Prompt + CoreHandlers []CommandHandler + Events *EventPool + EventsIgnoreList *EventsIgnoreList + UnkCmdCallback UnknownCommandCallback + Firewall firewall.FirewallManager } func New() (*Session, error) { @@ -122,10 +109,11 @@ func New() (*Session, error) { Active: false, Queue: nil, - CoreHandlers: make([]CommandHandler, 0), - Modules: make([]Module, 0), - Events: nil, - UnkCmdCallback: nil, + CoreHandlers: make([]CommandHandler, 0), + Modules: make([]Module, 0), + Events: nil, + EventsIgnoreList: NewEventsIgnoreList(), + UnkCmdCallback: nil, } if *s.Options.CpuProfile != "" { @@ -140,6 +128,10 @@ func New() (*Session, error) { return nil, err } + if s.Aliases, err = data.NewUnsortedKV(aliasesFileName, data.FlushOnEdit); err != nil { + return nil, err + } + s.Events = NewEventPool(*s.Options.Debug, *s.Options.Silent) s.registerCoreHandlers() @@ -260,25 +252,25 @@ func (s *Session) Start() error { s.Firewall = firewall.Make(s.Interface) - s.HID = network.NewHID(func(dev *network.HIDDevice) { + s.HID = network.NewHID(s.Aliases, func(dev *network.HIDDevice) { s.Events.Add("hid.device.new", dev) }, func(dev *network.HIDDevice) { s.Events.Add("hid.device.lost", dev) }) - s.BLE = network.NewBLE(func(dev *network.BLEDevice) { + s.BLE = network.NewBLE(s.Aliases, func(dev *network.BLEDevice) { s.Events.Add("ble.device.new", dev) }, func(dev *network.BLEDevice) { s.Events.Add("ble.device.lost", dev) }) - s.WiFi = network.NewWiFi(s.Interface, func(ap *network.AccessPoint) { + s.WiFi = network.NewWiFi(s.Interface, s.Aliases, func(ap *network.AccessPoint) { s.Events.Add("wifi.ap.new", ap) }, func(ap *network.AccessPoint) { s.Events.Add("wifi.ap.lost", ap) }) - s.Lan = network.NewLAN(s.Interface, s.Gateway, func(e *network.Endpoint) { + s.Lan = network.NewLAN(s.Interface, s.Gateway, s.Aliases, func(e *network.Endpoint) { s.Events.Add("endpoint.new", e) }, func(e *network.Endpoint) { s.Events.Add("endpoint.lost", e) diff --git a/session/session_core_handlers.go b/session/session_core_handlers.go index 1a8fa83e..51f40be0 100644 --- a/session/session_core_handlers.go +++ b/session/session_core_handlers.go @@ -4,7 +4,9 @@ import ( "bufio" "fmt" "os" + "os/exec" "path/filepath" + "runtime" "sort" "strconv" "strings" @@ -249,13 +251,14 @@ func (s *Session) readHandler(args []string, sess *Session) error { } func (s *Session) clsHandler(args []string, sess *Session) error { - // fixes a weird bug which causes the screen not to be fully - // cleared if a "clear; net.show" commands chain is executed - // in the interactive session. - for i := 0; i < 180; i++ { - fmt.Println() + cmd := "clear" + if runtime.GOOS == "windows" { + cmd = "cls" } - readline.ClearScreen(s.Input.Stdout()) + + c := exec.Command(cmd) + c.Stdout = os.Stdout + c.Run() return nil } @@ -271,10 +274,55 @@ func (s *Session) shHandler(args []string, sess *Session) error { return err } +func normalizeMac(mac string) string { + var parts []string + if strings.ContainsRune(mac, '-') { + parts = strings.Split(mac, "-") + } else { + parts = strings.Split(mac, ":") + } + + for i, p := range parts { + if len(p) < 2 { + parts[i] = "0" + p + } + } + return strings.ToLower(strings.Join(parts, ":")) +} + +func (s *Session) propagateAlias(mac, alias string) { + mac = normalizeMac(mac) + + s.Aliases.Set(mac, alias) + + if dev, found := s.BLE.Get(mac); found { + dev.Alias = alias + } + + if dev, found := s.HID.Get(mac); found { + dev.Alias = alias + } + + if ap, found := s.WiFi.Get(mac); found { + ap.Alias = alias + } + + if sta, found := s.WiFi.GetClient(mac); found { + sta.Alias = alias + } + + if host, found := s.Lan.Get(mac); found { + host.Alias = alias + } +} + func (s *Session) aliasHandler(args []string, sess *Session) error { mac := args[0] alias := str.Trim(args[1]) - s.Lan.SetAliasFor(mac, alias) + if alias == "\"\"" || alias == "''" { + alias = "" + } + s.propagateAlias(mac, alias) return nil } @@ -380,7 +428,7 @@ func (s *Session) registerCoreHandlers() { readline.PcItem("!")) s.addHandler(NewCommandHandler("alias MAC NAME", - "^alias\\s+([a-fA-F0-9:]{17})\\s*(.*)", + "^alias\\s+([a-fA-F0-9:]{14,17})\\s*(.*)", "Assign an alias to a given endpoint given its MAC address.", s.aliasHandler), readline.PcItem("alias", readline.PcItemDynamic(func(prefix string) []string { diff --git a/session/session_json.go b/session/session_json.go new file mode 100644 index 00000000..1f91b3ea --- /dev/null +++ b/session/session_json.go @@ -0,0 +1,122 @@ +package session + +import ( + "encoding/json" + "net" + "runtime" + "time" + + "github.com/bettercap/bettercap/caplets" + "github.com/bettercap/bettercap/core" + "github.com/bettercap/bettercap/network" + "github.com/bettercap/bettercap/packets" +) + +var flagNames = []string{ + "UP", + "BROADCAST", + "LOOPBACK", + "POINT2POINT", + "MULTICAST", +} + +type addrJSON struct { + Address string `json:"address"` + Type string `json:"type"` +} + +type ifaceJSON struct { + Index int `json:"index"` + MTU int `json:"mtu"` + Name string `json:"name"` + MAC string `json:"mac"` + Vendor string `json:"vendor"` + Flags []string `json:"flags"` + Addresses []addrJSON `json:"addresses"` +} + +type SessionJSON struct { + Version string `json:"version"` + OS string `json:"os"` + Arch string `json:"arch"` + GoVersion string `json:"goversion"` + Interfaces []ifaceJSON `json:"interfaces"` + Options core.Options `json:"options"` + Interface *network.Endpoint `json:"interface"` + Gateway *network.Endpoint `json:"gateway"` + Env *Environment `json:"env"` + Lan *network.LAN `json:"lan"` + WiFi *network.WiFi `json:"wifi"` + BLE *network.BLE `json:"ble"` + HID *network.HID `json:"hid"` + Queue *packets.Queue `json:"packets"` + StartedAt time.Time `json:"started_at"` + PolledAt time.Time `json:"polled_at"` + Active bool `json:"active"` + GPS GPS `json:"gps"` + Modules ModuleList `json:"modules"` + Caplets []*caplets.Caplet `json:"caplets"` +} + +func (s *Session) MarshalJSON() ([]byte, error) { + doc := SessionJSON{ + Version: core.Version, + OS: runtime.GOOS, + Arch: runtime.GOARCH, + GoVersion: runtime.Version(), + Interfaces: make([]ifaceJSON, 0), + Options: s.Options, + Interface: s.Interface, + Gateway: s.Gateway, + Env: s.Env, + Lan: s.Lan, + WiFi: s.WiFi, + BLE: s.BLE, + HID: s.HID, + Queue: s.Queue, + StartedAt: s.StartedAt, + PolledAt: time.Now(), + Active: s.Active, + GPS: s.GPS, + Modules: s.Modules, + Caplets: caplets.List(), + } + + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + for _, iface := range ifaces { + mac := network.NormalizeMac(iface.HardwareAddr.String()) + + ij := ifaceJSON{ + Index: iface.Index, + MTU: iface.MTU, + Name: iface.Name, + MAC: mac, + Vendor: network.ManufLookup(mac), + Flags: make([]string, 0), + Addresses: make([]addrJSON, 0), + } + + if addrs, err := iface.Addrs(); err == nil { + for _, addr := range addrs { + ij.Addresses = append(ij.Addresses, addrJSON{ + Address: addr.String(), + Type: addr.Network(), + }) + } + } + + for bit, name := range flagNames { + if iface.Flags&(1< 0 { u = append(u, UUID{d[:w]}) d = d[w:] diff --git a/vendor/github.com/evilsocket/islazy/zip/unzip.go b/vendor/github.com/evilsocket/islazy/zip/unzip.go index 79ec84b3..dc6edffb 100644 --- a/vendor/github.com/evilsocket/islazy/zip/unzip.go +++ b/vendor/github.com/evilsocket/islazy/zip/unzip.go @@ -13,6 +13,8 @@ import ( // within the zip file (parameter 1) to an output directory (parameter 2). // Credits to https://golangcode.com/unzip-files-in-go/ func Unzip(src string, dest string) ([]string, error) { + var outFile *os.File + var zipFile io.ReadCloser var filenames []string r, err := zip.OpenReader(src) @@ -21,33 +23,55 @@ func Unzip(src string, dest string) ([]string, error) { } defer r.Close() + clean := func() { + if outFile != nil { + outFile.Close() + outFile = nil + } + + if zipFile != nil { + zipFile.Close() + zipFile = nil + } + } + for _, f := range r.File { - rc, err := f.Open() + zipFile, err = f.Open() if err != nil { return filenames, err } - defer rc.Close() // Store filename/path for returning and using later on fpath := filepath.Join(dest, f.Name) // Check for ZipSlip. More Info: https://snyk.io/research/zip-slip-vulnerability#go if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + clean() return filenames, fmt.Errorf("%s: illegal file path", fpath) } filenames = append(filenames, fpath) if f.FileInfo().IsDir() { os.MkdirAll(fpath, os.ModePerm) - } else if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + clean() + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil { + clean() return filenames, err - } else if outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()); err != nil { + } + + outFile, err = os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + clean() + return filenames, err + } + + _, err = io.Copy(outFile, zipFile) + clean() + if err != nil { return filenames, err - } else { - defer outFile.Close() - if _, err = io.Copy(outFile, rc); err != nil { - return filenames, err - } } } diff --git a/vendor/github.com/kr/binarydist/.gitignore b/vendor/github.com/kr/binarydist/.gitignore new file mode 100644 index 00000000..653f1601 --- /dev/null +++ b/vendor/github.com/kr/binarydist/.gitignore @@ -0,0 +1 @@ +test.* diff --git a/vendor/github.com/kr/binarydist/License b/vendor/github.com/kr/binarydist/License new file mode 100644 index 00000000..183c3898 --- /dev/null +++ b/vendor/github.com/kr/binarydist/License @@ -0,0 +1,22 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/kr/binarydist/Readme.md b/vendor/github.com/kr/binarydist/Readme.md new file mode 100644 index 00000000..dadc3683 --- /dev/null +++ b/vendor/github.com/kr/binarydist/Readme.md @@ -0,0 +1,7 @@ +# binarydist + +Package binarydist implements binary diff and patch as described on +. It reads and writes files +compatible with the tools there. + +Documentation at . diff --git a/vendor/github.com/kr/binarydist/bzip2.go b/vendor/github.com/kr/binarydist/bzip2.go new file mode 100644 index 00000000..a2516b81 --- /dev/null +++ b/vendor/github.com/kr/binarydist/bzip2.go @@ -0,0 +1,40 @@ +package binarydist + +import ( + "io" + "os/exec" +) + +type bzip2Writer struct { + c *exec.Cmd + w io.WriteCloser +} + +func (w bzip2Writer) Write(b []byte) (int, error) { + return w.w.Write(b) +} + +func (w bzip2Writer) Close() error { + if err := w.w.Close(); err != nil { + return err + } + return w.c.Wait() +} + +// Package compress/bzip2 implements only decompression, +// so we'll fake it by running bzip2 in another process. +func newBzip2Writer(w io.Writer) (wc io.WriteCloser, err error) { + var bw bzip2Writer + bw.c = exec.Command("bzip2", "-c") + bw.c.Stdout = w + + if bw.w, err = bw.c.StdinPipe(); err != nil { + return nil, err + } + + if err = bw.c.Start(); err != nil { + return nil, err + } + + return bw, nil +} diff --git a/vendor/github.com/kr/binarydist/diff.go b/vendor/github.com/kr/binarydist/diff.go new file mode 100644 index 00000000..1d2d951b --- /dev/null +++ b/vendor/github.com/kr/binarydist/diff.go @@ -0,0 +1,408 @@ +package binarydist + +import ( + "bytes" + "encoding/binary" + "io" + "io/ioutil" +) + +func swap(a []int, i, j int) { a[i], a[j] = a[j], a[i] } + +func split(I, V []int, start, length, h int) { + var i, j, k, x, jj, kk int + + if length < 16 { + for k = start; k < start+length; k += j { + j = 1 + x = V[I[k]+h] + for i = 1; k+i < start+length; i++ { + if V[I[k+i]+h] < x { + x = V[I[k+i]+h] + j = 0 + } + if V[I[k+i]+h] == x { + swap(I, k+i, k+j) + j++ + } + } + for i = 0; i < j; i++ { + V[I[k+i]] = k + j - 1 + } + if j == 1 { + I[k] = -1 + } + } + return + } + + x = V[I[start+length/2]+h] + jj = 0 + kk = 0 + for i = start; i < start+length; i++ { + if V[I[i]+h] < x { + jj++ + } + if V[I[i]+h] == x { + kk++ + } + } + jj += start + kk += jj + + i = start + j = 0 + k = 0 + for i < jj { + if V[I[i]+h] < x { + i++ + } else if V[I[i]+h] == x { + swap(I, i, jj+j) + j++ + } else { + swap(I, i, kk+k) + k++ + } + } + + for jj+j < kk { + if V[I[jj+j]+h] == x { + j++ + } else { + swap(I, jj+j, kk+k) + k++ + } + } + + if jj > start { + split(I, V, start, jj-start, h) + } + + for i = 0; i < kk-jj; i++ { + V[I[jj+i]] = kk - 1 + } + if jj == kk-1 { + I[jj] = -1 + } + + if start+length > kk { + split(I, V, kk, start+length-kk, h) + } +} + +func qsufsort(obuf []byte) []int { + var buckets [256]int + var i, h int + I := make([]int, len(obuf)+1) + V := make([]int, len(obuf)+1) + + for _, c := range obuf { + buckets[c]++ + } + for i = 1; i < 256; i++ { + buckets[i] += buckets[i-1] + } + copy(buckets[1:], buckets[:]) + buckets[0] = 0 + + for i, c := range obuf { + buckets[c]++ + I[buckets[c]] = i + } + + I[0] = len(obuf) + for i, c := range obuf { + V[i] = buckets[c] + } + + V[len(obuf)] = 0 + for i = 1; i < 256; i++ { + if buckets[i] == buckets[i-1]+1 { + I[buckets[i]] = -1 + } + } + I[0] = -1 + + for h = 1; I[0] != -(len(obuf) + 1); h += h { + var n int + for i = 0; i < len(obuf)+1; { + if I[i] < 0 { + n -= I[i] + i -= I[i] + } else { + if n != 0 { + I[i-n] = -n + } + n = V[I[i]] + 1 - i + split(I, V, i, n, h) + i += n + n = 0 + } + } + if n != 0 { + I[i-n] = -n + } + } + + for i = 0; i < len(obuf)+1; i++ { + I[V[i]] = i + } + return I +} + +func matchlen(a, b []byte) (i int) { + for i < len(a) && i < len(b) && a[i] == b[i] { + i++ + } + return i +} + +func search(I []int, obuf, nbuf []byte, st, en int) (pos, n int) { + if en-st < 2 { + x := matchlen(obuf[I[st]:], nbuf) + y := matchlen(obuf[I[en]:], nbuf) + + if x > y { + return I[st], x + } else { + return I[en], y + } + } + + x := st + (en-st)/2 + if bytes.Compare(obuf[I[x]:], nbuf) < 0 { + return search(I, obuf, nbuf, x, en) + } else { + return search(I, obuf, nbuf, st, x) + } + panic("unreached") +} + +// Diff computes the difference between old and new, according to the bsdiff +// algorithm, and writes the result to patch. +func Diff(old, new io.Reader, patch io.Writer) error { + obuf, err := ioutil.ReadAll(old) + if err != nil { + return err + } + + nbuf, err := ioutil.ReadAll(new) + if err != nil { + return err + } + + pbuf, err := diffBytes(obuf, nbuf) + if err != nil { + return err + } + + _, err = patch.Write(pbuf) + return err +} + +func diffBytes(obuf, nbuf []byte) ([]byte, error) { + var patch seekBuffer + err := diff(obuf, nbuf, &patch) + if err != nil { + return nil, err + } + return patch.buf, nil +} + +func diff(obuf, nbuf []byte, patch io.WriteSeeker) error { + var lenf int + I := qsufsort(obuf) + db := make([]byte, len(nbuf)) + eb := make([]byte, len(nbuf)) + var dblen, eblen int + + var hdr header + hdr.Magic = magic + hdr.NewSize = int64(len(nbuf)) + err := binary.Write(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + + // Compute the differences, writing ctrl as we go + pfbz2, err := newBzip2Writer(patch) + if err != nil { + return err + } + var scan, pos, length int + var lastscan, lastpos, lastoffset int + for scan < len(nbuf) { + var oldscore int + scan += length + for scsc := scan; scan < len(nbuf); scan++ { + pos, length = search(I, obuf, nbuf[scan:], 0, len(obuf)) + + for ; scsc < scan+length; scsc++ { + if scsc+lastoffset < len(obuf) && + obuf[scsc+lastoffset] == nbuf[scsc] { + oldscore++ + } + } + + if (length == oldscore && length != 0) || length > oldscore+8 { + break + } + + if scan+lastoffset < len(obuf) && obuf[scan+lastoffset] == nbuf[scan] { + oldscore-- + } + } + + if length != oldscore || scan == len(nbuf) { + var s, Sf int + lenf = 0 + for i := 0; lastscan+i < scan && lastpos+i < len(obuf); { + if obuf[lastpos+i] == nbuf[lastscan+i] { + s++ + } + i++ + if s*2-i > Sf*2-lenf { + Sf = s + lenf = i + } + } + + lenb := 0 + if scan < len(nbuf) { + var s, Sb int + for i := 1; (scan >= lastscan+i) && (pos >= i); i++ { + if obuf[pos-i] == nbuf[scan-i] { + s++ + } + if s*2-i > Sb*2-lenb { + Sb = s + lenb = i + } + } + } + + if lastscan+lenf > scan-lenb { + overlap := (lastscan + lenf) - (scan - lenb) + s := 0 + Ss := 0 + lens := 0 + for i := 0; i < overlap; i++ { + if nbuf[lastscan+lenf-overlap+i] == obuf[lastpos+lenf-overlap+i] { + s++ + } + if nbuf[scan-lenb+i] == obuf[pos-lenb+i] { + s-- + } + if s > Ss { + Ss = s + lens = i + 1 + } + } + + lenf += lens - overlap + lenb -= lens + } + + for i := 0; i < lenf; i++ { + db[dblen+i] = nbuf[lastscan+i] - obuf[lastpos+i] + } + for i := 0; i < (scan-lenb)-(lastscan+lenf); i++ { + eb[eblen+i] = nbuf[lastscan+lenf+i] + } + + dblen += lenf + eblen += (scan - lenb) - (lastscan + lenf) + + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(lenf)) + if err != nil { + pfbz2.Close() + return err + } + + val := (scan - lenb) - (lastscan + lenf) + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val)) + if err != nil { + pfbz2.Close() + return err + } + + val = (pos - lenb) - (lastpos + lenf) + err = binary.Write(pfbz2, signMagLittleEndian{}, int64(val)) + if err != nil { + pfbz2.Close() + return err + } + + lastscan = scan - lenb + lastpos = pos - lenb + lastoffset = pos - scan + } + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Compute size of compressed ctrl data + l64, err := patch.Seek(0, 1) + if err != nil { + return err + } + hdr.CtrlLen = int64(l64 - 32) + + // Write compressed diff data + pfbz2, err = newBzip2Writer(patch) + if err != nil { + return err + } + n, err := pfbz2.Write(db[:dblen]) + if err != nil { + pfbz2.Close() + return err + } + if n != dblen { + pfbz2.Close() + return io.ErrShortWrite + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Compute size of compressed diff data + n64, err := patch.Seek(0, 1) + if err != nil { + return err + } + hdr.DiffLen = n64 - l64 + + // Write compressed extra data + pfbz2, err = newBzip2Writer(patch) + if err != nil { + return err + } + n, err = pfbz2.Write(eb[:eblen]) + if err != nil { + pfbz2.Close() + return err + } + if n != eblen { + pfbz2.Close() + return io.ErrShortWrite + } + err = pfbz2.Close() + if err != nil { + return err + } + + // Seek to the beginning, write the header, and close the file + _, err = patch.Seek(0, 0) + if err != nil { + return err + } + err = binary.Write(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + return nil +} diff --git a/vendor/github.com/kr/binarydist/doc.go b/vendor/github.com/kr/binarydist/doc.go new file mode 100644 index 00000000..3c92d875 --- /dev/null +++ b/vendor/github.com/kr/binarydist/doc.go @@ -0,0 +1,24 @@ +// Package binarydist implements binary diff and patch as described on +// http://www.daemonology.net/bsdiff/. It reads and writes files +// compatible with the tools there. +package binarydist + +var magic = [8]byte{'B', 'S', 'D', 'I', 'F', 'F', '4', '0'} + +// File format: +// 0 8 "BSDIFF40" +// 8 8 X +// 16 8 Y +// 24 8 sizeof(newfile) +// 32 X bzip2(control block) +// 32+X Y bzip2(diff block) +// 32+X+Y ??? bzip2(extra block) +// with control block a set of triples (x,y,z) meaning "add x bytes +// from oldfile to x bytes from the diff block; copy y bytes from the +// extra block; seek forwards in oldfile by z bytes". +type header struct { + Magic [8]byte + CtrlLen int64 + DiffLen int64 + NewSize int64 +} diff --git a/vendor/github.com/kr/binarydist/encoding.go b/vendor/github.com/kr/binarydist/encoding.go new file mode 100644 index 00000000..75ba5856 --- /dev/null +++ b/vendor/github.com/kr/binarydist/encoding.go @@ -0,0 +1,53 @@ +package binarydist + +// SignMagLittleEndian is the numeric encoding used by the bsdiff tools. +// It implements binary.ByteOrder using a sign-magnitude format +// and little-endian byte order. Only methods Uint64 and String +// have been written; the rest panic. +type signMagLittleEndian struct{} + +func (signMagLittleEndian) Uint16(b []byte) uint16 { panic("unimplemented") } + +func (signMagLittleEndian) PutUint16(b []byte, v uint16) { panic("unimplemented") } + +func (signMagLittleEndian) Uint32(b []byte) uint32 { panic("unimplemented") } + +func (signMagLittleEndian) PutUint32(b []byte, v uint32) { panic("unimplemented") } + +func (signMagLittleEndian) Uint64(b []byte) uint64 { + y := int64(b[0]) | + int64(b[1])<<8 | + int64(b[2])<<16 | + int64(b[3])<<24 | + int64(b[4])<<32 | + int64(b[5])<<40 | + int64(b[6])<<48 | + int64(b[7]&0x7f)<<56 + + if b[7]&0x80 != 0 { + y = -y + } + return uint64(y) +} + +func (signMagLittleEndian) PutUint64(b []byte, v uint64) { + x := int64(v) + neg := x < 0 + if neg { + x = -x + } + + b[0] = byte(x) + b[1] = byte(x >> 8) + b[2] = byte(x >> 16) + b[3] = byte(x >> 24) + b[4] = byte(x >> 32) + b[5] = byte(x >> 40) + b[6] = byte(x >> 48) + b[7] = byte(x >> 56) + if neg { + b[7] |= 0x80 + } +} + +func (signMagLittleEndian) String() string { return "signMagLittleEndian" } diff --git a/vendor/github.com/kr/binarydist/go.mod b/vendor/github.com/kr/binarydist/go.mod new file mode 100644 index 00000000..ecdfe3ea --- /dev/null +++ b/vendor/github.com/kr/binarydist/go.mod @@ -0,0 +1 @@ +module "github.com/kr/binarydist" diff --git a/vendor/github.com/kr/binarydist/patch.go b/vendor/github.com/kr/binarydist/patch.go new file mode 100644 index 00000000..eb032257 --- /dev/null +++ b/vendor/github.com/kr/binarydist/patch.go @@ -0,0 +1,109 @@ +package binarydist + +import ( + "bytes" + "compress/bzip2" + "encoding/binary" + "errors" + "io" + "io/ioutil" +) + +var ErrCorrupt = errors.New("corrupt patch") + +// Patch applies patch to old, according to the bspatch algorithm, +// and writes the result to new. +func Patch(old io.Reader, new io.Writer, patch io.Reader) error { + var hdr header + err := binary.Read(patch, signMagLittleEndian{}, &hdr) + if err != nil { + return err + } + if hdr.Magic != magic { + return ErrCorrupt + } + if hdr.CtrlLen < 0 || hdr.DiffLen < 0 || hdr.NewSize < 0 { + return ErrCorrupt + } + + ctrlbuf := make([]byte, hdr.CtrlLen) + _, err = io.ReadFull(patch, ctrlbuf) + if err != nil { + return err + } + cpfbz2 := bzip2.NewReader(bytes.NewReader(ctrlbuf)) + + diffbuf := make([]byte, hdr.DiffLen) + _, err = io.ReadFull(patch, diffbuf) + if err != nil { + return err + } + dpfbz2 := bzip2.NewReader(bytes.NewReader(diffbuf)) + + // The entire rest of the file is the extra block. + epfbz2 := bzip2.NewReader(patch) + + obuf, err := ioutil.ReadAll(old) + if err != nil { + return err + } + + nbuf := make([]byte, hdr.NewSize) + + var oldpos, newpos int64 + for newpos < hdr.NewSize { + var ctrl struct{ Add, Copy, Seek int64 } + err = binary.Read(cpfbz2, signMagLittleEndian{}, &ctrl) + if err != nil { + return err + } + + // Sanity-check + if newpos+ctrl.Add > hdr.NewSize { + return ErrCorrupt + } + + // Read diff string + _, err = io.ReadFull(dpfbz2, nbuf[newpos:newpos+ctrl.Add]) + if err != nil { + return ErrCorrupt + } + + // Add old data to diff string + for i := int64(0); i < ctrl.Add; i++ { + if oldpos+i >= 0 && oldpos+i < int64(len(obuf)) { + nbuf[newpos+i] += obuf[oldpos+i] + } + } + + // Adjust pointers + newpos += ctrl.Add + oldpos += ctrl.Add + + // Sanity-check + if newpos+ctrl.Copy > hdr.NewSize { + return ErrCorrupt + } + + // Read extra string + _, err = io.ReadFull(epfbz2, nbuf[newpos:newpos+ctrl.Copy]) + if err != nil { + return ErrCorrupt + } + + // Adjust pointers + newpos += ctrl.Copy + oldpos += ctrl.Seek + } + + // Write the new file + for len(nbuf) > 0 { + n, err := new.Write(nbuf) + if err != nil { + return err + } + nbuf = nbuf[n:] + } + + return nil +} diff --git a/vendor/github.com/kr/binarydist/seek.go b/vendor/github.com/kr/binarydist/seek.go new file mode 100644 index 00000000..96c03461 --- /dev/null +++ b/vendor/github.com/kr/binarydist/seek.go @@ -0,0 +1,43 @@ +package binarydist + +import ( + "errors" +) + +type seekBuffer struct { + buf []byte + pos int +} + +func (b *seekBuffer) Write(p []byte) (n int, err error) { + n = copy(b.buf[b.pos:], p) + if n == len(p) { + b.pos += n + return n, nil + } + b.buf = append(b.buf, p[n:]...) + b.pos += len(p) + return len(p), nil +} + +func (b *seekBuffer) Seek(offset int64, whence int) (ret int64, err error) { + var abs int64 + switch whence { + case 0: + abs = offset + case 1: + abs = int64(b.pos) + offset + case 2: + abs = int64(len(b.buf)) + offset + default: + return 0, errors.New("binarydist: invalid whence") + } + if abs < 0 { + return 0, errors.New("binarydist: negative position") + } + if abs >= 1<<31 { + return 0, errors.New("binarydist: position out of range") + } + b.pos = int(abs) + return abs, nil +}