Skip to content

Commit

Permalink
Add ebpf based dns lookup hooks
Browse files Browse the repository at this point in the history
When using DoT or DoH opensnitch cannot intercept the dns packets.
Therfore 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.
  • Loading branch information
calesanz committed Feb 12, 2022
1 parent feaf8de commit 32965e2
Show file tree
Hide file tree
Showing 4 changed files with 423 additions and 1 deletion.
181 changes: 181 additions & 0 deletions daemon/dns/ebpfhook.go
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <link.h>
#include <dlfcn.h>
#include <string.h>
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
}
11 changes: 11 additions & 0 deletions daemon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion ebpf_prog/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 32965e2

Please sign in to comment.