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,102 @@
|
||||
---
|
||||
name: defeating-string-and-api-obfuscation
|
||||
description: 'Recovers obfuscated strings and resolves dynamically loaded APIs in malware:
|
||||
reversing XOR/stack-string/encoding schemes and mapping API-hash lookups back to function
|
||||
names. Activates for requests to deobfuscate strings, decode stack strings, or resolve
|
||||
API hashing in a binary.'
|
||||
domain: cybersecurity
|
||||
subdomain: reverse-engineering
|
||||
tags:
|
||||
- reverse-engineering
|
||||
- obfuscation
|
||||
- strings
|
||||
- api-hashing
|
||||
- xor
|
||||
- deobfuscation
|
||||
version: 1.0.0
|
||||
author: analyst-ai-pack
|
||||
license: Apache-2.0
|
||||
mitre_attack:
|
||||
- T1027
|
||||
- T1140
|
||||
- T1106
|
||||
d3fend:
|
||||
- D3-FCR
|
||||
- D3-FA
|
||||
references:
|
||||
- 'FLOSS (FLARE Obfuscated String Solver) — https://github.com/mandiant/flare-floss'
|
||||
- 'API hashing techniques (ROR13 / djb2) — https://learn.microsoft.com/windows/win32/api/'
|
||||
---
|
||||
|
||||
# Defeating String and API Obfuscation
|
||||
|
||||
## When to Use
|
||||
|
||||
- A binary has few readable strings because they are XOR/stack-encoded or built at runtime.
|
||||
- The import table is sparse because APIs are resolved by hash at runtime.
|
||||
- You need to recover C2 URLs, paths, and the real API set to understand behavior.
|
||||
|
||||
**Do not use** plain `strings` and conclude "no indicators" — modern malware hides strings;
|
||||
absence of readable strings is itself a sign of obfuscation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A disassembler/decompiler to read the decode routine, plus the bundled XOR/hash tooling.
|
||||
- Optionally FLOSS for automated stack/decoded-string recovery.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Recognize the obfuscation type
|
||||
|
||||
```text
|
||||
Stack strings : bytes mov'd to the stack one/few at a time, then used
|
||||
Single-byte XOR: a loop XORing a buffer with a constant
|
||||
Multi-byte/RC4 : a keyed stream over a blob
|
||||
API hashing : a hash compared against export-name hashes to resolve functions
|
||||
```
|
||||
|
||||
### Step 2: Recover XOR-encoded strings
|
||||
|
||||
If you find the key and ciphertext, decode directly. The script brute-forces single-byte XOR
|
||||
and surfaces readable results:
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py xor-strings sample.bin
|
||||
```
|
||||
|
||||
### Step 3: Reconstruct stack strings
|
||||
|
||||
Read the decompiler to collect the byte sequence assembled on the stack and reassemble it. For
|
||||
volume, FLOSS emulates and extracts these automatically.
|
||||
|
||||
### Step 4: Resolve API hashing
|
||||
|
||||
Identify the hash algorithm (often ROR13/ROR7 additive, or djb2). Precompute hashes for known
|
||||
export names and match the constants in the binary back to functions:
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py api-hash --algo ror13 --hash 0x726774C
|
||||
```
|
||||
|
||||
### Step 5: Reannotate and extract IOCs
|
||||
|
||||
Apply recovered strings and API names back in the disassembler and extract the now-visible
|
||||
URLs, paths, and behavior.
|
||||
|
||||
## Validation
|
||||
|
||||
- Decoded strings are meaningful (URLs, DLL/API names, paths), not random bytes.
|
||||
- Resolved API names match the calls' usage in the surrounding code.
|
||||
- The recovered API set explains behavior seen dynamically.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- Assuming single-byte XOR when it is keyed/RC4; check key length and the decode loop.
|
||||
- Using the wrong hash algorithm/seed and getting no matches — verify against a known function.
|
||||
- Recovering strings but not feeding them back into the analysis.
|
||||
|
||||
## References
|
||||
|
||||
- See [`references/api-reference.md`](references/api-reference.md) for the XOR and API-hash
|
||||
tools.
|
||||
- FLOSS and API-hashing references (linked in frontmatter).
|
||||
@@ -0,0 +1,45 @@
|
||||
# API Reference: Defeating String and API Obfuscation
|
||||
|
||||
## Overview
|
||||
|
||||
`scripts/analyst.py` provides two deobfuscation utilities: brute-forcing single-byte XOR to
|
||||
surface readable strings, and resolving API-name hashes (ROR13/ROR7/djb2) back to function
|
||||
names. Both read input only and never execute the sample.
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| (stdlib) | — | regex, hashing arithmetic |
|
||||
|
||||
## External tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| FLOSS | Automated stack-string and decoded-string recovery via emulation |
|
||||
| Ghidra / IDA | Read the decode routine and reapply recovered names |
|
||||
|
||||
## Core functions
|
||||
|
||||
### `xor_strings(data, min_hits)`
|
||||
Brute-forces single-byte XOR keys and returns keys that yield several interesting readable
|
||||
strings (URLs, DLL/API names, paths).
|
||||
|
||||
### `resolve_hash(algo, target, names)`
|
||||
Returns candidate API names whose hash (ROR13/ROR7/djb2) equals the target constant.
|
||||
|
||||
### `hash_ror13` / `hash_ror7` / `hash_djb2`
|
||||
Implementations of the common API-hashing algorithms.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
python scripts/analyst.py xor-strings sample.bin
|
||||
python scripts/analyst.py api-hash --algo ror13 --hash 0x726774C
|
||||
```
|
||||
|
||||
## Sources
|
||||
|
||||
- FLOSS (FLARE Obfuscated String Solver) — https://github.com/mandiant/flare-floss
|
||||
- ROR13 API-hashing technique (widely used in shellcode/loaders)
|
||||
- djb2 string hash (Daniel J. Bernstein)
|
||||
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recover XOR-obfuscated strings and resolve API hashes.
|
||||
|
||||
Two original utilities for deobfuscation triage:
|
||||
- ``xor-strings``: brute-force single-byte XOR over a file and surface readable strings.
|
||||
- ``api-hash``: compute common API-name hashes (ROR13/ROR7/djb2) and match a target constant
|
||||
against a provided or built-in list of export names.
|
||||
|
||||
Reads bytes/text only; never executes the sample.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_READABLE = re.compile(rb"[\x20-\x7e]{5,}")
|
||||
|
||||
COMMON_APIS = [
|
||||
"LoadLibraryA", "GetProcAddress", "VirtualAlloc", "VirtualProtect",
|
||||
"WriteProcessMemory", "CreateRemoteThread", "CreateProcessA", "WinExec",
|
||||
"InternetOpenA", "InternetConnectA", "HttpSendRequestA", "URLDownloadToFileA",
|
||||
"RegSetValueExA", "CreateFileA", "WriteFile", "ReadFile", "CloseHandle",
|
||||
"GetProcAddress", "ExitProcess", "Sleep", "WSAStartup", "connect", "send", "recv",
|
||||
]
|
||||
|
||||
|
||||
def _ror(val: int, bits: int, width: int = 32) -> int:
|
||||
mask = (1 << width) - 1
|
||||
val &= mask
|
||||
return ((val >> bits) | (val << (width - bits))) & mask
|
||||
|
||||
|
||||
def hash_ror13(name: str) -> int:
|
||||
h = 0
|
||||
for ch in name.encode() + b"\x00":
|
||||
h = (_ror(h, 13) + ch) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def hash_ror7(name: str) -> int:
|
||||
h = 0
|
||||
for ch in name.encode():
|
||||
h = (_ror(h, 7) + ch) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
def hash_djb2(name: str) -> int:
|
||||
h = 5381
|
||||
for ch in name.encode():
|
||||
h = ((h * 33) + ch) & 0xFFFFFFFF
|
||||
return h
|
||||
|
||||
|
||||
_ALGOS = {"ror13": hash_ror13, "ror7": hash_ror7, "djb2": hash_djb2}
|
||||
|
||||
|
||||
def xor_strings(data: bytes, min_hits: int = 3) -> list[dict]:
|
||||
results = []
|
||||
for key in range(1, 256):
|
||||
decoded = bytes(b ^ key for b in data)
|
||||
hits = [m.group(0).decode("ascii", "replace")
|
||||
for m in _READABLE.finditer(decoded)]
|
||||
interesting = [s for s in hits
|
||||
if re.search(r"https?://|\.dll|\.exe|\\|[A-Z][a-z]+[A-Z]", s)]
|
||||
if len(interesting) >= min_hits:
|
||||
results.append({"key": hex(key), "samples": interesting[:20],
|
||||
"count": len(interesting)})
|
||||
return sorted(results, key=lambda r: r["count"], reverse=True)[:5]
|
||||
|
||||
|
||||
def resolve_hash(algo: str, target: int, names: list[str]) -> list[str]:
|
||||
fn = _ALGOS[algo]
|
||||
return [n for n in names if fn(n) == (target & 0xFFFFFFFF)]
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="Defeat string/API obfuscation")
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
p_x = sub.add_parser("xor-strings", help="brute single-byte XOR for readable strings")
|
||||
p_x.add_argument("file")
|
||||
|
||||
p_h = sub.add_parser("api-hash", help="match an API hash to a function name")
|
||||
p_h.add_argument("--algo", choices=sorted(_ALGOS), required=True)
|
||||
p_h.add_argument("--hash", required=True, help="target hash, e.g. 0x726774C")
|
||||
p_h.add_argument("--names", help="optional file of candidate API names (one per line)")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
if args.cmd == "xor-strings":
|
||||
data = Path(args.file).read_bytes()
|
||||
print(json.dumps(xor_strings(data), indent=2))
|
||||
return 0
|
||||
|
||||
names = COMMON_APIS
|
||||
if args.names:
|
||||
names = [n.strip() for n in Path(args.names).read_text().splitlines() if n.strip()]
|
||||
target = int(args.hash, 0)
|
||||
matches = resolve_hash(args.algo, target, names)
|
||||
print(json.dumps({"algo": args.algo, "hash": hex(target),
|
||||
"matches": matches}, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user