Skip to content

Commit

Permalink
feat: filter file capture by ELF type (#3361)
Browse files Browse the repository at this point in the history
Add a filter for file IO capture filtering ELF files.
ELF files are the most important files to capture in security research,
so filtering according to ELF files is very effective to reduce noise.
  • Loading branch information
AlonZivony authored Sep 3, 2023
1 parent 1feab86 commit fd47dfb
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 39 deletions.
2 changes: 1 addition & 1 deletion docs/docs/forensics/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Tracee can capture the following types of artifacts:
file will be captured. I/O files can be filtered using 3 optional filters:
1. path - prefix of the file written/read. Up to 3 path filters can be
provided per capture type.
2. type - file's type can be `pipe`, `socket` or `regular`.
2. type - file's type can be `pipe`, `socket`, `elf` or `regular`.
3. fd - standard FD, one of the following: `stdin`, `stdout` and `stderr`.

***write example***
Expand Down
3 changes: 2 additions & 1 deletion pkg/cmd/flags/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ The different filter types have logical 'and' between them, but logical 'or' bet
The filter is given in the following format - <read/write>:<filter_type>=<filter_value>
Filters types:
path A filter for the file path prefix (up to 50 characters). Up to 3 filters can be given. Identical to using '<read/write>=/path/prefix*'.
type A file type from the following options: 'regular', 'pipe' and 'socket'.
type A file type from the following options: 'regular', 'pipe', 'socket' and 'elf'.
fd The file descriptor of the file. Can be one of the three standards - 'stdin', 'stdout' and 'stderr'.
Expand Down Expand Up @@ -272,6 +272,7 @@ var captureFileTypeStringToFlag = map[string]config.FileCaptureType{
"pipe": config.CapturePipeFiles,
"socket": config.CaptureSocketFiles,
"regular": config.CaptureRegularFiles,
"elf": config.CaptureELFFiles,
}

// parseFileCaptureType parse file type string to its matching bit-flag value
Expand Down
1 change: 1 addition & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const (
CaptureRegularFiles FileCaptureType = 1 << iota
CapturePipeFiles
CaptureSocketFiles
CaptureELFFiles
)

// Filters for FDs flags
Expand Down
46 changes: 41 additions & 5 deletions pkg/ebpf/c/capture_filtering.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@
#define FILTER_NORMAL_FILES (1 << (FILTER_FILE_TYPE_START_BIT + 0))
#define FILTER_PIPE_FILES (1 << (FILTER_FILE_TYPE_START_BIT + 1))
#define FILTER_SOCKET_FILES (1 << (FILTER_FILE_TYPE_START_BIT + 2))
#define FILTER_ELF_FILES (1 << (FILTER_FILE_TYPE_START_BIT + 3))
#define FILTER_STDIN_FILES (1 << (FILTER_FILE_FD_START_BIT + STDIN))
#define FILTER_STDOUT_FILES (1 << (FILTER_FILE_FD_START_BIT + STDOUT))
#define FILTER_STDERR_FILES (1 << (FILTER_FILE_FD_START_BIT + STDERR))

#define FILTER_FILE_TYPE_MASK ((FILTER_SOCKET_FILES << 1) - FILTER_NORMAL_FILES)
#define FILTER_FILE_TYPE_MASK ((FILTER_ELF_FILES << 1) - FILTER_NORMAL_FILES)
#define FILTER_FDS_MASK ((FILTER_STDERR_FILES << 1) - FILTER_STDIN_FILES)

#define CAPTURE_READ_TYPE_FILTER_IDX 0
Expand All @@ -28,7 +29,7 @@
// PROTOTYPES

statfunc bool filter_file_path(void *, void *, struct file *);
statfunc bool filter_file_type(void *, void *, size_t, struct file *);
statfunc bool filter_file_type(void *, void *, size_t, struct file *, io_data_t, off_t);
statfunc bool filter_file_fd(void *, void *, size_t, struct file *);

// FUNCTIONS
Expand Down Expand Up @@ -72,7 +73,12 @@ statfunc bool filter_file_path(void *ctx, void *filter_map, struct file *file)

// Return if the file does not match any given file type filters in the filter map (so it should be
// filtered out). The result will be false if no filter exist.
statfunc bool filter_file_type(void *ctx, void *filter_map, size_t map_idx, struct file *file)
statfunc bool filter_file_type(void *ctx,
void *filter_map,
size_t map_idx,
struct file *file,
io_data_t io_data,
off_t start_pos)
{
bool has_type_filter = false;
bool type_filter_match = false;
Expand All @@ -92,18 +98,48 @@ statfunc bool filter_file_type(void *ctx, void *filter_map, size_t map_idx, stru
struct pipe_inode_info *pipe = get_file_pipe_info(file);
if (pipe != NULL) {
type_filter_match = true;
goto exit;
}
} else if (*type_filter & FILTER_SOCKET_FILES) {
}
if (*type_filter & FILTER_SOCKET_FILES) {
if (imode_mode & S_IFSOCK) {
type_filter_match = true;
goto exit;
}
} else if (*type_filter & FILTER_NORMAL_FILES) {
}
if (*type_filter & FILTER_NORMAL_FILES) {
if (imode_mode & S_IFREG) {
type_filter_match = true;
goto exit;
}
}
if (*type_filter & FILTER_ELF_FILES) {
file_id_t file_id = get_file_id(file);
file_id.ctime = 0;
if (start_pos == 0) {
// Check if header is matching ELF header
u8 header[FILE_MAGIC_HDR_SIZE] = {};
fill_file_header(header, io_data);
if (header[0] == 0x7f && header[1] == 0x45 && header[2] == 0x4c &&
header[3] == 0x46) {
type_filter_match = true;
bpf_map_update_elem(&elf_files_map, &file_id, &type_filter_match, BPF_ANY);
} else {
// Avoid considering the file ELF if further IO operation will occur
bpf_map_delete_elem(&elf_files_map, &file_id);
}
} else {
// In the case of IO operation in chunks one after another, we want to check
// if the file is already confirmed to be an ELF file in previous operation.
bool *is_elf = bpf_map_lookup_elem(&elf_files_map, &file_id);
if (is_elf != NULL && *is_elf == true) {
type_filter_match = true;
}
}
}
}

exit:
return (has_type_filter && !type_filter_match);
}

Expand Down
20 changes: 20 additions & 0 deletions pkg/ebpf/c/common/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ statfunc struct super_block *get_super_block_from_inode(struct inode *);
statfunc unsigned long get_s_magic_from_super_block(struct super_block *);
statfunc void fill_vfs_file_metadata(struct file *, u32, u8 *);
statfunc void fill_vfs_file_bin_args_io_data(io_data_t, bin_args_t *);
statfunc void fill_file_header(u8[FILE_MAGIC_HDR_SIZE], io_data_t);
statfunc void
fill_vfs_file_bin_args(u32, struct file *, loff_t *, io_data_t, size_t, int, bin_args_t *);

Expand Down Expand Up @@ -448,4 +449,23 @@ statfunc void fill_vfs_file_bin_args(u32 type,
fill_vfs_file_bin_args_io_data(io_data, bin_args);
}

statfunc void fill_file_header(u8 header[FILE_MAGIC_HDR_SIZE], io_data_t io_data)
{
u32 len = (u32) io_data.len;
if (io_data.is_buf) {
if (len < FILE_MAGIC_HDR_SIZE)
bpf_probe_read(header, len & FILE_MAGIC_MASK, io_data.ptr);
else
bpf_probe_read(header, FILE_MAGIC_HDR_SIZE, io_data.ptr);
} else {
struct iovec io_vec;
__builtin_memset(&io_vec, 0, sizeof(io_vec));
bpf_probe_read(&io_vec, sizeof(struct iovec), io_data.ptr);
if (len < FILE_MAGIC_HDR_SIZE)
bpf_probe_read(header, len & FILE_MAGIC_MASK, io_vec.iov_base);
else
bpf_probe_read(header, FILE_MAGIC_HDR_SIZE, io_vec.iov_base);
}
}

#endif
1 change: 1 addition & 0 deletions pkg/ebpf/c/maps.h
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ BPF_HASH(logs_count, bpf_log_t, bpf_log_count_t, 4096); // logs count
BPF_PERCPU_ARRAY(scratch_map, scratch_t, 1); // scratch space to avoid allocating stuff on the stack
BPF_LRU_HASH(file_modification_map, file_mod_key_t, int, 10240); // hold file data to decide if should submit file modification event
BPF_LRU_HASH(io_file_path_cache_map, file_id_t, path_buf_t, 5); // store cache for IO operations path
BPF_LRU_HASH(elf_files_map, file_id_t, bool, 64); // store cache for file ELF type check

// clang-format on

Expand Down
87 changes: 57 additions & 30 deletions pkg/ebpf/c/tracee.bpf.c
Original file line number Diff line number Diff line change
Expand Up @@ -2044,22 +2044,40 @@ int BPF_KPROBE(trace_security_inode_unlink)
if (!should_trace(&p))
return 0;

if (!should_submit(SECURITY_INODE_UNLINK, p.event))
bool should_trace_inode_unlink = should_submit(SECURITY_INODE_UNLINK, p.event);
bool should_capture_io = false;
if ((p.config->options & (OPT_CAPTURE_FILES_READ | OPT_CAPTURE_FILES_WRITE)) != 0)
should_capture_io = true;

if (!should_trace_inode_unlink && !should_capture_io)
return 0;

file_id_t unlinked_file_id = {};
int ret = 0;

// struct inode *dir = (struct inode *)PT_REGS_PARM1(ctx);
struct dentry *dentry = (struct dentry *) PT_REGS_PARM2(ctx);
void *dentry_path = get_dentry_path_str(dentry);
unsigned long inode_nr = get_inode_nr_from_dentry(dentry);
dev_t dev = get_dev_from_dentry(dentry);
u64 ctime = get_ctime_nanosec_from_dentry(dentry);
unlinked_file_id.inode = get_inode_nr_from_dentry(dentry);
unlinked_file_id.device = get_dev_from_dentry(dentry);

save_str_to_buf(&p.event->args_buf, dentry_path, 0);
save_to_submit_buf(&p.event->args_buf, &inode_nr, sizeof(unsigned long), 1);
save_to_submit_buf(&p.event->args_buf, &dev, sizeof(dev_t), 2);
save_to_submit_buf(&p.event->args_buf, &ctime, sizeof(u64), 3);
if (should_trace_inode_unlink) {
void *dentry_path = get_dentry_path_str(dentry);
unlinked_file_id.ctime = get_ctime_nanosec_from_dentry(dentry);

save_str_to_buf(&p.event->args_buf, dentry_path, 0);
save_to_submit_buf(&p.event->args_buf, &unlinked_file_id.inode, sizeof(unsigned long), 1);
save_to_submit_buf(&p.event->args_buf, &unlinked_file_id.device, sizeof(dev_t), 2);
save_to_submit_buf(&p.event->args_buf, &unlinked_file_id.ctime, sizeof(u64), 3);
ret = events_perf_submit(&p, SECURITY_INODE_UNLINK, 0);
}

if (should_capture_io) {
// We want to avoid reacquisition of the same inode-device affecting capture behavior
unlinked_file_id.ctime = 0;
bpf_map_delete_elem(&elf_files_map, &unlinked_file_id);
}

return events_perf_submit(&p, SECURITY_INODE_UNLINK, 0);
return ret;
}

SEC("kprobe/commit_creds")
Expand Down Expand Up @@ -2733,20 +2751,7 @@ submit_magic_write(program_data_t *p, file_info_t *file_info, io_data_t io_data,

save_str_to_buf(&(p->event->args_buf), file_info->pathname_p, 0);

if (io_data.is_buf) {
if (header_bytes < FILE_MAGIC_HDR_SIZE)
bpf_probe_read(header, header_bytes & FILE_MAGIC_MASK, io_data.ptr);
else
bpf_probe_read(header, FILE_MAGIC_HDR_SIZE, io_data.ptr);
} else {
struct iovec io_vec;
__builtin_memset(&io_vec, 0, sizeof(io_vec));
bpf_probe_read(&io_vec, sizeof(struct iovec), io_data.ptr);
if (header_bytes < FILE_MAGIC_HDR_SIZE)
bpf_probe_read(header, header_bytes & FILE_MAGIC_MASK, io_vec.iov_base);
else
bpf_probe_read(header, FILE_MAGIC_HDR_SIZE, io_vec.iov_base);
}
fill_file_header(header, io_data);

save_bytes_to_buf(&(p->event->args_buf), header, header_bytes, 1);
save_to_submit_buf(&(p->event->args_buf), &file_info->id.device, sizeof(dev_t), 2);
Expand Down Expand Up @@ -2855,10 +2860,16 @@ extract_vfs_ret_io_data(struct pt_regs *ctx, args_t *saved_args, io_data_t *io_d
}

// Filter capture of file writes according to path prefix, type and fd.
statfunc bool filter_file_write_capture(program_data_t *p, struct file *file)
statfunc bool
filter_file_write_capture(program_data_t *p, struct file *file, io_data_t io_data, off_t start_pos)
{
return filter_file_path(p->ctx, &file_write_path_filter, file) ||
filter_file_type(p->ctx, &file_type_filter, CAPTURE_WRITE_TYPE_FILTER_IDX, file) ||
filter_file_type(p->ctx,
&file_type_filter,
CAPTURE_WRITE_TYPE_FILTER_IDX,
file,
io_data,
start_pos) ||
filter_file_fd(p->ctx, &file_type_filter, CAPTURE_WRITE_TYPE_FILTER_IDX, file);
}

Expand Down Expand Up @@ -2889,8 +2900,15 @@ statfunc int capture_file_write(struct pt_regs *ctx, u32 event_id, bool is_buf)
extract_vfs_ret_io_data(ctx, &saved_args, &io_data, is_buf);
struct file *file = (struct file *) saved_args.args[0];
loff_t *pos = (loff_t *) saved_args.args[3];
size_t written_bytes = PT_REGS_RC(ctx);

off_t start_pos;
bpf_probe_read(&start_pos, sizeof(off_t), pos);
// Calculate write start offset
if (start_pos != 0)
start_pos -= written_bytes;

if (filter_file_write_capture(&p, file)) {
if (filter_file_write_capture(&p, file, io_data, start_pos)) {
// There is a filter, but no match
del_args(event_id);
return 0;
Expand All @@ -2916,10 +2934,12 @@ statfunc int capture_file_write(struct pt_regs *ctx, u32 event_id, bool is_buf)
}

// Filter capture of file reads according to path prefix, type and fd.
statfunc bool filter_file_read_capture(program_data_t *p, struct file *file)
statfunc bool
filter_file_read_capture(program_data_t *p, struct file *file, io_data_t io_data, off_t start_pos)
{
return filter_file_path(p->ctx, &file_read_path_filter, file) ||
filter_file_type(p->ctx, &file_type_filter, CAPTURE_READ_TYPE_FILTER_IDX, file) ||
filter_file_type(
p->ctx, &file_type_filter, CAPTURE_READ_TYPE_FILTER_IDX, file, io_data, start_pos) ||
filter_file_fd(p->ctx, &file_type_filter, CAPTURE_READ_TYPE_FILTER_IDX, file);
}

Expand All @@ -2946,8 +2966,15 @@ statfunc int capture_file_read(struct pt_regs *ctx, u32 event_id, bool is_buf)
extract_vfs_ret_io_data(ctx, &saved_args, &io_data, is_buf);
struct file *file = (struct file *) saved_args.args[0];
loff_t *pos = (loff_t *) saved_args.args[3];
size_t read_bytes = PT_REGS_RC(ctx);

off_t start_pos;
bpf_probe_read(&start_pos, sizeof(off_t), pos);
// Calculate write start offset
if (start_pos != 0)
start_pos -= read_bytes;

if (filter_file_read_capture(&p, file)) {
if (filter_file_read_capture(&p, file, io_data, start_pos)) {
// There is a filter, but no match
del_args(event_id);
return 0;
Expand Down
4 changes: 2 additions & 2 deletions pkg/ebpf/c/tracee.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ statfunc int submit_magic_write(program_data_t *, file_info_t *, io_data_t, u32)
statfunc bool should_submit_io_event(u32, program_data_t *);
statfunc int do_file_io_operation(struct pt_regs *, u32, u32, bool, bool);
statfunc void extract_vfs_ret_io_data(struct pt_regs *, args_t *, io_data_t *, bool);
statfunc bool filter_file_write_capture(program_data_t *, struct file *);
statfunc bool filter_file_write_capture(program_data_t *, struct file *, io_data_t, off_t);
statfunc int capture_file_write(struct pt_regs *, u32, bool);
statfunc bool filter_file_read_capture(program_data_t *, struct file *);
statfunc bool filter_file_read_capture(program_data_t *, struct file *, io_data_t, off_t);
statfunc int capture_file_read(struct pt_regs *, u32, bool);
statfunc struct pipe_buffer *get_last_write_pipe_buffer(struct pipe_inode_info *);

Expand Down
2 changes: 2 additions & 0 deletions pkg/events/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -9974,6 +9974,7 @@ var CoreEvents = map[ID]Definition{
{handle: probes.VfsWriteVRet, required: false},
{handle: probes.KernelWrite, required: false},
{handle: probes.KernelWriteRet, required: false},
{handle: probes.SecurityInodeUnlink, required: false}, // Used for ELF filter
},
tailCalls: []TailCall{
{"prog_array", "trace_ret_vfs_write_tail", []uint32{TailVfsWrite}},
Expand All @@ -9997,6 +9998,7 @@ var CoreEvents = map[ID]Definition{
{handle: probes.VfsReadRet, required: true},
{handle: probes.VfsReadV, required: false},
{handle: probes.VfsReadVRet, required: false},
{handle: probes.SecurityInodeUnlink, required: false}, // Used for ELF filter
},
tailCalls: []TailCall{
{"prog_array", "trace_ret_vfs_read_tail", []uint32{TailVfsRead}},
Expand Down

0 comments on commit fd47dfb

Please sign in to comment.