Wednesday, February 3, 2016

Collecting ioctl denials for selinux ioctl whitelisting - Android Marshmallow




This requires building your own kernel and putting selinux into permissive mode via the kernel command line. These seem like reasonable requirements as anyone writing selinux policy for a device should have these capabilities. My patch is based on the Nexus-9 Marshmallow-mr1 kernel (branch android-tegra-flounder-3.10-marshmallow-mr1).*

Apply the following patch to your Android-M kernel:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
diff --git a/security/selinux/avc.c b/security/selinux/avc.c
index f3dbbc0..0ac7908 100644
--- a/security/selinux/avc.c
+++ b/security/selinux/avc.c
@@ -686,6 +686,9 @@ static struct avc_node *avc_insert(u32 ssid, u32 tsid, u16 tclass,
 
   hvalue = avc_hash(ssid, tsid, tclass);
   avc_node_populate(node, ssid, tsid, tclass, avd);
+  /* hack to keep ops_node around for all ioctl capable files */
+  if (avd->allowed & FILE__IOCTL)
+   ops_node->ops.len = 1;
   rc = avc_operation_populate(node, ops_node);
   if (rc) {
    kmem_cache_free(avc_node_cachep, node);

This patch is intended to be temporary for collecting ioctls for selinux policy.

After applying the patch, building the kernel and flashing a new bootimage onto the device logcat should be spewing ioctl denials (all denials collected from a Nexus 9 AOSP m-mr1 build):

avc: denied { ioctl } for path="/dev/ptmx" dev="tmpfs" ino=5495 ioctlcmd=5431 scontext=u:r:init:s0 tcontext=u:object_r:ptmx_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="/dev/ptmx" dev="tmpfs" ino=5495 ioctlcmd=5430 scontext=u:r:init:s0 tcontext=u:object_r:ptmx_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="/sys/fs/selinux/null" dev="selinuxfs" ino=22 ioctlcmd=5401 scontext=u:r:fsck:s0 tcontext=u:object_r:null_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="socket:[5817]" dev="sockfs" ino=5817 ioctlcmd=8913 scontext=u:r:init:s0 tcontext=u:r:init:s0 tclass=udp_socket permissive=1
avc: denied { ioctl } for path="socket:[5817]" dev="sockfs" ino=5817 ioctlcmd=8914 scontext=u:r:init:s0 tcontext=u:r:init:s0 tclass=udp_socket permissive=1
avc: denied { ioctl } for path="/dev/binder" dev="tmpfs" ino=5525 ioctlcmd=6209 scontext=u:r:healthd:s0 tcontext=u:object_r:binder_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="/dev/binder" dev="tmpfs" ino=5525 ioctlcmd=6205 scontext=u:r:healthd:s0 tcontext=u:object_r:binder_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="/dev/binder" dev="tmpfs" ino=5525 ioctlcmd=6205 scontext=u:r:healthd:s0 tcontext=u:object_r:binder_device:s0 tclass=chr_file permissive=1
avc: denied { ioctl } for path="/dev/binder" dev="tmpfs" ino=5525 ioctlcmd=6201 scontext=u:r:healthd:s0 tcontext=u:object_r:binder_device:s0 tclass=chr_file permissive=1

Note the "permissive=1" at the end of the denial. If "permissive=0" then the device is still in enforcing mode and the ioctls are actually being denied.

For me, the selinux denials caused so much logging overhead that the device never made it past the google splash screen. That's fine it just means that collecting denials will be a multistep process e.g.:

  1. Log ioctl denials - if using the script below, collect all denials into a single log file i.e. append new denials to the end.
  2. denial --> allow rules (sorry adding this to audit2allow is on my todo list).
  3. rebuild with the new ioctl allow rules
  4. repeat until you have a working device

I have put together a small python script to partially automate the the denial --> allow rule step. Note that this is not an audit2allow replacement. Audit2allow checks the existing policy to avoid duplicate allow rules. This just naively consolidates ioctl commands with source/target/class sets.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
import itertools
import sys
import os.path

def print_usage():
    print "You must specify an input file with selinux ioctl denials"
    print "python <myscript.py> <mylogcapture>"

# combine sequential ioctls into ranges e.g. 1, 2, 3, 4 --> 1-4
def listtoranges(i):
    for a, b in itertools.groupby(enumerate(i), lambda (x, y): y - x):
        b = list(b)
        yield b[0][1], b[-1][1]

# group ioctls with source/target/class key
def group_ioctls(f):
    sourcetargetclass = {}
    for line in f:
        if ("avc" in line) and ("ioctlcmd" in line) and ("tcontext" in line) and ("tclass" in line):
            # extract source/target/class
            try:
                source = line.split('scontext=u:r:')[1]
                source = source.split(':')[0]
                target = line.split('tcontext=u:object_r:')
                if len(target) < 2:
                    target = line.split('tcontext=u:r:')
                if len(target) < 2:
                    continue
                target = target[1]
                target = target.split(':')[0]
                tclass = line.split('tclass=')[1]
                tclass = tclass.split(' ')[0]
                path = line.split('path=')[1]
                path = path.split(' ')[0][1:-1]
                cmd = line.split('ioctlcmd=')[1]
                cmd = cmd.split(' ')[0]
                # add cmd to dict
                #key = target+" "+tclass
                key = source+" "+target+" "+tclass
                if key not in sourcetargetclass.keys():
                    sourcetargetclass[key] = [cmd[-4:]]
                else:
                    if cmd[-4:] not in sourcetargetclass[key]:
                        sourcetargetclass[key].append(cmd[-4:])
            except ValueError:
                print ValueError
                print line
                print source
                print target
                print tclass
                print cmd
                continue
    return sourcetargetclass

def print_allow_rules(sourcetargetclass):
    ioctlcmds = []
    for key in sourcetargetclass.keys():
        out = ""
        source, target, tclass = key.split(' ')
        results = sorted([int(cmd,16) for cmd in sourcetargetclass[key]])
        ioctlcmds+= results
        ranges = list(listtoranges(results))
        out+= "allow " + source + " " + target + ":" + tclass + " "
        if (len(ranges) > 1 or (ranges[0][0] != ranges[0][1])):
            out += "{ "
        for range in ranges:
            if (range[0] == range[1]):
                 out += hex(range[0]) + " "
            else:
                out += hex(range[0]) + "-" + hex(range[1]) + " "
        if (len(ranges) > 1 or (ranges[0][0] != ranges[0][1])):
            out += "};"
        out = out[0:-1] + ";"
        print out

if __name__ == "__main__":
    if len(sys.argv) != 2 or not os.path.isfile(sys.argv[1]):
        print_usage()
        exit()

    fin = open(sys.argv[1], 'r')
    sourcetargetclass = group_ioctls(fin)
    print_allow_rules(sourcetargetclass)
    fin.close()

Output:
allow init null_device:chr_file 0x5401;
allow init block_device:blk_file 0x125d;
allow surfaceflinger binder_device:chr_file 0x6209;
allow vold userdata_block_device:blk_file 0x1260;
allow init init:udp_socket 0x8913;
allow shell console_device:chr_file 0x5401;
allow servicemanager binder_device:chr_file 0x6209;
allow init console_device:chr_file 0x540e;
allow ueventd null_device:chr_file 0x5401;
allow init system_block_device:blk_file 0x125d;
allow mediaserver binder_device:chr_file 0x6209;
allow netd ptmx_device:chr_file 0x5431;
allow watchdogd watchdog_device:chr_file { 0x5706-0x5707 };

Let me know if you have any questions or if I am missing anything important. Please feel free to add me as a reviewer to your policy changes.

* June 2015 I submitted the extended permissions for ioctl commands patchset to the upstream kernel allowing per-command whitelisting of ioctls. Unfortunately an earlier patchset had already been submitted to the Android kernel leading to differing versions. The primary difference is nomenclature, functionally the patches are similar although the compiled policy binaries are not compatible. To deal with this I will have another post describing the same process for the patchset accepted by the upstream kernel.

No comments:

Post a Comment