use crate::dcom::{
orpc_this, orpc_this_flags, IID_ISYSTEM_ACTIVATOR, IID_IWBEM_LEVEL1_LOGIN, IID_IWBEM_SERVICES,
};
use crate::ndr::{NdrDecoder, NdrEncoder};
use crate::transport::RpcTcp;
use crate::{Result, RpcError, Syntax};
use windows_sddl::sid::Guid;
const CLSID_ACTIVATION_PROPERTIES_IN: &str = "00000338-0000-0000-c000-000000000046";
const IID_IACTIVATION_PROPERTIES_IN: &str = "000001a2-0000-0000-c000-000000000046";
const CLSID_INSTANTIATION_INFO: &str = "000001ab-0000-0000-c000-000000000046";
const CLSID_ACTIVATION_CONTEXT_INFO: &str = "000001a5-0000-0000-c000-000000000046";
const CLSID_SERVER_LOCATION_INFO: &str = "000001a4-0000-0000-c000-000000000046";
const CLSID_SCM_REQUEST_INFO: &str = "000001aa-0000-0000-c000-000000000046";
const NCACN_IP_TCP: u16 = 0x07;
const MSHCTX_DIFFERENTMACHINE: u32 = 2;
fn guid_bytes(s: &str) -> [u8; 16] {
Guid::parse(s).expect("valid guid").0
}
fn pickle_header(body_len: usize) -> [u8; 16] {
let mut h = [0u8; 16];
h[0] = 0x01; h[1] = 0x10; h[2] = 0x08; h[3] = 0x00;
h[4..8].copy_from_slice(&0xcccc_ccccu32.to_le_bytes()); h[8..12].copy_from_slice(&(body_len as u32).to_le_bytes()); h[12..16].copy_from_slice(&0xcccc_ccccu32.to_le_bytes()); h
}
fn pickle(body: &[u8]) -> Vec<u8> {
let mut v = Vec::with_capacity(16 + body.len());
v.extend_from_slice(&pickle_header(body.len()));
v.extend_from_slice(body);
v
}
fn instantiation_info(clsid: &str, iids: &[&str]) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.uuid(&guid_bytes(clsid)); e.u32(0); e.u32(0); e.u32(0); e.u32(iids.len() as u32); e.u32(0); e.referent(); e.u32(0); e.u16(5); e.u16(7); e.u32(iids.len() as u32); for iid in iids {
e.uuid(&guid_bytes(iid));
}
pickle(&e.into_bytes())
}
fn activation_context_info() -> Vec<u8> {
let mut e = NdrEncoder::new();
e.u32(0); e.u32(0); e.u32(0); e.u32(0); e.null_ptr(); e.null_ptr(); pickle(&e.into_bytes())
}
fn location_info() -> Vec<u8> {
let mut e = NdrEncoder::new();
e.null_ptr(); e.u32(0); e.u32(0); e.u32(0); pickle(&e.into_bytes())
}
fn scm_request_info() -> Vec<u8> {
let mut e = NdrEncoder::new();
e.null_ptr(); e.referent(); e.u32(0); e.u16(1); e.u16(0); e.referent(); e.u32(1); e.u16(NCACN_IP_TCP);
pickle(&e.into_bytes())
}
fn activation_properties_in(clsid: &str, iids: &[&str]) -> Vec<u8> {
let props: [(&str, Vec<u8>); 4] = [
(CLSID_INSTANTIATION_INFO, instantiation_info(clsid, iids)),
(CLSID_ACTIVATION_CONTEXT_INFO, activation_context_info()),
(CLSID_SERVER_LOCATION_INFO, location_info()),
(CLSID_SCM_REQUEST_INFO, scm_request_info()),
];
let n = props.len();
let padded: Vec<Vec<u8>> = props
.iter()
.map(|(_, blob)| {
let mut b = blob.clone();
let pad = (8 - (b.len() % 8)) % 8;
b.extend(std::iter::repeat(0xFA).take(pad));
b
})
.collect();
let mut ch = NdrEncoder::new();
ch.u32(0); ch.u32(0); ch.u32(0); ch.u32(MSHCTX_DIFFERENTMACHINE); ch.u32(n as u32); ch.uuid(&[0u8; 16]); ch.referent(); ch.referent(); ch.null_ptr(); ch.u32(n as u32); for (c, _) in &props {
ch.uuid(&guid_bytes(c));
}
ch.u32(n as u32); for b in &padded {
ch.u32(b.len() as u32); }
let mut custom_header = pickle(&ch.into_bytes());
let mut props_bytes = Vec::new();
for b in &padded {
props_bytes.extend_from_slice(b);
}
let header_size = custom_header.len() as u32;
let total_size = header_size + props_bytes.len() as u32;
custom_header[16..20].copy_from_slice(&total_size.to_le_bytes());
custom_header[20..24].copy_from_slice(&header_size.to_le_bytes());
let mut blob = Vec::new();
let dw_size = custom_header.len() + props_bytes.len();
blob.extend_from_slice(&(dw_size as u32).to_le_bytes()); blob.extend_from_slice(&0u32.to_le_bytes()); blob.extend_from_slice(&custom_header);
blob.extend_from_slice(&props_bytes);
objref_custom(
CLSID_ACTIVATION_PROPERTIES_IN,
IID_IACTIVATION_PROPERTIES_IN,
&blob,
)
}
fn objref_custom(clsid: &str, iid: &str, object_data: &[u8]) -> Vec<u8> {
let mut o = Vec::new();
o.extend_from_slice(b"MEOW"); o.extend_from_slice(&4u32.to_le_bytes()); o.extend_from_slice(&guid_bytes(iid)); o.extend_from_slice(&guid_bytes(clsid)); o.extend_from_slice(&0u32.to_le_bytes()); o.extend_from_slice(&((object_data.len() + 8) as u32).to_le_bytes()); o.extend_from_slice(object_data); o
}
fn marshal_minterface_ptr(e: &mut NdrEncoder, abdata: &[u8]) {
e.referent(); e.u32(abdata.len() as u32); e.u32(abdata.len() as u32); e.bytes(abdata);
e.align(4);
}
pub fn remote_create_instance_stub(cid: &[u8; 16], clsid: &str, iids: &[&str]) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.bytes(&orpc_this(cid)); e.null_ptr(); marshal_minterface_ptr(&mut e, &activation_properties_in(clsid, iids)); e.into_bytes()
}
#[derive(Debug, Clone, Default)]
pub struct StdObjRef {
pub oxid: u64,
pub oid: u64,
pub ipid: [u8; 16],
}
pub fn parse_stdobjref(reply: &[u8]) -> Result<StdObjRef> {
let mut i = 0;
while i + 8 <= reply.len() {
if &reply[i..i + 4] == b"MEOW" {
let flags = u32::from_le_bytes(reply[i + 4..i + 8].try_into().unwrap());
if flags == 1 {
let s = i + 8 + 16;
let mut d = NdrDecoder::new(&reply[s..]);
let _std_flags = d.u32()?;
let _public_refs = d.u32()?;
let oxid = d.u64()?;
let oid = d.u64()?;
let ipid = d.uuid()?;
return Ok(StdObjRef { oxid, oid, ipid });
}
}
i += 1;
}
Err(RpcError::Protocol(
"no OBJREF_STANDARD in RemoteCreateInstance reply".into(),
))
}
pub fn activation_hresult(reply: &[u8]) -> i32 {
reply
.get(reply.len().wrapping_sub(4)..)
.and_then(|b| b.try_into().ok())
.map(i32::from_le_bytes)
.unwrap_or(-1)
}
async fn bind_wmi(
rpc: &mut RpcTcp,
syntax: Syntax,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
workstation: &str,
) -> Result<()> {
match nt_hash {
Some(h) => {
rpc.bind_sealed_hash(syntax, domain, user, h, workstation)
.await
}
None => {
rpc.bind_sealed(syntax, domain, user, password, workstation)
.await
}
}
}
pub async fn remote_create_instance(
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
workstation: &str,
clsid: &str,
iids: &[&str],
) -> Result<(StdObjRef, i32)> {
let reply = remote_create_instance_raw(
host,
domain,
user,
password,
nt_hash,
workstation,
clsid,
iids,
)
.await?;
let hr = activation_hresult(&reply);
let obj = parse_stdobjref(&reply)?;
Ok((obj, hr))
}
pub async fn remote_create_instance_raw(
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
workstation: &str,
clsid: &str,
iids: &[&str],
) -> Result<Vec<u8>> {
let addr = if host.contains(':') {
host.to_string()
} else {
format!("{host}:135")
};
let mut rpc = RpcTcp::connect(&addr).await?;
bind_wmi(
&mut rpc,
Syntax::new(IID_ISYSTEM_ACTIVATOR, 0, 0),
domain,
user,
password,
nt_hash,
workstation,
)
.await?;
let cid = [0x5Au8; 16]; rpc.call_sealed(4, &remote_create_instance_stub(&cid, clsid, iids))
.await
}
const CLSID_WBEM_LEVEL1_LOGIN: &str = "8bc3f05e-d86b-11d0-a075-00c04fb68820";
fn parse_oxid_binding_port(reply: &[u8]) -> Result<u16> {
let mut cur = String::new();
for c in reply.chunks_exact(2) {
let w = u16::from_le_bytes([c[0], c[1]]);
if w == 0 {
if let Some((_host, rest)) = cur.rsplit_once('[') {
if let Some(p) = rest.strip_suffix(']').and_then(|s| s.parse::<u16>().ok()) {
return Ok(p);
}
}
cur.clear();
} else if let Some(ch) = char::from_u32(w as u32) {
cur.push(ch);
}
}
Err(RpcError::Protocol(
"no host[port] OXID binding in reply".into(),
))
}
fn ntlm_login_stub(cid: &[u8; 16], namespace: &str) -> Vec<u8> {
let mut e = NdrEncoder::new();
e.bytes(&orpc_this_flags(cid, 0));
e.referent(); e.conformant_varying_wstr(namespace);
e.align(4);
e.null_ptr(); e.u32(0); e.null_ptr(); e.into_bytes()
}
pub struct WmiSession {
pub host: String,
pub port: u16,
pub services_ipid: [u8; 16],
pub oxid: u64,
}
pub async fn wmi_connect(
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
workstation: &str,
) -> Result<WmiSession> {
let reply = remote_create_instance_raw(
host,
domain,
user,
password,
nt_hash,
workstation,
CLSID_WBEM_LEVEL1_LOGIN,
&[IID_IWBEM_LEVEL1_LOGIN],
)
.await?;
let hr = activation_hresult(&reply);
if hr != 0 {
return Err(RpcError::Protocol(format!(
"WMI activation refused (HRESULT {hr:#010x})"
)));
}
let login = parse_stdobjref(&reply)?; let port = parse_oxid_binding_port(&reply)?;
let host_ip = host.split(':').next().unwrap_or(host).to_string();
let addr = format!("{host_ip}:{port}");
let mut rpc = RpcTcp::connect(&addr).await?;
bind_wmi(
&mut rpc,
Syntax::new(IID_IWBEM_LEVEL1_LOGIN, 0, 0),
domain,
user,
password,
nt_hash,
workstation,
)
.await?;
let cid = [0x5Au8; 16];
let stub = ntlm_login_stub(&cid, "//./root/cimv2");
let resp = rpc.call_sealed_object(6, &login.ipid, &stub).await?;
let hr = activation_hresult(&resp);
if hr != 0 {
return Err(RpcError::Protocol(format!(
"NTLMLogin failed (HRESULT {hr:#010x})"
)));
}
let svc = parse_stdobjref(&resp)?; Ok(WmiSession {
host: host_ip,
port,
services_ipid: svc.ipid,
oxid: svc.oxid,
})
}
const EXEC_TEMPLATE: &[u8] = include_bytes!("wmi_exec_template.bin");
const EXEC_CMD_OFF: usize = 1850; const EXEC_CMD_LEN: usize = 82; const EXEC_LEN_FIELDS: [usize; 6] = [120, 124, 172, 180, 1804, 1818];
const EXEC_HEAP_LEN_OFF: usize = 1831;
fn patch_len(buf: &mut [u8], off: usize, delta: i64) {
let v = u32::from_le_bytes(buf[off..off + 4].try_into().unwrap());
let nv = (v as i64 + delta) as u32;
buf[off..off + 4].copy_from_slice(&nv.to_le_bytes());
}
pub fn exec_method_stub_dump(command: &str) -> Vec<u8> {
exec_method_stub(&[0x5Au8; 16], command)
}
fn exec_method_stub(cid: &[u8; 16], command: &str) -> Vec<u8> {
let mut t = EXEC_TEMPLATE.to_vec();
t[..32].copy_from_slice(&orpc_this_flags(cid, 0)); let new_cmd: Vec<u8> = command
.encode_utf16()
.flat_map(|u| u.to_le_bytes())
.collect();
let delta = new_cmd.len() as i64 - EXEC_CMD_LEN as i64;
for &off in &EXEC_LEN_FIELDS {
patch_len(&mut t, off, delta);
}
let v = u32::from_le_bytes(
t[EXEC_HEAP_LEN_OFF..EXEC_HEAP_LEN_OFF + 4]
.try_into()
.unwrap(),
);
let nv = (v & 0x8000_0000) | (((v & 0x7fff_ffff) as i64 + delta) as u32 & 0x7fff_ffff);
t[EXEC_HEAP_LEN_OFF..EXEC_HEAP_LEN_OFF + 4].copy_from_slice(&nv.to_le_bytes());
let mut out = Vec::with_capacity(t.len() + delta.max(0) as usize);
out.extend_from_slice(&t[..EXEC_CMD_OFF]);
out.extend_from_slice(&new_cmd);
out.extend_from_slice(&t[EXEC_CMD_OFF + EXEC_CMD_LEN..]);
out
}
pub async fn wmi_exec(
host: &str,
domain: &str,
user: &str,
password: &str,
nt_hash: Option<&[u8; 16]>,
workstation: &str,
command: &str,
) -> Result<i32> {
let s = wmi_connect(host, domain, user, password, nt_hash, workstation).await?;
let addr = format!("{}:{}", s.host, s.port);
let mut rpc = RpcTcp::connect(&addr).await?;
bind_wmi(
&mut rpc,
Syntax::new(IID_IWBEM_SERVICES, 0, 0),
domain,
user,
password,
nt_hash,
workstation,
)
.await?;
let cid = [0x5Au8; 16];
let resp = rpc
.call_sealed_object(24, &s.services_ipid, &exec_method_stub(&cid, command))
.await?;
Ok(activation_hresult(&resp))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wmi_activation_stub_matches_reference_blob() {
let cid = [0x5Au8; 16];
let stub = remote_create_instance_stub(
&cid,
"8bc3f05e-d86b-11d0-a075-00c04fb68820", &["f309ad18-d86a-11d0-a075-00c04fb68820"], );
assert_eq!(stub.len(), 464, "activation stub size");
assert_eq!(&stub[4..8], &[1, 0, 0, 0], "ORPCTHIS flags must be 1");
let meow = stub.windows(4).position(|w| w == b"MEOW").expect("MEOW");
assert_eq!(meow, 48);
let count_ab = stub
.windows(4)
.filter(|w| *w == [0xab, 0x01, 0x00, 0x00])
.count();
assert_eq!(count_ab, 1, "InstantiationInfo CLSID present once");
}
#[test]
fn pickle_header_shape() {
let h = pickle_header(0x40);
assert_eq!(&h[0..4], &[0x01, 0x10, 0x08, 0x00]);
assert_eq!(&h[4..8], &0xcccc_ccccu32.to_le_bytes());
assert_eq!(&h[8..12], &0x40u32.to_le_bytes());
}
#[test]
fn objref_custom_signature_and_len() {
let o = objref_custom(
CLSID_ACTIVATION_PROPERTIES_IN,
IID_IACTIVATION_PROPERTIES_IN,
&[0xAA; 8],
);
assert_eq!(&o[0..4], b"MEOW");
assert_eq!(&o[4..8], &4u32.to_le_bytes()); let n = o.len();
assert_eq!(&o[n - 16..n - 12], &0u32.to_le_bytes()); assert_eq!(&o[n - 12..n - 8], &16u32.to_le_bytes()); assert_eq!(&o[n - 8..], &[0xAA; 8]); }
#[test]
fn minterface_ptr_has_conformant_prefix() {
let mut e = NdrEncoder::new();
marshal_minterface_ptr(&mut e, &[0xBB; 12]);
let b = e.into_bytes();
assert_ne!(u32::from_le_bytes(b[0..4].try_into().unwrap()), 0); assert_eq!(u32::from_le_bytes(b[4..8].try_into().unwrap()), 12); assert_eq!(u32::from_le_bytes(b[8..12].try_into().unwrap()), 12); }
#[test]
fn instantiation_info_has_class_and_iid() {
let b = instantiation_info(
"8bc3f05e-d86b-11d0-a075-00c04fb68820",
&["f309ad18-d86a-11d0-a075-00c04fb68820"],
);
assert_eq!(
&b[16..20],
&guid_bytes("8bc3f05e-d86b-11d0-a075-00c04fb68820")[0..4]
);
assert_eq!(&b[44..48], &1u32.to_le_bytes());
}
#[tokio::test]
#[ignore = "live DC"]
async fn remote_create_instance_live() {
let (Ok(dc), Ok(dom), Ok(user), Ok(pass)) = (
std::env::var("ADH_DC"),
std::env::var("ADH_DOMAIN"),
std::env::var("ADH_USER"),
std::env::var("ADH_PASS"),
) else {
return;
};
use crate::dcom::{CLSID_WBEM_LEVEL1_LOGIN, IID_IWBEM_LEVEL1_LOGIN};
let reply = remote_create_instance_raw(
&dc,
&dom,
&user,
&pass,
None,
"ADHAMMER",
CLSID_WBEM_LEVEL1_LOGIN,
&[IID_IWBEM_LEVEL1_LOGIN],
)
.await
.expect("activation call");
println!("reply {} bytes:", reply.len());
for (i, ch) in reply.chunks(16).enumerate() {
let hex: Vec<String> = ch.iter().map(|b| format!("{b:02x}")).collect();
println!(" {:04x}: {}", i * 16, hex.join(" "));
}
let mut i = 0;
while i + 8 <= reply.len() {
if &reply[i..i + 4] == b"MEOW" {
let f = u32::from_le_bytes(reply[i + 4..i + 8].try_into().unwrap());
println!(" MEOW @0x{i:04x} flags={f}");
}
i += 1;
}
}
#[test]
fn stdobjref_parse_roundtrip() {
let mut r = Vec::new();
r.extend_from_slice(b"MEOW");
r.extend_from_slice(&1u32.to_le_bytes()); r.extend_from_slice(&[0x11; 16]); r.extend_from_slice(&0u32.to_le_bytes()); r.extend_from_slice(&5u32.to_le_bytes()); r.extend_from_slice(&0x1122_3344_5566_7788u64.to_le_bytes()); r.extend_from_slice(&0x99AA_BBCC_DDEE_FF00u64.to_le_bytes()); r.extend_from_slice(&[0x22; 16]); let s = parse_stdobjref(&r).unwrap();
assert_eq!(s.oxid, 0x1122_3344_5566_7788);
assert_eq!(s.oid, 0x99AA_BBCC_DDEE_FF00);
assert_eq!(s.ipid, [0x22; 16]);
}
}