咱们先聊个实际场景服务器上突然出现可疑命令执行比如有人偷偷查看系统文件你查进程只能看到命令本身却不知道是谁通过SSH登录后操作的——是远程的黑客还是内部人员如果能把每一个执行的进程都对应到发起SSH连接的客户端IP、端口和用户排查问题就会轻松很多。而这就是eBPF代理的核心作用给SSH相关的进程“打标签”无论经过多少层操作都能追溯到最初的SSH客户端来源。一、先搞懂这个eBPF代理到底是做什么的简单说这是一个运行在Linux系统上的“监控小工具”基于eBPF技术实现专门盯着通过SSH登录到服务器的用户。只要有用户通过SSH连接上来这个工具就会记录下他的客户端IP、端口和用户名然后跟着这个用户的所有操作——比如执行命令、切换用户sudo su、打开新shell把这些操作和最初的SSH客户端信息绑定在一起。哪怕用户玩点“小花样”比如从本地再SSH到本地多层localhost连接、切换用户隐藏身份这个工具也能精准找到最初的“源头”把每一条命令和对应的SSH客户端IP对应起来生成日志方便排查。举个例子用户A从IP为192.168.85.129的电脑通过端口50642 SSH登录到服务器执行了ls /home、cat /etc/passwd命令之后又用sudo su切换到root执行了cat /etc/shadow。这个eBPF代理会记录下所有这些命令并且都标注上“来源IP192.168.85.129端口50642原始用户A”不会因为切换用户、打开新shell就丢失溯源信息。它的核心价值就是减少服务器安全取证的手动工作量遇到可疑操作时不用一个个查进程、查连接直接看日志就能定位到是谁通过SSH操作的提升排查效率。二、核心知识点铺垫这些基础你得懂在讲设计和代码之前先梳理几个关键知识点都是这个工具的“底层支撑”大白话讲明白不用记专业术语理解意思就行1. eBPFLinux内核里的“万能监控器”eBPF是Linux内核自带的一项技术简单说就是“可以在 kernel 里运行小程序不用修改内核代码、不用重启服务器”。它能实时监控系统里的各种操作——比如进程执行、网络连接、系统调用而且效率极高不会给服务器带来太大负担。咱们这个代理就是利用eBPF的特性在系统内核层面“监听”SSH相关的操作不用侵入SSH本身的代码就能拿到需要的客户端信息。2. 几个关键的系统调用工具的“信息来源”这个代理能拿到SSH客户端信息全靠两个核心系统调用咱们不用懂具体实现知道它们的作用就行getpeername获取“对方”的IP和端口——比如SSH客户端连接服务器时服务器通过这个调用就能拿到客户端的IP和端口。getsockname获取“自己”的IP和端口——补充获取服务器这边的连接信息配合getpeername就能完整拿到SSH连接的两端信息。execve进程执行的“触发信号”——只要有命令执行比如ls、cat都会调用这个系统调用代理就是通过监听这个调用来记录每一个执行的进程。3. BPF Map临时“存信息”的容器BPF Map是eBPF里的一种“数据结构”相当于一个临时的“数据库”用来存关键信息——比如进程IDPID对应SSH客户端IP、进程ID对应用户名。代理在监听过程中会把拿到的信息存到这里后续需要追溯时直接从这里查就行。4. 进程树追溯解决“多层操作”的溯源问题用户通过SSH登录后可能会切换用户、打开新shell每一步都会产生新的进程这些进程的父进程PPID会关联起来形成一个“进程树”。代理会顺着这个进程树往上找直到找到SSH相关的进程sshd进程从而拿到最初的客户端信息——哪怕经过多层切换也能精准溯源。三、设计思路这个代理是怎么“想”出来的设计这个eBPF代理的核心思路其实很简单“监听关键操作 → 存关键信息 → 追溯进程源头 → 生成日志”咱们分步骤拆解结合实际场景讲一看就懂第一步明确核心需求——要解决什么问题痛点很明显传统的SSH日志只能记录谁登录了、登录时间却没法把“登录后的操作”和“客户端IP”绑定。比如有人登录后执行了可疑命令你只能看到命令和执行进程却不知道这个进程是谁发起的、来自哪个IP。所以核心需求是把每一个通过SSH发起的进程都和最初的SSH客户端IP、端口、用户名绑定无论经过多少层操作切换用户、多层SSH、打开新shell都能追溯到源头。第二步确定监控对象——盯着哪些操作既然要绑定SSH和进程就必须盯着三个关键操作这也是代码里最核心的监控点SSH连接建立时通过getpeername和getsockname拿到客户端IP、端口以及服务器的连接信息存到BPF Map里关联当前的进程IDPID。用户执行命令时通过execve系统调用捕捉到进程执行事件然后顺着进程树往上找找到对应的SSH进程从BPF Map里取出客户端信息。特殊场景处理比如用户从本地SSH到本地localhost、切换用户sudo su、打开新shell这些操作会产生新的进程需要特殊处理确保客户端信息不丢失。第三步设计流程——从监听 to 日志完整链路这里给大家画一个简单的流程原理图流程原理图1. SSH客户端 → 连接服务器触发getpeername/getsockname→ 代理监听这两个调用拿到客户端IP、端口 → 存到BPF MapPID → IP/端口/用户名2. 用户执行命令触发execve→ 代理监听execve拿到当前进程IDPID和父进程IDPPID3. 代理顺着PPID往上找进程树直到找到sshd进程SSH相关进程4. 根据sshd进程的PID从BPF Map里取出对应的客户端IP、端口、原始用户名5. 把进程信息PID、PPID、命令和客户端信息绑定写入日志/var/log/sshtrace.log同时支持实时打印日志6. 无论用户切换用户、打开新shell重复步骤2-5确保客户端信息始终绑定第四步考虑异常场景——避免溯源失败设计时特意考虑了4种常见场景避免出现“溯源断档”远程SSH连接最常见的场景直接通过getpeername拿到客户端IP正常溯源。本地多层SSH比如用户先SSH到localhost再执行命令此时IP会显示127.0.0.1代理会通过端口关联找到最初的远程客户端IP。权限提升用户用sudo su切换到root进程ID会变化代理通过进程树追溯依然能找到最初的SSH客户端信息。打开新shell用户执行/bin/bash打开新shell新shell的进程父进程是SSH相关进程代理会继承客户端信息不会丢失。四、代码实现原理不用懂代码也能看懂核心逻辑constcharargp_program_doc[]Trace ssh session spawned execve syscall\n\nUSAGE: sudo ./sshtrace [-a] [-p] [-v] [-w] [-h]\nEXAMPLES:\n ./sshtrace # trace all ssh-spawned execve syscall\n ./sshtrace -a # trace all execve syscalls\n ./sshtrace -p # printf all logs\n ./sshtrace -v # verbose events\n ./sshtrace -w # verbose warnings\n ./sshtrace -h # show help\n;staticconststructargp_optionopts[]{{all,a,NULL,0,trace all execve syscall},{print,p,NULL,0,printf all logs},{verbose,v,NULL,0,verbose debugging},{warning,w,NULL,0,verbose warnings},{max-args,MAX_ARGS_KEY,MAX_ARGS,0,max number of arg param logged, defaults to 20},{NULL,h,NULL,OPTION_HIDDEN,Show the full help},{},};...structdata_t{pid_t pid;uid_t uid;pid_t ppid;charcommand[TASK_COMM_LEN];intret;structsockaddr_in6addr;inttype_id;// 0:others 1:getpeername 2:getsockname 3:execve};structevent{pid_t pid;pid_t ppid;uid_t uid;intretval;intargs_count;unsignedintargs_size;charcomm[TASK_COMM_LEN];charargs[FULL_MAX_ARGS_ARR];};...staticintparse_arg(intkey,char*arg,structargp_state*state){longintmax_args;switch(key){casep:envVar.printtrue;break;casev:envVar.verbosetrue;logLevelLOG_TRACE;break;casea:envVar.alltrue;break;casew:envVar.warningtrue;logLevelLOG_DEBUG;break;caseh:argp_state_help(state,stderr,ARGP_HELP_STD_HELP);break;caseMAX_ARGS_KEY:errno0;max_argsstrtol(arg,NULL,10);if(errno||max_args1||max_argsTOTAL_MAX_ARGS){fprintf(stderr,Invalid MAX_ARGS %s, should be in [1, %d] range\n,arg,TOTAL_MAX_ARGS);argp_usage(state);}envVar.max_argsmax_args;break;}return0;}intmain(intargc,char**argv){staticconststructargpargp{.optionsopts,.parserparse_arg,.docargp_program_doc,};intargErrargp_parse(argp,argc,argv,0,NULL,NULL);if(argErr)returnargErr;log_info(%s,Starting program...);log_set_level(logLevel);if(envVar.print){printf(%-24s %-6s %-6s %-6s %-16s %-16s %-16s %-16s %-16s %-6s\n,Timestamp,PID,PPID,UID,Current User,Origin User,Command,IP Address,Port,Command Args);}fpfopen(/var/log/sshtrace.log,a);// open fileif(fpNULL){log_info(Log file could not be created or opened);return-1;}log_trace(%s,Setting LIBBPF options);libbpf_set_print(libbpf_print_fn);charlog_buf[128*1024];LIBBPF_OPTS(bpf_object_open_opts,opts,.kernel_log_buflog_buf,.kernel_log_sizesizeof(log_buf),.kernel_log_level1,);log_trace(%s,Opening BPF skeleton object);structsshtrace_bpf*skelsshtrace_bpf__open_opts(opts);if(!skel){log_trace(%s,Error while opening BPF skeleton object);returnEXIT_FAILURE;}interr0;log_trace(%s,Loading BPF skeleton object);errsshtrace_bpf__load(skel);// Print the verifier log/* for (int i0; i 10000; i) { if (log_buf[i] 0 log_buf[i1] 0) { break; } printf(%c, log_buf[i]); } */if(err){log_trace(%s,Error while loading BPF skeleton object);gotocleanup;}log_trace(%s,Attaching BPF skeleton object);errsshtrace_bpf__attach(skel);if(err){log_trace(%s,Error while attaching BPF skeleton object);gotocleanup;}log_trace(%s,Initializing perf buffer);structperf_buffer*pbperf_buffer__new(bpf_map__fd(skel-maps.output),8,handle_event,lost_event,NULL,NULL);if(!pb){log_trace(%s,Error while initializing perf buffer);gotocleanup;}log_trace(Setting up interrupt signal handler);signal(SIGINT,intHandler);log_trace(%s,Start polling for BPF events...);while(!intSignal){errperf_buffer__poll(pb,100/* timeout, ms */);}log_trace(%s,Freeing perf buffer);perf_buffer__free(pb);gotocleanup;cleanup:log_trace(%s,Closing File);fclose(fp);log_trace(%s,Entering cleanup);sshtrace_bpf__destroy(skel);log_trace(%s,Finished cleanup);returnEXIT_SUCCESS;}If you need the complete source code, please add the WeChat number (c17865354792)提供的代码是用C语言写的结合了eBPF相关的库libbpf等核心逻辑和咱们上面讲的设计流程完全对应。咱们不用逐行看代码重点拆解几个核心模块大白话讲明白它们的作用1. 初始化模块准备工作代码开头会做一些准备工作比如解析命令行参数比如-p表示实时打印日志、-a表示监控所有进程、-v表示详细日志、初始化日志文件/var/log/sshtrace.log、加载eBPF程序skeleton对象相当于eBPF程序的“容器”、设置信号处理比如按CtrlC退出时清理资源。简单说这一步就是“启动工具做好所有准备工作”确保后续能正常监听和记录。2. 核心监听模块handle_event函数最关键这个函数是整个代理的“核心大脑”所有监听和溯源逻辑都在这里咱们拆解它的核心操作首先拿到当前事件的类型是getpeername、getsockname还是execve代码里用1、2、3区分。如果是getpeername/getsockname调用ipHelper函数把获取到的IP和端口转换成人类能看懂的格式比如把二进制的IP转换成192.168.85.129然后存到BPF Map里关联当前的进程ID。如果是execve有命令执行这是最核心的逻辑——顺着当前进程的父进程PPID往上找直到找到sshd进程通过进程命令判断比如命令里包含sshd找到后从BPF Map里取出对应的客户端IP、端口和原始用户名再获取当前执行命令的用户、命令参数把所有信息绑定写入日志如果开启了-p参数就实时打印出来。3. 辅助函数帮核心模块“干活”代码里还有几个辅助函数相当于“工具人”帮核心模块完成具体操作比如ipHelper把内核里的IP地址格式二进制转换成咱们熟悉的字符串格式比如192.168.85.129支持IPv4和IPv6。getUser通过用户IDUID获取对应的用户名比如UID为1000对应用户名guac如果查不到就返回“n/a”。getPPID通过进程IDPID获取它的父进程IDPPID用于追溯进程树。getCommand通过进程IDPID获取进程执行的命令比如ls、cat。4. 日志模块记录溯源信息代码会把每一次进程执行的信息以JSON格式写入/var/log/sshtrace.log文件日志里包含时间戳、进程IDPID、父进程IDPPID、用户IDUID、当前用户、原始用户SSH登录用户、执行命令、客户端IP、端口、命令参数。这样一来后续排查问题时只要打开这个日志文件就能清晰看到每一条命令对应的SSH客户端信息实现“一键溯源”。五、基础测试1只追踪SSH并且屏幕打印sudo./sshtrace-p作用只抓SSH 登录后执行的命令控制台实时输出日志写入/var/log/sshtrace.log2全开模式抓所有进程 打印 详细信息sudo./sshtrace-a-p-v作用-a监控所有执行的命令不管是不是SSH-p屏幕打印-v显示调试信息3限定最多记录10个命令参数sudo./sshtrace-p--max-args104查看帮助验证参数是否生效sudo./sshtrace-h六、怎么测试它真的在工作步骤 1运行工具sudo./sshtrace-p步骤 2新开一个终端SSH 登录本机sshlocalhost或者从别的机器 SSH 过来。步骤 3在 SSH 里随便执行命令lswhoamipwdcat/etc/hosts步骤 4回到 sshtrace 界面你会立刻看到类似输出Timestamp PID PPID UID Current User Origin User Command IP Address Apr 06 10:00 1234 1122 1000 ubuntu ubuntu ls 127.0.0.1这就说明完全运行成功日志在哪里所有记录自动保存tail-f/var/log/sshtrace.log七、涉及的相关领域知识点总结这个eBPF代理看似简单其实融合了多个领域的知识点也是Linux系统监控、安全取证的常用技术组合总结一下核心领域方便大家拓展学习1. eBPF技术领域核心是eBPF的内核编程、BPF Map的使用、eBPF程序的加载和挂载通过libbpf库。eBPF现在是Linux系统监控、性能优化、安全审计的核心技术除了进程溯源还能用于网络监控、容器监控等场景。2. Linux系统调用领域涉及getpeername、getsockname、execve等系统调用的作用和使用场景这些是Linux系统编程的基础也是监控进程、网络连接的核心手段。3. Linux进程管理领域核心是进程树的概念、PID和PPID的关联以及如何通过/proc目录比如/proc/PID/stat、/proc/PID/status获取进程信息——代码里的getPPID、getCommand、getUID函数都是通过读取这些文件实现的。4. 安全取证领域这个代理本质上是一个安全取证工具核心需求是“行为溯源”——在服务器被入侵、出现可疑操作时通过日志快速定位攻击来源SSH客户端IP这也是企业服务器安全运维的重要需求。5. C语言与Linux内核编程领域代码用C语言编写结合了Linux内核编程的相关知识比如内核日志、信号处理、文件操作同时使用了libbpf等eBPF相关库是典型的“用户态内核态”结合的编程模式。总结总结下来这个eBPF代理的核心价值就是“简单、高效、精准”——不用修改内核代码不用重启服务器就能实现SSH进程的全链路溯源解决了传统SSH日志无法关联进程和客户端IP的痛点。它的主要应用场景的就是Linux服务器的安全运维和取证比如服务器出现可疑命令执行、文件被篡改运维人员可以通过这个代理的日志快速定位到是哪个IP、哪个用户通过SSH操作的为后续的排查和处置节省大量时间。而且它的设计思路很有参考意义——利用eBPF监听系统调用结合进程树追溯和BPF Map存储实现“操作-源头”的绑定这种思路也可以用到其他场景比如容器进程溯源、网络连接溯源等。Welcome to follow WeChat official account【程序猿编码】