Commit ec31cf6f authored by anonym's avatar anonym

Leverage AppArmor's in-kernel solution for determining executable paths.

Using /proc/pid/cmdline is not secure since it can be trivially set
with, for instance:

    exec -a "pwned" sh -c 'cat /proc/$$/cmdline'

The /proc/pid/exe symlink is not good enough for scripts (since it will
point to the interpreter, not the script) so let's instead use
AppArmor's in-kernel solution for determining executable paths. We
fallback to /proc/pid/exe for unconfined processes, which leaves us with
only unconfined scripts not being supported by tor-controlport-filter.
However, profiles in complain mode is still good enough, so a trivial
stub profile in complain mode is enough, which is exactly what we do for
onionshare and onioncircuits.
parent e02039f3
# This is a stub profile that allows this application to use
# tor-controlport-filter.
/usr/bin/onioncircuits flags=(complain) {
}
# This is a stub profile that allows this application to use
# tor-controlport-filter.
/usr/bin/onionshare flags=(complain) {
}
# This is a stub profile that allows this application to use
# tor-controlport-filter.
/usr/bin/onionshare-gui flags=(complain) {
}
---
- cmdline:
- '/usr/bin/python3 /usr/bin/onioncircuits'
- match-exe-paths:
- '/usr/bin/onioncircuits'
commands:
GETINFO:
- 'version'
......
---
- cmdline:
- '/usr/bin/python3 /usr/bin/onionshare'
- '/usr/bin/python3 /usr/bin/onionshare-gui'
- match-exe-paths:
- '/usr/bin/onionshare'
- '/usr/bin/onionshare-gui'
commands:
GETINFO:
- 'version'
......
---
- cmdline:
- '/usr/local/lib/tor-browser/firefox -allow-remote --class Tor Browser'
- match-exe-paths:
- '/usr/local/lib/tor-browser/firefox'
commands:
SIGNAL:
- 'NEWNYM'
......
......@@ -17,6 +17,7 @@ import glob
import psutil
import re
import socketserver
import subprocess
import stem
import stem.control
import yaml
......@@ -31,6 +32,42 @@ class UnexpectedAnswer(Exception):
def __str__(self):
return "[UnexpectedAnswer] " + self.msg
def exe_path_of_pid(pid):
# Here we leverage AppArmor's in-kernel solution for determining
# the exact executable invoked. Looking at /proc/pid/exe when an
# interpreted script is running will just point to the
# interpreter's binary, which is not fine-grained enough, but
# AppArmor will be aware of which script is running for processes
# using one of its profiles.
p = subprocess.Popen(['/usr/sbin/aa-status', '--verbose'],
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
shell = False)
stdout, _ = p.communicate()
returncode = p.returncode
assert(returncode == 0)
STATE_LOOKING_FOR_PROCS_SECTION = 0
STATE_FOUND_PROCS_SECTION = 1
parser_state = STATE_LOOKING_FOR_PROCS_SECTION
for line in str(stdout, 'UTF+8').split("\n"):
if parser_state == STATE_LOOKING_FOR_PROCS_SECTION:
if re.match(r'^\d+ processes ', line):
parser_state = STATE_FOUND_PROCS_SECTION
elif parser_state == STATE_FOUND_PROCS_SECTION:
match = re.match(r'^\s*(/.+)\s+\((\d+)\)\s*$', line)
if match:
proc_exe_path = match.group(1)
proc_pid = int(match.group(2))
if proc_pid == pid:
return proc_exe_path
else:
parser_state = STATE_LOOKING_FOR_PROCS_SECTION
# If no AppArmor profile was found for the PID, we fallback to the
# executable according to procfs, which will be good enough for
# binaries but not interpreted scripts.
return psutil.Process(pid).exe()
def handle_controlport_session(controller, readh, writeh, allowed_commands, allowed_events):
def respond(line, raw = False):
writeh.write(bytes(line, 'ascii'))
......@@ -126,9 +163,8 @@ class FilteredControlPortProxyHandler(socketserver.StreamRequestHandler):
def handle(self):
client_conn = next(conn for conn in psutil.net_connections() if conn.laddr == self.client_address)
client_proc = psutil.Process(client_conn.pid)
client_cmdline = " ".join(client_proc.cmdline())
client_filter = next(filter for filter in self.filters if any(cmdline for cmdline in filter['cmdline'] if client_cmdline.startswith(cmdline)))
client_exe_path = exe_path_of_pid(client_conn.pid)
client_filter = next(filter for filter in self.filters if any(exe_path for exe_path in filter['match-exe-paths'] if client_exe_path == exe_path))
allowed_commands = client_filter.get('commands', {})
allowed_events = client_filter.get('events', [])
controller = self.connect_to_real_control_port()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment