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 the 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# 14# When the system is idle it is likely that most files under /proc/PID will be 15# identical for various processes. Scanning _all_ the PIDs under /proc is 16# unnecessary and implies that we are thoroughly scanning /proc. This is _not_ 17# the case because there may be ways userspace can trigger creation of /proc 18# files that leak addresses but were not present during a scan. For these two 19# reasons we exclude all PID directories under /proc except '1/' 20 21use warnings; 22use strict; 23use POSIX; 24use File::Basename; 25use File::Spec; 26use Cwd 'abs_path'; 27use Term::ANSIColor qw(:constants); 28use Getopt::Long qw(:config no_auto_abbrev); 29use Config; 30use bigint qw/hex/; 31use feature 'state'; 32 33my $P = $0; 34 35# Directories to scan. 36my @DIRS = ('/proc', '/sys'); 37 38# Timer for parsing each file, in seconds. 39my $TIMEOUT = 10; 40 41# Kernel addresses vary by architecture. We can only auto-detect the following 42# architectures (using `uname -m`). (flag --32-bit overrides auto-detection.) 43my @SUPPORTED_ARCHITECTURES = ('x86_64', 'ppc64', 'x86'); 44 45# Command line options. 46my $help = 0; 47my $debug = 0; 48my $raw = 0; 49my $output_raw = ""; # Write raw results to file. 50my $input_raw = ""; # Read raw results from file instead of scanning. 51my $suppress_dmesg = 0; # Don't show dmesg in output. 52my $squash_by_path = 0; # Summary report grouped by absolute path. 53my $squash_by_filename = 0; # Summary report grouped by filename. 54my $kernel_config_file = ""; # Kernel configuration file. 55my $opt_32bit = 0; # Scan 32-bit kernel. 56my $page_offset_32bit = 0; # Page offset for 32-bit kernel. 57 58# Skip these absolute paths. 59my @skip_abs = ( 60 '/proc/kmsg', 61 '/proc/device-tree', 62 '/proc/1/syscall', 63 '/sys/firmware/devicetree', 64 '/sys/kernel/debug/tracing/trace_pipe', 65 '/sys/kernel/security/apparmor/revision'); 66 67# Skip these under any subdirectory. 68my @skip_any = ( 69 'pagemap', 70 'events', 71 'access', 72 'registers', 73 'snapshot_raw', 74 'trace_pipe_raw', 75 'ptmx', 76 'trace_pipe', 77 'fd', 78 'usbmon'); 79 80sub help 81{ 82 my ($exitcode) = @_; 83 84 print << "EOM"; 85 86Usage: $P [OPTIONS] 87 88Options: 89 90 -o, --output-raw=<file> Save results for future processing. 91 -i, --input-raw=<file> Read results from file instead of scanning. 92 --raw Show raw results (default). 93 --suppress-dmesg Do not show dmesg results. 94 --squash-by-path Show one result per unique path. 95 --squash-by-filename Show one result per unique filename. 96 --kernel-config-file=<file> Kernel configuration file (e.g /boot/config) 97 --32-bit Scan 32-bit kernel. 98 --page-offset-32-bit=o Page offset (for 32-bit kernel 0xABCD1234). 99 -d, --debug Display debugging output. 100 -h, --help, --version Display this help and exit. 101 102Scans the running kernel for potential leaking addresses. 103 104EOM 105 exit($exitcode); 106} 107 108GetOptions( 109 'd|debug' => \$debug, 110 'h|help' => \$help, 111 'version' => \$help, 112 'o|output-raw=s' => \$output_raw, 113 'i|input-raw=s' => \$input_raw, 114 'suppress-dmesg' => \$suppress_dmesg, 115 'squash-by-path' => \$squash_by_path, 116 'squash-by-filename' => \$squash_by_filename, 117 'raw' => \$raw, 118 'kernel-config-file=s' => \$kernel_config_file, 119 '32-bit' => \$opt_32bit, 120 'page-offset-32-bit=o' => \$page_offset_32bit, 121) or help(1); 122 123help(0) if ($help); 124 125if ($input_raw) { 126 format_output($input_raw); 127 exit(0); 128} 129 130if (!$input_raw and ($squash_by_path or $squash_by_filename)) { 131 printf "\nSummary reporting only available with --input-raw=<file>\n"; 132 printf "(First run scan with --output-raw=<file>.)\n"; 133 exit(128); 134} 135 136if (!(is_supported_architecture() or $opt_32bit or $page_offset_32bit)) { 137 printf "\nScript does not support your architecture, sorry.\n"; 138 printf "\nCurrently we support: \n\n"; 139 foreach(@SUPPORTED_ARCHITECTURES) { 140 printf "\t%s\n", $_; 141 } 142 printf("\n"); 143 144 printf("If you are running a 32-bit architecture you may use:\n"); 145 printf("\n\t--32-bit or --page-offset-32-bit=<page offset>\n\n"); 146 147 my $archname = `uname -m`; 148 printf("Machine hardware name (`uname -m`): %s\n", $archname); 149 150 exit(129); 151} 152 153if ($output_raw) { 154 open my $fh, '>', $output_raw or die "$0: $output_raw: $!\n"; 155 select $fh; 156} 157 158parse_dmesg(); 159walk(@DIRS); 160 161exit 0; 162 163sub dprint 164{ 165 printf(STDERR @_) if $debug; 166} 167 168sub is_supported_architecture 169{ 170 return (is_x86_64() or is_ppc64() or is_ix86_32()); 171} 172 173sub is_32bit 174{ 175 # Allow --32-bit or --page-offset-32-bit to override 176 if ($opt_32bit or $page_offset_32bit) { 177 return 1; 178 } 179 180 return is_ix86_32(); 181} 182 183sub is_ix86_32 184{ 185 state $arch = `uname -m`; 186 187 chomp $arch; 188 if ($arch =~ m/i[3456]86/) { 189 return 1; 190 } 191 return 0; 192} 193 194sub is_arch 195{ 196 my ($desc) = @_; 197 my $arch = `uname -m`; 198 199 chomp $arch; 200 if ($arch eq $desc) { 201 return 1; 202 } 203 return 0; 204} 205 206sub is_x86_64 207{ 208 state $is = is_arch('x86_64'); 209 return $is; 210} 211 212sub is_ppc64 213{ 214 state $is = is_arch('ppc64'); 215 return $is; 216} 217 218# Gets config option value from kernel config file. 219# Returns "" on error or if config option not found. 220sub get_kernel_config_option 221{ 222 my ($option) = @_; 223 my $value = ""; 224 my $tmp_file = ""; 225 my @config_files; 226 227 # Allow --kernel-config-file to override. 228 if ($kernel_config_file ne "") { 229 @config_files = ($kernel_config_file); 230 } elsif (-R "/proc/config.gz") { 231 my $tmp_file = "/tmp/tmpkconf"; 232 233 if (system("gunzip < /proc/config.gz > $tmp_file")) { 234 dprint "$0: system(gunzip < /proc/config.gz) failed\n"; 235 return ""; 236 } else { 237 @config_files = ($tmp_file); 238 } 239 } else { 240 my $file = '/boot/config-' . `uname -r`; 241 chomp $file; 242 @config_files = ($file, '/boot/config'); 243 } 244 245 foreach my $file (@config_files) { 246 dprint("parsing config file: %s\n", $file); 247 $value = option_from_file($option, $file); 248 if ($value ne "") { 249 last; 250 } 251 } 252 253 if ($tmp_file ne "") { 254 system("rm -f $tmp_file"); 255 } 256 257 return $value; 258} 259 260# Parses $file and returns kernel configuration option value. 261sub option_from_file 262{ 263 my ($option, $file) = @_; 264 my $str = ""; 265 my $val = ""; 266 267 open(my $fh, "<", $file) or return ""; 268 while (my $line = <$fh> ) { 269 if ($line =~ /^$option/) { 270 ($str, $val) = split /=/, $line; 271 chomp $val; 272 last; 273 } 274 } 275 276 close $fh; 277 return $val; 278} 279 280sub is_false_positive 281{ 282 my ($match) = @_; 283 284 if (is_32bit()) { 285 return is_false_positive_32bit($match); 286 } 287 288 # 64 bit false positives. 289 290 if ($match =~ '\b(0x)?(f|F){16}\b' or 291 $match =~ '\b(0x)?0{16}\b') { 292 return 1; 293 } 294 295 if (is_x86_64() and is_in_vsyscall_memory_region($match)) { 296 return 1; 297 } 298 299 return 0; 300} 301 302sub is_false_positive_32bit 303{ 304 my ($match) = @_; 305 state $page_offset = get_page_offset(); 306 307 if ($match =~ '\b(0x)?(f|F){8}\b') { 308 return 1; 309 } 310 311 if (hex($match) < $page_offset) { 312 return 1; 313 } 314 315 return 0; 316} 317 318# returns integer value 319sub get_page_offset 320{ 321 my $page_offset; 322 my $default_offset = 0xc0000000; 323 324 # Allow --page-offset-32bit to override. 325 if ($page_offset_32bit != 0) { 326 return $page_offset_32bit; 327 } 328 329 $page_offset = get_kernel_config_option('CONFIG_PAGE_OFFSET'); 330 if (!$page_offset) { 331 return $default_offset; 332 } 333 return $page_offset; 334} 335 336sub is_in_vsyscall_memory_region 337{ 338 my ($match) = @_; 339 340 my $hex = hex($match); 341 my $region_min = hex("0xffffffffff600000"); 342 my $region_max = hex("0xffffffffff601000"); 343 344 return ($hex >= $region_min and $hex <= $region_max); 345} 346 347# True if argument potentially contains a kernel address. 348sub may_leak_address 349{ 350 my ($line) = @_; 351 my $address_re; 352 353 # Signal masks. 354 if ($line =~ '^SigBlk:' or 355 $line =~ '^SigIgn:' or 356 $line =~ '^SigCgt:') { 357 return 0; 358 } 359 360 if ($line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or 361 $line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b') { 362 return 0; 363 } 364 365 $address_re = get_address_re(); 366 while ($line =~ /($address_re)/g) { 367 if (!is_false_positive($1)) { 368 return 1; 369 } 370 } 371 372 return 0; 373} 374 375sub get_address_re 376{ 377 if (is_ppc64()) { 378 return '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b'; 379 } elsif (is_32bit()) { 380 return '\b(0x)?[[:xdigit:]]{8}\b'; 381 } 382 383 return get_x86_64_re(); 384} 385 386sub get_x86_64_re 387{ 388 # We handle page table levels but only if explicitly configured using 389 # CONFIG_PGTABLE_LEVELS. If config file parsing fails or config option 390 # is not found we default to using address regular expression suitable 391 # for 4 page table levels. 392 state $ptl = get_kernel_config_option('CONFIG_PGTABLE_LEVELS'); 393 394 if ($ptl == 5) { 395 return '\b(0x)?ff[[:xdigit:]]{14}\b'; 396 } 397 return '\b(0x)?ffff[[:xdigit:]]{12}\b'; 398} 399 400sub parse_dmesg 401{ 402 open my $cmd, '-|', 'dmesg'; 403 while (<$cmd>) { 404 if (may_leak_address($_)) { 405 print 'dmesg: ' . $_; 406 } 407 } 408 close $cmd; 409} 410 411# True if we should skip this path. 412sub skip 413{ 414 my ($path) = @_; 415 416 foreach (@skip_abs) { 417 return 1 if (/^$path$/); 418 } 419 420 my($filename, $dirs, $suffix) = fileparse($path); 421 foreach (@skip_any) { 422 return 1 if (/^$filename$/); 423 } 424 425 return 0; 426} 427 428sub timed_parse_file 429{ 430 my ($file) = @_; 431 432 eval { 433 local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required. 434 alarm $TIMEOUT; 435 parse_file($file); 436 alarm 0; 437 }; 438 439 if ($@) { 440 die unless $@ eq "alarm\n"; # Propagate unexpected errors. 441 printf STDERR "timed out parsing: %s\n", $file; 442 } 443} 444 445sub parse_file 446{ 447 my ($file) = @_; 448 449 if (! -R $file) { 450 return; 451 } 452 453 if (! -T $file) { 454 return; 455 } 456 457 open my $fh, "<", $file or return; 458 while ( <$fh> ) { 459 if (may_leak_address($_)) { 460 print $file . ': ' . $_; 461 } 462 } 463 close $fh; 464} 465 466# Checks if the actual path name is leaking a kernel address. 467sub check_path_for_leaks 468{ 469 my ($path) = @_; 470 471 if (may_leak_address($path)) { 472 printf("Path name may contain address: $path\n"); 473 } 474} 475 476# Recursively walk directory tree. 477sub walk 478{ 479 my @dirs = @_; 480 481 while (my $pwd = shift @dirs) { 482 next if (!opendir(DIR, $pwd)); 483 my @files = readdir(DIR); 484 closedir(DIR); 485 486 foreach my $file (@files) { 487 next if ($file eq '.' or $file eq '..'); 488 489 my $path = "$pwd/$file"; 490 next if (-l $path); 491 492 # skip /proc/PID except /proc/1 493 next if (($path =~ /^\/proc\/[0-9]+$/) && 494 ($path !~ /^\/proc\/1$/)); 495 496 next if (skip($path)); 497 498 check_path_for_leaks($path); 499 500 if (-d $path) { 501 push @dirs, $path; 502 next; 503 } 504 505 dprint "parsing: $path\n"; 506 timed_parse_file($path); 507 } 508 } 509} 510 511sub format_output 512{ 513 my ($file) = @_; 514 515 # Default is to show raw results. 516 if ($raw or (!$squash_by_path and !$squash_by_filename)) { 517 dump_raw_output($file); 518 return; 519 } 520 521 my ($total, $dmesg, $paths, $files) = parse_raw_file($file); 522 523 printf "\nTotal number of results from scan (incl dmesg): %d\n", $total; 524 525 if (!$suppress_dmesg) { 526 print_dmesg($dmesg); 527 } 528 529 if ($squash_by_filename) { 530 squash_by($files, 'filename'); 531 } 532 533 if ($squash_by_path) { 534 squash_by($paths, 'path'); 535 } 536} 537 538sub dump_raw_output 539{ 540 my ($file) = @_; 541 542 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 543 while (<$fh>) { 544 if ($suppress_dmesg) { 545 if ("dmesg:" eq substr($_, 0, 6)) { 546 next; 547 } 548 } 549 print $_; 550 } 551 close $fh; 552} 553 554sub parse_raw_file 555{ 556 my ($file) = @_; 557 558 my $total = 0; # Total number of lines parsed. 559 my @dmesg; # dmesg output. 560 my %files; # Unique filenames containing leaks. 561 my %paths; # Unique paths containing leaks. 562 563 open (my $fh, '<', $file) or die "$0: $file: $!\n"; 564 while (my $line = <$fh>) { 565 $total++; 566 567 if ("dmesg:" eq substr($line, 0, 6)) { 568 push @dmesg, $line; 569 next; 570 } 571 572 cache_path(\%paths, $line); 573 cache_filename(\%files, $line); 574 } 575 576 return $total, \@dmesg, \%paths, \%files; 577} 578 579sub print_dmesg 580{ 581 my ($dmesg) = @_; 582 583 print "\ndmesg output:\n"; 584 585 if (@$dmesg == 0) { 586 print "<no results>\n"; 587 return; 588 } 589 590 foreach(@$dmesg) { 591 my $index = index($_, ': '); 592 $index += 2; # skid ': ' 593 print substr($_, $index); 594 } 595} 596 597sub squash_by 598{ 599 my ($ref, $desc) = @_; 600 601 print "\nResults squashed by $desc (excl dmesg). "; 602 print "Displaying [<number of results> <$desc>], <example result>\n"; 603 604 if (keys %$ref == 0) { 605 print "<no results>\n"; 606 return; 607 } 608 609 foreach(keys %$ref) { 610 my $lines = $ref->{$_}; 611 my $length = @$lines; 612 printf "[%d %s] %s", $length, $_, @$lines[0]; 613 } 614} 615 616sub cache_path 617{ 618 my ($paths, $line) = @_; 619 620 my $index = index($line, ': '); 621 my $path = substr($line, 0, $index); 622 623 $index += 2; # skip ': ' 624 add_to_cache($paths, $path, substr($line, $index)); 625} 626 627sub cache_filename 628{ 629 my ($files, $line) = @_; 630 631 my $index = index($line, ': '); 632 my $path = substr($line, 0, $index); 633 my $filename = basename($path); 634 635 $index += 2; # skip ': ' 636 add_to_cache($files, $filename, substr($line, $index)); 637} 638 639sub add_to_cache 640{ 641 my ($cache, $key, $value) = @_; 642 643 if (!$cache->{$key}) { 644 $cache->{$key} = (); 645 } 646 push @{$cache->{$key}}, $value; 647} 648