First commit

This commit is contained in:
geoffrey 2025-01-03 13:56:03 +01:00
commit 0c10e12608
10 changed files with 150640 additions and 0 deletions

2
.gitignore vendored Normal file

@ -0,0 +1,2 @@
src/**.o
src/ssh_trace

15
Makefile Normal file

@ -0,0 +1,15 @@
GCC=gcc
CL=clang-11
CFLAGS=-Wall
LIBS=-lbpf
all: ssh-trace.ebpf.o ssh-trace
ssh-trace.ebpf.o: src/ssh-trace.ebpf.c
$(CL) -g -O2 -target bpf -c src/ssh-trace.ebpf.c -o src/ssh-trace.ebpf.o
ssh-trace: src/load_bpf.c
$(GCC) $(CFLAGS) src/load_bpf.c -o ssh-trace $(LIBS)
clean:
rm -rf src/*.o && rm ssh-trace

54
README.md Normal file

@ -0,0 +1,54 @@
# Introduction
Nowadays, with the increase of numbers of servers in an infrastructure, it's important to trace all users activities for investigating when a suspicious activity has been detected. This project is borned for resolving that issue, which trace all user connected through SSH and the outcome is print to the stdout or to a file in rsyslog format.
The program detect all commands executed in the system from a user connected and the result is print into the terminal, the program has an advantage for detection any privilege escalations when the user switch to another one, and the program show to us the initial user connected with the username and the user who executed the command. The diagram below show us an example:
![Example](example.png)
# Installation
# Supported platforms
The program is based on [eBPF](https://ebpf.io/). It's a technology for developping a program which is loaded into the Kernel for security, networking and tracing all event in the kernel. This program has been tested on these systems:
| System | Architecture | Version | Kernel version |
|--------|--------------|---------|----------------|
| Ubuntu | x64| 20.04| 5.15.0|
| Debian | x64| 11| 5.10.0|
## Requirements
The program is based on eBPF and developped in C language. You should install these packages if you want to generate the binary:
* bpftool
* clang-11
* libbpf-dev
* gcc gcc-multilib
# Usage
After you clone the project, you can move into it. The arboresence of the project is quite simple. You have the repository `src/` which contains all C sources and headers files and you have the Makefile for generating the binary with the command `make all`:
```
$ git clone https://gitea.bucchino.org/gbucchino/ssh-trace
$ cd ssh-trace
$ make all
```
That will generate the binary `ssh-trace` and you can execute it:
```
$ sudo ./ssh-trace
```
By default, the result is print into the stdout, but, you can export it to rsyslog file format with the parameter -f:
```
$ sudo ./ssh-trace -f ssh-trace_`$(echo date '+%F')`.log
$ cat ssh-trace*.log
$ sudo ./ssh-trace -f logs-ssh
Jan 03 12:21:33 ubuntu ssh-trace: <info> host=user@192.168.1.37;ppid=8516;pathname=/usr/bin/ls --color=auto -la /home/user;pid=9112
Jan 03 12:21:35 ubuntu ssh-trace: <info> host=user@192.168.1.37;ppid=8516;pathname=/usr/sbin/ip address show;pid=9113
Jan 03 12:21:37 ubuntu ssh-trace: <info> host=user@192.168.1.37;ppid=8516;pathname=/usr/bin/cat /etc/group;pid=9114
```
If you want to read more about the project, you should go to my [blog](https://www.bucchino.org/projects/sshtrace/), I made an article regarding it. Enjoy the read :).
# References
* https://developers.redhat.com/articles/2023/10/19/ebpf-application-development-beyond-basics#an_example_c_application_using_libbpf

BIN
example.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

8
exec.sh Executable file

@ -0,0 +1,8 @@
#!/usr/bin/sh
# clang-11 -Wall -g -O2 -target bpf -c src/ssh-trace.ebpf.c -o src/ssh-trace.ebpf.o && \
# bpftool gen skeleton src/ssh-trace.ebpf.o name sshtrace > src/skel.ebpf.h
# gcc -Wall src/load_bpf.c -o src/ssh-trace -lbpf && \
make clean
make all && sudo ./ssh-trace

28
src/common.h Normal file

@ -0,0 +1,28 @@
#ifndef H_COMMON
#define H_COMMON
#define FILENAME_SIZE 128
#define COMM_LEN 24
#define ARGS_CNT 20
#define ARGS_LEN 128
#define ARGS_TLEN (ARGS_CNT * ARGS_LEN)
struct execve {
pid_t pid;
pid_t ppid;
char filename[FILENAME_SIZE];
};
struct event{
pid_t pid;
pid_t ppid;
uid_t uid;
char filename[FILENAME_SIZE];
char comm[COMM_LEN];
int argc;
int tlen;
char args[ARGS_TLEN];
};
#endif

387
src/load_bpf.c Normal file

@ -0,0 +1,387 @@
#include <stdio.h>
#include <stdlib.h>
#include <linux/bpf.h>
#include <bpf/bpf.h>
#include <bpf/libbpf.h>
#include <linux/perf_event.h>
#include <argp.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <string.h>
#include <utmp.h>
#include <signal.h>
#include <time.h>
#include <pwd.h>
#include <unistd.h>
#include <fcntl.h>
// #include "skel.ebpf.h"
#include "common.h"
static int fd_map_execve;
static int fd_map_args;
static struct arguments arguments;
static FILE *f;
static int running = 1;
static char hostname[127];
struct arguments {
char *filename;
int to_output;
};
/*
* Functions for arguments
* https://www.gnu.org/software/libc/manual/html_node/Argp-Example-3.html
*/
static char doc[] = "SSH Trace usage:";
static char args_doc[] = "ARG1 ARG2";
static error_t parse_opts(int key, char *arg, struct argp_state *state){
struct arguments *arguments = state->input;
switch(key){
case 'f':
arguments->filename = arg;
arguments->to_output = 0;
break;
case 'o':
arguments->to_output = 1;
break;
case ARGP_KEY_ARG:
break;
case ARGP_KEY_END:
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
}
struct arguments parse_args(int argc, char *argv[]){
static const struct argp_option opts[] = {
{"filename", 'f', "FILENAME", 0, "Save result to logs"},
{"to-output", 'o', NULL, 0, "Print to output"},
{NULL, 'h', NULL, OPTION_HIDDEN, "help"},
{},
};
struct arguments arguments;
arguments.filename = NULL;
arguments.to_output = 1;
static struct argp argp = {opts, parse_opts, args_doc, doc};
argp_parse(&argp, argc, argv, 0, 0, &arguments);
return arguments;
}
/* End functions arguments */
static void signalHandler(int signum){
running = 0;
}
/*
* This function get the username from the uid
*/
static void get_username(uid_t uid, char *username){
struct passwd *p;
p = getpwuid(uid);
if (p == NULL){
printf("Failed to get the username from the UID\n");
strncpy(username, "Unknown", 8);
return;
}
strncpy(username, p->pw_name, 64);
}
/*
* With the pid, walk through the process tree and to find the sshd proc
*/
static pid_t walk_process_tree(const pid_t pid){
char filename[64];
FILE *fd;
int ppid;
char proc_name[128];
snprintf(filename, 64, "/proc/%d/stat", pid);
if ((fd = fopen(filename, "r")) == NULL){
printf("Failed to open the file\n");
return -1;
}
// 1866044 (sshd) S 1 1866044
fscanf(fd, "%*d %s %*c %d", proc_name, &ppid);
if (strncmp(proc_name, "(sshd)", 128) == 0){
fclose(fd);
return ppid;
}
// If parent processes
if (ppid == 0 || ppid == 1){
fclose(fd);
return 0;
}
// printf("PID: %d; PPID: %d; Proc name: %s\n", pid, ppid, proc_name);
fclose(fd);
return walk_process_tree(ppid);
}
/*
* We can find the user attached to the pid
*/
static struct utmp find_user_from_pid(pid_t pid){
int fd;
size_t len;
struct utmp u = {0};
if ((fd = open("/var/run/utmp", O_RDONLY)) < 0){
printf("Failed to open the file\n");
return u;
}
lseek(fd, 0, SEEK_SET);
while ((len = read(fd, &u, sizeof(struct utmp))) > 0){
if (u.ut_type == USER_PROCESS && u.ut_pid == pid)
break;
memset(&u, 0, sizeof(struct utmp));
}
close(fd);
return u;
}
/*
* Try to find the sshd forked in the map
*/
static struct execve find_sshd_daemon(pid_t pid){
struct execve s_execve = {0};
// Get the execve struct
int err = bpf_map_lookup_elem(fd_map_execve, &pid, &s_execve);
if (err != 0)
return s_execve;
if(strncmp(s_execve.filename, "sshd", 4) == 0)
return s_execve;
else
return find_sshd_daemon(s_execve.ppid);
}
/*
* This function get the localtime into the rsyslog format
*/
static int syslog_time(time_t ts, char t[32], size_t l){
const char format[] = "%b %d %T";
struct tm *lt = localtime(&ts);
if(strftime(t, l, format, lt) == 0)
return -1;
return 0;
}
int handle_event(void *ctx, void *data, size_t data_sz){
struct event *s_event = (struct event *)data;
struct execve s_execveParent = {0};
struct utmp u = {0};
time_t ts = time(NULL);
char username[64];
// Fid the sshd process parent
s_execveParent = find_sshd_daemon(s_event->ppid);
if (s_execveParent.pid == 0){
//printf("SSH daemon not exist\t%s\t%d\n", s_execve.filename, s_execve.pid);
s_execveParent.ppid = walk_process_tree(s_event->pid); /* Walk through process tree to find the sshd daemon */
if (s_execveParent.ppid <= 0) // We didn't find it
return 0;
}
// Find the user related
u = find_user_from_pid(s_execveParent.ppid);
if (u.ut_type == 0)
return 0;
// Get the username
get_username(s_event->uid, username);
if (arguments.to_output == 1){
char t[32];
syslog_time(ts, t, sizeof(t));
printf("%-20s%-10d%s@%-25s", t, s_execveParent.ppid, u.ut_user, u.ut_host);
printf("%-10s%-10d", username, s_event->pid);
printf("%-20s", s_event->filename);
printf("%s ", s_event->comm);
// Arguments
if (s_event->argc > 0){
for(int i = 0; i < s_event->tlen; i++){
char c = s_event->args[i];
if (c == '\0')
printf(" ");
else
printf("%c", c);
}
}
printf("\n");
}
if (arguments.filename != NULL && f != NULL){
/*
* Rsyslog format
* <time> <hostname> <procname>: <info> <data>
*/
char t[32];
char host[294];
char ppid[16];
char command[ARGS_LEN * ARGS_CNT];
char pid[16];
char hname[128];
if (syslog_time(ts, t, sizeof(t)) == 0)
fwrite(t, strlen(t), 1, f);
snprintf(hname, 128, " %s", hostname);
fwrite(hname, strlen(hname), 1, f);
fwrite(" ssh-trace: ", 12, 1, f);
fwrite("<info> ", 7, 1, f);
snprintf(host, 294, "host=%s@%s;", u.ut_user, u.ut_host);
fwrite(host, strlen(host), 1, f);
snprintf(ppid, 16, "ppid=%d;", s_execveParent.ppid);
fwrite(ppid, strlen(ppid), 1, f);
snprintf(command, ARGS_LEN * ARGS_CNT, "pathname=%s ", s_event->filename);
if (s_event->argc > 0){
char output[ARGS_TLEN];
int i;
for(i = 0; i < s_event->tlen; i++){
char c = s_event->args[i];
if (c == '\0')
output[i] = ' ';
else
output[i] = c;
}
output[i] = '\0';
strncat(command, output, strlen(output));
}
command[strlen(command) - 1] = ';';
fwrite(command, strlen(command), 1, f);
snprintf(pid, 16, "pid=%d\n", s_event->pid);
fwrite(pid, strlen(pid), 1, f);
}
return 0;
}
int main(int argc, char *argv[]){
const char *fileObj = "src/ssh-trace.ebpf.o";
struct bpf_object *obj;
struct bpf_program *programProcessFork;
struct bpf_program *programExecve;
struct bpf_program *programExitExecve;
struct ring_buffer *rb;
// struct sshtrace *skel;
int err;
int fd_map_data;
arguments = parse_args(argc, argv); // Parsing arguments
signal(SIGINT, signalHandler);
/* Generate our skel for the ring buffer */
/*skel = sshtrace__open();
if (!skel){
printf("Failed to init skel\n");
return -1;
}*/
/* Open and load our eBPF object */
obj = bpf_object__open_file(fileObj, NULL);
if (!obj){
printf("Failed to open the file\n");
return -1;
}
err = bpf_object__load(obj);
if (err){
printf("Failed to load object\n");
return -1;
}
/* Retrieving fd of maps */
fd_map_data = bpf_object__find_map_fd_by_name(obj, "data");
if (!fd_map_data){
printf("Failed to find the fd map data\n");
bpf_object__close(obj);
return -1;
}
fd_map_execve = bpf_object__find_map_fd_by_name(obj, "execve_maps");
if (!fd_map_execve){
printf("Failed to find the fd map execve\n");
bpf_object__close(obj);
return -1;
}
fd_map_args = bpf_object__find_map_fd_by_name(obj, "execve_args");
if (!fd_map_args){
printf("Failed to find the fd map args\n");
bpf_object__close(obj);
return -1;
}
/* Retrieving our programs */
programExecve = bpf_object__find_program_by_name(obj, "syscall_execve");
programExitExecve = bpf_object__find_program_by_name(obj, "syscall_exit_execve");
programProcessFork = bpf_object__find_program_by_name(obj, "syscall_process_fork");
if (!programExitExecve || !programExecve || !programProcessFork){
printf("Failed to find program\n");
bpf_object__close(obj);
return -1;
}
bpf_program__attach(programProcessFork);
bpf_program__attach(programExecve);
bpf_program__attach(programExitExecve);
/* Start the ringbuffer */
rb = ring_buffer__new(fd_map_data, handle_event, NULL, NULL);
if (!rb){
printf("Failed to create the ringbuf\n");
bpf_object__close(obj);
return -1;
}
if (arguments.to_output == 1)
printf("%-20s%-10s%-30s%-10s%-10s%-20s%s\n", "Datetime", "PID sshd", "Username@IP Address", "Username", "Pid", "Pathname", "Command");
if (arguments.filename != NULL){
f = fopen(arguments.filename, "a");
if (f == NULL){
printf("Failed to create the file %s\n", arguments.filename);
return 0;
}
}
/* Get the hostname */
if (gethostname(hostname, 127) == -1){
printf("Failed to get the hostname\n");
strncpy(hostname, "ubuntu", 7);
}
while(running){
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
if (err == -EINTR){
printf("Failed to get the ringbuf\n");
running = 0;
break;
}
}
if (f != NULL)
fclose(f);
ring_buffer__free(rb);
bpf_object__close(obj);
return 0;
}

204
src/ssh-trace.ebpf.c Normal file

@ -0,0 +1,204 @@
#define BPF_NO_GLOBAL_DATA
#define __TARGET_ARCH_x86
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_endian.h>
#include <string.h>
#include "common.h"
/*
* Helper:
* Issue: invalid indirect read from stack R2 off
* Fix: check if all variables is initialised
* Issue: R1 invalid mem access 'inv'
* Fix: the value can be NULL
*/
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024 /* 256kb */);
} data SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 32768); // pid_max -> https://linux.die.net/man/5/proc
__type(key, pid_t);
__type(value, struct execve);
} execve_maps SEC(".maps");
struct ctx_execve {
__u16 common_type; // Unsigned short
__u8 common_flags; // Unsigned char
__u8 common_preempt_count; // Unsigned char
__s32 common_pid; // int
__s32 syscall_nr; // int
const __u8 *filename; // unsigned char
const __u8 *const *argv;
const __u8 *const *envp;
};
struct ctx_fork {
__u16 common_type; // Unsigned short
__u8 common_flags; // Unsigned char
__u8 common_preempt_count; // Unsigned char
__s32 common_pid;
char parent_comm[16];
pid_t parent_pid;
char child_comm[16];
pid_t child_pid;
};
/*
* Made my own strncmp, because the function bpf_strncmp not available
*/
static int b_strncmp(const char *s1, __s32 len, const char *s2){
__s32 c = 0;
__s32 i;
for (i = 0; i < len; i++){
//while (c != '\0' || i != len){
//c = s1[i];
if (s1[i] == '\0')
break;
if (s1[i] == s2[i])
c++;
}
return (c == i) ? 0: 1;
}
/*
* Need to detect the sshd forked, because, when a new user is connected
* we cannot detect the execve sshd, but only the fork
* Execve of the sshd is executed when the user is connected
* But, if the user is authenticated, the process is forked
*/
SEC("tp/sched/sched_process_fork")
int syscall_process_fork(struct ctx_fork *ctx){
pid_t ppid;
char child[16];
pid_t pid = bpf_get_current_pid_tgid() >> 32;
struct task_struct *t = (struct task_struct*)bpf_get_current_task();
struct task_struct *real_parent;
bpf_probe_read(&real_parent, sizeof(struct task_struct *), &t->real_parent);
bpf_probe_read(&ppid, sizeof(pid_t), &real_parent->pid);
bpf_probe_read(&child, 16, &ctx->child_comm);
if (b_strncmp(child, 16, "sshd") == 0){
struct execve s_execve = {0};
s_execve.pid = pid;
s_execve.ppid = ppid;
bpf_probe_read(&s_execve.filename, sizeof(s_execve.filename), ctx->child_comm);
// bpf_printk("Fork: %d %d", s_execve.pid, s_execve.ppid);
if (bpf_map_update_elem(&execve_maps, &pid, &s_execve, BPF_ANY) < 0){
bpf_printk("Failed to update the map");
return 0;
}
}
return 0;
}
SEC("tp/syscalls/sys_exit_execve")
int syscall_exit_execve(void) {
pid_t ppid;
struct task_struct *t = (struct task_struct*)bpf_get_current_task();
struct task_struct *real_parent;
bpf_probe_read(&real_parent, sizeof(struct task_struct *), &t->real_parent);
bpf_probe_read(&ppid, sizeof(pid_t), &real_parent->tgid);
// bpf_printk("Exit execve: %d %d", pid, ppid);
return 0;
}
static pid_t walk_process_tree(struct task_struct *parent, pid_t pid){
struct task_struct *r;
pid_t p;
if(bpf_probe_read(&r, sizeof(struct task_struct *), &parent->real_parent) < 0)
return 0;
if(bpf_probe_read(&p, sizeof(pid_t), &r->tgid) < 0)
return 0;
return p;
}
static int execve(struct ctx_execve *ctx) {
struct event *s_event;
__u32 tlen = 0;
const char *argv;
// kuid_t kuid; /* https://elixir.bootlin.com/linux/v6.11.8/source/include/linux/uidgid_types.h#L9 */
s_event = bpf_ringbuf_reserve(&data, sizeof(*s_event), 0);
if (!s_event)
return 0;
s_event->pid = bpf_get_current_pid_tgid();
s_event->ppid = 0;
s_event->uid = bpf_get_current_uid_gid();
bpf_probe_read_user_str(&s_event->filename, sizeof(s_event->filename), ctx->filename);
// Arguments
s_event->argc = 0;
s_event->tlen = 0;
// Take the command executed
long err = bpf_probe_read_user_str(&argv, sizeof(argv), &ctx->argv[0]);
if (err < 0){
bpf_printk("Failed to get the command");
}
else{
__s32 len = bpf_probe_read_user_str(&s_event->comm, COMM_LEN, argv);
if (len > COMM_LEN)
bpf_printk("Cannot get the command");
}
// Start at one, because 0 is the command
for (__s32 i = 1; i < ARGS_CNT; i++){
err = bpf_probe_read_user_str(&argv, sizeof(argv), &ctx->argv[i]);
if (err < 0){
//bpf_ringbuf_discard(s_event, 0);
bpf_printk("Failed to read args");
break;
}
/*
* To avoid error:
* Unbounded memory access, make sure to bounds check any such access
*/
if (tlen > (ARGS_TLEN - ARGS_LEN)){
//bpf_ringbuf_discard(s_event, 0);
bpf_printk("Max arguments exceeded: %d %d", i, tlen);
break;
}
__u32 len = bpf_probe_read_user_str(&s_event->args[tlen], ARGS_LEN, argv);
if (len > ARGS_LEN){
break;
}
tlen += len;
s_event->argc++;
}
s_event->tlen = tlen;
// Get ppid
struct task_struct *t = (struct task_struct*)bpf_get_current_task();
s_event->ppid = walk_process_tree(t, s_event->pid);
/*if (bpf_ringbuf_output(&data, &s_event, sizeof(struct event), 0) != 0){
bpf_printk("Failed to push into the ringbuf");
return 0;
}*/
bpf_ringbuf_submit(s_event, 0);
return 0;
}
SEC("tp/syscalls/sys_enter_execve")
int syscall_execve(struct ctx_execve *ctx) {
return execve(ctx);
}
/*SEC("tp/syscalls/sys_enter_mmap")
int syscall_mmap(void){
bpf_printk("mmap detected");
return 0;
}*/
char LICENSE[] SEC("license") = "GPL";

149942
src/vmlinux.h Normal file

File diff suppressed because it is too large Load Diff

BIN
ssh-trace Executable file

Binary file not shown.