1#!/usr/bin/env perl 2# 3# (c) 2017 Tobin C. Harding <me@tobin.cc> 4# Licensed under the terms of the GNU GPL License version 2 5# 6# leaking_addresses.pl: Scan 64 bit kernel for potential leaking addresses. 7# - Scans dmesg output. 8# - Walks directory tree and parses each file (for each directory in @DIRS). 9# 10# You can configure the behaviour of the script; 11# 12# - By adding paths, for directories you do not want to walk; 13# absolute paths: @skip_walk_dirs_abs 14# directory names: @skip_walk_dirs_any 15# 16# - By adding paths, for files you do not want to parse; 17# absolute paths: @skip_parse_files_abs 18# file names: @skip_parse_files_any 19# 20# The use of @skip_xxx_xxx_any causes files to be skipped where ever they occur. 21# For example adding 'fd' to @skip_walk_dirs_any causes the fd/ directory to be 22# skipped for all PID sub-directories of /proc 23# 24# The same thing can be achieved by passing command line options to --dont-walk 25# and --dont-parse. If absolute paths are supplied to these options they are 26# appended to the @skip_xxx_xxx_abs arrays. If file names are supplied to these 27# options, they are appended to the @skip_xxx_xxx_any arrays. 28# 29# Use --debug to output path before parsing, this is useful to find files that 30# cause the script to choke. 31# 32# You may like to set kptr_restrict=2 before running script 33# (see Documentation/sysctl/kernel.txt). 34 35use warnings; 36use strict; 37use POSIX; 38use File::Basename; 39use File::Spec; 40use Cwd 'abs_path'; 41use Term::ANSIColor qw(:constants); 42use Getopt::Long qw(:config no_auto_abbrev); 43 44my $P = $0; 45my $V = '0.01'; 46 47# Directories to scan. 48my @DIRS = ('/proc', '/sys'); 49 50# Command line options. 51my $help = 0; 52my $debug = 0; 53my @dont_walk = (); 54my @dont_parse = (); 55 56# Do not parse these files (absolute path). 57my @skip_parse_files_abs = ('/proc/kmsg', 58 '/proc/kcore', 59 '/proc/fs/ext4/sdb1/mb_groups', 60 '/proc/1/fd/3', 61 '/sys/kernel/debug/tracing/trace_pipe', 62 '/sys/kernel/security/apparmor/revision'); 63 64# Do not parse thes files under any subdirectory. 65my @skip_parse_files_any = ('0', 66 '1', 67 '2', 68 'pagemap', 69 'events', 70 'access', 71 'registers', 72 'snapshot_raw', 73 'trace_pipe_raw', 74 'ptmx', 75 'trace_pipe'); 76 77# Do not walk these directories (absolute path). 78my @skip_walk_dirs_abs = (); 79 80# Do not walk these directories under any subdirectory. 81my @skip_walk_dirs_any = ('self', 82 'thread-self', 83 'cwd', 84 'fd', 85 'stderr', 86 'stdin', 87 'stdout'); 88 89sub help 90{ 91 my ($exitcode) = @_; 92 93 print << "EOM"; 94Usage: $P [OPTIONS] 95Version: $V 96 97Options: 98 99 --dont-walk=<dir> Don't walk tree starting at <dir>. 100 --dont-parse=<file> Don't parse <file>. 101 -d, --debug Display debugging output. 102 -h, --help, --version Display this help and exit. 103 104If an absolute path is passed to --dont_XXX then this path is skipped. If a 105single filename is passed then this file/directory will be skipped when 106appearing under any subdirectory. 107 108Example: 109 110 # Just scan dmesg output. 111 scripts/leaking_addresses.pl --dont_walk_abs /proc --dont_walk_abs /sys 112 113Scans the running (64 bit) kernel for potential leaking addresses. 114 115EOM 116 exit($exitcode); 117} 118 119GetOptions( 120 'dont-walk=s' => \@dont_walk, 121 'dont-parse=s' => \@dont_parse, 122 'd|debug' => \$debug, 123 'h|help' => \$help, 124 'version' => \$help 125) or help(1); 126 127help(0) if ($help); 128 129push_to_global(); 130 131parse_dmesg(); 132walk(@DIRS); 133 134exit 0; 135 136sub debug_arrays 137{ 138 print 'dirs_any: ' . join(", ", @skip_walk_dirs_any) . "\n"; 139 print 'dirs_abs: ' . join(", ", @skip_walk_dirs_abs) . "\n"; 140 print 'parse_any: ' . join(", ", @skip_parse_files_any) . "\n"; 141 print 'parse_abs: ' . join(", ", @skip_parse_files_abs) . "\n"; 142} 143 144sub dprint 145{ 146 printf(STDERR @_) if $debug; 147} 148 149sub push_in_abs_any 150{ 151 my ($in, $abs, $any) = @_; 152 153 foreach my $path (@$in) { 154 if (File::Spec->file_name_is_absolute($path)) { 155 push @$abs, $path; 156 } elsif (index($path,'/') == -1) { 157 push @$any, $path; 158 } else { 159 print 'path error: ' . $path; 160 } 161 } 162} 163 164# Push command line options to global arrays. 165sub push_to_global 166{ 167 push_in_abs_any(\@dont_walk, \@skip_walk_dirs_abs, \@skip_walk_dirs_any); 168 push_in_abs_any(\@dont_parse, \@skip_parse_files_abs, \@skip_parse_files_any); 169} 170 171sub is_false_positive 172{ 173 my ($match) = @_; 174 175 if ($match =~ '\b(0x)?(f|F){16}\b' or 176 $match =~ '\b(0x)?0{16}\b') { 177 return 1; 178 } 179 180 # vsyscall memory region, we should probably check against a range here. 181 if ($match =~ '\bf{10}600000\b' or 182 $match =~ '\bf{10}601000\b') { 183 return 1; 184 } 185 186 return 0; 187} 188 189# True if argument potentially contains a kernel address. 190sub may_leak_address 191{ 192 my ($line) = @_; 193 my $address = '\b(0x)?ffff[[:xdigit:]]{12}\b'; 194 195 # Signal masks. 196 if ($line =~ '^SigBlk:' or 197 $line =~ '^SigCgt:') { 198 return 0; 199 } 200 201 if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or 202 $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') { 203 return 0; 204 } 205 206 while (/($address)/g) { 207 if (!is_false_positive($1)) { 208 return 1; 209 } 210 } 211 212 return 0; 213} 214 215sub parse_dmesg 216{ 217 open my $cmd, '-|', 'dmesg'; 218 while (<$cmd>) { 219 if (may_leak_address($_)) { 220 print 'dmesg: ' . $_; 221 } 222 } 223 close $cmd; 224} 225 226# True if we should skip this path. 227sub skip 228{ 229 my ($path, $paths_abs, $paths_any) = @_; 230 231 foreach (@$paths_abs) { 232 return 1 if (/^$path$/); 233 } 234 235 my($filename, $dirs, $suffix) = fileparse($path); 236 foreach (@$paths_any) { 237 return 1 if (/^$filename$/); 238 } 239 240 return 0; 241} 242 243sub skip_parse 244{ 245 my ($path) = @_; 246 return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any); 247} 248 249sub parse_file 250{ 251 my ($file) = @_; 252 253 if (! -R $file) { 254 return; 255 } 256 257 if (skip_parse($file)) { 258 dprint "skipping file: $file\n"; 259 return; 260 } 261 dprint "parsing: $file\n"; 262 263 open my $fh, "<", $file or return; 264 while ( <$fh> ) { 265 if (may_leak_address($_)) { 266 print $file . ': ' . $_; 267 } 268 } 269 close $fh; 270} 271 272 273# True if we should skip walking this directory. 274sub skip_walk 275{ 276 my ($path) = @_; 277 return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any) 278} 279 280# Recursively walk directory tree. 281sub walk 282{ 283 my @dirs = @_; 284 my %seen; 285 286 while (my $pwd = shift @dirs) { 287 next if (skip_walk($pwd)); 288 next if (!opendir(DIR, $pwd)); 289 my @files = readdir(DIR); 290 closedir(DIR); 291 292 foreach my $file (@files) { 293 next if ($file eq '.' or $file eq '..'); 294 295 my $path = "$pwd/$file"; 296 next if (-l $path); 297 298 if (-d $path) { 299 push @dirs, $path; 300 } else { 301 parse_file($path); 302 } 303 } 304 } 305} 306