Skip to main content

windows_sddl/
lib.rs

1//! # windows-sddl
2//!
3//! A pure-Rust, **no-FFI** parser and builder for the Windows *self-relative*
4//! `SECURITY_DESCRIPTOR` blob (MS-DTYP §2.4.6) — the binary form stored in
5//! `nTSecurityDescriptor`, returned over LDAP, and found in registry hives and backup
6//! formats. It works cross-platform against raw bytes: no `windows` crate, no OS calls.
7//!
8//! It also ships the [`Sid`]/[`Guid`] types and a table of Active-Directory extended-right
9//! GUIDs ([`rights`]) so a generic-looking ACE mask can be resolved into a concrete right
10//! (DCSync, Shadow Credentials, RBCD, cert enrollment, …).
11//!
12//! ## Example
13//!
14//! ```
15//! use windows_sddl::{parse, AccessMask};
16//!
17//! // A self-relative SD with one ACCESS_ALLOWED ACE granting full control to a trustee:
18//! let sd_bytes = windows_sddl::build_rbcd_sd(&windows_sddl::Sid::parse("S-1-5-21-1-2-3-1104").unwrap());
19//! let sd = parse(&sd_bytes).unwrap();
20//! let ace = &sd.dacl.unwrap().aces[0];
21//! assert!(ace.is_allow());
22//! assert!(ace.mask.contains(AccessMask::WRITE_DAC));
23//! ```
24//!
25//! ## Uses
26//!
27//! - DFIR / forensics: read ACLs out of offline hives or LDAP dumps without a Windows host.
28//! - ACL auditing: enumerate who has `WriteDacl`/`WriteOwner`/`GenericAll` on an object.
29//! - Backup / migration tooling: inspect or rebuild security descriptors portably.
30
31use bitflags::bitflags;
32
33pub mod rights;
34pub mod sid;
35
36pub use sid::{Guid, Sid};
37
38/// Serialize a SID to its binary (`objectSid`) form. (Convenience alias for [`Sid::to_bytes`].)
39pub fn sid_to_bytes(sid: &Sid) -> Vec<u8> {
40    sid.to_bytes()
41}
42
43/// Build a `msDS-AllowedToActOnBehalfOfOtherIdentity`-style security descriptor granting
44/// `trustee` full control (the RBCD primitive): a self-relative SD with one allow ACE, owner
45/// `BUILTIN\Administrators`. Handy for tests and for tooling that needs to *write* an SD.
46pub fn build_rbcd_sd(trustee: &Sid) -> Vec<u8> {
47    let owner = Sid {
48        revision: 1,
49        identifier_authority: 5,
50        sub_authorities: vec![32, 544],
51    };
52    let ownerb = owner.to_bytes();
53    let trusteeb = trustee.to_bytes();
54
55    // ACCESS_ALLOWED_ACE: type 0, flags 0, size, mask (0x000F01FF = full control), sid.
56    let ace_size = (4 + 4 + trusteeb.len()) as u16;
57    let mut ace = vec![0x00u8, 0x00];
58    ace.extend_from_slice(&ace_size.to_le_bytes());
59    ace.extend_from_slice(&0x000F_01FFu32.to_le_bytes());
60    ace.extend_from_slice(&trusteeb);
61
62    // ACL: revision 2, size, ace_count 1.
63    let dacl_size = (8 + ace.len()) as u16;
64    let mut dacl = vec![0x02u8, 0x00];
65    dacl.extend_from_slice(&dacl_size.to_le_bytes());
66    dacl.extend_from_slice(&1u16.to_le_bytes());
67    dacl.extend_from_slice(&0u16.to_le_bytes());
68    dacl.extend_from_slice(&ace);
69
70    // Self-relative SD: owner = group = BA, DACL present.
71    let owner_off = 20u32;
72    let group_off = 20 + ownerb.len() as u32;
73    let dacl_off = group_off + ownerb.len() as u32;
74    let mut sd = vec![1u8, 0];
75    sd.extend_from_slice(&0x8004u16.to_le_bytes()); // SE_SELF_RELATIVE | SE_DACL_PRESENT
76    sd.extend_from_slice(&owner_off.to_le_bytes());
77    sd.extend_from_slice(&group_off.to_le_bytes());
78    sd.extend_from_slice(&0u32.to_le_bytes()); // SACL offset
79    sd.extend_from_slice(&dacl_off.to_le_bytes());
80    sd.extend_from_slice(&ownerb); // owner
81    sd.extend_from_slice(&ownerb); // group
82    sd.extend_from_slice(&dacl);
83    sd
84}
85
86#[derive(Debug, thiserror::Error)]
87pub enum SddlError {
88    #[error("buffer too short at {0}")]
89    Truncated(&'static str),
90    #[error("bad ACE sid")]
91    BadSid,
92}
93
94type Result<T> = std::result::Result<T, SddlError>;
95
96bitflags! {
97    /// ACCESS_MASK bits (MS-DTYP §2.4.3 + AD-specific extended rights).
98    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
99    pub struct AccessMask: u32 {
100        const CREATE_CHILD    = 0x0000_0001;
101        const DELETE_CHILD    = 0x0000_0002;
102        const LIST_CHILDREN   = 0x0000_0004;
103        const SELF            = 0x0000_0008; // validated write
104        const READ_PROP       = 0x0000_0010; // read property (scoped by object GUID)
105        const WRITE_PROP      = 0x0000_0020; // write property (scoped by object GUID)
106        const DELETE_TREE     = 0x0000_0040;
107        const LIST_OBJECT     = 0x0000_0080;
108        const CONTROL_ACCESS  = 0x0000_0100; // extended right (scoped by object GUID)
109        const DELETE          = 0x0001_0000;
110        const READ_CONTROL    = 0x0002_0000;
111        const WRITE_DAC       = 0x0004_0000;
112        const WRITE_OWNER     = 0x0008_0000;
113        const SYNCHRONIZE     = 0x0010_0000;
114        const ACCESS_SYSTEM_SECURITY = 0x0100_0000;
115        const GENERIC_ALL     = 0x1000_0000;
116        const GENERIC_EXECUTE = 0x2000_0000;
117        const GENERIC_WRITE   = 0x4000_0000;
118        const GENERIC_READ    = 0x8000_0000;
119    }
120}
121
122/// ACE header type byte (MS-DTYP §2.4.4). Allow/deny + their object variants; everything else
123/// is preserved as [`AceType::Other`].
124#[derive(Clone, Copy, Debug, PartialEq, Eq)]
125pub enum AceType {
126    AccessAllowed,
127    AccessDenied,
128    AccessAllowedObject,
129    AccessDeniedObject,
130    Other(u8),
131}
132
133#[derive(Clone, Debug)]
134pub struct Ace {
135    pub ace_type: AceType,
136    pub flags: u8,
137    pub mask: AccessMask,
138    pub trustee: Sid,
139    /// Present for *object* ACEs: which property-set / extended-right / child-class this grants.
140    pub object_type: Option<Guid>,
141    pub inherited_object_type: Option<Guid>,
142}
143
144impl Ace {
145    pub fn is_allow(&self) -> bool {
146        matches!(
147            self.ace_type,
148            AceType::AccessAllowed | AceType::AccessAllowedObject
149        )
150    }
151}
152
153#[derive(Clone, Debug, Default)]
154pub struct Acl {
155    pub aces: Vec<Ace>,
156}
157
158#[derive(Clone, Debug, Default)]
159pub struct SecurityDescriptor {
160    pub owner: Option<Sid>,
161    pub group: Option<Sid>,
162    pub dacl: Option<Acl>,
163}
164
165fn u16le(b: &[u8], o: usize) -> Result<u16> {
166    Ok(u16::from_le_bytes(
167        b.get(o..o + 2)
168            .ok_or(SddlError::Truncated("u16"))?
169            .try_into()
170            .unwrap(),
171    ))
172}
173fn u32le(b: &[u8], o: usize) -> Result<u32> {
174    Ok(u32::from_le_bytes(
175        b.get(o..o + 4)
176            .ok_or(SddlError::Truncated("u32"))?
177            .try_into()
178            .unwrap(),
179    ))
180}
181fn sid_at(b: &[u8], o: usize) -> Result<Sid> {
182    let count = *b.get(o + 1).ok_or(SddlError::Truncated("sid"))? as usize;
183    let end = o + 8 + count * 4;
184    Sid::from_bytes(b.get(o..end).ok_or(SddlError::Truncated("sid"))?).ok_or(SddlError::BadSid)
185}
186
187/// Parse a self-relative `SECURITY_DESCRIPTOR`. Offsets are from the start of `b`. Never panics
188/// on malformed / hostile input — returns [`SddlError`] instead.
189pub fn parse(b: &[u8]) -> Result<SecurityDescriptor> {
190    if b.len() < 20 {
191        return Err(SddlError::Truncated("sd header"));
192    }
193    let owner_off = u32le(b, 4)? as usize;
194    let group_off = u32le(b, 8)? as usize;
195    let dacl_off = u32le(b, 16)? as usize;
196
197    let owner = (owner_off != 0).then(|| sid_at(b, owner_off)).transpose()?;
198    let group = (group_off != 0).then(|| sid_at(b, group_off)).transpose()?;
199    let dacl = (dacl_off != 0)
200        .then(|| parse_acl(b, dacl_off))
201        .transpose()?;
202
203    Ok(SecurityDescriptor { owner, group, dacl })
204}
205
206fn parse_acl(b: &[u8], off: usize) -> Result<Acl> {
207    // ACL header: Revision(1) Sbz1(1) AclSize(2) AceCount(2) Sbz2(2)
208    let ace_count = u16le(b, off + 4)? as usize;
209    let mut cur = off + 8;
210    let mut aces = Vec::with_capacity(ace_count);
211    for _ in 0..ace_count {
212        let ace_type_byte = *b.get(cur).ok_or(SddlError::Truncated("ace type"))?;
213        let flags = *b.get(cur + 1).ok_or(SddlError::Truncated("ace flags"))?;
214        let size = u16le(b, cur + 2)? as usize;
215        let ace_type = match ace_type_byte {
216            0x00 => AceType::AccessAllowed,
217            0x01 => AceType::AccessDenied,
218            0x05 => AceType::AccessAllowedObject,
219            0x06 => AceType::AccessDeniedObject,
220            x => AceType::Other(x),
221        };
222        let mask = AccessMask::from_bits_truncate(u32le(b, cur + 4)?);
223
224        let (object_type, inherited_object_type, sid_off) = match ace_type {
225            AceType::AccessAllowedObject | AceType::AccessDeniedObject => {
226                // Mask(4) Flags(4) [ObjectType 16] [InheritedObjectType 16] Sid
227                let obj_flags = u32le(b, cur + 8)?;
228                let mut p = cur + 12;
229                let mut ot = None;
230                let mut iot = None;
231                if obj_flags & 0x1 != 0 {
232                    ot = b.get(p..p + 16).and_then(Guid::from_bytes);
233                    p += 16;
234                }
235                if obj_flags & 0x2 != 0 {
236                    iot = b.get(p..p + 16).and_then(Guid::from_bytes);
237                    p += 16;
238                }
239                (ot, iot, p)
240            }
241            _ => (None, None, cur + 8),
242        };
243
244        let trustee = sid_at(b, sid_off)?;
245        aces.push(Ace {
246            ace_type,
247            flags,
248            mask,
249            trustee,
250            object_type,
251            inherited_object_type,
252        });
253        if size == 0 {
254            break;
255        }
256        cur += size;
257    }
258    Ok(Acl { aces })
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn rbcd_sd_roundtrips_through_parser() {
267        let sid = Sid::parse("S-1-5-21-1-2-3-1104").unwrap();
268        let sd = build_rbcd_sd(&sid);
269        let parsed = parse(&sd).expect("parse our own SD");
270        let aces = &parsed.dacl.expect("dacl").aces;
271        assert_eq!(aces.len(), 1);
272        assert!(aces[0].is_allow());
273        assert_eq!(aces[0].trustee, sid);
274        assert!(aces[0].mask.contains(AccessMask::WRITE_DAC));
275    }
276
277    /// A truncated object-ACE (ObjectType flag set, no GUID bytes) must not panic.
278    #[test]
279    fn truncated_object_ace_does_not_panic() {
280        let mut sd = vec![1, 0, 0, 0];
281        sd.extend_from_slice(&0u32.to_le_bytes()); // owner off
282        sd.extend_from_slice(&0u32.to_le_bytes()); // group off
283        sd.extend_from_slice(&0u32.to_le_bytes()); // sacl off
284        sd.extend_from_slice(&20u32.to_le_bytes()); // dacl off
285        sd.extend_from_slice(&[2, 0, 0x30, 0, 1, 0, 0, 0]); // ACL hdr: 1 ACE
286        sd.extend_from_slice(&[0x05, 0, 0x20, 0]); // AccessAllowedObject, size 0x20
287        sd.extend_from_slice(&0u32.to_le_bytes()); // mask
288        sd.extend_from_slice(&1u32.to_le_bytes()); // obj_flags = ObjectType present, no GUID follows
289        let _ = parse(&sd); // must not panic
290    }
291
292    /// Fuzz-lite: random + seed-mutated bytes must never panic (deterministic seed).
293    #[test]
294    fn fuzz_parse_never_panics() {
295        let mut seed = vec![1, 0, 0, 0];
296        seed.extend_from_slice(&0u32.to_le_bytes());
297        seed.extend_from_slice(&0u32.to_le_bytes());
298        seed.extend_from_slice(&0u32.to_le_bytes());
299        seed.extend_from_slice(&20u32.to_le_bytes());
300        seed.extend_from_slice(&[2, 0, 0x30, 0, 1, 0, 0, 0]);
301        seed.extend_from_slice(&[0x05, 0, 0x2c, 0]);
302        seed.extend_from_slice(&[0u8; 40]);
303
304        let mut s: u64 = 0xDEAD_BEEF_CAFE_F00D;
305        let mut rng = || {
306            s ^= s >> 12;
307            s ^= s << 25;
308            s ^= s >> 27;
309            s.wrapping_mul(0x2545_F491_4F6C_DD1D)
310        };
311        let prev = std::panic::take_hook();
312        std::panic::set_hook(Box::new(|_| {}));
313        let mut fail = None;
314        for _ in 0..200_000 {
315            let mut buf = if rng() & 1 == 0 {
316                seed.clone()
317            } else {
318                let n = (rng() as usize) % 80;
319                (0..n).map(|_| rng() as u8).collect::<Vec<u8>>()
320            };
321            for _ in 0..(rng() as usize % 6) {
322                if !buf.is_empty() {
323                    let i = (rng() as usize) % buf.len();
324                    buf[i] = rng() as u8;
325                }
326            }
327            let b = buf.clone();
328            if std::panic::catch_unwind(|| {
329                let _ = parse(&b);
330            })
331            .is_err()
332            {
333                fail = Some(buf);
334                break;
335            }
336        }
337        std::panic::set_hook(prev);
338        if let Some(buf) = fail {
339            panic!(
340                "parse panicked on {} bytes: {}",
341                buf.len(),
342                buf.iter().map(|x| format!("{x:02x}")).collect::<String>()
343            );
344        }
345    }
346}