1#!/usr/bin/python 2# author: https://github.com/agentzex 3# Licensed under the Apache License, Version 2.0 (the "License") 4 5# tcp_mon_block.py - uses netlink TC, kernel tracepoints and kprobes to monitor outgoing connections from given PIDs 6# and block connections to all addresses initiated from them (acting like an in-process firewall), unless they are listed in allow_list 7 8# outputs blocked connections attempts from monitored processes 9# Usage: 10# python3 tcp_mon_block.py -i network_interface_name 11# python3 tcp_mon_block.py -v -i network_interface_name (-v --verbose - will output all connections attempts, including allowed ones) 12# 13 14 15from bcc import BPF 16import pyroute2 17import socket 18import struct 19import json 20import argparse 21from urllib.parse import urlparse 22 23 24# TCP flags 25FIN = 0x01 26SYN = 0x02 27RST = 0x04 28PSH = 0x08 29ACK = 0x10 30URG = 0x20 31ECE = 0x40 32CWR = 0x80 33 34 35verbose_states = { 36 1: "Connection not allowed detected - forwarding to block", 37 2: "Connection allowed", 38 3: "Connection destroyed", 39} 40 41 42def get_verbose_message(state): 43 if state not in verbose_states: 44 return "" 45 46 return verbose_states[state] 47 48 49def parse_tcp_flags(flags): 50 found_flags = "" 51 if flags & FIN: 52 found_flags += "FIN; " 53 if flags & SYN: 54 found_flags += "SYN; " 55 if flags & RST: 56 found_flags += "RST; " 57 if flags & PSH: 58 found_flags += "PSH; " 59 if flags & ACK: 60 found_flags += "ACK; " 61 if flags & URG: 62 found_flags += "URG; " 63 if flags & ECE: 64 found_flags += "ECE; " 65 if flags & CWR: 66 found_flags += "CWR;" 67 68 return found_flags 69 70 71def ip_to_network_address(ip): 72 return struct.unpack("I", socket.inet_aton(ip))[0] 73 74 75def network_address_to_ip(ip): 76 return socket.inet_ntop(socket.AF_INET, struct.pack("I", ip)) 77 78 79def parse_address(url_or_ip): 80 is_ipv4 = True 81 domain = "" 82 83 #first check if valid ipv4 84 try: 85 socket.inet_aton(url_or_ip) 86 except socket.error: 87 is_ipv4 = False 88 89 if is_ipv4: 90 return [url_or_ip] 91 92 # if not check if valid URL, parse and get its domain, resolve it to IPv4 and return it 93 try: 94 domain = urlparse(url_or_ip).netloc 95 except: 96 print(f"[-] {url_or_ip} is invalid IPv4 or URL") 97 return False 98 99 # should get a list of IPv4 addresses resolved from the domain 100 try: 101 hostname, aliaslist, ipaddrlist = socket.gethostbyname_ex(domain) 102 except: 103 print(f"[-] Failed to resolve {url_or_ip} to Ipv4") 104 return False 105 106 return ipaddrlist 107 108 109def create_bpf_allow_list(bpf): 110 bpf_allow_list = bpf.get_table("allow_list") 111 bpf_pid_list = bpf.get_table("pid_list") 112 with open("allow_list.json", "r") as f: 113 pids_to_list = json.loads(f.read()) 114 115 print("[+] Reading and parsing allow_list.json") 116 for pid_to_list in pids_to_list: 117 try: 118 pid = int(pid_to_list["pid"]) 119 except ValueError: 120 print(f"[-] invalid PID: {pid_to_list['pid']}") 121 continue 122 123 print(f"[+] Adding {pid} to monitored processes") 124 bpf_pid_list[bpf_pid_list.Key(pid)] = bpf_pid_list.Leaf(pid) 125 126 for url_or_ip in pid_to_list["allow_list"]: 127 ips = parse_address(url_or_ip) 128 if not ips: 129 continue 130 for ip in ips: 131 print(f"[+] Adding {ip} to allowed IPs") 132 ip = ip_to_network_address(ip) 133 bpf_allow_list[bpf_allow_list.Key(ip)] = bpf_allow_list.Leaf(ip) 134 135 136def create_tc(interface): 137 ip = pyroute2.IPRoute() 138 ipdb = pyroute2.IPDB(nl=ip) 139 try: 140 idx = ipdb.interfaces[interface].index 141 except: 142 print(f"[-] {interface} interface not found") 143 return False, False, False 144 145 try: 146 # deleting if exists from previous run 147 ip.tc("del", "clsact", idx) 148 except: 149 pass 150 ip.tc("add", "clsact", idx) 151 return ip, ipdb, idx 152 153 154def parse_blocked_event(cpu, data, size): 155 event = bpf["blocked_events"].event(data) 156 src_ip = network_address_to_ip(event.src_ip) 157 dst_ip = network_address_to_ip(event.dst_ip) 158 flags = parse_tcp_flags(event.tcp_flags) 159 print(f"{event.pid}: {event.comm.decode()} - {src_ip}:{event.src_port} -> {dst_ip}:{event.dst_port} Flags: {flags} was blocked!") 160 161 162def parse_verbose_event(cpu, data, size): 163 event = bpf["verbose_events"].event(data) 164 src_ip = network_address_to_ip(event.src_ip) 165 dst_ip = network_address_to_ip(event.dst_ip) 166 verbose_message = get_verbose_message(event.state) 167 print(f"{event.pid}: {event.comm.decode()} - {src_ip}:{event.src_port} -> {dst_ip}:{event.dst_port} - {verbose_message}") 168 169 170 171parser = argparse.ArgumentParser(description="Monitor given PIDs and block outgoing connections to all addresses initiated from them, unless they are listed in allow_list.json") 172parser.add_argument("-i", "--interface", help="Network interface name to monitor traffic on", required=True, type=str) 173parser.add_argument("-v", "--verbose", action="store_true", help="Set verbose output") 174args = parser.parse_args() 175print(f"[+] Monitoring {args.interface} interface") 176 177 178with open("tcp_mon_block.c", "r") as f: 179 bpf_text = f.read() 180 181if args.verbose: 182 print("[+] Verbose output is ON!") 183 bpf_text = bpf_text.replace("static bool VERBOSE_OUTPUT = false", "static bool VERBOSE_OUTPUT = true") 184 185 186ip, ipdb, idx = create_tc(args.interface) 187if not ip: 188 exit(-1) 189 190bpf = BPF(text=bpf_text) 191create_bpf_allow_list(bpf) 192 193# loading kprobe 194bpf.attach_kprobe(event="tcp_connect", fn_name="trace_connect_entry") 195 196# loading TC 197fn = bpf.load_func("handle_egress", BPF.SCHED_CLS) 198 199#default parent handlers: 200#https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/pkt_sched.h?id=1f211a1b929c804100e138c5d3d656992cfd5622 201#define TC_H_MIN_INGRESS 0xFFF2U 202#define TC_H_MIN_EGRESS 0xFFF3U 203 204ip.tc("add-filter", "bpf", idx, ":1", fd=fn.fd, name=fn.name, parent="ffff:fff3", classid=1, direct_action=True) 205bpf["blocked_events"].open_perf_buffer(parse_blocked_event) 206bpf["verbose_events"].open_perf_buffer(parse_verbose_event) 207 208 209print("[+] Monitoring started\n") 210while True: 211 try: 212 bpf.perf_buffer_poll() 213 except KeyboardInterrupt: 214 break 215 216ip.tc("del", "clsact", idx) 217ipdb.release() 218 219 220 221 222 223 224 225 226 227 228