Skip to content

Commit 6dfc1e9

Browse files
authored
Add macaddr uniqueness (#1862)
* Fix build time key grabbing * Add mac address uniqueness * stubbed * First run * Fix windows * fmt
1 parent c04ff00 commit 6dfc1e9

File tree

10 files changed

+520
-14
lines changed

10 files changed

+520
-14
lines changed

implants/lib/host_unique/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ edition = "2021"
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9-
uuid = { workspace = true, features = ["v4", "fast-rng"] }
9+
uuid = { workspace = true, features = ["v4", "v5", "fast-rng"] }
1010
log = { workspace = true }
11+
netstat = { workspace = true }
1112

1213
[target.'cfg(target_os = "windows")'.dependencies]
1314
winreg = { workspace = true }

implants/lib/host_unique/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ mod env;
44
pub use env::Env;
55
mod file;
66
pub use file::File;
7+
mod mac_addr;
8+
pub use mac_addr::MacAddr;
79
mod registry;
810
pub use registry::Registry;
911

@@ -46,5 +48,6 @@ pub fn defaults() -> Vec<Box<dyn HostIDSelector>> {
4648
target_os = "netbsd"
4749
))]
4850
Box::new(File::new_with_file("/etc/system-id")),
51+
Box::<MacAddr>::default(),
4952
]
5053
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use uuid::Uuid;
2+
3+
use crate::HostIDSelector;
4+
5+
#[derive(Default)]
6+
pub struct MacAddr {}
7+
8+
impl MacAddr {
9+
/// Returns the first non-zero MAC address from a sorted list of network interfaces.
10+
fn get_mac_bytes(&self) -> Option<[u8; 6]> {
11+
let mut interfaces = netstat::list_interfaces().ok()?;
12+
interfaces.sort_by(|a, b| a.iface_name.cmp(&b.iface_name));
13+
14+
for iface in interfaces {
15+
if iface.mac_address != [0u8; 6] {
16+
return Some(iface.mac_address);
17+
}
18+
}
19+
None
20+
}
21+
}
22+
23+
impl HostIDSelector for MacAddr {
24+
fn get_name(&self) -> String {
25+
"mac_address".to_string()
26+
}
27+
28+
fn get_host_id(&self) -> Option<Uuid> {
29+
let mac_bytes = self.get_mac_bytes()?;
30+
Some(Uuid::new_v5(&Uuid::NAMESPACE_OID, &mac_bytes))
31+
}
32+
}
33+
34+
#[cfg(test)]
35+
mod tests {
36+
use super::*;
37+
38+
#[test]
39+
fn test_mac_addr_deterministic() {
40+
let selector = MacAddr::default();
41+
let id_one = selector.get_host_id();
42+
let id_two = selector.get_host_id();
43+
44+
assert!(id_one.is_some(), "expected a MAC-based host id");
45+
assert_eq!(id_one, id_two, "MAC-based host id must be deterministic");
46+
}
47+
48+
#[test]
49+
fn test_mac_addr_is_v5_uuid() {
50+
let selector = MacAddr::default();
51+
if let Some(id) = selector.get_host_id() {
52+
let s = id.to_string();
53+
// UUID v5 has the version nibble '5' as the 13th hex character
54+
assert!(s.contains("-5"), "expected a v5 UUID, got: {}", s);
55+
}
56+
}
57+
}

implants/lib/netstat/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,20 @@ log = { workspace = true }
1010
[target.'cfg(target_os = "windows")'.dependencies]
1111
windows-sys = { workspace = true, features = [
1212
"Win32_Foundation",
13-
"Win32_Networking",
13+
"Win32_Networking_WinSock",
1414
"Win32_NetworkManagement_IpHelper",
15+
"Win32_NetworkManagement_Ndis",
1516
"Win32_System_ProcessStatus",
1617
"Win32_System_Threading",
1718
"Win32_System_Diagnostics_ToolHelp",
1819
]}
1920

21+
[target.'cfg(target_os = "linux")'.dependencies]
22+
libc = { workspace = true }
23+
24+
[target.'cfg(target_os = "freebsd")'.dependencies]
25+
libc = { workspace = true }
26+
2027
[target.'cfg(target_os = "macos")'.dependencies]
2128
libc = { workspace = true }
2229
byteorder = { workspace = true }

implants/lib/netstat/src/freebsd.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
use super::{ConnectionState, NetstatEntry, SocketType};
21
use anyhow::Result;
2+
use std::collections::HashMap;
3+
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
4+
5+
use super::{ConnectionState, InterfaceEntry, NetstatEntry, SocketType};
36

47
pub fn netstat() -> Result<Vec<NetstatEntry>> {
58
// TODO: Implement FreeBSD sysctl using net.inet.tcp.pcblist
@@ -8,6 +11,86 @@ pub fn netstat() -> Result<Vec<NetstatEntry>> {
811
Ok(Vec::new())
912
}
1013

14+
struct IfaceData {
15+
mac: [u8; 6],
16+
ipv4: Option<IpAddr>,
17+
ipv6: Option<IpAddr>,
18+
}
19+
20+
pub fn list_interfaces() -> Result<Vec<InterfaceEntry>> {
21+
let mut ifaddrs_ptr: *mut libc::ifaddrs = std::ptr::null_mut();
22+
23+
unsafe {
24+
if libc::getifaddrs(&mut ifaddrs_ptr) != 0 {
25+
return Err(anyhow::anyhow!(
26+
"getifaddrs failed: {}",
27+
std::io::Error::last_os_error()
28+
));
29+
}
30+
}
31+
32+
let mut ifaces: HashMap<String, IfaceData> = HashMap::new();
33+
34+
unsafe {
35+
let mut cur = ifaddrs_ptr;
36+
while !cur.is_null() {
37+
let ifa = &*cur;
38+
39+
if !ifa.ifa_addr.is_null() {
40+
let name = std::ffi::CStr::from_ptr(ifa.ifa_name)
41+
.to_string_lossy()
42+
.to_string();
43+
let family = (*ifa.ifa_addr).sa_family as i32;
44+
let entry = ifaces.entry(name).or_insert(IfaceData {
45+
mac: [0u8; 6],
46+
ipv4: None,
47+
ipv6: None,
48+
});
49+
50+
match family {
51+
libc::AF_LINK => {
52+
let sdl = &*(ifa.ifa_addr as *const libc::sockaddr_dl);
53+
if sdl.sdl_alen == 6 {
54+
let mac_offset = sdl.sdl_nlen as usize;
55+
let data_ptr = sdl.sdl_data.as_ptr().add(mac_offset) as *const u8;
56+
entry
57+
.mac
58+
.copy_from_slice(std::slice::from_raw_parts(data_ptr, 6));
59+
}
60+
}
61+
libc::AF_INET => {
62+
if entry.ipv4.is_none() {
63+
let sin = &*(ifa.ifa_addr as *const libc::sockaddr_in);
64+
let bytes = sin.sin_addr.s_addr.to_ne_bytes();
65+
entry.ipv4 = Some(IpAddr::V4(Ipv4Addr::from(bytes)));
66+
}
67+
}
68+
libc::AF_INET6 => {
69+
if entry.ipv6.is_none() {
70+
let sin6 = &*(ifa.ifa_addr as *const libc::sockaddr_in6);
71+
entry.ipv6 = Some(IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr)));
72+
}
73+
}
74+
_ => {}
75+
}
76+
}
77+
78+
cur = ifa.ifa_next;
79+
}
80+
81+
libc::freeifaddrs(ifaddrs_ptr);
82+
}
83+
84+
Ok(ifaces
85+
.into_iter()
86+
.map(|(name, data)| InterfaceEntry {
87+
iface_name: name,
88+
mac_address: data.mac,
89+
ip_address: data.ipv4.or(data.ipv6),
90+
})
91+
.collect())
92+
}
93+
1194
#[cfg(test)]
1295
mod tests {
1396
use super::*;
@@ -18,4 +101,13 @@ mod tests {
18101
assert!(result.is_ok());
19102
assert_eq!(result.unwrap().len(), 0);
20103
}
104+
105+
#[test]
106+
fn test_list_interfaces() {
107+
let result = list_interfaces();
108+
assert!(result.is_ok());
109+
110+
let interfaces = result.unwrap();
111+
assert!(!interfaces.is_empty(), "Should have at least one interface");
112+
}
21113
}

implants/lib/netstat/src/lib.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,32 @@ pub fn netstat() -> Result<Vec<NetstatEntry>> {
9494
)))]
9595
return Ok(Vec::new());
9696
}
97+
98+
#[derive(Debug, Clone, PartialEq)]
99+
pub struct InterfaceEntry {
100+
pub iface_name: String,
101+
pub mac_address: [u8; 6],
102+
pub ip_address: Option<IpAddr>,
103+
}
104+
105+
pub fn list_interfaces() -> Result<Vec<InterfaceEntry>> {
106+
#[cfg(target_os = "linux")]
107+
return linux::list_interfaces();
108+
109+
#[cfg(target_os = "macos")]
110+
return macos::list_interfaces();
111+
112+
#[cfg(target_os = "windows")]
113+
return windows::list_interfaces();
114+
115+
#[cfg(target_os = "freebsd")]
116+
return freebsd::list_interfaces();
117+
118+
#[cfg(not(any(
119+
target_os = "linux",
120+
target_os = "macos",
121+
target_os = "windows",
122+
target_os = "freebsd"
123+
)))]
124+
return Ok(Vec::new());
125+
}

implants/lib/netstat/src/linux.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::fs;
44
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
55
use std::path::Path;
66

7-
use super::{ConnectionState, NetstatEntry, SocketType};
7+
use super::{ConnectionState, InterfaceEntry, NetstatEntry, SocketType};
88

99
pub fn netstat() -> Result<Vec<NetstatEntry>> {
1010
let mut entries = Vec::new();
@@ -294,6 +294,82 @@ fn read_process_name(pid: u32) -> Result<String> {
294294
))
295295
}
296296

297+
struct IfaceData {
298+
mac: [u8; 6],
299+
ipv4: Option<IpAddr>,
300+
ipv6: Option<IpAddr>,
301+
}
302+
303+
pub fn list_interfaces() -> Result<Vec<InterfaceEntry>> {
304+
let mut ifaddrs_ptr: *mut libc::ifaddrs = std::ptr::null_mut();
305+
306+
unsafe {
307+
if libc::getifaddrs(&mut ifaddrs_ptr) != 0 {
308+
return Err(anyhow::anyhow!(
309+
"getifaddrs failed: {}",
310+
std::io::Error::last_os_error()
311+
));
312+
}
313+
}
314+
315+
let mut ifaces: HashMap<String, IfaceData> = HashMap::new();
316+
317+
unsafe {
318+
let mut cur = ifaddrs_ptr;
319+
while !cur.is_null() {
320+
let ifa = &*cur;
321+
322+
if !ifa.ifa_addr.is_null() {
323+
let name = std::ffi::CStr::from_ptr(ifa.ifa_name)
324+
.to_string_lossy()
325+
.to_string();
326+
let family = (*ifa.ifa_addr).sa_family as i32;
327+
let entry = ifaces.entry(name).or_insert(IfaceData {
328+
mac: [0u8; 6],
329+
ipv4: None,
330+
ipv6: None,
331+
});
332+
333+
match family {
334+
libc::AF_PACKET => {
335+
let sll = &*(ifa.ifa_addr as *const libc::sockaddr_ll);
336+
if sll.sll_halen == 6 {
337+
entry.mac.copy_from_slice(&sll.sll_addr[..6]);
338+
}
339+
}
340+
libc::AF_INET => {
341+
if entry.ipv4.is_none() {
342+
let sin = &*(ifa.ifa_addr as *const libc::sockaddr_in);
343+
let bytes = sin.sin_addr.s_addr.to_ne_bytes();
344+
entry.ipv4 = Some(IpAddr::V4(Ipv4Addr::from(bytes)));
345+
}
346+
}
347+
libc::AF_INET6 => {
348+
if entry.ipv6.is_none() {
349+
let sin6 = &*(ifa.ifa_addr as *const libc::sockaddr_in6);
350+
entry.ipv6 = Some(IpAddr::V6(Ipv6Addr::from(sin6.sin6_addr.s6_addr)));
351+
}
352+
}
353+
_ => {}
354+
}
355+
}
356+
357+
cur = ifa.ifa_next;
358+
}
359+
360+
libc::freeifaddrs(ifaddrs_ptr);
361+
}
362+
363+
Ok(ifaces
364+
.into_iter()
365+
.map(|(name, data)| InterfaceEntry {
366+
iface_name: name,
367+
mac_address: data.mac,
368+
ip_address: data.ipv4.or(data.ipv6),
369+
})
370+
.collect())
371+
}
372+
297373
#[cfg(test)]
298374
mod tests {
299375
use super::*;
@@ -389,4 +465,23 @@ mod tests {
389465
assert!(found, "Our test socket should appear in netstat results");
390466
Ok(())
391467
}
468+
469+
#[test]
470+
fn test_list_interfaces() {
471+
let result = list_interfaces();
472+
assert!(result.is_ok());
473+
474+
let interfaces = result.unwrap();
475+
assert!(!interfaces.is_empty(), "Should have at least one interface");
476+
477+
// Loopback interface should exist on Linux
478+
let has_lo = interfaces.iter().any(|i| i.iface_name == "lo");
479+
assert!(has_lo, "Loopback interface 'lo' should be present");
480+
}
481+
482+
#[test]
483+
fn print_interfaces() {
484+
let result = list_interfaces();
485+
println!("debug {:?}", result)
486+
}
392487
}

0 commit comments

Comments
 (0)