https://github.com/securityronin/lnk-forensic
Windows Shell Link (.lnk) forensics — parse target path, volume serial, MAC times, tracker machine ID; detect removable-media and network targets. Pure Rust. (JumpLists in v0.2.)
https://github.com/securityronin/lnk-forensic
dfir digital-forensics forensics incident-response lnk rust shell-link windows
Last synced: about 2 months ago
JSON representation
Windows Shell Link (.lnk) forensics — parse target path, volume serial, MAC times, tracker machine ID; detect removable-media and network targets. Pure Rust. (JumpLists in v0.2.)
- Host: GitHub
- URL: https://github.com/securityronin/lnk-forensic
- Owner: SecurityRonin
- License: other
- Created: 2026-06-13T06:38:15.000Z (about 2 months ago)
- Default Branch: main
- Last Pushed: 2026-06-13T07:35:57.000Z (about 2 months ago)
- Last Synced: 2026-06-13T09:22:26.521Z (about 2 months ago)
- Topics: dfir, digital-forensics, forensics, incident-response, lnk, rust, shell-link, windows
- Language: Rust
- Size: 673 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
# lnk-forensic
[](https://crates.io/crates/lnk-core)
[](https://crates.io/crates/lnk-forensic)
[](https://docs.rs/lnk-core)
[](https://www.rust-lang.org)
[](LICENSE)
[](https://github.com/sponsors/h4x0r)
[](https://github.com/SecurityRonin/lnk-forensic/actions/workflows/ci.yml)
[](https://github.com/SecurityRonin/lnk-forensic/actions/workflows/ci.yml)
[](https://github.com/rust-secure-code/safety-dance/)
[](deny.toml)
**Turn a Windows `.lnk` shortcut — or a whole Jump List — into graded forensic findings — surface the file opened from a USB stick, the share it came off, and the machine it was authored on, with the volume serial that ties it back to the physical device.**
A `.lnk` is a rich `[MS-SHLLINK]` artifact: it records the target path, the volume
serial and MAC timestamps, the origin machine's NetBIOS name, and a distributed-
link-tracking droid GUID — often evidence of a file that no longer exists.
`lnk-forensic` reads it from a link authored on **any** Windows host and grades
what matters for triage. It also parses **Jump Lists** — the taskbar/Start MRU
artifact — both `*.automaticDestinations-ms` (an OLE/CFB compound file with a
`DestList` MRU stream + one embedded `.lnk` per entry) and
`*.customDestinations-ms` (a flat run of embedded `.lnk`s), reusing the same
shell-link audit over every embedded link.
## Audit a Shell Link in 30 seconds
```toml
[dependencies]
lnk-forensic = "0.2" # pulls in lnk-core
```
```rust
use lnk_core::parse_shell_link;
use lnk_forensic::{audit_findings};
// .lnk bytes off disk; a malformed header yields None, never a panic.
if let Some(link) = parse_shell_link(lnk_bytes) {
for f in audit_findings(&link, "volume: E:") {
println!("[{:?}] {} — {}", f.severity, f.code, f.note);
// e.g. [Some(Medium)] LNK-REMOVABLE-MEDIA-TARGET — the link target resolves to a removable …
}
}
```
Want the typed stream instead of graded findings? `audit(&link)` returns
`Vec`; each anomaly emits a `forensicnomicon::report::Finding` via
`to_finding(source)`.
## The anomaly codes
Each anomaly is an **observation** ("consistent with …"); the examiner draws the
conclusions. Codes are a stable, published contract.
| Code | Severity | Category | What it observes |
|---|---|---|---|
| `LNK-REMOVABLE-MEDIA-TARGET` | Medium | Threat | The `VolumeID` describes a `DRIVE_REMOVABLE` volume — consistent with a file opened from external media (MITRE T1052.001 / T1091). The **volume serial** is surfaced as the join key to a peripheral device connection. |
| `LNK-NETWORK-TARGET` | Low | Threat | The link carries a `CommonNetworkRelativeLink` — consistent with a file opened from a network share (MITRE T1021). |
| `LNK-TRACKER-MACHINE` | Info | Provenance | The `TrackerDataBlock` records the origin machine's NetBIOS name — consistent with the link having been authored on that machine (attribution). |
## Jump Lists — Automatic + Custom Destinations
`parse_automatic_destinations(bytes, filename)` opens a `*.automaticDestinations-ms`
as a CFB compound file, reads the `DestList` MRU stream (Windows 7 v1 and
Windows 10/11 v2+ layouts), and decodes each embedded `.lnk` sub-stream;
`parse_custom_destinations(bytes, filename)` splits a flat
`*.customDestinations-ms` into its embedded `.lnk`s by the `[MS-SHLLINK]` CLSID
and `0xBABFFBAB` footer. `audit_jumplist(&jl, acquisition_host, scope)` runs the
**existing per-link audit over every embedded link** (so the codes above fire for
free) plus four Jump-List-level codes:
| Code | Severity | Category | What it observes |
|---|---|---|---|
| `JUMPLIST-PINNED-TARGET` | Low | Provenance | A `DestList` entry is **pinned** — consistent with the user having deliberately fixed this target to the application's Jump List. |
| `JUMPLIST-CROSS-MACHINE` | Low | Provenance | A `DestList` entry's origin hostname (or droid volume GUID) has **no match to the acquisition host** — consistent with the target/artifact having originated on a different machine. |
| `JUMPLIST-MRU-RECENCY` | Info | History | A `DestList` entry's last-access time + access count — the application's own usage history for the target. |
| `JUMPLIST-APPID-IDENTIFIED` | Info | Provenance | The Jump List `AppID` resolves to a known application via `forensicnomicon::jumplist::appid_name`. |
The DestList offset tables, the `0xBABFFBAB` footer, the embedded-LNK CLSID
boundary, and the `AppID` map all come from
[`forensicnomicon::jumplist`](https://crates.io/crates/forensicnomicon).
## The volume serial is a cross-artifact join key
A `.lnk`'s `VolumeID.DriveSerialNumber` is the same 32-bit volume serial a USB
mass-storage device records in the registry / setupapi log. `lnk-forensic`
surfaces it first-class on the removable-media anomaly so an examiner can
**correlate** a file opened from external media (this link) with the **physical
device** that carried it (a
[`peripheral-forensic`](https://github.com/SecurityRonin/peripheral-forensic)
`DeviceConnection`). The serial is the join key — the link surfaces the value, the
examiner reconciles it.
## The two-crate split
- **`lnk-core`** — the reader. Parses the 0x4C `ShellLinkHeader` (LinkFlags,
FileAttributes, the three target FILETIMEs → Unix epoch, file size, icon index,
show command, hotkey), the `LinkInfo` block (`VolumeID` drive type + serial +
label, local base path, `CommonNetworkRelativeLink`), ANSI/Unicode `StringData`,
the raw `LinkTargetIDList` PIDL blob (full PIDL decode is a shellbag parser's
job), the `ExtraData` `TrackerDataBlock`, and **Jump Lists** (Automatic + Custom
Destinations). Format constants come from
[`forensicnomicon::shlink`](https://crates.io/crates/forensicnomicon) and
[`forensicnomicon::jumplist`](https://crates.io/crates/forensicnomicon); the
parsing algorithm lives here. No findings.
- **`lnk-forensic`** — the analyzer. Audits a `ShellLink` or a `JumpList` into
graded `forensicnomicon::report::Finding`s. Depends on `lnk-core`.
### Third-party dependency note
`lnk-core` depends on the mature MIT-licensed
[`cfb`](https://crates.io/crates/cfb) crate to read the OLE Compound-File
container that `*.automaticDestinations-ms` Jump Lists are stored in — a
documented exception to "prefer our own", on the same footing as `lznt1` for
NTFS: reusing a correct, maintained, better-scoped reader beats reinventing an
OLE/CFB parser. Our own code stays `#![forbid(unsafe_code)]`.
## Trust, but verify
Built for untrusted `.lnk` files from potentially compromised systems:
- **`#![forbid(unsafe_code)]`** across both crates — no FFI, no C bindings.
- **Panic-free on malicious input** — every integer/length/offset read is
bounds-checked; the workspace denies `clippy::unwrap_used` and
`clippy::expect_used` in production code. A truncated or garbled link yields
absent sub-structures or `None`, never a crash.
- **Fuzzed** — `cargo-fuzz` targets `shelllink` (the reader), `forensic` (the
full parse → audit pipeline), and `jumplist` (the CFB/DestList + custom-
destinations parse → audit); a `fuzz.yml` CI workflow builds and smoke-runs
each.
- **Validated against spec-exact artifacts** — the pipeline is exercised
end-to-end against hand-authored fixtures: `[MS-SHLLINK]` links (a removable-
media link with a volume serial + a network-share link;
`forensic/tests/real_data.rs`) and Jump Lists (a real CFB
`*.automaticDestinations-ms` with a pinned, cross-machine removable entry + a
flat `*.customDestinations-ms`; `forensic/tests/jumplist.rs`), reconciling the
surfaced serial and findings.
```bash
cargo test
cargo +nightly fuzz run forensic # requires nightly + cargo-fuzz
```
## Where this fits
`lnk-forensic` is a parser/analyzer in the SecurityRonin forensic fleet: each
crate is a deep expert in one artifact family, emitting the shared
`forensicnomicon::report` vocabulary so findings aggregate uniformly across disk,
memory, log, and registry artifacts.
[Privacy Policy](https://securityronin.github.io/lnk-forensic/privacy/) · [Terms of Service](https://securityronin.github.io/lnk-forensic/terms/) · © 2026 Security Ronin Ltd