reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
Apache License 2.0 — see the repository root LICENSE file for the full text.
|
||||
|
||||
Copyright the AnalystAIPack contributors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: resolving-dynamic-api-hashing
|
||||
description: 'Resolves dynamically hashed Windows API names by brute-forcing observed hash constants
|
||||
against a wordlist of API/DLL names using common malware hashing algorithms (ROR13, djb2, FNV,
|
||||
CRC32). Activates for requests to resolve API hashes, identify hashed imports, or reverse an API
|
||||
hashing routine.'
|
||||
domain: cybersecurity
|
||||
subdomain: reverse-engineering
|
||||
tags:
|
||||
- reverse-engineering
|
||||
- api-hashing
|
||||
- obfuscation
|
||||
- imports
|
||||
- deobfuscation
|
||||
version: 1.0.0
|
||||
author: analyst-ai-pack
|
||||
license: Apache-2.0
|
||||
mitre_attack:
|
||||
- T1027.007
|
||||
- T1106
|
||||
- T1140
|
||||
d3fend:
|
||||
- D3-SDA
|
||||
- D3-DA
|
||||
references:
|
||||
- 'MITRE ATT&CK T1027.007 Dynamic API Resolution — https://attack.mitre.org/techniques/T1027/007/'
|
||||
- 'Windows API Index — https://learn.microsoft.com/windows/win32/apiindex/windows-api-list'
|
||||
---
|
||||
|
||||
# Resolving Dynamic API Hashing
|
||||
|
||||
## When to Use
|
||||
|
||||
- A sample resolves APIs by hash (no plaintext import names) and you have the hash constants from
|
||||
disassembly.
|
||||
- You need to map each hash back to an API/DLL name to understand functionality.
|
||||
|
||||
**Do not use** this to execute the resolver — it computes hashes over a name wordlist statically
|
||||
and matches them to your observed constants.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- The hash constants observed in the binary and (optionally) a names wordlist.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Identify the algorithm
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py id 0x6A4ABC5B
|
||||
```
|
||||
|
||||
Computes the constant under each supported algorithm against a built-in seed list to suggest which
|
||||
algorithm/seed reproduces known API hashes.
|
||||
|
||||
### Step 2: Resolve hashes against a wordlist
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py resolve hashes.txt --algo ror13 --wordlist names.txt
|
||||
```
|
||||
|
||||
Brute-forces the provided hashes against the wordlist using the chosen algorithm and reports
|
||||
matches.
|
||||
|
||||
### Step 3: Annotate the disassembly
|
||||
|
||||
Label resolved call sites with their API names to recover behavior.
|
||||
|
||||
### Step 4: Document the routine
|
||||
|
||||
Record the algorithm, seed/initial value, and any key so the routine is reusable.
|
||||
|
||||
## Validation
|
||||
|
||||
- The chosen algorithm reproduces at least one known API hash (sanity anchor).
|
||||
- Resolved names are real exported symbols of plausible DLLs.
|
||||
- Unresolved hashes are reported, not silently dropped.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Wrong endianness/rotation direction producing no matches — try variants.
|
||||
- Algorithms seeded with a per-sample key; without the key, matches fail.
|
||||
- Case sensitivity: some routines hash uppercased names, others as-is.
|
||||
|
||||
## References
|
||||
|
||||
- See [`references/api-reference.md`](references/api-reference.md) for the resolver.
|
||||
- ATT&CK T1027.007 and the Windows API Index (linked in frontmatter).
|
||||
@@ -0,0 +1,37 @@
|
||||
# API Reference: Resolving Dynamic API Hashing
|
||||
|
||||
## Overview
|
||||
|
||||
`scripts/analyst.py` implements common malware API-hash algorithms (ROR13-add, djb2, FNV-1a, CRC32)
|
||||
and matches observed 32-bit hash constants against a names wordlist. It is pure computation over
|
||||
name strings and never loads or executes the sample.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| (stdlib) | — | binascii (CRC32), JSON, file IO |
|
||||
|
||||
## Core functions
|
||||
|
||||
### `ror13_add(name)`, `djb2(name)`, `fnv1a(name)`, `crc32(name)`
|
||||
The supported 32-bit hashing algorithms.
|
||||
|
||||
### `identify(value)`
|
||||
Tries each algorithm against a small seed API list to suggest which algorithm a constant came from.
|
||||
|
||||
### `resolve(hashes, algo, names)`
|
||||
Builds a hash→name table for the wordlist and resolves the provided hashes; reports unresolved.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py id 0x6A4ABC5B
|
||||
python scripts/analyst.py resolve hashes.txt --algo ror13 --wordlist names.txt
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
- MITRE ATT&CK T1027.007 Dynamic API Resolution — https://attack.mitre.org/techniques/T1027/007/
|
||||
- ROR13 PEB-walk shellcode hashing pattern (public shellcode/RE literature)
|
||||
- djb2 (Daniel J. Bernstein), FNV-1a (Fowler/Noll/Vo), CRC-32 (ISO 3309) standard definitions
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve dynamically hashed API names by brute-forcing hash constants over a wordlist.
|
||||
|
||||
Implements common malware API-hash algorithms (ROR13 add, djb2, FNV-1a, CRC32) and matches a list
|
||||
of observed 32-bit hash constants against a names wordlist. Pure computation over names; never
|
||||
loads or executes the sample.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import binascii
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_NAMES = [
|
||||
"LoadLibraryA", "GetProcAddress", "VirtualAlloc", "VirtualAllocEx", "VirtualProtect",
|
||||
"WriteProcessMemory", "CreateRemoteThread", "CreateProcessA", "WinExec", "ExitProcess",
|
||||
"GetModuleHandleA", "CreateFileA", "WriteFile", "ReadFile", "InternetOpenA",
|
||||
"InternetConnectA", "HttpSendRequestA", "URLDownloadToFileA", "RegSetValueExA",
|
||||
"CryptDecrypt", "kernel32.dll", "ntdll.dll", "wininet.dll", "advapi32.dll",
|
||||
]
|
||||
|
||||
|
||||
def ror32(v: int, n: int) -> int:
|
||||
return ((v >> n) | (v << (32 - n))) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def ror13_add(name: str) -> int:
|
||||
h = 0
|
||||
for ch in name.encode("ascii", "ignore"):
|
||||
h = ror32(h, 13)
|
||||
h = (h + ch) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def djb2(name: str) -> int:
|
||||
h = 5381
|
||||
for ch in name.encode("ascii", "ignore"):
|
||||
h = ((h * 33) + ch) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def fnv1a(name: str) -> int:
|
||||
h = 0x811C9DC5
|
||||
for ch in name.encode("ascii", "ignore"):
|
||||
h = ((h ^ ch) * 0x01000193) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def crc32(name: str) -> int:
|
||||
return binascii.crc32(name.encode("ascii", "ignore")) & 0xFFFFFFFF
|
||||
|
||||
|
||||
ALGOS = {"ror13": ror13_add, "djb2": djb2, "fnv1a": fnv1a, "crc32": crc32}
|
||||
|
||||
|
||||
def identify(value: int) -> dict:
|
||||
matches = {}
|
||||
for algo, fn in ALGOS.items():
|
||||
for name in DEFAULT_NAMES:
|
||||
if fn(name) == value:
|
||||
matches.setdefault(algo, []).append(name)
|
||||
return {"value": hex(value), "matches": matches,
|
||||
"note": "matched against a small seed list; use resolve with a full wordlist"}
|
||||
|
||||
|
||||
def resolve(hashes: list[int], algo: str, names: list[str]) -> dict:
|
||||
fn = ALGOS[algo]
|
||||
table = {fn(n): n for n in names}
|
||||
resolved = {hex(h): table.get(h) for h in hashes}
|
||||
return {
|
||||
"algorithm": algo,
|
||||
"resolved": {k: v for k, v in resolved.items() if v},
|
||||
"unresolved": [k for k, v in resolved.items() if not v],
|
||||
}
|
||||
|
||||
|
||||
def _read_hashes(path: str) -> list[int]:
|
||||
out = []
|
||||
for line in Path(path).read_text(encoding="utf-8-sig").splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
out.append(int(line, 0))
|
||||
return out
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Resolve API hashes")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
pid = sub.add_parser("id")
|
||||
pid.add_argument("value")
|
||||
pr = sub.add_parser("resolve")
|
||||
pr.add_argument("hashes_file")
|
||||
pr.add_argument("--algo", choices=list(ALGOS), required=True)
|
||||
pr.add_argument("--wordlist")
|
||||
args = parser.parse_args(argv)
|
||||
if args.cmd == "id":
|
||||
print(json.dumps(identify(int(args.value, 0)), indent=2))
|
||||
else:
|
||||
names = (Path(args.wordlist).read_text(encoding="utf-8-sig").split()
|
||||
if args.wordlist else DEFAULT_NAMES)
|
||||
print(json.dumps(resolve(_read_hashes(args.hashes_file), args.algo, names), indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user