Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 61 additions & 41 deletions crates/rrg/src/action/dump_process_memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,52 @@ impl From<Permissions> for rrg_proto::dump_process_memory::Permissions {
}
}

/// Criteria for filtering memory regions.
///
/// `RegionFilter` defines a set of rules used to evaluate whether a specific
/// memory region should be included in a target set. This logic is exposed via
/// the `matches` method: if a region possesses an attribute that corresponds to
/// an enabled `skip_*` flag, `matches` returns `false` (indicating exclusion).
/// Otherwise, it returns `true` (indicating inclusion).
///
/// By default, all skip flags are `false`. This means that using
/// `RegionFilter::default()` creates a completely permissive filter,
/// including everything without any exclusions.
#[derive(Default)]
pub struct RegionFilter {
/// Excludes filesystem-mapped memory regions (e.g., memory-mapped files).
pub skip_mapped_files: bool,
/// Excludes memory regions shared with other processes (e.g., shared libraries or IPC memory).
pub skip_shared_regions: bool,
/// Excludes memory regions containing executable data (e.g., `.text` sections).
pub skip_executable_regions: bool,
/// Excludes memory regions that are strictly read-only (e.g., `.rodata` sections).
pub skip_readonly_regions: bool,
}

impl RegionFilter {
/// Evaluates whether a given memory region should be included based on the filter rules.
pub fn matches(&self, region: &MappedRegion) -> bool {
if self.skip_shared_regions && region.permissions.shared {
return false;
}
if self.skip_executable_regions && region.permissions.execute {
return false;
}
if self.skip_mapped_files && (region.inode.is_some() || region.path.is_some()) {
return false;
}
if self.skip_readonly_regions
&& region.permissions.read
&& !region.permissions.write
&& !region.permissions.execute
{
return false;
}
true
}
}

/// Arguments of the `dump_process_memory` action.
#[derive(Default)]
pub struct Args {
Expand All @@ -660,14 +706,8 @@ pub struct Args {
// If not reached, the remaining memory pages will be dumped up to `total_size_limit`.
priority_offsets: Option<Vec<u64>>,

// Set this flag to avoid dumping mapped files.
skip_mapped_files: bool,
// Set this flag to avoid dumping shared memory regions.
skip_shared_regions: bool,
// Set this flag to avoid dumping executable memory regions.
skip_executable_regions: bool,
// Set this flag to avoid dumping readonly memory regions.
skip_readonly_regions: bool,
// Determines which memory regions should be considered for dumping.
filter: RegionFilter,
}

use crate::request::ParseArgsError;
Expand Down Expand Up @@ -703,37 +743,16 @@ impl crate::request::Args for Args {
pids: proto.take_pids(),
total_size_limit: proto.total_size_limit,
priority_offsets,
skip_mapped_files: proto.skip_mapped_files,
skip_shared_regions: proto.skip_shared_regions,
skip_executable_regions: proto.skip_executable_regions,
skip_readonly_regions: proto.skip_readonly_regions,
filter: RegionFilter {
skip_mapped_files: proto.skip_mapped_files,
skip_shared_regions: proto.skip_shared_regions,
skip_executable_regions: proto.skip_executable_regions,
skip_readonly_regions: proto.skip_readonly_regions,
},
})
}
}

impl Args {
/// Whether `region` should be dumped according to `self`'s filtering parameters.
fn should_dump(&self, region: &MappedRegion) -> bool {
if self.skip_shared_regions && region.permissions.shared {
return false;
}
if self.skip_executable_regions && region.permissions.execute {
return false;
}
if self.skip_mapped_files && (region.inode.is_some() || region.path.is_some()) {
return false;
}
if self.skip_readonly_regions
&& region.permissions.read
&& !region.permissions.write
&& !region.permissions.execute
{
return false;
}
true
}
}

const MAX_BLOB_SIZE: u64 = 2 * (1024 * 1024);

/// Successful result of the `dump_process_memory` action.
Expand Down Expand Up @@ -923,9 +942,7 @@ where
S: crate::session::Session,
{
let mut total_size_left = args.total_size_limit.unwrap_or(u64::MAX);
// Circumvent borrow checker complaint about partial moves with `take`
let pids = std::mem::take(&mut args.pids);
for pid in pids {
for pid in args.pids {
let regions = match MappedRegionIter::from_pid(pid) {
Ok(regions) => regions,
Err(cause) => {
Expand Down Expand Up @@ -959,7 +976,7 @@ where
// so we enforce that by `take`ing it here.
// This too is a workaround for partial moves.
let offsets = args.priority_offsets.take();
let regions = regions.into_iter().filter(|reg| args.should_dump(reg));
let regions = regions.into_iter().filter(|reg| args.filter.matches(reg));

let regions = if let Some(offsets) = offsets {
sort_by_priority(regions, offsets)
Expand Down Expand Up @@ -1199,8 +1216,11 @@ pub mod tests {
pids: vec![std::process::id()],
// Set limit to keep unit test time reasonable
total_size_limit: Some(10000),
skip_executable_regions: true,
skip_shared_regions: true,
filter: RegionFilter {
skip_executable_regions: true,
skip_shared_regions: true,
..Default::default()
},
..Default::default()
};

Expand Down
58 changes: 14 additions & 44 deletions crates/rrg/src/action/scan_memory_yara.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Use of this source code is governed by an MIT-style license that can be found
// in the LICENSE file or at https://opensource.org/licenses/MIT.

use crate::action::dump_process_memory::{MappedRegion, MemoryReader};
use crate::action::dump_process_memory::{MappedRegion, MemoryReader, RegionFilter};
use std::time::Duration;

use yara_x::Compiler;
Expand All @@ -12,7 +12,6 @@ use yara_x::blocks::Scanner;
use rrg_proto::scan_memory_yara as proto;

/// Arguments of the `scan_memory_yara` action.
#[derive(Default)]
pub struct Args {
/// PIDs of the processes whose memory we are interested in.
pids: Vec<u32>,
Expand All @@ -23,14 +22,8 @@ pub struct Args {
/// Maximum time spent scanning a single process.
timeout: Option<Duration>,

/// Set this flag to avoid scanning mapped files.
skip_mapped_files: bool,
/// Set this flag to avoid scanning shared memory regions.
skip_shared_regions: bool,
/// Set this flag to avoid scanning executable memory regions.
skip_executable_regions: bool,
/// Set this flag to avoid scanning readonly memory regions.
skip_readonly_regions: bool,
/// Determines which memory regions should be considered for scanning.
filter: RegionFilter,

/// Length of the chunks used to read large memory regions, in bytes.
chunk_size: u64,
Expand All @@ -57,39 +50,18 @@ impl crate::request::Args for Args {
pids: proto.pids,
signature: proto.signature,
timeout,
skip_mapped_files: proto.skip_mapped_files,
skip_shared_regions: proto.skip_shared_regions,
skip_executable_regions: proto.skip_executable_regions,
skip_readonly_regions: proto.skip_readonly_regions,
filter: RegionFilter {
skip_mapped_files: proto.skip_mapped_files,
skip_shared_regions: proto.skip_shared_regions,
skip_executable_regions: proto.skip_executable_regions,
skip_readonly_regions: proto.skip_readonly_regions,
},
chunk_size: proto.chunk_size.unwrap_or(DEFAULT_CHUNK_SIZE),
chunk_overlap: proto.chunk_overlap.unwrap_or(DEFAULT_CHUNK_OVERLAP),
})
}
}

impl Args {
/// Whether `region` should be dumped according to `self`'s filtering parameters.
fn should_dump(&self, region: &MappedRegion) -> bool {
if self.skip_shared_regions && region.permissions.shared {
return false;
}
if self.skip_executable_regions && region.permissions.execute {
return false;
}
if self.skip_mapped_files && (region.inode.is_some() || region.path.is_some()) {
return false;
}
if self.skip_readonly_regions
&& region.permissions.read
&& !region.permissions.write
&& !region.permissions.execute
{
return false;
}
true
}
}

// Unfortunately need to recreate some of the YARA structs in our code
// as they depend on the lifetime of the Compiler struct, whereas RRG
// action items need to be 'static. Additionally, we want to send
Expand Down Expand Up @@ -270,7 +242,7 @@ fn scan_region<M: MemoryReader>(
}

#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn handle<S>(session: &mut S, mut args: Args) -> crate::session::Result<()>
pub fn handle<S>(session: &mut S, args: Args) -> crate::session::Result<()>
where
S: crate::session::Session,
{
Expand All @@ -284,9 +256,7 @@ where
compiler.build()
};

// Circumvent borrow checker complaint about partial moves with `take`
let pids = std::mem::take(&mut args.pids);
for pid in pids {
for pid in args.pids {
let regions = match MappedRegionIter::from_pid(pid) {
Ok(regions) => regions,
Err(cause) => {
Expand Down Expand Up @@ -321,7 +291,7 @@ where

if let Err(error) = regions
.into_iter()
.filter(|reg| args.should_dump(reg))
.filter(|reg| args.filter.matches(reg))
.try_for_each(|region| {
scan_region(
&region,
Expand Down Expand Up @@ -500,7 +470,7 @@ mod tests {
timeout: Some(Duration::from_secs(30)),
chunk_size: 100 * 1024 * 1024,
chunk_overlap: 50 * 1024 * 1024,
..Default::default()
filter: Default::default(),
};

handle(&mut session, args).unwrap();
Expand Down Expand Up @@ -545,7 +515,7 @@ mod tests {
timeout: Some(Duration::from_millis(500)),
chunk_size: 10000,
chunk_overlap: 500,
..Default::default()
filter: Default::default(),
};

handle(&mut session, args).unwrap();
Expand Down
Loading