|
3 | 3 | All notable changes to **capa** are documented here. |
4 | 4 | This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
5 | 5 |
|
| 6 | +## [0.5.0] — AArch64 thread-through |
| 7 | + |
| 8 | +Threads the AArch64 surface from smda 0.6.x into the analysis |
| 9 | +pipeline. No API breaks at the `AnalyzeBuilder` / `FileCapabilities` |
| 10 | +level; the `Extractor` trait gains one new method (`arch()`) so |
| 11 | +downstream consumers implementing the trait need to add it. |
| 12 | + |
| 13 | +### Dependency bumps |
| 14 | + |
| 15 | +- **smda 0.5 → 0.6.** Picks up AArch64 (Apple-silicon Mach-O, |
| 16 | + Linux EM_AARCH64, Windows ARM64 PE), the seven AArch64 analyser |
| 17 | + ports (jump-table, indirect-call, tail-call, .pdata, NOP, |
| 18 | + exit-syscall, is_api_thunk), ELF GOT/PLT API resolution, and |
| 19 | + the 0.6.2 hygiene patches. Semver-compatible with all future |
| 20 | + 0.6.x. |
| 21 | + |
| 22 | +### Added |
| 23 | + |
| 24 | +- typed iced operand walking in `extract_insn_offset_features`. |
| 25 | + Pre-0.5.0 split formatted operand strings on commas and ran |
| 26 | + regexes; missed offsets buried in SIB-displacement forms like |
| 27 | + `[rax + rcx*4 + 0x10]`. Now consults `op_kind(i)`, `memory_base()`, |
| 28 | + and `memory_displacement64()` |
| 29 | +- real `translate_com_features` impl backed by a generated |
| 30 | + ~29 k-entry GUID database (`src/rules/com_db.rs`, ~5.8 MB) for |
| 31 | + every COM class and interface known to upstream Python capa. |
| 32 | + Rule-load-time rewrite of `com/class: WbemLocator` → |
| 33 | + `or: [bytes: <guid1>, bytes: <guid2>]`. Database regenerated via |
| 34 | + `scripts/gen_com_tables.py`. |
| 35 | +- scope-keyed feature dump |
| 36 | + (`map_features_by_scope: HashMap<&'static str, …>`) so the |
| 37 | + `--map-features` CLI flag surfaces feature counts per scope |
| 38 | + (file / function / basic_block / instruction). New CLI summary |
| 39 | + table in `capa_cli`. |
| 40 | + |
| 41 | +### Added — AArch64 thread-through (task #236) |
| 42 | + |
| 43 | +- **`Extractor::arch()`** trait method. Both `Smda` and `DnFile` |
| 44 | + impls provided. Surfaces the real `FileArchitecture` from the |
| 45 | + underlying disassembler instead of the previous bitness-based |
| 46 | + guess. |
| 47 | +- **`FileCapabilities::get_arch`** now calls `extractor.arch()` |
| 48 | + instead of mapping `bitness == 64 → AMD64` unconditionally — |
| 49 | + which had been silently mislabelling every AArch64 binary that |
| 50 | + smda 0.6 started producing. |
| 51 | +- **`extract_insn_mnemonic_features`** branches on |
| 52 | + `Instruction::mnemonic_aarch64()` so ARM64 instructions emit |
| 53 | + their disarm64 mnemonic (`ldr`, `str`, `bl`, …) instead of the |
| 54 | + iced sentinel string `"invalid"`. Rules `mnemonic: ldr` etc. |
| 55 | + now fire on ARM64 binaries. |
| 56 | + |
| 57 | +### Added — AArch64 instruction-scope feature parity |
| 58 | + |
| 59 | +- **`extract_insn_offset_features` ARM64 path.** Routes through |
| 60 | + smda 0.6's `disassembler::aarch64_ops` decoders instead of the |
| 61 | + x86-only iced operand walk (which returned sentinel/zero on |
| 62 | + ARM64 and silently dropped every offset feature). Now emits: |
| 63 | + - `LDR/STR Xt, [Xn, #imm12]` → `Offset(imm12)` + |
| 64 | + `OperandOffset(1, imm12)` — base = SP (R31) / X29 (frame |
| 65 | + pointer) skipped, matching the x86 path's EBP/RBP filter. |
| 66 | + - `ADR Xd, label` → `Number(label_va)` — mirrors |
| 67 | + x86 LEA's "constant address into register" emission. |
| 68 | + - `ADRP Xd, page` → `Number(page_va)` — same shape |
| 69 | + at 4 KiB granularity (compilers emit ADRP+ADD/LDR to |
| 70 | + materialise a full address; the page VA is close enough for |
| 71 | + the typical "constant in code" pattern that `number:` rules |
| 72 | + look for). |
| 73 | +- **`extract_insn_peb_access_characteristic_features` ARM64 |
| 74 | + path.** Windows on ARM64 reserves x18 as the TEB pointer |
| 75 | + (Microsoft "platform register" ABI; PEB lives at |
| 76 | + `[x18 + 0x60]`, mirroring `gs:[0x60]` on x64). Loads with |
| 77 | + base = x18 are flagged as `peb access` regardless of |
| 78 | + displacement — same "touches TEB" granularity as the x86 |
| 79 | + `fs:`/`gs:` segment-read match. Stores are excluded; writing |
| 80 | + through x18 is exotic and doesn't pattern-match the rule. |
| 81 | + |
| 82 | +### Fixed — Mach-O closeout (audit findings) |
| 83 | + |
| 84 | +- **Fat (universal) Mach-O now routed to smda instead of |
| 85 | + rejected.** `is_macho_magic` previously excluded `cafebabe` / |
| 86 | + `cafebabf` (and their byte-swapped variants), so every |
| 87 | + Apple-Silicon system binary that ships as a universal binary |
| 88 | + (`/bin/ls`, `/usr/bin/file`, …) returned |
| 89 | + `UnsupportedFormatError` even though smda's |
| 90 | + `extract_macho_with_offset` already has fat-slice selection |
| 91 | + wired up. Now accepted; smda picks the matching slice via |
| 92 | + the `MachoArchPreference::HostNative` default. Caveat: the |
| 93 | + `cafebabe` magic is also Java `.class` file magic — goblin's |
| 94 | + `Mach::parse` rejects class files cleanly (the nfat_arch |
| 95 | + sanity check fails), so a misrouted `.class` surfaces as a |
| 96 | + parse error rather than silent misinterpretation. |
| 97 | +- **`security::get_security_checks` no longer aborts on |
| 98 | + Mach-O.** Pre-0.5.0 returned `UnsupportedBinaryFormat` for |
| 99 | + every Mach-O input, which `from_file` propagates via `?` — |
| 100 | + so even with the fat-Mach-O fix above, no Mach-O could |
| 101 | + actually reach the capability engine. The PE/ELF security |
| 102 | + checklist (ASLR, DEP, SEH, CHECKSUM, RELRO, CANARY, …) has |
| 103 | + no 1:1 equivalent on Darwin, so the path now returns an |
| 104 | + empty `Vec` and lets analysis continue — mirrors how |
| 105 | + `from_buffer` skips security checks for shellcode. |
| 106 | +- **`extract_insn_nzxor_characteristic_features` ARM64 path.** |
| 107 | + Pre-0.5.0 the function checked iced's x86 `Mnemonic` enum |
| 108 | + only, which is `INVALID` on AArch64 decodes — so the `nzxor` |
| 109 | + characteristic never fired on ARM64 binaries. Now branches |
| 110 | + on `insn.decoded` and detects AArch64 `EOR` / `EOR3` / |
| 111 | + `EORS` via `mnemonic_aarch64()`, with a self-XOR exclusion |
| 112 | + (`Rn == Rm`, the AArch64 zeroing idiom) mirroring the x86 |
| 113 | + `dst == src` filter. Security-cookie filter is skipped on |
| 114 | + ARM64 — the RBP-relative cookie pattern doesn't exist |
| 115 | + verbatim (Darwin uses `__stack_chk_guard` loads instead). |
| 116 | +- **Mach-O OS now reports `MACOS`, not the `LINUX` |
| 117 | + placeholder.** Added `Os::MACOS` and `Os::IOS` to the |
| 118 | + public `consts::Os` enum (additive — non-breaking for |
| 119 | + matchers using `_`). `extract_os` + `get_os` route Mach-O |
| 120 | + through `MACOS`. Rules `os: macos` now fire on Mach-O |
| 121 | + input; `os: linux` rules no longer fire incorrectly. iOS |
| 122 | + vs macOS isn't distinguishable from cputype alone |
| 123 | + (CPU_TYPE_ARM64 covers both); future work could promote to |
| 124 | + `IOS` based on `LC_VERSION_MIN_IPHONEOS`. |
| 125 | + |
| 126 | +### Dependency bumps (continued) |
| 127 | + |
| 128 | +- **smda 0.6.2 → 0.6.4.** Picks up two Mach-O fixes that |
| 129 | + capa-rs 0.5.0 depends on: |
| 130 | + - **0.6.3 tail-call resolver fix:** when |
| 131 | + `resolve_tailcalls(true)` is active (capa_cli default), |
| 132 | + `TailCallAnalyser::resolve_tailcalls` used to fatally |
| 133 | + propagate `CollisionError` from `analyse_function` — |
| 134 | + aborting the whole `Disassembler::parse` call. Now treats |
| 135 | + CollisionError as expected (skip candidate, continue), |
| 136 | + matching the main candidate loop's `.ok()` pattern. Pre-fix |
| 137 | + repro: `SMDAError(CollisionError(0x100003698))` in ~95 ms |
| 138 | + on every Apple-Silicon /bin/ls invocation. |
| 139 | + - **0.6.4 Mach-O imports → `disassembly.apis` bridge:** |
| 140 | + `analyse_buffer` had an explicit ELF→apis bridge and PE was |
| 141 | + handled by the WinApiResolver, but Mach-O had neither. |
| 142 | + Imports stopped at file-scope `binary_info.imports` and |
| 143 | + never reached `disassembly.apis` / `addr_to_api`, so |
| 144 | + `Function::apirefs` was empty for every Mach-O function and |
| 145 | + capa's `extract_insn_api_features` emitted zero |
| 146 | + `Feature::Api` for Mach-O input — making /bin/ls match zero |
| 147 | + capa rules. Now a Mach-O-symmetric block in `analyse_buffer` |
| 148 | + walks `macho::extract_macho_dynamic_apis` (new in 0.6.4) and |
| 149 | + populates both `apis` and `addr_to_api` directly. Coverage: |
| 150 | + `__DATA,__got` / `__DATA,__la_symbol_ptr` slot VAs, reached |
| 151 | + by the canonical ARM64 PIC patterns (`adrp+ldr+blr` inlined |
| 152 | + or via __TEXT,__stubs thunks). |
| 153 | + |
| 154 | +### Added — Mach-O security checklist |
| 155 | + |
| 156 | +- **Real `security/macho.rs` module** replacing the placeholder |
| 157 | + `Ok(Vec::new())` that pre-0.5.0 shipped (an empty Security |
| 158 | + Checks table on every Mach-O input). Nine checks emitted: |
| 159 | + - **PIE** — `MH_PIE` (`0x200000`), Darwin ASLR equivalent. |
| 160 | + - **DATA-EXEC-PREVENT** — `MH_NO_HEAP_EXECUTION` (`0x01000000`) |
| 161 | + OR no `__DATA*` segment with `VM_PROT_EXECUTE` in |
| 162 | + `initprot`. |
| 163 | + - **STACK-CANARY** — `___stack_chk_guard` / |
| 164 | + `___stack_chk_fail` in the symbol table. |
| 165 | + - **RESTRICT** — presence of a `__RESTRICT` segment |
| 166 | + (anti-`DYLD_INSERT_LIBRARIES` marker). |
| 167 | + - **CODE-SIGNATURE** — `LC_CODE_SIGNATURE` load command with |
| 168 | + non-zero `datasize`. |
| 169 | + - **TWO-LEVEL-NAMESPACE** — `MH_TWOLEVEL` (`0x80`). |
| 170 | + - **NO-UNDEF-SYMS** — `MH_NOUNDEFS` (`0x01`). |
| 171 | + - **HARDENED-RUNTIME** — `Unknown` (requires `CS_SuperBlob` → |
| 172 | + `CS_CodeDirectory.flags & CS_RUNTIME (0x10000)` walk; |
| 173 | + deferred to a follow-up, ~80 LOC of big-endian parsing |
| 174 | + inside `__LINKEDIT`). |
| 175 | + - **ALLOW-JIT** — `Unknown` (requires |
| 176 | + `CS_EmbeddedEntitlements` PLIST parsing; deferred). |
| 177 | + Fat binaries: first parseable slice's checks are reported, |
| 178 | + same convention as smda's `MachoArchPreference::HostNative` |
| 179 | + picks for analysis. |
| 180 | + |
6 | 181 | ## [0.4.3] — 2026-05-27 — FLIRT polish |
7 | 182 |
|
8 | 183 | ### Added |
|
0 commit comments