Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1000,17 +1000,27 @@ jobs:
ref: ${{ needs.preflight.outputs.ref }}

- name: Prepare runner
run: rustup toolchain install nightly
run: |
rustup toolchain install nightly
choco install pstools --yes

- name: Prepare PEDM simulator build
run: ./crates/pedm-simulator/prepare.ps1

- name: Build PEDM simulator container
run: ./crates/pedm-simulator/build-container.ps1

- name: Run PEDM simulator (limited test)
- name: Run PEDM simulator (container with limited user)
run: ./crates/pedm-simulator/run-container.ps1

# It’s difficult to get realtime stdout/stderr when using psexec -s.
# Instead, the run-expect-elevation.ps1 script will redirect the output into a file that we can read afterwards.
- name: Run PEDM simulator (as LocalSystem)
run: |
$scriptPath = Resolve-Path -Path "./crates/pedm-simulator/run-expect-elevation.ps1"
psexec -accepteula -s pwsh.exe $scriptPath
Get-Content -Path ./crates/pedm-simulator/pedm-simulator_run-expect-elevation.out

success:
name: Success
runs-on: ubuntu-latest
Expand Down
22 changes: 12 additions & 10 deletions crates/pedm-simulator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@

## Limited test with no elevation

This mode is a stopgap until we have proper infrastructure for performing
the complete test in the CI. Indeed, there is no way to perform the complete
test using GitHub-hosted runners (or docker), because for security reasons
ContainerAdministrator is not assigned the SE_CREATE_TOKEN_NAME privilege, and
it’s not possible to perform nested virtualization either.
A limited test where the elevation procedure will fail. This test is still
useful because it will exercise a different code path than the "complete" test.
It’s also convenient for testing without using an elevated account with the
`SE_CREATE_TOKEN_NAME` privilege assigned.

You can run the pedm-simulator.exe executable directly.
When running under a user without the `SE_CREATE_TOKEN_NAME` privilege, you can
just run the `pedm-simulator.exe` executable directly.

If you are running using an elevated account with the SE_CREATE_TOKEN_NAME
If you are running using an elevated account with the `SE_CREATE_TOKEN_NAME`
privilege assigned, you can build and run a container instead to ensure
the privilege is not available.

Expand All @@ -25,9 +25,11 @@ the privilege is not available.
.\run-container.ps1
```

You may also consider logging into a different user with lower privileges.

## Complete test with elevation

It is recommended to use a dedicated VM.
It is recommended to use a dedicated VM for this test.

Visual C++ Redistribuable are required.

Expand All @@ -40,9 +42,9 @@ Start-Process -filepath C:\vc_redist.x64.exe -ArgumentList "/install", "/passive
Remove-Item -Force vc_redist.x64.exe
```

Retrieve the artifacts (including clang_rt.asan_dynamic-x86_64.dll).
Retrieve the artifacts (including `clang_rt.asan_dynamic-x86_64.dll`).

For the next steps, make sure to use an account with the SE_CREATE_TOKEN_NAME
For the next steps, make sure to use an account with the `SE_CREATE_TOKEN_NAME`
privilege assigned.

Set the `PEDM_SIMULATOR_EXPECT_ELEVATION` environment variable.
Expand Down
16 changes: 16 additions & 0 deletions crates/pedm-simulator/run-expect-elevation.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/bin/env pwsh

$ErrorActionPreference = "Stop"

Push-Location -Path $PSScriptRoot

$exitCode = 0

try {
$Env:PEDM_SIMULATOR_EXPECT_ELEVATION = '1'
& ./artifacts/pedm-simulator.exe 2>&1 | Out-File ./pedm-simulator_run-expect-elevation.out
$exitCode = $LASTEXITCODE
} finally {
Pop-Location
exit $exitCode
}
82 changes: 57 additions & 25 deletions crates/pedm-simulator/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use anyhow::Context as _;
use win_api_wrappers::identity::account::{enumerate_account_rights, get_username, lookup_account_by_name};
use win_api_wrappers::identity::sid::{Sid, SidAndAttributes};
use win_api_wrappers::process::Process;
use win_api_wrappers::raw::core::HRESULT;
use win_api_wrappers::raw::Win32::Foundation::LUID;
use win_api_wrappers::raw::Win32::Security;
use win_api_wrappers::raw::Win32::System::SystemServices;
Expand All @@ -25,32 +26,63 @@ fn main() -> anyhow::Result<()> {
.context("open current process token")?;

// Verify that the current account is assigned with the SE_CREATE_TOKEN_NAME privilege.
let account_username = get_username(Security::Authentication::Identity::NameSamCompatible).unwrap();
println!("Retrieve the current account name...");
let account_username =
get_username(Security::Authentication::Identity::NameSamCompatible).context("retrieve account username")?;
println!("Account name: {account_username:?}");
let account = lookup_account_by_name(&account_username).unwrap();
let rights = enumerate_account_rights(&account.sid).unwrap();
let has_create_token_right = rights.iter().any(|right| right == u16cstr!("SeCreateTokenPrivilege"));

if expect_elevation {
assert!(has_create_token_right);

// SE_CREATE_TOKEN_NAME is required for performing the elevation.
let se_create_token_name_luid = privilege::lookup_privilege_value(None, privilege::SE_CREATE_TOKEN_NAME)
.context("lookup SE_CREATE_TOKEN_NAME privilege")?;
token
.adjust_privileges(&TokenPrivilegesAdjustment::Enable(vec![se_create_token_name_luid]))
.context("enable SE_CREATE_TOKEN_NAME privilege")?;

// Verify the SE_CREATE_TOKEN_NAME privilege is actually enabled.
let se_create_token_name_is_enabled = token
.privileges()
.context("list token privileges")?
.as_slice()
.iter()
.find(|privilege| privilege.Luid == se_create_token_name_luid)
.is_some();

assert!(se_create_token_name_is_enabled);
match lookup_account_by_name(&account_username) {
Ok(account) => {
// Verify that the current account is assigned with the SE_CREATE_TOKEN_NAME privilege.
println!(
"Attempting to verify whether the current account is assigned with the SE_CREATE_TOKEN_NAME privilege"
);

let rights = enumerate_account_rights(&account.sid)
.with_context(|| format!("enumerate account rights for {account_username:?}"))?;
let has_create_token_right = rights.iter().any(|right| right == u16cstr!("SeCreateTokenPrivilege"));
println!("Has SeCreateTokenPrivilege right? {has_create_token_right}");

if expect_elevation {
assert!(has_create_token_right);
println!("Current user is assigned the SeCreateTokenPrivilege right. Enabling it...");

// SE_CREATE_TOKEN_NAME is required for performing the elevation.
let se_create_token_name_luid =
privilege::lookup_privilege_value(None, privilege::SE_CREATE_TOKEN_NAME)
.context("lookup SE_CREATE_TOKEN_NAME privilege")?;
token
.adjust_privileges(&TokenPrivilegesAdjustment::Enable(vec![se_create_token_name_luid]))
.context("enable SE_CREATE_TOKEN_NAME privilege")?;

// Verify the SE_CREATE_TOKEN_NAME privilege is actually enabled.
let se_create_token_name_is_enabled = token
.privileges()
.context("list token privileges")?
.as_slice()
.iter()
.find(|privilege| privilege.Luid == se_create_token_name_luid)
.is_some();

assert!(se_create_token_name_is_enabled);
}
}
Err(e) => {
println!("Failed to look up account for {account_username:?}: {e}");

// Possible issue when running this program using psexec -s, under `NT AUTHORITY\SYSTEM` (LocalSystem):
// - There is no direct domain credentials by default, unless the machine is domain joined and has a line-of-sight to the DC.
// - This context may not be able to see the same network resources or DC that the interactive user.
// Causing LookupAccountNameW to fail with a "no mapping" error.
// Let’s just go ahead with the elevation in this case, assuming LocalSystem is enough for all intents and purposes at this point.
if e.code() == HRESULT(0x80070534u32 as i32) {
println!("Got the 'no mapping' error; continuing...")
} else {
return Err(anyhow::Error::new(e).context(format!(
"unexpected error when looking up the account for {account_username:?}"
)));
}
}
}

let token_source = build_token_source(LADM_SRC_NAME, LADM_SRC_LUID);
Expand Down Expand Up @@ -122,7 +154,7 @@ fn main() -> anyhow::Result<()> {
} else {
match res {
Ok(_) => {
anyhow::bail!("admin token creation should have failed, because the current process is not elevated")
anyhow::bail!("admin token creation succeded, but this was not expected")
}
Err(e) => {
assert_eq!(e.to_string(), "no token found for SE_CREATE_TOKEN_NAME privilege");
Expand Down