#!/usr/bin/env perl # # (c) 2017 Tobin C. Harding <me@tobin.cc> # Licensed under the terms of the GNU GPL License version 2 # # leaking_addresses.pl: Scan 64 bit kernel for potential leaking addresses. # - Scans dmesg output. # - Walks directory tree and parses each file (for each directory in @DIRS). # # You can configure the behaviour of the script; # # - By adding paths, for directories you do not want to walk; # absolute paths: @skip_walk_dirs_abs # directory names: @skip_walk_dirs_any # # - By adding paths, for files you do not want to parse; # absolute paths: @skip_parse_files_abs # file names: @skip_parse_files_any # # The use of @skip_xxx_xxx_any causes files to be skipped where ever they occur. # For example adding 'fd' to @skip_walk_dirs_any causes the fd/ directory to be # skipped for all PID sub-directories of /proc # # The same thing can be achieved by passing command line options to --dont-walk # and --dont-parse. If absolute paths are supplied to these options they are # appended to the @skip_xxx_xxx_abs arrays. If file names are supplied to these # options, they are appended to the @skip_xxx_xxx_any arrays. # # Use --debug to output path before parsing, this is useful to find files that # cause the script to choke. # # You may like to set kptr_restrict=2 before running script # (see Documentation/sysctl/kernel.txt). use warnings; use strict; use POSIX; use File::Basename; use File::Spec; use Cwd 'abs_path'; use Term::ANSIColor qw(:constants); use Getopt::Long qw(:config no_auto_abbrev); my $P = $0; my $V = '0.01'; # Directories to scan. my @DIRS = ('/proc', '/sys'); # Command line options. my $help = 0; my $debug = 0; my @dont_walk = (); my @dont_parse = (); # Do not parse these files (absolute path). my @skip_parse_files_abs = ('/proc/kmsg', '/proc/kcore', '/proc/fs/ext4/sdb1/mb_groups', '/proc/1/fd/3', '/sys/kernel/debug/tracing/trace_pipe', '/sys/kernel/security/apparmor/revision'); # Do not parse thes files under any subdirectory. my @skip_parse_files_any = ('0', '1', '2', 'pagemap', 'events', 'access', 'registers', 'snapshot_raw', 'trace_pipe_raw', 'ptmx', 'trace_pipe'); # Do not walk these directories (absolute path). my @skip_walk_dirs_abs = (); # Do not walk these directories under any subdirectory. my @skip_walk_dirs_any = ('self', 'thread-self', 'cwd', 'fd', 'stderr', 'stdin', 'stdout'); sub help { my ($exitcode) = @_; print << "EOM"; Usage: $P [OPTIONS] Version: $V Options: --dont-walk=<dir> Don't walk tree starting at <dir>. --dont-parse=<file> Don't parse <file>. -d, --debug Display debugging output. -h, --help, --version Display this help and exit. If an absolute path is passed to --dont_XXX then this path is skipped. If a single filename is passed then this file/directory will be skipped when appearing under any subdirectory. Example: # Just scan dmesg output. scripts/leaking_addresses.pl --dont_walk_abs /proc --dont_walk_abs /sys Scans the running (64 bit) kernel for potential leaking addresses. EOM exit($exitcode); } GetOptions( 'dont-walk=s' => \@dont_walk, 'dont-parse=s' => \@dont_parse, 'd|debug' => \$debug, 'h|help' => \$help, 'version' => \$help ) or help(1); help(0) if ($help); push_to_global(); parse_dmesg(); walk(@DIRS); exit 0; sub debug_arrays { print 'dirs_any: ' . join(", ", @skip_walk_dirs_any) . "\n"; print 'dirs_abs: ' . join(", ", @skip_walk_dirs_abs) . "\n"; print 'parse_any: ' . join(", ", @skip_parse_files_any) . "\n"; print 'parse_abs: ' . join(", ", @skip_parse_files_abs) . "\n"; } sub dprint { printf(STDERR @_) if $debug; } sub push_in_abs_any { my ($in, $abs, $any) = @_; foreach my $path (@$in) { if (File::Spec->file_name_is_absolute($path)) { push @$abs, $path; } elsif (index($path,'/') == -1) { push @$any, $path; } else { print 'path error: ' . $path; } } } # Push command line options to global arrays. sub push_to_global { push_in_abs_any(\@dont_walk, \@skip_walk_dirs_abs, \@skip_walk_dirs_any); push_in_abs_any(\@dont_parse, \@skip_parse_files_abs, \@skip_parse_files_any); } sub is_false_positive { my ($match) = @_; if ($match =~ '\b(0x)?(f|F){16}\b' or $match =~ '\b(0x)?0{16}\b') { return 1; } # vsyscall memory region, we should probably check against a range here. if ($match =~ '\bf{10}600000\b' or $match =~ '\bf{10}601000\b') { return 1; } return 0; } # True if argument potentially contains a kernel address. sub may_leak_address { my ($line) = @_; my $address = '\b(0x)?ffff[[:xdigit:]]{12}\b'; # Signal masks. if ($line =~ '^SigBlk:' or $line =~ '^SigCgt:') { return 0; } if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') { return 0; } while (/($address)/g) { if (!is_false_positive($1)) { return 1; } } return 0; } sub parse_dmesg { open my $cmd, '-|', 'dmesg'; while (<$cmd>) { if (may_leak_address($_)) { print 'dmesg: ' . $_; } } close $cmd; } # True if we should skip this path. sub skip { my ($path, $paths_abs, $paths_any) = @_; foreach (@$paths_abs) { return 1 if (/^$path$/); } my($filename, $dirs, $suffix) = fileparse($path); foreach (@$paths_any) { return 1 if (/^$filename$/); } return 0; } sub skip_parse { my ($path) = @_; return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any); } sub parse_file { my ($file) = @_; if (! -R $file) { return; } if (skip_parse($file)) { dprint "skipping file: $file\n"; return; } dprint "parsing: $file\n"; open my $fh, "<", $file or return; while ( <$fh> ) { if (may_leak_address($_)) { print $file . ': ' . $_; } } close $fh; } # True if we should skip walking this directory. sub skip_walk { my ($path) = @_; return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any) } # Recursively walk directory tree. sub walk { my @dirs = @_; my %seen; while (my $pwd = shift @dirs) { next if (skip_walk($pwd)); next if (!opendir(DIR, $pwd)); my @files = readdir(DIR); closedir(DIR); foreach my $file (@files) { next if ($file eq '.' or $file eq '..'); my $path = "$pwd/$file"; next if (-l $path); if (-d $path) { push @dirs, $path; } else { parse_file($path); } } } }