rsleigh 0.4.0

SLEIGH (.slaspec) parser and Rust decoder/P-code emitter codegen — Ghidra-compatible disassembly in pure Rust
Documentation
# CLI triage flags

Three high-level flags ship as a malware-triage front end on top of the
decompiler. Each works on PE32 / PE32+ / ELF / Mach-O where applicable
and is usable both interactively (human-readable output) and from
pipelines (`--json`).

| Flag | Purpose | JSON | Aux flag |
|------|---------|------|----------|
| `--ioc` | Indicators of compromise (URLs, IPs, paths, registry, mutexes, secrets) | yes ||
| `--sigcheck` | Authenticode signature parse (signer, issuer, timestamp, chain) | yes ||
| `--resources` | PE resource directory walk; payload extraction | yes | `--dump <DIR>` |

All three are independent of the decoder/decompiler pipeline and run in
constant time relative to binary size — they only walk file structure
and string runs, never the SSA passes. Use them as the first call on
any unknown sample before deciding whether to invest in `--all`
decompilation.

---

## `--ioc` — Indicator-of-compromise extraction

```
rsleigh <binary> --ioc           # human-readable
rsleigh <binary> --ioc --json    # pipeline-ready
```

Scans ASCII (≥6 chars) and UTF-16LE (≥4 chars) string runs out of the
raw image and bins matches into seven categories:

- **URLs**`http://`, `https://`, `ftp://`. PE security-blob DER tail
  bytes are trimmed off cert URLs (`http://...crl0E``http://...crl`).
- **IPv4** — octet-validated. Rejects `.NET`-version-style literals
  (`4.0.0.0`, `1.0.0.0`) by counting zero octets — real routable IPs
  almost never have ≥2 zero octets.
- **Domains** — TLD-anchored against a curated list. Pascal-case
  namespace tokens (`System.IO`, `MyApplication.app`) and tokens
  containing `/`, `\`, or `:` are rejected.
- **Paths** — Windows drive-letter paths (`C:\…`, uppercase only),
  `%ENVVAR%` paths (env-var name must be ≥2 uppercase chars to avoid
  `%s` / `%d` printf noise), Unix `/tmp`, `/var`, `/etc`, `/usr`,
  `/home`, `/root`, `/dev`, `/proc`. Each path is validated to contain
  only path-legal characters end to end.
- **Registry**`HKEY_*`, `HKLM\`, `HKCU\`, `HKCR\` prefix matches.
- **Mutexes / named objects**`Global\`, `Local\`, `Session\`,
  `BaseNamedObjects\`.
- **Secret-like strings**`password=`, `bearer `, `api_key=`,
  `client_secret=`, `private_key=`, `ssh-rsa `, `-----BEGIN PRIVATE`.
  `.NET` assembly-identity strings are filtered out (otherwise every
  `PublicKeyToken=…` line lights up).

Output is `BTreeSet`-deduped and sorted within each category.

### Example

```
rsleigh ~/Downloads/sample.exe --ioc
```

```
=== IOCs from /Users/.../sample.exe ===

URLs (12)
  http://178.16.54.109/grab.exe
  http://178.16.54.109/xmr.exe
  ...

IPv4 (1)
  178.16.54.109

Paths (3)
  %APPDATA%\2353253532535.txt
  %TEMP%\d3333333333333333333.txt
  %s\%d%d.exe

Mutexes/Named Objects (2)
  Global\WixWaitForEventFail
  Global\WixWaitForEventSucceed

Total: 18 indicators
```

### `--json` schema

```json
{
  "binary":   "<path>",
  "urls":     ["..."],
  "ips":      ["..."],
  "domains":  ["..."],
  "paths":    ["..."],
  "registry": ["..."],
  "mutexes":  ["..."],
  "secrets":  ["..."]
}
```

### Known limitations

- `unicode.to`-style Go internal package paths can survive the domain
  filter when the second label happens to be a real TLD. Acceptable
  noise; rare in practice outside Go binaries.
- Truncated path runs (`/dev/nulH`, `/etc/locH` from Go binaries where
  the next byte after the path is one consistent uppercase letter)
  appear with a junk trailing char. Hard to fix textually without
  cutting real paths.

---

## `--sigcheck` — Authenticode signature parse

```
rsleigh <binary> --sigcheck             # human-readable
rsleigh <binary> --sigcheck --json      # pipeline-ready
```

Parses the PE Security data directory and surfaces the parts of an
Authenticode signature an analyst actually wants on first look:

- Signed yes/no (presence of a non-zero Security directory entry)
- `WIN_CERTIFICATE` header (size, revision, type)
- **Signer CN** — first non-CA-like Subject `commonName` in the chain
- **Issuer CN** — the CA-like CN immediately preceding the signer
- **Signing time** — UTCTime / GeneralizedTime decoded to
  `YYYY-MM-DD HH:MM:SS UTC`
- **Counter-signature timestamp signer** when present
- Full chain of CNs in DER scan order

### Implementation notes

Hand-rolled DER walker, no new ASN.1 / CMS dependency. Pattern-matches
three fixed OID prefixes:

| OID | Encoded prefix | Meaning |
|-----|----------------|---------|
| `2.5.4.3` | `06 03 55 04 03` | `commonName` |
| `1.2.840.113549.1.9.5` | `06 09 2A 86 48 86 F7 0D 01 09 05` | `signingTime` |
| `1.2.840.113549.1.9.6` | `06 09 2A 86 48 86 F7 0D 01 09 06` | `counterSignature` |

Strings decoded from the standard text tags: `0x13` `PrintableString`,
`0x0C` `UTF8String`, `0x16` `IA5String`, `0x14` `T61String`, `0x1E`
`BMPString` (UTF-16BE). Times handle both `UTCTime` (`YYMMDDHHMMSSZ`,
20YY heuristic for two-digit years <50) and `GeneralizedTime`
(`YYYYMMDDHHMMSSZ`).

The leaf-signer heuristic is: the first commonName in the chain that
does **not** match `" CA"` / `"Code Signing"` / `"Root"` /
`"Time(s)tamping"`. Intermediate / root certs almost always carry one
of those tokens; publisher leaf certs carry the company / product
name. The issuer is the most recent CA-like CN appearing **before**
the signer in scan order.

### Example

```
rsleigh ~/Downloads/ScreenConnect.ClientSetup.exe --sigcheck
```

```
=== Authenticode signature for /Users/.../ScreenConnect.ClientSetup.exe ===

  Cert blob size:  93984 bytes  (revision 0x0200, type 0x0002)
  Signer CN:       Connectwise, LLC
  Issuer CN:       DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1
  Signing time:    2025-04-08 18:37:56 UTC

  Cert chain CNs (6):
    [0] DigiCert Trusted Root G4
    [1] DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1
    [2] Connectwise, LLC
    [3] DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA
    [4] DigiCert Timestamp 2024
    [5] DigiCert Assured ID Root CA
```

For an unsigned PE / ELF / Mach-O:

```
=== Authenticode signature for /tmp/bed ===

UNSIGNED — no PE Security directory entry, or directory was empty.
```

### `--json` schema

```json
{
  "binary":              "<path>",
  "signed":              true,
  "signer_cn":           "Connectwise, LLC",
  "issuer_cn":           "DigiCert ...",
  "signing_time":        "2025-04-08 18:37:56 UTC",
  "timestamp_signer_cn": null,
  "all_cns":             ["...", "..."],
  "cert_blob_size":      93984,
  "win_cert_revision":   "0x0200",
  "win_cert_type":       "0x0002"
}
```

### Known limitations

- Cryptographic signature **validity** is not verified — `--sigcheck`
  only parses the structure and extracts the names. Pair with
  `signtool verify` / `osslsigncode verify` to confirm the signature
  hashes the binary correctly and chains to a trust anchor. Forged
  / mismatched signatures will still print signer info.
- Counter-signature timestamp signer extraction is best-effort and
  depends on OID order in the blob; it is `null` more often than the
  Microsoft / DigiCert chains print it.

---

## `--resources` — PE resource directory walk

```
rsleigh <binary> --resources                       # listing only
rsleigh <binary> --resources --json                # JSON
rsleigh <binary> --resources --dump /tmp/out       # extract every blob
```

Walks the three-level PE resource directory tree
(TYPE → NAME/ID → LANGUAGE → DATA_ENTRY) using only spec-defined
offsets — no `goblin` resource dependency. Surfaces every embedded
resource with the type-name decoded from the standard `RT_*` table.

Recognized type IDs:

| ID | Name | ID | Name |
|----|------|----|------|
|  1 | CURSOR        |  3 | ICON |
|  2 | BITMAP        |  4 | MENU |
|  5 | DIALOG        |  6 | STRING |
|  7 | FONTDIR       |  8 | FONT |
|  9 | ACCELERATOR   | 10 | RCDATA |
| 11 | MESSAGETABLE  | 12 | GROUP_CURSOR |
| 14 | GROUP_ICON    | 16 | VERSION |
| 17 | DLGINCLUDE    | 19 | PLUGPLAY |
| 20 | VXD           | 21 | ANICURSOR |
| 22 | ANIICON       | 23 | HTML |
| 24 | MANIFEST      | other | `TYPE_<n>` |

Named (non-numeric) types are read from the resource directory string
table and printed verbatim.

### Preview heuristics

For each resource, a one-line preview is printed. Magic-byte sniffs
fire first and produce a tagged annotation:

| Bytes | Preview |
|-------|---------|
| `MZ` | `[embedded PE/EXE, N bytes]` |
| `D0 CF 11 E0 A1 B1 1A E1` | `[OLE compound (likely MSI), N bytes]` |
| `MSCF` | `[CAB archive, N bytes]` |
| `89 PNG …` | `[PNG image, N bytes]` |
| `FF D8 FF` | `[JPEG image, N bytes]` |

If no magic matches, type-specific preview takes over:

- `RT_MANIFEST` — first 80 chars of the XML (control chars stripped)
- `RT_VERSION` — printable UTF-16LE run from `VS_VERSIONINFO`
- everything else — first 32 bytes hex + ASCII gutter

### `--dump <DIR>` extraction

Writes every resource blob to disk under
`<DIR>/<TYPE>_<id>_<lang>.bin`. Stable naming so the same input
always produces the same files. Useful for:

- Pulling embedded payloads out of installer bootstrappers and
  feeding them back through `rsleigh` for nested triage
- Feeding extracted CABs / MSIs into a downstream unpacker
- Recovering icons / manifests for visual inspection

### Example

```
rsleigh ~/Downloads/ScreenConnect.ClientSetup.exe --resources --dump /tmp/sc
```

```
=== Resources for /Users/.../ScreenConnect.ClientSetup.exe ===

6 entries

type           id                       lang       size  preview
------------------------------------------------------------------------------
FILES          SCREENCONNECT.CORE,...   0       550912  [embedded PE/EXE, 550912 bytes]
FILES          SCREENCONNECT.WINDOWS... 0      1729024  [embedded PE/EXE, 1729024 bytes]
FILES          SCREENCONNECT.WINDOWSI.. 0       109568  [embedded PE/EXE, 109568 bytes]
FILES          _ENTRYPOINT              0      3072792  [embedded PE/EXE, 3072792 bytes]
FILES          _RESOLVER                0         5632  [embedded PE/EXE, 5632 bytes]
MANIFEST       #1                       1033       392  <?xml version='1.0' encoding...

Resources dumped to /tmp/sc/
```

`file(1)` on the dumped blobs confirms five valid `.NET PE32`
assemblies plus an XML manifest.

### `--json` schema

```json
{
  "binary":         "<path>",
  "has_resources": true,
  "count":         6,
  "entries": [
    {
      "type":        "FILES",
      "type_id":     0,
      "id":          "_ENTRYPOINT",
      "id_raw":      0,
      "lang":        0,
      "rva":         "0x...",
      "file_offset": "0x...",
      "size":        3072792,
      "preview":    "[embedded PE/EXE, 3072792 bytes]"
    }
  ]
}
```

For binaries without a resource directory:

```json
{ "binary": "<path>", "has_resources": false }
```

### Known limitations

- Three-level walk only — does not recurse into multi-level directory
  structures beyond TYPE/NAME/LANGUAGE (no real-world PE uses deeper).
- No `RT_VERSION` field-by-field decode (no `FileVersion`,
  `ProductVersion` extraction yet); only a flattened printable preview.
- `--dump` filenames replace nothing; if a named ID contains a path
  separator (rare), the file write may fail. Sanitize the dump dir
  before re-running on different binaries.

---

## Recommended triage workflow

For an unknown PE binary picked up in incident response:

```bash
# 1. What is it, who signed it, when?
rsleigh sample.exe --sigcheck

# 2. Where does it reach out / what does it touch?
rsleigh sample.exe --ioc

# 3. Does it carry embedded payloads we should pull out?
rsleigh sample.exe --resources --dump /tmp/sample-rsrc

# 4. If yes — recurse. Each extracted blob is a fresh sample.
for blob in /tmp/sample-rsrc/*.bin; do
  rsleigh "$blob" --sigcheck --json
done

# 5. Decompile the suspicious ones.
rsleigh sample.exe --vulnscan
rsleigh sample.exe 0x401000 --disasm
```

For pipeline ingestion, replace each command with `--json` and pipe
into `jq` / your enrichment layer.

## See also

- `docs/features.md` — broader analysis catalog
- `docs/decompiler-passes.md` — pipeline internals
- `docs/architectures.md` — supported architectures and binary formats
- `docs/TESTING.md` — running the test harness and benchmarks