Skip to content

Commit cdbe0b7

Browse files
authored
ci: run PEDM simulator as LocalSystem using psexec (#1296)
1 parent 46435e5 commit cdbe0b7

4 files changed

Lines changed: 97 additions & 37 deletions

File tree

.github/workflows/ci.yml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,17 +1000,27 @@ jobs:
10001000
ref: ${{ needs.preflight.outputs.ref }}
10011001

10021002
- name: Prepare runner
1003-
run: rustup toolchain install nightly
1003+
run: |
1004+
rustup toolchain install nightly
1005+
choco install pstools --yes
10041006
10051007
- name: Prepare PEDM simulator build
10061008
run: ./crates/pedm-simulator/prepare.ps1
10071009

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

1011-
- name: Run PEDM simulator (limited test)
1013+
- name: Run PEDM simulator (container with limited user)
10121014
run: ./crates/pedm-simulator/run-container.ps1
10131015

1016+
# It’s difficult to get realtime stdout/stderr when using psexec -s.
1017+
# Instead, the run-expect-elevation.ps1 script will redirect the output into a file that we can read afterwards.
1018+
- name: Run PEDM simulator (as LocalSystem)
1019+
run: |
1020+
$scriptPath = Resolve-Path -Path "./crates/pedm-simulator/run-expect-elevation.ps1"
1021+
psexec -accepteula -s pwsh.exe $scriptPath
1022+
Get-Content -Path ./crates/pedm-simulator/pedm-simulator_run-expect-elevation.out
1023+
10141024
success:
10151025
name: Success
10161026
runs-on: ubuntu-latest

crates/pedm-simulator/README.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99
## Limited test with no elevation
1010

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

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

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

@@ -25,9 +25,11 @@ the privilege is not available.
2525
.\run-container.ps1
2626
```
2727

28+
You may also consider logging into a different user with lower privileges.
29+
2830
## Complete test with elevation
2931

30-
It is recommended to use a dedicated VM.
32+
It is recommended to use a dedicated VM for this test.
3133

3234
Visual C++ Redistribuable are required.
3335

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

43-
Retrieve the artifacts (including clang_rt.asan_dynamic-x86_64.dll).
45+
Retrieve the artifacts (including `clang_rt.asan_dynamic-x86_64.dll`).
4446

45-
For the next steps, make sure to use an account with the SE_CREATE_TOKEN_NAME
47+
For the next steps, make sure to use an account with the `SE_CREATE_TOKEN_NAME`
4648
privilege assigned.
4749

4850
Set the `PEDM_SIMULATOR_EXPECT_ELEVATION` environment variable.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/bin/env pwsh
2+
3+
$ErrorActionPreference = "Stop"
4+
5+
Push-Location -Path $PSScriptRoot
6+
7+
$exitCode = 0
8+
9+
try {
10+
$Env:PEDM_SIMULATOR_EXPECT_ELEVATION = '1'
11+
& ./artifacts/pedm-simulator.exe 2>&1 | Out-File ./pedm-simulator_run-expect-elevation.out
12+
$exitCode = $LASTEXITCODE
13+
} finally {
14+
Pop-Location
15+
exit $exitCode
16+
}

crates/pedm-simulator/src/main.rs

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::Context as _;
44
use win_api_wrappers::identity::account::{enumerate_account_rights, get_username, lookup_account_by_name};
55
use win_api_wrappers::identity::sid::{Sid, SidAndAttributes};
66
use win_api_wrappers::process::Process;
7+
use win_api_wrappers::raw::core::HRESULT;
78
use win_api_wrappers::raw::Win32::Foundation::LUID;
89
use win_api_wrappers::raw::Win32::Security;
910
use win_api_wrappers::raw::Win32::System::SystemServices;
@@ -25,32 +26,63 @@ fn main() -> anyhow::Result<()> {
2526
.context("open current process token")?;
2627

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

34-
if expect_elevation {
35-
assert!(has_create_token_right);
36-
37-
// SE_CREATE_TOKEN_NAME is required for performing the elevation.
38-
let se_create_token_name_luid = privilege::lookup_privilege_value(None, privilege::SE_CREATE_TOKEN_NAME)
39-
.context("lookup SE_CREATE_TOKEN_NAME privilege")?;
40-
token
41-
.adjust_privileges(&TokenPrivilegesAdjustment::Enable(vec![se_create_token_name_luid]))
42-
.context("enable SE_CREATE_TOKEN_NAME privilege")?;
43-
44-
// Verify the SE_CREATE_TOKEN_NAME privilege is actually enabled.
45-
let se_create_token_name_is_enabled = token
46-
.privileges()
47-
.context("list token privileges")?
48-
.as_slice()
49-
.iter()
50-
.find(|privilege| privilege.Luid == se_create_token_name_luid)
51-
.is_some();
52-
53-
assert!(se_create_token_name_is_enabled);
34+
match lookup_account_by_name(&account_username) {
35+
Ok(account) => {
36+
// Verify that the current account is assigned with the SE_CREATE_TOKEN_NAME privilege.
37+
println!(
38+
"Attempting to verify whether the current account is assigned with the SE_CREATE_TOKEN_NAME privilege"
39+
);
40+
41+
let rights = enumerate_account_rights(&account.sid)
42+
.with_context(|| format!("enumerate account rights for {account_username:?}"))?;
43+
let has_create_token_right = rights.iter().any(|right| right == u16cstr!("SeCreateTokenPrivilege"));
44+
println!("Has SeCreateTokenPrivilege right? {has_create_token_right}");
45+
46+
if expect_elevation {
47+
assert!(has_create_token_right);
48+
println!("Current user is assigned the SeCreateTokenPrivilege right. Enabling it...");
49+
50+
// SE_CREATE_TOKEN_NAME is required for performing the elevation.
51+
let se_create_token_name_luid =
52+
privilege::lookup_privilege_value(None, privilege::SE_CREATE_TOKEN_NAME)
53+
.context("lookup SE_CREATE_TOKEN_NAME privilege")?;
54+
token
55+
.adjust_privileges(&TokenPrivilegesAdjustment::Enable(vec![se_create_token_name_luid]))
56+
.context("enable SE_CREATE_TOKEN_NAME privilege")?;
57+
58+
// Verify the SE_CREATE_TOKEN_NAME privilege is actually enabled.
59+
let se_create_token_name_is_enabled = token
60+
.privileges()
61+
.context("list token privileges")?
62+
.as_slice()
63+
.iter()
64+
.find(|privilege| privilege.Luid == se_create_token_name_luid)
65+
.is_some();
66+
67+
assert!(se_create_token_name_is_enabled);
68+
}
69+
}
70+
Err(e) => {
71+
println!("Failed to look up account for {account_username:?}: {e}");
72+
73+
// Possible issue when running this program using psexec -s, under `NT AUTHORITY\SYSTEM` (LocalSystem):
74+
// - There is no direct domain credentials by default, unless the machine is domain joined and has a line-of-sight to the DC.
75+
// - This context may not be able to see the same network resources or DC that the interactive user.
76+
// Causing LookupAccountNameW to fail with a "no mapping" error.
77+
// Let’s just go ahead with the elevation in this case, assuming LocalSystem is enough for all intents and purposes at this point.
78+
if e.code() == HRESULT(0x80070534u32 as i32) {
79+
println!("Got the 'no mapping' error; continuing...")
80+
} else {
81+
return Err(anyhow::Error::new(e).context(format!(
82+
"unexpected error when looking up the account for {account_username:?}"
83+
)));
84+
}
85+
}
5486
}
5587

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

0 commit comments

Comments
 (0)