From ce97d32213d0593ea20aee594fbd435c6cdb9175 Mon Sep 17 00:00:00 2001 From: Christopher Schmitt Date: Thu, 13 Jun 2019 15:23:38 -0600 Subject: [PATCH] Initial commit from https://github.com/fireeye/gocrack --- .gitignore | 1 + LICENSE.txt | 21 ++ README.md | 24 ++ ctypes.go | 20 ++ doc.go | 7 + go.mod | 5 + go.sum | 7 + gocat.go | 307 ++++++++++++++++++ gocat_posix_test.go | 97 ++++++ gocat_status.go | 163 ++++++++++ gocat_test.go | 249 ++++++++++++++ handlers.go | 134 ++++++++ handlers_test.go | 55 ++++ hcargp/gocat_hashcat_options.go | 189 +++++++++++ hcargp/gocat_hashcat_options_test.go | 87 +++++ reentrant_patch_posix.go | 48 +++ reentrant_patch_windows.go | 11 + restoreutil/restoreutil.go | 161 +++++++++ restoreutil/restoreutil_test.go | 53 +++ restoreutil/testdata/unittest_example.restore | Bin 0 -> 410 bytes testdata/mix_of_invalid_and_valid.hashes | 3 + testdata/one_md5_in_potfile.hashes | 2 + testdata/one_md5_in_potfile.potfile | 1 + testdata/russian_test.dictionary | 4 + testdata/russian_test.hashes | 4 + testdata/test_dictionary.txt | 4 + testdata/two_md5.hashes | 2 + testdata/two_md5.potfile | 2 + validator.go | 109 +++++++ validator_test.go | 48 +++ wrapper.c | 24 ++ wrapper.h | 27 ++ 32 files changed, 1869 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 ctypes.go create mode 100644 doc.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 gocat.go create mode 100644 gocat_posix_test.go create mode 100644 gocat_status.go create mode 100644 gocat_test.go create mode 100644 handlers.go create mode 100644 handlers_test.go create mode 100644 hcargp/gocat_hashcat_options.go create mode 100644 hcargp/gocat_hashcat_options_test.go create mode 100644 reentrant_patch_posix.go create mode 100644 reentrant_patch_windows.go create mode 100644 restoreutil/restoreutil.go create mode 100644 restoreutil/restoreutil_test.go create mode 100644 restoreutil/testdata/unittest_example.restore create mode 100644 testdata/mix_of_invalid_and_valid.hashes create mode 100644 testdata/one_md5_in_potfile.hashes create mode 100644 testdata/one_md5_in_potfile.potfile create mode 100644 testdata/russian_test.dictionary create mode 100644 testdata/russian_test.hashes create mode 100644 testdata/test_dictionary.txt create mode 100644 testdata/two_md5.hashes create mode 100644 testdata/two_md5.potfile create mode 100644 validator.go create mode 100644 validator_test.go create mode 100644 wrapper.c create mode 100644 wrapper.h 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 0000000000000000000000000000000000000000..35c5e0c11652bc4ee71c0d388f03b3f1d252e0e7 GIT binary patch literal 410 zcmd^5F%H5o5DO9my1%4BVq#$93ye^ml0%8wq*Z;D*mx4(;seZ3P{qI(@MKH2CHu56 z#vLF7oQID&sk*(O(XKcDgynba?ZZ~HzCX6d=hc|Vh@O?Kg#f85yRl4P3AC0A=e`xP oOdGP9)|AIqQ~izz==a@fu5ySxNN_>UJo#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