NtCreateSymbolicLinkObject
Creates an object-manager symbolic link from a name to an arbitrary NT target string.
Prototype
NTSTATUS NtCreateSymbolicLinkObject( PHANDLE LinkHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PUNICODE_STRING LinkTarget );
Arguments
| Name | Type | Dir | Description |
|---|---|---|---|
| LinkHandle | PHANDLE | out | Receives the handle to the new symbolic link object. |
| DesiredAccess | ACCESS_MASK | in | Typically SYMBOLIC_LINK_ALL_ACCESS or SYMBOLIC_LINK_QUERY. |
| ObjectAttributes | POBJECT_ATTRIBUTES | in | Name of the link, optional RootDirectory and attribute flags. |
| LinkTarget | PUNICODE_STRING | in | Target NT path the link resolves to. Not validated — may point anywhere in the namespace. |
Syscall IDs by Windows version
| Windows version | Syscall ID | Build |
|---|---|---|
| Win10 1507 | 0xB2 | win10-1507 |
| Win10 1607 | 0xB5 | win10-1607 |
| Win10 1703 | 0xB8 | win10-1703 |
| Win10 1709 | 0xB9 | win10-1709 |
| Win10 1803 | 0xBA | win10-1803 |
| Win10 1809 | 0xBB | win10-1809 |
| Win10 1903 | 0xBC | win10-1903 |
| Win10 1909 | 0xBC | win10-1909 |
| Win10 2004 | 0xC0 | win10-2004 |
| Win10 20H2 | 0xC0 | win10-20h2 |
| Win10 21H1 | 0xC0 | win10-21h1 |
| Win10 21H2 | 0xC1 | win10-21h2 |
| Win10 22H2 | 0xC1 | win10-22h2 |
| Win11 21H2 | 0xC5 | win11-21h2 |
| Win11 22H2 | 0xC6 | win11-22h2 |
| Win11 23H2 | 0xC6 | win11-23h2 |
| Win11 24H2 | 0xC8 | win11-24h2 |
| Server 2016 | 0xB5 | winserver-2016 |
| Server 2019 | 0xBB | winserver-2019 |
| Server 2022 | 0xC4 | winserver-2022 |
| Server 2025 | 0xC8 | winserver-2025 |
Kernel module
Related APIs
Syscall stub
4C 8B D1 mov r10, rcx B8 C8 00 00 00 mov eax, 0xC8 F6 04 25 08 03 FE 7F 01 test byte ptr [0x7FFE0308], 1 75 03 jne short +3 0F 05 syscall C3 ret CD 2E int 2Eh C3 ret
Undocumented notes
SSN drifts from `0xB2` (1507) to `0xC8` (24H2); resolve dynamically when targeting multiple builds. Object-manager symbolic links are completely distinct from NTFS symlinks/reparse points — they live in the in-memory object namespace and are evaluated lazily at name-parse time. The standard DOS drive letters you see in Explorer (`C:`, `D:`) are actually `\\??\\C: -> \\Device\\HarddiskVolume3` symbolic links planted by Session Manager. Creating links in the per-session `\\??` directory normally only requires DIRECTORY_CREATE_OBJECT access on that directory (unprivileged from a normal interactive session); creating links in `\\GLOBAL??` or anywhere requiring OBJ_PERMANENT requires SeCreateSymbolicLinkPrivilege (default for admins).
Common malware usage
The flagship offensive primitive for **sandbox escape**: plant `\\??\\X -> \\??\\C:` (or anything) inside the per-session DOS device directory and any subsequent path lookup that hits the link gets redirected. CVE-2019-0808 family wild abuse used exactly this pattern to coerce privileged services into writing into attacker-controlled locations. Combined with `NtFsControlFile(FSCTL_SET_REPARSE_POINT)` for NTFS-level mountpoint redirection (Forshaw's symboliclink-testing-tools), you get full filesystem-redirection chains. Capture-the-flag mainstay because the primitive is in the unprivileged reach of any process that can open `\\??` for CREATE_OBJECT. Modern variations target `\\??\\NamedPipe\\...` (planting a pipe redirect so an authenticated SMB client connects to the attacker's namedpipe and discloses credentials), and `\\??\\GLOBALROOT\\Device\\...` exposure (a per-session symlink that bypasses the GLOBALROOT path filter in many sandboxes).
Detection opportunities
Symbolic-link creation by non-system processes is rare and very high-value telemetry. Sysmon doesn't directly log it, but **kernel-mode ObRegisterCallbacks on `IoSymbolicLinkObjectType`** catches every create at the source. Microsoft Defender for Endpoint has had a behavioral rule since ~2020 that flags symlink creates from medium-IL processes into `\\??` pointing back into `\\??\\C:` or `\\Device\\Harddisk*`. ETW Microsoft-Windows-Kernel-Audit-API-Calls also exposes it. The strongest signal is **a non-admin process creating a symlink whose target string starts with `\\Device\\`, `\\??\\GLOBALROOT\\`, or another `\\??\\` drive letter** — the redirection patterns essentially never appear in legitimate code.
Direct syscall examples
asmx64 direct stub (Win11 24H2)
; Direct syscall stub for NtCreateSymbolicLinkObject (SSN 0xC8, Win11 24H2)
NtCreateSymbolicLinkObject PROC
mov r10, rcx ; syscall convention
mov eax, 0C8h ; SSN (BUILD-SPECIFIC, resolve dynamically)
syscall
ret
NtCreateSymbolicLinkObject ENDPcPlant \??\X -> \??\C: for path redirection
// Plant an object-manager symlink: \??\X -> \??\C:
// A subsequent CreateFile("X:\\Users\\victim\\...") will resolve through the
// link and hit the chosen target. Used as a stepping stone in CVE-2019-0808
// -style sandbox-escape chains.
#include <windows.h>
#include <winternl.h>
#define SYMBOLIC_LINK_ALL_ACCESS 0xF0001
#define OBJ_CASE_INSENSITIVE 0x00000040
typedef NTSTATUS (NTAPI *pNtCreateSymbolicLinkObject)(
PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES, PUNICODE_STRING);
BOOL PlantDriveSymlink(WCHAR letter, PCWSTR target) {
pNtCreateSymbolicLinkObject NtCreateSymbolicLinkObject =
(pNtCreateSymbolicLinkObject)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtCreateSymbolicLinkObject");
WCHAR path[16];
swprintf_s(path, 16, L"\\??\\%lc", letter);
UNICODE_STRING usName; RtlInitUnicodeString(&usName, path);
UNICODE_STRING usTgt; RtlInitUnicodeString(&usTgt, target);
OBJECT_ATTRIBUTES oa;
InitializeObjectAttributes(&oa, &usName, OBJ_CASE_INSENSITIVE, NULL, NULL);
HANDLE hLink = NULL;
NTSTATUS s = NtCreateSymbolicLinkObject(
&hLink, SYMBOLIC_LINK_ALL_ACCESS, &oa, &usTgt);
return NT_SUCCESS(s);
}rustNamedPipe symlink for SMB credential capture
// Cargo: ntapi = "0.4", widestring = "1"
// Plant \??\PIPE\srvsvc -> \??\PIPE\attacker so that an authenticated SMB
// client connecting via UNC \\victim\IPC$\srvsvc lands on the attacker's pipe.
// Used in NTLM-relay and credential-coercion PoCs.
use ntapi::ntobapi::NtCreateSymbolicLinkObject;
use ntapi::ntrtl::RtlInitUnicodeString;
use winapi::shared::ntdef::{OBJECT_ATTRIBUTES, UNICODE_STRING};
use widestring::U16CString;
pub unsafe fn plant_pipe_symlink(name: &str, target: &str) -> bool {
let wn = U16CString::from_str(name).unwrap();
let wt = U16CString::from_str(target).unwrap();
let mut usn: UNICODE_STRING = std::mem::zeroed();
let mut ust: UNICODE_STRING = std::mem::zeroed();
RtlInitUnicodeString(&mut usn, wn.as_ptr());
RtlInitUnicodeString(&mut ust, wt.as_ptr());
let mut oa: OBJECT_ATTRIBUTES = std::mem::zeroed();
oa.Length = std::mem::size_of::<OBJECT_ATTRIBUTES>() as u32;
oa.ObjectName = &mut usn;
oa.Attributes = 0x40; // OBJ_CASE_INSENSITIVE
let mut h: isize = 0;
let s = NtCreateSymbolicLinkObject(&mut h as *mut _ as _, 0xF0001, &mut oa, &mut ust);
s >= 0
}MITRE ATT&CK mappings
Last verified: 2026-05-20