├── .gitignore ├── Cargo.toml ├── README.md ├── build.rs ├── snuffy-probes ├── Cargo.toml ├── build.rs ├── include │ └── user_bindings.h └── src │ ├── lib.rs │ ├── snuffy │ ├── main.rs │ └── mod.rs │ └── user_bindings.rs └── src └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snuffy" 3 | version = "0.1.0" 4 | authors = ["Alessandro Decina "] 5 | edition = "2018" 6 | 7 | [build-dependencies] 8 | cargo-bpf = { version = "^1.0.1", default-features = false, features = ["build"] } 9 | 10 | [dependencies] 11 | snuffy-probes = { path = "./snuffy-probes" } 12 | redbpf = { version = "^1.0.1", features = ["load"] } 13 | tokio = { version = "0.2.4", features = ["rt-core", "io-driver", "macros", "signal", "time"] } 14 | futures = "0.3" 15 | hexdump = "0.1" 16 | time = "0.2" 17 | toml = "0.5" 18 | anyhow = "1.0" 19 | structopt = "0.3.17" 20 | serde = {version = "1.0", features = ["derive"]} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # snuffy 2 | 3 | Snuffy is a simple command line tool to inspect SSL/TLS connections. It currently supports [OpenSSL](https://openssl.org) and [NSS](https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS). 4 | 5 | For background info see the blog post https://confused.ai/posts/intercepting-zoom-tls-encryption-bpf-uprobes. 6 | 7 | # Installation 8 | 9 | In order to use snuffy you need to install the headers for the running kernel and LLVM 10. 10 | 11 | To install them on ubuntu run: 12 | 13 | ```sh 14 | sudo apt-get -y install build-essential zlib1g-dev \ 15 | llvm-10-dev libclang-10-dev linux-headers-$(uname -r) 16 | ``` 17 | 18 | On fedora run: 19 | 20 | ```sh 21 | yum install clang llvm llvm-devel zlib-devel kernel-devel 22 | export LLVM_SYS_100_PREFIX=/usr 23 | ``` 24 | 25 | Finally install snuffy itself running: 26 | 27 | ```sh 28 | cargo install --git https://github.com/alessandrod/snuffy snuffy 29 | ``` 30 | 31 | **NOTE**: if you installed rust in your home directory, the binary will be placed in `$HOME/.cargo/bin/snuffy`. If you use sudo to run snuffy, you'll have to use the full path. 32 | 33 | # Usage 34 | 35 | Snuffy uses the `bpf()` syscall, so you need to run it as root or a user with `CAP_SYS_ADMIN` privileges. 36 | 37 | ## With programs that link to OpenSSL or NSS dynamically 38 | 39 | To instruments commands that link to OpenSSL or NSS dynamically, run: 40 | 41 | ``` 42 | # snuffy --hex-dump --command [COMMAND] 43 | ``` 44 | 45 | For example to instrument curl: 46 | 47 | ``` 48 | # snuffy --hex-dump --command /usr/bin/curl # then in another terminal run: curl --http1.1 https://www.google.com 49 | [6:05:19] Connected to 127.0.0.53:53 50 | [6:05:19] Resolved www.google.com to 216.58.199.68 51 | [6:05:19] Connected to www.google.com:443 (216.58.199.68:443) 52 | [6:05:19] Write 78 bytes to www.google.com:443 (216.58.199.68:443) 53 | [6:05:19] |47455420 2f204854 54502f31 2e310d0a| GET / HTTP/1.1.. 00000000 54 | [6:05:19] |486f7374 3a207777 772e676f 6f676c65| Host: www.google 00000010 55 | [6:05:19] |2e636f6d 0d0a5573 65722d41 67656e74| .com..User-Agent 00000020 56 | [6:05:19] |3a206375 726c2f37 2e36352e 330d0a41| : curl/7.65.3..A 00000030 57 | [6:05:19] |63636570 743a202a 2f2a0d0a 0d0a| ccept: */*.... 00000040 58 | [6:05:19] 0000004e 59 | [6:05:19] Read 1396 bytes from www.google.com:443 (216.58.199.68:443) 60 | [6:05:19] |48545450 2f312e31 20323030 204f4b0d| HTTP/1.1 200 OK. 00000000 61 | [6:05:19] |0a446174 653a2046 72692c20 30342053| .Date: Fri, 04 S 00000010 62 | [6:05:19] |65702032 30323020 30363a32 303a3033| ep 2020 06:20:03 00000020 63 | [6:05:19] |20474d54 0d0a4578 70697265 733a202d| GMT..Expires: - 00000030 64 | [6:05:19] |310d0a43 61636865 2d436f6e 74726f6c| 1..Cache-Control 00000040 65 | [6:05:19] |3a207072 69766174 652c206d 61782d61| : private, max-a 00000050 66 | ``` 67 | 68 | If you omit the `--command` option, snuffy will intercept **all** the programs that use OpenSSL or NSS. 69 | 70 | **NOTE**: Firefox links to NSS dynamically, but ships its own `libssl3.so` and `libnspr4.so`. To instrument firefox, you have to provide a config file pointing to those libraries, eg: 71 | 72 | ```toml 73 | [nss] 74 | libssl3="/usr/lib/firefox/libssl3.so" 75 | libnspr4="/usr/lib/firefox/libnspr4.so" 76 | ``` 77 | 78 | ## With programs that link to OpenSSL or NSS statically 79 | 80 | If you want to instrument a program that links statically to OpenSSL or NSS and the symbols have been stripped, you need to provide a configuration file containing the `.text` section offsets of the TLS functions. 81 | 82 | For example for OpenSSL put this in `config.toml`: 83 | 84 | ```toml 85 | [openssl] 86 | SSL_set_fd = 0xBADDCAFE 87 | SSL_read = 0xBAAAAAAD 88 | SSL_write = 0xDECAFBAD 89 | ``` 90 | 91 | And for NSS: 92 | 93 | ```toml 94 | [nss] 95 | SSL_SetURL = 0xBADDCAFE 96 | PR_Recv = 0xBAAAAAAD 97 | PR_Send = 0xDECAFBAD 98 | ``` 99 | 100 | (The offsets above are just examples, you need to provide working ones.) 101 | 102 | Then run: 103 | 104 | ``` 105 | # snuffy --hex-dump --command COMMAND --config config.toml 106 | ``` 107 | 108 | For example assuming `zoom-config.toml` contains valid OpenSSL offsets for the zoom client: 109 | 110 | ``` 111 | # snuffy --hex-dump --command /opt/zoom/zoom --config zoom-config.toml # then start zoom 112 | [4:56:18] Connected to 127.0.0.53:53 113 | [4:56:18] Resolved us04web.zoom.us to 3.235.69.6 114 | [4:56:18] Connected to us04web.zoom.us:443 (3.235.69.6:443) 115 | [4:56:19] Write 571 bytes to us04web.zoom.us:443 (3.235.69.6:443) 116 | [4:56:19] |504f5354 202f7265 6c656173 656e6f74| POST /releasenot 00000000 117 | [4:56:19] |65732048 5454502f 312e310d 0a486f73| es HTTP/1.1..Hos 00000010 118 | [4:56:19] |743a2075 73303477 65622e7a 6f6f6d2e| t: us04web.zoom. 00000020 119 | [4:56:19] |75730d0a 55736572 2d416765 6e743a20| us..User-Agent: 00000030 120 | [4:56:19] |4d6f7a69 6c6c612f 352e3020 285a4f4f| Mozilla/5.0 (ZOO 00000040 121 | [4:56:19] |4d2e4c69 6e757820 5562756e 74752031| M.Linux Ubuntu 1 00000050 122 | ... 123 | 124 | [4:56:19] Read 3088 bytes from us04web.zoom.us:443 (3.235.69.6:443) 125 | [4:56:19] |48545450 2f312e31 20323030 200d0a44| HTTP/1.1 200 ..D 00000000 126 | [4:56:19] |6174653a 20467269 2c203034 20536570| ate: Fri, 04 Sep 00000010 127 | [4:56:19] |20323032 30203035 3a31313a 30352047| 2020 05:11:05 G 00000020 128 | [4:56:19] |4d540d0a 436f6e74 656e742d 54797065| MT..Content-Type 00000030 129 | [4:56:19] |3a206170 706c6963 6174696f 6e2f782d| : application/x- 00000040 130 | [4:56:19] |70726f74 6f627566 3b636861 72736574| protobuf;charset 00000050 131 | ... 132 | ``` 133 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use cargo_bpf_lib as cargo_bpf; 5 | 6 | fn main() { 7 | let cargo = PathBuf::from(env::var("CARGO").unwrap()); 8 | let target = PathBuf::from(env::var("OUT_DIR").unwrap()); 9 | let probes = Path::new("snuffy-probes"); 10 | 11 | cargo_bpf::build( 12 | &cargo, 13 | &probes, 14 | &target.join("target"), 15 | Vec::new(), 16 | ) 17 | .expect("couldn't compile probes"); 18 | 19 | cargo_bpf::probe_files(&probes) 20 | .expect("couldn't list probe files") 21 | .iter() 22 | .for_each(|file| { 23 | println!("cargo:rerun-if-changed={}", file); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /snuffy-probes/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "snuffy-probes" 3 | version = "0.1.0" 4 | edition = '2018' 5 | 6 | [dependencies] 7 | cty = "0.2" 8 | redbpf-macros = "1.0" 9 | redbpf-probes = "1.0" 10 | 11 | [build-dependencies] 12 | cargo-bpf = { version = "1.0", default-features = false, features = ["bindings"] } 13 | 14 | [features] 15 | default = [] 16 | probes = [] 17 | 18 | [lib] 19 | path = "src/lib.rs" 20 | 21 | [[bin]] 22 | name = "snuffy" 23 | path = "src/snuffy/main.rs" 24 | required-features = ["probes"] 25 | -------------------------------------------------------------------------------- /snuffy-probes/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::{self, Write}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use cargo_bpf_lib::bindgen as bpf_bindgen; 7 | 8 | fn main() { 9 | if env::var("CARGO_FEATURE_PROBES").is_err() { 10 | return; 11 | } 12 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 13 | gen_bindings( 14 | &out_dir, 15 | "include/user_bindings.h", 16 | "gen_user_bindings", 17 | &["addrinfo"], 18 | ); 19 | } 20 | 21 | fn gen_bindings(out_dir: &Path, header: &str, name: &str, types: &[&str]) { 22 | let mut builder = bpf_bindgen::builder().header(header); 23 | for ty in types { 24 | builder = builder.whitelist_type(*ty); 25 | } 26 | 27 | let mut bindings = builder 28 | .generate() 29 | .expect("failed to generate bindings") 30 | .to_string(); 31 | let accessors = bpf_bindgen::generate_read_accessors(&bindings, types); 32 | bindings.push_str("use redbpf_probes::helpers::bpf_probe_read;"); 33 | bindings.push_str(&accessors); 34 | create_module(out_dir.join(name).with_extension("rs"), name, &bindings).unwrap(); 35 | } 36 | 37 | fn create_module(path: PathBuf, name: &str, bindings: &str) -> io::Result<()> { 38 | let mut file = File::create(path)?; 39 | writeln!( 40 | &mut file, 41 | r" 42 | mod {name} {{ 43 | #![allow(non_camel_case_types)] 44 | #![allow(non_upper_case_globals)] 45 | #![allow(non_snake_case)] 46 | #![allow(unused_unsafe)] 47 | #![allow(clippy::all)] 48 | {bindings} 49 | }} 50 | pub use {name}::*; 51 | ", 52 | name = name, 53 | bindings = bindings 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /snuffy-probes/include/user_bindings.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /snuffy-probes/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | #[cfg(feature = "probes")] 4 | pub mod user_bindings; 5 | 6 | pub mod snuffy; 7 | -------------------------------------------------------------------------------- /snuffy-probes/src/snuffy/main.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | #![no_main] 3 | use redbpf_probes::helpers::gen; 4 | use redbpf_probes::uprobe::prelude::*; 5 | use snuffy_probes::snuffy::{ 6 | AccessMode, Config, Connection, SSLBuffer, SSLFd, SSLHost, BUF_LEN, COMM_LEN, 7 | CONFIG_KEY, DNS, HOST_LEN, 8 | }; 9 | use snuffy_probes::user_bindings::addrinfo; 10 | 11 | const REQ_OP_WRITE: u32 = 1; 12 | 13 | program!(0xFFFFFFFE, "GPL"); 14 | 15 | #[map("config")] 16 | static mut config: HashMap = HashMap::with_max_entries(1); 17 | 18 | #[map("dns")] 19 | static mut dns_events: PerfMap = PerfMap::with_max_entries(1024); 20 | 21 | #[map("ssl_fd")] 22 | static mut ssl_fd_events: PerfMap = PerfMap::with_max_entries(1024); 23 | 24 | #[map("ssl_buffer")] 25 | static mut ssl_buffer_events: PerfMap = PerfMap::with_max_entries(1024); 26 | 27 | #[map("connection")] 28 | static mut connection_events: PerfMap = PerfMap::with_max_entries(1024); 29 | 30 | #[map("dns_hosts")] 31 | static mut dns_hosts: HashMap = HashMap::with_max_entries(1024); 32 | 33 | #[map("ssl_args")] 34 | static mut ssl_args: HashMap = HashMap::with_max_entries(1024); 35 | 36 | #[map("nss_ssl_contexts")] 37 | static mut nss_ssl_contexts: HashMap = HashMap::with_max_entries(1024); 38 | 39 | #[map("ssl_host")] 40 | static mut ssl_host_events: PerfMap = PerfMap::with_max_entries(1024); 41 | 42 | const SYS_CONNECT: i32 = 42; 43 | 44 | struct SSLArgs { 45 | ssl: usize, 46 | buf: usize, 47 | } 48 | 49 | #[uprobe] 50 | fn getaddrinfo(regs: Registers) { 51 | if !is_target_command() { 52 | return; 53 | } 54 | 55 | let tid = bpf_get_current_pid_tgid(); 56 | unsafe { dns_hosts.set(&tid, &(regs.parm1(), regs.parm4())) }; 57 | } 58 | 59 | #[uretprobe] 60 | fn getaddrinfo_ret(regs: Registers) { 61 | let _ = do_getaddrinfo_ret(regs); 62 | } 63 | 64 | fn do_getaddrinfo_ret(regs: Registers) -> Option<()> { 65 | let tid = bpf_get_current_pid_tgid(); 66 | let (node, ret_addr) = unsafe { *dns_hosts.get(&tid)? }; 67 | 68 | let info = unsafe { &*bpf_probe_read(ret_addr as *const *const addrinfo).ok()? }; 69 | if info.ai_family()? as u32 != AF_INET { 70 | return None; 71 | } 72 | let addr = unsafe { &*(info.ai_addr()? as *const sockaddr_in) }; 73 | 74 | let mut dns = DNS { 75 | pid: current_pid(), 76 | comm: current_comm(), 77 | addr: addr.sin_addr()?.s_addr()? as u64, 78 | host: [0; HOST_LEN], 79 | }; 80 | 81 | unsafe { 82 | bpf_probe_read_str( 83 | dns.host.as_mut_ptr() as *mut _, 84 | HOST_LEN as i32, 85 | node as *const c_void, 86 | ) 87 | }; 88 | 89 | unsafe { 90 | dns_events.insert(regs.ctx, &dns); 91 | } 92 | 93 | None 94 | } 95 | 96 | #[uprobe] 97 | fn connect(regs: Registers) { 98 | let _ = do_connect(regs); 99 | } 100 | 101 | fn do_connect(regs: Registers) -> Option<()> { 102 | let fd = regs.parm1() as i32; 103 | let addr = regs.parm2() as *const sockaddr; 104 | 105 | if unsafe { &*addr }.sa_family()? as u32 != AF_INET { 106 | return None; 107 | } 108 | 109 | if !is_target_command() { 110 | return None; 111 | } 112 | 113 | let addr = unsafe { &*(addr as *const sockaddr_in) }; 114 | let conn = Connection { 115 | pid: current_pid(), 116 | comm: current_comm(), 117 | fd: fd as u64, 118 | addr: addr.sin_addr()?.s_addr()?, 119 | port: u16::from_be(addr.sin_port()?) as u32, 120 | }; 121 | 122 | unsafe { 123 | connection_events.insert(regs.ctx, &conn); 124 | } 125 | 126 | None 127 | } 128 | 129 | #[uprobe] 130 | fn SSL_set_fd(regs: Registers) { 131 | let ssl_ctx = regs.parm1() as usize; 132 | let fd = regs.parm2() as i32; 133 | 134 | if fd < 0 { 135 | return; 136 | } 137 | 138 | unsafe { 139 | ssl_fd_events.insert( 140 | regs.ctx, 141 | &SSLFd { 142 | pid: current_pid(), 143 | comm: current_comm(), 144 | ssl_ctx, 145 | fd: fd as usize, 146 | }, 147 | ) 148 | } 149 | } 150 | 151 | fn do_read(regs: Registers) { 152 | let ssl = regs.parm1() as usize; 153 | let buf = regs.parm2() as usize; 154 | let len = regs.parm3() as usize; 155 | if len <= 0 { 156 | return; 157 | } 158 | 159 | if is_target_command() { 160 | unsafe { 161 | ssl_args.set(&bpf_get_current_pid_tgid(), &SSLArgs { ssl, buf }); 162 | } 163 | } 164 | } 165 | 166 | fn do_read_ret(regs: Registers) { 167 | let len = regs.rc() as i32; 168 | 169 | if len < 0 { 170 | return; 171 | } 172 | let args = unsafe { ssl_args.get(&bpf_get_current_pid_tgid()) }; 173 | if let Some(SSLArgs { ssl, buf }) = args { 174 | output_buf(regs, *ssl, AccessMode::Read, *buf, len as u32); 175 | unsafe { ssl_args.delete(&bpf_get_current_pid_tgid()) }; 176 | } 177 | } 178 | 179 | fn do_write(regs: Registers) { 180 | let ssl = regs.parm1() as usize; 181 | let buf = regs.parm2() as usize; 182 | let len = regs.parm3() as i32; 183 | if len <= 0 { 184 | return; 185 | } 186 | if !is_target_command() { 187 | return; 188 | } 189 | 190 | output_buf(regs, ssl, AccessMode::Write, buf, len as u32); 191 | } 192 | 193 | #[uprobe] 194 | fn SSL_read(regs: Registers) { 195 | do_read(regs); 196 | } 197 | 198 | #[uretprobe] 199 | fn SSL_read_ret(regs: Registers) { 200 | do_read_ret(regs); 201 | } 202 | 203 | #[uprobe] 204 | fn SSL_write(regs: Registers) { 205 | do_write(regs); 206 | } 207 | 208 | #[uprobe] 209 | fn SSL_SetURL(regs: Registers) { 210 | let ssl_ctx = regs.parm1() as usize; 211 | let host = regs.parm2() as *const c_void; 212 | 213 | let mut event = SSLHost { 214 | pid: current_pid(), 215 | comm: current_comm(), 216 | ssl_ctx, 217 | host: [0; HOST_LEN], 218 | }; 219 | 220 | unsafe { bpf_probe_read_str(event.host.as_mut_ptr() as *mut _, HOST_LEN as i32, host) }; 221 | 222 | unsafe { 223 | let value = 1; 224 | nss_ssl_contexts.set(&ssl_ctx, &value); 225 | ssl_host_events.insert(regs.ctx, &event); 226 | } 227 | } 228 | 229 | #[uprobe] 230 | fn nss_read(regs: Registers) { 231 | let ssl_ctx = regs.parm1() as usize; 232 | if unsafe { nss_ssl_contexts.get(&ssl_ctx) }.is_some() { 233 | do_read(regs); 234 | } 235 | } 236 | 237 | #[uretprobe] 238 | fn nss_read_ret(regs: Registers) { 239 | let len = regs.rc(); 240 | // 0 means connection closed 241 | if len > 0 { 242 | do_read_ret(regs); 243 | } 244 | } 245 | 246 | #[uprobe] 247 | fn nss_write(regs: Registers) { 248 | let ssl_ctx = regs.parm1() as usize; 249 | if unsafe { nss_ssl_contexts.get(&ssl_ctx) }.is_some() { 250 | do_write(regs); 251 | } 252 | } 253 | 254 | fn output_buf(regs: Registers, ssl_ctx: usize, mode: AccessMode, buf_addr: usize, len: u32) { 255 | let mut buf = SSLBuffer { 256 | pid: current_pid(), 257 | comm: current_comm(), 258 | ssl_ctx, 259 | mode, 260 | len: len as usize, 261 | chunk_len: 0, 262 | chunk: [0u8; BUF_LEN], 263 | }; 264 | 265 | let len = len as usize; 266 | let mut read = 0; 267 | let read_len = BUF_LEN; 268 | for _ in 0..110 { 269 | let err = unsafe { 270 | gen::bpf_probe_read( 271 | buf.chunk.as_mut_ptr() as *mut _, 272 | read_len as u32, 273 | (buf_addr + read) as *const c_void, 274 | ) 275 | }; 276 | if err < 0 { 277 | break; 278 | } 279 | let left = len - read; 280 | if left > read_len { 281 | read += read_len; 282 | buf.chunk_len = read_len; 283 | } else { 284 | buf.chunk_len = left; 285 | read = len; 286 | } 287 | 288 | unsafe { ssl_buffer_events.insert(regs.ctx, &buf) }; 289 | 290 | if read == len { 291 | break; 292 | } 293 | } 294 | 295 | buf.chunk_len = 0; 296 | unsafe { ssl_buffer_events.insert(regs.ctx, &buf) }; 297 | } 298 | 299 | fn is_target_command() -> bool { 300 | let comm = bpf_get_current_comm(); 301 | let key = CONFIG_KEY; 302 | let conf = unsafe { config.get(&key) }; 303 | conf.map(|c| { 304 | if c.target_comm_set == 0 { 305 | return true; 306 | } 307 | 308 | let cmd = unsafe { core::slice::from_raw_parts(comm.as_ptr(), COMM_LEN) }; 309 | cmd[..COMM_LEN] == c.target_comm 310 | }) 311 | .unwrap_or(false) 312 | } 313 | 314 | fn current_pid() -> u64 { 315 | (bpf_get_current_pid_tgid() >> 32) as u64 316 | } 317 | 318 | fn current_comm() -> [c_char; COMM_LEN] { 319 | let mut comm: [c_char; COMM_LEN] = [0; COMM_LEN]; 320 | unsafe { gen::bpf_get_current_comm(&mut comm as *mut _ as *mut c_void, COMM_LEN as u32) }; 321 | comm 322 | } 323 | -------------------------------------------------------------------------------- /snuffy-probes/src/snuffy/mod.rs: -------------------------------------------------------------------------------- 1 | use cty::c_char; 2 | 3 | pub const CONFIG_KEY: usize = 1; 4 | pub const COMM_LEN: usize = 16; 5 | pub const BUF_LEN: usize = 368; 6 | pub const HOST_LEN: usize = 256; 7 | 8 | #[repr(C)] 9 | #[derive(Clone)] 10 | pub struct Config { 11 | pub target_comm_set: usize, 12 | pub target_comm: [c_char; COMM_LEN], 13 | } 14 | 15 | #[repr(C)] 16 | pub struct DNS { 17 | pub pid: u64, 18 | pub comm: [c_char; COMM_LEN], 19 | pub addr: u64, 20 | pub host: [c_char; HOST_LEN], 21 | } 22 | #[repr(C)] 23 | #[derive(Clone)] 24 | pub struct Connection { 25 | pub pid: u64, 26 | pub comm: [c_char; COMM_LEN], 27 | pub fd: u64, 28 | pub addr: u32, 29 | pub port: u32, 30 | } 31 | 32 | #[repr(C)] 33 | pub struct SSLBuffer { 34 | pub pid: u64, 35 | pub comm: [c_char; COMM_LEN], 36 | pub ssl_ctx: usize, 37 | pub mode: AccessMode, 38 | pub len: usize, 39 | pub chunk_len: usize, 40 | pub chunk: [u8; BUF_LEN], 41 | } 42 | 43 | #[repr(C)] 44 | pub struct SSLHost { 45 | pub pid: u64, 46 | pub comm: [c_char; COMM_LEN], 47 | pub ssl_ctx: usize, 48 | pub host: [c_char; HOST_LEN], 49 | } 50 | 51 | #[repr(C)] 52 | pub struct SSLFd { 53 | pub pid: u64, 54 | pub comm: [c_char; COMM_LEN], 55 | pub ssl_ctx: usize, 56 | pub fd: usize, 57 | } 58 | 59 | #[repr(u64)] 60 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 61 | pub enum AccessMode { 62 | Read, 63 | Write, 64 | } 65 | -------------------------------------------------------------------------------- /snuffy-probes/src/user_bindings.rs: -------------------------------------------------------------------------------- 1 | include!(concat!(env!("OUT_DIR"), "/gen_user_bindings.rs")); -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Alessandro Decina 2 | // 3 | // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be 6 | // copied, modified, or distributed except according to those terms. 7 | use std::collections::{HashMap, HashSet}; 8 | use std::env; 9 | use std::ffi::CStr; 10 | use std::io::{self, Write}; 11 | use std::net::{Ipv4Addr, SocketAddrV4}; 12 | use std::str::FromStr; 13 | use std::{cmp, fs, path::Path, ptr, slice}; 14 | 15 | use anyhow::anyhow; 16 | use futures::stream::StreamExt; 17 | use hexdump::hexdump_iter; 18 | use redbpf::{load::Loader, HashMap as BPFHashMap}; 19 | use serde::Deserialize; 20 | use structopt::StructOpt; 21 | use time::OffsetDateTime; 22 | use tokio; 23 | use tokio::runtime::Runtime; 24 | use tokio::signal; 25 | 26 | use snuffy_probes::snuffy::{ 27 | AccessMode, Config, Connection, SSLBuffer, SSLFd, SSLHost, COMM_LEN, CONFIG_KEY, DNS, 28 | }; 29 | 30 | static TLS_LIBS: [&str; 2] = ["openssl", "nss"]; 31 | 32 | macro_rules! attach_uprobe { 33 | ($uprobe:ident, $name:literal, $target:expr, $offset:expr, $opts:ident) => { 34 | { 35 | let (fn_name, offset, target) = match $offset { 36 | Some(offset) => (None, offset, $opts.command.as_ref().unwrap().as_str()), 37 | None => (Some($name), 0, $target), 38 | }; 39 | attach_uprobe!(@IMPL, $uprobe, $name, fn_name, offset, target, $opts.pid) 40 | } 41 | }; 42 | 43 | ($uprobe:ident, $name:literal, $target:expr, $opts:ident) => { 44 | attach_uprobe!(@IMPL, $uprobe, $name, Some($name), 0, $target, $opts.pid) 45 | }; 46 | 47 | (@IMPL, $uprobe:ident, $name:literal, $fn_name:expr, $offset:expr, $target:expr, $pid:expr) => { 48 | $uprobe 49 | .attach_uprobe($fn_name, $offset, $target, $pid) 50 | .map_err(|e| anyhow!("error attaching to `{}`: {:?}", $name, e)) 51 | }; 52 | } 53 | 54 | macro_rules! output { 55 | ($comm:expr, $pid:expr, $($args:tt)*) => { 56 | { 57 | let mut stdout = io::stdout(); 58 | let comm = unsafe { CStr::from_ptr($comm.as_ptr()) } 59 | .to_str() 60 | .unwrap(); 61 | write!(&mut stdout, "{} {}[{}] ", now(), comm, $pid).unwrap(); 62 | write!(&mut stdout, $($args)*).unwrap(); 63 | write!(&mut stdout, "\n").unwrap(); 64 | } 65 | }; 66 | } 67 | 68 | fn main() -> Result<(), anyhow::Error> { 69 | let mut opts = Opts::from_args(); 70 | let target_libs: HashSet = if !opts.libs.is_empty() { 71 | opts.libs.drain(..).collect() 72 | } else { 73 | TLS_LIBS.iter().cloned().map(String::from).collect() 74 | }; 75 | 76 | let mut runtime = Runtime::new()?; 77 | let _ = runtime.block_on(async { 78 | let mut loader = 79 | Loader::load(probe_code()).map_err(|e| anyhow!("Error loading probes: {:?}", e))?; 80 | let target_comm_set = opts.command.is_some(); 81 | let mut target_comm = [0i8; COMM_LEN]; 82 | if let Some(command) = opts 83 | .command 84 | .as_ref() 85 | .and_then(|c| Path::new(c).file_name()) 86 | .and_then(|c| c.to_str()) 87 | { 88 | let len = cmp::min(command.len(), COMM_LEN); 89 | let cmd = command[..len].as_bytes(); 90 | target_comm[..len] 91 | .copy_from_slice(unsafe { slice::from_raw_parts(cmd.as_ptr() as *const i8, len) }); 92 | } 93 | 94 | let config = BPFHashMap::::new(loader.map("config").unwrap()).unwrap(); 95 | config.set( 96 | CONFIG_KEY, 97 | Config { 98 | target_comm_set: target_comm_set as usize, 99 | target_comm, 100 | }, 101 | ); 102 | 103 | let conf = opts.config.take().unwrap_or_default(); 104 | let libssl = conf 105 | .openssl 106 | .libssl 107 | .as_ref() 108 | .map(|s| s.as_str()) 109 | .unwrap_or("libssl"); 110 | let libssl3 = conf 111 | .nss 112 | .libssl3 113 | .as_ref() 114 | .map(|s| s.as_str()) 115 | .unwrap_or("libssl3"); 116 | let libnspr4 = conf 117 | .nss 118 | .libnspr4 119 | .as_ref() 120 | .map(|s| s.as_str()) 121 | .unwrap_or("libnspr4"); 122 | 123 | // attach the uprobes 124 | for uprobe in loader.uprobes_mut() { 125 | match uprobe.name().as_str() { 126 | "getaddrinfo" | "getaddrinfo_ret" => { 127 | attach_uprobe!(uprobe, "getaddrinfo", "libc", opts)?; 128 | } 129 | "connect" => { 130 | attach_uprobe!(uprobe, "connect", "libpthread", opts)?; 131 | } 132 | // OpenSSL 133 | "SSL_set_fd" if target_libs.contains("openssl") => { 134 | attach_uprobe!(uprobe, "SSL_set_fd", libssl, conf.openssl.SSL_set_fd, opts)?; 135 | } 136 | "SSL_read" | "SSL_read_ret" if target_libs.contains("openssl") => { 137 | attach_uprobe!(uprobe, "SSL_read", libssl, conf.openssl.SSL_read, opts)?; 138 | } 139 | "SSL_write" if target_libs.contains("openssl") => { 140 | attach_uprobe!(uprobe, "SSL_write", libssl, conf.openssl.SSL_write, opts)?; 141 | } 142 | // NSS 143 | "SSL_SetURL" if target_libs.contains("nss") => { 144 | attach_uprobe!(uprobe, "SSL_SetURL", libssl3, conf.nss.SSL_SetURL, opts)?; 145 | } 146 | "nss_read" | "nss_read_ret" if target_libs.contains("nss") => { 147 | attach_uprobe!(uprobe, "PR_Read", libnspr4, conf.nss.PR_Read, opts)?; 148 | attach_uprobe!(uprobe, "PR_Recv", libnspr4, conf.nss.PR_Recv, opts)?; 149 | attach_uprobe!(uprobe, "PR_RecvFrom", libnspr4, conf.nss.PR_RecvFrom, opts)?; 150 | } 151 | "nss_write" if target_libs.contains("nss") => { 152 | attach_uprobe!(uprobe, "PR_Write", libnspr4, conf.nss.PR_Write, opts)?; 153 | attach_uprobe!(uprobe, "PR_Send", libnspr4, conf.nss.PR_Send, opts)?; 154 | } 155 | _ => continue, 156 | } 157 | } 158 | 159 | let mut state = ObservedState::new(); 160 | let mut buffers = Buffers::new(); 161 | tokio::spawn(async move { 162 | while let Some((name, events)) = loader.events.next().await { 163 | for event in events { 164 | match unsafe { Event::from_raw(&name, event) } { 165 | Event::DNS(event) => { 166 | let host = unsafe { CStr::from_ptr(event.host.as_ptr()) } 167 | .to_str() 168 | .unwrap(); 169 | let ip = Ipv4Addr::from((event.addr as u32).to_ne_bytes()); 170 | output!(event.comm, event.pid, "Resolved {} to {}", host, ip); 171 | state.record_dns(host.to_string(), vec![ip]); 172 | } 173 | Event::Connection(conn) => { 174 | let ip = Ipv4Addr::from(conn.addr.to_ne_bytes()); 175 | let addr = SocketAddrV4::new(ip, conn.port as u16); 176 | state.record_connection(conn.fd as i32, addr); 177 | output!( 178 | conn.comm, 179 | conn.pid, 180 | "Connected to {}", 181 | state.format_address(&addr) 182 | ); 183 | } 184 | Event::SetFd(event) => { 185 | state.record_ssl_fd(event.ssl_ctx, event.fd as i32); 186 | } 187 | Event::SetHost(event) => { 188 | let host = unsafe { CStr::from_ptr(event.host.as_ptr()) } 189 | .to_str() 190 | .unwrap(); 191 | state.record_ssl_host(event.ssl_ctx, host.to_string()); 192 | output!( 193 | event.comm, 194 | event.pid, 195 | "SSL context 0x{:x} connected to {}", 196 | event.ssl_ctx, 197 | host 198 | ); 199 | } 200 | Event::Buffer(buf) => { 201 | if let Some(data) = buffers.push(&buf) { 202 | let addr = state 203 | .lookup_ssl_fd(&buf.ssl_ctx) 204 | .and_then(|fd| state.address_by_fd(fd)); 205 | let addr = if let Some(addr) = addr { 206 | state.format_address(&addr) 207 | } else if let Some(host) = state.lookup_ssl_host(&buf.ssl_ctx) { 208 | host.to_string() 209 | } else { 210 | "".to_string() 211 | }; 212 | output!( 213 | buf.comm, 214 | buf.pid, 215 | "{} {} bytes {} {}", 216 | if buf.mode == AccessMode::Read { 217 | "Read" 218 | } else { 219 | "Write" 220 | }, 221 | buf.len, 222 | if buf.mode == AccessMode::Read { 223 | "from" 224 | } else { 225 | "to" 226 | }, 227 | addr 228 | ); 229 | 230 | if opts.hex_dump { 231 | for line in hexdump_iter(&data) { 232 | println!("{} {}", now(), line); 233 | } 234 | } 235 | } 236 | } 237 | } 238 | } 239 | } 240 | }); 241 | Ok::<(), anyhow::Error>(signal::ctrl_c().await?) 242 | })?; 243 | 244 | Ok(()) 245 | } 246 | struct Buffers { 247 | buffers: HashMap<(usize, AccessMode), Vec>, 248 | } 249 | 250 | impl Buffers { 251 | fn new() -> Self { 252 | Buffers { 253 | buffers: HashMap::new(), 254 | } 255 | } 256 | 257 | fn push(&mut self, ssl_buf: &SSLBuffer) -> Option> { 258 | let buf = self 259 | .buffers 260 | .entry((ssl_buf.ssl_ctx, ssl_buf.mode)) 261 | .or_insert_with(Vec::new); 262 | let len = ssl_buf.chunk_len; 263 | if len > 0 { 264 | buf.extend(&ssl_buf.chunk[..len]); 265 | None 266 | } else { 267 | Some(buf.drain(..).collect()) 268 | } 269 | } 270 | } 271 | 272 | #[allow(non_snake_case)] 273 | #[derive(Debug, Deserialize)] 274 | struct OpenSSLConfig { 275 | libssl: Option, 276 | SSL_set_fd: Option, 277 | SSL_read: Option, 278 | SSL_write: Option, 279 | } 280 | 281 | impl Default for OpenSSLConfig { 282 | fn default() -> OpenSSLConfig { 283 | OpenSSLConfig { 284 | libssl: None, 285 | SSL_set_fd: None, 286 | SSL_read: None, 287 | SSL_write: None, 288 | } 289 | } 290 | } 291 | 292 | #[allow(non_snake_case)] 293 | #[derive(Debug, Deserialize)] 294 | struct NSSConfig { 295 | libssl3: Option, 296 | libnspr4: Option, 297 | SSL_SetURL: Option, 298 | PR_Read: Option, 299 | PR_Recv: Option, 300 | PR_RecvFrom: Option, 301 | PR_Write: Option, 302 | PR_Send: Option, 303 | } 304 | 305 | impl Default for NSSConfig { 306 | fn default() -> NSSConfig { 307 | NSSConfig { 308 | libssl3: None, 309 | libnspr4: None, 310 | SSL_SetURL: None, 311 | PR_Read: None, 312 | PR_Recv: None, 313 | PR_RecvFrom: None, 314 | PR_Write: None, 315 | PR_Send: None, 316 | } 317 | } 318 | } 319 | 320 | #[derive(Debug, Deserialize)] 321 | struct ConfigFile { 322 | #[serde(default)] 323 | openssl: OpenSSLConfig, 324 | #[serde(default)] 325 | nss: NSSConfig, 326 | } 327 | 328 | impl FromStr for ConfigFile { 329 | type Err = anyhow::Error; 330 | 331 | fn from_str(file: &str) -> Result { 332 | let config = fs::read_to_string(file)?; 333 | Ok(toml::from_str(&config)?) 334 | } 335 | } 336 | 337 | impl Default for ConfigFile { 338 | fn default() -> ConfigFile { 339 | ConfigFile { 340 | openssl: Default::default(), 341 | nss: Default::default(), 342 | } 343 | } 344 | } 345 | 346 | #[derive(Debug, StructOpt)] 347 | #[structopt(name = "snuffy", about = "Sniff TLS data")] 348 | struct Opts { 349 | #[structopt(short = "p", long = "pid")] 350 | pid: Option, 351 | #[structopt(short = "c", long = "command")] 352 | command: Option, 353 | #[structopt(short = "d", long = "hex-dump")] 354 | hex_dump: bool, 355 | #[structopt(long = "libs", help="Which TLS libraries to attach to", 356 | possible_values = &TLS_LIBS)] 357 | libs: Vec, 358 | #[structopt(long = "config", parse(try_from_str))] 359 | config: Option, 360 | } 361 | 362 | fn now() -> String { 363 | OffsetDateTime::now_local().format("[%T]") 364 | } 365 | 366 | fn probe_code() -> &'static [u8] { 367 | include_bytes!(concat!( 368 | env!("OUT_DIR"), 369 | "/target/bpf/programs/snuffy/snuffy.elf" 370 | )) 371 | } 372 | 373 | struct ObservedState { 374 | fd_conns: HashMap, 375 | dns: HashMap>, 376 | rev_dns: HashMap, 377 | ssl_hosts: HashMap, 378 | ssl_fds: HashMap, 379 | } 380 | 381 | impl ObservedState { 382 | fn new() -> Self { 383 | ObservedState { 384 | fd_conns: HashMap::new(), 385 | dns: HashMap::new(), 386 | rev_dns: HashMap::new(), 387 | ssl_hosts: HashMap::new(), 388 | ssl_fds: HashMap::new(), 389 | } 390 | } 391 | 392 | fn record_connection(&mut self, fd: i32, address: SocketAddrV4) { 393 | self.fd_conns.insert(fd, address); 394 | } 395 | 396 | fn address_by_fd(&self, fd: &i32) -> Option<&SocketAddrV4> { 397 | self.fd_conns.get(fd) 398 | } 399 | 400 | fn record_dns(&mut self, host: String, ips: Vec) { 401 | for ip in &ips { 402 | self.rev_dns.insert(ip.clone(), host.clone()); 403 | } 404 | self.dns.insert(host, ips); 405 | } 406 | 407 | fn lookup_name(&self, host: &str) -> Option<&Vec> { 408 | self.dns.get(host) 409 | } 410 | 411 | fn lookup_ip(&self, ip: &Ipv4Addr) -> Option<&str> { 412 | self.rev_dns.get(ip).map(|s| s.as_str()) 413 | } 414 | 415 | fn record_ssl_host(&mut self, ssl_ctx: usize, host: String) { 416 | self.ssl_hosts.insert(ssl_ctx, host); 417 | } 418 | 419 | fn lookup_ssl_host(&self, ssl_ctx: &usize) -> Option<&str> { 420 | self.ssl_hosts.get(ssl_ctx).map(|s| s.as_str()) 421 | } 422 | 423 | fn record_ssl_fd(&mut self, ssl_ctx: usize, fd: i32) { 424 | self.ssl_fds.insert(ssl_ctx, fd); 425 | } 426 | 427 | fn lookup_ssl_fd(&self, ssl_ctx: &usize) -> Option<&i32> { 428 | self.ssl_fds.get(ssl_ctx) 429 | } 430 | 431 | fn format_address(&self, addr: &SocketAddrV4) -> String { 432 | let host = self.lookup_ip(addr.ip()); 433 | if let Some(host) = host { 434 | format!("{}:{} ({})", host, addr.port(), addr) 435 | } else { 436 | format!("{}", addr) 437 | } 438 | } 439 | } 440 | 441 | enum Event { 442 | DNS(DNS), 443 | Connection(Connection), 444 | SetFd(SSLFd), 445 | SetHost(SSLHost), 446 | Buffer(SSLBuffer), 447 | } 448 | 449 | impl Event { 450 | unsafe fn from_raw(ty_name: &str, raw: Box<[u8]>) -> Event { 451 | match ty_name { 452 | "dns" => Event::DNS(ptr::read(raw.as_ptr() as *const DNS)), 453 | "connection" => Event::Connection(ptr::read(raw.as_ptr() as *const Connection)), 454 | "ssl_fd" => Event::SetFd(ptr::read(raw.as_ptr() as *const SSLFd)), 455 | "ssl_host" => Event::SetHost(ptr::read(raw.as_ptr() as *const SSLHost)), 456 | "ssl_buffer" => Event::Buffer(ptr::read(raw.as_ptr() as *const SSLBuffer)), 457 | _ => panic!("unexpected raw"), 458 | } 459 | } 460 | } 461 | 462 | #[cfg(test)] 463 | mod tests { 464 | use super::*; 465 | 466 | #[test] 467 | fn test_record_connection() { 468 | let mut state = ObservedState::new(); 469 | let ip = Ipv4Addr::new(127, 0, 0, 1); 470 | let port = 1234; 471 | let addr = SocketAddrV4::new(ip, port); 472 | state.record_connection(1, addr.clone()); 473 | 474 | assert_eq!(state.address_by_fd(&0), None); 475 | assert_eq!(state.address_by_fd(&1), Some(&addr)); 476 | } 477 | 478 | #[test] 479 | fn test_record_dns() { 480 | let mut state = ObservedState::new(); 481 | let host = "example.com".to_string(); 482 | let ip = Ipv4Addr::new(127, 0, 0, 1); 483 | state.record_dns(host, vec![ip]); 484 | 485 | assert_eq!(state.lookup_name("confused.ai"), None); 486 | assert_eq!(state.lookup_name("example.com"), Some(&vec![ip])); 487 | assert_eq!(state.lookup_ip(&Ipv4Addr::new(1, 1, 1, 1)), None); 488 | assert_eq!( 489 | state.lookup_ip(&Ipv4Addr::new(127, 0, 0, 1)), 490 | Some("example.com") 491 | ); 492 | } 493 | 494 | #[test] 495 | fn test_record_ssl_host() { 496 | let mut state = ObservedState::new(); 497 | let ctx_addr = 1234; 498 | let host = "example.com".to_string(); 499 | state.record_ssl_host(ctx_addr, host); 500 | 501 | assert_eq!(state.lookup_ssl_host(&1111usize), None); 502 | assert_eq!(state.lookup_ssl_host(&ctx_addr), Some("example.com")); 503 | } 504 | 505 | #[test] 506 | fn test_record_ssl_fd() { 507 | let mut state = ObservedState::new(); 508 | let ctx_addr = 1234; 509 | let fd = 0; 510 | state.record_ssl_fd(ctx_addr, fd); 511 | 512 | assert_eq!(state.lookup_ssl_fd(&1111usize), None); 513 | assert_eq!(state.lookup_ssl_fd(&ctx_addr), Some(&0)); 514 | } 515 | } 516 | --------------------------------------------------------------------------------