Skip to content

Commit ec383ce

Browse files
committed
ci: run PEDM simulator as LocalSystem using psexec
1 parent 7d706c5 commit ec383ce

4 files changed

Lines changed: 90 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+
- name: Run PEDM simulator (as LocalSystem)
1017+
run: |
1018+
# It’s difficult to get realtime stdout/stderr when using psexec -s.
1019+
# Instead, the run-expect-elevation.ps1 script will redirect the output into a file that we can read afterwards.
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: 50 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,56 @@ 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!("Attempting to verify whether the current account is assigned with the SE_CREATE_TOKEN_NAME privilege");
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+
let rights = enumerate_account_rights(&account.sid)
37+
.with_context(|| format!("enumerate account rights for {account_username:?}"))?;
38+
let has_create_token_right = rights.iter().any(|right| right == u16cstr!("SeCreateTokenPrivilege"));
39+
40+
if expect_elevation {
41+
assert!(has_create_token_right);
42+
43+
// SE_CREATE_TOKEN_NAME is required for performing the elevation.
44+
let se_create_token_name_luid =
45+
privilege::lookup_privilege_value(None, privilege::SE_CREATE_TOKEN_NAME)
46+
.context("lookup SE_CREATE_TOKEN_NAME privilege")?;
47+
token
48+
.adjust_privileges(&TokenPrivilegesAdjustment::Enable(vec![se_create_token_name_luid]))
49+
.context("enable SE_CREATE_TOKEN_NAME privilege")?;
50+
51+
// Verify the SE_CREATE_TOKEN_NAME privilege is actually enabled.
52+
let se_create_token_name_is_enabled = token
53+
.privileges()
54+
.context("list token privileges")?
55+
.as_slice()
56+
.iter()
57+
.find(|privilege| privilege.Luid == se_create_token_name_luid)
58+
.is_some();
59+
60+
assert!(se_create_token_name_is_enabled);
61+
}
62+
}
63+
Err(e) => {
64+
println!("Failed to look up account for {account_username:?}: {e}");
65+
66+
// Possible issue when running this program using psexec -s, under `NT AUTHORITY\SYSTEM` (LocalSystem):
67+
// - There is no direct domain credentials by default, unless the machine is domain joined and has a line-of-sight to the DC.
68+
// - This context may not be able to see the same network resources or DC that the interactive user.
69+
// Causing LookupAccountNameW to fail with a "no mapping" error.
70+
// Let’s just go ahead with the elevation in this case, assuming LocalSystem is enough for all intents and purposes at this point.
71+
if e.code() == HRESULT(0x80070534u32 as i32) {
72+
println!("Got the 'no mapping' error; continuing...")
73+
} else {
74+
return Err(anyhow::Error::new(e).context(format!(
75+
"unexpected error when looking up the account for {account_username:?}"
76+
)));
77+
}
78+
}
5479
}
5580

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

0 commit comments

Comments
 (0)