Skip to content

Commit e3afd1b

Browse files
committed
fix(windows): 提权运行时打通跨完整性级别的进程通信
提权(High IL)运行时,Windows 静默丢弃来自低完整性(Medium IL)来源的管道连接与窗口消息,致浏览器接管、拖链建任务、 单实例参数转发全部失效。 加固:命名管道 SDDL 打低完整性标签;单实例互斥体改用跨完整性 SECURITY_ATTRIBUTES;WM_COPYDATA 与拖放消息经 UIPI 显式放行。
1 parent 8bcf90b commit e3afd1b

4 files changed

Lines changed: 165 additions & 7 deletions

File tree

native/hub/src/native_messaging.rs

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,117 @@ mod server {
172172
}
173173
}
174174

175+
/// Windows pipe security: grant Everyone read/write and stamp a Low
176+
/// mandatory integrity label so a Medium-IL `fluxdown_nmh.exe` (spawned by
177+
/// the browser) can connect even when FluxDown runs elevated (High IL).
178+
/// Without it the pipe inherits the creator's High IL and the no-write-up
179+
/// rule silently rejects the relay — browser interception dies until the
180+
/// app next runs unelevated.
181+
mod pipe_security {
182+
use std::ffi::c_void;
183+
use std::io;
184+
185+
#[repr(C)]
186+
struct SecurityAttributes {
187+
n_length: u32,
188+
lp_security_descriptor: *mut c_void,
189+
b_inherit_handle: i32,
190+
}
191+
192+
#[link(name = "advapi32")]
193+
unsafe extern "system" {
194+
fn ConvertStringSecurityDescriptorToSecurityDescriptorW(
195+
string_security_descriptor: *const u16,
196+
string_sddl_revision: u32,
197+
security_descriptor: *mut *mut c_void,
198+
security_descriptor_size: *mut u32,
199+
) -> i32;
200+
}
201+
202+
#[link(name = "kernel32")]
203+
unsafe extern "system" {
204+
fn LocalFree(hmem: *mut c_void) -> *mut c_void;
205+
}
206+
207+
/// Owns the security descriptor allocated by the SDDL conversion plus
208+
/// the `SECURITY_ATTRIBUTES` pointing at it; frees it on drop. Never
209+
/// held across an `.await` (built and dropped inside the synchronous
210+
/// `create_instance`), so the raw pointer needs no `Send`.
211+
pub struct PipeSecurity {
212+
attrs: SecurityAttributes,
213+
}
214+
215+
impl PipeSecurity {
216+
/// Build the descriptor, or error if the SDDL conversion fails.
217+
pub fn new() -> io::Result<Self> {
218+
// D: Everyone (WD) generic read+write. S: Low mandatory label,
219+
// no-write-up (equal/higher IL subjects may still write).
220+
let sddl: Vec<u16> = "D:(A;;GRGW;;;WD)S:(ML;;NW;;;LW)"
221+
.encode_utf16()
222+
.chain(std::iter::once(0))
223+
.collect();
224+
let mut psd: *mut c_void = std::ptr::null_mut();
225+
// SAFETY: `sddl` is a valid NUL-terminated UTF-16 string; on
226+
// success the call allocates `psd`, freed in `Drop`. Revision
227+
// 1 == SDDL_REVISION_1.
228+
let ok = unsafe {
229+
ConvertStringSecurityDescriptorToSecurityDescriptorW(
230+
sddl.as_ptr(),
231+
1,
232+
&mut psd,
233+
std::ptr::null_mut(),
234+
)
235+
};
236+
if ok == 0 {
237+
return Err(io::Error::last_os_error());
238+
}
239+
Ok(Self {
240+
attrs: SecurityAttributes {
241+
n_length: std::mem::size_of::<SecurityAttributes>() as u32,
242+
lp_security_descriptor: psd,
243+
b_inherit_handle: 0,
244+
},
245+
})
246+
}
247+
248+
/// Pointer to the `SECURITY_ATTRIBUTES`, valid while `self` lives.
249+
pub fn as_ptr(&self) -> *mut c_void {
250+
(&raw const self.attrs).cast::<c_void>().cast_mut()
251+
}
252+
}
253+
254+
impl Drop for PipeSecurity {
255+
fn drop(&mut self) {
256+
if !self.attrs.lp_security_descriptor.is_null() {
257+
// SAFETY: descriptor was allocated by the SDDL conversion.
258+
unsafe { LocalFree(self.attrs.lp_security_descriptor) };
259+
}
260+
}
261+
}
262+
}
263+
264+
/// Create one pipe server instance with a hardened security descriptor
265+
/// (Everyone R/W + Low integrity label) so a Medium-IL NMH relay can
266+
/// connect even when FluxDown runs elevated. Falls back to default security
267+
/// if the descriptor cannot be built, never breaking the unelevated path.
268+
fn create_instance(
269+
first: bool,
270+
) -> std::io::Result<tokio::net::windows::named_pipe::NamedPipeServer> {
271+
let mut options = ServerOptions::new();
272+
options.first_pipe_instance(first);
273+
match pipe_security::PipeSecurity::new() {
274+
// SAFETY: `sec` and its descriptor outlive this create call, which
275+
// copies the SECURITY_ATTRIBUTES into the pipe synchronously.
276+
Ok(sec) => unsafe {
277+
options.create_with_security_attributes_raw(PIPE_NAME, sec.as_ptr())
278+
},
279+
Err(e) => {
280+
log_info!("[nmh-pipe] pipe security unavailable, using default: {}", e);
281+
options.create(PIPE_NAME)
282+
}
283+
}
284+
}
285+
175286
/// Spawn the Named Pipe server, feeding download requests into `tx`.
176287
/// The receiving end is owned by `download_actor` and shared with the
177288
/// local HTTP takeover server so both transports converge on one channel.
@@ -180,10 +291,7 @@ mod server {
180291
log_info!("[nmh-pipe] starting Named Pipe server at {}", PIPE_NAME);
181292

182293
// Create the first server instance before entering the loop.
183-
let mut server = match ServerOptions::new()
184-
.first_pipe_instance(true)
185-
.create(PIPE_NAME)
186-
{
294+
let mut server = match create_instance(true) {
187295
Ok(s) => s,
188296
Err(e) => {
189297
log_info!("[nmh-pipe] failed to create pipe server: {}", e);
@@ -204,7 +312,7 @@ mod server {
204312

205313
// Create the next server instance to accept the next client
206314
// while we handle the current one.
207-
let next_server = match ServerOptions::new().create(PIPE_NAME) {
315+
let next_server = match create_instance(false) {
208316
Ok(s) => s,
209317
Err(e) => {
210318
log_info!("[nmh-pipe] failed to create next pipe instance: {}", e);

windows/runner/floating_ball_drop_target.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,16 @@ HRESULT FloatingBallDropTarget::RegisterOn(HWND hwnd) {
2121
HRESULT hr = ::RegisterDragDrop(hwnd, this);
2222
if (SUCCEEDED(hr)) {
2323
registered_hwnd_ = hwnd;
24+
// UIPI hardening: when FluxDown runs elevated (High IL), Windows silently
25+
// filters drag-drop window messages posted by lower-integrity sources
26+
// (Explorer / browser at Medium IL), so OLE drops onto the ball never
27+
// fire. Explicitly allow them for this HWND. Best-effort; failures and
28+
// pre-Win7 systems are harmless (equal-IL drops already work).
29+
static constexpr UINT kWmCopyGlobalData = 0x0049; // undocumented, no macro
30+
::ChangeWindowMessageFilterEx(hwnd, WM_DROPFILES, MSGFLT_ALLOW, nullptr);
31+
::ChangeWindowMessageFilterEx(hwnd, WM_COPYDATA, MSGFLT_ALLOW, nullptr);
32+
::ChangeWindowMessageFilterEx(hwnd, kWmCopyGlobalData, MSGFLT_ALLOW,
33+
nullptr);
2434
}
2535
return hr;
2636
}

windows/runner/flutter_window.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ bool FlutterWindow::OnCreate() {
116116
// window is shown. It is a no-op if the first frame hasn't completed yet.
117117
flutter_controller_->ForceRedraw();
118118

119+
// UIPI hardening: allow a lower-integrity (Medium IL) second instance to
120+
// deliver its command-line args via WM_COPYDATA when this instance runs
121+
// elevated (High IL). Pairs with the single-instance mutex hardening in
122+
// main.cpp; without it the forwarded URL/torrent is silently dropped.
123+
if (HWND handle = GetHandle()) {
124+
::ChangeWindowMessageFilterEx(handle, WM_COPYDATA, MSGFLT_ALLOW, nullptr);
125+
}
126+
119127
return true;
120128
}
121129

windows/runner/main.cpp

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include <flutter/flutter_view_controller.h>
33
#include <windows.h>
44
#include <dbghelp.h>
5+
#include <sddl.h>
56

67
#include <iostream>
78
#include <sstream>
@@ -11,6 +12,7 @@
1112
#include "utils.h"
1213

1314
#pragma comment(lib, "dbghelp.lib")
15+
#pragma comment(lib, "advapi32.lib")
1416

1517
static LONG WINAPI FluxDownCrashHandler(EXCEPTION_POINTERS* ep) {
1618
std::ostringstream oss;
@@ -75,6 +77,31 @@ static bool SendArgsToExistingInstance(const std::vector<std::string>& args) {
7577
return true;
7678
}
7779

80+
// Build a SECURITY_ATTRIBUTES that grants Everyone full access to the
81+
// single-instance mutex and stamps it with a Low mandatory integrity label
82+
// (no-write-up). Without this, a Medium-IL second instance cannot open the
83+
// mutex a High-IL (elevated) first instance created: CreateMutex then returns
84+
// ACCESS_DENIED instead of ERROR_ALREADY_EXISTS and the app double-launches.
85+
// Returns nullptr (default security) on failure; on success the caller must
86+
// LocalFree(*out_sd) after CreateMutex.
87+
static LPSECURITY_ATTRIBUTES BuildCrossIntegritySA(SECURITY_ATTRIBUTES* sa,
88+
PSECURITY_DESCRIPTOR* out_sd) {
89+
*out_sd = nullptr;
90+
PSECURITY_DESCRIPTOR sd = nullptr;
91+
// D: Everyone (WD) mutex all-access (0x1F0001). S: Low mandatory label,
92+
// no-write-up so equal/higher IL subjects may still open it.
93+
if (!::ConvertStringSecurityDescriptorToSecurityDescriptorW(
94+
L"D:(A;;0x1F0001;;;WD)S:(ML;;NW;;;LW)", SDDL_REVISION_1, &sd,
95+
nullptr)) {
96+
return nullptr;
97+
}
98+
sa->nLength = sizeof(*sa);
99+
sa->lpSecurityDescriptor = sd;
100+
sa->bInheritHandle = FALSE;
101+
*out_sd = sd;
102+
return sa;
103+
}
104+
78105
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
79106
_In_ wchar_t *command_line, _In_ int show_command) {
80107
// Collect command-line arguments early (needed for both paths).
@@ -85,8 +112,13 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
85112
// --- Single-instance check ---
86113
// Try to create a named mutex. If it already exists, another instance
87114
// is running -- forward our args to it and exit.
88-
HANDLE mutex = ::CreateMutex(nullptr, FALSE, kMutexName);
89-
if (mutex && ::GetLastError() == ERROR_ALREADY_EXISTS) {
115+
SECURITY_ATTRIBUTES mutex_sa = {};
116+
PSECURITY_DESCRIPTOR mutex_sd = nullptr;
117+
LPSECURITY_ATTRIBUTES mutex_psa = BuildCrossIntegritySA(&mutex_sa, &mutex_sd);
118+
HANDLE mutex = ::CreateMutex(mutex_psa, FALSE, kMutexName);
119+
const DWORD mutex_err = ::GetLastError(); // capture before LocalFree
120+
if (mutex_sd) ::LocalFree(mutex_sd);
121+
if (mutex && mutex_err == ERROR_ALREADY_EXISTS) {
90122
SendArgsToExistingInstance(command_line_arguments);
91123
::CloseHandle(mutex);
92124
return EXIT_SUCCESS;

0 commit comments

Comments
 (0)