Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(process): add RunAsUser #32

Merged
merged 27 commits into from
Jun 5, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cc9f00d
Add run as user
Legend-Master Jun 5, 2024
ad0ac56
Add blog link
Legend-Master Jun 5, 2024
df183c1
Clippy
Legend-Master Jun 5, 2024
0c93df4
Add change file
Legend-Master Jun 5, 2024
0dbb10e
Use alloc vec instead of local alloc
Legend-Master Jun 5, 2024
c5c8526
Move to lib.rs
Legend-Master Jun 5, 2024
97f3117
Vec should be u8
Legend-Master Jun 5, 2024
e3f8124
Exctract *handle == 0 to is_invalid
Legend-Master Jun 5, 2024
9f4238a
return false on CreateProcessW fail
Legend-Master Jun 5, 2024
56bbaf5
GetWindowThreadProcessId returns 0 on fail
Legend-Master Jun 5, 2024
10399d7
Add to demo
Legend-Master Jun 5, 2024
fe9aa88
Push arguments to command line on when not empty
Legend-Master Jun 5, 2024
07fec45
Use timeout instead of pause
Legend-Master Jun 5, 2024
72d2ac3
Use vec! instead of with_capacity
Legend-Master Jun 5, 2024
3f8b8b5
Use OwnedHandle
Legend-Master Jun 5, 2024
fcfc349
Refactor to OwnedHandle
Legend-Master Jun 5, 2024
c17427b
Use FALSE
Legend-Master Jun 5, 2024
5c950fa
Change invalid an drop
Legend-Master Jun 5, 2024
3129b27
Avoid extra cast
Legend-Master Jun 5, 2024
0496503
Avoid more extra cast
Legend-Master Jun 5, 2024
0a7e900
More true false
Legend-Master Jun 5, 2024
19fd725
Avoid tricky lifetime from direct as_mut_ptr
Legend-Master Jun 5, 2024
08f20df
Apply suggestions from code review
Legend-Master Jun 5, 2024
58a259c
Shouldn't use OwnedHandle GetShellWindow
Legend-Master Jun 5, 2024
6184076
Update crates/nsis-process/src/lib.rs
amrbashir Jun 5, 2024
820d702
Update .changes/run-as-user.md
amrbashir Jun 5, 2024
8217ea0
Update crates/nsis-process/src/lib.rs
amrbashir Jun 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changes/run-as-user.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nsis_process": "minor"
---

Add a new function `RunAsUser` to run command as unelevated user
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
232 changes: 178 additions & 54 deletions crates/nsis-process/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@

extern crate alloc;

use alloc::vec;
use alloc::vec::Vec;
use core::{ffi::c_void, mem, ptr};
use alloc::{borrow::ToOwned, vec, vec::Vec};
use core::{ffi::c_void, mem, ops::Deref, ops::DerefMut, ptr};

use nsis_plugin_api::*;
use windows_sys::Win32::{
Foundation::{CloseHandle, HANDLE},
Foundation::{CloseHandle, GetLastError, ERROR_INSUFFICIENT_BUFFER, FALSE, HANDLE, TRUE},
Security::{EqualSid, GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER},
System::{
Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
TH32CS_SNAPPROCESS,
},
Threading::{
GetCurrentProcessId, OpenProcess, OpenProcessToken, TerminateProcess,
PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE,
CreateProcessW, GetCurrentProcessId, InitializeProcThreadAttributeList, OpenProcess,
OpenProcessToken, TerminateProcess, UpdateProcThreadAttribute,
CREATE_NEW_PROCESS_GROUP, CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT,
LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_CREATE_PROCESS, PROCESS_INFORMATION,
PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
STARTUPINFOEXW, STARTUPINFOW,
},
},
UI::WindowsAndMessaging::{GetShellWindow, GetWindowThreadProcessId},
};

nsis_plugin!();
Expand Down Expand Up @@ -118,97 +122,212 @@ fn KillProcessCurrentUser() -> Result<(), Error> {
}
}

/// Run command as unelevated user
///
/// Needs 2 strings on the stack
/// $1: command
/// $2: arguments
#[nsis_fn]
fn RunAsUser() -> Result<(), Error> {
let command = popstr()?;
let arguments = popstr()?;
if run_as_user(&command, &arguments) {
push(ZERO)
} else {
push(ONE)
}
}

unsafe fn belongs_to_user(user_sid: *mut c_void, pid: u32) -> bool {
let p_sid = get_sid(pid);
// Trying to get the sid of a process of another user will give us an "Access Denied" error.
// TODO: Consider checking for HRESULT(0x80070005) if we want to return true for other errors to try and kill those processes later.
p_sid
.map(|p_sid| EqualSid(user_sid, p_sid) != 0)
.map(|p_sid| EqualSid(user_sid, p_sid) != FALSE)
.unwrap_or_default()
}

fn kill(pid: u32) -> bool {
unsafe {
let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
let success = TerminateProcess(handle, 1);
CloseHandle(handle);
success != 0
let handle = OwnedHandle::new(OpenProcess(PROCESS_TERMINATE, 0, pid));
let success = TerminateProcess(*handle, 1);
success != FALSE
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
}
}

// Get the SID of a process. Returns None on error.
unsafe fn get_sid(pid: u32) -> Option<*mut c_void> {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);

let mut sid = None;
let mut token_handle = HANDLE::default();

if OpenProcessToken(handle, TOKEN_QUERY, &mut token_handle) != 0 {
let mut info_length = 0;

GetTokenInformation(
token_handle,
TokenUser,
ptr::null_mut(),
0,
&mut info_length as *mut u32,
);

// GetTokenInformation always returns 0 for the first call so we check if it still gave us the buffer length
if info_length == 0 {
return sid;
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
}

let info = vec![0u8; info_length as usize].as_mut_ptr() as *mut TOKEN_USER;

if GetTokenInformation(
token_handle,
TokenUser,
info as *mut c_void,
info_length,
&mut info_length,
) == 0
{
return sid;
}

sid = Some((*info).User.Sid)
let handle = OwnedHandle::new(OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid));
if handle.is_invalid() {
return None;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let mut token_handle = OwnedHandle::new(HANDLE::default());
if OpenProcessToken(*handle, TOKEN_QUERY, &mut *token_handle) == FALSE {
return None;
}

CloseHandle(token_handle);
CloseHandle(handle);
let mut info_length = 0;
GetTokenInformation(
*token_handle,
TokenUser,
ptr::null_mut(),
0,
&mut info_length,
);
// GetTokenInformation always returns 0 for the first call so we check if it still gave us the buffer length
if info_length == 0 {
return None;
}

sid
let mut buffer = vec![0u8; info_length as usize];
let info = buffer.as_mut_ptr() as *mut TOKEN_USER;
if GetTokenInformation(
*token_handle,
TokenUser,
info as *mut c_void,
info_length,
&mut info_length,
) == FALSE
{
None
} else {
Some((*info).User.Sid)
}
}

fn get_processes(name: &str) -> Vec<u32> {
let current_pid = unsafe { GetCurrentProcessId() };
let mut processes = Vec::new();

unsafe {
let handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
let handle = OwnedHandle::new(CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0));

let mut process = PROCESSENTRY32W {
dwSize: mem::size_of::<PROCESSENTRY32W>() as u32,
..mem::zeroed()
};

if Process32FirstW(handle, &mut process) != 0 {
while Process32NextW(handle, &mut process) != 0 {
if Process32FirstW(*handle, &mut process) == TRUE {
while Process32NextW(*handle, &mut process) == TRUE {
if current_pid != process.th32ProcessID
&& decode_utf16_lossy(&process.szExeFile).to_lowercase() == name.to_lowercase()
{
processes.push(process.th32ProcessID);
}
}
}

CloseHandle(handle);
}

processes
}

/// Return true if success
///
/// Ported from https://devblogs.microsoft.com/oldnewthing/20190425-00/?p=102443
unsafe fn run_as_user(command: &str, arguments: &str) -> bool {
let hwnd = OwnedHandle::new(GetShellWindow());
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
if hwnd.is_invalid() {
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let mut proccess_id = 0;
if GetWindowThreadProcessId(*hwnd, &mut proccess_id) == 0 {
amrbashir marked this conversation as resolved.
Show resolved Hide resolved
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let process = OwnedHandle::new(OpenProcess(PROCESS_CREATE_PROCESS, FALSE, proccess_id));
if process.is_invalid() {
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let mut size = 0;
if !(InitializeProcThreadAttributeList(ptr::null_mut(), 1, 0, &mut size) == FALSE
&& GetLastError() == ERROR_INSUFFICIENT_BUFFER)
{
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let mut buffer = vec![0u8; size];
let attribute_list = buffer.as_mut_ptr() as LPPROC_THREAD_ATTRIBUTE_LIST;
if InitializeProcThreadAttributeList(attribute_list, 1, 0, &mut size) == FALSE {
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
if UpdateProcThreadAttribute(
attribute_list,
0,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS as _,
&*process as *const _ as _,
mem::size_of::<HANDLE>(),
ptr::null_mut(),
ptr::null(),
) == FALSE
{
return false;
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let startup_info = STARTUPINFOEXW {
StartupInfo: STARTUPINFOW {
cb: mem::size_of::<STARTUPINFOEXW>() as _,
..mem::zeroed()
},
lpAttributeList: attribute_list,
};
let mut process_info = PROCESS_INFORMATION { ..mem::zeroed() };
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
let mut command_line = command.to_owned();
if !arguments.is_empty() {
command_line.push(' ');
command_line.push_str(arguments);
}
Legend-Master marked this conversation as resolved.
Show resolved Hide resolved
if CreateProcessW(
encode_utf16(command).as_ptr(),
encode_utf16(&command_line).as_mut_ptr(),
ptr::null(),
ptr::null(),
FALSE,
CREATE_UNICODE_ENVIRONMENT | CREATE_NEW_PROCESS_GROUP | EXTENDED_STARTUPINFO_PRESENT,
ptr::null(),
ptr::null(),
&startup_info as *const _ as _,
&mut process_info,
) == FALSE
{
false
} else {
CloseHandle(process_info.hProcess);
CloseHandle(process_info.hThread);
true
}
}

struct OwnedHandle(HANDLE);

impl OwnedHandle {
fn new(handle: HANDLE) -> Self {
Self(handle)
}

fn is_invalid(&self) -> bool {
self.0 == 0
}
}

impl Drop for OwnedHandle {
fn drop(&mut self) {
if !self.is_invalid() {
unsafe { CloseHandle(self.0) };
}
}
}

impl Deref for OwnedHandle {
type Target = HANDLE;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl DerefMut for OwnedHandle {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -226,4 +345,9 @@ mod tests {
// This will return true on empty iterators so it's basically no-op right now
assert!(processes.into_iter().map(kill).all(|b| b));
}

#[test]
fn spawn_cmd() {
unsafe { run_as_user("cmd", "/c timeout 3") };
}
}
13 changes: 4 additions & 9 deletions crates/nsis-tauri-utils/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::io::Write;

fn main() {
combine_plugins_and_write_to_out_dir();
if std::env::var("CARGO_FEATURE_TEST").as_deref() != Ok("1") {
Expand All @@ -13,14 +15,7 @@ fn main() {
fn combine_plugins_and_write_to_out_dir() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let path = format!("{out_dir}/combined_libs.rs");

let mut file = std::fs::File::options()
.truncate(true)
.write(true)
.create(true)
.open(path)
.unwrap();

let mut file = std::fs::File::create(path).unwrap();
for plugin in [
include_str!("../nsis-semvercompare/src/lib.rs"),
include_str!("../nsis-process/src/lib.rs"),
Expand All @@ -39,6 +34,6 @@ fn combine_plugins_and_write_to_out_dir() {

// skip last line which should be #[cfg(test)]
let content = lines[..lines.len() - 1].join("\n");
std::io::Write::write_all(&mut file, content.as_bytes()).unwrap();
file.write_all(content.as_bytes()).unwrap();
}
}
5 changes: 4 additions & 1 deletion demo.nsi
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,7 @@ Section
nsis_process::FindProcess "abcdef.exe"
Pop $1
DetailPrint "FindProcess(abcdef.exe): $1"
SectionEnd
nsis_process::RunAsUser "C:\\Windows\\System32\\cmd.exe" "/c timeout 3"
Pop $1
DetailPrint "RunAsUser(cmd, /c timeout 3): $1"
SectionEnd
Loading