diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..600d2d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..af31629 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Christopher Schmitt, FireEye + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..35ace57 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# gocat + +gocat is a cgo library for interacting with libhashcat. gocat enables you to create purpose-built password cracking tools that leverage the capabilities of [hashcat](https://hashcat.net/hashcat/). + +## Installation + +gocat requires hashcat [v5.1.0](https://github.com/hashcat/hashcat/releases) or higher to be compiled as a shared library. This can be accomplished by modifying hashcat's `src/Makefile` and setting `SHARED` to `1` + +Installing the Go Library: + + go get github.com/fireeye/gocat + +## Known Issues + +* Lack of Windows Support: This won't work on windows as I haven't figured out how to build hashcat on windows +* Memory Leaks: hashcat has several (small) memory leaks that could cause increase of process memory over time + +## Contributing + +Contributions are welcome via pull requests provided they meet the following criteria: + +1. One feature or bug fix per PR +1. Code should be properly formatted (using go fmt) +1. Tests coverage should rarely decrease. All new features should have proper coverage diff --git a/ctypes.go b/ctypes.go new file mode 100644 index 0000000..cbef5a4 --- /dev/null +++ b/ctypes.go @@ -0,0 +1,20 @@ +package gocat + +// #include +import "C" +import "unsafe" + +var cChar *C.char + +// convertArgsToC converts go strings into a **C.char. +// The results of this MUST be free'd +func convertArgsToC(args ...string) (C.int, **C.char) { + ptrSize := unsafe.Sizeof(cChar) + ptr := C.malloc(C.size_t(len(args)) * C.size_t(ptrSize)) + + for i := 0; i < len(args); i++ { + element := (**C.char)(unsafe.Pointer(uintptr(ptr) + uintptr(i)*ptrSize)) + *element = C.CString(string(args[i])) + } + return C.int(len(args)), (**C.char)(ptr) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..cf830c1 --- /dev/null +++ b/doc.go @@ -0,0 +1,7 @@ +/* +Package gocat is a cgo interface around libhashcat. It's main purpose is to abstract hashcat and allow you to build tools in Go that leverage the hashcat engine. + +gocat should be used with libhashcat v5.1.0 as previous versions have known memory leaks and could affect long running processes running multiple cracking tasks. + +*/ +package gocat diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..563c0ac --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/fireeye/gocat + +go 1.12 + +require github.com/stretchr/testify v1.3.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4347755 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/gocat.go b/gocat.go new file mode 100644 index 0000000..7e51ac4 --- /dev/null +++ b/gocat.go @@ -0,0 +1,307 @@ +package gocat + +/* +#cgo CFLAGS: -I/usr/local/include/hashcat -std=c99 -Wall -O0 -g +#cgo linux CFLAGS: -D_GNU_SOURCE +#cgo linux LDFLAGS: -L/usr/local/lib -lhashcat +#cgo darwin LDFLAGS: -L/usr/local/lib -lhashcat.5.1.0 + +#include "wrapper.h" +*/ +import "C" +import ( + "errors" + "fmt" + "os" + "sync" + "time" + "unsafe" + + "github.com/fireeye/gocat/hcargp" +) + +var ( + // CompileTime should be set using -ldflags "-X" during compilation. This value is passed into hashcat_session_init + CompileTime = time.Now().UTC().Unix() + // ErrUnableToStopAtCheckpoint is raised whenever hashcat is unable to stop at next checkpoint. This is caused when + // the device status != STATUS_RUNNING or --restore-disable is set + ErrUnableToStopAtCheckpoint = errors.New("gocat: unable to stop at next checkpoint") + // ErrUnableToStop is raised whenever we were unable to stop a hashcat session. At this time, hashcat's hashcat_session_quit + // always returns success so you'll most likely never see this. + ErrUnableToStop = errors.New("gocat: unable to stop task") +) + +const ( + // sessionCracked indicates all hashes were cracked + sessionCracked int = iota + // sessionExhausted indicates all possible permutations were reached in the session + sessionExhausted + // sessionQuit indicates the session was quit by the user + sessionQuit + // sessionAborted indicates the session was aborted by the user + sessionAborted + // sessionAbortedCheckpoint indicates the session was checkpointed by the user (usually due to an abort signal or temperature limit) + sessionAbortedCheckpoint + // sessionAbortedRuntime indicates the session was stopped by hashcat (usually due to a runtime limit) + sessionAbortedRuntime +) + +// EventCallback defines the callback that hashcat/gocat calls +type EventCallback func(Hashcat unsafe.Pointer, Payload interface{}) + +// Options defines all the configuration options for gocat/hashcat +type Options struct { + // SharedPath should be set to the location where OpenCL kernels and .hcstat/hctune files live + SharedPath string + // ExecutablePath should be set to the location of the binary and not the binary itself. + // Hashcat does some weird logic and will ignore the shared path if this is incorrectly set. + // If ExecutablePath is not set, we'll attempt to calculate it using os.Args[0] + ExecutablePath string + // PatchEventContext if true will patch hashcat's event_ctx with a reentrant lock which will allow + // you to call several hashcat APIs (which fire another callback) from within an event callback. + // This is supported on macOS, Linux, and Windows. + PatchEventContext bool +} + +// ErrNoSharedPath is raised whenever Options.SharedPath is not set +var ErrNoSharedPath = errors.New("shared path must be set") + +func (o *Options) validate() (err error) { + if o.SharedPath == "" { + return ErrNoSharedPath + } + + if o.ExecutablePath == "" { + path, err := os.Executable() + if err != nil { + return err + } + + o.ExecutablePath = path + } else { + if _, err = os.Stat(o.ExecutablePath); err != nil { + return err + } + } + + return nil +} + +// Hashcat is the interface which interfaces with libhashcat to provide password cracking capabilities. +type Hashcat struct { + // unexported fields below + wrapper C.gocat_ctx_t + cb EventCallback + opts Options + isEventPatched bool + l sync.Mutex + + // these must be free'd + executablePath *C.char + sharedPath *C.char +} + +// New creates a context that can be reused to crack passwords. +func New(opts Options, cb EventCallback) (hc *Hashcat, err error) { + if err = opts.validate(); err != nil { + return nil, err + } + + fmt.Println(opts) + hc = &Hashcat{ + executablePath: C.CString(opts.ExecutablePath), + sharedPath: C.CString(opts.SharedPath), + cb: cb, + opts: opts, + } + + hc.wrapper = C.gocat_ctx_t{ + ctx: C.hashcat_ctx_t{}, + gowrapper: unsafe.Pointer(hc), + bValidateHashes: false, + } + + if retval := C.hashcat_init(&hc.wrapper.ctx, (*[0]byte)(unsafe.Pointer(C.event))); retval != 0 { + return + } + + return +} + +// EventCallbackIsReentrant returns a boolean indicating if hashcat_ctx.event_ctx has been patched to allow an event to fire another event +func (hc *Hashcat) EventCallbackIsReentrant() bool { + return hc.isEventPatched +} + +// RunJob starts a hashcat session and blocks until it has been finished. +func (hc *Hashcat) RunJob(args ...string) (err error) { + hc.l.Lock() + defer hc.l.Unlock() + + // initialize the default options in hashcat_ctx->user_options + if retval := C.user_options_init(&hc.wrapper.ctx); retval != 0 { + return + } + + argc, argv := convertArgsToC(append([]string{hc.opts.ExecutablePath}, args...)...) + defer C.freeargv(argc, argv) + + if retval := C.user_options_getopt(&hc.wrapper.ctx, argc, argv); retval != 0 { + return getErrorFromCtx(hc.wrapper.ctx) + } + + if retval := C.user_options_sanity(&hc.wrapper.ctx); retval != 0 { + return getErrorFromCtx(hc.wrapper.ctx) + } + + if retval := C.hashcat_session_init(&hc.wrapper.ctx, hc.executablePath, hc.sharedPath, argc, argv, C.int(CompileTime)); retval != 0 { + return getErrorFromCtx(hc.wrapper.ctx) + } + defer C.hashcat_session_destroy(&hc.wrapper.ctx) + + if hc.opts.PatchEventContext { + isPatchSuccessful, err := patchEventMutex(hc.wrapper.ctx) + if err != nil { + return err + } + hc.isEventPatched = isPatchSuccessful + } + + rc := C.hashcat_session_execute(&hc.wrapper.ctx) + switch int(rc) { + case sessionCracked, sessionExhausted, sessionQuit, sessionAborted, + sessionAbortedCheckpoint, sessionAbortedRuntime: + err = nil + default: + return getErrorFromCtx(hc.wrapper.ctx) + } + + return +} + +// RunJobWithOptions is a convenience function to take a HashcatSessionOptions struct and craft the necessary argvs to use +// for the hashcat session. +// This is NOT goroutine safe. If you are needing to run multiple jobs, create a context for each one. +func (hc *Hashcat) RunJobWithOptions(opts hcargp.HashcatSessionOptions) error { + args, err := opts.MarshalArgs() + if err != nil { + return err + } + return hc.RunJob(args...) +} + +// StopAtCheckpoint instructs the running hashcat session to stop at the next available checkpoint +func (hc *Hashcat) StopAtCheckpoint() error { + if retval := C.hashcat_session_checkpoint(&hc.wrapper.ctx); retval != 0 { + return ErrUnableToStopAtCheckpoint + } + return nil +} + +// AbortRunningTask instructs hashcat to abruptly stop the running session +func (hc *Hashcat) AbortRunningTask() { + C.hashcat_session_quit(&hc.wrapper.ctx) +} + +func getErrorFromCtx(ctx C.hashcat_ctx_t) error { + msg := C.hashcat_get_log(&ctx) + return fmt.Errorf("gocat: %s", C.GoString(msg)) +} + +//export callback +func callback(id uint32, hcCtx *C.hashcat_ctx_t, wrapper unsafe.Pointer, buf unsafe.Pointer, len C.size_t) { + ctx := (*Hashcat)(wrapper) + + var payload interface{} + var err error + + switch id { + case C.EVENT_LOG_ERROR: + payload = logMessageCbFromEvent(hcCtx, InfoMessage) + case C.EVENT_LOG_INFO: + payload = logMessageCbFromEvent(hcCtx, InfoMessage) + case C.EVENT_LOG_WARNING: + payload = logMessageCbFromEvent(hcCtx, WarnMessage) + case C.EVENT_BITMAP_INIT_PRE: + payload = logHashcatAction(id, "Generating bitmap tables") + case C.EVENT_BITMAP_INIT_POST: + payload = logHashcatAction(id, "Generated bitmap tables") + case C.EVENT_CALCULATED_WORDS_BASE: + if hcCtx.user_options.keyspace { + payload = logHashcatAction(id, fmt.Sprintf("Calculated Words Base: %d", hcCtx.status_ctx.words_base)) + } + case C.EVENT_HASHLIST_SORT_SALT_PRE: + payload = logHashcatAction(id, "Sorting salts...") + case C.EVENT_HASHLIST_SORT_SALT_POST: + payload = logHashcatAction(id, "Sorted salts...") + case C.EVENT_OPENCL_SESSION_PRE: + payload = logHashcatAction(id, "Initializing device kernels and memory") + case C.EVENT_OPENCL_SESSION_POST: + payload = logHashcatAction(id, "Initialized device kernels and memory") + case C.EVENT_AUTOTUNE_STARTING: + payload = logHashcatAction(id, "Starting Autotune threads") + case C.EVENT_AUTOTUNE_FINISHED: + payload = logHashcatAction(id, "Autotune threads have started..") + case C.EVENT_OUTERLOOP_MAINSCREEN: + hashes := ctx.wrapper.ctx.hashes + payload = TaskInformationPayload{ + NumHashes: uint32(hashes.hashes_cnt_orig), + NumHashesUnique: uint32(hashes.digests_cnt), + NumSalts: uint32(hashes.salts_cnt), + } + case C.EVENT_MONITOR_PERFORMANCE_HINT: + payload = logHashcatAction(id, "Device performance might be suffering due to a less than optimal configuration") + case C.EVENT_POTFILE_REMOVE_PARSE_PRE: + payload = logHashcatAction(id, "Comparing hashes with potfile entries...") + case C.EVENT_POTFILE_REMOVE_PARSE_POST: + payload = logHashcatAction(id, "Compared hashes with potfile entries") + case C.EVENT_POTFILE_ALL_CRACKED: + payload = logHashcatAction(id, "All hashes exist in potfile") + if ctx.isEventPatched { + C.potfile_handle_show(&ctx.wrapper.ctx) + } + payload = FinalStatusPayload{ + Status: nil, + EndedAt: time.Now().UTC(), + AllHashesCracked: true, + } + case C.EVENT_SET_KERNEL_POWER_FINAL: + payload = logHashcatAction(id, "Approaching final keyspace, workload adjusted") + case C.EVENT_POTFILE_NUM_CRACKED: + ctxHashes := hcCtx.hashes + if ctxHashes.digests_done > 0 { + payload = logHashcatAction(id, fmt.Sprintf("Removed %d hash(s) found in potfile", ctxHashes.digests_done)) + } + case C.EVENT_CRACKER_HASH_CRACKED, C.EVENT_POTFILE_HASH_SHOW: + // Grab the separator for this session out of user options + userOpts := hcCtx.user_options + sepr := C.GoString(&userOpts.separator) + // XXX(cschmitt): What changed here that this is no longer set? + if sepr == "" { + sepr = ":" + } + + msg := C.GoString((*C.char)(buf)) + if payload, err = getCrackedPassword(id, msg, sepr); err != nil { + payload = logMessageWithError(id, err) + } + case C.EVENT_OUTERLOOP_FINISHED: + payload = FinalStatusPayload{ + Status: ctx.GetStatus(), + EndedAt: time.Now().UTC(), + } + } + + // Events we're ignoring: + // EVENT_CRACKER_STARTING + // EVENT_OUTERLOOP_MAINSCREEN + + ctx.cb(unsafe.Pointer(hcCtx), payload) +} + +// Free releases all allocations. Call this when you're done with hashcat or exiting the application +func (hc *Hashcat) Free() { + C.hashcat_destroy(&hc.wrapper.ctx) + C.free(unsafe.Pointer(hc.executablePath)) + C.free(unsafe.Pointer(hc.sharedPath)) +} diff --git a/gocat_posix_test.go b/gocat_posix_test.go new file mode 100644 index 0000000..dac0b09 --- /dev/null +++ b/gocat_posix_test.go @@ -0,0 +1,97 @@ +// +build linux,cgo darwin,cgo + +package gocat + +/* +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReentrantPatchWithPotfile(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + PatchEventContext: true, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + if err != nil { + t.Fatal(err) + } + + assert.NotNil(t, hc) + assert.NoError(t, err) + + potfilePath, err := filepath.Abs("./testdata/two_md5.potfile") + if err != nil { + t.Fatal(err) + } + + hashfilePath, err := filepath.Abs("./testdata/two_md5.hashes") + if err != nil { + t.Fatal(err) + } + + err = hc.RunJob("-a", "0", "-m", "0", "--potfile-path", potfilePath, hashfilePath, "./testdata/test_dictionary.txt") + assert.NoError(t, err) + assert.Len(t, crackedHashes, 2) + assert.Equal(t, "hello", *crackedHashes["5d41402abc4b2a76b9719d911017c592"]) + assert.Equal(t, "world", *crackedHashes["7d793037a0760186574b0282f2f435e7"]) +} + + +func TestReentrantPatchWithPotfileMixed(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + PatchEventContext: true, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + if err != nil { + t.Fatal(err) + } + + assert.NotNil(t, hc) + assert.NoError(t, err) + + potfilePath, err := filepath.Abs("./testdata/one_md5_in_potfile.potfile") + if err != nil { + t.Fatal(err) + } + + // We need to make a backup of this because once this cracks, hashcat will update the potfile + orignalPotfileContents, err := ioutil.ReadFile(potfilePath) + if err != nil { + t.Fatal(err) + } + + hashfilePath, err := filepath.Abs("./testdata/one_md5_in_potfile.hashes") + if err != nil { + t.Fatal(err) + } + + err = hc.RunJob("-a", "0", "-m", "0", "--potfile-path", potfilePath, hashfilePath, "./testdata/test_dictionary.txt") + assert.NoError(t, err) + assert.Len(t, crackedHashes, 1) + // Unfortunately, I haven't found a way to show partial potfile results so the hash for "hello" won't be returned by this test + assert.NotContains(t, "5d41402abc4b2a76b9719d911017c592", crackedHashes) + // ...this one should crack successfully though + + assert.Equal(t, "chris", *crackedHashes["6b34fe24ac2ff8103f6fce1f0da2ef57"]) + + fd, err := os.OpenFile(potfilePath, os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + t.Logf("Test will be corrupted as we were unable to open the potfile to restore the original contents: %s", err) + t.FailNow() + } + fd.WriteAt(orignalPotfileContents, 0) +} +*/ \ No newline at end of file diff --git a/gocat_status.go b/gocat_status.go new file mode 100644 index 0000000..3ef68df --- /dev/null +++ b/gocat_status.go @@ -0,0 +1,163 @@ +package gocat + +// #include "wrapper.h" +import "C" +import ( + "fmt" + "unsafe" +) + +// DeviceStatus contains information about the OpenCL device that is cracking +type DeviceStatus struct { + DeviceID int + HashesSec string + ExecDev float64 +} + +// Status contains data about the current cracking session +type Status struct { + Session string + Status string + HashType string + HashTarget string + TimeStarted string + TimeEstimated string + TimeEstimatedRelative string + DeviceStatus []DeviceStatus + TotalSpeed string + ProgressMode int + Candidates map[int]string // map[DeviceID]string + Progress string + Rejected string + Recovered string + RestorePoint string + GuessMode int + GuessMask string `json:",omitempty"` + GuessQueue string `json:",omitempty"` + GuessBase string `json:",omitempty"` + GuessMod string `json:",omitempty"` + GuessCharset string `json:",omitempty"` +} + +var szStatusStruct = unsafe.Sizeof(C.hashcat_status_t{}) + +// GetStatus returns the status of the cracking. +// This is an implementation of https://github.com/hashcat/hashcat/blob/master/src/terminal.c#L709 +func (hc *Hashcat) GetStatus() *Status { + ptr := C.malloc(C.size_t(szStatusStruct)) + hcStatus := (*C.hashcat_status_t)(unsafe.Pointer(ptr)) + defer C.free(unsafe.Pointer(hcStatus)) + + if retval := C.hashcat_get_status(&hc.wrapper.ctx, hcStatus); retval != 0 { + return nil + } + defer C.status_status_destroy(&hc.wrapper.ctx, hcStatus) + + stats := &Status{ + Session: C.GoString(hcStatus.session), + Status: C.GoString(hcStatus.status_string), + HashType: C.GoString(hcStatus.hash_type), + HashTarget: C.GoString(hcStatus.hash_target), + TimeStarted: C.GoString(hcStatus.time_started_absolute), + TimeEstimated: C.GoString(hcStatus.time_estimated_absolute), + TimeEstimatedRelative: C.GoString(hcStatus.time_estimated_relative), + // Instead of using hcStatus.device_info_cnt here, we'll let go manage this + // to avoid having empty DeviceStatus's + DeviceStatus: make([]DeviceStatus, 0), + TotalSpeed: C.GoString(hcStatus.speed_sec_all), + ProgressMode: int(hcStatus.progress_mode), + Candidates: make(map[int]string), + GuessMode: int(hcStatus.guess_mode), + } + + switch stats.ProgressMode { + case C.PROGRESS_MODE_KEYSPACE_KNOWN: + stats.Progress = fmt.Sprintf("%d/%d (%.02f%%)", hcStatus.progress_cur_relative_skip, + hcStatus.progress_end_relative_skip, + hcStatus.progress_finished_percent) + stats.Rejected = fmt.Sprintf("%d/%d (%.02f%%)", hcStatus.progress_rejected, + hcStatus.progress_cur_relative_skip, + hcStatus.progress_rejected_percent) + stats.RestorePoint = fmt.Sprintf("%d/%d (%.02f%%)", hcStatus.restore_point, + hcStatus.restore_total, + hcStatus.restore_percent) + case C.PROGRESS_MODE_KEYSPACE_UNKNOWN: + stats.Progress = fmt.Sprintf("%d", hcStatus.progress_cur_relative_skip) + stats.Rejected = fmt.Sprintf("%d", hcStatus.progress_rejected) + stats.RestorePoint = fmt.Sprintf("%d", hcStatus.restore_point) + } + + switch stats.GuessMode { + case C.GUESS_MODE_STRAIGHT_FILE: + stats.GuessBase = C.GoString(hcStatus.guess_base) + case C.GUESS_MODE_STRAIGHT_FILE_RULES_FILE: + stats.GuessBase = C.GoString(hcStatus.guess_base) + stats.GuessMod = C.GoString(hcStatus.guess_mod) + case C.GUESS_MODE_STRAIGHT_FILE_RULES_GEN: + stats.GuessBase = C.GoString(hcStatus.guess_base) + stats.GuessMod = "Rules (Generated)" + case C.GUESS_MODE_STRAIGHT_STDIN: + stats.GuessBase = "Pipe" + case C.GUESS_MODE_STRAIGHT_STDIN_RULES_FILE: + stats.GuessBase = "Pipe" + stats.GuessMod = C.GoString(hcStatus.guess_mod) + case C.GUESS_MODE_STRAIGHT_STDIN_RULES_GEN: + stats.GuessBase = "Pipe" + stats.GuessMod = "Rules (Generated)" + case C.GUESS_MODE_COMBINATOR_BASE_LEFT: + stats.GuessBase = fmt.Sprintf("File (%s), Left Side", C.GoString(hcStatus.guess_base)) + stats.GuessMod = fmt.Sprintf("File (%s), Right Side", C.GoString(hcStatus.guess_mod)) + case C.GUESS_MODE_COMBINATOR_BASE_RIGHT: + stats.GuessBase = fmt.Sprintf("File (%s), Right Side", C.GoString(hcStatus.guess_base)) + stats.GuessMod = fmt.Sprintf("File (%s), Left Side", C.GoString(hcStatus.guess_mod)) + case C.GUESS_MODE_MASK: + stats.GuessMask = fmt.Sprintf("%s [%d]", C.GoString(hcStatus.guess_base), int(hcStatus.guess_mask_length)) + case C.GUESS_MODE_MASK_CS: + stats.GuessMask = fmt.Sprintf("%s [%d]", C.GoString(hcStatus.guess_base), int(hcStatus.guess_mask_length)) + stats.GuessCharset = C.GoString(hcStatus.guess_charset) + case C.GUESS_MODE_HYBRID1_CS: + stats.GuessCharset = C.GoString(hcStatus.guess_charset) + fallthrough // grab GuessBase/GuessMod from below as it's the same + case C.GUESS_MODE_HYBRID1: + stats.GuessBase = fmt.Sprintf("File (%s), Left Side", C.GoString(hcStatus.guess_base)) + stats.GuessMod = fmt.Sprintf("Mask (%s) [%d], Right Side", C.GoString(hcStatus.guess_base), int(hcStatus.guess_mask_length)) + } + + switch stats.GuessMode { + case C.GUESS_MODE_STRAIGHT_FILE, C.GUESS_MODE_STRAIGHT_FILE_RULES_FILE, + C.GUESS_MODE_STRAIGHT_FILE_RULES_GEN, C.GUESS_MODE_MASK: + stats.GuessQueue = fmt.Sprintf("%d/%d (%.02f%%)", hcStatus.guess_base_offset, + hcStatus.guess_base_count, hcStatus.guess_base_percent) + case C.GUESS_MODE_HYBRID1, C.GUESS_MODE_HYBRID2: + stats.GuessQueue = fmt.Sprintf("%d/%d (%.02f%%)", hcStatus.guess_base_offset, + hcStatus.guess_base_count, hcStatus.guess_base_percent) + } + + stats.Recovered = fmt.Sprintf("%d/%d (%.2f%%) Digests, %d/%d (%.2f%%) Salts", + hcStatus.digests_done, + hcStatus.digests_cnt, + hcStatus.digests_percent, + hcStatus.salts_done, + hcStatus.salts_cnt, + hcStatus.salts_percent, + ) + + for i := 0; i < int(hcStatus.device_info_cnt); i++ { + deviceInfo := hcStatus.device_info_buf[i] + if deviceInfo.skipped_dev { + continue + } + + stats.DeviceStatus = append(stats.DeviceStatus, DeviceStatus{ + DeviceID: i + 1, + HashesSec: C.GoString(deviceInfo.speed_sec_dev), + ExecDev: float64(deviceInfo.exec_msec_dev), + }) + + if deviceInfo.guess_candidates_dev != nil { + stats.Candidates[i+1] = C.GoString(deviceInfo.guess_candidates_dev) + } + } + + return stats +} diff --git a/gocat_test.go b/gocat_test.go new file mode 100644 index 0000000..73b1e81 --- /dev/null +++ b/gocat_test.go @@ -0,0 +1,249 @@ +package gocat + +import ( + "fmt" + "log" + "strings" + "testing" + "unsafe" + + "github.com/fireeye/gocat/hcargp" + + "github.com/stretchr/testify/require" +) + +const ( + // Set this to true if you want the gocat callbacks used in the tests to print out + DebugTest bool = true + DefaultSharedPath string = "/usr/local/share/hashcat" +) + +type testStruct struct { + opts hcargp.HashcatSessionOptions + expectedError error +} + +func emptyCallback(hc unsafe.Pointer, payload interface{}) {} + +func callbackForTests(resultsmap map[string]*string) EventCallback { + return func(hc unsafe.Pointer, payload interface{}) { + switch pl := payload.(type) { + case LogPayload: + if DebugTest { + fmt.Printf("LOG [%s] %s\n", pl.Level, pl.Message) + } + case ActionPayload: + if DebugTest { + fmt.Printf("ACTION [%d] %s\n", pl.HashcatEvent, pl.Message) + } + case CrackedPayload: + if DebugTest { + fmt.Printf("CRACKED %s -> %s\n", pl.Hash, pl.Value) + } + if resultsmap != nil { + resultsmap[pl.Hash] = hcargp.GetStringPtr(pl.Value) + } + case FinalStatusPayload: + if DebugTest { + fmt.Printf("FINAL STATUS -> %v\n", pl.Status) + } + case TaskInformationPayload: + if DebugTest { + fmt.Printf("TASK INFO -> %v\n", pl) + } + } + } +} + +func TestOptionsExecPath(t *testing.T) { + // Valid + opts := Options{ + ExecutablePath: "", + SharedPath: "/tmp", + } + + err := opts.validate() + require.Nil(t, err) + fmt.Println(opts.ExecutablePath) + require.True(t, strings.HasSuffix(opts.ExecutablePath, "test")) + + // Not valid because executable path was incorrectly set by the user + opts.ExecutablePath = "/nope" + err = opts.validate() + require.Error(t, err) +} + +func TestGoCatOptionsValidatorErrors(t *testing.T) { + for _, test := range []struct { + opts Options + expectedError error + expectedOpts map[string]interface{} + }{ + { + opts: Options{ + SharedPath: "", + }, + expectedError: ErrNoSharedPath, + }, + { + opts: Options{ + SharedPath: "/deadbeef", + ExecutablePath: "", + }, + }, + } { + err := test.opts.validate() + require.Equal(t, test.expectedError, err) + } +} + +func TestGoCatCrackingMD5(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + require.NotNil(t, hc) + require.NoError(t, err) + + err = hc.RunJob("-a", "0", "-m", "0", "--potfile-disable", "5d41402abc4b2a76b9719d911017c592", "./testdata/test_dictionary.txt") + require.NoError(t, err) + require.Len(t, crackedHashes, 1) + require.Equal(t, "hello", *crackedHashes["5d41402abc4b2a76b9719d911017c592"]) +} + +func TestGoCatReusingContext(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + require.NotNil(t, hc) + require.NoError(t, err) + + err = hc.RunJob("-a", "0", "-m", "0", "--potfile-disable", "5d41402abc4b2a76b9719d911017c592", "./testdata/test_dictionary.txt") + require.NoError(t, err) + require.Len(t, crackedHashes, 1) + require.Equal(t, "hello", *crackedHashes["5d41402abc4b2a76b9719d911017c592"]) + + err = hc.RunJob("-a", "0", "-m", "0", "--potfile-disable", "9f9d51bc70ef21ca5c14f307980a29d8", "./testdata/test_dictionary.txt") + require.NoError(t, err) + require.Len(t, crackedHashes, 2) // the previous run will still exist in this map + require.Equal(t, "bob", *crackedHashes["9f9d51bc70ef21ca5c14f307980a29d8"]) +} + +func TestGoCatRunJobWithOptions(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + require.NotNil(t, hc) + require.NoError(t, err) + + err = hc.RunJobWithOptions(hcargp.HashcatSessionOptions{ + AttackMode: hcargp.GetIntPtr(0), + HashType: hcargp.GetIntPtr(0), + PotfileDisable: hcargp.GetBoolPtr(true), + InputFile: "9f9d51bc70ef21ca5c14f307980a29d8", + DictionaryMaskDirectoryInput: hcargp.GetStringPtr("./testdata/test_dictionary.txt"), + }) + + require.NoError(t, err) + require.Len(t, crackedHashes, 1) // the previous run will still exist in this map + require.Equal(t, "bob", *crackedHashes["9f9d51bc70ef21ca5c14f307980a29d8"]) +} + +func TestGocatRussianHashes(t *testing.T) { + crackedHashes := map[string]*string{} + + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + }, callbackForTests(crackedHashes)) + defer hc.Free() + + require.NotNil(t, hc) + require.NoError(t, err) + + err = hc.RunJobWithOptions(hcargp.HashcatSessionOptions{ + AttackMode: hcargp.GetIntPtr(0), + HashType: hcargp.GetIntPtr(0), + PotfileDisable: hcargp.GetBoolPtr(true), + InputFile: "./testdata/russian_test.hashes", + DictionaryMaskDirectoryInput: hcargp.GetStringPtr("./testdata/russian_test.dictionary"), + }) + + require.NoError(t, err) + require.Len(t, crackedHashes, 4) // the previous run will still exist in this map + fmt.Println("HI", crackedHashes) + fmt.Println(crackedHashes) +} + +func TestGoCatStopAtCheckpointWithNoRunningSession(t *testing.T) { + hc, err := New(Options{ + SharedPath: DefaultSharedPath, + }, emptyCallback) + defer hc.Free() + + require.NotNil(t, hc) + require.NoError(t, err) + + err = hc.StopAtCheckpoint() + require.Equal(t, ErrUnableToStopAtCheckpoint, err) +} + +func TestExampleHashcat_RunJobWithOptions(t *testing.T) { + eventCallback := func(hc unsafe.Pointer, payload interface{}) { + switch pl := payload.(type) { + case LogPayload: + if DebugTest { + fmt.Printf("LOG [%s] %s\n", pl.Level, pl.Message) + } + case ActionPayload: + if DebugTest { + fmt.Printf("ACTION [%d] %s\n", pl.HashcatEvent, pl.Message) + } + case CrackedPayload: + if DebugTest { + fmt.Printf("CRACKED %s -> %s\n", pl.Hash, pl.Value) + } + case FinalStatusPayload: + if DebugTest { + fmt.Printf("FINAL STATUS -> %v\n", pl.Status) + } + case TaskInformationPayload: + if DebugTest { + fmt.Printf("TASK INFO -> %v\n", pl) + } + } + } + + hc, err := New(Options{ + SharedPath: "/usr/local/share/hashcat", + ExecutablePath: "/usr/local/share/hashcat", + }, eventCallback) + defer hc.Free() + + if err != nil { + log.Fatal(err) + } + + err = hc.RunJobWithOptions(hcargp.HashcatSessionOptions{ + AttackMode: hcargp.GetIntPtr(0), + HashType: hcargp.GetIntPtr(0), + PotfileDisable: hcargp.GetBoolPtr(true), + OptimizedKernelEnabled: hcargp.GetBoolPtr(true), + InputFile: "9f9d51bc70ef21ca5c14d307980a29d2", + DictionaryMaskDirectoryInput: hcargp.GetStringPtr("./testdata/test_dictionary.txt"), + }) + + if err != nil { + log.Fatal(err) + } +} diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..8ae332a --- /dev/null +++ b/handlers.go @@ -0,0 +1,134 @@ +package gocat + +// #include "wrapper.h" +import "C" +import ( + "fmt" + "strings" + "time" +) + +const ( + // InfoMessage is a log message from hashcat with the id of EVENT_LOG_INFO + InfoMessage LogLevel = iota + // WarnMessage is a log message from hashcat with the id of EVENT_LOG_WARNING + WarnMessage + // ErrorMessage is a log message from hashcat with the id of EVENT_LOG_ERROR + ErrorMessage + // AdviceMessage is a log message from hashcat with the id of EVENT_LOG_ADVICE + AdviceMessage +) + +// LogPayload defines the structure of an event log message from hashcat and sent to the user via the callback +type LogPayload struct { + Level LogLevel + Message string + Error error +} + +// TaskInformationPayload includes information about the task that hashcat is getting ready to process. This includes deduplicated hashes, etc. +type TaskInformationPayload struct { + NumHashes uint32 + NumHashesUnique uint32 + NumSalts uint32 +} + +// ActionPayload defines the structure of a generic hashcat event and sent to the user via the callback. +// An example of this would be the numerous PRE/POST events. +type ActionPayload struct { + HashcatEvent uint32 + LogPayload +} + +// CrackedPayload defines the structure of a cracked message from hashcat and sent to the user via the callback +type CrackedPayload struct { + IsPotfile bool + Hash string + Value string + CrackedAt time.Time +} + +// FinalStatusPayload is returned at the end of the cracking session +type FinalStatusPayload struct { + Status *Status + EndedAt time.Time + // AllHashesCracked is set when all hashes either exist in a potfile or are considered "weak" + AllHashesCracked bool +} + +// ErrCrackedPayload is raised whenever we get a cracked password callback but was unable to parse the message from hashcat +type ErrCrackedPayload struct { + Separator string + CrackedMsg string +} + +func (e ErrCrackedPayload) Error() string { + return fmt.Sprintf("Could not locate separator `%s` in msg", e.Separator) +} + +// LogLevel indicates the type of log message from hashcat +type LogLevel int8 + +func (s LogLevel) String() string { + switch s { + case InfoMessage: + return "INFO" + case WarnMessage: + return "WARN" + case ErrorMessage: + return "ERROR" + case AdviceMessage: + return "ADVICE" + default: + return "UNKNOWN" + } +} + +// logMessageCbFromEvent is called whenever hashcat sends a INFO/WARN/ERROR message +func logMessageCbFromEvent(ctx *C.hashcat_ctx_t, lvl LogLevel) LogPayload { + ectx := ctx.event_ctx + + return LogPayload{ + Level: lvl, + Message: C.GoStringN(&ectx.msg_buf[0], C.int(ectx.msg_len)), + } +} + +func logMessageWithError(id uint32, err error) LogPayload { + return LogPayload{ + Level: ErrorMessage, + Message: err.Error(), + Error: err, + } +} + +func logHashcatAction(id uint32, msg string) ActionPayload { + return ActionPayload{ + LogPayload: LogPayload{ + Level: InfoMessage, + Message: msg, + }, + HashcatEvent: id, + } +} + +func getCrackedPassword(id uint32, msg string, sep string) (pl CrackedPayload, err error) { + // Some messages can have multiple variations of the separator (example: kerberos 13100) + // so we find the last one and use that to separate the original hash and it's value + idx := strings.LastIndex(msg, sep) + if idx == -1 { + err = ErrCrackedPayload{ + Separator: sep, + CrackedMsg: msg, + } + return + } + + pl = CrackedPayload{ + Hash: msg[:idx], + Value: msg[idx+1:], + IsPotfile: id == C.EVENT_POTFILE_HASH_SHOW, + CrackedAt: time.Now().UTC(), + } + return +} diff --git a/handlers_test.go b/handlers_test.go new file mode 100644 index 0000000..bf85be3 --- /dev/null +++ b/handlers_test.go @@ -0,0 +1,55 @@ +package gocat + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLogLevelString(t *testing.T) { + for _, test := range []struct { + ll LogLevel + expected string + }{ + { + ll: InfoMessage, + expected: "INFO", + }, + { + ll: WarnMessage, + expected: "WARN", + }, + { + ll: ErrorMessage, + expected: "ERROR", + }, + { + ll: AdviceMessage, + expected: "ADVICE", + }, + { + ll: LogLevel(9), + expected: "UNKNOWN", + }, + } { + assert.Equal(t, test.expected, test.ll.String()) + } +} + +func TestGetCrackedPassword(t *testing.T) { + pl, err := getCrackedPassword(1, "deadbeefdeadbeefdeadbeefdeadbeef:chris", ":") + assert.Nilf(t, err, "expected err to be nil") + assert.Equalf(t, "chris", pl.Value, "expected the value to be chris") + assert.Equal(t, "deadbeefdeadbeefdeadbeefdeadbeef", pl.Hash, "expected the hash to be deadbeef yo!") + + pl, err = getCrackedPassword(1, "deadbeefdeadbeefdeadbeefdeadbeef:chris", ";") + assert.Equalf(t, err, ErrCrackedPayload{ + Separator: ";", + CrackedMsg: "deadbeefdeadbeefdeadbeefdeadbeef:chris", + }, "err payload is not correct") + + ecperr, ok := err.(ErrCrackedPayload) + assert.True(t, ok) + assert.NotNil(t, ecperr) + assert.Equal(t, "Could not locate separator `;` in msg", err.Error()) +} diff --git a/hcargp/gocat_hashcat_options.go b/hcargp/gocat_hashcat_options.go new file mode 100644 index 0000000..7d45326 --- /dev/null +++ b/hcargp/gocat_hashcat_options.go @@ -0,0 +1,189 @@ +package hcargp + +import ( + "fmt" + "reflect" + "runtime" + "strings" +) + +// GetStringPtr returns the pointer of s +func GetStringPtr(s string) *string { + return &s +} + +// GetIntPtr returns the pointer of i +func GetIntPtr(i int) *int { + return &i +} + +// GetBoolPtr returns the pointer of b +func GetBoolPtr(b bool) *bool { + return &b +} + +/* +We skip the following arguments because they are not needed: +- version +- help +- quiet +- status +- status-timer +- machine-readable +- stdout +- show +- left +- benchmark +- speed-only (todo?) +- progress-only (todo?) +- opencl-info +- keyspace +*/ + +// HashcatSessionOptions represents all the available hashcat options. The values here should always follow the latest version of hashcat +type HashcatSessionOptions struct { + HashType *int `hashcat:"--hash-type,omitempty"` + AttackMode *int `hashcat:"--attack-mode,omitempty"` + IsHexCharset *bool `hashcat:"--hex-charset,omitempty"` + IsHexSalt *bool `hashcat:"--hex-salt,omitempty"` + IsHexWordlist *bool `hashcat:"--hex-wordlist,omitempty"` + KeepGuessing *bool `hashcat:"--keep-guessing,omitempty"` + Loopback *bool `hashcat:"--loopback,omitempty"` + WeakHashThreshold *int `hashcat:"--weak-hash-threshold,omitempty"` + MarkovHCStat *string `hashcat:"--markov-hcstat,omitempty"` + DisableMarkov *bool `hashcat:"--markov-disable,omitempty"` + EnableClassicMarkov *bool `hashcat:"--markov-classic,omitempty"` + MarkovThreshold *int `hashcat:"--markov-threshold,omitempty"` + Force *bool `hashcat:"--force,omitempty"` + MaxRuntimeSeconds *int `hashcat:"--runtime,omitempty"` + SessionName *string `hashcat:"--session,omitempty"` + RestoreSession *bool `hashcat:"--restore,omitempty"` + DisableRestore *bool `hashcat:"--restore-disable,omitempty"` + RestoreFilePath *string `hashcat:"--restore-file-path,omitempty"` + OutfilePath *string `hashcat:"--outfile,omitempty"` + OutfileFormat *int `hashcat:"--outfile-format,omitempty"` + OutfileDisableAutoHex *bool `hashcat:"--outfile-autohex-disable,omitempty"` + OutfileCheckTimer *int `hashcat:"--outfile-check-timer,omitempty"` + Separator *string `hashcat:"--separator,omitempty"` + IgnoreUsername *bool `hashcat:"--username,omitempty"` + RemoveCrackedHash *bool `hashcat:"--remove,omitempty"` + RemoveCrackedHashTimer *int `hashcat:"--remove-timer,omitempty"` + PotfileDisable *bool `hashcat:"--potfile-disable,omitempty"` + PotfilePath *string `hashcat:"--potfile-path,omitempty"` + DebugMode *int `hashcat:"--debug-mode,omitempty"` + DebugFile *string `hashcat:"--debug-file,omitempty"` + InductionDir *string `hashcat:"--induction-dir,omitempty"` + LogfileDisable *bool `hashcat:"--logfile-disable,omitempty"` + HccapxMessagePair *string `hashcat:"--hccapx-message-pair,omitempty"` + NonceErrorCorrections *int `hashcat:"--nonce-error-corrections,omitempty"` + TrueCryptKeyFiles *string `hashcat:"--truecrypt-keyfiles,omitempty"` + VeraCryptKeyFiles *string `hashcat:"--veracrypt-keyfiles,omitempty"` + VeraCryptPIM *int `hashcat:"--veracrypt-pim,omitempty"` + SegmentSize *int `hashcat:"--segment-size,omitempty"` + BitmapMin *int `hashcat:"--bitmap-min,omitempty"` + BitmapMax *int `hashcat:"--bitmap-max,omitempty"` + CPUAffinity *string `hashcat:"--cpu-affinity,omitempty"` + OpenCLPlatforms *string `hashcat:"--opencl-platforms,omitempty"` + OpenCLDevices *string `hashcat:"--opencl-devices,omitempty"` + OpenCLDeviceTypes *string `hashcat:"--opencl-device-types,omitempty"` + OpenCLVectorWidth *string `hashcat:"--opencl-vector-width,omitempty"` + WorkloadProfile *int `hashcat:"--workload-profile,omitempty"` + KernelAccel *int `hashcat:"--kernel-accel,omitempty"` + KernelLoops *int `hashcat:"--kernel-loops,omitempty"` + NVIDIASpinDamp *int `hashcat:"--nvidia-spin-damp,omitempty"` + GPUTempDisable *bool `hashcat:"--gpu-temp-disable,omitempty"` + GPUTempAbort *int `hashcat:"--gpu-temp-abort,omitempty"` + GPUTempRetain *int `hashcat:"--gpu-temp-retain,omitempty"` + PowertuneEnable *bool `hashcat:"--powertune-enable,omitempty"` + ScryptTMTO *int `hashcat:"--scrypt-tmto,omitempty"` + Skip *int `hashcat:"--skip,omitempty"` + Limit *int `hashcat:"--limit,omitempty"` + RuleLeft *string `hashcat:"--rule-left,omitempty"` + RuleRight *string `hashcat:"--rule-right,omitempty"` + RulesFile *string `hashcat:"--rules-file,omitempty"` + GenerateRules *int `hashcat:"--generate-rules,omitempty"` + GenerateRulesFuncMin *int `hashcat:"--generate-rules-func-min,omitempty"` + GenerateRulesFuncMax *int `hashcat:"--generate-rules-func-max,omitempty"` + GenerateRulesSeed *int `hashcat:"--generate-rules-seed,omitempty"` + CustomCharset1 *string `hashcat:"--custom-charset1,omitempty"` + CustomCharset2 *string `hashcat:"--custom-charset2,omitempty"` + CustomCharset3 *string `hashcat:"--custom-charset3,omitempty"` + CustomCharset4 *string `hashcat:"--custom-charset4,omitempty"` + IncrementMask *bool `hashcat:"--increment,omitempty"` + IncrementMaskMin *int `hashcat:"--increment-min,omitempty"` + IncrementMaskMax *int `hashcat:"--increment-max,omitempty"` + OptimizedKernelEnabled *bool `hashcat:"--optimized-kernel-enable,omitempty"` + + // InputFile can be a single hash or multiple hashes via a hashfile or hccapx + InputFile string `hashcat:","` + DictionaryMaskDirectoryInput *string `hashcat:",omitempty"` +} + +func parseTag(t string) (tag, options string) { + if idx := strings.Index(t, ","); idx != -1 { + return t[:idx], t[idx+1:] + } + return tag, "" +} + +// MarshalArgs returns a list of arguments set by the user to be passed into hashcat's session for execution +func (o HashcatSessionOptions) MarshalArgs() (args []string, err error) { + defer func() { + if r := recover(); r != nil { + if _, ok := r.(runtime.Error); ok { + panic(r) + } + if s, ok := r.(string); ok { + panic(s) + } + + err = r.(error) + } + }() + + v := reflect.ValueOf(o) + for i := 0; i < v.NumField(); i++ { + tag := v.Type().Field(i).Tag.Get("hashcat") + if tag == "" { + continue + } + + name, opts := parseTag(tag) + val := v.Field(i) + + hasOmitEmpty := strings.Contains(opts, "omitempty") + if (val.Type().Kind() == reflect.Ptr && val.IsNil()) && hasOmitEmpty { + continue + } + + if val.Type().Kind() == reflect.Ptr { + val = reflect.Indirect(val) + } + + switch val.Type().Kind() { + case reflect.Bool: + if val.Bool() { + args = append(args, name) + } + case reflect.Int: + // Int's should always have a name... + if name != "" { + args = append(args, fmt.Sprintf("%s=%d", name, val.Int())) + } + case reflect.String: + if val.String() == "" { + continue + } + + if name != "" { + args = append(args, fmt.Sprintf("%s=%s", name, val.String())) + } else { + args = append(args, val.String()) + } + default: + err = fmt.Errorf("unknown type %s", val.Type().Kind()) + return + } + } + return +} diff --git a/hcargp/gocat_hashcat_options_test.go b/hcargp/gocat_hashcat_options_test.go new file mode 100644 index 0000000..0f07515 --- /dev/null +++ b/hcargp/gocat_hashcat_options_test.go @@ -0,0 +1,87 @@ +package hcargp + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func ExampleHashcatSessionOptions_MarshalArgs() { + opts := HashcatSessionOptions{ + AttackMode: GetIntPtr(0), + HashType: GetIntPtr(0), + SessionName: GetStringPtr("example_args_session"), + PotfileDisable: GetBoolPtr(true), + InputFile: "deadbeefdeadbeefdeadbeefdeadbeef", + } + + args, err := opts.MarshalArgs() + if err != nil { + fmt.Printf("Failed to marshal args: %s\n", err) + return + } + + fmt.Println(strings.Join(args, " ")) + // Output: --hash-type=0 --attack-mode=0 --session=example_args_session --potfile-disable deadbeefdeadbeefdeadbeefdeadbeef +} + +func TestInternalParseTag(t *testing.T) { + tag, opts := parseTag("--example,omitempty,required") + + assert.Equal(t, "--example", tag) + assert.Equal(t, "omitempty,required", opts) + + tag, opts = parseTag("--example2,") + assert.Equal(t, "--example2", tag) + assert.Equal(t, "", opts) +} + +func TestHashcatSessionOptionsMarshalArgs(t *testing.T) { + for _, test := range []struct { + opts HashcatSessionOptions + expectedError error + expectedArgs []string + }{ + { + opts: HashcatSessionOptions{ + AttackMode: GetIntPtr(0), + HashType: nil, + InputFile: "deadbeefdeadbeefdeadbeefdeadbeef", + }, + expectedError: nil, + expectedArgs: []string{"--attack-mode=0", "deadbeefdeadbeefdeadbeefdeadbeef"}, + }, + { + opts: HashcatSessionOptions{ + IsHexCharset: GetBoolPtr(true), + InputFile: "deadbeefdeadbeefdeadbeefdeadbeef", + }, + expectedError: nil, + expectedArgs: []string{"--hex-charset", "deadbeefdeadbeefdeadbeefdeadbeef"}, + }, + { + opts: HashcatSessionOptions{ + IsHexCharset: GetBoolPtr(false), + InputFile: "deadbeefdeadbeefdeadbeefdeadbeef", + }, + expectedError: nil, + expectedArgs: []string{"deadbeefdeadbeefdeadbeefdeadbeef"}, + }, + { + opts: HashcatSessionOptions{ + AttackMode: GetIntPtr(0), + InputFile: "deadbeefdeadbeefdeadbeefdeadbeef", + DictionaryMaskDirectoryInput: GetStringPtr("./testdata/test_dictionary.txt"), + }, + expectedError: nil, + expectedArgs: []string{"--attack-mode=0", "deadbeefdeadbeefdeadbeefdeadbeef", "./testdata/test_dictionary.txt"}, + }, + } { + args, err := test.opts.MarshalArgs() + + assert.Equal(t, test.expectedError, err) + assert.Equal(t, test.expectedArgs, args) + } +} diff --git a/reentrant_patch_posix.go b/reentrant_patch_posix.go new file mode 100644 index 0000000..1211f9d --- /dev/null +++ b/reentrant_patch_posix.go @@ -0,0 +1,48 @@ +// +build linux,cgo darwin,cgo + +package gocat + +/* +#include +#include "wrapper.h" + +extern int patch_event_ctx(hashcat_ctx_t *hashcat_ctx) +{ + int rc = -1; + + pthread_mutex_t pMutex; + pthread_mutexattr_t pAttr; + + event_ctx_t *event_ctx = hashcat_ctx->event_ctx; + + hc_thread_mutex_delete(event_ctx->mux_event); + rc = pthread_mutexattr_init(&pAttr); + if (rc != 0) + goto finished; + + pthread_mutexattr_settype(&pAttr, PTHREAD_MUTEX_RECURSIVE); + + rc = pthread_mutex_init(&pMutex, &pAttr); + if (rc != 0) + goto finished; + + event_ctx->mux_event = pMutex; + +finished: + return rc; +} +*/ +import "C" +import "errors" + +var errReentrantPatch = errors.New("failed to patch hashcat_ctx->event_ctx mutex") + +// patchEventMutex frees and updates hashcat's event mutex with a recursive one +// that allows an event callback to call another event callback without a deadlock condition +// NOTE: this only works on posix systems (linux, darwin) +func patchEventMutex(ctx C.hashcat_ctx_t) (patched bool, err error) { + if retval := C.patch_event_ctx(&ctx); retval != 0 { + return false, errReentrantPatch + } + return true, nil +} diff --git a/reentrant_patch_windows.go b/reentrant_patch_windows.go new file mode 100644 index 0000000..a0113c1 --- /dev/null +++ b/reentrant_patch_windows.go @@ -0,0 +1,11 @@ +// +build windows + +package gocat + +// patchEventMutex frees and updates hashcat's event mutex with a recursive one +// that allows an event callback to call another event callback without a deadlock condition. +func patchEventMutex(ctx interface{}) (patched bool, err error) { + // EnterCriticalSection on windows already allows for reentry into a critical section if called by the same thread + // see remarks @ https://msdn.microsoft.com/en-us/library/windows/desktop/ms682608(v=vs.85).aspx + return true, nil +} diff --git a/restoreutil/restoreutil.go b/restoreutil/restoreutil.go new file mode 100644 index 0000000..ae0d670 --- /dev/null +++ b/restoreutil/restoreutil.go @@ -0,0 +1,161 @@ +package restoreutil + +import ( + "bufio" + "bytes" + "encoding/binary" + "io" + "os" + "strings" +) + +// StructSize is the size of the hashcat restore structure as defined here +// https://hashcat.net/wiki/doku.php?id=restore +const StructSize = 283 + +// RestoreData defines hashcat's .restore file +type RestoreData struct { + // Version is the hashcat version used to create the file + Version uint32 + // WorkingDirectory is the current working directory that hashcat was in at the time of this restore point. + // Hashcat will change to this directory. + WorkingDirectory string + // DictionaryPosition is the current poisition within the dictionary + DictionaryPosition uint32 + // MasksPosition is the current position within the list of masks + MasksPosition uint32 + // WordsPosition is the position within the dictionary/mask + WordsPosition uint64 + // ArgCount is the number of command line arguments passed into hashcat + ArgCount uint32 + + ArgvPointer uint64 + // Args contains the command line arguments + Args []string +} + +func (s *RestoreData) Write(w io.Writer) error { + if err := binary.Write(w, binary.LittleEndian, &s.Version); err != nil { + return err + } + + bwd := make([]byte, 256) + for i, r := range s.WorkingDirectory { + bwd[i] = byte(r) + } + + if _, err := w.Write(bwd); err != nil { + return err + } + + if err := binary.Write(w, binary.LittleEndian, &s.DictionaryPosition); err != nil { + return err + } + + if err := binary.Write(w, binary.LittleEndian, &s.MasksPosition); err != nil { + return err + } + + // padding + if _, err := w.Write(make([]byte, 0x4)); err != nil { + return err + } + + if err := binary.Write(w, binary.LittleEndian, &s.WordsPosition); err != nil { + return err + } + + if err := binary.Write(w, binary.LittleEndian, &s.ArgCount); err != nil { + return err + } + + // padding + if _, err := w.Write(make([]byte, 0x4)); err != nil { + return err + } + + if err := binary.Write(w, binary.LittleEndian, &s.ArgvPointer); err != nil { + return err + } + + for _, arg := range s.Args { + barg := []byte(arg) + if !bytes.HasSuffix(barg, []byte("\n")) { + barg = append(barg, 10) + } + if _, err := w.Write(barg); err != nil { + return err + } + } + + return nil +} + +func restoreParser(f io.ReadSeeker, rd *RestoreData) error { + if err := binary.Read(f, binary.LittleEndian, &rd.Version); err != nil { + return err + } + + buf := make([]byte, 256) + if err := binary.Read(f, binary.LittleEndian, &buf); err != nil { + return err + } + rd.WorkingDirectory = string(bytes.Trim(buf, "\x00")) + + if err := binary.Read(f, binary.LittleEndian, &rd.DictionaryPosition); err != nil { + return err + } + + if err := binary.Read(f, binary.LittleEndian, &rd.MasksPosition); err != nil { + return err + } + + // there's 4 bytes of padding here in the struct + if _, err := f.Seek(0x4, os.SEEK_CUR); err != nil { + return err + } + + if err := binary.Read(f, binary.LittleEndian, &rd.WordsPosition); err != nil { + return err + } + + if err := binary.Read(f, binary.LittleEndian, &rd.ArgCount); err != nil { + return err + } + + // more padding... + if _, err := f.Seek(0x4, os.SEEK_CUR); err != nil { + return err + } + + if err := binary.Read(f, binary.LittleEndian, &rd.ArgvPointer); err != nil { + return err + } + + rdr := bufio.NewReader(f) + arg, err := rdr.ReadString('\n') + for err != io.EOF { + rd.Args = append(rd.Args, strings.TrimSpace(arg)) + arg, err = rdr.ReadString('\n') + } + return nil +} + +// ReadRestoreFile reads the restore file from fp +func ReadRestoreFile(fp string) (rd RestoreData, err error) { + f, err := os.Open(fp) + if err != nil { + return rd, err + } + defer f.Close() + + err = restoreParser(f, &rd) + return +} + +// ReadRestoreBytes reads the restore file from the bytes passed in +func ReadRestoreBytes(b []byte) (rd RestoreData, err error) { + rdr := bytes.NewReader(b) + err = restoreParser(rdr, &rd) + return +} diff --git a/restoreutil/restoreutil_test.go b/restoreutil/restoreutil_test.go new file mode 100644 index 0000000..5547f77 --- /dev/null +++ b/restoreutil/restoreutil_test.go @@ -0,0 +1,53 @@ +package restoreutil + +import ( + "bytes" + "crypto/md5" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/assert" +) + +func testRestoreFileContents(rd RestoreData, t *testing.T) { + assert.Equal(t, uint32(0x15e), rd.Version) + assert.Equal(t, "/Users/cschmitt/Desktop", rd.WorkingDirectory) + assert.Equal(t, uint32(0x0), rd.DictionaryPosition) + assert.Equal(t, uint32(0x4), rd.MasksPosition) + assert.Equal(t, uint64(0xaf0000), rd.WordsPosition) + assert.Equal(t, uint32(8), rd.ArgCount) + assert.Equal(t, 8, len(rd.Args)) +} + +func TestReadRestoreFile(t *testing.T) { + rd, err := ReadRestoreFile("./testdata/unittest_example.restore") + assert.Nil(t, err) + testRestoreFileContents(rd, t) +} + +func TestRestoreBytes(t *testing.T) { + b, err := ioutil.ReadFile("./testdata/unittest_example.restore") + assert.Nil(t, err) + + rd, err := ReadRestoreBytes(b) + assert.Nil(t, err) + testRestoreFileContents(rd, t) +} + +func TestWrite(t *testing.T) { + b, err := ioutil.ReadFile("./testdata/unittest_example.restore") + assert.Nil(t, err) + + beforeXsum := md5.Sum(b) + + rd, err := ReadRestoreBytes(b) + assert.Nil(t, err) + + buf := new(bytes.Buffer) + // Write the restore file to a byte Buffer + err = rd.Write(buf) + assert.Nil(t, err) + + afterXsum := md5.Sum(buf.Bytes()) + assert.Equalf(t, beforeXsum, afterXsum, "checksum mismatch") +} diff --git a/restoreutil/testdata/unittest_example.restore b/restoreutil/testdata/unittest_example.restore new file mode 100644 index 0000000..35c5e0c Binary files /dev/null and b/restoreutil/testdata/unittest_example.restore differ diff --git a/testdata/mix_of_invalid_and_valid.hashes b/testdata/mix_of_invalid_and_valid.hashes new file mode 100644 index 0000000..b055d47 --- /dev/null +++ b/testdata/mix_of_invalid_and_valid.hashes @@ -0,0 +1,3 @@ +5d41402abc4b2a76b9719d911017c592 +lolnope +golangbestlang \ No newline at end of file diff --git a/testdata/one_md5_in_potfile.hashes b/testdata/one_md5_in_potfile.hashes new file mode 100644 index 0000000..a4f6cab --- /dev/null +++ b/testdata/one_md5_in_potfile.hashes @@ -0,0 +1,2 @@ +5d41402abc4b2a76b9719d911017c592 +6b34fe24ac2ff8103f6fce1f0da2ef57 \ No newline at end of file diff --git a/testdata/one_md5_in_potfile.potfile b/testdata/one_md5_in_potfile.potfile new file mode 100644 index 0000000..4f66b24 --- /dev/null +++ b/testdata/one_md5_in_potfile.potfile @@ -0,0 +1 @@ +5d41402abc4b2a76b9719d911017c592:hello \ No newline at end of file diff --git a/testdata/russian_test.dictionary b/testdata/russian_test.dictionary new file mode 100644 index 0000000..6715aa8 --- /dev/null +++ b/testdata/russian_test.dictionary @@ -0,0 +1,4 @@ +фыва +фыв +фы +ф \ No newline at end of file diff --git a/testdata/russian_test.hashes b/testdata/russian_test.hashes new file mode 100644 index 0000000..1324ad7 --- /dev/null +++ b/testdata/russian_test.hashes @@ -0,0 +1,4 @@ +7b903cd2cb84bf0df76f133ade2b1d09 +809336c0a3882d1f9865b50eaa4b6f9b +056f15cdde7ff06bdcdcdb44feadcd9e +2bfe4581ac6cf8ce4c3e7ee8f07f518b diff --git a/testdata/test_dictionary.txt b/testdata/test_dictionary.txt new file mode 100644 index 0000000..2f37a40 --- /dev/null +++ b/testdata/test_dictionary.txt @@ -0,0 +1,4 @@ +hello +world +chris +bob \ No newline at end of file diff --git a/testdata/two_md5.hashes b/testdata/two_md5.hashes new file mode 100644 index 0000000..f7f65f6 --- /dev/null +++ b/testdata/two_md5.hashes @@ -0,0 +1,2 @@ +5d41402abc4b2a76b9719d911017c592 +7d793037a0760186574b0282f2f435e7 \ No newline at end of file diff --git a/testdata/two_md5.potfile b/testdata/two_md5.potfile new file mode 100644 index 0000000..590c972 --- /dev/null +++ b/testdata/two_md5.potfile @@ -0,0 +1,2 @@ +5d41402abc4b2a76b9719d911017c592:hello +7d793037a0760186574b0282f2f435e7:world \ No newline at end of file diff --git a/validator.go b/validator.go new file mode 100644 index 0000000..ccb7e61 --- /dev/null +++ b/validator.go @@ -0,0 +1,109 @@ +package gocat + +// #include "wrapper.h" +import "C" +import ( + "fmt" + "strings" + "unsafe" +) + +// ValidationResult is the output from ValidateHashes and includes information about the hash file +type ValidationResult struct { + Valid bool + Errors []string + NumHashes uint32 + NumHashesUnique uint32 + NumSalts uint32 +} + +const errStr = "linter: %s failed with rv %d" + +// ValidateHashes is a linter that validates hashes before creating and executing a hashcat session +func ValidateHashes(pathToHashes string, hashType uint32) (*ValidationResult, error) { + var err error + var hashes *C.hashes_t + vr := &ValidationResult{ + Valid: true, + } + + hashpath := C.CString(pathToHashes) + defer C.free(unsafe.Pointer(hashpath)) + + validator := C.gocat_ctx_t{ + ctx: C.hashcat_ctx_t{}, + gowrapper: unsafe.Pointer(vr), + bValidateHashes: true, + } + + if retval := C.hashcat_init(&validator.ctx, (*[0]byte)(unsafe.Pointer(C.event))); retval != 0 { + err = fmt.Errorf(errStr, "hashcat_init", retval) + goto cleanup + } + + if retval := C.user_options_init(&validator.ctx); retval != 0 { + err = fmt.Errorf(errStr, "user_options_init", retval) + goto cleanup + } + + validator.ctx.user_options.hash_mode = C.u32(hashType) + validator.ctx.user_options_extra.hc_hash = hashpath + + if retval := C.hashconfig_init(&validator.ctx); retval != 0 { + err = fmt.Errorf(errStr, "hashconfig_init", retval) + goto cleanup + } + + // New in 5.1.X + C.hashes_init_filename(&validator.ctx) + + hashes = validator.ctx.hashes + // Load hashes + if retval := C.hashes_init_stage1(&validator.ctx); retval != 0 { + err = fmt.Errorf(errStr, "hashes_init_stage1", retval) + goto cleanup + } + + // Removes duplicates + hashes.hashes_cnt_orig = hashes.hashes_cnt + if retval := C.hashes_init_stage2(&validator.ctx); retval != 0 { + err = fmt.Errorf(errStr, "hashes_init_stage2", retval) + goto cleanup + } + +cleanup: + if validator.ctx.hashes != nil { + vr.NumHashes = uint32(validator.ctx.hashes.hashes_cnt_orig) + vr.NumHashesUnique = uint32(validator.ctx.hashes.digests_cnt) + vr.NumSalts = uint32(validator.ctx.hashes.salts_cnt) + } + + if &validator.ctx != nil { + C.hashcat_destroy(&validator.ctx) + } + + return vr, err +} + +//export validatorCallback +func validatorCallback(id uint32, hcCtx *C.hashcat_ctx_t, results unsafe.Pointer, buf unsafe.Pointer, len C.size_t) { + var r = (*ValidationResult)(results) + + switch id { + case C.EVENT_LOG_WARNING: + ectx := hcCtx.event_ctx + msg := C.GoStringN(&ectx.msg_buf[0], C.int(ectx.msg_len)) + if strings.Contains(msg, "kernel not found") || strings.Contains(msg, "falling back to") { + return + } + + if strings.Contains(msg, "Hashfile") && strings.Contains(msg, "on ") { + // strip out the filename in the warning as it's unnecessary for our purposes + onIndex := strings.Index(msg, "on ") + msg = msg[onIndex+3:] + } + + r.Valid = false + r.Errors = append(r.Errors, msg) + } +} diff --git a/validator_test.go b/validator_test.go new file mode 100644 index 0000000..191bde0 --- /dev/null +++ b/validator_test.go @@ -0,0 +1,48 @@ +package gocat + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateHashes(t *testing.T) { + for i, test := range []struct { + HashPathToTest string + HashType uint32 + IsValid bool + ExpectedHashes uint32 + ExpectedUniqueHashes uint32 + ExpectedSalts uint32 + ExpectedNumErrors int + }{ + { + HashPathToTest: "./testdata/mix_of_invalid_and_valid.hashes", + HashType: 0, + IsValid: false, + ExpectedHashes: 1, + ExpectedUniqueHashes: 1, + ExpectedSalts: 1, + ExpectedNumErrors: 2, + }, + { + HashPathToTest: "./testdata/two_md5.hashes", + HashType: 0, + IsValid: true, + ExpectedHashes: 2, + ExpectedUniqueHashes: 2, + ExpectedSalts: 1, + }, + } { + vr, err := ValidateHashes(test.HashPathToTest, test.HashType) + if err != nil { + assert.FailNow(t, "failed to initialize the validator") + } + + assert.Equalf(t, test.IsValid, vr.Valid, "failed equality check in test %d", i) + assert.Equalf(t, test.ExpectedHashes, vr.NumHashes, "failed equality check in test %d", i) + assert.Equalf(t, test.ExpectedUniqueHashes, vr.NumHashesUnique, "failed equality check in test %d", i) + assert.Equalf(t, test.ExpectedSalts, vr.NumSalts, "failed equality check in test %d", i) + assert.Equalf(t, test.ExpectedNumErrors, len(vr.Errors), "failed equality check in test %d", i) + } +} diff --git a/wrapper.c b/wrapper.c new file mode 100644 index 0000000..cb3e0ae --- /dev/null +++ b/wrapper.c @@ -0,0 +1,24 @@ +#include "wrapper.h" + +void event(const u32 id, hashcat_ctx_t *hashcat_ctx, const void *buf, const size_t len) +{ + gocat_ctx_t *worker_tuple = (gocat_ctx_t*)hashcat_ctx; + // call the validator callback if we're in the hash validation mode + if (worker_tuple->bValidateHashes) + { + validatorCallback(id, &worker_tuple->ctx, worker_tuple->gowrapper, (void*)buf, (size_t)len); + } + else + { + callback(id, &worker_tuple->ctx, worker_tuple->gowrapper, (void*)buf, (size_t)len); + } +} + +void freeargv(int argc, char **argv) +{ + for (int i = 0; i < argc; i++) + { + free(argv[i]); + } + free(argv); +} diff --git a/wrapper.h b/wrapper.h new file mode 100644 index 0000000..5ebeff0 --- /dev/null +++ b/wrapper.h @@ -0,0 +1,27 @@ +#ifndef GOHASHCAT_H_ +#define GOHASHCAT_H_ + +#include "common.h" +#include "types.h" +#include "memory.h" +#include "status.h" +#include "user_options.h" +#include "hashcat.h" +#include "potfile.h" +#include "thread.h" +#include "hashes.h" +#include "interface.h" + +typedef struct +{ + hashcat_ctx_t ctx; + void *gowrapper; + bool bValidateHashes; +} gocat_ctx_t; + +void callback(u32 id, hashcat_ctx_t *hashcat_ctx, void *wrapper, void *buf, size_t len); +void validatorCallback(u32 id, hashcat_ctx_t *hashcat_ctx, void *wrapper, void *buf, size_t len); +void event(const u32 id, hashcat_ctx_t *hashcat_ctx, const void *buf, const size_t len); +void freeargv(int argc, char **argv); + +#endif \ No newline at end of file