| #!/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); | |
| } | |
| } | |
| } | |
| } |