use crate::ndr::{NdrDecoder, NdrEncoder};
use crate::transport::SmbPipe;
use crate::{Result, RpcError, Syntax};
use smb2_client::SmbClient;
pub fn svcctl_syntax() -> Syntax {
Syntax::new("367abb81-9844-35f1-ad32-98f038001003", 2, 0)
}
pub mod opnum {
pub const CLOSE_SERVICE_HANDLE: u16 = 0;
pub const DELETE_SERVICE: u16 = 2;
pub const CREATE_SERVICE_W: u16 = 12;
pub const OPEN_SC_MANAGER_W: u16 = 15;
pub const START_SERVICE_W: u16 = 19;
}
const SC_MANAGER_ALL_ACCESS: u32 = 0x000F_003F;
const SERVICE_ALL_ACCESS: u32 = 0x000F_01FF;
const SERVICE_WIN32_OWN_PROCESS: u32 = 0x0000_0010;
const SERVICE_DEMAND_START: u32 = 0x0000_0003;
const SERVICE_ERROR_IGNORE: u32 = 0x0000_0000;
const ERROR_SERVICE_REQUEST_TIMEOUT: u32 = 1053;
const ERROR_SERVICE_NO_THREAD: u32 = 1054;
const ERROR_EXCEPTION_IN_SERVICE: u32 = 1064;
const ERROR_PROCESS_ABORTED: u32 = 1067;
#[derive(Clone, Copy, Debug, Default)]
pub struct ScHandle(pub [u8; 20]);
impl ScHandle {
fn decode(d: &mut NdrDecoder) -> Result<Self> {
let attrs = d.u32()?;
let uuid = d.uuid()?;
let mut h = [0u8; 20];
h[..4].copy_from_slice(&attrs.to_le_bytes());
h[4..].copy_from_slice(&uuid);
Ok(ScHandle(h))
}
fn encode(&self, e: &mut NdrEncoder) {
e.bytes(&self.0);
}
fn is_null(&self) -> bool {
self.0 == [0u8; 20]
}
}
fn encode_open_scm(access: u32) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.null_ptr(); e.null_ptr(); e.u32(access);
e.into_bytes()
}
fn encode_create_service(scm: &ScHandle, name: &str, binpath: &str) -> Vec<u8> {
let mut e = NdrEncoder::new();
scm.encode(&mut e); e.conformant_varying_wstr(name); e.align(4);
e.null_ptr(); e.u32(SERVICE_ALL_ACCESS); e.u32(SERVICE_WIN32_OWN_PROCESS); e.u32(SERVICE_DEMAND_START); e.u32(SERVICE_ERROR_IGNORE); e.conformant_varying_wstr(binpath); e.align(4);
e.null_ptr(); e.null_ptr(); e.null_ptr(); e.u32(0); e.null_ptr(); e.null_ptr(); e.u32(0); e.into_bytes()
}
fn encode_start_service(svc: &ScHandle) -> Vec<u8> {
let mut e = NdrEncoder::new();
svc.encode(&mut e);
e.u32(0); e.null_ptr(); e.into_bytes()
}
fn encode_handle_only(h: &ScHandle) -> Vec<u8> {
let mut e = NdrEncoder::new();
h.encode(&mut e);
e.into_bytes()
}
fn tail_return(stub: &[u8]) -> u32 {
if stub.len() < 4 {
return u32::MAX;
}
u32::from_le_bytes(stub[stub.len() - 4..].try_into().unwrap())
}
pub struct SvcctlClient<'a> {
pipe: SmbPipe<'a>,
}
impl<'a> SvcctlClient<'a> {
pub async fn bind(client: &'a mut SmbClient, file_id: [u8; 16]) -> Result<Self> {
let mut pipe = SmbPipe::new(client, file_id);
pipe.bind(svcctl_syntax()).await?;
Ok(SvcctlClient { pipe })
}
async fn open_scm(&mut self) -> Result<ScHandle> {
let resp = self
.pipe
.call(
opnum::OPEN_SC_MANAGER_W,
&encode_open_scm(SC_MANAGER_ALL_ACCESS),
)
.await?;
let mut d = NdrDecoder::new(&resp);
let handle = ScHandle::decode(&mut d)?;
let ret = d.u32().unwrap_or(u32::MAX);
if ret != 0 || handle.is_null() {
return Err(RpcError::Protocol(format!(
"ROpenSCManagerW failed (win32 {ret})"
)));
}
Ok(handle)
}
async fn create_service(
&mut self,
scm: &ScHandle,
name: &str,
binpath: &str,
) -> Result<ScHandle> {
let resp = self
.pipe
.call(
opnum::CREATE_SERVICE_W,
&encode_create_service(scm, name, binpath),
)
.await?;
let ret = tail_return(&resp);
if ret != 0 {
return Err(RpcError::Protocol(format!(
"RCreateServiceW failed (win32 {ret})"
)));
}
let mut d = NdrDecoder::new(&resp);
let tag_ref = d.u32()?;
if tag_ref != 0 {
let _ = d.u32(); }
ScHandle::decode(&mut d)
}
async fn start_service(&mut self, svc: &ScHandle) -> Result<u32> {
let resp = self
.pipe
.call(opnum::START_SERVICE_W, &encode_start_service(svc))
.await?;
Ok(tail_return(&resp))
}
async fn delete_service(&mut self, svc: &ScHandle) -> Result<u32> {
let resp = self
.pipe
.call(opnum::DELETE_SERVICE, &encode_handle_only(svc))
.await?;
Ok(tail_return(&resp))
}
async fn close_handle(&mut self, h: &ScHandle) {
let _ = self
.pipe
.call(opnum::CLOSE_SERVICE_HANDLE, &encode_handle_only(h))
.await;
}
}
pub struct ExecResult {
pub service: String,
pub start_win32: u32,
pub ran: bool,
pub cleaned: bool,
pub output: Option<String>,
}
pub async fn exec(client: &mut SmbClient, host: &str, command: &str) -> Result<ExecResult> {
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let name = format!("ADh{tag:08x}");
let out_rel = format!("Windows\\Temp\\ADh{tag:08x}.out"); let out_win = format!("C:\\{out_rel}"); let full = format!("{command} > {out_win} 2>&1");
let binpath = format!("%COMSPEC% /Q /c start \"\" /b %COMSPEC% /Q /c \"{full}\"");
let start_ret = create_start_delete(client, &name, &binpath).await?;
let cleaned = true;
let ran = matches!(
start_ret,
0 | ERROR_SERVICE_REQUEST_TIMEOUT
| ERROR_SERVICE_NO_THREAD
| ERROR_EXCEPTION_IN_SERVICE
| ERROR_PROCESS_ABORTED
);
let output = if ran {
match client.tree_connect(&format!("\\\\{host}\\C$")).await {
Ok(()) => match client.read_file_delete(&out_rel).await {
Ok(bytes) if !bytes.is_empty() => Some(
String::from_utf8_lossy(&bytes)
.replace('\r', "")
.trim_end()
.to_string(),
),
Ok(_) => Some(String::new()),
Err(e) => {
tracing::warn!("output read failed: {e}");
None
}
},
Err(e) => {
tracing::warn!("C$ tree-connect failed: {e}");
None
}
}
} else {
None
};
Ok(ExecResult {
service: name,
start_win32: start_ret,
ran,
cleaned,
output,
})
}
pub async fn run(client: &mut SmbClient, command: &str) -> Result<u32> {
let tag = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
let name = format!("ADh{tag:08x}");
let binpath = format!("%COMSPEC% /Q /c start \"\" /b %COMSPEC% /Q /c \"{command}\"");
create_start_delete(client, &name, &binpath).await
}
async fn create_start_delete(client: &mut SmbClient, name: &str, binpath: &str) -> Result<u32> {
let file_id = client
.open_pipe("svcctl")
.await
.map_err(|e| RpcError::Protocol(format!("open \\svcctl: {e}")))?;
let mut scm = SvcctlClient::bind(client, file_id).await?;
let scm_handle = scm.open_scm().await?;
let svc = scm.create_service(&scm_handle, name, binpath).await?;
let start_ret = scm.start_service(&svc).await?;
let del_ret = scm.delete_service(&svc).await?;
scm.close_handle(&svc).await;
scm.close_handle(&scm_handle).await;
if del_ret != 0 {
tracing::warn!("RDeleteService returned win32 {del_ret}");
}
Ok(start_ret)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn create_service_stub_shape() {
let scm = ScHandle([0x11; 20]);
let stub = encode_create_service(&scm, "X", "cmd /c whoami");
assert_eq!(&stub[..20], &[0x11u8; 20]);
let d = |o: usize| u32::from_le_bytes(stub[o..o + 4].try_into().unwrap());
assert_eq!(d(20), 2, "lpServiceName WSTR max_count = 2 (X\\0)");
assert_eq!(d(24), 0, "offset = 0");
assert_eq!(d(28), 2, "actual_count = 2");
assert_eq!(d(36), 0, "lpDisplayName must be NULL");
assert_eq!(d(40), SERVICE_ALL_ACCESS);
assert_eq!(d(44), SERVICE_WIN32_OWN_PROCESS);
assert_eq!(d(48), SERVICE_DEMAND_START);
assert_eq!(d(52), SERVICE_ERROR_IGNORE);
assert_eq!(
d(56),
13 + 1,
"lpBinaryPathName WSTR max_count = 14 (cmd /c whoami\\0)"
);
}
#[test]
fn open_scm_stub_is_two_nulls_plus_access() {
let stub = encode_open_scm(SC_MANAGER_ALL_ACCESS);
assert_eq!(stub, vec![0, 0, 0, 0, 0, 0, 0, 0, 0x3f, 0x00, 0x0f, 0x00]);
}
}