From a4b7f578062d4fa083fe977517a888caf22d1e11 Mon Sep 17 00:00:00 2001 From: calesanz <8714917+calesanz@users.noreply.github.com> Date: Tue, 15 Feb 2022 21:25:35 +0100 Subject: [PATCH] Add ebpf based dns lookup hooks (#582) When using DoT or DoH opensnitch cannot intercept the dns packets. Therefore the UI always shows IP addresses instead of hostnames. To fix this issue an ebpf (uprobe) filter was created to hook getaddrinfo and gethostbyname calls. In order to be independent of libbcc an additional module was added to ebpf_prog. Without libbcc the libc function offsets must be resolved manually. In order to find the loaded glibc version some cgo code was added. --- daemon/dns/ebpfhook.go | 181 +++++++++++++++++++++++++++++ daemon/main.go | 11 ++ ebpf_prog/Makefile | 2 +- ebpf_prog/opensnitch-dns.c | 230 +++++++++++++++++++++++++++++++++++++ 4 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 daemon/dns/ebpfhook.go create mode 100644 ebpf_prog/opensnitch-dns.c diff --git a/daemon/dns/ebpfhook.go b/daemon/dns/ebpfhook.go new file mode 100644 index 0000000000..20754e61e7 --- /dev/null +++ b/daemon/dns/ebpfhook.go @@ -0,0 +1,181 @@ +package dns + +import ( + "bytes" + "debug/elf" + "encoding/binary" + "errors" + "fmt" + "net" + "os" + "os/signal" + "strings" + + "github.com/evilsocket/opensnitch/daemon/log" + bpf "github.com/iovisor/gobpf/elf" +) + +/* +#cgo LDFLAGS: -ldl + +#define _GNU_SOURCE +#include +#include +#include +#include +#include + +char* find_libc() { + void *handle; + struct link_map * map; + + handle = dlopen(NULL, RTLD_NOW); + if (handle == NULL) { + fprintf(stderr, "EBPF-DNS dlopen() failed: %s\n", dlerror()); + return NULL; + } + + + if (dlinfo(handle, RTLD_DI_LINKMAP, &map) == -1) { + fprintf(stderr, "EBPF-DNS: dlinfo failed: %s\n", dlerror()); + return NULL; + } + + while(1){ + if(map == NULL){ + break; + } + + if(strstr(map->l_name, "libc.so")){ + fprintf(stderr,"found %s\n", map->l_name); + return map->l_name; + } + map = map->l_next; + } + return NULL; +} + + +*/ +import "C" + +type nameLookupEvent struct { + AddrType uint32 + Ip [16]uint8 + Host [252]byte +} + +func findLibc() (string, error) { + ret := C.find_libc() + + if ret == nil { + return "", errors.New("Could not find path to libc.so") + } + str := C.GoString(ret) + + return str, nil +} + +// Iterates over all symbols in an elf file and returns the offset matching the provided symbol name. +func lookupSymbol(elffile *elf.File, symbolName string) (uint64, error) { + symbols, err := elffile.Symbols() + if err != nil { + return 0, err + } + for _, symb := range symbols { + if symb.Name == symbolName { + return symb.Value, nil + } + } + return 0, errors.New(fmt.Sprintf("Symbol: '%s' not found.", symbolName)) +} + +func DnsListenerEbpf() error { + + m := bpf.NewModule("/etc/opensnitchd/opensnitch-dns.o") + if err := m.Load(nil); err != nil { + log.Error("EBPF-DNS: Failed to load /etc/opensnitchd/opensnitch-dns.o: %v", err) + return err + } + defer m.Close() + + // libbcc resolves the offsets for us. without bcc the offset for uprobes must parsed from the elf files + // some how 0 must be replaced with the offset of getaddrinfo bcc does this using bcc_resolve_symname + + // Attaching to uprobe using perf open might be a better aproach requires https://github.com/iovisor/gobpf/pull/277 + libcFile, err := findLibc() + + if err != nil { + log.Error("EBPF-DNS: Failed to find libc.so: %v", err) + return err + } + + libc_elf, err := elf.Open(libcFile) + if err != nil { + log.Error("EBPF-DNS: Failed to open %s: %v", libcFile, err) + return err + } + probes_attached := 0 + for uprobe := range m.IterUprobes() { + probeFunction := strings.Replace(uprobe.Name, "uretprobe/", "", 1) + probeFunction = strings.Replace(probeFunction, "uprobe/", "", 1) + offset, err := lookupSymbol(libc_elf, probeFunction) + if err != nil { + log.Warning("EBPF-DNS: Failed to find symbol for uprobe %s : %s\n", uprobe.Name, err) + continue + } + err = bpf.AttachUprobe(uprobe, libcFile, offset) + if err != nil { + log.Error("EBPF-DNS: Failed to attach uprobe %s : %s\n", uprobe.Name, err) + return err + } + probes_attached++ + } + + if probes_attached == 0 { + log.Warning("EBPF-DNS: Failed to find symbols for uprobes.") + return errors.New("Failed to find symbols for uprobes.") + } + + // Reading Events + channel := make(chan []byte) + //log.Warning("EBPF-DNS: %+v\n", m) + perfMap, err := bpf.InitPerfMap(m, "events", channel, nil) + if err != nil { + log.Error("EBPF-DNS: Failed to init perf map: %s\n", err) + return err + } + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, os.Kill) + + go func() { + var event nameLookupEvent + for { + data := <-channel + log.Debug("EBPF-DNS: LookupEvent %d %x %x %x", len(data), data[:4], data[4:20], data[20:]) + err := binary.Read(bytes.NewBuffer(data), binary.LittleEndian, &event) + if err != nil { + log.Warning("EBPF-DNS: Failed to decode ebpf nameLookupEvent: %s\n", err) + continue + } + // Convert C string (null-terminated) to Go string + host := string(event.Host[:bytes.IndexByte(event.Host[:], 0)]) + var ip net.IP + // 2 -> AF_INET (ipv4) + if event.AddrType == 2 { + ip = net.IP(event.Ip[:4]) + } else { + ip = net.IP(event.Ip[:]) + } + + log.Debug("EBPF-DNS: Tracking Resolved Message: %s -> %s\n", host, ip.String()) + Track(ip.String(), host) + } + }() + + perfMap.PollStart() + <-sig + log.Info("EBPF-DNS: Received signal: terminating ebpf dns hook.") + perfMap.PollStop() + return nil +} diff --git a/daemon/main.go b/daemon/main.go index 254e65d4dd..b25288f909 100644 --- a/daemon/main.go +++ b/daemon/main.go @@ -257,6 +257,10 @@ func acceptOrDeny(packet *netfilter.Packet, con *conman.Connection) *rule.Rule { } packet = &pkt + // Update the hostname again. + // This is required due to a race between the ebpf dns hook and the actual first packet beeing sent + con.DstHost = dns.HostOr(con.DstIP, con.DstHost) + r = uiClient.Ask(con) if r == nil { log.Error("Invalid rule received, applying default action") @@ -396,6 +400,13 @@ func main() { } } + go func() { + err := dns.DnsListenerEbpf() + if err != nil { + log.Warning("EBPF-DNS: Unable to attach ebpf listener.") + } + }() + log.Info("Running on netfilter queue #%d ...", queueNum) for { select { diff --git a/ebpf_prog/Makefile b/ebpf_prog/Makefile index 934951c249..0213fc41c0 100644 --- a/ebpf_prog/Makefile +++ b/ebpf_prog/Makefile @@ -11,7 +11,7 @@ LIBBPF = $(TOOLS_PATH)/lib/bpf/libbpf.a CGROUP_HELPERS := ../../tools/testing/selftests/bpf/cgroup_helpers.o TRACE_HELPERS := ../../tools/testing/selftests/bpf/trace_helpers.o -always-y += opensnitch.o +always-y += opensnitch.o opensnitch-dns.o ifeq ($(ARCH), arm) # Strip all except -D__LINUX_ARM_ARCH__ option needed to handle linux diff --git a/ebpf_prog/opensnitch-dns.c b/ebpf_prog/opensnitch-dns.c new file mode 100644 index 0000000000..7be2d35fec --- /dev/null +++ b/ebpf_prog/opensnitch-dns.c @@ -0,0 +1,230 @@ +#define KBUILD_MODNAME "dummy" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAPSIZE 12000 + +//-------------------------------map definitions +// which github.com/iovisor/gobpf/elf expects +#define BUF_SIZE_MAP_NS 256 + +typedef struct bpf_map_def { + unsigned int type; + unsigned int key_size; + unsigned int value_size; + unsigned int max_entries; + unsigned int map_flags; + unsigned int pinning; + char namespace[BUF_SIZE_MAP_NS]; +} bpf_map_def; + +enum bpf_pin_type { + PIN_NONE = 0, + PIN_OBJECT_NS, + PIN_GLOBAL_NS, + PIN_CUSTOM_NS, +}; +//----------------------------------- + +#define MAX_ALIASES 5 +#define MAX_IPS 5 + +struct nameLookupEvent { + u32 addr_type; + u8 ip[16]; + char host[252]; +} __attribute__((packed)); + +struct hostent { + char *h_name; /* Official name of host. */ + char **h_aliases; /* Alias list. */ + int h_addrtype; /* Host address type. */ + int h_length; /* Length of address. */ + char **h_addr_list; /* List of addresses from name server. */ +#ifdef __USE_MISC +#define h_addr h_addr_list[0] /* Address, for backward compatibility.*/ +#endif +}; + +struct addrinfo { + int ai_flags; /* Input flags. */ + int ai_family; /* Protocol family for socket. */ + int ai_socktype; /* Socket type. */ + int ai_protocol; /* Protocol for socket. */ + size_t ai_addrlen; /* Length of socket address. */ + struct sockaddr *ai_addr; /* Socket address for socket. */ + char *ai_canonname; /* Canonical name for service location. */ + struct addrinfo *ai_next; /* Pointer to next in list. */ +}; + +struct addrinfo_args_cache { + struct addrinfo **addrinfo_ptr; + char node[256]; +}; +// define temporary array for data +struct bpf_map_def SEC("maps/addrinfo_args_hash") addrinfo_args_hash = { + .type = BPF_MAP_TYPE_HASH, + .max_entries = MAPSIZE, + .key_size = sizeof(u32), + .value_size = sizeof(struct addrinfo_args_cache), +}; + +// BPF output events +struct bpf_map_def SEC("maps/events") events = { + .type = BPF_MAP_TYPE_PERF_EVENT_ARRAY, + .key_size = sizeof(u32), + .value_size = sizeof(u32), + .max_entries = MAPSIZE, +}; + +/** + * Hooks gethostbyname calls and emits multiple nameLookupEvent events. + * It supports at most MAX_IPS many addresses. + */ +SEC("uretprobe/gethostbyname") +int uretprobe__gethostbyname(struct pt_regs *ctx) { + // bpf_tracing_prinkt("Called gethostbyname %d\n",1); + struct nameLookupEvent data = {0}; + + if (!PT_REGS_RC(ctx)) + return 0; + + struct hostent *host = (struct hostent *)PT_REGS_RC(ctx); + char * hostnameptr; + bpf_probe_read(&hostnameptr, sizeof(hostnameptr), &host->h_name); + bpf_probe_read_str(&data.host, sizeof(data.host), hostnameptr); + + char **ips; + bpf_probe_read(&ips, sizeof(ips), &host->h_addr_list); +#pragma clang loop unroll(full) + for (int i = 0; i < MAX_IPS; i++) { + char *ip; + bpf_probe_read(&ip, sizeof(ip), &ips[i]); + + if (ip == NULL) { + return 0; + } + bpf_probe_read_user(&data.addr_type, sizeof(data.addr_type), + &host->h_addrtype); + + if (data.addr_type == AF_INET) { + // Only copy the 4 relevant bytes + bpf_probe_read_user(&data.ip, 4, ip); + } else { + bpf_probe_read_user(&data.ip, sizeof(data.ip), ip); + } + + bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, + sizeof(data)); + + // char **alias = host->h_aliases; + char **aliases; + bpf_probe_read(&aliases, sizeof(aliases), &host->h_aliases); + +#pragma clang loop unroll(full) + for (int j = 0; j < MAX_ALIASES; j++) { + char *alias; + bpf_probe_read(&alias, sizeof(alias), &aliases[i]); + + if (alias == NULL) { + return 0; + } + bpf_probe_read_user(&data.host, sizeof(data.host), alias); + bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, + sizeof(data)); + } + } + + return 0; +} + +// capture getaddrinfo call and store the relevant arguments to a hash. +SEC("uprobe/getaddrinfo") +int addrinfo(struct pt_regs *ctx) { + struct addrinfo_args_cache addrinfo_args = {0}; + if (!PT_REGS_PARM1(ctx)) + return 0; + if (!PT_REGS_PARM4(ctx)) + return 0; + + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tid = (u32)pid_tgid; + + addrinfo_args.addrinfo_ptr = (struct addrinfo **)PT_REGS_PARM4(ctx); + + bpf_probe_read_user_str(&addrinfo_args.node, sizeof(addrinfo_args.node), + (char *)PT_REGS_PARM1(ctx)); + + bpf_map_update_elem(&addrinfo_args_hash, &tid, &addrinfo_args, + 0 /* flags */); + + return 0; +} + +SEC("uretprobe/getaddrinfo") +int ret_addrinfo(struct pt_regs *ctx) { + struct nameLookupEvent data = {0}; + struct addrinfo_args_cache *addrinfo_args = {0}; + + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 tid = (u32)pid_tgid; + + addrinfo_args = bpf_map_lookup_elem(&addrinfo_args_hash, &tid); + + if (addrinfo_args == 0) { + return 0; // missed start + } + + struct addrinfo **res_p; + bpf_probe_read(&res_p, sizeof(res_p), &addrinfo_args->addrinfo_ptr); + +#pragma clang loop unroll(full) + for (int i = 0; i < MAX_IPS; i++) { + struct addrinfo *res; + bpf_probe_read(&res, sizeof(res), res_p); + if (res == NULL) { + return 0; + } + bpf_probe_read(&data.addr_type, sizeof(data.addr_type), + &res->ai_family); + + if (data.addr_type == AF_INET) { + struct sockaddr_in *ipv4; + bpf_probe_read(&ipv4, sizeof(ipv4), &res->ai_addr); + // Only copy the 4 relevant bytes + bpf_probe_read_user(&data.ip, 4, &ipv4->sin_addr); + } else if(data.addr_type == AF_INET6) { + struct sockaddr_in6 *ipv6; + bpf_probe_read(&ipv6, sizeof(ipv6), &res->ai_addr); + + bpf_probe_read_user(&data.ip, sizeof(data.ip), &ipv6->sin6_addr); + } else { + return 1; + } + + bpf_probe_read_kernel_str(&data.host, sizeof(data.host), + &addrinfo_args->node); + + bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &data, + sizeof(data)); + + + struct addrinfo * next; + bpf_probe_read(&next, sizeof(next), &res->ai_next); + res_p = &next; + } + + return 0; +} + +char _license[] SEC("license") = "GPL"; +u32 _version SEC("version") = 0xFFFFFFFE;