Skip to content

Instantly share code, notes, and snippets.

@JamesHovious
Last active October 5, 2021 16:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JamesHovious/1d6adb1dd6623af88735eeb5eba08eba to your computer and use it in GitHub Desktop.
Save JamesHovious/1d6adb1dd6623af88735eeb5eba08eba to your computer and use it in GitHub Desktop.
Extract a Hancitor configuration using only the Go standard library
package hancitor
// References:
// https://hub.gke2.mybinder.org/user/oalabs-lab-notes-4vubrm7f/notebooks/Hancitor/hancitor.ipynb
// https://www.youtube.com/watch?v=OQuRwpUTBpQ
// https://www.binarydefense.com/analysis-of-hancitor-when-boring-begets-beacon/
// https://github.com/kevoreilly/CAPEv2/blob/master/modules/processing/parsers/mwcp/Hancitor.py
import (
"bytes"
"crypto/rc4"
"crypto/sha1"
"debug/pe"
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"unicode"
)
// Config represents a parsed malware configuration
type Config struct {
Url []url.URL
CampaignID string
Family string
Raw json.RawMessage // any data that is not represented elsewhere in the struct can be put here
}
// Extract extracts configuration a HANCITOR PE
func Extract(mal *pe.File) (config.Config, error) {
if mal.Section(".data") == nil {
return config.Config{}, errors.New("invalid pointer to PE data section")
}
dataSection, err := mal.Section(".data").Data()
if err != nil {
return config.Config{}, err
}
// using hard coded offsets since debug/pe doesn't
// let you calculate them
hash := sha1.Sum(dataSection[16:24])
rc4Key := hash[:5]
// again, hard coded offsets
// 24 == starting after the key material
// 2000 == total size of config
conf, err := decryptConfig(rc4Key, dataSection[24:2000])
if err != nil {
return config.Config{}, err
}
return conf, nil
}
func decryptConfig(rc4Key, ciphertext []byte) (config.Config, error) {
cipher, err := rc4.NewCipher(rc4Key)
if err != nil {
return config.Config{}, err
}
// decrypt the config
cipher.XORKeyStream(ciphertext, ciphertext)
var conf config.Config
build := buildID(ciphertext)
r, err := json.Marshal(fmt.Sprintf(`{"rc4_key": "%v" }`, rc4Key))
if err != nil {
return config.Config{}, err
}
// remove the build_id from the config
minusBuild := strings.TrimPrefix(string(ciphertext), build)
// the config is a single pipe delimited string
c2s := strings.Split(minusBuild, "|")
for _, c2 := range c2s {
cleaned := string(bytes.Trim([]byte(c2), "\x00"))
u, err := url.Parse(cleaned)
if err != nil {
continue
}
if config.IsTLDValid(u.Host) {
conf.Url = append(conf.Url, *u)
}
}
conf.CampaignID = build
conf.Raw = r
conf.Family = "hancitor"
return conf, nil
}
func buildID(buf []byte) string {
f := func(c rune) bool {
return !unicode.IsLetter(c) && !unicode.IsNumber(c)
}
// parse out characters from the junk
fields := bytes.FieldsFunc(buf, f)
return string(fields[0]) + "_" + string(fields[1])
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment