diff --git a/duetector/collectors/models.py b/duetector/collectors/models.py index cf3d86d..3c20111 100644 --- a/duetector/collectors/models.py +++ b/duetector/collectors/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Dict, NamedTuple, Optional +from typing import Any, Dict, NamedTuple, Optional import pydantic @@ -23,7 +23,7 @@ class Tracking(pydantic.BaseModel): fname: Optional[str] = None timestamp: Optional[int] = None - extended: Dict[str, str] = {} + extended: Dict[str, Any] = {} @staticmethod def from_namedtuple(tracer, data: NamedTuple) -> Tracking: # type: ignore diff --git a/duetector/static/config.toml b/duetector/static/config.toml index 2da3fa3..83e450f 100644 --- a/duetector/static/config.toml +++ b/duetector/static/config.toml @@ -32,12 +32,23 @@ exclude_gid = [ [tracer] disabled = false +[tracer.clonetracer] +disabled = false +attach_event = "__x64_sys_clone" +poll_timeout = 10 + +[tracer.tcpconnecttracer] +disabled = false +poll_timeout = 10 + [tracer.unametracer] disabled = false enable_cache = true [tracer.opentracer] disabled = false +attach_event = "do_sys_openat2" +poll_timeout = 10 [collector] disabled = false diff --git a/duetector/tracers/__init__.py b/duetector/tracers/__init__.py index ba0ac70..c7d2655 100644 --- a/duetector/tracers/__init__.py +++ b/duetector/tracers/__init__.py @@ -3,6 +3,6 @@ __all__ = ["BccTracer"] # Expose for plugin system -from . import openat2, uname +from . import clone, openat2, tcpconnect, uname -registers = [openat2, uname] +registers = [openat2, uname, tcpconnect, clone] diff --git a/duetector/tracers/base.py b/duetector/tracers/base.py index 4c593a4..ba7f703 100644 --- a/duetector/tracers/base.py +++ b/duetector/tracers/base.py @@ -66,7 +66,7 @@ class BccTracer(Tracer): **Tracer.default_config, } - attach_type: str + attach_type: Optional[str] = None attatch_args: Dict[str, str] = {} many_attatchs: List[Tuple[str, Dict[str, str]]] = [] poll_fn: str @@ -86,6 +86,8 @@ def _convert_data(self, data) -> NamedTuple: return self.data_t(**args) # type: ignore def _attatch(self, host, attatch_type, attatch_args): + if not attatch_type: + return attatcher = getattr(host, f"attach_{attatch_type}") # Prevent AttributeError attatch_args = attatch_args or {} @@ -95,11 +97,6 @@ def attach(self, host): if self.disabled: raise TreacerDisabledError("Tracer is disabled") - if not self.attach_type: - # No need to attach, in this case, function name indicates - # More: https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md - return - attatch_list = [*self.many_attatchs, (self.attach_type, self.attatch_args)] for attatch_type, attatch_args in attatch_list: diff --git a/duetector/tracers/clone.py b/duetector/tracers/clone.py new file mode 100644 index 0000000..39f6d66 --- /dev/null +++ b/duetector/tracers/clone.py @@ -0,0 +1,95 @@ +from collections import namedtuple +from typing import Callable, NamedTuple + +from duetector.extension.tracer import hookimpl +from duetector.tracers.base import BccTracer + + +class CloneTracer(BccTracer): + """ + A tracer for clone syscall + """ + + default_config = { + **BccTracer.default_config, + "attach_event": "__x64_sys_clone", + "poll_timeout": 10, + } + attach_type = "kprobe" + + @property + def attatch_args(self): + return {"fn_name": "do_trace", "event": self.config.attach_event} + + poll_fn = "perf_buffer_poll" + + @property + def poll_args(self): + return {"timeout": int(self.config.poll_timeout)} + + data_t = namedtuple("CloneTracking", ["pid", "timestamp", "comm"]) + prog = """ + #include + + // define output data structure in C + struct data_t { + u32 pid; + u32 uid; + u32 gid; + u64 timestamp; + char comm[TASK_COMM_LEN]; + }; + BPF_PERF_OUTPUT(events); + + int do_trace(struct pt_regs *ctx) { + struct data_t data = {}; + + data.pid = bpf_get_current_pid_tgid(); + data.uid = bpf_get_current_uid_gid(); + data.gid = bpf_get_current_uid_gid() >> 32; + data.timestamp = bpf_ktime_get_ns(); + bpf_get_current_comm(&data.comm, sizeof(data.comm)); + + events.perf_submit(ctx, &data, sizeof(data)); + + return 0; + } + """ + + def set_callback(self, host, callback: Callable[[NamedTuple], None]): + def _(ctx, data, size): + event = host["events"].event(data) + return callback(self._convert_data(event)) # type: ignore + + host["events"].open_perf_buffer(_) + + +@hookimpl +def init_tracer(config): + return CloneTracer(config) + + +if __name__ == "__main__": + from bcc import BPF + + b = BPF(text=CloneTracer.prog) + + tracer = CloneTracer() + tracer.attach(b) + start = 0 + + def print_callback(data: NamedTuple): + global start + if start == 0: + print(f"[{data.comm} ({data.pid})] 0 ") + else: + print(f"[{data.comm} ({data.pid}) {data.gid} {data.uid}] {(data.timestamp-start)/1000000000}") # type: ignore + start = data.timestamp + + tracer.set_callback(b, print_callback) + poller = tracer.get_poller(b) + while True: + try: + poller(**tracer.poll_args) + except KeyboardInterrupt: + exit() diff --git a/duetector/tracers/openat2.py b/duetector/tracers/openat2.py index a853f26..b367314 100644 --- a/duetector/tracers/openat2.py +++ b/duetector/tracers/openat2.py @@ -10,10 +10,25 @@ class OpenTracer(BccTracer): A tracer for openat2 syscall """ + default_config = { + **BccTracer.default_config, + "attach_event": "do_sys_openat2", + "poll_timeout": 10, + } + attach_type = "kprobe" + + @property + def attatch_args(self): + return {"fn_name": "do_trace", "event": self.config.attach_event} + attatch_args = {"fn_name": "trace_entry", "event": "do_sys_openat2"} poll_fn = "ring_buffer_poll" - poll_args = {} + + @property + def poll_args(self): + return {"timeout": int(self.config.poll_timeout)} + data_t = namedtuple("OpenTracking", ["pid", "uid", "gid", "comm", "fname", "timestamp"]) prog = """ @@ -72,6 +87,6 @@ def print_callback(data: NamedTuple): poller = tracer.get_poller(b) while True: try: - poller() + poller(**tracer.poll_args) except KeyboardInterrupt: exit() diff --git a/duetector/tracers/tcpconnect.py b/duetector/tracers/tcpconnect.py new file mode 100644 index 0000000..377fda5 --- /dev/null +++ b/duetector/tracers/tcpconnect.py @@ -0,0 +1,138 @@ +from collections import namedtuple +from typing import Callable, NamedTuple + +from duetector.extension.tracer import hookimpl +from duetector.tracers.base import BccTracer +from duetector.utils import inet_ntoa + + +class TcpconnectTracer(BccTracer): + """ + A tracer for tcpconnect syscall + """ + + default_config = { + **BccTracer.default_config, + "poll_timeout": 10, + } + + many_attatchs = [ + ("kprobe", {"fn_name": "do_trace", "event": "tcp_v4_connect"}), + ("kretprobe", {"fn_name": "do_return", "event": "tcp_v4_connect"}), + ] + + poll_fn = "ring_buffer_poll" + + @property + def poll_args(self): + return {"timeout": int(self.config.poll_timeout)} + + data_t = namedtuple("TcpTracking", ["pid", "uid", "gid", "comm", "saddr", "daddr", "dport"]) + + # define BPF program + prog = """ + #include + #include + #include + #define TASK_COMM_LEN 16 + + BPF_RINGBUF_OUTPUT(buffer, 1 << 4); + BPF_HASH(currsock, u32, struct sock *); + + struct event { + u32 dport; + u32 saddr; + u32 daddr; + u32 pid; + u32 uid; + u32 gid; + char comm[TASK_COMM_LEN]; + }; + int do_trace(struct pt_regs *ctx, struct sock *sk) + { + u32 pid = bpf_get_current_pid_tgid(); + + // stash the sock ptr for lookup on return + currsock.update(&pid, &sk); + + return 0; + } + + int do_return(struct pt_regs *ctx) + { + int ret = PT_REGS_RC(ctx); + u32 pid = bpf_get_current_pid_tgid(); + + struct event event= {}; + + struct sock **skpp; + skpp = currsock.lookup(&pid); + if (skpp == 0) { + return 0; // missed entry + } + + if (ret != 0) { + // failed to send SYNC packet, may not have populated + // socket __sk_common.{skc_rcv_saddr, ...} + currsock.delete(&pid); + return 0; + } + + // pull in details + struct sock *skp = *skpp; + u32 saddr = skp->__sk_common.skc_rcv_saddr; + u32 daddr = skp->__sk_common.skc_daddr; + u16 dport = skp->__sk_common.skc_dport; + event.saddr = saddr; + event.daddr = daddr; + event.dport = dport; + event.pid = pid; + event.uid = bpf_get_current_uid_gid(); + event.gid = bpf_get_current_uid_gid() >> 32; + bpf_get_current_comm(&event.comm, sizeof(event.comm)); + // output + buffer.ringbuf_output(&event, sizeof(event), 0); + //bpf_trace_printk("trace_tcp4connect %x %x %d\\n", saddr, daddr, ntohs(dport)); + + currsock.delete(&pid); + + return 0; + } + """ + + def _convert_data(self, data) -> NamedTuple: + data = super()._convert_data(data) + return data._replace( + saddr=inet_ntoa(data.saddr).decode("utf-8"), daddr=inet_ntoa(data.daddr).decode("utf-8") + ) # type: ignore + + def set_callback(self, host, callback: Callable[[NamedTuple], None]): + def _(ctx, data, size): + event = host["buffer"].event(data) + return callback(self._convert_data(event)) # type: ignore + + host["buffer"].open_ring_buffer(_) + + +@hookimpl +def init_tracer(config): + return TcpconnectTracer(config) + + +if __name__ == "__main__": + from bcc import BPF + + b = BPF(text=TcpconnectTracer.prog) + tracer = TcpconnectTracer() + tracer.attach(b) + + def print_callback(data: NamedTuple): + print(f"[{data.comm} ({data.pid}) {data.uid} {data.gid}] TCP_CONNECT SADDR:{data.saddr} DADDR: {data.daddr} DPORT:{data.dport}") # type: ignore + + tracer.set_callback(b, print_callback) + poller = tracer.get_poller(b) + while True: + try: + poller(**tracer.poll_args) + except KeyboardInterrupt: + exit() diff --git a/duetector/utils.py b/duetector/utils.py index 3776cb9..8c18017 100644 --- a/duetector/utils.py +++ b/duetector/utils.py @@ -5,3 +5,13 @@ def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + + +def inet_ntoa(addr) -> bytes: + dq = b"" + for i in range(0, 4): + dq = dq + str(addr & 0xFF).encode() + if i != 3: + dq = dq + b"." + addr = addr >> 8 + return dq