├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── VERSION ├── bpf └── snoopy.c ├── go.mod ├── go.sum ├── internal └── util │ └── util.go ├── main.go └── pkg └── tls_trace ├── bpf.go └── tls.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Travis Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHONY: run 2 | 3 | run: 4 | sudo go run main.go -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snoopy 2 | 3 | ## Overview 4 | 5 | Snoopy is a tool for tracing and monitoring SSL/TLS connections in applications that use common SSL libraries. It leverages eBPF uprobes to hook into SSL functions, collecting metadata before encryption/decryption. This enables Snoopy to monitor SSL traffic without decryption. 6 | 7 | Snoopy supports inspecting traffic from applications that use OpenSSL (libssl.so) or GnuTLS (libgnutls.so). 8 | 9 | 10 | ## Building 11 | Snoopy relies on [gobpf](https://github.com/iovisor/gobpf/tree/master), which are Go bindings for bcc. You will need to install [libbcc](https://github.com/iovisor/bcc/blob/master/INSTALL.md) for your specific kernel. 12 | 13 | ``` 14 | go build -o snoopy 15 | ``` 16 | 17 | ## Usage 18 | 19 | Snoopy supports two optional flags, --json and --pid. 20 | 21 | ```bash 22 | sudo ./snoopy --json --pid 1337 23 | ``` 24 | 25 | - `json`: Print TLS information in JSON format. 26 | - `pid`: Only print TLS information from a specific process. 27 | 28 | Example 29 | 30 | ```bash 31 | sudo snoopy --json --pid 1716580 32 | 33 | { 34 | "function": "SSL_READ", 35 | "process_name": "curl", 36 | "elapsed_time": 0.022584, 37 | "pid": 1716580, 38 | "tid": 1716580, 39 | "message_size": 1369, 40 | "result": 0, 41 | "tls_content": "106.8,\"High\":58335.1...." 42 | } 43 | 44 | ``` 45 | This will print TLS information in JSON format only from process ID 1337. 46 | Not supplying either flag, Snoopy will visually display all intercepted SSL/TLS traffic from all processes that use the OpenSSL library. 47 | 48 | ```bash 49 | sudo snoopy 50 | 51 | [ TLS Message Information ] 52 | +--------------+-----------------+ 53 | | DESCRIPTION | VALUE | 54 | +--------------+-----------------+ 55 | | Timestamp | 23:26:54.337542 | 56 | | Function | SSL_READ | 57 | | Process Name | curl | 58 | | PID | 1719190 | 59 | | TID | 1719190 | 60 | | Message Size | 1369 bytes | 61 | +--------------+-----------------+ 62 | [ TLS Content ] 63 | Open":0.2,"High":0.5,"ChangePercentFromLastMonth":..."} 64 | ... 65 | [ End of TLS Message ] 66 | ``` 67 | 68 | ## Contributing 69 | 70 | Feel free to create issues for bugs and feature requests, or make pull requests to improve the utility. 71 | 72 | ## License 73 | 74 | This project is licensed under the MIT License. 75 | 76 | ## References 77 | * https://www.airplane.dev/blog/decrypting-ssl-at-scale-with-ebpf 78 | * https://medium.com/@yunwei356/ebpf-practical-tutorial-capturing-ssl-tls-plain-text-using-uprobe-fccb010cfd64 79 | * https://github.com/eunomia-bpf/bpf-developer-tutorial 80 | * https://www.datadoghq.com/blog/ebpf-guide/ 81 | * https://blog.px.dev/ebpf-openssl-tracing/ 82 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.0.0 -------------------------------------------------------------------------------- /bpf/snoopy.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #define TASK_COMM_LEN 16 4 | #define MAX_DATA_SIZE 10000 5 | 6 | // Struct for storing TLS related data. 7 | struct TLS_MESSAGE { 8 | uint64_t elapsed; 9 | uint32_t pid; // Process ID 10 | uint32_t tid; // Thread ID 11 | int retval; 12 | char function_name[20]; // Function name 13 | char process_name[TASK_COMM_LEN]; 14 | char message[MAX_DATA_SIZE]; 15 | }; 16 | 17 | // eBPF data structures. 18 | BPF_HASH(tls_map_data, u64, const char *); 19 | BPF_HASH(tls_map_timestamp, u64, u64); 20 | BPF_PERF_OUTPUT(TLS_DATA_PERF_OUTPUT); 21 | BPF_PERCPU_ARRAY(tls_data_array, struct TLS_MESSAGE); 22 | 23 | // Utility function to extract the process ID from a PID-TID combo. 24 | static inline u32 extractProcessID(u64 ptID) { 25 | return ptID >> 32; 26 | } 27 | 28 | // Utility function to extract the thread ID from a PID-TID combo. 29 | static inline u32 extractThreadID(u64 ptID) { 30 | return (u32) ptID; 31 | } 32 | 33 | // Function to send TLS message data to a perf event. 34 | static inline int send_tls_message_to_perf(struct pt_regs* ctx, u32 bufferLen, u64 id, const char * buffer, const char * function_name) { 35 | u32 zeroPointer = 0; 36 | struct TLS_MESSAGE* tlsMessage = tls_data_array.lookup(&zeroPointer); 37 | 38 | if (!tlsMessage) { 39 | return 0; 40 | } 41 | 42 | // Extract and populate PID and TID from the combined ID. 43 | tlsMessage->pid = extractProcessID(id); 44 | tlsMessage->tid = extractThreadID(id); 45 | 46 | u64 *et = tls_map_timestamp.lookup(&id); 47 | if (!et) { 48 | return 0; 49 | } 50 | 51 | // Get the current command name and elapsed time. 52 | bpf_get_current_comm(&tlsMessage->process_name, sizeof(tlsMessage->process_name)); 53 | tlsMessage->elapsed = bpf_ktime_get_ns() - *et; 54 | 55 | // Populate function_name. 56 | __builtin_memcpy(tlsMessage->function_name, function_name, sizeof(tlsMessage->function_name)); 57 | 58 | // Copy data from user space to kernel space. 59 | u32 outputBufferLen = bufferLen < MAX_DATA_SIZE ? bufferLen : MAX_DATA_SIZE; 60 | bpf_probe_read(&tlsMessage->retval, sizeof(int), (void*)PT_REGS_RC(ctx)); 61 | bpf_probe_read(tlsMessage->message, outputBufferLen, buffer); 62 | 63 | // Submit data to perf event. 64 | TLS_DATA_PERF_OUTPUT.perf_submit(ctx, tlsMessage, sizeof(*tlsMessage)); 65 | 66 | // Clean up maps. 67 | tls_map_data.delete(&id); 68 | tls_map_timestamp.delete(&id); 69 | 70 | return 0; 71 | } 72 | 73 | // Function to handle entry into SSL functions. 74 | static inline int handle_uprobe_entry(struct pt_regs* ctx, const char * function_name) { 75 | u64 processThreadID = bpf_get_current_pid_tgid(); 76 | u64 ts = bpf_ktime_get_ns(); 77 | const char* buffer = (const char*)PT_REGS_PARM2(ctx); 78 | 79 | // Store timestamp and buffer pointer in maps. 80 | tls_map_timestamp.update(&processThreadID, &ts); 81 | tls_map_data.update(&processThreadID, &buffer); 82 | 83 | return 0; 84 | } 85 | 86 | // Function to handle return from SSL functions. 87 | static inline int handle_uprobe_return(struct pt_regs* ctx, const char * function_name) { 88 | u64 processThreadID = bpf_get_current_pid_tgid(); 89 | const char** buffer = tls_map_data.lookup(&processThreadID); 90 | 91 | if (!buffer) { 92 | return 0; 93 | } 94 | 95 | int len = (int)PT_REGS_RC(ctx); 96 | if (len >= 0) { 97 | send_tls_message_to_perf(ctx, len, processThreadID, *buffer, function_name); 98 | } 99 | 100 | return 0; 101 | } 102 | 103 | // Uprobe functions for SSL write (OpenSSL). 104 | int uprobe_entry_SSL_write(struct pt_regs* ctx) { 105 | return handle_uprobe_entry(ctx, "SSL_write"); 106 | } 107 | 108 | int uprobe_return_SSL_write(struct pt_regs* ctx) { 109 | return handle_uprobe_return(ctx, "SSL_write"); 110 | } 111 | 112 | // Uprobe functions for SSL read (OpenSSL). 113 | int uprobe_entry_SSL_read(struct pt_regs* ctx) { 114 | return handle_uprobe_entry(ctx, "SSL_read"); 115 | } 116 | 117 | int uprobe_return_SSL_read(struct pt_regs* ctx) { 118 | return handle_uprobe_return(ctx, "SSL_read"); 119 | } 120 | 121 | // Uprobe functions for record recv (GnuTLS). 122 | int uprobe_entry_gnutls_record_recv(struct pt_regs* ctx) { 123 | return handle_uprobe_entry(ctx, "gnutls_record_recv"); 124 | } 125 | 126 | int uprobe_return_gnutls_record_recv(struct pt_regs* ctx) { 127 | return handle_uprobe_return(ctx, "gnutls_record_recv"); 128 | } 129 | 130 | // Uprobe functions for record send (GnuTLS). 131 | int uprobe_entry_gnutls_record_send(struct pt_regs* ctx) { 132 | return handle_uprobe_entry(ctx, "gnutls_record_send"); 133 | } 134 | 135 | int uprobe_return_gnutls_record_send(struct pt_regs* ctx) { 136 | return handle_uprobe_return(ctx, "gnutls_record_send"); 137 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tks98/snoopy 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/fatih/color v1.15.0 7 | github.com/iovisor/gobpf v0.2.0 8 | github.com/jedib0t/go-pretty v4.3.0+incompatible 9 | ) 10 | 11 | require ( 12 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect 13 | github.com/go-openapi/errors v0.20.3 // indirect 14 | github.com/go-openapi/strfmt v0.21.7 // indirect 15 | github.com/mattn/go-colorable v0.1.13 // indirect 16 | github.com/mattn/go-isatty v0.0.17 // indirect 17 | github.com/mattn/go-runewidth v0.0.15 // indirect 18 | github.com/mitchellh/mapstructure v1.5.0 // indirect 19 | github.com/oklog/ulid v1.3.1 // indirect 20 | github.com/rivo/uniseg v0.2.0 // indirect 21 | github.com/stretchr/testify v1.8.4 // indirect 22 | go.mongodb.org/mongo-driver v1.11.3 // indirect 23 | golang.org/x/sys v0.6.0 // indirect 24 | ) 25 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= 2 | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= 8 | github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= 9 | github.com/go-openapi/errors v0.20.3 h1:rz6kiC84sqNQoqrtulzaL/VERgkoCyB6WdEkc2ujzUc= 10 | github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= 11 | github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= 12 | github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= 13 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 14 | github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= 15 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 16 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 17 | github.com/iovisor/gobpf v0.2.0 h1:34xkQxft+35GagXBk3n23eqhm0v7q0ejeVirb8sqEOQ= 18 | github.com/iovisor/gobpf v0.2.0/go.mod h1:WSY9Jj5RhdgC3ci1QaacvbFdQ8cbrEjrpiZbLHLt2s4= 19 | github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= 20 | github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= 21 | github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= 22 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 23 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 24 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 25 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 26 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 27 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 28 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 29 | github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= 30 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 31 | github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= 32 | github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 33 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 34 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 35 | github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 36 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 37 | github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= 38 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 39 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 44 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 45 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 46 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 47 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 48 | github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= 49 | github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= 50 | github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= 51 | github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= 52 | github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= 53 | github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= 54 | go.mongodb.org/mongo-driver v1.11.3 h1:Ql6K6qYHEzB6xvu4+AU0BoRoqf9vFPcc4o7MUIdPW8Y= 55 | go.mongodb.org/mongo-driver v1.11.3/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g= 56 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 57 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 58 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 64 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 65 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 66 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 67 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 68 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 69 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 71 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 72 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 75 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 76 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | ) 9 | 10 | const ( 11 | LibSSLSoPathAMd64 = "/lib/x86_64-linux-gnu/libssl.so.3" 12 | LibSSLSoPathArm64 = "/lib/aarch64-linux-gnu/libssl.so.3" 13 | LibGnuTLSSoPathAMd64 = "/usr/lib/x86_64-linux-gnu/libgnutls.so.30.31.0" 14 | LibGnuTLSSoPathArm64 = "/usr/lib/aarch64-linux-gnu/libgnutls.so.30.31.0" 15 | ) 16 | 17 | // CLIOptions is a struct for command line options 18 | type CLIOptions struct { 19 | JSONOutput bool 20 | PID int 21 | } 22 | 23 | // ParseCLIOptions functions parses the command line arguments to 24 | // create and return an object of CLIOptions 25 | func ParseCLIOptions() CLIOptions { 26 | // Declare a boolean variable for JSON output option 27 | var jsonOutput bool 28 | flag.BoolVar(&jsonOutput, "json", false, "Output in JSON format") 29 | 30 | // Declare an integer variable for Process ID option 31 | pid := flag.Int("pid", -1, "Specify the PID to trace (optional, if not specified, all processes will be traced)") 32 | flag.Parse() 33 | 34 | // Return a new object of CLIOptions with the parsed options 35 | return CLIOptions{ 36 | JSONOutput: jsonOutput, 37 | PID: *pid, 38 | } 39 | } 40 | 41 | // ReadEbpfProgram function reads a file from the given path 42 | func ReadEbpfProgram(filePath string) (string, error) { 43 | // Read the file from the given path 44 | b, err := os.ReadFile(filePath) 45 | return string(b), err 46 | } 47 | 48 | // GetLibPaths function returns a map of library names to their paths based on the architecture 49 | func GetLibPaths() (map[string]string, error) { 50 | libPaths := make(map[string]string) 51 | 52 | switch runtime.GOARCH { 53 | case "amd64": 54 | libPaths["OpenSSL"] = LibSSLSoPathAMd64 55 | libPaths["GnuTLS"] = LibGnuTLSSoPathAMd64 // You should confirm this path 56 | case "arm64": 57 | libPaths["OpenSSL"] = LibSSLSoPathArm64 58 | libPaths["GnuTLS"] = LibGnuTLSSoPathArm64 // This path was confirmed from your system 59 | default: 60 | return nil, fmt.Errorf("unsupported architecture") 61 | } 62 | 63 | return libPaths, nil 64 | } 65 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/tks98/snoopy/internal/util" 8 | "github.com/tks98/snoopy/pkg/tls_trace" 9 | ) 10 | 11 | func main() { 12 | 13 | // Check if the user is running with root privileges 14 | if os.Geteuid() != 0 { 15 | log.Fatal("snoopy must be run as root!") 16 | } 17 | // Parse and command line options 18 | opts := util.ParseCLIOptions() 19 | 20 | // Read BPF program from source file 21 | sources, err := util.ReadEbpfProgram("bpf/snoopy.c") 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | 26 | // Get shared SSL library paths for Uprobes 27 | binaryPaths, err := util.GetLibPaths() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | // Create new TLS tracer with parsed options 33 | tracer := tls_trace.New(opts.JSONOutput, sources, binaryPaths, &opts.PID) 34 | 35 | // Start tracing by obtaining the message channel 36 | tlsMessages, err := tracer.TraceMessageChannel() 37 | if err != nil { 38 | log.Fatalf("Failed to start tls_trace: %s", err) 39 | } 40 | 41 | // Loop over messages from the TLS tracer 42 | for message := range tlsMessages { 43 | message.Print(opts.JSONOutput) 44 | // If PID was provided and equals the message Pid, or if no PID was provided 45 | // and the message has content, print the message. 46 | if (opts.PID < 0 || opts.PID == int(message.Pid)) && message.HasContent() { 47 | message.Print(opts.JSONOutput) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /pkg/tls_trace/bpf.go: -------------------------------------------------------------------------------- 1 | package tls_trace 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/signal" 10 | 11 | bpf "github.com/iovisor/gobpf/bcc" 12 | ) 13 | 14 | // Constants used within the package 15 | const ( 16 | MessageMaxBuffer = 10000 17 | 18 | DefaultUprobeEntry = "entry" 19 | DefaultUprobeRet = "return" 20 | SSLRead = 0 21 | SSLWrite = 1 22 | ) 23 | 24 | // Tracer struct for TLS tracing 25 | type Tracer struct { 26 | jsonOutput bool 27 | bpfModule *bpf.Module 28 | sources string 29 | binaryPaths map[string]string 30 | pid *int 31 | } 32 | 33 | // New returns a new tracer instance 34 | func New(jsonOutput bool, sources string, binaryPaths map[string]string, pid *int) *Tracer { 35 | return &Tracer{ 36 | jsonOutput: jsonOutput, 37 | sources: sources, 38 | binaryPaths: binaryPaths, 39 | pid: pid, 40 | } 41 | } 42 | 43 | // attachProbes attaches required uprobe for tracing specific functions 44 | func (t *Tracer) attachProbes() error { 45 | // Attach OpenSSL Probes 46 | if path, exists := t.binaryPaths["OpenSSL"]; exists { 47 | t.attachUprobeEntry(path, "SSL_read") 48 | t.attachUprobeReturn(path, "SSL_read") 49 | t.attachUprobeEntry(path, "SSL_write") 50 | t.attachUprobeReturn(path, "SSL_write") 51 | } 52 | 53 | // Attach GnuTLS Probes 54 | if path, exists := t.binaryPaths["GnuTLS"]; exists { 55 | t.attachUprobeEntry(path, "gnutls_record_recv") 56 | t.attachUprobeReturn(path, "gnutls_record_recv") 57 | t.attachUprobeEntry(path, "gnutls_record_send") 58 | t.attachUprobeReturn(path, "gnutls_record_send") 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // TraceMessageChannel retrieves the trace in a channel from BPF program 65 | func (t *Tracer) TraceMessageChannel() (<-chan TlsMessage, error) { 66 | 67 | t.bpfModule = bpf.NewModule(t.sources, []string{}) 68 | if err := t.attachProbes(); err != nil { 69 | return nil, fmt.Errorf("Error attaching probes: %s", err) 70 | } 71 | 72 | // Map table in bpf module to channel 73 | tlsData := t.bpfModule.TableId("TLS_DATA_PERF_OUTPUT") 74 | table := bpf.NewTable(tlsData, t.bpfModule) 75 | channel := make(chan []byte) 76 | out := make(chan TlsMessage) 77 | 78 | // Create performance map to relay traced data to channel 79 | perfMap, err := bpf.InitPerfMap(table, channel, nil) 80 | if err != nil { 81 | return nil, fmt.Errorf("Failed to init perf map: %s", err) 82 | } 83 | 84 | // Message decoding routine 85 | go func() { 86 | var msg TlsMessage 87 | for { 88 | data := <-channel 89 | buffer := bytes.NewBuffer(data) 90 | for _, field := range []interface{}{&msg.Elapsed, &msg.Pid, &msg.Tid, &msg.Result, &msg.FunctionName, &msg.ProcessName, &msg.Message} { // Added &msg.FunctionName 91 | if err := binary.Read(buffer, binary.LittleEndian, field); err != nil { 92 | log.Printf("Failed to decode data: %s\n", err) 93 | continue 94 | } 95 | } 96 | // Send the parsed message into output channel 97 | out <- msg 98 | } 99 | }() 100 | 101 | // Capture Interrupt / Kill signals for graceful exit of routines and map 102 | sig := make(chan os.Signal, 1) 103 | signal.Notify(sig, os.Interrupt, os.Kill) 104 | 105 | go func() { 106 | <-sig 107 | perfMap.Stop() 108 | t.bpfModule.Close() 109 | close(channel) 110 | close(out) 111 | }() 112 | 113 | perfMap.Start() 114 | 115 | return out, nil 116 | } 117 | 118 | // UProbe function attachment helpers 119 | 120 | func (t *Tracer) attachUprobeEntry(binaryPath, funcName string) { 121 | t.attachUprobe(binaryPath, funcName, DefaultUprobeEntry) 122 | } 123 | 124 | func (t *Tracer) attachUprobeReturn(binaryPath, funcName string) { 125 | t.attachUprobe(binaryPath, funcName, DefaultUprobeRet) 126 | } 127 | 128 | // attachUprobe attaches a uProbe for the function funcName on binaryPath 129 | func (t *Tracer) attachUprobe(binaryPath, funcName, probeType string) { 130 | probeName := fmt.Sprintf("uprobe_%s_%s", probeType, funcName) 131 | uProbe, err := t.bpfModule.LoadUprobe(probeName) 132 | 133 | if err != nil { 134 | log.Fatalf("Failed to load %s: %s\n", probeName, err) 135 | } 136 | 137 | if probeType == DefaultUprobeEntry { 138 | err = t.bpfModule.AttachUprobe(binaryPath, funcName, uProbe, -1) 139 | } else { 140 | err = t.bpfModule.AttachUretprobe(binaryPath, funcName, uProbe, -1) 141 | } 142 | 143 | if err != nil { 144 | log.Fatalf("Failed to attach %s: %s", funcName, err) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /pkg/tls_trace/tls.go: -------------------------------------------------------------------------------- 1 | package tls_trace 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | "time" 9 | 10 | "github.com/fatih/color" 11 | "github.com/jedib0t/go-pretty/table" 12 | ) 13 | 14 | // TlsMessage struct to handle each message data 15 | type TlsMessage struct { 16 | Elapsed uint64 17 | Pid uint32 18 | Tid uint32 19 | Result int32 20 | FunctionName [20]byte // New field to store the function name 21 | ProcessName [16]byte 22 | Message [MessageMaxBuffer]byte 23 | EndIdx int 24 | } 25 | 26 | // getEndIndex returns end index of the message 27 | func (t *TlsMessage) getEndIndex() int { 28 | endIdx := bytes.IndexByte(t.Message[:], 0) 29 | if endIdx == -1 { 30 | endIdx = len(t.Message) 31 | } 32 | return endIdx 33 | } 34 | 35 | // HasContent checks if the TLS message has content 36 | func (t *TlsMessage) HasContent() bool { 37 | return bytes.IndexByte(t.Message[:], 0) != 0 38 | } 39 | 40 | // createJsonOutput creates and prints JSON representation of the message 41 | func (t *TlsMessage) createJsonOutput(funcName string, procName string, elapsedSeconds float64) { 42 | msg := struct { 43 | Function string `json:"function"` 44 | ProcessName string `json:"process_name"` 45 | ElapsedTime float64 `json:"elapsed_time"` 46 | PID uint32 `json:"pid"` 47 | TID uint32 `json:"tid"` 48 | MessageSize int `json:"message_size"` 49 | Result int32 `json:"result"` 50 | TLSContent string `json:"tls_content"` 51 | }{ 52 | Function: funcName, 53 | ProcessName: procName, 54 | ElapsedTime: elapsedSeconds, 55 | PID: t.Pid, 56 | TID: t.Tid, 57 | MessageSize: t.EndIdx, 58 | Result: t.Result, 59 | TLSContent: string(t.Message[:t.EndIdx]), 60 | } 61 | jsonData, err := json.MarshalIndent(msg, "", " ") 62 | if err != nil { 63 | fmt.Printf("Failed to encode message to JSON: %s\n", err) 64 | return 65 | } 66 | fmt.Println(string(jsonData)) 67 | } 68 | 69 | // printTableOutput prints message info as a table 70 | func (t *TlsMessage) printTableOutput(funcName, procName, timestamp string) { 71 | // New table writer 72 | tw := table.NewWriter() 73 | tw.SetOutputMirror(os.Stdout) 74 | tw.AppendRow([]interface{}{"Timestamp", timestamp}) 75 | tw.AppendRow([]interface{}{"Function", funcName}) 76 | tw.AppendRow([]interface{}{"Process Name", procName}) 77 | tw.AppendRow([]interface{}{"PID", t.Pid}) 78 | tw.AppendRow([]interface{}{"TID", t.Tid}) 79 | tw.AppendRow([]interface{}{"Message Size", fmt.Sprintf("%d bytes", t.EndIdx)}) 80 | 81 | title := color.New(color.FgHiMagenta, color.Bold).SprintfFunc() 82 | content := color.New(color.FgHiWhite).SprintfFunc() 83 | 84 | // Outputting data 85 | fmt.Println(title("[ TLS Message Information ]")) 86 | tw.Render() 87 | fmt.Println(title("[ TLS Content ]")) 88 | fmt.Println(content(string(t.Message[:t.EndIdx]))) 89 | fmt.Println(title("[ End of TLS Message ]")) 90 | } 91 | 92 | // Print prints the message info either as a table or JSON based on jsonOutput flag 93 | func (t *TlsMessage) Print(jsonOutput bool) { 94 | t.EndIdx = t.getEndIndex() 95 | 96 | elapsedSeconds := float64(t.Elapsed) / 1e6 97 | procName := string(t.ProcessName[:bytes.IndexByte(t.ProcessName[:], 0)]) 98 | funcName := string(t.FunctionName[:bytes.IndexByte(t.FunctionName[:], 0)]) // Using the new FunctionName field 99 | if jsonOutput { 100 | t.createJsonOutput(funcName, procName, elapsedSeconds) 101 | } else { 102 | currentTime := time.Now() 103 | timestamp := currentTime.Format("15:04:05.000000") 104 | t.printTableOutput(funcName, procName, timestamp) 105 | } 106 | } 107 | --------------------------------------------------------------------------------