From 86504cf5ac3a1ec8b8223c90a318d614927e200d Mon Sep 17 00:00:00 2001 From: doron zarhi Date: Wed, 9 Feb 2022 18:12:36 +0200 Subject: [PATCH] darwin_processes: implement lsof like utils --- src/rpcclient/requirements.txt | 1 + src/rpcclient/rpcclient/darwin_processes.py | 208 +++++++++++++++++--- src/rpcclient/rpcclient/structs/darwin.py | 138 +++++++++++-- src/rpcclient/rpcclient/structs/generic.py | 5 +- src/rpcclient/tests/test_processes.py | 17 ++ 5 files changed, 329 insertions(+), 40 deletions(-) create mode 100644 src/rpcclient/tests/test_processes.py diff --git a/src/rpcclient/requirements.txt b/src/rpcclient/requirements.txt index 09e1d94a..1800fd11 100644 --- a/src/rpcclient/requirements.txt +++ b/src/rpcclient/requirements.txt @@ -4,3 +4,4 @@ coloredlogs IPython traitlets cached-property +dataclasses; python_version<"3.7" diff --git a/src/rpcclient/rpcclient/darwin_processes.py b/src/rpcclient/rpcclient/darwin_processes.py index ed650abf..ab97f3be 100644 --- a/src/rpcclient/rpcclient/darwin_processes.py +++ b/src/rpcclient/rpcclient/darwin_processes.py @@ -1,20 +1,88 @@ -from _socket import htons +import dataclasses +import errno from collections import namedtuple -from typing import Optional, List +from pathlib import Path +from typing import Optional, List, Mapping from construct import Array +from rpcclient.common import path_to_str from rpcclient.exceptions import BadReturnValueError from rpcclient.processes import Processes -from rpcclient.structs.consts import AF_INET, AF_INET6 from rpcclient.structs.darwin import pid_t, MAXPATHLEN, PROC_PIDLISTFDS, proc_fdinfo, PROX_FDTYPE_VNODE, \ vnode_fdinfowithpath, PROC_PIDFDVNODEPATHINFO, proc_taskallinfo, PROC_PIDTASKALLINFO, PROX_FDTYPE_SOCKET, \ - PROC_PIDFDSOCKETINFO, socket_fdinfo, so_kind_t + PROC_PIDFDSOCKETINFO, socket_fdinfo, so_kind_t, so_family_t, PROX_FDTYPE_PIPE, PROC_PIDFDPIPEINFO, pipe_info Process = namedtuple('Process', 'pid path') -FileFd = namedtuple('FileFd', 'fd path') -Ipv4SocketFd = namedtuple('Ipv4SocketFd', 'fd local_port remote_port') # when remote 0, the socket is for listening -Ipv6SocketFd = namedtuple('Ipv6SocketFd', 'fd local_port remote_port') # when remote 0, the socket is for listening +FdStruct = namedtuple('FdStruct', 'fd struct') + + +@dataclasses.dataclass() +class Fd: + fd: int + + +@dataclasses.dataclass() +class FileFd(Fd): + path: str + + +@dataclasses.dataclass() +class UnixFd(Fd): + path: str + + +@dataclasses.dataclass() +class SocketFd(Fd): + pass + + +@dataclasses.dataclass() +class Ipv4SocketFd(SocketFd): + local_address: str + local_port: int + remote_address: str + remote_port: int # when remote 0, the socket is for listening + + +@dataclasses.dataclass() +class Ipv6SocketFd(SocketFd): + local_address: str + local_port: int + remote_address: str + remote_port: int # when remote 0, the socket is for listening + + +@dataclasses.dataclass() +class Ipv4TcpFd(Ipv4SocketFd): + pass + + +@dataclasses.dataclass() +class Ipv6TcpFd(Ipv6SocketFd): + pass + + +@dataclasses.dataclass() +class Ipv4UdpFd(Ipv4SocketFd): + pass + + +@dataclasses.dataclass() +class Ipv6UdpFd(Ipv6SocketFd): + pass + + +SOCKET_TYPE_DATACLASS = { + so_family_t.AF_INET: { + so_kind_t.SOCKINFO_TCP: Ipv4TcpFd, + so_kind_t.SOCKINFO_IN: Ipv4UdpFd, + }, + so_family_t.AF_INET6: { + so_kind_t.SOCKINFO_TCP: Ipv6TcpFd, + so_kind_t.SOCKINFO_IN: Ipv6UdpFd, + } +} class DarwinProcesses(Processes): @@ -26,9 +94,38 @@ def get_proc_path(self, pid: int) -> Optional[str]: return None return path.peek(path_len).decode() - def get_fds(self, pid: int) -> Optional[list]: + def get_fds(self, pid: int) -> List[Fd]: """ get a list of process opened file descriptors """ result = [] + for fdstruct in self.get_fd_structs(pid): + fd = fdstruct.fd + parsed = fdstruct.struct + + if fd.proc_fdtype == PROX_FDTYPE_VNODE: + result.append(FileFd(fd=fd.proc_fd, path=parsed.pvip.vip_path)) + + elif fd.proc_fdtype == PROX_FDTYPE_SOCKET: + if parsed.psi.soi_kind in (so_kind_t.SOCKINFO_TCP, so_kind_t.SOCKINFO_IN): + correct_class = SOCKET_TYPE_DATACLASS[parsed.psi.soi_family][parsed.psi.soi_kind] + + if parsed.psi.soi_kind == so_kind_t.SOCKINFO_TCP: + info = parsed.psi.soi_proto.pri_tcp.tcpsi_ini + else: + info = parsed.psi.soi_proto.pri_in + result.append(correct_class(fd=fd.proc_fd, + local_address=info.insi_laddr.ina_46.i46a_addr4, + local_port=info.insi_lport, + remote_address=info.insi_faddr.ina_46.i46a_addr4, + remote_port=info.insi_fport)) + + elif parsed.psi.soi_kind == so_kind_t.SOCKINFO_UN: + result.append(UnixFd(fd=fd.proc_fd, path=parsed.psi.soi_proto.pri_un.unsi_addr.ua_sun.sun_path)) + + return result + + def get_fd_structs(self, pid: int) -> List[FdStruct]: + """ get a list of process opened file descriptors as raw structs """ + result = [] size = self._client.symbols.proc_pidinfo(pid, PROC_PIDLISTFDS, 0, 0, 0) vi_size = 8196 # should be enough for all structs @@ -45,30 +142,95 @@ def get_fds(self, pid: int) -> Optional[list]: vs = self._client.symbols.proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDVNODEPATHINFO, vi_buf, vi_size) if not vs: - raise BadReturnValueError('proc_pidinfo(PROC_PIDFDVNODEPATHINFO) failed') + if self._client.errno == errno.EBADF: + # lsof treats this as fine + continue + raise BadReturnValueError( + f'proc_pidinfo(PROC_PIDFDVNODEPATHINFO) failed for fd: {fd.proc_fd} ' + f'({self._client.last_error})') - vi = vnode_fdinfowithpath.parse(vi_buf.peek(vnode_fdinfowithpath.sizeof())) - result.append(FileFd(fd=fd.proc_fd, path=vi.pvip.vip_path)) + result.append( + FdStruct(fd=fd, + struct=vnode_fdinfowithpath.parse(vi_buf.peek(vnode_fdinfowithpath.sizeof())))) elif fd.proc_fdtype == PROX_FDTYPE_SOCKET: # socket vs = self._client.symbols.proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDSOCKETINFO, vi_buf, vi_size) if not vs: - raise BadReturnValueError('proc_pidinfo(PROC_PIDFDSOCKETINFO) failed') - - vi = socket_fdinfo.parse(vi_buf.peek(vi_size)) - if vi.psi.soi_family == AF_INET and vi.psi.soi_kind == so_kind_t.SOCKINFO_TCP: - local_port = htons(vi.psi.soi_proto.pri_in.insi_lport) - remote_port = htons(vi.psi.soi_proto.pri_in.insi_fport) - result.append(Ipv4SocketFd(fd=fd.proc_fd, local_port=local_port, remote_port=remote_port)) - - elif vi.psi.soi_family == AF_INET6 and vi.psi.soi_kind == so_kind_t.SOCKINFO_TCP: - local_port = htons(vi.psi.soi_proto.pri_in.insi_lport) - remote_port = htons(vi.psi.soi_proto.pri_in.insi_fport) - result.append(Ipv6SocketFd(fd=fd.proc_fd, local_port=local_port, remote_port=remote_port)) + if self._client.errno == errno.EBADF: + # lsof treats this as fine + continue + raise BadReturnValueError( + f'proc_pidinfo(PROC_PIDFDSOCKETINFO) failed ({self._client.last_error})') + + result.append(FdStruct(fd=fd, struct=socket_fdinfo.parse(vi_buf.peek(vi_size)))) + + elif fd.proc_fdtype == PROX_FDTYPE_PIPE: + # pipe + vs = self._client.symbols.proc_pidfdinfo(pid, fd.proc_fd, PROC_PIDFDPIPEINFO, vi_buf, + vi_size) + if not vs: + if self._client.errno == errno.EBADF: + # lsof treats this as fine + continue + raise BadReturnValueError( + f'proc_pidinfo(PROC_PIDFDPIPEINFO) failed ({self._client.last_error})') + + result.append( + FdStruct(fd=fd, + struct=pipe_info.parse(vi_buf.peek(pipe_info.sizeof())))) + return result + def get_process_by_listening_port(self, port: int) -> Optional[Process]: + """ get a process object listening on the specified port """ + for process in self.list(): + try: + fds = self.get_fds(process.pid) + except BadReturnValueError: + # it's possible to get error if new processes have since died or the rpcserver + # doesn't have the required permissions to access all the processes + continue + + for fd in fds: + if (isinstance(fd, Ipv4SocketFd) or isinstance(fd, Ipv6SocketFd)) and \ + fd.local_port == port and fd.remote_port == 0: + return process + + def lsof(self) -> Mapping[int, List[Fd]]: + """ get dictionary of pid to its opened fds """ + result = {} + for process in self.list(): + try: + fds = self.get_fds(process.pid) + except BadReturnValueError: + # it's possible to get error if new processes have since died or the rpcserver + # doesn't have the required permissions to access all the processes + continue + + result[process.pid] = fds + return result + + @path_to_str('path') + def fuser(self, path: str) -> List[Process]: + """get a list of all processes have an open hande to the specified path """ + result = [] + for process in self.list(): + try: + fds = self.get_fds(process.pid) + except BadReturnValueError: + # it's possible to get error if new processes have since died or the rpcserver + # doesn't have the required permissions to access all the processes + continue + + for fd in fds: + if isinstance(fd, FileFd): + if str(Path(fd.path).absolute()) == str(Path(path).absolute()): + result.append(process) + + return result + def get_task_all_info(self, pid: int): """ get a list of process opened file descriptors """ with self._client.safe_malloc(proc_taskallinfo.sizeof()) as pti: diff --git a/src/rpcclient/rpcclient/structs/darwin.py b/src/rpcclient/rpcclient/structs/darwin.py index ca60842d..a20c20b5 100644 --- a/src/rpcclient/rpcclient/structs/darwin.py +++ b/src/rpcclient/rpcclient/structs/darwin.py @@ -1,8 +1,9 @@ from construct import PaddedString, Struct, Int32ul, Int16ul, Int64ul, Int8ul, this, Int32sl, Padding, Array, Int64sl, \ - Bytes, Computed, FlagsEnum, Int16sl, Union, Enum + Bytes, Computed, FlagsEnum, Int16sl, Union, Enum, Switch, Int16ub, Adapter, Default +from rpcclient.structs.consts import AF_INET, AF_INET6, AF_UNIX from rpcclient.structs.generic import uid_t, gid_t, long, mode_t, uint64_t, short, u_short, uint32_t, u_int32_t, \ - in_addr, uint8_t + in_addr, uint8_t, u_char, UNIX_PATH_MAX MAXPATHLEN = 1024 _SYS_NAMELEN = 256 @@ -403,22 +404,57 @@ TSI_T_2MSL = 3 # 2*msl quiet time timer TSI_T_NTIMERS = 4 + +class IpAddressAdapter(Adapter): + def _decode(self, obj, context, path): + return ".".join(map(str, obj)) + + def _encode(self, obj, context, path): + return list(map(int, obj.split("."))) + + in4in6_addr = Struct( 'i46a_pad32' / u_int32_t[3], - 'i46a_addr4' / in_addr, + 'i46a_addr4' / IpAddressAdapter(in_addr), +) + +in6_addr = Struct( + 'i46a_pad32' / u_int32_t[3], + 'i46a_addr4' / IpAddressAdapter(in_addr), ) in_sockinfo = Struct( - 'insi_fport' / Int32sl, - 'insi_lport' / Int32sl, + 'insi_fport' / Int16ub, + Padding(2), + 'insi_lport' / Int16ub, + Padding(2), 'insi_gencnt' / uint64_t, 'insi_flags' / uint32_t, 'insi_flow' / uint32_t, 'insi_vflag' / uint8_t, 'insi_ip_ttl' / uint8_t, + Padding(2), 'rfu_1' / uint32_t, - # TODO: complete + # protocol dependent part + 'insi_faddr' / Union(None, + 'ina_46' / in4in6_addr, + 'ina_6' / in6_addr), + + 'insi_laddr' / Union(None, + 'ina_46' / in4in6_addr, + 'ina_6' / in6_addr), + + 'insi_v4' / Struct( + 'in4_tos' / u_char, + ), + + 'insi_v6' / Struct( + 'in6_hlim' / uint8_t, + 'in6_cksum' / Int32sl, + 'in6_ifindex' / u_short, + 'in6_hops' / short, + ), ) tcp_sockinfo = Struct( @@ -431,6 +467,60 @@ 'tcpsi_tp' / uint64_t, # opaque handle of TCP protocol control block ) +SOCK_MAXADDRLEN = 255 + +# Unix Domain Sockets + +# we can't use sockaddr_un since the sun_path may contain utf8 invalid characters +sockaddr_un_raw = Struct( + 'sun_family' / Default(Int16sl, AF_UNIX), + '_sun_path' / Bytes(UNIX_PATH_MAX), + 'sun_path' / Computed(lambda x: x._sun_path.split(b'\x00', 1)[0].decode()) +) + +un_sockinfo = Struct( + 'unsi_conn_so' / uint64_t, + 'unsi_conn_pcb' / uint64_t, + 'unsi_addr' / Union(None, + 'ua_sun' / sockaddr_un_raw, + 'ua_dummy' / Bytes(SOCK_MAXADDRLEN)), + 'unsi_caddr' / Union(None, + 'ua_sun' / sockaddr_un_raw, + 'ua_dummy' / Bytes(SOCK_MAXADDRLEN)), +) + +IF_NAMESIZE = 16 + +# PF_NDRV Sockets + +ndrv_info = Struct( + 'ndrvsi_if_family' / uint32_t, + 'ndrvsi_if_unit' / uint32_t, + 'ndrvsi_if_name' / Bytes(IF_NAMESIZE), +) + +# Kernel Event Sockets + +kern_event_info = Struct( + 'kesi_vendor_code_filter' / uint32_t, + 'kesi_class_filter' / uint32_t, + 'kesi_subclass_filter' / uint32_t, +) + +# Kernel Control Sockets + +MAX_KCTL_NAME = 96 + +kern_ctl_info = Struct( + 'kcsi_id' / uint32_t, + 'kcsi_reg_unit' / uint32_t, + 'kcsi_flags' / uint32_t, + 'kcsi_recvbufsize' / uint32_t, + 'kcsi_sendbufsize' / uint32_t, + 'kcsi_unit' / uint32_t, + 'kcsi_name' / Bytes(MAX_KCTL_NAME), +) + so_kind_t = Enum(Int32ul, SOCKINFO_GENERIC=0, SOCKINFO_IN=1, @@ -441,13 +531,18 @@ SOCKINFO_KERN_CTL=6 ) +so_family_t = Enum(Int32ul, + AF_INET=AF_INET, + AF_INET6=AF_INET6, + ) + socket_info = Struct( 'soi_stat' / vinfo_stat, 'soi_so' / uint64_t, # opaque handle of socket 'soi_pcb' / uint64_t, # opaque handle of protocol control block 'soi_type' / Int32sl, 'soi_protocol' / Int32sl, - 'soi_family' / Int32sl, + 'soi_family' / so_family_t, 'soi_options' / short, 'soi_linger' / short, 'soi_state' / short, @@ -462,17 +557,30 @@ 'soi_kind' / so_kind_t, 'rfu_1' / uint32_t, # reserved - 'soi_proto' / Union(None, - 'pri_in' / in_sockinfo, - # 'tcp_sockinfo' / pri_tcp, - # 'un_sockinfo' / pri_un, - # 'ndrv_info' / pri_ndrv, - # 'kern_event_info' / pri_kern_event, - # 'kern_ctl_info' / pri_kern_ctl, - ) + 'soi_proto' / Switch(this.soi_kind, { + so_kind_t.SOCKINFO_IN: Struct('pri_in' / in_sockinfo), + so_kind_t.SOCKINFO_TCP: Struct('pri_tcp' / tcp_sockinfo), + so_kind_t.SOCKINFO_UN: Struct('pri_un' / un_sockinfo), + so_kind_t.SOCKINFO_NDRV: Struct('pri_ndrv' / ndrv_info), + so_kind_t.SOCKINFO_KERN_EVENT: Struct('pri_kern_event' / kern_event_info), + so_kind_t.SOCKINFO_KERN_CTL: Struct('pri_kern_ctl' / kern_ctl_info), + }), ) socket_fdinfo = Struct( 'pfi' / proc_fileinfo, 'psi' / socket_info, ) + +pipe_info = Struct( + 'pipe_stat' / vinfo_stat, + 'pipe_handle' / uint64_t, + 'pipe_peerhandle' / uint64_t, + 'pipe_status' / Int32sl, + 'rfu_1' / Int32sl # reserved +) + +pipe_fdinfo = Struct( + 'pfi' / proc_fileinfo, + 'pipeinfo' / pipe_info, +) diff --git a/src/rpcclient/rpcclient/structs/generic.py b/src/rpcclient/rpcclient/structs/generic.py index 3fc1c589..3df4b30d 100644 --- a/src/rpcclient/rpcclient/structs/generic.py +++ b/src/rpcclient/rpcclient/structs/generic.py @@ -1,4 +1,4 @@ -from construct import Int32ul, Int16ul, Struct, Int16sl, Bytes, Default, Int64sl, Const, PaddedString, Pointer, \ +from construct import Int32ul, Int16ul, Struct, Int16sl, Bytes, Default, Int64sl, PaddedString, Pointer, \ this, CString, LazyBound, Padding, If, Int8ul, Int64ul from rpcclient.structs.consts import AF_UNIX, AF_INET, AF_INET6 @@ -6,6 +6,7 @@ UNIX_PATH_MAX = 108 +u_char = Int8ul uint8_t = Int8ul short = Int16sl u_short = Int16ul @@ -36,7 +37,7 @@ ) sockaddr_un = Struct( - 'sun_family' / Const(AF_UNIX, Int16sl), + 'sun_family' / Default(Int16sl, AF_UNIX), 'sun_path' / PaddedString(UNIX_PATH_MAX, 'utf8'), ) diff --git a/src/rpcclient/tests/test_processes.py b/src/rpcclient/tests/test_processes.py new file mode 100644 index 00000000..c4e4bd85 --- /dev/null +++ b/src/rpcclient/tests/test_processes.py @@ -0,0 +1,17 @@ +from rpcclient.protocol import DEFAULT_PORT + +LAUNCHD_PID = 1 +LAUNCHD_PATH = '/sbin/launchd' + + +def test_list_sanity(client): + processes = client.processes.list() + assert len(processes) > 2 # at least launchd and us should be running + for p in processes: + if p.pid == LAUNCHD_PID: + assert p.path == LAUNCHD_PATH + + +def test_get_process_by_listening_port(client): + # there should only be one process listening on this port and that's us + assert client.processes.get_process_by_listening_port(DEFAULT_PORT).pid == client.symbols.getpid()