├── .gitignore ├── utils ├── lift.ico ├── lift.png ├── manifest.manifest ├── Cargo.toml ├── build.rs ├── resource.rc └── bin │ ├── eledo.rs │ ├── normdo.rs │ └── ptybridge.rs ├── Cargo.toml ├── deelevate ├── examples │ ├── show.rs │ ├── spawn.rs │ └── impersonate.rs ├── src │ ├── lib.rs │ ├── procthreadattr.rs │ ├── process.rs │ ├── sid.rs │ ├── psuedocon.rs │ ├── spawn.rs │ ├── pipe.rs │ ├── command.rs │ ├── bridge.rs │ └── token.rs └── Cargo.toml ├── .github └── workflows │ ├── rust.yml │ └── continuous.yml ├── LICENSE.md ├── spawn-output.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .*.sw* 2 | /target 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /utils/lift.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wez/EleDo/HEAD/utils/lift.ico -------------------------------------------------------------------------------- /utils/lift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wez/EleDo/HEAD/utils/lift.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "utils", 4 | "deelevate" 5 | ] 6 | -------------------------------------------------------------------------------- /utils/manifest.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | UTF-8 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /deelevate/examples/show.rs: -------------------------------------------------------------------------------- 1 | //! This example prints the effective privilege output from 2 | //! `whoami` as well as our understanding of that level 3 | use deelevate::Token; 4 | 5 | fn whoami() { 6 | let output = std::process::Command::new("whoami.exe") 7 | .arg("/groups") 8 | .output() 9 | .expect("failed to run whoami"); 10 | let stdout = String::from_utf8_lossy(&output.stdout); 11 | println!("{}", stdout); 12 | } 13 | 14 | fn main() { 15 | whoami(); 16 | 17 | let token = Token::with_current_process().unwrap(); 18 | let level = token.privilege_level().unwrap(); 19 | println!("priv level is {:?}", level); 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: "vs2017-win2016" 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Build 13 | run: cargo build --release --verbose 14 | - name: Run tests 15 | run: cargo test --release --verbose 16 | - name: "Capture utilities" 17 | shell: bash 18 | run: | 19 | mkdir pkg_ 20 | mv target/release/eledo.exe target/release/eledo-pty-bridge.exe target/release/normdo.exe pkg_/ 21 | cp README.md LICENSE.md pkg_ 22 | - name: "Upload artifact" 23 | uses: actions/upload-artifact@master 24 | with: 25 | name: "eledo" 26 | path: "pkg_" 27 | -------------------------------------------------------------------------------- /deelevate/examples/spawn.rs: -------------------------------------------------------------------------------- 1 | //! This example prints the effective privilege output from 2 | //! `whoami` as well as our understanding of that level 3 | use deelevate::{spawn_with_normal_privileges, Token}; 4 | 5 | fn whoami() { 6 | let output = std::process::Command::new("whoami.exe") 7 | .arg("/groups") 8 | .output() 9 | .expect("failed to run whoami"); 10 | let stdout = String::from_utf8_lossy(&output.stdout); 11 | println!("{}", stdout); 12 | } 13 | 14 | fn main() { 15 | whoami(); 16 | 17 | let token = Token::with_current_process().unwrap(); 18 | let level = token.privilege_level().unwrap(); 19 | println!("priv level is {:?}", level); 20 | 21 | spawn_with_normal_privileges().unwrap(); 22 | println!("now I'm safe to proceed with reduced privs"); 23 | } 24 | -------------------------------------------------------------------------------- /utils/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "utils" 3 | version = "0.1.1" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | description = "Drop privileges on Windows" 7 | license = "MIT" 8 | documentation = "https://docs.rs/deelevate" 9 | readme = "../README.md" 10 | keywords = ["UAC", "elevate", "privileges"] 11 | build = "build.rs" 12 | 13 | [[bin]] 14 | name = "eledo" 15 | path = "bin/eledo.rs" 16 | 17 | [[bin]] 18 | name = "normdo" 19 | path = "bin/normdo.rs" 20 | 21 | [[bin]] 22 | name = "eledo-pty-bridge" 23 | path = "bin/ptybridge.rs" 24 | 25 | [build-dependencies] 26 | embed-resource = "1.7" 27 | vergen = "3" 28 | cc = "1.0" 29 | 30 | [dependencies] 31 | structopt = "0.3" 32 | pathsearch = "0.2" 33 | deelevate = { path = "../deelevate" } 34 | winapi = { version = "0.3", features = [ 35 | "wincon", 36 | "winnls", 37 | ]} 38 | -------------------------------------------------------------------------------- /deelevate/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::io::Error as IoError; 3 | use std::os::windows::ffi::OsStrExt; 4 | 5 | mod bridge; 6 | mod command; 7 | mod pipe; 8 | mod process; 9 | mod procthreadattr; 10 | mod psuedocon; 11 | mod sid; 12 | mod spawn; 13 | mod token; 14 | 15 | pub use bridge::{BridgePtyClient, BridgeServer}; 16 | pub use command::Command; 17 | #[doc(hidden)] 18 | pub use pipe::PipeHandle; 19 | pub use spawn::{spawn_with_elevated_privileges, spawn_with_normal_privileges}; 20 | pub use token::PrivilegeLevel; 21 | pub use token::Token; 22 | 23 | fn win32_error_with_context(context: &str, err: IoError) -> IoError { 24 | IoError::new(err.kind(), format!("{}: {}", context, err)) 25 | } 26 | 27 | fn os_str_to_null_terminated_vec(s: &OsStr) -> Vec { 28 | s.encode_wide().chain(std::iter::once(0)).collect() 29 | } 30 | -------------------------------------------------------------------------------- /utils/build.rs: -------------------------------------------------------------------------------- 1 | use vergen::{generate_cargo_keys, ConstantsFlags}; 2 | fn main() { 3 | let mut flags = ConstantsFlags::all(); 4 | flags.remove(ConstantsFlags::SEMVER_FROM_CARGO_PKG); 5 | generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); 6 | println!("cargo:rerun-if-changed=resource.rc"); 7 | 8 | // Obtain MSVC environment so that the rc compiler can find the right headers. 9 | // https://github.com/nabijaczleweli/rust-embed-resource/issues/11#issuecomment-603655972 10 | let target = std::env::var("TARGET").unwrap(); 11 | if let Some(tool) = cc::windows_registry::find_tool(target.as_str(), "cl.exe") { 12 | for (key, value) in tool.env() { 13 | std::env::set_var(key, value); 14 | } 15 | } 16 | 17 | embed_resource::compile("resource.rc"); 18 | } 19 | -------------------------------------------------------------------------------- /deelevate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deelevate" 3 | version = "0.2.0" 4 | authors = ["Wez Furlong"] 5 | edition = "2018" 6 | description = "Drop privileges on Windows" 7 | license = "MIT" 8 | documentation = "https://docs.rs/deelevate" 9 | readme = "../README.md" 10 | keywords = ["UAC", "elevate", "privileges"] 11 | 12 | [lib] 13 | crate-type = ["lib", "staticlib"] 14 | 15 | [dependencies] 16 | pathsearch = "0.2" 17 | lazy_static = "1.4" 18 | rand = "0.8" 19 | shared_library = "0.1" 20 | termwiz = "0.15" 21 | winapi = { version = "0.3", features = [ 22 | "accctrl", 23 | "aclapi", 24 | "combaseapi", 25 | "consoleapi", 26 | "errhandlingapi", 27 | "fileapi", 28 | "handleapi", 29 | "ioapiset", 30 | "namedpipeapi", 31 | "objbase", 32 | "processenv", 33 | "processthreadsapi", 34 | "securitybaseapi", 35 | "shellapi", 36 | "synchapi", 37 | "userenv", 38 | "winbase", 39 | "wincontypes", 40 | "winerror", 41 | "winsafer", 42 | "winuser", 43 | ]} 44 | -------------------------------------------------------------------------------- /.github/workflows/continuous.yml: -------------------------------------------------------------------------------- 1 | name: Continuous 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: "vs2017-win2016" 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Build 13 | run: cargo build --release --verbose 14 | - name: "Build Release" 15 | shell: bash 16 | run: | 17 | mkdir EleDo 18 | mv target/release/eledo.exe target/release/eledo-pty-bridge.exe target/release/normdo.exe EleDo/ 19 | cp README.md LICENSE.md EleDo 20 | cd EleDo 21 | 7z a -tzip ../EleDo.zip * 22 | - name: "Upload artifact" 23 | uses: actions/upload-artifact@master 24 | with: 25 | name: "EleDo" 26 | path: "EleDo.zip" 27 | - name: "Upload to Continuous Release" 28 | uses: wez/upload-release-assets@releases/v1 29 | with: 30 | files: "EleDo.zip" 31 | release-tag: "Continuous" 32 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 33 | 34 | -------------------------------------------------------------------------------- /utils/resource.rc: -------------------------------------------------------------------------------- 1 | #include 2 | #define IDI_ICON 0x101 3 | IDI_ICON ICON "lift.ico" 4 | APP_MANIFEST RT_MANIFEST manifest.manifest 5 | VS_VERSION_INFO VERSIONINFO 6 | FILEVERSION 1,0,0,0 7 | PRODUCTVERSION 1,0,0,0 8 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 9 | FILEFLAGS 0 10 | FILEOS VOS__WINDOWS32 11 | FILETYPE VFT_APP 12 | FILESUBTYPE VFT2_UNKNOWN 13 | BEGIN 14 | BLOCK "StringFileInfo" 15 | BEGIN 16 | BLOCK "040904E4" 17 | BEGIN 18 | VALUE "CompanyName", "Wez Furlong\0" 19 | VALUE "FileDescription", "Elevated-Do Helper\0" 20 | VALUE "FileVersion", "1.0\0" 21 | VALUE "LegalCopyright", "Wez Furlong, MIT licensed\0" 22 | VALUE "InternalName", "\0" 23 | VALUE "OriginalFilename", "\0" 24 | VALUE "ProductName", "Elevated-Do\0" 25 | VALUE "ProductVersion", "1.0\0" 26 | END 27 | END 28 | BLOCK "VarFileInfo" 29 | BEGIN 30 | VALUE "Translation", 0x409, 1252 31 | END 32 | END 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wez Furlong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deelevate/examples/impersonate.rs: -------------------------------------------------------------------------------- 1 | //! This example demonstrates that impersonation isn't sufficient 2 | //! to effectively reduce privileges. It does this by running 3 | //! `whoami` before and after an impersonation request and showing 4 | //! that the effective prileges remain the same. 5 | 6 | use deelevate::Token; 7 | 8 | fn whoami() { 9 | let output = std::process::Command::new("whoami.exe") 10 | .arg("/groups") 11 | .output() 12 | .expect("failed to run whoami"); 13 | let stdout = String::from_utf8_lossy(&output.stdout); 14 | println!("{}", stdout); 15 | } 16 | 17 | fn main() { 18 | whoami(); 19 | 20 | let token = Token::with_current_process().unwrap(); 21 | let level = token.privilege_level().unwrap(); 22 | println!("priv level is {:?}", level); 23 | 24 | let medium = token 25 | .as_medium_integrity_safer_token() 26 | .expect("failed to make medium token"); 27 | 28 | medium 29 | .impersonate() 30 | .expect("failed to impersonate self with medium token"); 31 | 32 | println!("impersonation successful, but note that it isn't effective in changing whoami!"); 33 | 34 | whoami(); 35 | } 36 | -------------------------------------------------------------------------------- /deelevate/src/procthreadattr.rs: -------------------------------------------------------------------------------- 1 | use crate::psuedocon::HPCON; 2 | use crate::win32_error_with_context; 3 | use std::io::{Error as IoError, Result as IoResult}; 4 | use std::mem; 5 | use std::ptr; 6 | use winapi::shared::minwindef::DWORD; 7 | use winapi::um::processthreadsapi::*; 8 | 9 | const PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE: usize = 0x00020016; 10 | 11 | pub struct ProcThreadAttributeList { 12 | data: Vec, 13 | } 14 | 15 | impl ProcThreadAttributeList { 16 | pub fn with_capacity(num_attributes: DWORD) -> IoResult { 17 | let mut bytes_required: usize = 0; 18 | unsafe { 19 | InitializeProcThreadAttributeList( 20 | ptr::null_mut(), 21 | num_attributes, 22 | 0, 23 | &mut bytes_required, 24 | ) 25 | }; 26 | let mut data = Vec::with_capacity(bytes_required); 27 | // We have the right capacity, so force the vec to consider itself 28 | // that length. The contents of those bytes will be maintained 29 | // by the win32 apis used in this impl. 30 | unsafe { data.set_len(bytes_required) }; 31 | 32 | let attr_ptr = data.as_mut_slice().as_mut_ptr() as *mut _; 33 | let res = unsafe { 34 | InitializeProcThreadAttributeList(attr_ptr, num_attributes, 0, &mut bytes_required) 35 | }; 36 | if res == 0 { 37 | Err(win32_error_with_context( 38 | "InitializeProcThreadAttributeList failed", 39 | IoError::last_os_error(), 40 | )) 41 | } else { 42 | Ok(Self { data }) 43 | } 44 | } 45 | 46 | pub fn as_mut_ptr(&mut self) -> LPPROC_THREAD_ATTRIBUTE_LIST { 47 | self.data.as_mut_slice().as_mut_ptr() as *mut _ 48 | } 49 | 50 | pub fn set_pty(&mut self, con: HPCON) -> IoResult<()> { 51 | let res = unsafe { 52 | UpdateProcThreadAttribute( 53 | self.as_mut_ptr(), 54 | 0, 55 | PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, 56 | con, 57 | mem::size_of::(), 58 | ptr::null_mut(), 59 | ptr::null_mut(), 60 | ) 61 | }; 62 | if res == 0 { 63 | Err(win32_error_with_context( 64 | "UpdateProcThreadAttribute failed", 65 | IoError::last_os_error(), 66 | )) 67 | } else { 68 | Ok(()) 69 | } 70 | } 71 | } 72 | 73 | impl Drop for ProcThreadAttributeList { 74 | fn drop(&mut self) { 75 | unsafe { DeleteProcThreadAttributeList(self.as_mut_ptr()) }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /deelevate/src/process.rs: -------------------------------------------------------------------------------- 1 | //! Working with process handles 2 | use crate::win32_error_with_context; 3 | use std::io::{Error as IoError, Result as IoResult}; 4 | use winapi::shared::minwindef::DWORD; 5 | use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; 6 | use winapi::um::processthreadsapi::{GetExitCodeProcess, OpenProcess}; 7 | use winapi::um::synchapi::WaitForSingleObject; 8 | use winapi::um::winbase::{INFINITE, WAIT_FAILED}; 9 | use winapi::um::winnt::HANDLE; 10 | 11 | /// An owning wrapper around handles that represent processes 12 | pub struct Process(HANDLE); 13 | /// The compiler thinks it isn't send because HANDLE is a pointer 14 | /// type. We happen to know that moving the handle between threads 15 | /// is totally fine, hence this impl. 16 | unsafe impl Send for Process {} 17 | 18 | impl Drop for Process { 19 | fn drop(&mut self) { 20 | unsafe { 21 | CloseHandle(self.0); 22 | } 23 | } 24 | } 25 | 26 | impl Process { 27 | /// Delegates to the OpenProcess win32 API 28 | pub fn with_process_id( 29 | desired_access: DWORD, 30 | inherit_handles: bool, 31 | pid: DWORD, 32 | ) -> IoResult { 33 | let proc = unsafe { OpenProcess(desired_access, inherit_handles as _, pid) }; 34 | if proc == INVALID_HANDLE_VALUE { 35 | Err(win32_error_with_context( 36 | "OpenProcess", 37 | IoError::last_os_error(), 38 | )) 39 | } else { 40 | Ok(Self(proc)) 41 | } 42 | } 43 | 44 | /// Returns the underlying raw handle value 45 | pub fn as_handle(&self) -> HANDLE { 46 | self.0 47 | } 48 | 49 | /// Takes ownership of the provided handle and will close 50 | /// it when this Process instance is dropped! 51 | pub fn with_handle(proc: HANDLE) -> Self { 52 | Self(proc) 53 | } 54 | 55 | /// Wait for the specified duration (in milliseconds!) to pass. 56 | /// Use None to wait forever. 57 | pub fn wait_for(&self, duration: Option) -> IoResult { 58 | let res = unsafe { WaitForSingleObject(self.0, duration.unwrap_or(INFINITE)) }; 59 | if res == WAIT_FAILED { 60 | Err(win32_error_with_context( 61 | "WaitForSingleObject(process)", 62 | IoError::last_os_error(), 63 | )) 64 | } else { 65 | Ok(res) 66 | } 67 | } 68 | 69 | /// Retrieves the exit code from the process 70 | pub fn exit_code(&self) -> IoResult { 71 | let mut exit_code = 0; 72 | if unsafe { GetExitCodeProcess(self.0, &mut exit_code) } != 0 { 73 | Ok(exit_code) 74 | } else { 75 | Err(win32_error_with_context( 76 | "GetExitCodeProcess", 77 | IoError::last_os_error(), 78 | )) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /utils/bin/eledo.rs: -------------------------------------------------------------------------------- 1 | use deelevate::{BridgeServer, Command, PrivilegeLevel, Token}; 2 | use pathsearch::find_executable_in_path; 3 | use std::ffi::OsString; 4 | use structopt::*; 5 | 6 | /// EleDo - "Do" a command with Elevated privileges 7 | /// 8 | /// EleDo will check to see if the current context has admin privileges. 9 | /// If it does then it will execute the requested program directly, 10 | /// returning the exit status from that program. 11 | /// 12 | /// Otherwise, EleDo will arrange to run the program with an elevated 13 | /// PTY that is bridged to the current terminal session. Elevation 14 | /// requires that the current process be able to communicate with the 15 | /// shell in the current desktop session and will typically trigger 16 | /// a UAC prompt for that user to confirm that elevation should occur. 17 | /// 18 | /// Example: 19 | /// `eledo whoami /groups` 20 | #[derive(StructOpt)] 21 | #[structopt( 22 | about = "EleDo - \"Do\" a command with Elevated privileges", 23 | author = "Wez Furlong", 24 | setting(clap::AppSettings::TrailingVarArg), 25 | setting(clap::AppSettings::ArgRequiredElseHelp), 26 | version = env!("VERGEN_SEMVER_LIGHTWEIGHT") 27 | )] 28 | #[derive(Debug)] 29 | struct Opt { 30 | #[structopt(value_name("PROGRAM"), parse(from_os_str))] 31 | args: Vec, 32 | } 33 | 34 | fn main() -> std::io::Result<()> { 35 | let mut opt = Opt::from_args(); 36 | 37 | let token = Token::with_current_process()?; 38 | let level = token.privilege_level()?; 39 | 40 | opt.args[0] = match find_executable_in_path(&opt.args[0]) { 41 | Some(path) => path.into(), 42 | None => { 43 | eprintln!("Unable to find {:?} in path", opt.args[0]); 44 | std::process::exit(1); 45 | } 46 | }; 47 | 48 | let target_token = match level { 49 | PrivilegeLevel::NotPrivileged | PrivilegeLevel::HighIntegrityAdmin => { 50 | token.as_medium_integrity_safer_token()? 51 | } 52 | PrivilegeLevel::Elevated => Token::with_shell_process()?, 53 | }; 54 | 55 | let mut command = Command::with_environment_for_token(&target_token)?; 56 | 57 | let exit_code = match level { 58 | PrivilegeLevel::Elevated | PrivilegeLevel::HighIntegrityAdmin => { 59 | // We already have privs, so just run it directly 60 | command.set_argv(opt.args); 61 | let proc = command.spawn()?; 62 | let _ = proc.wait_for(None); 63 | proc.exit_code()? 64 | } 65 | PrivilegeLevel::NotPrivileged => { 66 | let mut server = BridgeServer::new(); 67 | 68 | let mut bridge_cmd = server.start_for_command(&mut opt.args, &target_token)?; 69 | 70 | let proc = bridge_cmd.shell_execute("runas")?; 71 | server.serve(proc)? 72 | } 73 | }; 74 | std::process::exit(exit_code as _); 75 | } 76 | -------------------------------------------------------------------------------- /utils/bin/normdo.rs: -------------------------------------------------------------------------------- 1 | use deelevate::{BridgeServer, Command, PrivilegeLevel, Token}; 2 | use pathsearch::find_executable_in_path; 3 | use std::ffi::OsString; 4 | use structopt::*; 5 | 6 | /// NormDo - "Do" a command with Normal privileges 7 | /// 8 | /// NormDo will check to see if the current context has admin privileges. 9 | /// If it doesn't then it will execute the requested program directly, 10 | /// returning the exit status from that program. 11 | /// 12 | /// Otherwise, NormDo will arrange to run the program with a Normal 13 | /// user token with Medium integrity level, dropping/denying the local 14 | /// administrator group from the token. The program will be run in a 15 | /// PTY that is bridged to the current terminal session. 16 | /// 17 | /// Example: 18 | /// `normdo whoami /groups` 19 | #[derive(StructOpt)] 20 | #[structopt( 21 | about = "NormDo - \"Do\" a command with Normal privileges", 22 | author = "Wez Furlong", 23 | setting(clap::AppSettings::TrailingVarArg), 24 | setting(clap::AppSettings::ArgRequiredElseHelp), 25 | version = env!("VERGEN_SEMVER_LIGHTWEIGHT") 26 | )] 27 | #[derive(Debug)] 28 | struct Opt { 29 | #[structopt(value_name("PROGRAM"), parse(from_os_str))] 30 | args: Vec, 31 | } 32 | 33 | fn main() -> std::io::Result<()> { 34 | let mut opt = Opt::from_args(); 35 | 36 | let token = Token::with_current_process()?; 37 | let level = token.privilege_level()?; 38 | 39 | opt.args[0] = match find_executable_in_path(&opt.args[0]) { 40 | Some(path) => path.into(), 41 | None => { 42 | eprintln!("Unable to find {:?} in path", opt.args[0]); 43 | std::process::exit(1); 44 | } 45 | }; 46 | 47 | let target_token = match level { 48 | PrivilegeLevel::NotPrivileged => token, 49 | PrivilegeLevel::HighIntegrityAdmin => token.as_medium_integrity_safer_token()?, 50 | PrivilegeLevel::Elevated => Token::with_shell_process()?, 51 | }; 52 | 53 | let mut command = Command::with_environment_for_token(&target_token)?; 54 | 55 | let exit_code = match level { 56 | PrivilegeLevel::NotPrivileged => { 57 | // We're already normal, so just run it directly 58 | command.set_argv(opt.args); 59 | let proc = command.spawn()?; 60 | let _ = proc.wait_for(None); 61 | proc.exit_code()? 62 | } 63 | PrivilegeLevel::HighIntegrityAdmin | PrivilegeLevel::Elevated => { 64 | let mut server = BridgeServer::new(); 65 | 66 | let mut bridge_cmd = server.start_for_command(&mut opt.args, &target_token)?; 67 | 68 | let proc = match level { 69 | PrivilegeLevel::Elevated => bridge_cmd.spawn_with_token(&target_token)?, 70 | PrivilegeLevel::NotPrivileged | PrivilegeLevel::HighIntegrityAdmin => { 71 | bridge_cmd.spawn_as_user(&target_token)? 72 | } 73 | }; 74 | 75 | server.serve(proc)? 76 | } 77 | }; 78 | 79 | std::process::exit(exit_code as _); 80 | } 81 | -------------------------------------------------------------------------------- /deelevate/src/sid.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Error as IoError, Result as IoResult}; 2 | use winapi::shared::minwindef::DWORD; 3 | use winapi::shared::winerror::ERROR_INSUFFICIENT_BUFFER; 4 | use winapi::um::errhandlingapi::GetLastError; 5 | use winapi::um::securitybaseapi::{CreateWellKnownSid, GetLengthSid, IsWellKnownSid}; 6 | use winapi::um::winnt::SID; 7 | use winapi::um::winnt::WELL_KNOWN_SID_TYPE; 8 | 9 | /// A little helper trait to make it easier to operate on SIDs 10 | /// that reside in various storage 11 | pub trait AsSid { 12 | fn as_sid(self) -> *const SID; 13 | } 14 | 15 | impl AsSid for *const SID { 16 | fn as_sid(self) -> *const SID { 17 | self 18 | } 19 | } 20 | 21 | /// Return true if sid matches the requested well known sid type 22 | pub fn is_well_known(sid: S, sid_type: WELL_KNOWN_SID_TYPE) -> bool { 23 | let result = unsafe { IsWellKnownSid(sid.as_sid() as *mut _, sid_type) }; 24 | result == 1 25 | } 26 | 27 | pub fn get_length_sid(sid: S) -> DWORD { 28 | unsafe { GetLengthSid(sid.as_sid() as *mut _) } 29 | } 30 | 31 | /// Stores the data for a well known sid instance 32 | pub struct WellKnownSid { 33 | data: Vec, 34 | } 35 | 36 | impl WellKnownSid { 37 | /// Construct a Sid from a well known sid identifier 38 | pub fn with_well_known(sid_type: WELL_KNOWN_SID_TYPE) -> IoResult { 39 | // Measure storage requirements by doing a dry run with no buffer 40 | let mut size: DWORD = 0; 41 | let err; 42 | unsafe { 43 | CreateWellKnownSid( 44 | sid_type, 45 | std::ptr::null_mut(), 46 | std::ptr::null_mut(), 47 | &mut size, 48 | ); 49 | err = GetLastError(); 50 | }; 51 | // The call should have failed and told us we need more space 52 | if err != ERROR_INSUFFICIENT_BUFFER { 53 | return Err(IoError::last_os_error()); 54 | } 55 | 56 | // Allocate and zero out the storage 57 | let mut data = vec![0u8; size as usize]; 58 | 59 | // and now populate the sid for real 60 | unsafe { 61 | if CreateWellKnownSid( 62 | sid_type, 63 | std::ptr::null_mut(), 64 | data.as_mut_ptr() as _, 65 | &mut size, 66 | ) == 0 67 | { 68 | return Err(IoError::last_os_error()); 69 | } 70 | } 71 | 72 | Ok(Self { data }) 73 | } 74 | } 75 | 76 | impl AsSid for &WellKnownSid { 77 | fn as_sid(self) -> *const SID { 78 | self.data.as_ptr() as *const SID 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | use winapi::um::winnt::{WinBuiltinAdministratorsSid, WinBuiltinUsersSid}; 86 | 87 | #[test] 88 | fn sid_well_known() { 89 | let sid = WellKnownSid::with_well_known(WinBuiltinAdministratorsSid).unwrap(); 90 | assert!(is_well_known(&sid, WinBuiltinAdministratorsSid)); 91 | assert!(!is_well_known(&sid, WinBuiltinUsersSid)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /deelevate/src/psuedocon.rs: -------------------------------------------------------------------------------- 1 | use crate::pipe::PipeHandle; 2 | use lazy_static::lazy_static; 3 | use shared_library::shared_library; 4 | use std::io::{Error as IoError, Result as IoResult}; 5 | use std::os::windows::io::AsRawHandle; 6 | use std::path::Path; 7 | use winapi::shared::minwindef::DWORD; 8 | use winapi::shared::winerror::{HRESULT, S_OK}; 9 | use winapi::um::handleapi::*; 10 | use winapi::um::wincon::COORD; 11 | use winapi::um::winnt::HANDLE; 12 | 13 | pub type HPCON = HANDLE; 14 | 15 | shared_library!(ConPtyFuncs, 16 | pub fn CreatePseudoConsole( 17 | size: COORD, 18 | hInput: HANDLE, 19 | hOutput: HANDLE, 20 | flags: DWORD, 21 | hpc: *mut HPCON 22 | ) -> HRESULT, 23 | pub fn ResizePseudoConsole(hpc: HPCON, size: COORD) -> HRESULT, 24 | pub fn ClosePseudoConsole(hpc: HPCON), 25 | ); 26 | 27 | fn load_conpty() -> ConPtyFuncs { 28 | // If the kernel doesn't export these functions then their system is 29 | // too old and we cannot run. 30 | let kernel = ConPtyFuncs::open(Path::new("kernel32.dll")).expect( 31 | "this system does not support conpty. Windows 10 October 2018 or newer is required", 32 | ); 33 | 34 | // We prefer to use a sideloaded conpty.dll and openconsole.exe host deployed 35 | // alongside the application. We check for this after checking for kernel 36 | // support so that we don't try to proceed and do something crazy. 37 | if let Ok(sideloaded) = ConPtyFuncs::open(Path::new("conpty.dll")) { 38 | sideloaded 39 | } else { 40 | kernel 41 | } 42 | } 43 | 44 | lazy_static! { 45 | static ref CONPTY: ConPtyFuncs = load_conpty(); 46 | } 47 | 48 | pub struct PsuedoCon { 49 | pub(crate) con: HPCON, 50 | } 51 | 52 | unsafe impl Send for PsuedoCon {} 53 | unsafe impl Sync for PsuedoCon {} 54 | 55 | impl Drop for PsuedoCon { 56 | fn drop(&mut self) { 57 | unsafe { (CONPTY.ClosePseudoConsole)(self.con) }; 58 | } 59 | } 60 | 61 | impl PsuedoCon { 62 | pub fn new(size: COORD, input: PipeHandle, output: PipeHandle) -> IoResult { 63 | let mut con: HPCON = INVALID_HANDLE_VALUE; 64 | let result = unsafe { 65 | (CONPTY.CreatePseudoConsole)( 66 | size, 67 | input.as_raw_handle() as _, 68 | output.as_raw_handle() as _, 69 | 0, 70 | &mut con, 71 | ) 72 | }; 73 | if result != S_OK { 74 | Err(IoError::new( 75 | std::io::ErrorKind::Other, 76 | format!("failed to create psuedo console: HRESULT {}", result), 77 | )) 78 | } else { 79 | Ok(Self { con }) 80 | } 81 | } 82 | 83 | pub fn resize(&self, size: COORD) -> IoResult<()> { 84 | let result = unsafe { (CONPTY.ResizePseudoConsole)(self.con, size) }; 85 | if result != S_OK { 86 | Err(IoError::new( 87 | std::io::ErrorKind::Other, 88 | format!( 89 | "failed to resize console to {}x{}: HRESULT: {}", 90 | size.X, size.Y, result 91 | ), 92 | )) 93 | } else { 94 | Ok(()) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /utils/bin/ptybridge.rs: -------------------------------------------------------------------------------- 1 | use deelevate::{BridgePtyClient, Command, PipeHandle, Token}; 2 | use std::convert::TryInto; 3 | use std::ffi::OsString; 4 | use std::path::PathBuf; 5 | use structopt::*; 6 | use winapi::um::wincon::{SetConsoleCP, SetConsoleCursorPosition, SetConsoleOutputCP, COORD}; 7 | use winapi::um::winnls::CP_UTF8; 8 | 9 | /// A helper program for `eledo` and `normdo` that is used to 10 | /// bridge pty and pipes between the different privilege levels. 11 | /// This utility is not intended to be run by humans. 12 | #[derive(StructOpt)] 13 | #[structopt( 14 | version = env!("VERGEN_SEMVER_LIGHTWEIGHT") 15 | )] 16 | struct Opt { 17 | #[structopt(long, parse(from_os_str))] 18 | stdin: Option, 19 | #[structopt(long, parse(from_os_str))] 20 | stdout: Option, 21 | #[structopt(long, parse(from_os_str))] 22 | stderr: Option, 23 | #[structopt(long, parse(from_os_str))] 24 | conin: Option, 25 | #[structopt(long, parse(from_os_str))] 26 | conout: Option, 27 | 28 | #[structopt(long)] 29 | width: Option, 30 | #[structopt(long)] 31 | height: Option, 32 | 33 | #[structopt(long)] 34 | cursor_x: Option, 35 | #[structopt(long)] 36 | cursor_y: Option, 37 | 38 | #[structopt(parse(from_os_str))] 39 | args: Vec, 40 | } 41 | 42 | fn main() -> std::io::Result<()> { 43 | let mut opt = Opt::from_args(); 44 | 45 | unsafe { 46 | SetConsoleCP(CP_UTF8); 47 | SetConsoleOutputCP(CP_UTF8); 48 | } 49 | 50 | let token = Token::with_current_process()?; 51 | 52 | if let Some(conin) = opt.conin { 53 | let pty_client = BridgePtyClient::with_params( 54 | &conin, 55 | &opt.conout.unwrap(), 56 | opt.width.unwrap(), 57 | opt.height.unwrap(), 58 | )?; 59 | 60 | let mut args: Vec = vec![std::env::current_exe()?.into()]; 61 | 62 | if let Some(stdin) = opt.stdin { 63 | args.push("--stdin".into()); 64 | args.push(stdin.into()); 65 | } 66 | if let Some(stdout) = opt.stdout { 67 | args.push("--stdout".into()); 68 | args.push(stdout.into()); 69 | } 70 | if let Some(stderr) = opt.stderr { 71 | args.push("--stderr".into()); 72 | args.push(stderr.into()); 73 | } 74 | if let Some(cursor_x) = opt.cursor_x { 75 | args.push("--cursor-x".into()); 76 | args.push(cursor_x.to_string().into()); 77 | } 78 | if let Some(cursor_y) = opt.cursor_x { 79 | args.push("--cursor-y".into()); 80 | args.push(cursor_y.to_string().into()); 81 | } 82 | 83 | args.push("--".into()); 84 | args.append(&mut opt.args); 85 | 86 | let mut cmd = Command::with_environment_for_token(&token)?; 87 | cmd.set_argv(args); 88 | 89 | let exit_code = pty_client.run(cmd)?; 90 | std::process::exit(exit_code as _); 91 | } else { 92 | let mut cmd = Command::with_environment_for_token(&token)?; 93 | cmd.set_argv(opt.args); 94 | 95 | if let Some(stdin) = opt.stdin { 96 | cmd.set_stdin(PipeHandle::open_pipe(stdin)?)?; 97 | } 98 | if let Some(stdout) = opt.stdout { 99 | cmd.set_stdout(PipeHandle::open_pipe(stdout)?)?; 100 | } 101 | if let Some(stderr) = opt.stderr { 102 | cmd.set_stderr(PipeHandle::open_pipe(stderr)?)?; 103 | } 104 | 105 | if let Some(cursor_x) = opt.cursor_x { 106 | let conout = PipeHandle::open_pipe("CONOUT$")?; 107 | unsafe { 108 | SetConsoleCursorPosition( 109 | conout.as_handle(), 110 | COORD { 111 | X: cursor_x.try_into().unwrap(), 112 | Y: opt.cursor_y.unwrap().try_into().unwrap(), 113 | }, 114 | ); 115 | } 116 | } 117 | 118 | let proc = cmd.spawn()?; 119 | let _ = proc.wait_for(None)?; 120 | let exit_code = proc.exit_code()?; 121 | std::process::exit(exit_code as _); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /deelevate/src/spawn.rs: -------------------------------------------------------------------------------- 1 | use crate::bridge::BridgeServer; 2 | use crate::command::*; 3 | use crate::{PrivilegeLevel, Token}; 4 | use std::convert::TryInto; 5 | use std::io::Result as IoResult; 6 | 7 | /// Spawn a copy of the current process using the provided token. 8 | /// The existing streams are passed through to the child. 9 | /// On success, does not return to the caller; it will terminate 10 | /// the current process and assign the exit status from the child. 11 | fn spawn_with_current_io_streams(token: &Token) -> IoResult<()> { 12 | let mut cmd = Command::with_environment_for_token(token)?; 13 | cmd.set_command_from_current_process()?; 14 | let proc = cmd.spawn_as_user(token)?; 15 | 16 | proc.wait_for(None)?; 17 | 18 | let exit_code = proc.exit_code()?; 19 | std::process::exit(exit_code.try_into().unwrap()); 20 | } 21 | 22 | /// If the token is PrivilegeLevel::NotPrivileged then this function 23 | /// will return `Ok` and the intent is that the host program continue 24 | /// with its normal operation. 25 | /// 26 | /// Otherwise, assuming no errors were detected, this function will 27 | /// not return to the caller. Instead a reduced privilege token 28 | /// will be created and used to spawn a copy of the host program, 29 | /// passing through the arguments from the current process. 30 | /// *This* process will remain running to bridge pipes for the stdio 31 | /// streams to the new process and to wait for the child process 32 | /// and then terminate *this* process and exit with the exit code 33 | /// from the child. 34 | pub fn spawn_with_normal_privileges() -> IoResult<()> { 35 | let token = Token::with_current_process()?; 36 | let level = token.privilege_level()?; 37 | 38 | match level { 39 | PrivilegeLevel::NotPrivileged => Ok(()), 40 | PrivilegeLevel::Elevated => { 41 | let target_token = Token::with_shell_process()?; 42 | let mut server = BridgeServer::new(); 43 | let mut argv = std::env::args_os().collect(); 44 | let mut bridge_cmd = server.start_for_command(&mut argv, &target_token)?; 45 | let proc = bridge_cmd.spawn_with_token(&target_token)?; 46 | std::process::exit(server.serve(proc)? as _); 47 | } 48 | PrivilegeLevel::HighIntegrityAdmin => { 49 | let medium_token = token.as_medium_integrity_safer_token()?; 50 | spawn_with_current_io_streams(&medium_token) 51 | } 52 | } 53 | } 54 | 55 | /// If the token is NOT PrivilegeLevel::NotPrivileged then this function 56 | /// will return `Ok` and the intent is that the host program continue 57 | /// with its normal operation. 58 | /// 59 | /// Otherwise, assuming no errors were detected, this function will 60 | /// not return to the caller. Instead an elevated privilege token 61 | /// will be created and used to spawn a copy of the host program, 62 | /// passing through the arguments from the current process. 63 | /// *This* process will remain running to bridge pipes for the stdio 64 | /// streams to the new process and to wait for the child process 65 | /// and then terminate *this* process and exit with the exit code 66 | /// from the child. 67 | pub fn spawn_with_elevated_privileges() -> IoResult<()> { 68 | let token = Token::with_current_process()?; 69 | let level = token.privilege_level()?; 70 | 71 | let target_token = match level { 72 | PrivilegeLevel::NotPrivileged => token.as_medium_integrity_safer_token()?, 73 | PrivilegeLevel::HighIntegrityAdmin | PrivilegeLevel::Elevated => return Ok(()), 74 | }; 75 | 76 | let mut server = BridgeServer::new(); 77 | let mut argv = std::env::args_os().collect(); 78 | let mut bridge_cmd = server.start_for_command(&mut argv, &target_token)?; 79 | let proc = bridge_cmd.spawn_with_token(&target_token)?; 80 | std::process::exit(server.serve(proc)? as _); 81 | } 82 | 83 | /// This function is for use by C/C++ code that wants to test whether the 84 | /// current session is elevated. The return value is 0 for a non-privileged 85 | /// process and non-zero for a privileged process. 86 | /// If an error occurs while obtaining this information, the program will 87 | /// terminate. 88 | #[no_mangle] 89 | pub extern "C" fn deelevate_is_privileged_process() -> i32 { 90 | match Token::with_current_process().and_then(|token| token.privilege_level()) { 91 | Ok(PrivilegeLevel::Elevated) | Ok(PrivilegeLevel::HighIntegrityAdmin) => 1, 92 | Ok(PrivilegeLevel::NotPrivileged) => 0, 93 | Err(e) => { 94 | eprintln!( 95 | "An error occurred while determining the privilege level: {}", 96 | e 97 | ); 98 | std::process::exit(1); 99 | } 100 | } 101 | } 102 | 103 | /// This function is for use by C/C++ code that wants to ensure that execution 104 | /// will only continue if the current token has a Normal privilege level. 105 | /// This function will attempt to re-execute the program in the appropriate 106 | /// context. 107 | /// This function will only return if the current context has normal privs. 108 | #[no_mangle] 109 | pub extern "C" fn deelevate_requires_normal_privileges() { 110 | if let Err(e) = spawn_with_normal_privileges() { 111 | eprintln!( 112 | "This program requires running with Normal privileges and \ 113 | encountered an issue while attempting to run in that context: {}", 114 | e 115 | ); 116 | std::process::exit(1); 117 | } 118 | } 119 | 120 | /// This function is for use by C/C++ code that wants to ensure that execution 121 | /// will only continue if the current token has an Elevated privilege level. 122 | /// This function will attempt to re-execute the program in the appropriate 123 | /// context. 124 | /// This function will only return if the current context has Elevated or 125 | /// High Integrity Admin privs. 126 | #[no_mangle] 127 | pub extern "C" fn deelevate_requires_elevated_privileges() { 128 | if let Err(e) = spawn_with_elevated_privileges() { 129 | eprintln!( 130 | "This program requires running with Elevated privileges and \ 131 | encountered an issue while attempting to run in that context: {}", 132 | e 133 | ); 134 | std::process::exit(1); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /spawn-output.md: -------------------------------------------------------------------------------- 1 | ## local session not elevated: 2 | 3 | ``` 4 | > cargo run --example spawn 5 | Finished dev [unoptimized + debuginfo] target(s) in 0.05s 6 | Running `target\debug\examples\spawn.exe` 7 | 8 | GROUP INFORMATION 9 | ----------------- 10 | 11 | Group Name Type SID Attributes 12 | ============================================================= ================ ============ ================================================== 13 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 14 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Group used for deny only 15 | BUILTIN\Administrators Alias S-1-5-32-544 Group used for deny only 16 | BUILTIN\Performance Log Users Alias S-1-5-32-559 Mandatory group, Enabled by default, Enabled group 17 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 18 | NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group 19 | CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group 20 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 21 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 22 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 23 | LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group 24 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 25 | Mandatory Label\Medium Mandatory Level Label S-1-16-8192 26 | 27 | priv level is NotPrivileged 28 | now I'm safe to proceed with reduced privs 29 | 30 | ``` 31 | 32 | ## elevated powershell: 33 | 34 | ``` 35 | PS C:\Users\wez\wez-personal\deelevate> .\target\debug\examples\spawn.exe 36 | 37 | GROUP INFORMATION 38 | ----------------- 39 | 40 | Group Name Type SID Attributes 41 | ============================================================= ================ ============ =============================================================== 42 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 43 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Mandatory group, Enabled by default, Enabled group 44 | BUILTIN\Administrators Alias S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner 45 | BUILTIN\Performance Log Users Alias S-1-5-32-559 Mandatory group, Enabled by default, Enabled group 46 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 47 | NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group 48 | CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group 49 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 50 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 51 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 52 | LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group 53 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 54 | Mandatory Label\High Mandatory Level Label S-1-16-12288 55 | 56 | 57 | priv level is Elevated 58 | 59 | GROUP INFORMATION 60 | ----------------- 61 | 62 | Group Name Type SID Attributes 63 | ============================================================= ================ ============ ================================================== 64 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 65 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Group used for deny only 66 | BUILTIN\Administrators Alias S-1-5-32-544 Group used for deny only 67 | BUILTIN\Performance Log Users Alias S-1-5-32-559 Mandatory group, Enabled by default, Enabled group 68 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 69 | NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group 70 | CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group 71 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 72 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 73 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 74 | LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group 75 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 76 | 77 | Mandatory Label\Medium Mandatory Level Label S-1-16-8192 78 | 79 | 80 | priv level is NotPrivileged 81 | now I'm safe to proceed with reduced privs 82 | ``` 83 | 84 | ## ssh session: 85 | 86 | ``` 87 | C:\Users\wez\wez-personal\deelevate>target\debug\examples\spawn.exe 88 | 89 | GROUP INFORMATION 90 | ----------------- 91 | 92 | Group Name Type SID Attributes 93 | ============================================================= ================ ============ =============================================================== 94 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 95 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Mandatory group, Enabled by default, Enabled group 96 | BUILTIN\Administrators Alias S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner 97 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 98 | NT AUTHORITY\NETWORK Well-known group S-1-5-2 Mandatory group, Enabled by default, Enabled group 99 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 100 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 101 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 102 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 103 | Mandatory Label\High Mandatory Level Label S-1-16-12288 104 | 105 | priv level is HighIntegrityAdmin 106 | 107 | GROUP INFORMATION 108 | ----------------- 109 | 110 | Group Name Type SID Attributes 111 | ============================================================= ================ ============ ================================================== 112 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 113 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Mandatory group, Enabled by default, Enabled group 114 | BUILTIN\Administrators Alias S-1-5-32-544 Group used for deny only 115 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 116 | NT AUTHORITY\NETWORK Well-known group S-1-5-2 Mandatory group, Enabled by default, Enabled group 117 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 118 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 119 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 120 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 121 | Mandatory Label\Medium Mandatory Level Label S-1-16-8192 122 | 123 | priv level is NotPrivileged 124 | now I'm safe to proceed with reduced privs 125 | 126 | ``` 127 | 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EleDo - Elevated-Do 2 | 3 | [Download EleDo.zip](https://github.com/wez/deelevate/releases/download/Continuous/EleDo.zip) 4 | 5 | This repo is the home of EleDo (and NormDo), utilities that allow switching 6 | privilege levels from the command line on Windows 10 and later. 7 | 8 | The core privilege shifting code is also available as a Rust crate and a 9 | small C library to make it possible for an console based application to 10 | to detect and adjust its privilege level. 11 | 12 | While this repo is named for the privilege elevation aspect, it originally 13 | started life with the goal of reducing privileges to "normal" levels. 14 | 15 | ## Why Elevate? 16 | 17 | In some cases you need a higher level of access than is normal; for example, to 18 | install software or make system configuration. This is not new; most users 19 | will use the "Run as Administrator" option for a powershell session and run 20 | their commands that way, leaving the privileged session open for convenience, 21 | mixing commands and running most of them with higher privileges than are 22 | strictly required. 23 | 24 | Users coming from unix systems generally prefer not to do this and instead use 25 | a utility known as `sudo` (Super-User Do) to run a specific command with 26 | increased privileges. 27 | 28 | This repo provides `eledo.exe` as a workalike to `sudo`; it will attempt to 29 | increase privileges using the User Account Control mechanism built in to 30 | Windows to prompt the user to confirm that it should run with increased 31 | privileges. 32 | 33 | This repo also provides `normdo.exe` (Normal User Do) that works in the 34 | opposite way, dropping privileges back to normal levels to run a command. 35 | This functionality is important for security-minded code that wishes to 36 | run with lower (if not least!) privilege regardless of the privilege level 37 | of the code that invoked it. 38 | 39 | ## How do I use the Rust crate? 40 | 41 | There are two logical halves to this crate; 42 | 43 | * Detecting the privilege level, including both *Elevation* and *High Integrity 44 | Administrative* privs, so that the embedding application can choose whether 45 | to surface this as an error, or to continue with the second half of the crate... 46 | 47 | ```rust 48 | use deelevate::{Token, PrivilegeLevel}; 49 | 50 | let token = Token::with_current_process()?; 51 | match token.privilege_level()? { 52 | PrivilegeLevel::NotPrivileged => { 53 | // No special privs 54 | } 55 | PrivilegeLevel::Elevated => { 56 | // Invoked via runas 57 | } 58 | PrivilegeLevel::HighIntegrityAdmin => { 59 | // Some other kind of admin priv. 60 | // For example: ssh session to Windows 10 SSH server 61 | } 62 | } 63 | ``` 64 | 65 | * Re-executing the application with altered privs, while passing the stdio 66 | streams and process exit status back to the original parent. 67 | 68 | ```rust 69 | use deelevate::spawn_with_normal_privileges; 70 | use deelevate::spawn_with_elevated_privileges; 71 | 72 | // If we have admin privs, this next line will either spawn a version 73 | // of the current process with reduced privs, or yield an error trying 74 | // to do that. 75 | // The spawn_with_elevated_privileges function works similarly, except 76 | // that it will only return when the calling process has elevated 77 | // privs. 78 | spawn_with_normal_privileges()?; 79 | 80 | // If we reach this line it is because we don't have any special privs 81 | // and we can therefore continue with our normal operation. 82 | ``` 83 | 84 | The `show` example demonstrates testing for the privilege level. 85 | 86 | The `spawn` example demonstrates re-executing the process at a lower priv level. 87 | 88 | ## Caveats? 89 | 90 | There are some privilege levels that are not mapped as privileged from the 91 | perspective of this crate. The rationale for this is that those levels are 92 | unusual enough that they are probably not humans and probably should not have 93 | this crate adjusting the privilege level. 94 | 95 | It may feel like this might be a security concern, but its worth noting that: 96 | 97 | * The calling code already has equal or higher privilege (so no escalation is possible) 98 | * This crate is intended for convenience and consistency for human users 99 | 100 | ## Utilities 101 | 102 | This crate provides `normdo.exe` for running a command with normal privileges, 103 | and `eledo.exe` for running a command with elevated privileges. Unlike other 104 | elevation solutions, both of these utilities are designed to run from inside 105 | a console and to keep the output from the target application in that console. 106 | In addition, these tools use the PTY APIs in order to support running terminal 107 | applications such as pagers and editors (vim.exe!) correctly! 108 | 109 | Both of these tools require that the `eledo-pty-bridge.exe` be installed 110 | alongside them, or otherwise be in the PATH. The bridge process is required 111 | to host the PTY and spawn the program in the alternatively privileged 112 | context. 113 | 114 | ### `eledo.exe` 115 | 116 | *Runs a program with elevated privs* 117 | 118 | ``` 119 | eledo.exe PROGRAM [ARGUMENTS] 120 | ``` 121 | 122 | `eledo.exe` will check to see if the current context has admin privileges; 123 | if it does then it will execute the requested `PROGRAM` directly, returning 124 | its exit status. 125 | 126 | Otherwise, `eledo.exe` will arrange to run the program with an elevated PTY 127 | that is bridged to the current terminal session. Elevation requires that the 128 | current process be able to communicate with the shell in the current desktop 129 | session, and will typically trigger a UAC prompt for that user. 130 | 131 | ``` 132 | > eledo.exe whoami /groups 133 | 134 | GROUP INFORMATION 135 | ----------------- 136 | 137 | Group Name Type SID Attributes 138 | ============================================================= ================ ============ =============================================================== 139 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 140 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Mandatory group, Enabled by default, Enabled group 141 | BUILTIN\Administrators Alias S-1-5-32-544 Mandatory group, Enabled by default, Enabled group, Group owner 142 | BUILTIN\Performance Log Users Alias S-1-5-32-559 Mandatory group, Enabled by default, Enabled group 143 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 144 | NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group 145 | CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group 146 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 147 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 148 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 149 | LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group 150 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 151 | Mandatory Label\High Mandatory Level Label S-1-16-12288 152 | ``` 153 | 154 | ### `normdo.exe` 155 | 156 | *Runs a program with normal privs* 157 | 158 | ``` 159 | normdo.exe PROGRAM [ARGUMENTS] 160 | ``` 161 | 162 | `normdo.exe` will check to see if the current context has admin privileges; 163 | if it does *not* then it will execute the requested `PROGRAM` directly, returning 164 | its exit status. 165 | 166 | Otherwise, `eledo.exe` will arrange to run the program with a Normal user token 167 | with Medium integrity level, dropping/denying the local administrator group 168 | from the current token. The program will be run in a PTY that is bridged to 169 | the current terminal session. 170 | 171 | ``` 172 | > normdo.exe whoami /groups 173 | 174 | GROUP INFORMATION 175 | ----------------- 176 | 177 | Group Name Type SID Attributes 178 | ============================================================= ================ ============ ================================================== 179 | Everyone Well-known group S-1-1-0 Mandatory group, Enabled by default, Enabled group 180 | NT AUTHORITY\Local account and member of Administrators group Well-known group S-1-5-114 Group used for deny only 181 | BUILTIN\Administrators Alias S-1-5-32-544 Group used for deny only 182 | BUILTIN\Performance Log Users Alias S-1-5-32-559 Mandatory group, Enabled by default, Enabled group 183 | BUILTIN\Users Alias S-1-5-32-545 Mandatory group, Enabled by default, Enabled group 184 | NT AUTHORITY\INTERACTIVE Well-known group S-1-5-4 Mandatory group, Enabled by default, Enabled group 185 | CONSOLE LOGON Well-known group S-1-2-1 Mandatory group, Enabled by default, Enabled group 186 | NT AUTHORITY\Authenticated Users Well-known group S-1-5-11 Mandatory group, Enabled by default, Enabled group 187 | NT AUTHORITY\This Organization Well-known group S-1-5-15 Mandatory group, Enabled by default, Enabled group 188 | NT AUTHORITY\Local account Well-known group S-1-5-113 Mandatory group, Enabled by default, Enabled group 189 | LOCAL Well-known group S-1-2-0 Mandatory group, Enabled by default, Enabled group 190 | NT AUTHORITY\NTLM Authentication Well-known group S-1-5-64-10 Mandatory group, Enabled by default, Enabled group 191 | Mandatory Label\Medium Mandatory Level Label S-1-16-8192 192 | ``` 193 | 194 | ## Thanks 195 | 196 | The elevator icons embedded into the utilities were made by Pixel perfect from www.flaticon.com 197 | -------------------------------------------------------------------------------- /deelevate/src/pipe.rs: -------------------------------------------------------------------------------- 1 | use crate::{os_str_to_null_terminated_vec, win32_error_with_context, Token}; 2 | use std::io::{Error as IoError, Result as IoResult}; 3 | use std::os::windows::prelude::*; 4 | use std::path::{Path, PathBuf}; 5 | use std::ptr::null_mut; 6 | use std::sync::atomic::AtomicUsize; 7 | use winapi::shared::winerror::ERROR_PIPE_CONNECTED; 8 | use winapi::um::errhandlingapi::GetLastError; 9 | use winapi::um::fileapi::{CreateFileW, FlushFileBuffers, ReadFile, WriteFile, OPEN_EXISTING}; 10 | use winapi::um::handleapi::{ 11 | CloseHandle, DuplicateHandle, SetHandleInformation, INVALID_HANDLE_VALUE, 12 | }; 13 | use winapi::um::minwinbase::SECURITY_ATTRIBUTES; 14 | use winapi::um::namedpipeapi::{ConnectNamedPipe, CreateNamedPipeW, CreatePipe}; 15 | use winapi::um::processthreadsapi::GetCurrentProcess; 16 | use winapi::um::processthreadsapi::GetCurrentProcessId; 17 | use winapi::um::winbase::*; 18 | use winapi::um::winnt::{DUPLICATE_SAME_ACCESS, GENERIC_READ, GENERIC_WRITE, HANDLE}; 19 | 20 | /// A little container type for holding a pipe file handle 21 | #[derive(Debug)] 22 | pub struct PipeHandle(HANDLE); 23 | /// The compiler thinks it isn't send because HANDLE is a pointer 24 | /// type. We happen to know that moving the handle between threads 25 | /// is totally fine, hence this impl. 26 | unsafe impl Send for PipeHandle {} 27 | 28 | impl PipeHandle { 29 | pub fn make_inheritable(&self) -> IoResult<()> { 30 | let res = unsafe { SetHandleInformation(self.0, HANDLE_FLAG_INHERIT, 1) }; 31 | if res != 1 { 32 | Err(win32_error_with_context( 33 | "SetHandleInformation HANDLE_FLAG_INHERIT", 34 | IoError::last_os_error(), 35 | )) 36 | } else { 37 | Ok(()) 38 | } 39 | } 40 | 41 | pub fn as_handle(&self) -> HANDLE { 42 | self.0 43 | } 44 | 45 | pub fn create_named_pipe_byte_mode_for_token>( 46 | name: P, 47 | token: &Token, 48 | ) -> IoResult { 49 | let descriptor = token.create_security_descriptor()?; 50 | 51 | let path = os_str_to_null_terminated_vec(name.as_ref().as_os_str()); 52 | let max_instances = 1; 53 | let buf_size = 4096; 54 | let default_timeout_ms = 100; 55 | let mut security_attr = SECURITY_ATTRIBUTES { 56 | nLength: std::mem::size_of::() as _, 57 | lpSecurityDescriptor: descriptor.0, 58 | bInheritHandle: 0, 59 | }; 60 | 61 | let handle = unsafe { 62 | CreateNamedPipeW( 63 | path.as_ptr(), 64 | PIPE_ACCESS_DUPLEX | FILE_FLAG_FIRST_PIPE_INSTANCE, 65 | PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_REJECT_REMOTE_CLIENTS, 66 | max_instances, 67 | buf_size, 68 | buf_size, 69 | default_timeout_ms, 70 | &mut security_attr, 71 | ) 72 | }; 73 | if handle != INVALID_HANDLE_VALUE { 74 | Ok(Self(handle)) 75 | } else { 76 | Err(win32_error_with_context( 77 | "CreateNamedPipeW", 78 | IoError::last_os_error(), 79 | )) 80 | } 81 | } 82 | 83 | /// Wait for a short period for a client to connect to 84 | /// this pipe instance. 85 | pub fn wait_for_pipe_client(&self) -> IoResult<()> { 86 | // One does not simply do non-blocking pipe work. 87 | // We spawn a thread that will cancel all IO on this pipe if 88 | // we don't send it a message within the timeout. 89 | use std::sync::mpsc::channel; 90 | let (tx, rx) = channel(); 91 | 92 | // A little helper for safely passing the HANDLE 93 | // pointer to another thread 94 | struct HandleHolder(HANDLE); 95 | unsafe impl Send for HandleHolder {} 96 | let handle = HandleHolder(self.0); 97 | 98 | // This thread will cancel all IO on self.0 if not signalled 99 | // in time to stop it. 100 | std::thread::spawn(move || { 101 | if rx 102 | .recv_timeout(std::time::Duration::from_millis(2500)) 103 | .is_err() 104 | { 105 | unsafe { winapi::um::ioapiset::CancelIoEx(handle.0, null_mut()) }; 106 | } 107 | }); 108 | 109 | // Wrap up the connect operation in a lambda so that we can 110 | // ensure that we send a message to the timeout thread before 111 | // we unwind. 112 | let res = (move || { 113 | let res = unsafe { ConnectNamedPipe(self.0, null_mut()) }; 114 | let err = unsafe { GetLastError() }; 115 | if res == 0 && err != ERROR_PIPE_CONNECTED { 116 | Err(win32_error_with_context( 117 | "ConnectNamedPipe", 118 | IoError::last_os_error(), 119 | )) 120 | } else { 121 | Ok(()) 122 | } 123 | })(); 124 | let _ = tx.send(()); 125 | 126 | res 127 | } 128 | 129 | pub fn duplicate(&self) -> IoResult { 130 | let proc = unsafe { GetCurrentProcess() }; 131 | let mut duped = INVALID_HANDLE_VALUE; 132 | let inheritable = false; 133 | let access = 0; 134 | let res = unsafe { 135 | DuplicateHandle( 136 | proc, 137 | self.0, 138 | proc, 139 | &mut duped, 140 | access, 141 | inheritable as _, 142 | DUPLICATE_SAME_ACCESS, 143 | ) 144 | }; 145 | if res == 0 { 146 | Err(win32_error_with_context( 147 | "DuplicateHandle", 148 | IoError::last_os_error(), 149 | )) 150 | } else { 151 | Ok(Self(duped)) 152 | } 153 | } 154 | 155 | pub fn open_pipe>(name: P) -> IoResult { 156 | let path = os_str_to_null_terminated_vec(name.as_ref().as_os_str()); 157 | let share_mode = 0; 158 | let mut security_attr = SECURITY_ATTRIBUTES { 159 | nLength: std::mem::size_of::() as _, 160 | lpSecurityDescriptor: null_mut(), 161 | bInheritHandle: 0, 162 | }; 163 | 164 | let flags = 0; 165 | let template_file = null_mut(); 166 | let handle = unsafe { 167 | CreateFileW( 168 | path.as_ptr(), 169 | GENERIC_READ | GENERIC_WRITE, 170 | share_mode, 171 | &mut security_attr, 172 | OPEN_EXISTING, 173 | flags, 174 | template_file, 175 | ) 176 | }; 177 | if handle != INVALID_HANDLE_VALUE { 178 | Ok(Self(handle)) 179 | } else { 180 | let err = IoError::last_os_error(); 181 | Err(win32_error_with_context( 182 | &format!("CreateFileW: {}", name.as_ref().display()), 183 | err, 184 | )) 185 | } 186 | } 187 | } 188 | 189 | impl AsRawHandle for PipeHandle { 190 | fn as_raw_handle(&self) -> RawHandle { 191 | self.as_handle() as RawHandle 192 | } 193 | } 194 | 195 | impl Drop for PipeHandle { 196 | fn drop(&mut self) { 197 | unsafe { 198 | CloseHandle(self.0); 199 | } 200 | } 201 | } 202 | 203 | impl std::io::Read for PipeHandle { 204 | fn read(&mut self, buf: &mut [u8]) -> IoResult { 205 | let mut num_read = 0; 206 | let ok = unsafe { 207 | ReadFile( 208 | self.0, 209 | buf.as_mut_ptr() as *mut _, 210 | buf.len() as _, 211 | &mut num_read, 212 | null_mut(), 213 | ) 214 | }; 215 | if ok == 0 { 216 | let err = IoError::last_os_error(); 217 | Err(win32_error_with_context("ReadFile", err)) 218 | /* 219 | if err.kind() == std::io::ErrorKind::BrokenPipe { 220 | Ok(0) 221 | } else { 222 | Err(win32_error_with_context("ReadFile", err)) 223 | } 224 | */ 225 | } else { 226 | Ok(num_read as usize) 227 | } 228 | } 229 | } 230 | 231 | impl std::io::Write for PipeHandle { 232 | fn write(&mut self, buf: &[u8]) -> IoResult { 233 | let mut num_wrote = 0; 234 | let ok = unsafe { 235 | WriteFile( 236 | self.0, 237 | buf.as_ptr() as *const _, 238 | buf.len() as u32, 239 | &mut num_wrote, 240 | null_mut(), 241 | ) 242 | }; 243 | if ok == 0 { 244 | Err(win32_error_with_context( 245 | "WriteFile", 246 | IoError::last_os_error(), 247 | )) 248 | } else { 249 | Ok(num_wrote as usize) 250 | } 251 | } 252 | 253 | fn flush(&mut self) -> IoResult<()> { 254 | if unsafe { FlushFileBuffers(self.0) } != 1 { 255 | Err(win32_error_with_context( 256 | "FlushFileBuffers", 257 | IoError::last_os_error(), 258 | )) 259 | } else { 260 | Ok(()) 261 | } 262 | } 263 | } 264 | 265 | pub struct NamedPipeServer { 266 | pub pipe: PipeHandle, 267 | pub path: PathBuf, 268 | } 269 | 270 | impl NamedPipeServer { 271 | pub fn for_token(token: &Token) -> IoResult { 272 | static ID: AtomicUsize = AtomicUsize::new(1); 273 | let path: PathBuf = format!( 274 | "\\\\.\\pipe\\eledo-bridge-{:x}-{:x}-{:x}", 275 | unsafe { GetCurrentProcessId() }, 276 | ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst), 277 | rand::random::() 278 | ) 279 | .into(); 280 | let pipe = PipeHandle::create_named_pipe_byte_mode_for_token(&path, token)?; 281 | Ok(Self { pipe, path }) 282 | } 283 | } 284 | 285 | /// A little helper for creating a pipe 286 | pub struct PipePair { 287 | pub read: PipeHandle, 288 | pub write: PipeHandle, 289 | } 290 | 291 | impl PipePair { 292 | /// Create a new pipe 293 | #[allow(unused)] 294 | pub fn new() -> IoResult { 295 | let mut sa = SECURITY_ATTRIBUTES { 296 | nLength: std::mem::size_of::() as u32, 297 | lpSecurityDescriptor: null_mut(), 298 | bInheritHandle: 0, 299 | }; 300 | let mut read: HANDLE = INVALID_HANDLE_VALUE as _; 301 | let mut write: HANDLE = INVALID_HANDLE_VALUE as _; 302 | if unsafe { CreatePipe(&mut read, &mut write, &mut sa, 0) } == 0 { 303 | return Err(win32_error_with_context( 304 | "CreatePipe", 305 | IoError::last_os_error(), 306 | )); 307 | } 308 | Ok(Self { 309 | read: PipeHandle(read), 310 | write: PipeHandle(write), 311 | }) 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /deelevate/src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::pipe::*; 2 | use crate::process::Process; 3 | use crate::procthreadattr::ProcThreadAttributeList; 4 | use crate::psuedocon::PsuedoCon; 5 | use crate::{os_str_to_null_terminated_vec, win32_error_with_context, Token}; 6 | use std::ffi::{OsStr, OsString}; 7 | use std::io::{Error as IoError, Result as IoResult}; 8 | use std::os::windows::ffi::OsStrExt; 9 | use std::path::PathBuf; 10 | use std::ptr::null_mut; 11 | use winapi::shared::minwindef::{BOOL, DWORD, LPVOID}; 12 | use winapi::um::combaseapi::CoInitializeEx; 13 | use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; 14 | use winapi::um::objbase::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE}; 15 | use winapi::um::processthreadsapi::{ 16 | CreateProcessAsUserW, CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW, 17 | }; 18 | use winapi::um::shellapi::{ShellExecuteExW, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW}; 19 | use winapi::um::userenv::{CreateEnvironmentBlock, DestroyEnvironmentBlock}; 20 | use winapi::um::winbase::{ 21 | CREATE_DEFAULT_ERROR_MODE, CREATE_NEW_CONSOLE, CREATE_NEW_PROCESS_GROUP, 22 | CREATE_UNICODE_ENVIRONMENT, EXTENDED_STARTUPINFO_PRESENT, STARTF_USESHOWWINDOW, 23 | STARTF_USESTDHANDLES, STARTUPINFOEXW, 24 | }; 25 | use winapi::um::winnt::{HANDLE, LPCWSTR, LPWSTR}; 26 | use winapi::um::winuser::{SW_HIDE, SW_SHOWNORMAL}; 27 | 28 | extern "system" { 29 | /// This is missing from the currently available versions of the winapi crate. 30 | fn CreateProcessWithTokenW( 31 | hToken: HANDLE, 32 | dwLogonFlags: DWORD, 33 | lpApplicationName: LPCWSTR, 34 | lpCommandLine: LPWSTR, 35 | dwCreationFlags: DWORD, 36 | lpEnvironment: LPVOID, 37 | lpCurrentDirectory: LPCWSTR, 38 | lpStartupInfo: *mut STARTUPINFOW, 39 | lpProcessInformation: *mut PROCESS_INFORMATION, 40 | ) -> BOOL; 41 | } 42 | 43 | pub struct EnvironmentBlock(pub LPVOID); 44 | impl Drop for EnvironmentBlock { 45 | fn drop(&mut self) { 46 | unsafe { 47 | DestroyEnvironmentBlock(self.0); 48 | } 49 | } 50 | } 51 | 52 | impl EnvironmentBlock { 53 | /// Create a copy of the current environment, but do so for the provided token. 54 | /// We have to do this explicitly as some of the CreateProcessAsXXX 55 | /// calls will default to a different process environment otherwise! 56 | pub fn with_token(token: &Token) -> IoResult { 57 | let mut block = null_mut(); 58 | let inherit = true; 59 | if unsafe { CreateEnvironmentBlock(&mut block, token.token, inherit as _) } != 1 { 60 | Err(win32_error_with_context( 61 | "CreateEnvironmentBlock", 62 | IoError::last_os_error(), 63 | )) 64 | } else { 65 | Ok(Self(block)) 66 | } 67 | } 68 | 69 | pub fn as_vec(&self) -> Vec { 70 | // This is safe because we know that the block was created by 71 | // CreateEnvironmentBlock and that it is well formed. 72 | // An environment block has the form: 73 | // key=value\0 74 | // ... 75 | // key=value\0 76 | // \0 77 | // So when we find the sequence \0\0 then we have found the extent 78 | // of the block. 79 | unsafe { 80 | let mut ptr = self.0 as *const u16; 81 | let mut size = 0; 82 | loop { 83 | let next = ptr.add(1); 84 | if ptr.read() == 0 { 85 | if next.read() == 0 { 86 | // We found the double-null terminator 87 | size += 2; 88 | break; 89 | } 90 | } 91 | ptr = next; 92 | size += 1; 93 | } 94 | 95 | let slice = std::slice::from_raw_parts(self.0 as *const u16, size); 96 | slice.to_vec() 97 | } 98 | } 99 | } 100 | 101 | /// Helper for ensuring that handles from a spawned 102 | /// process are closed 103 | struct ProcInfo(PROCESS_INFORMATION); 104 | impl Drop for ProcInfo { 105 | fn drop(&mut self) { 106 | unsafe { 107 | if !self.0.hProcess.is_null() { 108 | CloseHandle(self.0.hProcess); 109 | } 110 | if !self.0.hThread.is_null() { 111 | CloseHandle(self.0.hThread); 112 | } 113 | } 114 | } 115 | } 116 | 117 | impl ProcInfo { 118 | pub fn new() -> Self { 119 | Self(unsafe { std::mem::zeroed() }) 120 | } 121 | 122 | /// Take ownership of the process handle 123 | pub fn process(&mut self) -> Option { 124 | if self.0.hProcess.is_null() { 125 | None 126 | } else { 127 | let proc = Process::with_handle(self.0.hProcess); 128 | self.0.hProcess = null_mut(); 129 | Some(proc) 130 | } 131 | } 132 | } 133 | 134 | pub struct Command { 135 | args: Vec, 136 | env: Vec, 137 | cwd: PathBuf, 138 | hide_window: bool, 139 | stdin: Option, 140 | stdout: Option, 141 | stderr: Option, 142 | } 143 | 144 | impl Command { 145 | pub fn with_environment_for_token(token: &Token) -> IoResult { 146 | let env = EnvironmentBlock::with_token(token)?.as_vec(); 147 | let cwd = std::env::current_dir()?; 148 | Ok(Self { 149 | args: vec![], 150 | env, 151 | cwd, 152 | stdin: None, 153 | stdout: None, 154 | stderr: None, 155 | hide_window: false, 156 | }) 157 | } 158 | 159 | pub fn set_command_from_current_process(&mut self) -> IoResult<()> { 160 | self.args = std::env::args_os().collect(); 161 | Ok(()) 162 | } 163 | 164 | pub fn hide_window(&mut self) { 165 | self.hide_window = true; 166 | } 167 | 168 | pub fn set_argv(&mut self, argv: Vec) { 169 | self.args = argv; 170 | } 171 | 172 | fn executable_and_command_line(&self, skip: usize) -> IoResult<(Vec, Vec)> { 173 | let exe_path = PathBuf::from(self.args[0].clone()); 174 | let exe_path = if !exe_path.has_root() { 175 | pathsearch::find_executable_in_path(&exe_path).ok_or_else(|| { 176 | std::io::Error::new( 177 | std::io::ErrorKind::Other, 178 | format!("{:?} not found in the path", exe_path), 179 | ) 180 | })? 181 | } else { 182 | exe_path 183 | }; 184 | 185 | let executable = os_str_to_null_terminated_vec(&exe_path.as_os_str()); 186 | 187 | let mut cmdline = Vec::::new(); 188 | for arg in self.args.iter().skip(skip) { 189 | if !cmdline.is_empty() { 190 | cmdline.push(' ' as u16); 191 | } 192 | append_quoted(&arg, &mut cmdline); 193 | } 194 | cmdline.push(0); 195 | 196 | Ok((executable, cmdline)) 197 | } 198 | 199 | pub fn set_stdin(&mut self, p: PipeHandle) -> IoResult<()> { 200 | p.make_inheritable()?; 201 | self.stdin.replace(p); 202 | Ok(()) 203 | } 204 | 205 | pub fn set_stdout(&mut self, p: PipeHandle) -> IoResult<()> { 206 | p.make_inheritable()?; 207 | self.stdout.replace(p); 208 | Ok(()) 209 | } 210 | 211 | pub fn set_stderr(&mut self, p: PipeHandle) -> IoResult<()> { 212 | p.make_inheritable()?; 213 | self.stderr.replace(p); 214 | Ok(()) 215 | } 216 | 217 | fn make_startup_info(&self) -> STARTUPINFOW { 218 | let mut si: STARTUPINFOW = unsafe { std::mem::zeroed() }; 219 | si.cb = std::mem::size_of::() as u32; 220 | si.dwFlags = 0; 221 | 222 | if self.hide_window { 223 | si.dwFlags |= STARTF_USESHOWWINDOW; 224 | si.wShowWindow = SW_HIDE as _; 225 | } 226 | 227 | if self.stdin.is_some() || self.stdout.is_some() || self.stderr.is_some() { 228 | si.dwFlags |= STARTF_USESTDHANDLES; 229 | si.hStdInput = INVALID_HANDLE_VALUE; 230 | si.hStdOutput = INVALID_HANDLE_VALUE; 231 | si.hStdError = INVALID_HANDLE_VALUE; 232 | } 233 | 234 | if let Some(pipe) = self.stdin.as_ref() { 235 | si.hStdInput = pipe.as_handle(); 236 | } 237 | if let Some(pipe) = self.stdout.as_ref() { 238 | si.hStdOutput = pipe.as_handle(); 239 | } 240 | if let Some(pipe) = self.stderr.as_ref() { 241 | si.hStdError = pipe.as_handle(); 242 | } 243 | 244 | si 245 | } 246 | 247 | pub fn shell_execute(&mut self, verb: &str) -> IoResult { 248 | unsafe { 249 | CoInitializeEx( 250 | null_mut(), 251 | COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE, 252 | ) 253 | }; 254 | let (exe, params) = self.executable_and_command_line(1)?; 255 | let cwd = os_str_to_null_terminated_vec(self.cwd.as_os_str()); 256 | let verb = os_str_to_null_terminated_vec(OsStr::new(verb)); 257 | 258 | let mut info = SHELLEXECUTEINFOW { 259 | cbSize: std::mem::size_of::() as u32, 260 | fMask: SEE_MASK_NOCLOSEPROCESS, 261 | hwnd: null_mut(), 262 | lpVerb: verb.as_ptr(), 263 | lpFile: exe.as_ptr(), 264 | lpParameters: params.as_ptr(), 265 | lpDirectory: cwd.as_ptr(), 266 | nShow: if self.hide_window { 267 | SW_HIDE 268 | } else { 269 | SW_SHOWNORMAL 270 | }, 271 | hInstApp: null_mut(), 272 | lpIDList: null_mut(), 273 | lpClass: null_mut(), 274 | hkeyClass: null_mut(), 275 | hMonitor: null_mut(), 276 | dwHotKey: 0, 277 | hProcess: null_mut(), 278 | }; 279 | 280 | let res = unsafe { ShellExecuteExW(&mut info) }; 281 | 282 | if res == 0 { 283 | Err(win32_error_with_context( 284 | "ShellExecuteExW", 285 | IoError::last_os_error(), 286 | )) 287 | } else { 288 | Ok(Process::with_handle(info.hProcess)) 289 | } 290 | } 291 | 292 | pub fn spawn_with_pty(&mut self, psuedocon: &PsuedoCon) -> IoResult { 293 | let mut si = STARTUPINFOEXW { 294 | StartupInfo: self.make_startup_info(), 295 | lpAttributeList: null_mut(), 296 | }; 297 | si.StartupInfo.cb = std::mem::size_of::() as u32; 298 | 299 | let mut attrs = ProcThreadAttributeList::with_capacity(1)?; 300 | attrs.set_pty(psuedocon.con)?; 301 | si.lpAttributeList = attrs.as_mut_ptr(); 302 | 303 | let mut pi = ProcInfo::new(); 304 | let (mut exe, mut command_line) = self.executable_and_command_line(0)?; 305 | let mut cwd = os_str_to_null_terminated_vec(self.cwd.as_os_str()); 306 | 307 | let proc_attributes = null_mut(); 308 | let thread_attributes = null_mut(); 309 | let inherit_handles = true; 310 | 311 | let res = unsafe { 312 | CreateProcessW( 313 | exe.as_mut_ptr(), 314 | command_line.as_mut_ptr(), 315 | proc_attributes, 316 | thread_attributes, 317 | inherit_handles as _, 318 | EXTENDED_STARTUPINFO_PRESENT | CREATE_UNICODE_ENVIRONMENT, 319 | self.env.as_mut_ptr() as *mut _, 320 | cwd.as_mut_ptr(), 321 | &mut si.StartupInfo, 322 | &mut pi.0, 323 | ) 324 | }; 325 | if res == 0 { 326 | Err(win32_error_with_context( 327 | "CreateProcessAsUserW", 328 | IoError::last_os_error(), 329 | )) 330 | } else { 331 | Ok(pi.process().unwrap()) 332 | } 333 | } 334 | 335 | pub fn spawn(&mut self) -> IoResult { 336 | let mut si = self.make_startup_info(); 337 | let mut pi = ProcInfo::new(); 338 | let (mut exe, mut command_line) = self.executable_and_command_line(0)?; 339 | let mut cwd = os_str_to_null_terminated_vec(self.cwd.as_os_str()); 340 | 341 | let proc_attributes = null_mut(); 342 | let thread_attributes = null_mut(); 343 | let inherit_handles = true; 344 | 345 | let res = unsafe { 346 | CreateProcessW( 347 | exe.as_mut_ptr(), 348 | command_line.as_mut_ptr(), 349 | proc_attributes, 350 | thread_attributes, 351 | inherit_handles as _, 352 | CREATE_UNICODE_ENVIRONMENT, 353 | self.env.as_mut_ptr() as *mut _, 354 | cwd.as_mut_ptr(), 355 | &mut si, 356 | &mut pi.0, 357 | ) 358 | }; 359 | if res != 1 { 360 | Err(win32_error_with_context( 361 | "CreateProcessW", 362 | IoError::last_os_error(), 363 | )) 364 | } else { 365 | Ok(pi.process().unwrap()) 366 | } 367 | } 368 | 369 | pub fn spawn_as_user(&mut self, token: &Token) -> IoResult { 370 | let mut si = self.make_startup_info(); 371 | let mut pi = ProcInfo::new(); 372 | 373 | let (mut exe, mut command_line) = self.executable_and_command_line(0)?; 374 | let mut cwd = os_str_to_null_terminated_vec(self.cwd.as_os_str()); 375 | 376 | let proc_attributes = null_mut(); 377 | let thread_attributes = null_mut(); 378 | let inherit_handles = true; 379 | 380 | let res = unsafe { 381 | CreateProcessAsUserW( 382 | token.token, 383 | exe.as_mut_ptr(), 384 | command_line.as_mut_ptr(), 385 | proc_attributes, 386 | thread_attributes, 387 | inherit_handles as _, 388 | CREATE_UNICODE_ENVIRONMENT, 389 | self.env.as_mut_ptr() as *mut _, 390 | cwd.as_mut_ptr(), 391 | &mut si, 392 | &mut pi.0, 393 | ) 394 | }; 395 | if res != 1 { 396 | Err(win32_error_with_context( 397 | "CreateProcessAsUserW", 398 | IoError::last_os_error(), 399 | )) 400 | } else { 401 | Ok(pi.process().unwrap()) 402 | } 403 | } 404 | 405 | pub fn spawn_with_token(&mut self, token: &Token) -> IoResult { 406 | let mut si = self.make_startup_info(); 407 | 408 | let mut pi = ProcInfo::new(); 409 | let (mut exe, mut command_line) = self.executable_and_command_line(0)?; 410 | let mut cwd = os_str_to_null_terminated_vec(self.cwd.as_os_str()); 411 | 412 | let logon_flags = 0; 413 | 414 | let res = unsafe { 415 | CreateProcessWithTokenW( 416 | token.token, 417 | logon_flags, 418 | exe.as_mut_ptr(), 419 | command_line.as_mut_ptr(), 420 | CREATE_UNICODE_ENVIRONMENT| 421 | // Note that these flags are unconditionally or'd 422 | // in by CreateProcessWithTokenW: they're included 423 | // here to make it more obvious that these apply: 424 | CREATE_DEFAULT_ERROR_MODE| 425 | CREATE_NEW_CONSOLE| 426 | CREATE_NEW_PROCESS_GROUP, 427 | self.env.as_mut_ptr() as *mut _, 428 | cwd.as_mut_ptr(), 429 | &mut si, 430 | &mut pi.0, 431 | ) 432 | }; 433 | if res != 1 { 434 | Err(win32_error_with_context( 435 | "CreateProcessWithTokenW", 436 | IoError::last_os_error(), 437 | )) 438 | } else { 439 | Ok(pi.process().unwrap()) 440 | } 441 | } 442 | } 443 | 444 | // Borrowed from https://github.com/wez/wezterm/blob/65707aba56f940f8c370f0465f0f3f2a6303a9cc/pty/src/cmdbuilder.rs#L313 445 | // and thus from https://github.com/hniksic/rust-subprocess/blob/873dfed165173e52907beb87118b2c0c05d8b8a1/src/popen.rs#L1117 446 | // which in turn was translated from ArgvQuote at http://tinyurl.com/zmgtnls 447 | fn append_quoted(arg: &OsStr, cmdline: &mut Vec) { 448 | if !arg.is_empty() 449 | && !arg.encode_wide().any(|c| { 450 | c == ' ' as u16 451 | || c == '\t' as u16 452 | || c == '\n' as u16 453 | || c == '\x0b' as u16 454 | || c == '\"' as u16 455 | }) 456 | { 457 | cmdline.extend(arg.encode_wide()); 458 | return; 459 | } 460 | cmdline.push('"' as u16); 461 | 462 | let arg: Vec<_> = arg.encode_wide().collect(); 463 | let mut i = 0; 464 | while i < arg.len() { 465 | let mut num_backslashes = 0; 466 | while i < arg.len() && arg[i] == '\\' as u16 { 467 | i += 1; 468 | num_backslashes += 1; 469 | } 470 | 471 | if i == arg.len() { 472 | for _ in 0..num_backslashes * 2 { 473 | cmdline.push('\\' as u16); 474 | } 475 | break; 476 | } else if arg[i] == b'"' as u16 { 477 | for _ in 0..num_backslashes * 2 + 1 { 478 | cmdline.push('\\' as u16); 479 | } 480 | cmdline.push(arg[i]); 481 | } else { 482 | for _ in 0..num_backslashes { 483 | cmdline.push('\\' as u16); 484 | } 485 | cmdline.push(arg[i]); 486 | } 487 | i += 1; 488 | } 489 | cmdline.push('"' as u16); 490 | } 491 | -------------------------------------------------------------------------------- /deelevate/src/bridge.rs: -------------------------------------------------------------------------------- 1 | use crate::command::Command; 2 | use crate::pipe::*; 3 | use crate::process::Process; 4 | use crate::psuedocon::PsuedoCon; 5 | use crate::win32_error_with_context; 6 | use crate::Token; 7 | use std::ffi::{OsStr, OsString}; 8 | use std::io::{Error as IoError, Read, Result as IoResult, Write}; 9 | use std::os::windows::prelude::*; 10 | use std::path::{Path, PathBuf}; 11 | use winapi::shared::minwindef::DWORD; 12 | use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode}; 13 | use winapi::um::consoleapi::{ReadConsoleW, WriteConsoleW}; 14 | use winapi::um::fileapi::GetFileType; 15 | use winapi::um::winbase::FILE_TYPE_CHAR; 16 | use winapi::um::wincon::{ 17 | GetConsoleScreenBufferInfo, CONSOLE_SCREEN_BUFFER_INFO, DISABLE_NEWLINE_AUTO_RETURN, 18 | ENABLE_PROCESSED_OUTPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, 19 | ENABLE_WRAP_AT_EOL_OUTPUT, 20 | }; 21 | use winapi::um::wincontypes::COORD; 22 | 23 | pub struct BridgePtyClient { 24 | con: PsuedoCon, 25 | } 26 | 27 | impl BridgePtyClient { 28 | pub fn with_params(conin: &Path, conout: &Path, width: usize, height: usize) -> IoResult { 29 | let client_to_server = PipeHandle::open_pipe(conout)?; 30 | let server_to_client = PipeHandle::open_pipe(conin)?; 31 | 32 | let con = PsuedoCon::new( 33 | COORD { 34 | X: width as i16, 35 | Y: height as i16, 36 | }, 37 | server_to_client, 38 | client_to_server, 39 | )?; 40 | 41 | Ok(Self { con }) 42 | } 43 | 44 | pub fn run(&self, mut command: Command) -> IoResult { 45 | let proc = command.spawn_with_pty(&self.con)?; 46 | proc.wait_for(None)?; 47 | // Well, this is a bit awkward. 48 | // If we kill the pty immediately when the child process exits, 49 | // it will fracture the associated pipes and any buffered 50 | // output will be lost. 51 | // There doesn't seem to be a reasonable way to wait for that 52 | // flush to occur from here, so we just use a short sleep. 53 | // This is gross and I wonder if this is even long enough 54 | // for every case? 55 | std::thread::sleep(std::time::Duration::from_millis(300)); 56 | proc.exit_code() 57 | } 58 | } 59 | 60 | #[allow(unused)] 61 | fn join_with_timeout(join_handle: std::thread::JoinHandle<()>, timeout: std::time::Duration) { 62 | use std::sync::mpsc::channel; 63 | let (tx, rx) = channel(); 64 | std::thread::spawn(move || { 65 | let _ = join_handle.join(); 66 | let _ = tx.send(()); 67 | }); 68 | let _ = rx.recv_timeout(timeout); 69 | } 70 | 71 | /// The bridge server is the originator of the spawned command. 72 | /// It owns the server end of the connection and awaits the 73 | /// bridge client connection. 74 | pub struct BridgeServer { 75 | stdin_is_pty: bool, 76 | stdout_is_pty: bool, 77 | stderr_is_pty: bool, 78 | 79 | stdin: Option, 80 | stdout: Option, 81 | stderr: Option, 82 | 83 | conin: Option, 84 | conin_pipe: Option, 85 | conout: Option, 86 | conout_pipe: Option, 87 | 88 | input_mode: Option, 89 | output_mode: Option, 90 | } 91 | 92 | impl Drop for BridgeServer { 93 | fn drop(&mut self) { 94 | if let Some(mode) = self.output_mode { 95 | if let Ok(mut conout) = PipeHandle::open_pipe("CONOUT$") { 96 | // Emit a soft reset 97 | let _ = write!(&mut conout, "\x1b[!p"); 98 | // Restore mode 99 | let _ = set_console_mode(&conout, mode); 100 | } 101 | } 102 | if let Some(mode) = self.input_mode { 103 | if let Ok(conin) = PipeHandle::open_pipe("CONIN$") { 104 | let _ = set_console_mode(&conin, mode); 105 | } 106 | } 107 | } 108 | } 109 | 110 | fn get_console_mode(pipe: &PipeHandle) -> IoResult { 111 | let mut mode = 0; 112 | let res = unsafe { GetConsoleMode(pipe.as_handle(), &mut mode) }; 113 | if res == 0 { 114 | Err(win32_error_with_context( 115 | "GetConsoleMode", 116 | IoError::last_os_error(), 117 | )) 118 | } else { 119 | Ok(mode) 120 | } 121 | } 122 | 123 | fn set_console_mode(pipe: &PipeHandle, mode: DWORD) -> IoResult<()> { 124 | let res = unsafe { SetConsoleMode(pipe.as_handle(), mode) }; 125 | if res == 0 { 126 | Err(win32_error_with_context( 127 | "SetConsoleMode", 128 | IoError::last_os_error(), 129 | )) 130 | } else { 131 | Ok(()) 132 | } 133 | } 134 | 135 | // Due to https://github.com/microsoft/terminal/issues/4551 136 | // we cannot simply write bytes to the console, we have to 137 | // use WriteConsoleW to send the unicode data through, otherwise 138 | // we end up with problems with mismatching codepages. 139 | fn write_console(out: &mut PipeHandle, s: &str) -> IoResult<()> { 140 | let c: Vec = OsStr::new(s).encode_wide().collect(); 141 | let mut wrote = 0; 142 | let res = unsafe { 143 | WriteConsoleW( 144 | out.as_handle(), 145 | c.as_ptr() as *const _, 146 | c.len() as _, 147 | &mut wrote, 148 | std::ptr::null_mut(), 149 | ) 150 | }; 151 | if res == 0 { 152 | Err(IoError::last_os_error()) 153 | } else { 154 | Ok(()) 155 | } 156 | } 157 | 158 | fn is_pty_stream(f: &F) -> bool { 159 | let handle = f.as_raw_handle(); 160 | unsafe { GetFileType(handle as _) == FILE_TYPE_CHAR } 161 | } 162 | 163 | impl BridgeServer { 164 | pub fn new() -> Self { 165 | let stdin_is_pty = is_pty_stream(&std::io::stdin()); 166 | let stdout_is_pty = is_pty_stream(&std::io::stdout()); 167 | let stderr_is_pty = is_pty_stream(&std::io::stderr()); 168 | 169 | Self { 170 | stdin_is_pty, 171 | stdout_is_pty, 172 | stderr_is_pty, 173 | conin: None, 174 | conout: None, 175 | conin_pipe: None, 176 | conout_pipe: None, 177 | input_mode: None, 178 | output_mode: None, 179 | stderr: None, 180 | stdout: None, 181 | stdin: None, 182 | } 183 | } 184 | 185 | pub fn start_for_command( 186 | &mut self, 187 | argv: &mut Vec, 188 | target_token: &Token, 189 | ) -> IoResult { 190 | let bridge_path = locate_pty_bridge()?; 191 | let mut bridge_args = self.start(target_token)?; 192 | 193 | bridge_args.insert(0, bridge_path.into_os_string()); 194 | bridge_args.push("--".into()); 195 | bridge_args.append(argv); 196 | 197 | let mut bridge_cmd = Command::with_environment_for_token(&target_token)?; 198 | bridge_cmd.set_argv(bridge_args); 199 | bridge_cmd.hide_window(); 200 | 201 | Ok(bridge_cmd) 202 | } 203 | 204 | /// Creates the server pipe and returns the name of the pipe 205 | /// so that it can be passed to the client process 206 | pub fn start(&mut self, token: &Token) -> IoResult> { 207 | let mut args = vec![]; 208 | 209 | if !self.stdin_is_pty { 210 | let pipe = NamedPipeServer::for_token(token)?; 211 | self.stdin.replace(pipe.pipe); 212 | args.push("--stdin".into()); 213 | args.push(pipe.path.into()); 214 | } 215 | 216 | if !self.stdout_is_pty { 217 | let pipe = NamedPipeServer::for_token(token)?; 218 | self.stdout.replace(pipe.pipe); 219 | args.push("--stdout".into()); 220 | args.push(pipe.path.into()); 221 | } 222 | 223 | if !self.stderr_is_pty { 224 | let pipe = NamedPipeServer::for_token(token)?; 225 | self.stderr.replace(pipe.pipe); 226 | args.push("--stderr".into()); 227 | args.push(pipe.path.into()); 228 | } 229 | 230 | if let Ok(conin) = PipeHandle::open_pipe("CONIN$") { 231 | self.input_mode.replace(get_console_mode(&conin)?); 232 | let pipe = NamedPipeServer::for_token(token)?; 233 | self.conin_pipe.replace(pipe.pipe); 234 | 235 | args.push("--conin".into()); 236 | args.push(pipe.path.into()); 237 | 238 | set_console_mode( 239 | &conin, 240 | // ENABLE_PROCESSED_OUTPUT | FIXME: CTRl-C handling? 241 | ENABLE_VIRTUAL_TERMINAL_INPUT, 242 | )?; 243 | self.conin.replace(conin); 244 | } 245 | 246 | if let Ok(conout) = PipeHandle::open_pipe("CONOUT$") { 247 | self.output_mode.replace(get_console_mode(&conout)?); 248 | let pipe = NamedPipeServer::for_token(token)?; 249 | self.conout_pipe.replace(pipe.pipe); 250 | 251 | args.push("--conout".into()); 252 | args.push(pipe.path.into()); 253 | 254 | let mut console_info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { std::mem::zeroed() }; 255 | let res = unsafe { GetConsoleScreenBufferInfo(conout.as_handle(), &mut console_info) }; 256 | 257 | if res == 0 { 258 | return Err(win32_error_with_context( 259 | "GetConsoleScreenBufferInfo", 260 | IoError::last_os_error(), 261 | )); 262 | } 263 | 264 | // The console info describes the buffer dimensions. 265 | // We need to do a little bit of math to obtain the viewport dimensions! 266 | let width = console_info 267 | .srWindow 268 | .Right 269 | .saturating_sub(console_info.srWindow.Left) as usize 270 | + 1; 271 | 272 | args.push("--width".into()); 273 | args.push(width.to_string().into()); 274 | 275 | let height = console_info 276 | .srWindow 277 | .Bottom 278 | .saturating_sub(console_info.srWindow.Top) as usize 279 | + 1; 280 | 281 | args.push("--height".into()); 282 | args.push(height.to_string().into()); 283 | 284 | let cursor_x = console_info.dwCursorPosition.X as usize; 285 | let cursor_y = console_info 286 | .dwCursorPosition 287 | .Y 288 | .saturating_sub(console_info.srWindow.Top) as usize; 289 | 290 | args.push("--cursor-x".into()); 291 | args.push(cursor_x.to_string().into()); 292 | 293 | args.push("--cursor-y".into()); 294 | args.push(cursor_y.to_string().into()); 295 | 296 | set_console_mode( 297 | &conout, 298 | ENABLE_PROCESSED_OUTPUT 299 | | ENABLE_WRAP_AT_EOL_OUTPUT 300 | | ENABLE_VIRTUAL_TERMINAL_PROCESSING 301 | | DISABLE_NEWLINE_AUTO_RETURN, 302 | )?; 303 | 304 | self.conout.replace(conout); 305 | } 306 | 307 | Ok(args) 308 | } 309 | 310 | pub fn serve(mut self, proc: Process) -> IoResult { 311 | if let Some(conin) = self.conin.take() { 312 | let mut conin_dest = self.conin_pipe.take().unwrap(); 313 | conin_dest.wait_for_pipe_client()?; 314 | std::thread::spawn(move || -> IoResult<()> { 315 | let mut buf = [0u16; 8192]; 316 | let mut num_read = 0; 317 | loop { 318 | let res = unsafe { 319 | ReadConsoleW( 320 | conin.as_handle(), 321 | buf.as_mut_ptr() as *mut _, 322 | buf.len() as _, 323 | &mut num_read, 324 | std::ptr::null_mut(), 325 | ) 326 | }; 327 | 328 | if res == 0 { 329 | return Err(IoError::last_os_error()); 330 | } 331 | 332 | let s = OsString::from_wide(&buf[0..num_read as usize]); 333 | let utf8 = s.to_string_lossy(); 334 | 335 | conin_dest.write_all(utf8.as_bytes())?; 336 | } 337 | }); 338 | } 339 | 340 | // Start up the console output processing thread. 341 | // This is ostensibly just a matter of taking the output 342 | // from the pty created by the bridge executable and piping it 343 | // into our own CONOUT$ stream, but it is made a little bit 344 | // more complicated because the Windows console APIs emit 345 | // some slightly hostile initialization sequences when creating 346 | // a fresh PTY and launching a process inside it: it will emit 347 | // sequences that move the cursor, clear the screen and change 348 | // the window title sequence. 349 | // For our embedding use case those are distinctly unwanted. 350 | // In order to deal with this, we need to parse the terminal 351 | // output so that we can filter them out. 352 | // The approach is simple: until we spot that initial title 353 | // change, we'll filter out CSI and OSC sequences. 354 | // Just in case the behavior changes in the future, we'll 355 | // also disable suppression if we see any other kind of 356 | // output from the pty stream. 357 | let conout_thread = self.conout.take().map(|mut conout| { 358 | let mut conout_src = self.conout_pipe.take().unwrap(); 359 | let _ = conout_src.wait_for_pipe_client(); 360 | std::thread::spawn(move || -> IoResult<()> { 361 | use termwiz::escape::osc::OperatingSystemCommand; 362 | use termwiz::escape::parser::Parser; 363 | use termwiz::escape::Action; 364 | 365 | let mut parser = Parser::new(); 366 | 367 | let mut buf = [0u8; 4096]; 368 | let mut suppress_control = true; 369 | 370 | loop { 371 | let len = conout_src.read(&mut buf)?; 372 | if len == 0 { 373 | return Ok(()); 374 | } 375 | 376 | let mut error = None; 377 | let mut callback = |action: Action| -> IoResult<()> { 378 | match action { 379 | Action::OperatingSystemCommand(osc) => { 380 | match *osc { 381 | OperatingSystemCommand::SetIconNameAndWindowTitle(_) => { 382 | if suppress_control { 383 | // We're now sync'd up with the new pty instance. 384 | // We ignore this first title change request because 385 | // it is going to be the uninteresting bridge exe 386 | suppress_control = false; 387 | Ok(()) 388 | } else { 389 | write_console(&mut conout, &format!("{}", osc)) 390 | } 391 | } 392 | _ => write_console(&mut conout, &format!("{}", osc)), 393 | } 394 | } 395 | Action::CSI(c) => { 396 | if !suppress_control { 397 | write_console(&mut conout, &format!("{}", c)) 398 | } else { 399 | Ok(()) 400 | } 401 | } 402 | _ => { 403 | suppress_control = false; 404 | write_console(&mut conout, &format!("{}", action)) 405 | } 406 | } 407 | }; 408 | 409 | parser.parse(&buf[0..len], |action| { 410 | if let Err(e) = callback(action) { 411 | error.replace(e); 412 | } 413 | }); 414 | 415 | if let Some(e) = error.take() { 416 | return Err(e); 417 | } 418 | } 419 | }) 420 | }); 421 | 422 | if let Some(mut stdin_dest) = self.stdin.take() { 423 | stdin_dest.wait_for_pipe_client()?; 424 | std::thread::spawn(move || { 425 | let mut stdin = std::io::stdin(); 426 | let _ = std::io::copy(&mut stdin, &mut stdin_dest); 427 | }); 428 | } 429 | 430 | let stdout_thread = self.stdout.take().map(|mut stdout_src| { 431 | let _ = stdout_src.wait_for_pipe_client(); 432 | std::thread::spawn(move || { 433 | let mut stdout = std::io::stdout(); 434 | let _ = std::io::copy(&mut stdout_src, &mut stdout); 435 | }) 436 | }); 437 | let stderr_thread = self.stderr.take().map(|mut stderr_src| { 438 | let _ = stderr_src.wait_for_pipe_client(); 439 | std::thread::spawn(move || { 440 | let mut stderr = std::io::stderr(); 441 | let _ = std::io::copy(&mut stderr_src, &mut stderr); 442 | }) 443 | }); 444 | 445 | let _ = proc.wait_for(None)?; 446 | 447 | stdout_thread.map(|t| t.join()); 448 | stderr_thread.map(|t| t.join()); 449 | conout_thread.map(|t| t.join()); 450 | 451 | let exit_code = proc.exit_code()?; 452 | Ok(exit_code) 453 | } 454 | } 455 | 456 | fn locate_pty_bridge() -> IoResult { 457 | let bridge_name = "eledo-pty-bridge.exe"; 458 | let bridge_path = std::env::current_exe()? 459 | .parent() 460 | .ok_or_else(|| { 461 | std::io::Error::new( 462 | std::io::ErrorKind::Other, 463 | "current exe has no containing dir while locating pty bridge!?", 464 | ) 465 | })? 466 | .join(bridge_name); 467 | if bridge_path.exists() { 468 | Ok(bridge_path) 469 | } else { 470 | pathsearch::find_executable_in_path(bridge_name).ok_or_else(|| { 471 | std::io::Error::new( 472 | std::io::ErrorKind::Other, 473 | format!( 474 | "{} not found alongside executable or in the path", 475 | bridge_name 476 | ), 477 | ) 478 | }) 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /deelevate/src/token.rs: -------------------------------------------------------------------------------- 1 | use crate::process::Process; 2 | use crate::sid::{get_length_sid, is_well_known, AsSid, WellKnownSid}; 3 | use crate::win32_error_with_context; 4 | use std::io::{Error as IoError, Result as IoResult}; 5 | use std::ptr::null_mut; 6 | use winapi::shared::minwindef::{BOOL, DWORD}; 7 | use winapi::shared::winerror::{ERROR_INSUFFICIENT_BUFFER, ERROR_SUCCESS}; 8 | use winapi::um::accctrl::{ 9 | EXPLICIT_ACCESSW, NO_INHERITANCE, NO_MULTIPLE_TRUSTEE, SET_ACCESS, TRUSTEE_IS_SID, 10 | TRUSTEE_IS_USER, TRUSTEE_W, 11 | }; 12 | use winapi::um::aclapi::SetEntriesInAclW; 13 | use winapi::um::errhandlingapi::GetLastError; 14 | use winapi::um::handleapi::{CloseHandle, INVALID_HANDLE_VALUE}; 15 | use winapi::um::minwinbase::LPTR; 16 | use winapi::um::processthreadsapi::{GetCurrentProcess, OpenProcessToken}; 17 | use winapi::um::securitybaseapi::{ 18 | CheckTokenMembership, DuplicateTokenEx, GetTokenInformation, ImpersonateLoggedOnUser, 19 | InitializeSecurityDescriptor, SetSecurityDescriptorDacl, SetTokenInformation, 20 | }; 21 | use winapi::um::winbase::{LocalAlloc, LocalFree}; 22 | use winapi::um::winnt::{ 23 | SecurityImpersonation, TokenElevationType, TokenElevationTypeFull, TokenImpersonation, 24 | TokenIntegrityLevel, TokenPrimary, TokenUser, WinBuiltinAdministratorsSid, WinHighLabelSid, 25 | WinMediumLabelSid, GENERIC_READ, GENERIC_WRITE, HANDLE, PACL, PROCESS_QUERY_INFORMATION, 26 | PSECURITY_DESCRIPTOR, SECURITY_DESCRIPTOR_MIN_LENGTH, SECURITY_DESCRIPTOR_REVISION, 27 | SE_GROUP_INTEGRITY, SID, SID_AND_ATTRIBUTES, TOKEN_ADJUST_DEFAULT, TOKEN_ADJUST_SESSIONID, 28 | TOKEN_ASSIGN_PRIMARY, TOKEN_DUPLICATE, TOKEN_ELEVATION_TYPE, TOKEN_IMPERSONATE, 29 | TOKEN_MANDATORY_LABEL, TOKEN_QUERY, TOKEN_TYPE, 30 | }; 31 | use winapi::um::winsafer::{ 32 | SaferCloseLevel, SaferComputeTokenFromLevel, SaferCreateLevel, SAFER_LEVELID_NORMALUSER, 33 | SAFER_LEVEL_HANDLE, SAFER_LEVEL_OPEN, SAFER_SCOPEID_USER, 34 | }; 35 | use winapi::um::winuser::{GetShellWindow, GetWindowThreadProcessId}; 36 | 37 | /// Indicates the effective level of privileges held by the token 38 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 39 | pub enum PrivilegeLevel { 40 | /// The token isn't privileged OR may be privileged in 41 | /// an unusual way that we don't know how to guarantee 42 | /// that we can/should reduce privilege or do so successfully. 43 | NotPrivileged, 44 | /// The token is an elevated token produced via runas/UAC. 45 | /// It may be a different administrative user than the 46 | /// interactive desktop user! 47 | Elevated, 48 | /// The token isn't an elevated token but it does have 49 | /// high integrity privileges, such as those produced 50 | /// by sshing in to a Windows 10 system. 51 | HighIntegrityAdmin, 52 | } 53 | 54 | /// A helper that wraps a TOKEN_MANDATORY_LABEL struct. 55 | /// That struct holds a SID and some attribute flags. 56 | /// Its use in this module is to query the integrity level 57 | /// of the token, so we have a very targeted set of accessors 58 | /// for that purpose. 59 | /// The integrity level is a single SID that represents the 60 | /// degree of trust that the token has. 61 | /// A normal user is typically running with Medium integrity, 62 | /// whereas an elevated session is typically running with High 63 | /// integrity. 64 | struct TokenIntegrityLevel { 65 | data: Vec, 66 | } 67 | 68 | impl TokenIntegrityLevel { 69 | fn as_label(&self) -> &TOKEN_MANDATORY_LABEL { 70 | // This is safe because we cannot construct an invalid instance 71 | unsafe { &*(self.data.as_ptr() as *const TOKEN_MANDATORY_LABEL) } 72 | } 73 | 74 | fn sid(&self) -> *const SID { 75 | // For whatever reason, the PSID type in the SDK is defined 76 | // as void* and that is the type of Label.Sid, rather than 77 | // SID*, so we get to cast it here. 78 | self.as_label().Label.Sid as *const SID 79 | } 80 | 81 | /// Return true if this is a high integrity level label 82 | pub fn is_high(&self) -> bool { 83 | is_well_known(self.sid(), WinHighLabelSid) 84 | } 85 | } 86 | 87 | /// `Token` represents a set of credentials and privileges. A process 88 | /// typically inherits the token of its parent process for its primary 89 | /// token, and Windows allows for threads to create/obtain impersonation 90 | /// tokens so that a thread can run with a different identity for a 91 | /// while. 92 | /// 93 | /// For the purposes of this crate, we are concerned with reducing 94 | /// the scope of the privileges in a given Token. 95 | pub struct Token { 96 | pub(crate) token: HANDLE, 97 | } 98 | 99 | impl Drop for Token { 100 | fn drop(&mut self) { 101 | unsafe { 102 | CloseHandle(self.token); 103 | } 104 | } 105 | } 106 | 107 | pub(crate) struct SecurityDescriptor(pub PSECURITY_DESCRIPTOR); 108 | impl Drop for SecurityDescriptor { 109 | fn drop(&mut self) { 110 | unsafe { 111 | LocalFree(self.0); 112 | } 113 | } 114 | } 115 | 116 | impl Token { 117 | /// Obtain a handle to the primary token for this process 118 | pub fn with_current_process() -> IoResult { 119 | let mut token: HANDLE = INVALID_HANDLE_VALUE; 120 | let res = unsafe { 121 | OpenProcessToken( 122 | GetCurrentProcess(), 123 | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE, 124 | &mut token, 125 | ) 126 | }; 127 | if res != 1 { 128 | Err(win32_error_with_context( 129 | "OpenProcessToken(GetCurrentProcess))", 130 | IoError::last_os_error(), 131 | )) 132 | } else { 133 | Ok(Self { token }) 134 | } 135 | } 136 | 137 | pub(crate) fn create_security_descriptor(&self) -> IoResult { 138 | let user = self.user()?; 139 | 140 | let mut ea = EXPLICIT_ACCESSW { 141 | grfAccessPermissions: GENERIC_READ | GENERIC_WRITE, 142 | grfAccessMode: SET_ACCESS, 143 | grfInheritance: NO_INHERITANCE, 144 | Trustee: TRUSTEE_W { 145 | TrusteeForm: TRUSTEE_IS_SID, 146 | TrusteeType: TRUSTEE_IS_USER, 147 | MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE, 148 | ptstrName: user.sid() as *mut _, 149 | pMultipleTrustee: null_mut(), 150 | }, 151 | }; 152 | 153 | let mut acl: PACL = null_mut(); 154 | let res = unsafe { SetEntriesInAclW(1, &mut ea, null_mut(), &mut acl) }; 155 | if res != ERROR_SUCCESS { 156 | return Err(win32_error_with_context( 157 | "SetEntriesInAcl", 158 | IoError::last_os_error(), 159 | )); 160 | } 161 | 162 | let sd = SecurityDescriptor(unsafe { LocalAlloc(LPTR, SECURITY_DESCRIPTOR_MIN_LENGTH) }); 163 | let res = unsafe { InitializeSecurityDescriptor(sd.0, SECURITY_DESCRIPTOR_REVISION) }; 164 | if res == 0 { 165 | return Err(win32_error_with_context( 166 | "InitializeSecurityDescriptor", 167 | IoError::last_os_error(), 168 | )); 169 | } 170 | 171 | let dacl_present = true as _; 172 | let default_dacl = false as _; 173 | let res = unsafe { SetSecurityDescriptorDacl(sd.0, dacl_present, acl, default_dacl) }; 174 | if res == 0 { 175 | return Err(win32_error_with_context( 176 | "SetSecurityDescriptorDacl", 177 | IoError::last_os_error(), 178 | )); 179 | } 180 | Ok(sd) 181 | } 182 | 183 | /// Obtain the token from the shell process as a primary token. 184 | /// This can fail if there is no shell accessible to the process, 185 | /// for example if the process was spawned by an ssh session. 186 | /// Why might we want this token? We can't directly 187 | /// de-elevate a token so we need to obtain a non-elevated 188 | /// token from a well known source. 189 | pub fn with_shell_process() -> IoResult { 190 | let shell_window = unsafe { GetShellWindow() }; 191 | if shell_window.is_null() { 192 | return Err(IoError::new( 193 | std::io::ErrorKind::NotFound, 194 | "there is no shell window", 195 | )); 196 | } 197 | 198 | let mut shell_pid: DWORD = 0; 199 | let _thread_id = unsafe { GetWindowThreadProcessId(shell_window, &mut shell_pid) }; 200 | 201 | let proc = Process::with_process_id(PROCESS_QUERY_INFORMATION, false, shell_pid)?; 202 | 203 | let mut token: HANDLE = INVALID_HANDLE_VALUE; 204 | let res = unsafe { OpenProcessToken(proc.as_handle(), TOKEN_DUPLICATE, &mut token) }; 205 | if res != 1 { 206 | Err(win32_error_with_context( 207 | "OpenProcessToken(shell process)", 208 | IoError::last_os_error(), 209 | )) 210 | } else { 211 | let token = Self { token }; 212 | 213 | // And now that we have it, make a primary token from it! 214 | token.duplicate_as_primary_token() 215 | } 216 | } 217 | 218 | /// Build a medium integrity level normal user access token 219 | /// from the current token. 220 | /// This is most suitable in the case where you have a 221 | /// HighIntegrityAdmin privilege level and want to proceed 222 | /// with a normal privilege token. 223 | pub fn as_medium_integrity_safer_token(&self) -> IoResult { 224 | let mut level: SAFER_LEVEL_HANDLE = null_mut(); 225 | let res = unsafe { 226 | SaferCreateLevel( 227 | SAFER_SCOPEID_USER, 228 | SAFER_LEVELID_NORMALUSER, 229 | SAFER_LEVEL_OPEN, 230 | &mut level, 231 | null_mut(), 232 | ) 233 | }; 234 | if res != 1 { 235 | return Err(win32_error_with_context( 236 | "SaferCreateLevel", 237 | IoError::last_os_error(), 238 | )); 239 | } 240 | 241 | struct SaferHandle(SAFER_LEVEL_HANDLE); 242 | impl Drop for SaferHandle { 243 | fn drop(&mut self) { 244 | unsafe { SaferCloseLevel(self.0) }; 245 | } 246 | } 247 | let level = SaferHandle(level); 248 | 249 | let mut token = INVALID_HANDLE_VALUE; 250 | let res = 251 | unsafe { SaferComputeTokenFromLevel(level.0, self.token, &mut token, 0, null_mut()) }; 252 | if res != 1 { 253 | return Err(win32_error_with_context( 254 | "SaferComputeTokenFromLevel", 255 | IoError::last_os_error(), 256 | )); 257 | } 258 | 259 | let token = Self { token }; 260 | token.set_medium_integrity()?; 261 | Ok(token) 262 | } 263 | 264 | fn set_medium_integrity(&self) -> IoResult<()> { 265 | let medium = WellKnownSid::with_well_known(WinMediumLabelSid)?; 266 | let mut tml = TOKEN_MANDATORY_LABEL { 267 | Label: SID_AND_ATTRIBUTES { 268 | Attributes: SE_GROUP_INTEGRITY, 269 | Sid: medium.as_sid() as *mut _, 270 | }, 271 | }; 272 | 273 | let res = unsafe { 274 | SetTokenInformation( 275 | self.token, 276 | TokenIntegrityLevel, 277 | &mut tml as *mut TOKEN_MANDATORY_LABEL as *mut _, 278 | std::mem::size_of_val(&tml) as u32 + get_length_sid(&medium), 279 | ) 280 | }; 281 | if res != 1 { 282 | Err(win32_error_with_context( 283 | "SetTokenInformation(TokenIntegrityLevel Medium)", 284 | IoError::last_os_error(), 285 | )) 286 | } else { 287 | Ok(()) 288 | } 289 | } 290 | 291 | /// Attempt to duplicate this token as one that is suitable 292 | /// for use in impersonation related APIs, which includes the 293 | /// check_membership method. 294 | fn duplicate_as_impersonation_token(&self) -> IoResult { 295 | self.duplicate(TokenImpersonation) 296 | } 297 | 298 | fn duplicate_as_primary_token(&self) -> IoResult { 299 | self.duplicate(TokenPrimary) 300 | } 301 | 302 | fn duplicate(&self, token_type: TOKEN_TYPE) -> IoResult { 303 | let mut dup: HANDLE = INVALID_HANDLE_VALUE; 304 | let res = unsafe { 305 | DuplicateTokenEx( 306 | self.token, 307 | TOKEN_ADJUST_SESSIONID 308 | | TOKEN_ADJUST_DEFAULT 309 | | TOKEN_ASSIGN_PRIMARY 310 | | TOKEN_IMPERSONATE 311 | | TOKEN_DUPLICATE 312 | | TOKEN_QUERY, 313 | null_mut(), 314 | SecurityImpersonation, 315 | token_type, 316 | &mut dup, 317 | ) 318 | }; 319 | if res != 1 { 320 | Err(win32_error_with_context( 321 | "DuplicateTokenEx", 322 | IoError::last_os_error(), 323 | )) 324 | } else { 325 | Ok(Self { token: dup }) 326 | } 327 | } 328 | 329 | /// Returns true if `sid` is an enabled group on this token. 330 | /// The token must be an impersonation token, so you may need 331 | /// to use duplicate_as_impersonation_token() to obtain one. 332 | fn check_membership(&self, sid: S) -> IoResult { 333 | let mut is_member: BOOL = 0; 334 | let res = 335 | unsafe { CheckTokenMembership(self.token, sid.as_sid() as *mut _, &mut is_member) }; 336 | if res != 1 { 337 | Err(win32_error_with_context( 338 | "CheckTokenMembership", 339 | IoError::last_os_error(), 340 | )) 341 | } else { 342 | Ok(is_member == 1) 343 | } 344 | } 345 | 346 | /// A convenience wrapper around check_membership that tests for 347 | /// being a member of the builtin administrators group 348 | fn check_administrators_membership(&self) -> IoResult { 349 | let admins = WellKnownSid::with_well_known(WinBuiltinAdministratorsSid)?; 350 | self.check_membership(&admins) 351 | } 352 | 353 | /// Retrieve the user SID and attributes from the token. 354 | /// This is encoded in the same way as TokenIntegrityLevel, 355 | /// hence the return type. 356 | /// This method is used to obtain the SID in order to 357 | /// construct a DACL for a named pipe. 358 | fn user(&self) -> IoResult { 359 | let mut size: DWORD = 0; 360 | let err; 361 | 362 | unsafe { 363 | GetTokenInformation(self.token, TokenUser, null_mut(), 0, &mut size); 364 | err = GetLastError(); 365 | }; 366 | 367 | // The call should have failed and told us we need more space 368 | if err != ERROR_INSUFFICIENT_BUFFER { 369 | return Err(win32_error_with_context( 370 | "GetTokenInformation TokenUser unexpected failure", 371 | IoError::last_os_error(), 372 | )); 373 | } 374 | 375 | // Allocate and zero out the storage 376 | let mut data = vec![0u8; size as usize]; 377 | 378 | unsafe { 379 | if GetTokenInformation( 380 | self.token, 381 | TokenUser, 382 | data.as_mut_ptr() as *mut _, 383 | size, 384 | &mut size, 385 | ) == 0 386 | { 387 | return Err(win32_error_with_context( 388 | "GetTokenInformation TokenUser", 389 | IoError::last_os_error(), 390 | )); 391 | } 392 | }; 393 | 394 | Ok(TokenIntegrityLevel { data }) 395 | } 396 | 397 | /// Retrieve the integrity level label of the process. 398 | fn integrity_level(&self) -> IoResult { 399 | let mut size: DWORD = 0; 400 | let err; 401 | 402 | unsafe { 403 | GetTokenInformation(self.token, TokenIntegrityLevel, null_mut(), 0, &mut size); 404 | err = GetLastError(); 405 | }; 406 | 407 | // The call should have failed and told us we need more space 408 | if err != ERROR_INSUFFICIENT_BUFFER { 409 | return Err(win32_error_with_context( 410 | "GetTokenInformation TokenIntegrityLevel unexpected failure", 411 | IoError::last_os_error(), 412 | )); 413 | } 414 | 415 | // Allocate and zero out the storage 416 | let mut data = vec![0u8; size as usize]; 417 | 418 | unsafe { 419 | if GetTokenInformation( 420 | self.token, 421 | TokenIntegrityLevel, 422 | data.as_mut_ptr() as *mut _, 423 | size, 424 | &mut size, 425 | ) == 0 426 | { 427 | return Err(win32_error_with_context( 428 | "GetTokenInformation TokenIntegrityLevel", 429 | IoError::last_os_error(), 430 | )); 431 | } 432 | }; 433 | 434 | Ok(TokenIntegrityLevel { data }) 435 | } 436 | 437 | /// Return an enum value that indicates the degree of elevation 438 | /// applied to the current token; this can be one of: 439 | /// TokenElevationTypeDefault, TokenElevationTypeFull, 440 | /// TokenElevationTypeLimited. 441 | fn elevation_type(&self) -> IoResult { 442 | let mut ele_type: TOKEN_ELEVATION_TYPE = 0; 443 | let mut size: DWORD = 0; 444 | let res = unsafe { 445 | GetTokenInformation( 446 | self.token, 447 | TokenElevationType, 448 | &mut ele_type as *mut TOKEN_ELEVATION_TYPE as *mut _, 449 | std::mem::size_of_val(&ele_type) as u32, 450 | &mut size, 451 | ) 452 | }; 453 | if res != 1 { 454 | Err(win32_error_with_context( 455 | "GetTokenInformation TOKEN_ELEVATION_TYPE", 456 | IoError::last_os_error(), 457 | )) 458 | } else { 459 | Ok(ele_type) 460 | } 461 | } 462 | 463 | /// Determine the effective privilege level of the token 464 | pub fn privilege_level(&self) -> IoResult { 465 | let ele_type = self.elevation_type()?; 466 | if ele_type == TokenElevationTypeFull { 467 | return Ok(PrivilegeLevel::Elevated); 468 | } 469 | 470 | let level = self.integrity_level()?; 471 | if !level.is_high() { 472 | return Ok(PrivilegeLevel::NotPrivileged); 473 | } 474 | 475 | let imp_token = self.duplicate_as_impersonation_token()?; 476 | if imp_token.check_administrators_membership()? { 477 | Ok(PrivilegeLevel::HighIntegrityAdmin) 478 | } else { 479 | Ok(PrivilegeLevel::NotPrivileged) 480 | } 481 | } 482 | 483 | /// Impersonate applies the token to the current thread only. 484 | /// This isn't a supported API: it is present to furnish an 485 | /// example that shows that it doesn't behave how you might 486 | /// expect! 487 | #[doc(hidden)] 488 | pub fn impersonate(&self) -> IoResult<()> { 489 | let res = unsafe { ImpersonateLoggedOnUser(self.token) }; 490 | if res != 1 { 491 | Err(win32_error_with_context( 492 | "ImpersonateLoggedOnUser", 493 | IoError::last_os_error(), 494 | )) 495 | } else { 496 | Ok(()) 497 | } 498 | } 499 | } 500 | 501 | #[cfg(test)] 502 | mod test { 503 | use super::*; 504 | 505 | #[test] 506 | fn get_own_token() { 507 | let token = Token::with_current_process().unwrap(); 508 | let level = token.privilege_level().unwrap(); 509 | // We can't make any assertions about what the level is, 510 | // but we can at least assume that we should be able 511 | // to successfully reach this point 512 | eprintln!("priv level is {:?}", level); 513 | 514 | // Verify that we can build a medium token from this, 515 | // and that the medium token doesn't show as privileged 516 | let medium = token.as_medium_integrity_safer_token().unwrap(); 517 | let level = medium.privilege_level().unwrap(); 518 | assert_eq!(level, PrivilegeLevel::NotPrivileged); 519 | } 520 | 521 | #[test] 522 | fn get_shell_token() { 523 | // We should either successfully obtain the shell token (if we're 524 | // connected to a desktop with a shell), or get a NotFound error. 525 | // We treat any other error as a test failure. 526 | match Token::with_shell_process() { 527 | Ok(_) => eprintln!("got shell token!"), 528 | Err(err) => match err.kind() { 529 | std::io::ErrorKind::NotFound => eprintln!("There is no shell"), 530 | _ => panic!("failed to get shell token: {:?}", err), 531 | }, 532 | } 533 | } 534 | } 535 | --------------------------------------------------------------------------------