linux/scripts/leaking_addresses.pl
<<
>>
Prefs
   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