Deep Dive into Windows Sudo

self-destruct-mission-impossible

Last week, I wrote about the new things coming in Windows 24H2. This week, we turn our focus to understanding everything about Windows Sudo. Windows Sudo let’s local admins elevate without running as admin, but there is much more to it. Today, we will discuss:

This will be part 1 of 2. The following week, we will look more at examining Windows Sudo with Process Monitor and exploring more about how it works on a local PC.

How Console Interactions and RunAs Work Today

First, let’s define a few things:

The Windows Console Host is the server app for all Windows Console APIs and the Windows UI for working with CLI apps. The binary contents are part of csrs.exe.

The Console Driver is the device in this architecture below. It’s an intermediate message-handling comm layer between two processes (basically the CLI app and the conhost).

Graphic of command line communication from source to destination

Console handles are what the process leverages to access input and screen buffers in its console. It uses a few different functions like these below to open the handles:

GetStdHandle: Standard input, output, and error handles within a process are retrieved by this function.

CreateFile: Enables a process to get a handle to its console’s input buffer and active screen buffer.

CreateConsoleScreenBuffer: Creates a new screen buffer and returns a handle

Today, when you run anything inside of a console window like PowerShell or CMD, it uses ConDrv (console driver) to communicate with the console server (conhost). Once you execute any sort of command like edit.exe, it will inherit the same console handles and talk to the same console over ConDrv.

Graphic of the console driver process of executing executables

So, how do things change when you leverage RunAs?

The OS creates a new elevated console to serve the console APIs for the target as you can see below (this will be a good idea to become familiar with as its similar to Windows Sudo):

graphic of the runas elevation inside of CLI

What is Windows Sudo?

We start with a simple question: What exactly is Sudo? Historically, we know what Sudo is in the Linux world. Sudo let’s an administrator elevate themselves or other users to run something as root or potentially another user. For years, we’ve leveraged Sudo to perform various functions like restarting services, kicking off jobs, etc.

Windows Sudo is a way for a local admin to run elevated commands from an unelevated console session. You can run it in one of 3 ways: new window (ideal), in the same window, or with all input disabled. In addition, unlike typical Sudo you cannot just provide credentials to use Sudo. You must be a local administrator to execute it. You can change between the 3 ways via command line:

sudo config --enable (disable, enable, forceNewWindow, disableInput, normal, default)

The goal of Sudo is more so to elevate an existing window without needing to open a new elevated one.

Let’s discuss the 3 different options for configuring Sudo, but first check out this video about Sudo:

When you leverage Sudo Inline, sudo.exe launches a new elevated process, with the original process establishes an RPC connection with the new process. Basically, imagine it’s a pipeline feeding information from one process to another. Handles are being passes allow the elevated process to read input and write output back.

If you’re using, Input Closed mode of Sudo the elevated process isn’t passed any input and cannot read it. The reason we prefer “New Windows Mode” is these modes have security concerns.

A medium integrity process could drive the elevated process. Ironically despite Input Closed not being a great option it will mitigate that concern. Malicious processes can definitely exploit this, which is why we prefer to avoid it altogether.

Graphic of Sudo using RPC to send communication from a regular process to an elevated one

When we use Sudo in new window mode, Sudo will launch a new elevated console window to run the command inside of that window. The working directory will persist as will the environment variables. The flow itself is very similar to RunAs, which we discussed a few minutes ago. This feature is known as “forceNewWindow” and is the default setting for Sudo on Windows.

One thing to note, that there is a GPO for setting Sudo:

HKLM\Software\Policies\Microsoft\Windows\Sudo\Enabled, but it is NOT supported by any CSP at this point. You can use this PowerShell script to set the maximum value. So, it’s a bit confusing.

The registry setting is for the “max level allowed” with the numbers being:

Disabled = 0,
ForceNewWindow = 1,
DisableInput = 2,
Normal = 3,

So, if you set to 0 it disables it, set to 1 it enables it with ForceNewWindow, set it to 2 (it allows 0,1,2) and set it to 3 and it allows (0,1,2,3).

The only true control you get is by using 0 or 1. If you choose to use 2 or 3, it will let the user select which mode they want. I recommend setting it like this below:

#Set Sudo Level to forceNewWindow
sudo config --enable forceNewWindow

# Configure registry to only allow forceNewWindow
$registryPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Sudo"
# Define the key name and value
$keyName = "Enabled"
$value = 1
# Ensure the registry path exists or create it if it doesn't
if (-not (Test-Path $registryPath)) {
    # Create the registry path if it does not exist
    New-Item -Path $registryPath -Force | Out-Null
    Write-Host "Registry path created."
}
# Create or update the 'Enabled' key and set its value to 1
New-ItemProperty -Path $registryPath -Name $keyName -Value $value -PropertyType DWord -Force
Write-Host "Registry key 'Enabled' has been created or updated to 1."

Reviewing the Code of Windows Sudo

The code for Sudo is open sourced by Microsoft here. Let’s look at the source code folder a bit to see how some of it works. We have a few different files (let’s remember that this is written in Rust for fun):

Now, we will take a look at these files to see how they work!

Windows Sudo Main

Like most Rust apps, main’s job is to load the application. The main, loads our different modules referenced above, which is no big surprise. It will also identify its command line arguments and start the RPC server via the elevate handler, leverage the run handler to bring in the environment variables, sudo mode, and directory. We also see it pull certain Windows modules in: use windows::core::, Win32::Foundation::, Win32::System::Console::*;

Main essentially defines many of the key functions for Sudo:

  • Sudo_cli: this function is the sudo application that you essentially execute in the command line. The function has all of the commands that we see in the CLI like config, commands, disable, etc.
  • Run_Builder(): this function returns a command object as part of the command system.
  • Run_Args(): this function runs the arguments passed.
  • Log_Modes: checks to see if sudo is set to disabled and checks to see if trace logging is set.
  • Check_Enabled_or_Bail(): verified sudo is enabled. If it isn’t it will exit the process.
  • Allowed_Mode_For_Help(): checks the config for allowed mode.
  • Enable_VT(): enables VT mode (virtual terminal mode)
  • Restore_Console_Mode: gets the output handle and restores console mode.
  • Main(): runs the elevate, run, and config commands passed from the cli.
  • Do_Run: runs the passed sudo command
  • Do_Elevate: gets the parent PID, collects the command line, and starts the RPC server for the elevation command.
  • Do_Config: executes the config passed from the sudo_cli
  • Try_Enable_Sudo: executes the enabling of sudo from the sudo_cli

Windows Sudo Elevate Handler

The elevate handler pulls in the helper, logging bindings for event logs, ElevateRequest messages, rpc_bindings_server’s RPC server setup, and tracing. We also see it pulling in a few standard library items around handles and commands and standard io along with certain Windows modules in: : use windows::core::*, Win32::Foundation::, Win32::System::Console::*, Win32::System::Threading::*;

The elevate handler functions are:

  • handle_to_stdio: converts the Windows handle into a Rust stdio object.
  • spawn_target_for_request: Prepare the target process, spawn it, and hand back the Child process. This will take care of setting up the handles for redirected input/output and setting the environment variables.
  • handle_elevation_request: execute the elevation request, attach to the parent process’ console (if needed), and spawn the target process with redirected IO if needed along with the environment variables passed if required. This is called by the handle_elevation_request
  • start_rpc_server: Starts the RPC server and blocks until Shutdown() is called.

Windows Sudo Helpers

Now, we move onto helpers.rs. Helpers pulls in a few of the other pieces of the application to make things work. We, see it pulls in the RPC bindings and trace log messaging. We also see it references standard modules like other pieces of the code.

  • Pulls in the GetFullPathNameW from Win32 Storage FileSystem
  • Pulls in Win32 System Diagnostics Debug items (IMAGE_NT_HEADERS32, IMAGE_SUBSYSTEM)
  • Win32 System Environment strings (FreeEnvironmentStringsW, GetEnvironmentStringsW)
  • Win32 System RPC’s RPC_STATUS
  • Pulls in System Services items like (IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_NT_SIGNATURE, SE_TOKEN_USER, SE_TOKEN_USER_1)
  • Windows Core Services (core::, Win32::Foundation::, Win32::Security::Authorization::, Win32::Security::,Win32::System::Console::, Win32::System::Threading::)

They also define some constants, which will be helpful with troubleshooting:

  • RPC_S_ACCESS_DENIED
  • E_FILENOTFOUND
  • E_CANCELLED
  • MSG_DIR_BAD_COMMAND_OR_FILE (will be seen as Error 9009)
  • E_DIR_BAD_COMMAND_OR_FILE
  • E_ACCESS_DISABLED_BY_POLICY

They also specify integers for the different modes, which map to different implementations and use a match to make sure they’re correct:

pub enum SudoMode {
    Disabled = 0,
    ForceNewWindow = 1,
    DisableInput = 2,
    Normal = 3,
}
impl TryFrom<u32> for SudoMode {
    type Error = Error;
    fn try_from(value: u32) -> Result<Self> {
        match value {
            0 => Ok(SudoMode::Disabled),
            1 => Ok(SudoMode::ForceNewWindow),
            2 => Ok(SudoMode::DisableInput),
            3 => Ok(SudoMode::Normal),
            _ => Err(ERROR_INVALID_PARAMETER.into()),
        }
    }
}

Before using the memory, we see they call transmute, copy, and zero to free the memory. Now, we move onto functions:

  • ignore_ctrl_c: this function is interesting as it allows it to ignore Control C to cancel Sudo functions.
  • generate_rpc_endpoint_name: uses this format for the name: r”sudo_elevate_{pid}_{nonce}
  • is_running_elevated(): checks the token to see if the session is currently elevated or not.
  • current_process_token(): opens the process token.
  • get_process_token: gets and opens the process token.

We also see a helper trait that gets the token information class for a given type, which in turn implements the TokenElevationType, TokenElevation, and TokenUser for that token type.

  • get_token_info: Gets the token info for the process token
  • can_current_user_elevate(): Checks if that user is able to elevate or not by looking at the current process token and seeing if the elevation type is Full or Limited.
  • get_sid_for_process: Gets the SID from the get process token function and sets the token user as SE_TOKEN_USER
  • get_current_user(): sets use as the SID for the current process and converts it to a string: ConvertSidToStringSidW(PSID(&user.Sid as *const _ as _), &mut *str_sid)?; and then uses str_sid.to_hstring()
  • is_cmd_intrinsic: checks to see if the command is intrinsic or not e.g. copy, echo, move
  • env_as_string(): returns the environment as a null-delimited string
  • env_from_raw_bytes: Splits a null-delimited environment string into key/value pairs.
  • join_args: handles distinct command line parameters and are given as a single string. This function handles these sorts of scenarios.
  • pack_string_list_for_rpc: Joins a list of strings into a single string, each of which is null-terminated (including the final one).
  • unpack_string_list_from_rpc: Splits a string generated by `pack_args` up again.

We’ll also see the ConfigProvider declared as a trait and it runs a few functions to get_setting_mode and get_policy_mode out of the main.rs. It pulls data out of the registry to see if Sudo is enabled:

    fn get_setting_mode(&self) -> Result<u32> {
        windows_registry::LOCAL_MACHINE
            .open("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Sudo")
            .and_then(|key| key.get_u32("Enabled"))
    }
    fn get_policy_mode(&self) -> Result<u32> {
        windows_registry::LOCAL_MACHINE
            .open("SOFTWARE\\Policies\\Microsoft\\Windows\\Sudo")
            .and_then(|key| key.get_u32("Enabled"))
    }
}
  • get_allowed_mode_from_policy: checks in the registry to get the current mode allowed by policy.
  • get_setting_mode: Get the current setting mode from the registry
  • get_allowed_mode: Get the current allowed mode from the registry
  • get_process_path_from_handle: Get the process full path.
  • check_client Check the client process matches the server process.
  • absolute_path: Make a Windows path absolute, using GetFullPathNameW to resolve the file on disk.
  • read_struct_at: This reads data from a specific offset
  • get_exe_subsystem: This extracts the subsystem field from the headers to figure out how to run it.

Windows Sudo Messages

This one is interesting because it pulls the SudoMode helper and defines the structure for the Elevation Request:

use crate::helpers::SudoMode;
use windows::{core::GUID, Win32::Foundation::HANDLE};
pub struct ElevateRequest {
    pub parent_pid: u32,
    pub handles: [HANDLE; 3], // in, out, err
    pub sudo_mode: SudoMode,
    pub application: String,
    pub args: Vec<String>,
    pub target_dir: String,
    pub env_vars: String,
    pub event_id: GUID,
}

Windows Sudo RPC Client Bindings

This one is interesting because it pulls the SudoMode helper, calls the RPC bindings that rpc_bindings.ps defines, and things start to come together. A few of the things it does:

  • Pulls in Windows Core items: GUID, HRESULT, PCSTR, PSTR
  • Pulls in the “handle” from Win32 Foundation
  • Pulls in GetFileType, FILE_TYPE_DISK, and FILE_TYPE_PIPE from Win32 Storage
  • Pulls in RPC functions, constants, and types (RpcBindingFree, RpcBindingFromStringBindingA, RpcMgmtIsServerListening, RpcStringBindingComposeA, RpcStringFreeA, RPC_STATUS, RPC_S_OK)

We see a few different functions in here as well:

  • seh_wrapper_client_DoElevationRequest: used in the rpc_client_do_elevation_request for structured error handling.
   binding: *mut c_void,
        parent_handle: HANDLE,
        pipe_handles: *const [HANDLE; 3], // in, out, err
        file_handles: *const [HANDLE; 3], // in, out, err
        sudo_mode: u32,
        application: Utf8Str,
        args: Utf8Str,
        target_dir: Utf8Str,
        env_vars: Utf8Str,
        event_id: GUID,
        child: *mut HANDLE,
  • seh_wrapper_client_Shutdown: for the wrapper client shutdown obviously.
  • rpc_client_setup: sets up the rpc client.
  • rpc_client_cleanup: cleans up the RPC server, which is implemented on the server-side in server_Shutdown. This terminates the process on the RPC server.
  • rpc_client_do_elevation_request: You can see below; this is where it does the elevation request (this occurs in non-forceNewWindow modes):
screenshot of the RPC client elevation request.

Windows Sudo RPC Server Bindings

In the RPC server bindings, it will pull in all of the helpers along with the elevate handler’s handle elevation request, elevate request messages, and the RPC bindings. We see more data structure items being along with some Windows calls like before:

  • It’s using Win32 Foundation’s ERROR BUSY, GENERIC_ALL, HANDLE, PSID
  • We also see Windows items as well (core::, Win32::Security::Authorization::, Win32::Security::, Win32::System::Memory::,Win32::System::Rpc::, Win32::System::SystemServices::,, Win32::System::Threading::*)

It’s interesting that it defines a process-wide mutex so only one request is handled at a time. They define the RPC_SERVER_IN_USE from that perspective:

static RPC_SERVER_IN_USE: AtomicBool = AtomicBool::new(false);

We see a callback function may pass the handle toRpcImpersonateClient, RpcBindingServerFromClient, RpcGetAuthorizationContextForClient, or any other server-side function that accepts a client binding handle to obtain information about the client.

Now the functions:

  • create_security_descriptor_for_process(pid: u32) creates a descriptor of form
  • rpc_server_setup: as you can see below sets up the RPC server:
pub fn rpc_server_setup(endpoint: &CStr, expected_client_pid: u32) -> Result<()> {
    let owned_sd = create_security_descriptor_for_process(expected_client_pid)?;
    unsafe {
        RpcServerUseProtseqEpA(
            /* Protseq            */ s!("ncalrpc"),
            /* MaxCalls           */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
            /* Endpoint           */ PCSTR(endpoint.as_ptr() as _),
            /* SecurityDescriptor */ Some(&owned_sd.sd as *const _ as _),
        )
        .ok()?;
        RpcServerRegisterIf3(
            /* IfSpec             */ server_sudo_rpc_ServerIfHandle,
            /* MgrTypeUuid        */ None,
            /* MgrEpv             */ None,
            /* Flags              */ RPC_IF_ALLOW_LOCAL_ONLY | RPC_IF_ALLOW_SECURE_ONLY,
            /* MaxCalls           */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
            /* MaxRpcSize         */ u32::MAX,
            /* IfCallback         */ Some(rpc_server_callback),
            /* SecurityDescriptor */ Some(&owned_sd.sd as *const _ as _),
        )
        .ok()?;
        EXPECTED_CLIENT_PID = expected_client_pid;
        let res = RpcServerListen(
            /* MinimumCallThreads */ 1,
            /* MaxCalls           */ RPC_C_LISTEN_MAX_CALLS_DEFAULT,
            /* DontWait           */ 0,
        );
        if res.is_err() {
            _ = RpcServerUnregisterIf(None, None, 0);
        }
        res.ok()
    }
}
  • server_Shutdown: the RPC’s sudo_rpc::Shutdown callback function
  • server_DoElevationRequest :is the RPC’s sudo_rpc::DoElevationRequest callback function. You will see at the end of it that it passes the result to the wrap_elevate_request function.
  • wrap_elevate_request: creates the wrapper/collects the data for the actual request that is submitted for the elevate request.

Windows Sudo Run Handler

We move onto the Run handler to finish things up. We see it pulling in all of this:

  • Elevate Handler (spawn target for request)
  • Everything from helpers.rs
  • Logging Bindings (event log request)
  • Messages (Elevate request)
  • RPC Bindings structures
  • RPC Client Bindings (RPC client cleanup, RPC client do elevation request, RPC client setup)
  • Resource IDs and their code

We see more data structure items being along with some Windows calls like before:

  • Windows WDK Foundation (NTQueryObject, ObjectBasicInformation)
  • Windows Win32 System Windows Programming (Public Object Basic Information)
  • Windows (core::, Wdk::System::Threading::, Win32::Foundation::, Win32::Storage::FileSystem::,Win32::System::Console::, Win32::System::Diagnostics::Debug::, Win32::System::Rpc::, Win32::System::SystemInformation::, Win32::System::Threading::, Win32::UI::Shell::,Win32::UI::WindowsAndMessaging::*)

Now, we move onto the functions:

  • current_elevation_matches_request: (This is on their to-do list to support running as another user)
  • get_process_creation_time: helper to find the process creation time for a given process handle
  • is_in_windows_dir: validates if it’s in the windows directory
  • adjust_args_for_intrinsics_and_cmdlets: Attempts to modify this request to run the command in CMD, if the “application” that was passed to us was really just a CMD intrinsic.
  • adjust_args_for_gui_exes: checks if the file is a valid EXE and modify the request to run it if necessary.
  • run_target: executes the do_request function
  • prepare_request: Constructs an ElevateRequest from the given arguments. It can even convert the request to run in CMD if it’s a CMD intrinsic.
  • do_request: takes the request from run_target, checks if the elevation matches the request. If it doesn’t, it needs to start the elevated sudo and send the request over. The code below is cool. It checks to see if the mode is “New Window” and copies the env and directory over. It’s also interesting they call it “runas” as the code says for runas will join everything together for execution else it will send it to the “handoff_to_elevated” function:
 let should_use_runas =
            req.sudo_mode == SudoMode::ForceNewWindow && !copy_env && !manually_requested_dir;
        if should_use_runas {
            tracing::trace_log_message("Direct ShellExecute");
            runas_admin(&req.application, &join_args(&req.args), SW_NORMAL)?;
            Ok(0)
        } else {
            tracing::trace_log_message("starting RPC handoff");
            handoff_to_elevated(&req)
        }
    }
}
  • random_nonce: Generate a random nonce to include in the RPC endpoint name
  • handoff_to_elevated: this code sets the parent PID, gets a random nonce, writes to the log, sets the arguments, and runs the elevation. It will even invoke the ignore ctrl_c function as well. It will finish up with sending the request via RPC.
 // Build a single string from the request's application and args
    let parent_pid = std::process::id();
    // generate a pseudorandom nonce to include
    let nonce = random_nonce();
    tracing::trace_log_message(&format!(
        "running as user: '{}'",
        get_current_user().as_ref().unwrap_or(h!("unknown"))
    ));
    let path = env::current_exe().unwrap();
    let target_args = format!(
        "elevate -p {parent_pid} -n {nonce} {} {}",
        req.application,
        join_args(&req.args)
    );
    tracing::trace_log_message(&format!("elevate request: '{target_args:?}'"));
    runas_admin(&path, &target_args, SW_HIDE)?;
 unsafe {
        _ = SetConsoleCtrlHandler(Some(ignore_ctrl_c), true);
    }
    send_request_via_rpc(req, nonce)
}
  • send_request_via_rpc: sends the request via RPC by calling the generate_rpc_endpoint_name helper and trying to connect to the RPC server. It will loop and try to connect 10 times. Once done, it will run the function “rpc_client_do_elevation_request” with its attributes:
let rpc_elevate = rpc_client_do_elevation_request(
        *h_real,
        &req.handles,
        req.sudo_mode,
        Utf8Str::new(&req.application),
        Utf8Str::new(&pack_string_list_for_rpc(&req.args)),
        Utf8Str::new(&req.target_dir),
        Utf8Str::new(&req.env_vars),
        req.event_id,
        &mut *child_handle,
    );

Once finished, it invokes the “rpc_client_cleanup” function and confirms that the request properly limited the handle access rights. The final piece of the function checks if we were in “ForceNewWindow” mode by calling the r.rs (resource ID code): to get the New Window and execute the elevation request in the new window:

 if req.sudo_mode == SudoMode::ForceNewWindow {
        let translated_msg = r::IDS_LAUNCHEDNEWWINDOW.get();
        let replaced = translated_msg.replace("{0}", &req.application);
        println!("{}", replaced);
        Ok(0)
    } else {
        unsafe {
            let mut status = 0u32;
            _ = WaitForSingleObject(*child_handle, INFINITE);
            GetExitCodeProcess(*child_handle, &mut status)?;
            Ok(status as _)
        }
    }
}
  • runas_admin: this creates a generic wrapper for executing the EXE as admin with the “runas_admin_impl” function
where
    Exe: AsRef<OsStr> + ?Sized,
    Args: AsRef<OsStr> + ?Sized,
{
    runas_admin_impl(exe.as_ref(), args.as_ref(), show)
}
  • runas_admin_impl: uses the ShellExecuteExW to run a command as administrator by using the structure set by ShellExecuteInfoW:
let cwd = env::current_dir()?;
let h_exe = HSTRING::from(exe);
let h_commandline = HSTRING::from(args);
let h_cwd = HSTRING::from(cwd.as_os_str());
let mut sei = SHELLEXECUTEINFOW {
    cbSize: std::mem::size_of::<SHELLEXECUTEINFOW>() as u32,
    fMask: SEE_MASK_NOCLOSEPROCESS,
    lpVerb: w!("runas"),
    lpFile: PCWSTR(h_exe.as_ptr()),
    lpParameters: PCWSTR(h_commandline.as_ptr()),
    lpDirectory: PCWSTR(h_cwd.as_ptr()),
    nShow: show.0,
    ..Default::default()
};
unsafe { ShellExecuteExW(&mut sei) }

}

Understanding the Windows Sudo Code with Diagrams

After that giant wall of text, I think we could build some basic flow diagrams to show how a few of these things would work in theory in Windows OS.

Let’s look at a few things:

Changing the Sudo Config in CLI Process Flow

So, our high-level overview of this flow is:

  1. The administrator opens a CMD window and types: sudo config –enable forceNewWindow
  2. The sudo_cli function has an app running listening for input. Entering the word config triggers a subcommand to be captured, which will get information about the subcommand and arguments captured: Set the arg action and the forceNewWindow type.
  3. The main function has a match for elevate, run, or config subcommands. Once it sees config as a subcommand, it passes it to the do_config function.
  4. The do_config function sets the argument to the supported syntax e.g. SudoMode::ForceNewWindow and passes it to the try_enable_sudo function.
  5. The try_enable_sudo function checks if it is being run as elevated. If it isn’t, it will tell you: “You must run this command as an administrator.”
  6. Next, try_enable_sudo checks what your policy allows for the setting by invoking get_allowed_mode_from_policy from helpers.rs
  7. If requested mode is greater than the maximum mode allowed by policy it will do a match to confirm the policy setting is valid.
  8. Sets the Windows registry to the proper integer at HKLM\Microsoft\Windows\CurrentVersion\Sudo and key Enabled to 1 (for forceNewWindow)
  9. Afterwards, it will print what the new mode is set to.
Flow diagram of changing the Sudo mode via CLI

Running an Elevated Process with Sudo in New Windows Mode

Now, let’s look at how this flow works. What’s interesting is if you procmon it, you will see it starts iterating through several DLLs, until it runs into one that actually needs admin before you will see sudo appear.

  1. The administrator opens a CMD window and types: sudo netstat -ano
  2. The run target function runs capturing the arguments in a string, the sudo mode, requested directory. This executes the prepare request function.
  3. The prepare request sets the copy_env to true, the arguments, sudo mode, and directory and builds the elevation request. It will also set the pid, handles, application, and event id.
  4. Next, the prepare request info, the environment variables, and directory is passed to the do_request function.
  5. Do_request checks if the process is running as admin, and if it isn’t it will execute the runas_admin since this is leveraging ForceNewWindow. If it wasn’t, it would use the function handoff_to_elevated.
  6. The runas_admin function creates a generic wrapper for executing the EXE as admin with the “runas_admin_impl” function.
  7. The Runas_admin_impl function executes the command as administrator with ShellExecuteExW to by using the structure set by ShellExecuteInfoW
  8. RPC cleanup occurs terminating the process.
Flow diagram of using sudo in forceNewWindow mode

Final Thoughts on Windows Sudo

So, we covered a ton of stuff around Windows Sudo in this article. It’s interesting that this is basically an exploding runas /user:Administrator. I think one of my major takeaways was not being aware of the Windows Shell API, which is neat. One thing that gives me a bit of pause is a bit of a marketing faux pas. This tech is very neat without question, but they never should have called it Sudo. Similar to recent complaints from the community about the “Windows App” and the upcoming “Shared Frontline”, we need to be more careful. Certain words elicit specific feelings for administrators, and Sudo is one of those.

I’m looking forward to when you can use Sudo as other user, which is on the roadmap per the documentation. I think Sudo as a technology is a very exciting time, but we need to think less about the name than the spirit of it.

Facebook
Twitter
LinkedIn
This week, the focus shifts to Windows Sudo, which allows local admins to elevate commands without full admin access. The content discusses how Windows Console functions, the workings of Windows Sudo, and its code structure. Future discussions will delve deeper into its operation and monitoring through Process Monitor.

2 thoughts on “Deep Dive into Windows Sudo”

  1. Thanks for detailed article.
    We implemented sudo via intune, but to run sudo it is asking to elevate powershell as admin first, is this the case?

    thanks

Let me know what you think

Scroll to Top

Discover more from Mobile Jon's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading