Cryptographic hashes & ROR13

← hash index · docs/index

TL;DR

One-shot hex-string wrappers around crypto/md5, crypto/sha1, crypto/sha256, crypto/sha512, plus the ROR13 algorithm used by shellcode for plaintext-free Win32 API resolution. Pure Go, cross-platform, no system interaction.

Primer

Two distinct use cases share this file:

The cryptographic wrappers (MD5, SHA1, SHA256, SHA512) exist because Go's stdlib returns [N]byte arrays — convenient for machines, awkward for logs, command-line output, and string-keyed maps. The wrappers compress the boilerplate to one call and produce lower-case hex strings.

ROR13 is the canonical shellcode hash. Implants resolve Win32 APIs without keeping plaintext function names in the binary by walking the PE export directory of a loaded module and comparing each export name's ROR13 hash against precomputed targets. The trailing-null variant ROR13Module matches the convention used to hash module names from LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer. win/api.ResolveByHash consumes both.

The fuzzy hashes (ssdeep, TLSH) live in a separate page — fuzzy-hashing.md.

How it works

Cryptographic hashes

flowchart LR
    DATA[input bytes] --> H{algorithm}
    H -->|MD5| M[16-byte digest]
    H -->|SHA1| S1[20-byte digest]
    H -->|SHA256| S2[32-byte digest]
    H -->|SHA512| S5[64-byte digest]
    M --> HEX[lower-case hex string]
    S1 --> HEX
    S2 --> HEX
    S5 --> HEX

ROR13

flowchart LR
    NAME[function name] --> ITER[for each byte b]
    ITER --> ROT[hash = ror32 hash, 13]
    ROT --> ADD[hash += b]
    ADD --> NEXT{more bytes?}
    NEXT -->|yes| ITER
    NEXT -->|no| OUT[uint32 hash]

ROR13Module adds a trailing null byte to the input, then hashes — mirroring the wide-string traversal a PEB-walk shellcode performs over the unicode BaseDllName.

The arithmetic per byte:

$$ \text{hash}_{i+1} = \big(\text{hash}_i \mathbin{\text{ror}} 13\big) + b_i \mod 2^{32} $$

starting at $\text{hash}_0 = 0$. Pure 32-bit unsigned arithmetic, easy to encode in a few shellcode bytes.

API Reference

MD5(data []byte) string

godoc

Lower-case hex digest of md5.Sum(data). 32 hex characters.

[!CAUTION] MD5 is collision-broken. Use only for non-security identifiers (cache keys, log correlation). Never for integrity checks.

SHA1(data []byte) string

godoc

Lower-case hex digest. 40 hex characters.

[!WARNING] SHA-1 is also collision-broken (SHAttered, 2017). Prefer SHA-256.

SHA256(data []byte) string

godoc

Lower-case hex digest. 64 hex characters. The default integrity hash.

SHA512(data []byte) string

godoc

Lower-case hex digest. 128 hex characters. Use when truncation-resistant output is required.

ROR13(name string) uint32

godoc

Compute the 32-bit ROR13 hash of name. Case-sensitive. Used to match Win32 export names exactly as they appear in the export directory.

Example output: ROR13("LoadLibraryA") == 0xec0e4e8e.

ROR13Module(name string) uint32

godoc

Same as ROR13 but appends a null terminator before hashing — matches the convention that PEB-walk shellcode uses when hashing module names from LDR_DATA_TABLE_ENTRY.BaseDllName.Buffer.

Examples

Simple

fmt.Println(hash.SHA256([]byte("payload")))
// 239f59ed55e737c77147cf55ad0c1b030b6d7ee748a7426952f9b852d5a935e5

fmt.Printf("%#x\n", hash.ROR13("LoadLibraryA"))
// 0xec0e4e8e

See ExampleSHA256, ExampleROR13, ExampleROR13Module in hash_example_test.go.

Composed (precompute API hashes for a resolver)

import "github.com/oioio-space/maldev/hash"

// Precomputed table for the resolver to consume.
var apiHashes = map[string]uint32{
    "LoadLibraryA":    hash.ROR13("LoadLibraryA"),
    "GetProcAddress":  hash.ROR13("GetProcAddress"),
    "VirtualAlloc":    hash.ROR13("VirtualAlloc"),
    "VirtualProtect":  hash.ROR13("VirtualProtect"),
}

Advanced (hash + win/api.ResolveByHash)

import (
    "github.com/oioio-space/maldev/hash"
    "github.com/oioio-space/maldev/win/api"
)

// At runtime — no plaintext "VirtualAlloc" string in the binary.
addr, err := api.ResolveByHash(
    hash.ROR13Module("kernel32.dll"),
    hash.ROR13("VirtualAlloc"),
)

Complex (full resolver bootstrap pipeline)

import (
    "github.com/oioio-space/maldev/hash"
    "github.com/oioio-space/maldev/win/api"
)

type Resolver struct {
    handle uintptr
}

func NewResolver(moduleHash uint32) (*Resolver, error) {
    h, err := api.GetModuleHandleByHash(moduleHash)
    if err != nil { return nil, err }
    return &Resolver{handle: h}, nil
}

func (r *Resolver) Resolve(funcName string) (uintptr, error) {
    return api.GetProcAddressByHash(r.handle, hash.ROR13(funcName))
}

func main() {
    k32, _ := NewResolver(hash.ROR13Module("kernel32.dll"))
    valloc, _ := k32.Resolve("VirtualAlloc")
    _ = valloc
}

OPSEC & Detection

ArtefactWhere defenders look
Hex strings (especially SHA-256-shaped 64-char) in process memoryYARA over RW pages — hash strings are themselves a tell
Constant 0xec0e4e8e-class 32-bit values stored in .rdataStatic analysis: known-API ROR13 hash tables are publicly catalogued (e.g. ror13_hashes.csv from various reversing tools)
Absence of LoadLibraryA / GetProcAddress plaintext in IAT despite using the APIsDefenders flag "no IAT entries for kernel32 but a kernel32 handle is held"
ROR13 resolution loop signature (ror eax, 13; add eax, ebx) in .textCapa, IDA signature plugins, MAEC ML classifiers

D3FEND counters:

  • D3-SEA — static EXE analysis catches the hash table or the ROR13 loop.
  • D3-PSA — flags processes that resolve APIs after a delay (typical of packers).

Hardening: spread API resolution across the binary's lifetime rather than batching at startup; randomise hash constants per build (a salt fed into ROR13's initial state); pair with sleep-masking so the resolved-address table does not sit decrypted in heap.

MITRE ATT&CK

T-IDNameSub-coverageD3FEND counter
T1027Obfuscated Files or InformationROR13 API hashing — no plaintext API namesD3-SEA
T1027.007Dynamic API ResolutionROR13 resolver patternD3-SEA

The cryptographic hash wrappers themselves are utility — no MITRE mapping.

Limitations

  • MD5 and SHA-1 are broken. Avoid for any integrity / signature use case. The package keeps them only because some legacy formats (e.g. PE Authenticode V1, NTLM) require MD5/SHA-1.
  • ROR13 is case-sensitive. Hash mismatches between LoadLibraryA and loadlibrarya are silent — you'll fail to resolve and the call returns nil. Use ROR13Module for module names where Windows is case-insensitive (the function takes care of the null suffix; case still matters).
  • No streaming API. Every wrapper takes the whole buffer. For multi-GB inputs, use crypto/sha256.New() directly and io.Copy into it.

See also