From acf33c1dfa66f605d9f07c425f35fd86874bd4d8 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Sun, 18 Feb 2024 17:23:57 +0000 Subject: [PATCH] Add more wrappers and stuff (CI won't work) --- .devcontainer/Dockerfile | 28 ++- Payload_Type/thanatos/agent/errors/src/lib.rs | 2 + .../thanatos/agent/ffiwrappers/src/errors.rs | 3 + .../agent/ffiwrappers/src/linux/addrinfo.rs | 121 +++------- .../agent/ffiwrappers/src/linux/group.rs | 49 ++++ .../agent/ffiwrappers/src/linux/ifaddrs.rs | 66 +++++ .../agent/ffiwrappers/src/linux/mod.rs | 7 +- .../agent/ffiwrappers/src/linux/socket.rs | 112 +++++++++ .../agent/ffiwrappers/src/linux/uname.rs | 108 +++++++++ .../agent/ffiwrappers/src/linux/user.rs | 140 +++++++++++ .../agent/ffiwrappers/src/linux/username.rs | 43 ---- Payload_Type/thanatos/agent/thanatos/build.rs | 22 +- .../agent/thanatos/src/agent/egress.rs | 6 +- .../agent/thanatos/src/native/checkininfo.rs | 24 ++ .../agent/thanatos/src/native/linux/mod.rs | 1 + .../agent/thanatos/src/native/linux/system.rs | 225 ++++++++++++++++-- .../agent/thanatos/src/native/linux/user.rs | 8 + .../thanatos/agent/thanatos/src/native/mod.rs | 2 +- .../agent/thanatos/src/native/windows/mod.rs | 5 - .../thanatos/mythic/protos/msg/checkin.proto | 21 +- 20 files changed, 796 insertions(+), 197 deletions(-) create mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/group.rs create mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/ifaddrs.rs create mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/socket.rs create mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/uname.rs create mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/user.rs delete mode 100644 Payload_Type/thanatos/agent/ffiwrappers/src/linux/username.rs create mode 100644 Payload_Type/thanatos/agent/thanatos/src/native/linux/user.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index bc4a6dd..c92858f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ ARG userGid=1000 ARG upgradePackages=false ARG goversion=1.22.0 ARG golangcilintversion=1.56.2 -ARG shell=bash +ARG shell=fish ARG extraPackages ARG protocversion=25.3 @@ -38,13 +38,21 @@ WORKDIR /tmp RUN curl -L "https://go.dev/dl/go${goversion}.linux-amd64.tar.gz" -o go${goversion}.linux-amd64.tar.gz RUN rm -rf /usr/local/go RUN tar -C /usr/local -xzf go${goversion}.linux-amd64.tar.gz -RUN echo "export PATH=$PATH:/usr/local/go/bin" >> /home/${username}/.profile + +RUN grep -qxF 'export PATH=$PATH:/usr/local/go/bin' /home/${username}/.profile \ + || echo 'export PATH=$PATH:/usr/local/go/bin' >> /home/${username}/.profile + RUN rm -f go${goversion}.linux-amd64.tar.gz # Install golangci-lint USER ${username} RUN mkdir -p /home/${username}/go/bin -RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /home/${username}/go/bin v${golangcilintversion} + +RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh \ + | sh -s -- -b /home/${username}/go/bin v${golangcilintversion} + +RUN grep -qxF 'export PATH=$PATH:/home/${username}/go/bin' /home/${username}/.profile \ + || echo 'export PATH=$PATH:/home/${username}/go/bin' >> /home/${username}/.profile # Install Rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup.sh \ @@ -56,7 +64,9 @@ RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup.sh \ -t i686-unknown-linux-gnu \ -t i686-pc-windows-gnu -RUN echo '. "$HOME/.cargo/env"' >> /home/${username}/.profile +RUN grep -qxF '. "$HOME/.cargo/env"' /home/${username}/.profile \ + || echo '. "$HOME/.cargo/env"' >> /home/${username}/.profile + RUN rm -f rustup.sh # Install rustfmt and clippy @@ -67,7 +77,8 @@ USER 0 # Install protoc RUN mkdir -p /tmp/protoc WORKDIR /tmp/protoc -RUN curl -L https://github.com/protocolbuffers/protobuf/releases/download/v${protocversion}/protoc-${protocversion}-linux-x86_64.zip -o protoc.zip +RUN curl -L https://github.com/protocolbuffers/protobuf/releases/download/v${protocversion}/protoc-${protocversion}-linux-x86_64.zip \ + -o protoc.zip RUN unzip protoc.zip RUN mv bin/protoc /usr/local/bin RUN chmod 755 /usr/local/bin/protoc @@ -90,3 +101,10 @@ RUN echo "${username} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers RUN if [ "${extraPackages}" != "" ]; then DEBIAN_FRONTEND=noninteractive apt-get install -y ${extraPackages}; fi USER ${username} + +# Setup PATH if fish is the shell. This needs to be in the context of the user +RUN if [ "${shell}" = "fish" ]; then \ + /usr/bin/fish -c 'set -U fish_user_paths /usr/local/go/bin $fish_user_paths' \ + && /usr/bin/fish -c 'set -U fish_user_paths $HOME/.cargo/bin $fish_user_paths' \ + && /usr/bin/fish -c 'set -U fish_user_paths $HOME/go/bin $fish_user_paths' \ + ; fi diff --git a/Payload_Type/thanatos/agent/errors/src/lib.rs b/Payload_Type/thanatos/agent/errors/src/lib.rs index 6614845..faa9901 100644 --- a/Payload_Type/thanatos/agent/errors/src/lib.rs +++ b/Payload_Type/thanatos/agent/errors/src/lib.rs @@ -6,6 +6,8 @@ pub enum ThanatosError { FFIError(FfiError), NotDomainJoined, + IoError(std::io::Error), + ConfigParseError, } diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/errors.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/errors.rs index 70c6ba4..523b77a 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/errors.rs +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/errors.rs @@ -8,6 +8,9 @@ pub enum FfiError { GaiError(EaiError), NonNullPointer, CanonNameNotFound, + + #[cfg(target_os = "linux")] + NoGroupMembership, } impl FfiError { diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/addrinfo.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/addrinfo.rs index f15adf2..04fb372 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/addrinfo.rs +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/addrinfo.rs @@ -3,64 +3,7 @@ use std::{ffi::CStr, marker::PhantomData, ptr::NonNull}; use crate::errors::{EaiError, FfiError}; use bitflags::bitflags; -#[repr(i32)] -#[derive(Default)] -pub enum Family { - AfInet = libc::AF_INET, - AfInet6 = libc::AF_INET6, - #[default] - Unspec = libc::AF_UNSPEC, - Other(i32), -} - -impl From for Family { - fn from(value: i32) -> Self { - match value { - libc::AF_INET => Self::AfInet, - libc::AF_INET6 => Self::AfInet6, - libc::AF_UNSPEC => Self::Unspec, - _ => Self::Other(value), - } - } -} - -impl From for i32 { - fn from(value: Family) -> Self { - match value { - Family::AfInet => libc::AF_INET, - Family::AfInet6 => libc::AF_INET6, - Family::Unspec => libc::AF_UNSPEC, - Family::Other(v) => v, - } - } -} - -#[repr(i32)] -#[derive(Default)] -pub enum SockType { - #[default] - Any = 0, - SockStream = libc::SOCK_STREAM, - SockDgram = libc::SOCK_DGRAM, - SockSeqPacket = libc::SOCK_SEQPACKET, - SockRaw = libc::SOCK_RAW, - SockRdm = libc::SOCK_RDM, - SockPacket = libc::SOCK_PACKET, -} - -impl From for SockType { - fn from(value: i32) -> Self { - match value { - libc::SOCK_STREAM => Self::SockStream, - libc::SOCK_DGRAM => Self::SockDgram, - libc::SOCK_SEQPACKET => Self::SockSeqPacket, - libc::SOCK_RAW => Self::SockRaw, - libc::SOCK_RDM => Self::SockRdm, - libc::SOCK_PACKET => Self::SockPacket, - _ => Self::Any, - } - } -} +use super::socket::{Family, SockType}; bitflags! { pub struct AiFlags: i32 { @@ -85,7 +28,6 @@ pub struct Hints { pub flags: AiFlags, } -#[repr(transparent)] pub struct AddrInfoList { addrinfo: NonNull, _marker: PhantomData, @@ -125,11 +67,8 @@ impl AddrInfoList { }) } - pub fn iter(&self) -> AddrInfoListIterator<'_> { - AddrInfoListIterator { - addrinfo: self.addrinfo.as_ptr(), - _marker: PhantomData, - } + pub fn first<'a>(&'a self) -> AddrInfo<'a> { + self.addrinfo.into() } } @@ -139,26 +78,6 @@ impl Drop for AddrInfoList { } } -#[repr(transparent)] -pub struct AddrInfoListIterator<'a> { - addrinfo: *mut libc::addrinfo, - _marker: PhantomData<&'a libc::addrinfo>, -} - -impl<'a> Iterator for AddrInfoListIterator<'a> { - type Item = AddrInfo<'a>; - - fn next(&mut self) -> Option { - let curr = AddrInfo { - addrinfo: NonNull::new(self.addrinfo)?, - _marker: PhantomData, - }; - - self.addrinfo = unsafe { *self.addrinfo }.ai_next; - Some(curr) - } -} - #[repr(transparent)] pub struct AddrInfo<'a> { addrinfo: NonNull, @@ -167,26 +86,44 @@ pub struct AddrInfo<'a> { impl<'a> AddrInfo<'a> { pub fn ai_flags(&self) -> i32 { - unsafe { self.addrinfo.as_ref() }.ai_flags + unsafe { self.addrinfo.as_ref().ai_flags } } pub fn ai_family(&self) -> Family { - unsafe { self.addrinfo.as_ref() }.ai_family.into() + unsafe { self.addrinfo.as_ref().ai_family }.into() } pub fn ai_socktype(&self) -> SockType { - unsafe { self.addrinfo.as_ref() }.ai_socktype.into() + unsafe { self.addrinfo.as_ref().ai_socktype }.into() } pub fn ai_protocol(&self) -> i32 { - unsafe { self.addrinfo.as_ref() }.ai_protocol + unsafe { self.addrinfo.as_ref().ai_protocol } } - pub fn canonname(&self) -> Option<&CStr> { - if unsafe { self.addrinfo.as_ref().ai_canonname }.is_null() { - return None; + pub fn canonname(&self) -> &str { + unsafe { + CStr::from_ptr(self.addrinfo.as_ref().ai_canonname) + .to_str() + .unwrap_unchecked() } + } +} + +impl<'a> From> for AddrInfo<'a> { + fn from(value: NonNull) -> Self { + AddrInfo { + addrinfo: value, + _marker: PhantomData, + } + } +} - Some(unsafe { CStr::from_ptr(self.addrinfo.as_ref().ai_canonname) }) +impl<'a> Iterator for AddrInfo<'a> { + type Item = AddrInfo<'a>; + + fn next(&mut self) -> Option { + self.addrinfo = NonNull::new(unsafe { self.addrinfo.as_ref().ai_next })?; + Some(self.addrinfo.into()) } } diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/group.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/group.rs new file mode 100644 index 0000000..3fe0ea4 --- /dev/null +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/group.rs @@ -0,0 +1,49 @@ +use std::{ffi::CStr, ptr::NonNull}; + +use crate::errors::FfiError; + +pub struct GroupInfo(NonNull); + +impl GroupInfo { + pub fn current_group() -> Result { + Self::lookup_gid(unsafe { libc::getgid() }) + } + + pub fn effective_user() -> Result { + Self::lookup_gid(unsafe { libc::getegid() }) + } + + pub fn lookup_username(username: &CStr) -> Result { + let grpasswd = unsafe { libc::getgrnam(username.as_ptr()) }; + Ok(Self( + NonNull::new(grpasswd).ok_or_else(|| FfiError::os_error())?, + )) + } + + pub fn lookup_gid(gid: u32) -> Result { + let grpasswd = unsafe { libc::getgrgid(gid) }; + Ok(Self( + NonNull::new(grpasswd).ok_or_else(|| FfiError::os_error())?, + )) + } + + pub fn groupname<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().gr_name) + .to_str() + .unwrap_unchecked() + } + } + + pub fn passwd<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().gr_passwd) + .to_str() + .unwrap_unchecked() + } + } + + pub fn gid(&self) -> u32 { + unsafe { self.0.as_ref().gr_gid } + } +} diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/ifaddrs.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/ifaddrs.rs new file mode 100644 index 0000000..1df389f --- /dev/null +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/ifaddrs.rs @@ -0,0 +1,66 @@ +use std::{ffi::CStr, marker::PhantomData, ptr::NonNull}; + +use crate::errors::FfiError; + +pub struct IfAddrsList { + ifaddrs: NonNull, + _marker: PhantomData, +} + +impl IfAddrsList { + pub fn new() -> Result { + let mut ifap = std::ptr::null_mut(); + + if unsafe { libc::getifaddrs(&mut ifap) } != 0 { + return Err(FfiError::os_error()); + } + + Ok(IfAddrsList { + ifaddrs: NonNull::new(ifap).ok_or(FfiError::os_error())?, + _marker: PhantomData, + }) + } + + pub fn first<'a>(&'a self) -> IfAddr<'a> { + self.ifaddrs.into() + } +} + +impl Drop for IfAddrsList { + fn drop(&mut self) { + unsafe { libc::freeifaddrs(self.ifaddrs.as_ptr()) }; + } +} + +pub struct IfAddr<'a> { + ifaddr: NonNull, + _marker: PhantomData<&'a libc::ifaddrs>, +} + +impl<'a> IfAddr<'a> { + pub fn name(&self) -> &str { + unsafe { + CStr::from_ptr(self.ifaddr.as_ref().ifa_name) + .to_str() + .unwrap_unchecked() + } + } +} + +impl<'a> From> for IfAddr<'a> { + fn from(value: NonNull) -> Self { + IfAddr { + ifaddr: value, + _marker: PhantomData, + } + } +} + +impl<'a> Iterator for IfAddr<'a> { + type Item = IfAddr<'a>; + + fn next(&mut self) -> Option { + self.ifaddr = NonNull::new(unsafe { self.ifaddr.as_ref().ifa_next })?; + Some(self.ifaddr.into()) + } +} diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/mod.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/mod.rs index aa90346..30f2a4c 100644 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/mod.rs +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/mod.rs @@ -1,7 +1,10 @@ mod gethostname; -mod username; pub mod addrinfo; +pub mod group; +pub mod ifaddrs; +pub mod socket; +pub mod uname; +pub mod user; pub use gethostname::gethostname; -pub use username::username; diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/socket.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/socket.rs new file mode 100644 index 0000000..5a9f65a --- /dev/null +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/socket.rs @@ -0,0 +1,112 @@ +use std::{marker::PhantomData, ptr::NonNull}; + +#[repr(i32)] +#[derive(Default)] +pub enum Family { + AfInet = libc::AF_INET, + AfInet6 = libc::AF_INET6, + #[default] + Unspec = libc::AF_UNSPEC, + Other(i32), +} + +impl From for Family { + fn from(value: i32) -> Self { + match value { + libc::AF_INET => Self::AfInet, + libc::AF_INET6 => Self::AfInet6, + libc::AF_UNSPEC => Self::Unspec, + _ => Self::Other(value), + } + } +} + +impl From for i32 { + fn from(value: Family) -> Self { + match value { + Family::AfInet => libc::AF_INET, + Family::AfInet6 => libc::AF_INET6, + Family::Unspec => libc::AF_UNSPEC, + Family::Other(v) => v, + } + } +} + +impl From for Family { + fn from(value: u16) -> Self { + (value as i32).into() + } +} + +impl From for u16 { + fn from(value: Family) -> Self { + value.into() + } +} + +#[repr(i32)] +#[derive(Default)] +pub enum SockType { + #[default] + Any = 0, + SockStream = libc::SOCK_STREAM, + SockDgram = libc::SOCK_DGRAM, + SockSeqPacket = libc::SOCK_SEQPACKET, + SockRaw = libc::SOCK_RAW, + SockRdm = libc::SOCK_RDM, + SockPacket = libc::SOCK_PACKET, +} + +impl From for SockType { + fn from(value: i32) -> Self { + match value { + libc::SOCK_STREAM => Self::SockStream, + libc::SOCK_DGRAM => Self::SockDgram, + libc::SOCK_SEQPACKET => Self::SockSeqPacket, + libc::SOCK_RAW => Self::SockRaw, + libc::SOCK_RDM => Self::SockRdm, + libc::SOCK_PACKET => Self::SockPacket, + _ => Self::Any, + } + } +} + +pub struct AfInet; +pub struct AfInet6; + +pub trait SockAddrFamily { + type Inner; +} + +impl SockAddrFamily for AfInet { + type Inner = libc::sockaddr_in; +} + +impl SockAddrFamily for AfInet6 { + type Inner = libc::sockaddr_in6; +} + +pub enum SockAddr<'a> { + AfInet(SockAddrIn<'a, AfInet>), + AfInet6(SockAddrIn<'a, AfInet6>), +} + +#[repr(transparent)] +pub struct SockAddrIn<'a, T: SockAddrFamily> { + addr: NonNull, + _marker: PhantomData<&'a T::Inner>, +} + +impl<'a> SockAddrIn<'a, AfInet> { + pub fn sin_family(&self) -> Family { + unsafe { self.addr.as_ref().sin_family }.into() + } + + pub fn sin_port(&self) -> u16 { + unsafe { self.addr.as_ref().sin_port } + } + + pub fn sin_addr(&self) -> &libc::in_addr { + unsafe { &self.addr.as_ref().sin_addr } + } +} diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/uname.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/uname.rs new file mode 100644 index 0000000..a6eca84 --- /dev/null +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/uname.rs @@ -0,0 +1,108 @@ +use std::ffi::CStr; + +use crate::errors::FfiError; + +pub struct UtsName(libc::utsname); + +impl UtsName { + pub fn new() -> Result { + let mut buf: libc::utsname = unsafe { std::mem::zeroed() }; + + if unsafe { libc::uname(&mut buf) } != 0 { + return Err(FfiError::os_error()); + } + + Ok(Self(buf)) + } + + pub fn sysname<'a>(&'a self) -> &'a str { + // SAFETY: + // - This field will always contain a trailing nullbyte according to `uname(2)` + // so the call to `CStr::from_ptr` is safe in this context. + // - The `to_str()` call will do utf8 validation. The system name will only + // ever contain the ascii values "Linux" so there should never be an issue + // with the sysname containing invalid utf8 characters + unsafe { + CStr::from_ptr(self.0.sysname.as_ptr()) + .to_str() + .unwrap_unchecked() + } + } + + pub fn nodename<'a>(&'a self) -> &'a str { + // SAFETY: + // - This field will always contain a trailing nullbyte. + // - The node name contains valid ascii characters. + unsafe { + CStr::from_ptr(self.0.nodename.as_ptr()) + .to_str() + .unwrap_unchecked() + } + } + + pub fn release<'a>(&'a self) -> &'a str { + // SAFETY: + // - This field will always contain a trailing nullbyte. + // - The release name will always contain valid ascii characters. + unsafe { + CStr::from_ptr(self.0.release.as_ptr()) + .to_str() + .unwrap_unchecked() + } + } + + pub fn version<'a>(&'a self) -> &'a str { + // SAFETY: + // - The version field will contain a trailing nullbyte. + // - The version contains only valid utf8 data. + unsafe { + CStr::from_ptr(self.0.version.as_ptr()) + .to_str() + .unwrap_unchecked() + } + } + + pub fn machine<'a>(&'a self) -> &'a str { + // SAFETY: + // - The machine name will contain a trailing nullbyte. + // - The machine name contains only valid utf8 data. + unsafe { + CStr::from_ptr(self.0.machine.as_ptr()) + .to_str() + .unwrap_unchecked() + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn sysname() { + let u = super::UtsName::new().unwrap(); + dbg!(u.sysname()); + } + + #[test] + fn nodename() { + let u = super::UtsName::new().unwrap(); + dbg!(u.nodename()); + } + + #[test] + fn release() { + let u = super::UtsName::new().unwrap(); + dbg!(u.release()); + } + + #[test] + fn version() { + let u = super::UtsName::new().unwrap(); + dbg!(u.version()); + } + + #[test] + fn machine() { + let u = super::UtsName::new().unwrap(); + dbg!(u.machine()); + } +} diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/user.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/user.rs new file mode 100644 index 0000000..ce311f7 --- /dev/null +++ b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/user.rs @@ -0,0 +1,140 @@ +use crate::errors::FfiError; +use std::{ffi::CStr, ptr::NonNull}; + +use super::group::GroupInfo; + +pub struct UserInfo(NonNull); + +pub struct GroupMembership { + pub primary: GroupInfo, + pub members: Vec, +} + +impl UserInfo { + pub fn current_user() -> Result { + Self::lookup_uid(unsafe { libc::getuid() }) + } + + pub fn effective_user() -> Result { + Self::lookup_uid(unsafe { libc::geteuid() }) + } + + pub fn lookup_username(username: &CStr) -> Result { + let passwd = unsafe { libc::getpwnam(username.as_ptr()) }; + + Ok(Self( + NonNull::new(passwd).ok_or_else(|| FfiError::os_error())?, + )) + } + + pub fn lookup_uid(uid: u32) -> Result { + let passwd = unsafe { libc::getpwuid(uid) }; + + Ok(Self( + NonNull::new(passwd).ok_or_else(|| FfiError::os_error())?, + )) + } + + pub fn group_membership(&self) -> Result { + let mut ngroups = 0i32; + unsafe { + libc::getgrouplist( + self.0.as_ref().pw_name, + self.0.as_ref().pw_gid, + std::ptr::null_mut(), + &mut ngroups, + ) + }; + + if ngroups <= 0 { + return Err(FfiError::os_error()); + } + + let mut gid_list = vec![0u32; ngroups as usize]; + + if unsafe { + libc::getgrouplist( + self.0.as_ref().pw_name, + self.gid(), + gid_list.as_mut_ptr(), + &mut ngroups, + ) + } != 0 + { + return Err(FfiError::os_error()); + } + + let members = gid_list + .into_iter() + .flat_map(|gid| GroupInfo::lookup_gid(gid)) + .collect::>(); + + let primary = GroupInfo::current_group().map_err(|_| FfiError::NoGroupMembership)?; + + Ok(GroupMembership { primary, members }) + } + + pub fn username<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().pw_name) + .to_str() + .unwrap_unchecked() + } + } + + pub fn passwd<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().pw_passwd) + .to_str() + .unwrap_unchecked() + } + } + + pub fn uid(&self) -> u32 { + unsafe { self.0.as_ref().pw_uid } + } + + pub fn gid(&self) -> u32 { + unsafe { self.0.as_ref().pw_gid } + } + + pub fn gecos<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().pw_gecos) + .to_str() + .unwrap_unchecked() + } + } + + pub fn home_dir<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().pw_dir) + .to_str() + .unwrap_unchecked() + } + } + + pub fn shell<'a>(&'a self) -> &'a str { + unsafe { + CStr::from_ptr(self.0.as_ref().pw_shell) + .to_str() + .unwrap_unchecked() + } + } +} + +#[cfg(test)] +mod tests { + #[test] + pub fn info() { + let current_user = super::UserInfo::current_user().unwrap(); + + dbg!(current_user.username()); + dbg!(current_user.passwd()); + dbg!(current_user.uid()); + dbg!(current_user.gid()); + dbg!(current_user.gecos()); + dbg!(current_user.home_dir()); + dbg!(current_user.shell()); + } +} diff --git a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/username.rs b/Payload_Type/thanatos/agent/ffiwrappers/src/linux/username.rs deleted file mode 100644 index 641d239..0000000 --- a/Payload_Type/thanatos/agent/ffiwrappers/src/linux/username.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::ffi::CStr; - -use crate::errors::FfiError; - -pub fn username() -> Result { - // Get a pointer to a the `passwd` entry of the current user id - // - // SAFETY: This will return NULL on error - let passwd = unsafe { libc::getpwuid(libc::getuid()) }; - - // Check if the returned `passwd` pointer is NULL - if passwd.is_null() { - return Err(FfiError::os_error()); - } - - // Get a pointer to the username buffer from the passwd buffer. - // - // SAFETY: The `passwd` pointer was checked above to verify that it is not NULL. - // This should also be NULL checked. - let username_ptr = unsafe { (*passwd).pw_name }; - - // Check if the username pointer is NULL - if username_ptr.is_null() { - return Err(FfiError::os_error()); - } - - // Convert the username pointer into a CStr. - // - // SAFETY: The `username_ptr` was NULL checked above. - // This buffer will be a NULL terminated according to `getpwnam(3)` - let username = unsafe { CStr::from_ptr(username_ptr) }; - - Ok(username.to_string_lossy().into_owned()) -} - -#[cfg(test)] -mod tests { - #[test] - fn username_test() { - let username = super::username().unwrap(); - dbg!(username); - } -} diff --git a/Payload_Type/thanatos/agent/thanatos/build.rs b/Payload_Type/thanatos/agent/thanatos/build.rs index 02fb466..b49f1b1 100644 --- a/Payload_Type/thanatos/agent/thanatos/build.rs +++ b/Payload_Type/thanatos/agent/thanatos/build.rs @@ -11,16 +11,16 @@ fn main() { let checkin_proto = proto_dir.join("msg").join("checkin.proto"); - if checkin_proto.exists() { - println!( - "cargo:rerun-if-changed={}", - checkin_proto - .canonicalize() - .expect("Failed to find checkin.proto") - .to_str() - .unwrap() - ); - } + let output_path = Path::new(&std::env::var("OUT_DIR").unwrap()).join("msg.checkin.rs"); + let _ = std::fs::remove_file(output_path); - prost_build::compile_protos(&[checkin_proto], &[proto_dir]).expect("Failed to compile protos"); + prost_build::compile_protos(&[&checkin_proto], &[proto_dir]).expect("Failed to compile protos"); + println!( + "cargo:rerun-if-changed={}", + checkin_proto + .canonicalize() + .expect("Failed to find checkin.proto") + .to_str() + .unwrap() + ); } diff --git a/Payload_Type/thanatos/agent/thanatos/src/agent/egress.rs b/Payload_Type/thanatos/agent/thanatos/src/agent/egress.rs index 63774ee..33e1555 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/agent/egress.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/agent/egress.rs @@ -7,7 +7,7 @@ use errors::ThanatosError; use super::workinghours; pub struct Agent { - _uuid: String, + uuid: String, working_hours: WorkingHours, } @@ -19,7 +19,7 @@ pub(super) struct WorkingHours { impl Agent { pub fn new(config: ConfigVars) -> Result { Ok(Agent { - _uuid: config.uuid()?.to_string(), + uuid: config.uuid()?.to_string(), working_hours: WorkingHours { start: NaiveTime::from_num_seconds_from_midnight_opt( config.working_hours_start(), @@ -34,6 +34,8 @@ impl Agent { pub fn run(self) { self.handle_working_hours(); + + let checkin_msg = crate::native::checkininfo::get_checkininfo(self.uuid.clone()); } fn handle_working_hours(&self) { diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/checkininfo.rs b/Payload_Type/thanatos/agent/thanatos/src/native/checkininfo.rs index 8b13789..b5e7fd7 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/native/checkininfo.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/native/checkininfo.rs @@ -1 +1,25 @@ +use ffiwrappers::linux::user::UserInfo; +use crate::proto::checkin::CheckinInfo; + +#[cfg(target_os = "linux")] +use crate::native::linux::system; + +#[cfg(target_os = "windows")] +use crate::native::windows::system; + +pub fn get_checkininfo(uuid: String) -> CheckinInfo { + CheckinInfo { + uuid, + user: UserInfo::current_user() + .ok() + .map(|info| info.username().to_string()), + host: system::hostname().ok(), + domain: system::domain().ok(), + pid: Some(std::process::id()), + architecture: system::architecture().into(), + platform: system::platform(), + integrity_level: system::integrity_level().ok(), + process_name: system::process_name().ok(), + } +} diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/linux/mod.rs b/Payload_Type/thanatos/agent/thanatos/src/native/linux/mod.rs index ac77f63..06b37da 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/native/linux/mod.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/native/linux/mod.rs @@ -1 +1,2 @@ pub mod system; +pub mod user; diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/linux/system.rs b/Payload_Type/thanatos/agent/thanatos/src/native/linux/system.rs index 9ac1213..8810ebf 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/native/linux/system.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/native/linux/system.rs @@ -1,24 +1,38 @@ -use std::ffi::CString; +use std::{ + ffi::CString, + io::{BufRead, BufReader}, +}; use errors::ThanatosError; use ffiwrappers::{ errors::FfiError, - linux::addrinfo::{AddrInfoList, AiFlags, Hints, SockType}, + linux::{ + addrinfo::{AddrInfoList, AiFlags, Hints, SockType}, + uname, + user::UserInfo, + }, }; -#[allow(unused)] +use crate::proto::checkin::Architecture; + +#[derive(Default, Debug)] +pub struct OsReleaseInfo { + name: String, + version: String, + pretty_name: Option, +} + pub fn hostname() -> Result { let h = ffiwrappers::linux::gethostname().map_err(ThanatosError::FFIError)?; Ok(h.split('.').next().unwrap_or(&h).to_string()) } -#[allow(unused)] pub fn domain() -> Result { let current_host = ffiwrappers::linux::gethostname().map_err(ThanatosError::FFIError)?; let current_host = CString::new(current_host).map_err(|_| ThanatosError::FFIError(FfiError::InteriorNull))?; - let addrlist = AddrInfoList::new( + let mut addrlist = AddrInfoList::new( Some(¤t_host), None, Some(Hints { @@ -29,15 +43,7 @@ pub fn domain() -> Result { ) .map_err(ThanatosError::FFIError)?; - let canonname = addrlist - .iter() - .next() - .and_then(|addrentry| { - addrentry - .canonname() - .map(|c| c.to_string_lossy().to_string()) - }) - .ok_or(ThanatosError::FFIError(FfiError::CanonNameNotFound))?; + let canonname = addrlist.first().canonname().to_string(); let mut s = canonname.split('.'); s.next() @@ -45,9 +51,171 @@ pub fn domain() -> Result { Ok(s.collect::>().join(".")) } -#[allow(unused)] -pub fn username() -> Result { - ffiwrappers::linux::username().map_err(ThanatosError::FFIError) +// TODO: Make this return an enum value for the container environment and return +// it as a separate field in the initial check in +pub fn check_container_environment() -> Option<&'static str> { + if let Ok(readdir) = std::fs::read_dir("/") { + for entry_result in readdir { + if let Ok(entry) = entry_result { + if entry.file_name() == ".dockerenv" { + return Some("Docker"); + } + } + } + } + + if let Ok(readdir) = std::fs::read_dir("/run") { + for entry_result in readdir { + if let Ok(entry) = entry_result { + if entry.file_name() == ".containerenv" { + return Some("Container"); + } + } + } + } + + None +} + +// TODO: Return this into a separate initial check in field. +// Parse /proc/self/mountinfo for selinux detection instead of looking for /sys/fs/selinux +pub fn check_selinux() -> bool { + if let Ok(readdir) = std::fs::read_dir("/sys/fs") { + for entry_result in readdir { + if let Ok(entry) = entry_result { + if entry.file_name() == "selinux" { + return true; + } + } + } + } + + false +} + +pub fn os_release() -> Result { + let f = std::fs::File::open("/etc/os-release").map_err(ThanatosError::IoError)?; + let reader = BufReader::new(f); + + let mut release_info = OsReleaseInfo::default(); + for line_read in reader.lines() { + if let Ok(line) = line_read { + if line.starts_with("NAME=") { + let s = line.split('='); + if let Some(name_quoted) = s.last() { + release_info.name = name_quoted[1..name_quoted.len() - 1].to_string(); + } + continue; + } + + if line.starts_with("VERSION=") { + let s = line.split('='); + if let Some(version_quoted) = s.last() { + release_info.version = version_quoted[1..version_quoted.len() - 1].to_string(); + } + + continue; + } + + if line.starts_with("PRETTY_NAME=") { + let s = line.split('='); + if let Some(pretty_name_quoted) = s.last() { + release_info.pretty_name = + Some(pretty_name_quoted[1..pretty_name_quoted.len() - 1].to_string()); + } + + continue; + } + } + } + + Ok(release_info) +} + +// TODO: Split up platform values into separate check in fields and create the platform +// string server side. Also grab the architecture from the initial check in instead +// of embedding it into this string +pub fn platform() -> String { + let distro = os_release() + .map(|os_info| { + os_info + .pretty_name + .unwrap_or_else(|| format!("{} {}", os_info.name, os_info.version)) + }) + .unwrap_or_else(|_| "Linux".to_string()); + + let utsname = uname::UtsName::new(); + + let mut platform_name = match utsname { + Ok(utsname) => format!( + "{} kernel {} {}", + distro, + utsname.release(), + utsname.machine() + ) + .to_string(), + Err(_) => distro, + }; + + if check_selinux() { + platform_name.push_str(" (SELinux)"); + } + + if let Some(runtime) = check_container_environment() { + platform_name.push_str(&format!(" ({runtime})")); + } + + platform_name +} + +pub fn architecture() -> Architecture { + #[cfg(target_arch = "x86_64")] + let mut arch = Architecture::X8664; + + #[cfg(target_arch = "x86")] + let mut arch = Architecture::X86; + + if let Ok(utsname) = uname::UtsName::new() { + match utsname.machine() { + "x86_64" => arch = Architecture::X8664, + "x86" => arch = Architecture::X86, + _ => (), + } + } + + arch +} + +pub fn integrity_level() -> Result { + let effective_user = UserInfo::effective_user().map_err(ThanatosError::FFIError)?; + if effective_user.uid() == 0 { + return Ok(4); + } + + let current_groups = UserInfo::current_user() + .map_err(ThanatosError::FFIError)? + .group_membership() + .map_err(ThanatosError::FFIError)?; + + for group in current_groups.members { + if group.gid() == 0 { + return Ok(3); + } + + if group.groupname() == "sudoers" { + return Ok(3); + } + + if group.groupname() == "wheel" { + return Ok(3); + } + } + + Ok(2) +} + +pub fn process_name() -> Result { + std::fs::read_to_string("/proc/self/comm").map_err(ThanatosError::IoError) } #[cfg(test)] @@ -84,7 +252,7 @@ mod tests { let current_host = ffiwrappers::linux::gethostname().unwrap(); let current_host = CString::new(current_host).unwrap(); - let addrlist = AddrInfoList::new( + let mut addrlist = AddrInfoList::new( Some(¤t_host), None, Some(Hints { @@ -95,14 +263,7 @@ mod tests { ) .unwrap(); - let mut canonname = addrlist - .iter() - .next() - .unwrap() - .canonname() - .unwrap() - .to_string_lossy() - .to_string(); + let mut canonname = addrlist.first().canonname().to_string(); if !canonname.ends_with('.') { canonname.push('.'); @@ -110,4 +271,16 @@ mod tests { assert_eq!(canonname, fqdn); } + + #[test] + fn platform_test() { + let platform = super::platform(); + dbg!(platform); + } + + #[test] + fn os_release_test() { + let distro = super::os_release().unwrap(); + dbg!(distro); + } } diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/linux/user.rs b/Payload_Type/thanatos/agent/thanatos/src/native/linux/user.rs new file mode 100644 index 0000000..7b61ed6 --- /dev/null +++ b/Payload_Type/thanatos/agent/thanatos/src/native/linux/user.rs @@ -0,0 +1,8 @@ +use errors::ThanatosError; +use ffiwrappers::linux::user::UserInfo; + +pub fn username() -> Result { + UserInfo::current_user() + .map(|user| user.username().to_string()) + .map_err(ThanatosError::FFIError) +} diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/mod.rs b/Payload_Type/thanatos/agent/thanatos/src/native/mod.rs index 90a73b0..d360608 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/native/mod.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/native/mod.rs @@ -4,4 +4,4 @@ pub mod linux; #[cfg(target_os = "windows")] pub mod windows; -mod checkininfo; +pub mod checkininfo; diff --git a/Payload_Type/thanatos/agent/thanatos/src/native/windows/mod.rs b/Payload_Type/thanatos/agent/thanatos/src/native/windows/mod.rs index f599754..018a515 100644 --- a/Payload_Type/thanatos/agent/thanatos/src/native/windows/mod.rs +++ b/Payload_Type/thanatos/agent/thanatos/src/native/windows/mod.rs @@ -1,10 +1,5 @@ pub mod system { - #[cfg(feature = "domaincheck")] pub use ffiwrappers::windows::domain; - - #[cfg(feature = "hostnamecheck")] pub use ffiwrappers::windows::hostname; - - #[cfg(feature = "usernamecheck")] pub use ffiwrappers::windows::username; } diff --git a/Payload_Type/thanatos/mythic/protos/msg/checkin.proto b/Payload_Type/thanatos/mythic/protos/msg/checkin.proto index cb6f382..fbb7909 100644 --- a/Payload_Type/thanatos/mythic/protos/msg/checkin.proto +++ b/Payload_Type/thanatos/mythic/protos/msg/checkin.proto @@ -3,18 +3,19 @@ syntax = "proto3"; package msg.checkin; message CheckinInfo { - repeated string ips = 1; - string platform = 2; - optional string user = 3; - optional string host = 4; - optional uint32 pid = 5; - Architecture architecture = 6; - optional string domain = 7; - optional uint32 integrity_level = 8; - optional string process_name = 9; + string uuid = 1; + repeated string ips = 2; + string platform = 3; + optional string user = 4; + optional string host = 5; + optional uint32 pid = 6; + Architecture architecture = 7; + optional string domain = 8; + optional uint32 integrity_level = 9; + optional string process_name = 10; } enum Architecture { X86 = 0; - AMD64 = 1; + X86_64 = 1; }