Skip to content

Commit

Permalink
Fixes #12: Using kernel ftrace extension to hook to execve calls and …
Browse files Browse the repository at this point in the history
…prevent path manipulation.
  • Loading branch information
evilsocket committed Apr 30, 2017
1 parent 753316d commit 81429d0
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 32 deletions.
5 changes: 0 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,6 @@ OpenSnitch is a GNU/Linux port of the Little Snitch application firewall.

grep -r TODO opensnitch | cut -d '#' -f 2 | sort -u

## Known Limitations

As [pointed out in this thread](https://github.com/evilsocket/opensnitch/issues/12), OpenSnitch relies on the `/proc` filesystem in order to link a connection to a process. Being this information relatively easy to manipulate by an attacker, the path and the list of arguments shown in the UI might not match the real
context of the process.

## License

This project is copyleft of [Simone Margaritelli](http://www.evilsocket.net/) and released under the GPL 3 license.
Binary file added opensnitch/.app.py.swo
Binary file not shown.
Binary file added opensnitch/.connection.py.swo
Binary file not shown.
Binary file added opensnitch/.proc.py.swo
Binary file not shown.
Binary file added opensnitch/.procmon.py.swo
Binary file not shown.
Binary file added opensnitch/.rule.py.swo
Binary file not shown.
Binary file added opensnitch/.snitch.py.swo
Binary file not shown.
30 changes: 17 additions & 13 deletions opensnitch/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import re
import os
from threading import Lock
import logging

class LinuxDesktopParser:
lock = Lock()
Expand Down Expand Up @@ -73,21 +74,24 @@ def get_info_by_path( path ):
return ( name, icon )

class Application:
def __init__( self, pid, path ):
def __init__( self, procmon, pid, path ):
self.pid = pid
self.path = path
self.name, self.icon = LinuxDesktopParser.get_info_by_path(self.path)

# this is an attempt to resolve interpreted scripts to more useful
# things than just 'python2.7' or 'bash'
try:
with open( "/proc/%s/comm" % pid ) as com_fd, open( "/proc/%s/cmdline" % pid ) as cmd_fd:
self.comm = com_fd.read().replace('\0', ' ').strip()
self.cmdline = cmd_fd.read().replace('\0', ' ').strip().split()
except IOError:
self.comm = ''
self.cmdline = []

if self.comm not in self.name:
self.name = self.comm
self.path = filter(lambda x: self.comm in x, self.cmdline)[0]

self.cmdline = None

if self.pid is not None:
if procmon.running:
self.cmdline = procmon.get_cmdline( pid )
if self.cmdline is None:
logging.debug( "Could not find pid %s command line with ProcMon" % pid )

if self.cmdline is None:
with open( "/proc/%s/cmdline" % pid ) as cmd_fd:
self.cmdline = cmd_fd.read().replace( '\0', ' ').strip()

except Exception as e:
logging.exception(e)
16 changes: 13 additions & 3 deletions opensnitch/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from socket import inet_ntoa, getservbyport

class Connection:
def __init__( self, payload ):
def __init__( self, procmon, payload ):
self.data = payload
self.pkt = ip.IP( self.data )
self.src_addr = inet_ntoa( self.pkt.src )
Expand All @@ -48,12 +48,13 @@ def __init__( self, payload ):
except:
self.service = None

self.pid, self.app_path = get_pid_by_connection( self.src_addr,
self.pid, self.app_path = get_pid_by_connection( procmon,
self.src_addr,
self.src_port,
self.dst_addr,
self.dst_port,
self.proto )
self.app = Application( self.pid, self.app_path )
self.app = Application( procmon, self.pid, self.app_path )
self.app_path = self.app.path

def get_app_name(self):
Expand All @@ -66,6 +67,15 @@ def get_app_name(self):
else:
return "'%s' ( %s )" % ( self.app.name, self.app_path )

def get_app_name_and_cmdline(self):
if self.app.cmdline is not None:
if self.app.cmdline.startswith( self.app.path ):
return self.app.cmdline
else:
return "%s %s" % ( self.app.path, self.app.cmdline )
else:
return self.app.path

def __repr__(self):
return "[%s] %s (%s) -> %s:%s" % ( self.pid, self.app_path, self.proto, self.dst_addr, self.dst_port )

15 changes: 13 additions & 2 deletions opensnitch/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,26 @@
import logging
import psutil

def get_pid_by_connection( src_addr, src_p, dst_addr, dst_p, proto = 'tcp' ):
def get_pid_by_connection( procmon, src_addr, src_p, dst_addr, dst_p, proto = 'tcp' ):
connections_list = [connection for connection in psutil.net_connections(kind=proto) if connection.laddr==(src_addr, src_p) and connection.raddr==(dst_addr, dst_p)]

# We always take the first element as we assume it contains only one, because
# it should not be possible to keep two connections which are exactly the same.
if connections_list:
pid = connections_list[0][-1]
try:
return ( pid, os.readlink( "/proc/%s/exe" % pid ) )
appname = None
if procmon.running:
appname = procmon.get_app_name(pid)

if appname is None:
appname = os.readlink( "/proc/%s/exe" % pid )
if procmon.running:
logging.debug( "Could not find pid %s with ProcMon, faiiling back to /proc/%s/exe -> %s" % ( pid, pid, appname ) )
else:
logging.debug( "ProcMon(%s) = %s" % ( pid, appname ) )

return ( pid, appname )
except OSError:
return (None, "Unknown")
else:
Expand Down
158 changes: 158 additions & 0 deletions opensnitch/procmon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# This file is part of OpenSnitch.
#
# Copyright(c) 2017 Simone Margaritelli
# [email protected]
# http://www.evilsocket.net
#
# This file may be licensed under the terms of of the
# GNU General Public License Version 2 (the ``GPL'').
#
# Software distributed under the License is distributed
# on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
# express or implied. See the GPL for the specific language
# governing rights and limitations.
#
# You should have received a copy of the GPL along with this
# program. If not, go to http://www.gnu.org/licenses/gpl.html
# or write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import select
import re
from collections import defaultdict
import os
import threading
import logging

class ProcMon(threading.Thread):
PROBE_NAME = "opensnitch_sys_execve"

def __init__(self):
threading.Thread.__init__(self)
self.pids = defaultdict(dict)
self.lock = threading.Lock()
self.running = False
self.daemon = True

@staticmethod
def enable():
ProcMon.disable(False)
logging.info( "Enabling ProcMon ..." )

open("/sys/kernel/debug/tracing/events/sched/sched_process_fork/enable", 'w').write("1")
open("/sys/kernel/debug/tracing/events/sched/sched_process_exec/enable", 'w').write("1")
open("/sys/kernel/debug/tracing/events/sched/sched_process_exit/enable", 'w').write("1")

# Create the custom execve kprobe consumer
with open("/sys/kernel/debug/tracing/kprobe_events", "w") as f:
f.write( "p:kprobes/%s sys_execve" % ProcMon.PROBE_NAME )
#Command line args will be in %si, we're asking ftrace to give them to us
for i in range(1, 16):
f.write(" arg%d=+0(+%d(%%si)):string" % (i, i*8))

open("/sys/kernel/debug/tracing/events/kprobes/%s/enable" % ProcMon.PROBE_NAME, 'w').write('1')

@staticmethod
def disable(verbose=True):
if verbose:
logging.info( "Disabling ProcMon ..." )

try:
open("/sys/kernel/debug/tracing/events/sched/sched_process_fork/enable", 'w').write("0")
open("/sys/kernel/debug/tracing/events/sched/sched_process_exec/enable", 'w').write("0")
open("/sys/kernel/debug/tracing/events/sched/sched_process_exit/enable", 'w').write("0")
open("/sys/kernel/debug/tracing/events/kprobes/%s/enable" % ProcMon.PROBE_NAME, 'w').write('0')
open("/sys/kernel/debug/tracing/kprobe_events", 'a+').write("-:"+ ProcMon.PROBE_NAME)
open("/sys/kernel/debug/tracing/trace", 'w').write('')
except:
pass

@staticmethod
def is_ftrace_available():
try:
with open( '/proc/sys/kernel/ftrace_enabled', 'rt' ) as fp:
return fp.read().strip() == '1'
except:
pass

return False

def get_app_name( self, pid ):
pid = int(pid)
with self.lock:
if pid in self.pids and 'filename' in self.pids[pid]:
return self.pids[pid]['filename']

return None

def get_cmdline( self, pid ):
pid = int(pid)
with self.lock:
if pid in self.pids and 'args' in self.pids[pid]:
return self.pids[pid]['args']

return None

def _dump( self, pid, e ):
logging.debug( "(pid=%d) %s %s" % ( pid, e['filename'], e['args'] if 'args' in e else '' ) )

def _on_exec( self, pid, filename ):
with self.lock:
self.pids[pid]['filename'] = filename
self._dump( pid, self.pids[pid] )

def _on_args( self, pid, args ):
with self.lock:
self.pids[pid]['args'] = args

def _on_exit( self, pid ):
with self.lock:
if pid in self.pids:
del self.pids[pid]

def run(self):
logging.info( "ProcMon running ..." )
self.running = True

with open("/sys/kernel/debug/tracing/trace_pipe") as pipe:
while True:
try:
r, w, e = select.select([pipe], [], [], 0)
if pipe not in r:
continue
line = pipe.readline()

if ProcMon.PROBE_NAME in line:
m = re.search(r'^.*?\-(\d+)\s*\[', line)

if m is not None:
pid = int(m.group(1))
#"walk" over every argument field, 'fault' is our terminator.
# If we see it it means that there are more cmdline args.
if '(fault)' in line:
line = line[:line.find('(fault)')]

args = ' '.join(re.findall(r'arg\d+="(.*?)"', line))

self._on_args( pid, args )

else:
m = re.search(r'sched_process_(.*?):', line)
if m is not None:
event = m.group(1)

if event == 'exec':
filename = re.search(r'filename=(.*?)\s+pid=', line).group(1)
pid = int(re.search(r'\spid=(\d+)', line).group(1))

self._on_exec( pid, filename )

elif event == 'exit':
mm = re.search(r'\scomm=(.*?)\s+pid=(\d+)', line)
command = mm.group(1)
pid = int(mm.group(2))

self._on_exit( pid )

except Exception as e:
logging.warning(e)

19 changes: 13 additions & 6 deletions opensnitch/snitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from opensnitch.connection import Connection
from opensnitch.dns import DNSCollector
from opensnitch.rule import Rule, Rules
from opensnitch.procmon import ProcMon

class Snitch:
IPTABLES_RULES = ( # Get DNS responses
Expand All @@ -38,11 +39,12 @@ class Snitch:

# TODO: Support IPv6!
def __init__( self ):
self.lock = Lock()
self.rules = Rules()
self.dns = DNSCollector()
self.q = NetfilterQueue()
self.qt_app = QtApp()
self.lock = Lock()
self.rules = Rules()
self.dns = DNSCollector()
self.q = NetfilterQueue()
self.procmon = ProcMon()
self.qt_app = QtApp()

self.q.bind( 0, self.pkt_callback, 1024 * 2 )

Expand All @@ -69,7 +71,7 @@ def pkt_callback(self,pkt):
self.dns.add_response(packet)

else:
conn = Connection(data)
conn = Connection( self.procmon, data )
if conn.proto is None:
logging.debug( "Could not detect protocol for packet." )

Expand All @@ -95,6 +97,10 @@ def start(self):
logging.debug( "Applying iptables rule '%s'" % r )
os.system( "iptables -I %s" % r )

if ProcMon.is_ftrace_available():
self.procmon.enable()
self.procmon.start()

self.qt_app.run()
self.q.run()

Expand All @@ -103,4 +109,5 @@ def stop(self):
logging.debug( "Deleting iptables rule '%s'" % r )
os.system( "iptables -D %s" % r )

self.procmon.disable()
self.q.unbind()
4 changes: 2 additions & 2 deletions opensnitch/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ def __init__( self, connection, parent=None ):
def setup_labels(self):
self.app_name_label.setText( self.connection.app.name )

message = "%s (pid=%s) wants to connect to %s on %s port %s%s" % ( \
self.connection.app_path,
message = "<b>%s</b> (pid=%s) wants to connect to <b>%s</b> on <b>%s port %s%s</b>" % ( \
self.connection.get_app_name_and_cmdline(),
self.connection.app.pid,
self.connection.hostname,
self.connection.proto.upper(),
Expand Down
2 changes: 1 addition & 1 deletion opensnitch/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
# program. If not, go to http://www.gnu.org/licenses/gpl.html
# or write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
VERSION = '0.0.1a1'
VERSION = '0.0.2'

0 comments on commit 81429d0

Please sign in to comment.