#!/usr/bin/perl # Copyright 2011, Intel Corporation # # This file is part of the Linux kernel # # This program file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by the # Free Software Foundation; version 2 of the License. # # Authors: # Arjan van de Ven # # This script runs a set of rule based checks against the /sys/kernel/debug/kernel_page_tables # file as generated by the CONFIG_X86_PTDUMP configuration option. # # The rules in question are (on a high level, details in the code below) # 1) No memory range should be both writable and executable # 2) All known functions should be in a executable memory range # 3) All known const variables should be in a write protected/non executable memory range # 4) All known non-const variables should be in a writeable/not executable memory range # 5) All executable memory regions should have known kernel code in them # (eg no memory region should be "ghost executable") # my @start_address; my @end_address; my @range; my @writable; my @executable; my @present; my @area; my @accountedfor; my $areacount; my %whitelist; my $writestr; my $execstr; my $presentstr; my %objtypes; # # Push a memory range, from the debugfs file, into the arrays above # sub push_area { my ($start, $end, $w, $x, $p, $area) = @_; $start_address[$areacount] = hex("0x" . $start); $end_address[$areacount] = hex("0x" . $end); $range[$areacount] = "$start-$end"; $writable[$areacount] = $w; $executable[$areacount] = $x; $present[$areacount] = $p; $area[$areacount] = $area; $accountedfor[$areacount] = 0; # basic sanity checks # rule 1: fixmap area is not executable # rule 2: kmap's are not executable # rule 3: memory must not be both executable and writeable if ($area =~ /Fixmap Area/ && $x == 1) { print "Error: Fixmap Area $range[$areacount] is executable\n"; } if ($area =~ /Persisent kmap/ && $x == 1) { print "Error: Perstistent kmap area $range[$areacount] is executable\n"; } if ($w == 1 && $x == 1) { print "Error: Memory range $range[$areacount] is both writable and executable\n"; } $areacount = $areacount + 1; } my $startcache = 0; # # Enforce a set of write/execute permissions for a specific address # sub enforce_perms { my ($address, $w, $x, $comment, $type) = @_; my $counter = 0; if ($address >= $start_address[$startcache]) { $counter = $startcache; } while ($counter < $areacount) { if ($address >= $start_address[$counter] && $address < $end_address[$counter]) { if ($w != $writable[$counter]) { print "Error: Memory range $range[$counter] has incorrect write permission $writestr[$writable[$counter]] for '$objtypes{$type}' $comment (", sprintf("%x", $address), ")\n"; } if ($x != $executable[$counter]) { print "Error: Memory range $range[$counter] has incorrect execute permission $execstr[$executable[$counter]] for '$objtypes{$type}' $comment (", sprintf("%x", $address), ")\n"; } if ($present[$counter] != 1) { print "Error: Memory range $range[$counter] has incorrect present bit $presentstr[$present[$counter]] for '$objtypes{$type}' $comment (", sprintf("%x", $address), ")\n"; } $accountedfor[$counter] = 1; $startcache = $counter; $counter = $areacount; } $counter = $counter + 1; } } # # Debug helper, not used in production # sub dump_areas { my $counter = 0; while ($counter < $areacount) { print "$range[$counter]\tW - $writable[$counter]\tX - $executable[$counter]\tP - $present[$counter]\t$area[$counter]\t$accountedfor[$counter]\n"; $counter = $counter + 1; } } # # Dump all memory areas that are executable but are unaccounted for with # actual code # sub show_unaccounted_areas { my $counter = 0; while ($counter < $areacount) { if ($accountedfor[$counter] == 0 && $executable[$counter]==1) { print "Warning: Memory range $range[$counter] is executable without known kernel functions in the range\n"; } $counter = $counter + 1; } } # # return the content from a file that has a hex number inside it # sub readhex { my ($filename) = @_; my $address = 0; open(FILE, $filename) || return 0; while () { my $line = $_; chomp($line); $address = hex($_); } close(FILE); return $address; } print "Checking the kernel pagetable structures\n"; $writestr[0] = "RO"; $writestr[1] = "W"; $execstr[0] = "NX"; $execstr[1] = "X"; $presentstr[0] = "NP"; $presentstr[1] = "P"; # # A few whitelisted symbols. # These are "zero size" end markers, who's address gets put at the start of the next # page due to alignment; they're zero size so the page they're at is of a different # region/type. $whitelist{"__end_rodata"} = 1; $whitelist{"__smp_locks_end"} = 1; $whitelist{"i386_start_kernel"} = 1; $whitelist{"_sinittext"} = 1; # # Parse "/sys/kernel/debug/kernel_page_tables" into the datastructures # my $ar = ""; open (SYSFS, "/sys/kernel/debug/kernel_page_tables") || die "debugfs file not found -- are you root? is CONFIG_X86_PTDUMP enabled?"; while () { my $line = $_; chomp($line); if ($line =~ /\-\-\-\[ (.*) \]-\-\-/) { $ar = $1; } if ($line =~ /0x([0-9a-f]+)\-0x([0-9a-f]+)/) { my $st = $1; my $end = $2; my $ex = 0; my $p = 0; my $w = 0; if ($line =~ / RW /) { $w = 1; $p = 1;}; if ($line =~ / ro /) { $w = 0; $p = 1;}; if ($line =~ / NX /) { $ex = 0; $p = 1;}; if ($line =~ / x /) { $ex = 1; $p = 1;}; push_area($st, $end, $w, $ex, $p, $ar); } } close(SYSFS); # # Correlate the memory map with /proc/kallsyms # $objtypes{"R"} = "const data"; $objtypes{"r"} = "static const data"; $objtypes{"T"} = "code"; $objtypes{"t"} = "static code"; $objtypes{"D"} = "data"; $objtypes{"d"} = "static data"; $objtypes{"B"} = "data (BSS)"; $objtypes{"b"} = "static data (BSS)"; open(KALL, "/proc/kallsyms") || die "Cannot open /proc/kallsyms"; my $ininit = 0; my $inpercpu = 0; while () { my $line = $_; chomp($line); if ($line =~ /([0-9a-f]+) ([tTrRdDWwBb]) (.*)/) { my $address = hex("0x" . $1); my $type = $2; my $comment = $3; # __init functions are gone from memory, but are still in kallsyms # so we need to skip them. if ($comment eq "__init_begin") { $ininit = 1; }; if ($comment eq "__per_cpu_start") { $inpercpu = 1; }; if ($ininit == 0 && $inpercpu == 0 && !defined($whitelist{$comment})) { # Code must be read only and executable if ($type eq "T" || $type eq "t") { enforce_perms($address, 0, 1, $comment, $type); } # Data must be writable and non-executable if ($type eq "D" || $type eq "d") { enforce_perms($address, 1, 0, $comment, $type); } # BSS is same as data if ($type eq "B" || $type eq "b") { enforce_perms($address, 1, 0, $comment, $type); } # const data must be neither writable nor executable if ($type eq "R" || $type eq "r") { enforce_perms($address, 0, 0, $comment, $type); } } if ($comment eq "__init_end") { $ininit = 0; }; if ($comment eq "__per_cpu_end") { $inpercpu = 0; }; } else { print "No match for $line\n"; } } close(KALL); # # Now correlate the memory map with the /sys/modules/*/sections/ information # opendir(DIR, "/sys/module") || die "Cannot open /sys/modules"; @names = readdir(DIR); closedir(DIR); foreach $name (@names) { my $module = $name; my $addr; next if ($name eq "."); next if ($name eq ".."); $name = "/sys/module/" . $name . "/sections/"; $addr = readhex($name . ".data"); if ($addr > 0) { enforce_perms($addr, 1, 0, $module, "D"); } $addr = readhex($name . ".rodata"); if ($addr > 0) { enforce_perms($addr, 0, 0, $module, "R"); } $addr = readhex($name . ".text"); if ($addr > 0) { enforce_perms($addr, 0, 1, $module, "T"); } $addr = readhex($name . ".bss"); if ($addr > 0) { enforce_perms($addr, 1, 0, $module, "B"); } } show_unaccounted_areas(); # dump_areas();