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;