Skip to main content

buac/
lib.rs

1use std::ffi::c_void;
2use std::os::windows::ffi::OsStrExt;
3use std::ptr::{null, null_mut};
4use windows_sys::core::{GUID, HRESULT};
5use windows_sys::Win32::Foundation::{CloseHandle, GetLastError, HANDLE, S_FALSE, S_OK};
6use windows_sys::Win32::Security::{
7    GetTokenInformation, TokenElevation, TOKEN_ELEVATION, TOKEN_QUERY,
8};
9use windows_sys::Win32::System::Com::{
10    CoGetObject, CoInitializeEx, CoUninitialize, BIND_OPTS3, CLSCTX_LOCAL_SERVER,
11    COINIT_APARTMENTTHREADED,
12};
13use windows_sys::Win32::System::LibraryLoader::{GetModuleHandleW, GetProcAddress};
14use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
15use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOW;
16
17const RPC_E_CHANGED_MODE: HRESULT = 0x80010106u32 as i32;
18
19const IID_ICMLUAUTIL: GUID = GUID {
20    data1: 0x6EDD6D74,
21    data2: 0xC007,
22    data3: 0x4E75,
23    data4: [0xB7, 0x6A, 0xE5, 0x74, 0x09, 0x95, 0xE2, 0x4C],
24};
25
26#[repr(C)]
27struct ICMLuaUtilVtbl {
28    _query_interface: *const c_void,
29    _add_ref: *const c_void,
30    release: unsafe extern "system" fn(this: *mut ICMLuaUtil) -> u32,
31    _reserved: [*const c_void; 6],
32    shell_exec: unsafe extern "system" fn(
33        this: *mut ICMLuaUtil,
34        lp_file: *const u16,
35        lp_parameters: *const u16,
36        lp_directory: *const u16,
37        f_mask: u32,
38        n_show: u32,
39    ) -> HRESULT,
40}
41
42#[repr(C)]
43struct ICMLuaUtil {
44    lp_vtbl: *const ICMLuaUtilVtbl,
45}
46
47#[repr(C)]
48#[derive(Clone, Copy)]
49struct UnicodeString {
50    length: u16,
51    maximum_length: u16,
52    buffer: *mut u16,
53}
54
55type RtlInitUnicodeStringFn = unsafe extern "system" fn(*mut UnicodeString, *const u16);
56type RtlAcquirePebLockFn = unsafe extern "system" fn();
57type RtlReleasePebLockFn = unsafe extern "system" fn();
58
59struct Ntdll {
60    rtl_init_unicode_string: RtlInitUnicodeStringFn,
61    rtl_acquire_peb_lock: RtlAcquirePebLockFn,
62    rtl_release_peb_lock: RtlReleasePebLockFn,
63}
64
65impl Ntdll {
66    unsafe fn load() -> Result<Self, Error> {
67        let ntdll = GetModuleHandleW(windows_sys::core::w!("ntdll.dll"));
68        if ntdll.is_null() {
69            return Err(Error::Masquerade("ntdll.dll module handle not found"));
70        }
71
72        let p_init_unicode = GetProcAddress(ntdll, c"RtlInitUnicodeString".as_ptr() as *const u8);
73        let p_acquire_lock = GetProcAddress(ntdll, c"RtlAcquirePebLock".as_ptr() as *const u8);
74        let p_release_lock = GetProcAddress(ntdll, c"RtlReleasePebLock".as_ptr() as *const u8);
75
76        let rtl_init_unicode_string: RtlInitUnicodeStringFn = p_init_unicode
77            .ok_or(Error::Masquerade("RtlInitUnicodeString export not found"))
78            .map(|f| std::mem::transmute(f))?;
79
80        let rtl_acquire_peb_lock: RtlAcquirePebLockFn = p_acquire_lock
81            .ok_or(Error::Masquerade("RtlAcquirePebLock export not found"))
82            .map(|f| std::mem::transmute(f))?;
83
84        let rtl_release_peb_lock: RtlReleasePebLockFn = p_release_lock
85            .ok_or(Error::Masquerade("RtlReleasePebLock export not found"))
86            .map(|f| std::mem::transmute(f))?;
87
88        Ok(Self {
89            rtl_init_unicode_string,
90            rtl_acquire_peb_lock,
91            rtl_release_peb_lock,
92        })
93    }
94}
95
96/// Errors returned by `buac` operations.
97#[derive(Debug)]
98pub enum Error {
99    /// Failed to initialize COM subsystem.
100    ComInit(HRESULT),
101    /// Failed during PEB masquerading or NTDLL function resolution.
102    Masquerade(&'static str),
103    /// Failed to activate the CMSTPLUA elevated COM moniker.
104    Activation(HRESULT),
105    /// Failed during ICMLuaUtil::ShellExec execution.
106    ShellExec(HRESULT),
107    /// Failed to query the process access token.
108    TokenQuery(u32),
109    /// Failed to determine the path to the current executable.
110    CurrentExePath(std::io::Error),
111}
112
113impl std::fmt::Display for Error {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        match self {
116            Self::ComInit(hr) => write!(f, "COM initialization failed with HRESULT: {hr:#010X}"),
117            Self::Masquerade(msg) => write!(f, "Process masquerade failed: {msg}"),
118            Self::Activation(hr) => {
119                write!(f, "Elevation moniker activation failed with HRESULT: {hr:#010X}")
120            }
121            Self::ShellExec(hr) => write!(f, "ShellExec failed with HRESULT: {hr:#010X}"),
122            Self::TokenQuery(code) => write!(f, "Token query failed with OS error {code}"),
123            Self::CurrentExePath(err) => {
124                write!(f, "Failed to retrieve current executable path: {err}")
125            }
126        }
127    }
128}
129
130impl std::error::Error for Error {}
131
132fn to_wide(s: &str) -> Vec<u16> {
133    std::ffi::OsStr::new(s)
134        .encode_wide()
135        .chain(std::iter::once(0))
136        .collect()
137}
138
139fn encode_arg(arg: &str) -> String {
140    if arg.is_empty() {
141        return "\"\"".to_string();
142    }
143    if !arg.contains([' ', '\t', '\n', '\x0b', '\"']) {
144        return arg.to_string();
145    }
146
147    let mut result = String::with_capacity(arg.len() + 2);
148    result.push('"');
149
150    let mut backslashes = 0;
151    for c in arg.chars() {
152        if c == '\\' {
153            backslashes += 1;
154        } else if c == '"' {
155            for _ in 0..backslashes * 2 + 1 {
156                result.push('\\');
157            }
158            backslashes = 0;
159            result.push('"');
160        } else {
161            for _ in 0..backslashes {
162                result.push('\\');
163            }
164            backslashes = 0;
165            result.push(c);
166        }
167    }
168
169    for _ in 0..backslashes * 2 {
170        result.push('\\');
171    }
172    result.push('"');
173
174    result
175}
176
177struct TokenGuard(HANDLE);
178
179impl Drop for TokenGuard {
180    fn drop(&mut self) {
181        unsafe { CloseHandle(self.0) };
182    }
183}
184
185/// Checks whether the current process token is running with elevated (Administrator) privileges.
186pub fn is_elevated() -> Result<bool, Error> {
187    unsafe {
188        let mut token: HANDLE = null_mut();
189        if OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) == 0 {
190            return Err(Error::TokenQuery(GetLastError()));
191        }
192
193        let _guard = TokenGuard(token);
194
195        let mut elevation = TOKEN_ELEVATION { TokenIsElevated: 0 };
196        let mut return_length = 0u32;
197
198        let result = GetTokenInformation(
199            token,
200            TokenElevation,
201            &mut elevation as *mut _ as *mut c_void,
202            std::mem::size_of::<TOKEN_ELEVATION>() as u32,
203            &mut return_length,
204        );
205
206        if result == 0 {
207            return Err(Error::TokenQuery(GetLastError()));
208        }
209
210        Ok(elevation.TokenIsElevated != 0)
211    }
212}
213
214struct PebOffsets {
215    ldr: usize,
216    params: usize,
217    in_load_order_list: usize,
218    full_dll_name: usize,
219    base_dll_name: usize,
220    image_path: usize,
221    command_line: usize,
222}
223
224#[cfg(target_arch = "x86_64")]
225const OFFSETS: PebOffsets = PebOffsets {
226    ldr: 0x18,
227    params: 0x20,
228    in_load_order_list: 0x10,
229    full_dll_name: 0x48,
230    base_dll_name: 0x58,
231    image_path: 0x60,
232    command_line: 0x70,
233};
234
235#[cfg(target_arch = "x86")]
236const OFFSETS: PebOffsets = PebOffsets {
237    ldr: 0x0C,
238    params: 0x10,
239    in_load_order_list: 0x0C,
240    full_dll_name: 0x24,
241    base_dll_name: 0x2C,
242    image_path: 0x38,
243    command_line: 0x40,
244};
245
246#[cfg(target_arch = "x86_64")]
247#[inline]
248unsafe fn get_peb() -> *mut u8 {
249    let teb: *mut u8;
250    std::arch::asm!("mov {}, gs:[0x30]", out(reg) teb, options(pure, nomem, nostack));
251    if teb.is_null() {
252        return null_mut();
253    }
254    *(teb.add(0x60) as *mut *mut u8)
255}
256
257#[cfg(target_arch = "x86")]
258#[inline]
259unsafe fn get_peb() -> *mut u8 {
260    let teb: *mut u8;
261    std::arch::asm!("mov {}, fs:[0x18]", out(reg) teb, options(pure, nomem, nostack));
262    if teb.is_null() {
263        return null_mut();
264    }
265    *(teb.add(0x30) as *mut *mut u8)
266}
267
268#[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))]
269compile_error!("buac only supports x86 and x86_64 Windows architectures");
270
271struct MasqueradeGuard {
272    image_path: *mut UnicodeString,
273    orig_image_path: UnicodeString,
274    command_line: *mut UnicodeString,
275    orig_command_line: UnicodeString,
276    full_dll_name: *mut UnicodeString,
277    orig_full_dll_name: UnicodeString,
278    base_dll_name: *mut UnicodeString,
279    orig_base_dll_name: UnicodeString,
280    acquire_lock: RtlAcquirePebLockFn,
281    release_lock: RtlReleasePebLockFn,
282}
283
284impl Drop for MasqueradeGuard {
285    fn drop(&mut self) {
286        unsafe {
287            (self.acquire_lock)();
288            *self.image_path = self.orig_image_path;
289            *self.command_line = self.orig_command_line;
290            *self.full_dll_name = self.orig_full_dll_name;
291            *self.base_dll_name = self.orig_base_dll_name;
292            (self.release_lock)();
293        }
294    }
295}
296
297unsafe fn masquerade_process() -> Result<MasqueradeGuard, Error> {
298    let ntdll = Ntdll::load()?;
299
300    let peb = get_peb();
301    if peb.is_null() {
302        return Err(Error::Masquerade("PEB address is null"));
303    }
304
305    let ldr = *(peb.add(OFFSETS.ldr) as *mut *mut u8);
306    let process_parameters = *(peb.add(OFFSETS.params) as *mut *mut u8);
307
308    if ldr.is_null() {
309        return Err(Error::Masquerade("PEB Ldr pointer is null"));
310    }
311    if process_parameters.is_null() {
312        return Err(Error::Masquerade("PEB ProcessParameters pointer is null"));
313    }
314
315    let in_load_order_list = *(ldr.add(OFFSETS.in_load_order_list) as *mut *mut u8);
316    if in_load_order_list.is_null() {
317        return Err(Error::Masquerade("Ldr InLoadOrderModuleList pointer is null"));
318    }
319
320    let image_path = process_parameters.add(OFFSETS.image_path) as *mut UnicodeString;
321    let command_line = process_parameters.add(OFFSETS.command_line) as *mut UnicodeString;
322    let full_dll_name = in_load_order_list.add(OFFSETS.full_dll_name) as *mut UnicodeString;
323    let base_dll_name = in_load_order_list.add(OFFSETS.base_dll_name) as *mut UnicodeString;
324
325    let orig_image_path = *image_path;
326    let orig_command_line = *command_line;
327    let orig_full_dll_name = *full_dll_name;
328    let orig_base_dll_name = *base_dll_name;
329
330    (ntdll.rtl_acquire_peb_lock)();
331
332    let fake_full_path = windows_sys::core::w!("C:\\Windows\\explorer.exe");
333    let fake_base_name = windows_sys::core::w!("explorer.exe");
334
335    (ntdll.rtl_init_unicode_string)(image_path, fake_full_path);
336    (ntdll.rtl_init_unicode_string)(command_line, fake_base_name);
337    (ntdll.rtl_init_unicode_string)(full_dll_name, fake_full_path);
338    (ntdll.rtl_init_unicode_string)(base_dll_name, fake_base_name);
339
340    (ntdll.rtl_release_peb_lock)();
341
342    Ok(MasqueradeGuard {
343        image_path,
344        orig_image_path,
345        command_line,
346        orig_command_line,
347        full_dll_name,
348        orig_full_dll_name,
349        base_dll_name,
350        orig_base_dll_name,
351        acquire_lock: ntdll.rtl_acquire_peb_lock,
352        release_lock: ntdll.rtl_release_peb_lock,
353    })
354}
355
356struct ComGuard {
357    need_uninit: bool,
358}
359
360impl Drop for ComGuard {
361    fn drop(&mut self) {
362        if self.need_uninit {
363            unsafe { CoUninitialize() };
364        }
365    }
366}
367
368struct LuaUtilGuard(*mut ICMLuaUtil);
369
370impl Drop for LuaUtilGuard {
371    fn drop(&mut self) {
372        if !self.0.is_null() {
373            unsafe {
374                let vtbl = (*self.0).lp_vtbl;
375                ((*vtbl).release)(self.0);
376            }
377        }
378    }
379}
380
381/// Executes a target binary elevated as Administrator using the CMSTPLUA COM elevation moniker.
382///
383/// Inherits the caller's current working directory.
384pub fn execute(target_path: &str, arguments: Option<&str>) -> Result<(), Error> {
385    unsafe {
386        let hr_init = CoInitializeEx(null(), COINIT_APARTMENTTHREADED as u32);
387        let _com_guard = ComGuard {
388            need_uninit: hr_init == S_OK || hr_init == S_FALSE,
389        };
390
391        if hr_init < 0 && hr_init != RPC_E_CHANGED_MODE {
392            return Err(Error::ComInit(hr_init));
393        }
394
395        let mut lua_util: *mut ICMLuaUtil = null_mut();
396        {
397            let _masquerade_guard = masquerade_process()?;
398
399            let mut bind_opts: BIND_OPTS3 = std::mem::zeroed();
400            bind_opts.Base.Base.cbStruct = std::mem::size_of::<BIND_OPTS3>() as u32;
401            bind_opts.Base.dwClassContext = CLSCTX_LOCAL_SERVER;
402
403            let hr_get = CoGetObject(
404                windows_sys::core::w!(
405                    "Elevation:Administrator!new:{3E5FC7F9-9A51-4367-9063-A120244FBEC7}"
406                ),
407                &bind_opts as *const _ as *const _,
408                &IID_ICMLUAUTIL,
409                &mut lua_util as *mut _ as *mut *mut c_void,
410            );
411
412            if hr_get != S_OK || lua_util.is_null() {
413                return Err(Error::Activation(hr_get));
414            }
415        }
416
417        let _lua_guard = LuaUtilGuard(lua_util);
418
419        let file_w = to_wide(target_path);
420        let params_w = arguments.map(to_wide);
421        let params_ptr = params_w.as_ref().map_or(null(), |p| p.as_ptr());
422
423        let cwd = std::env::current_dir()
424            .ok()
425            .map(|p| to_wide(&p.to_string_lossy()));
426        let cwd_ptr = cwd.as_ref().map_or(null(), |p| p.as_ptr());
427
428        let hr_exec = ((*(*lua_util).lp_vtbl).shell_exec)(
429            lua_util,
430            file_w.as_ptr(),
431            params_ptr,
432            cwd_ptr,
433            0,
434            SW_SHOW as u32,
435        );
436
437        if hr_exec != S_OK {
438            return Err(Error::ShellExec(hr_exec));
439        }
440
441        Ok(())
442    }
443}
444
445/// Spawns an elevated child instance of the current executable with original arguments.
446///
447/// Returns `Ok(true)` in the non-elevated parent process after successfully spawning the child.
448/// Returns `Ok(false)` if the current process is already elevated.
449pub fn spawn_elevated() -> Result<bool, Error> {
450    if is_elevated()? {
451        return Ok(false);
452    }
453
454    let current_exe = std::env::current_exe().map_err(Error::CurrentExePath)?;
455    let current_exe_str = current_exe.to_string_lossy();
456
457    let args: Vec<String> = std::env::args_os()
458        .skip(1)
459        .map(|a| a.to_string_lossy().into_owned())
460        .collect();
461
462    let args_str = if args.is_empty() {
463        None
464    } else {
465        Some(
466            args.iter()
467                .map(|a| encode_arg(a))
468                .collect::<Vec<String>>()
469                .join(" "),
470        )
471    };
472
473    execute(&current_exe_str, args_str.as_deref())?;
474    Ok(true)
475}
476
477/// Automatically restarts the current process with elevated privileges and terminates
478/// the non-elevated parent process immediately (`std::process::exit(0)`).
479///
480/// If already elevated, this function is a no-op and returns `Ok(())`.
481pub fn elevate() -> Result<(), Error> {
482    if spawn_elevated()? {
483        std::process::exit(0);
484    }
485    Ok(())
486}