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# Use --debug to output path before parsing, this is useful to find files that 11# cause the script to choke. 12# 13# You may like to set kptr_restrict=2 before running script 14# (see Documentation/sysctl/kernel.txt). 15 16use warnings; 17use strict; 18use POSIX; 19use File::Basename; 20use File::Spec; 21use Cwd 'abs_path'; 22use Term::ANSIColor qw(:constants); 23use Getopt::Long qw(:config no_auto_abbrev); 24use Config; 25 26my $P = $0; 27my $V = '0.01'; 28 29# Directories to scan. 30my @DIRS = ('/proc', '/sys'); 31 32# Timer for parsing each file, in seconds. 33my $TIMEOUT = 10; 34 35# Script can only grep for kernel addresses on the following architectures. If 36# your architecture is not listed here and has a grep'able kernel address please 37# consider submitting a patch. 38my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64'); 39 40# Command line options. 41my $help = 0; 42my $debug = 0; 43my $raw = 0; 44my $output_raw = ""; # Write raw results to file. 45my $input_raw = ""; # Read raw results from file instead of scanning. 46 47my $suppress_dmesg = 0; # Don't show dmesg in output. 48my $squash_by_path = 0; # Summary report grouped by absolute path. 49my $squash_by_filename = 0; # Summary report grouped by filename. 50 51# Do not parse these files (absolute path). 52my @skip_parse_files_abs = ('/proc/kmsg', 53 '/proc/kcore', 54 '/proc/fs/ext4/sdb1/mb_groups', 55 '/proc/1/fd/3', 56 '/sys/firmware/devicetree', 57 '/proc/device-tree', 58 '/sys/kernel/debug/tracing/trace_pipe', 59 '/sys/kernel/security/apparmor/revision'); 60 61# Do not parse these files under any subdirectory. 62my @skip_parse_files_any = ('0', 63 '1', 64 '2', 65 'pagemap', 66 'events', 67 'access', 68 'registers', 69 'snapshot_raw', 70 'trace_pipe_raw', 71 'ptmx', 72 'trace_pipe'); 73 74# Do not walk these directories (absolute path). 75my @skip_walk_dirs_abs = (); 76 77# Do not walk these directories under any subdirectory. 78my @skip_walk_dirs_any = ('self', 79 'thread-self', 80 'cwd', 81 'fd', 82 'usbmon', 83 'stderr', 84 'stdin', 85 'stdout'); 86 87sub help 88{ 89 my ($exitcode) = @_; 90 91 print << "EOM"; 92 93Usage: $P [OPTIONS] 94Version: $V 95 96Options: 97 98 -o, --output-raw=<file> Save results for future processing. 99 -i, --input-raw=<file> Read results from file instead of scanning. 100 --raw Show raw results (default). 101 --suppress-dmesg Do not show dmesg results. 102 --squash-by-path Show one result per unique path. 103 --squash-by-filename Show one result per unique filename. 104 -d, --debug Display debugging output. 105 -h, --help, --version Display this help and exit. 106 107Examples: 108 109 # Scan kernel and dump raw results. 110 $0 111 112 # Scan kernel and save results to file. 113 $0 --output-raw scan.out 114 115 # View summary report. 116 $0 --input-raw scan.out --squash-by-filename 117 118Scans the running (64 bit) kernel for potential leaking addresses. 119 120EOM 121 exit($exitcode); 122} 123 124GetOptions( 125 'd|debug' => \$debug, 126 'h|help' => \$help, 127 'version' => \$help, 128 'o|output-raw=s' => \$output_raw, 129 'i|input-raw=s' => \$input_raw, 130 'suppress-dmesg' => \$suppress_dmesg, 131 'squash-by-path' => \$squash_by_path, 132 'squash-by-filename' => \$squash_by_filename, 133 'raw' => \$raw, 134) or help(1); 135 136help(0) if ($help); 137 138if ($input_raw) { 139 format_output($input_raw); 140 exit(0); 141} 142 143if (!$input_raw and ($squash_by_path or $squash_by_filename)) { 144 printf "\nSummary reporting only available with --input-raw=<file>\n"; 145 printf "(First run scan with --output-raw=<file>.)\n"; 146 exit(128); 147} 148 149if (!is_supported_architecture()) { 150 printf "\nScript does not support your architecture, sorry.\n"; 151 printf "\nCurrently we support: \n\n"; 152 foreach(@SUPPORTED_ARCHITECTURES) { 153 printf "\t%s\n", $_; 154 } 155 156 my $archname = $Config{archname}; 157 printf "\n\$ perl -MConfig -e \'print \"\$Config{archname}\\n\"\'\n"; 158 printf "%s\n", $archname; 159 160 exit(129); 161} 162 163if ($output_raw) { 164 open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n"; 165 select $fh; 166} 167 168parse_dmesg(); 169walk(@DIRS); 170 171exit 0; 172 173sub dprint 174{ 175 printf(STDERR @_) if $debug; 176} 177 178sub is_supported_architecture 179{ 180 return (is_x86_64() or is_ppc64()); 181} 182 183sub is_x86_64 184{ 185 my $archname = $Config{archname}; 186 187 if ($archname =~ m/x86_64/) { 188 return 1; 189 } 190 return 0; 191} 192 193sub is_ppc64 194{ 195 my $archname = $Config{archname}; 196 197 if ($archname =~ m/powerpc/ and $archname =~ m/64/) { 198 return 1; 199 } 200 return 0; 201} 202 203sub is_false_positive 204{ 205 my ($match) = @_; 206 207 if ($match =~ '\b(0x)?(f|F){16}\b' or 208 $match =~ '\b(0x)?0{16}\b') { 209 return 1; 210 } 211 212 if (is_x86_64) { 213 # vsyscall memory region, we should probably check against a range here. 214 if ($match =~ '\bf{10}600000\b' or 215 $match =~ '\bf{10}601000\b') { 216 return 1; 217 } 218 } 219 220 return 0; 221} 222 223# True if argument potentially contains a kernel address. 224sub may_leak_address 225{ 226 my ($line) = @_; 227 my $address_re; 228 229 # Signal masks. 230 if ($line =~ '^SigBlk:' or 231 $line =~ '^SigIgn:' or 232 $line =~ '^SigCgt:') { 233 return 0; 234 } 235 236 if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or 237 $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') { 238 return 0; 239 } 240 241 # One of these is guaranteed to be true. 242 if (is_x86_64()) { 243 $address_re = '\b(0x)?ffff[[:xdigit:]]{12}\b'; 244 } elsif (is_ppc64()) { 245 $address_re = '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b'; 246 } 247 248 while (/($address_re)/g) { 249 if (!is_false_positive($1)) { 250 return 1; 251 } 252 } 253 254 return 0; 255} 256 257sub parse_dmesg 258{ 259 open my $cmd, '-|', 'dmesg'; 260 while (<$cmd>) { 261 if (may_leak_address($_)) { 262 print 'dmesg: ' . $_; 263 } 264 } 265 close $cmd; 266} 267 268# True if we should skip this path. 269sub skip 270{ 271 my ($path, $paths_abs, $paths_any) = @_; 272 273 foreach (@$paths_abs) { 274 return 1 if (/^$path$/); 275 } 276 277 my($filename, $dirs, $suffix) = fileparse($path); 278 foreach (@$paths_any) { 279 return 1 if (/^$filename$/); 280 } 281 282 return 0; 283} 284 285sub skip_parse 286{ 287 my ($path) = @_; 288 return skip($path, \@skip_parse_files_abs, \@skip_parse_files_any); 289} 290 291sub timed_parse_file 292{ 293 my ($file) = @_; 294 295 eval { 296 local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required. 297 alarm $TIMEOUT; 298 parse_file($file); 299 alarm 0; 300 }; 301 302 if ($@) { 303 die unless $@ eq "alarm\n"; # Propagate unexpected errors. 304 printf STDERR "timed out parsing: %s\n", $file; 305 } 306} 307 308sub parse_file 309{ 310 my ($file) = @_; 311 312 if (! -R $file) { 313 return; 314 } 315 316 if (skip_parse($file)) { 317 dprint "skipping file: $file\n"; 318 return; 319 } 320 dprint "parsing: $file\n"; 321 322 open my $fh, "<", $file or return; 323 while ( <$fh> ) { 324 if (may_leak_address($_)) { 325 print $file . ': ' . $_; 326 } 327 } 328 close $fh; 329} 330 331 332# True if we should skip walking this directory. 333sub skip_walk 334{ 335 my ($path) = @_; 336 return skip($path, \@skip_walk_dirs_abs, \@skip_walk_dirs_any) 337} 338 339# Recursively walk directory tree. 340sub walk 341{ 342 my @dirs = @_; 343 344 while (my $pwd = shift @dirs) { 345 next if (skip_walk($pwd)); 346 next if (!opendir(DIR, $pwd)); 347 my @files = readdir(DIR); 348 closedir(DIR); 349 350 foreach my $file (@files) { 351 next if ($file eq '.' or $file eq '..'); 352 353 my $path = "$pwd/$file"; 354 next if (-l $path); 355 356 if (-d $path) { 357 push @dirs, $path; 358 } else { 359 timed_parse_file($path); 360 } 361 } 362 } 363} 364 365sub format_output 366{ 367 my ($file) = @_; 368 369 # Default is to show raw results. 370 if ($raw or (!$squash_by_path and !$squash_by_filename)) { 371 dump_raw_output($file); 372 return; 373 } 374 375 my ($total, $dmesg, $paths, $files) = parse_raw_file($file); 376 377 printf "\nTotal number of results from scan (incl dmesg): %d\n", $total; 378 379 if (!$suppress_dmesg) { 380 print_dmesg($dmesg); 381 } 382 383 if ($squash_by_filename) { 384 squash_by($files, 'filename'); 385 } 386 387 if ($squash_by_path) { 388 squash_by($paths, 'path'); 389 } 390} 391 392sub dump_raw_output 393{ 394 my ($file) = @_; 395 396 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 397 while (<$fh>) { 398 if ($suppress_dmesg) { 399 if ("dmesg:" eq substr($_, 0, 6)) { 400 next; 401 } 402 } 403 print $_; 404 } 405 close $fh; 406} 407 408sub parse_raw_file 409{ 410 my ($file) = @_; 411 412 my $total = 0; # Total number of lines parsed. 413 my @dmesg; # dmesg output. 414 my %files; # Unique filenames containing leaks. 415 my %paths; # Unique paths containing leaks. 416 417 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 418 while (my $line = <$fh>) { 419 $total++; 420 421 if ("dmesg:" eq substr($line, 0, 6)) { 422 push @dmesg, $line; 423 next; 424 } 425 426 cache_path(\%paths, $line); 427 cache_filename(\%files, $line); 428 } 429 430 return $total, \@dmesg, \%paths, \%files; 431} 432 433sub print_dmesg 434{ 435 my ($dmesg) = @_; 436 437 print "\ndmesg output:\n"; 438 439 if (@$dmesg == 0) { 440 print "<no results>\n"; 441 return; 442 } 443 444 foreach(@$dmesg) { 445 my $index = index($_, ': '); 446 $index += 2; # skid ': ' 447 print substr($_, $index); 448 } 449} 450 451sub squash_by 452{ 453 my ($ref, $desc) = @_; 454 455 print "\nResults squashed by $desc (excl dmesg). "; 456 print "Displaying [<number of results> <$desc>], <example result>\n"; 457 458 if (keys %$ref == 0) { 459 print "<no results>\n"; 460 return; 461 } 462 463 foreach(keys %$ref) { 464 my $lines = $ref->{$_}; 465 my $length = @$lines; 466 printf "[%d %s] %s", $length, $_, @$lines[0]; 467 } 468} 469 470sub cache_path 471{ 472 my ($paths, $line) = @_; 473 474 my $index = index($line, ': '); 475 my $path = substr($line, 0, $index); 476 477 $index += 2; # skip ': ' 478 add_to_cache($paths, $path, substr($line, $index)); 479} 480 481sub cache_filename 482{ 483 my ($files, $line) = @_; 484 485 my $index = index($line, ': '); 486 my $path = substr($line, 0, $index); 487 my $filename = basename($path); 488 489 $index += 2; # skip ': ' 490 add_to_cache($files, $filename, substr($line, $index)); 491} 492 493sub add_to_cache 494{ 495 my ($cache, $key, $value) = @_; 496 497 if (!$cache->{$key}) { 498 $cache->{$key} = (); 499 } 500 push @{$cache->{$key}}, $value; 501} 502