2017-11-06 08:19:27 +03:00
#!/usr/bin/env perl
2019-05-27 09:55:14 +03:00
# SPDX-License-Identifier: GPL-2.0-only
2017-11-06 08:19:27 +03:00
#
# (c) 2017 Tobin C. Harding <me@tobin.cc>
#
2018-01-29 07:00:16 +03:00
# leaking_addresses.pl: Scan the kernel for potential leaking addresses.
2017-11-06 08:19:27 +03:00
# - Scans dmesg output.
# - Walks directory tree and parses each file (for each directory in @DIRS).
#
# Use --debug to output path before parsing, this is useful to find files that
# cause the script to choke.
2018-02-27 07:02:57 +03:00
#
# When the system is idle it is likely that most files under /proc/PID will be
# identical for various processes. Scanning _all_ the PIDs under /proc is
# unnecessary and implies that we are thoroughly scanning /proc. This is _not_
# the case because there may be ways userspace can trigger creation of /proc
# files that leak addresses but were not present during a scan. For these two
# reasons we exclude all PID directories under /proc except '1/'
2017-11-06 08:19:27 +03:00
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 ) ;
2017-11-09 07:19:40 +03:00
use Config ;
2017-12-07 04:33:21 +03:00
use bigint qw/hex/ ;
2017-12-07 06:40:29 +03:00
use feature 'state' ;
2017-11-06 08:19:27 +03:00
my $ P = $ 0 ;
# Directories to scan.
my @ DIRS = ( '/proc' , '/sys' ) ;
2017-11-09 07:37:06 +03:00
# Timer for parsing each file, in seconds.
my $ TIMEOUT = 10 ;
2018-01-29 07:00:16 +03:00
# Kernel addresses vary by architecture. We can only auto-detect the following
# architectures (using `uname -m`). (flag --32-bit overrides auto-detection.)
my @ SUPPORTED_ARCHITECTURES = ( 'x86_64' , 'ppc64' , 'x86' ) ;
2017-11-09 07:19:40 +03:00
2017-11-06 08:19:27 +03:00
# Command line options.
my $ help = 0 ;
my $ debug = 0 ;
2017-11-09 07:07:15 +03:00
my $ raw = 0 ;
my $ output_raw = "" ; # Write raw results to file.
my $ input_raw = "" ; # Read raw results from file instead of scanning.
my $ suppress_dmesg = 0 ; # Don't show dmesg in output.
my $ squash_by_path = 0 ; # Summary report grouped by absolute path.
my $ squash_by_filename = 0 ; # Summary report grouped by filename.
2017-12-07 05:53:41 +03:00
my $ kernel_config_file = "" ; # Kernel configuration file.
2018-01-29 07:00:16 +03:00
my $ opt_32bit = 0 ; # Scan 32-bit kernel.
my $ page_offset_32bit = 0 ; # Page offset for 32-bit kernel.
2017-11-06 08:19:27 +03:00
2018-02-19 03:03:37 +03:00
# Skip these absolute paths.
my @ skip_abs = (
'/proc/kmsg' ,
'/proc/device-tree' ,
2018-02-27 06:14:24 +03:00
'/proc/1/syscall' ,
2018-02-19 03:03:37 +03:00
'/sys/firmware/devicetree' ,
2023-03-14 00:17:44 +03:00
'/sys/kernel/tracing/trace_pipe' ,
2018-02-19 03:03:37 +03:00
'/sys/kernel/debug/tracing/trace_pipe' ,
'/sys/kernel/security/apparmor/revision' ) ;
# Skip these under any subdirectory.
my @ skip_any = (
'pagemap' ,
'events' ,
'access' ,
'registers' ,
'snapshot_raw' ,
'trace_pipe_raw' ,
'ptmx' ,
'trace_pipe' ,
'fd' ,
'usbmon' ) ;
2017-11-06 08:19:27 +03:00
sub help
{
my ( $ exitcode ) = @ _ ;
print << "EOM" ;
2017-11-09 07:07:15 +03:00
2017-11-06 08:19:27 +03:00
Usage: $ P [ OPTIONS ]
Options:
2017-12-07 05:57:53 +03:00
- o , - - output - raw = <file> Save results for future processing .
- i , - - input - raw = <file> Read results from file instead of scanning .
- - raw Show raw results ( default ) .
- - suppress - dmesg Do not show dmesg results .
- - squash - by - path Show one result per unique path .
- - squash - by - filename Show one result per unique filename .
2017-12-07 05:53:41 +03:00
- - kernel - config - file = <file> Kernel configuration file ( e . g /boot/co nfig )
2018-01-29 07:00:16 +03:00
- - 32 - bit Scan 32 - bit kernel .
- - page - offset - 32 - bit = o Page offset ( for 32 - bit kernel 0xABCD1234 ) .
2017-12-07 05:57:53 +03:00
- d , - - debug Display debugging output .
2018-10-23 03:37:02 +03:00
- h , - - help Display this help and exit .
2017-11-09 07:07:15 +03:00
2018-01-29 07:00:16 +03:00
Scans the running kernel for potential leaking addresses .
2017-11-06 08:19:27 +03:00
EOM
exit ( $ exitcode ) ;
}
GetOptions (
'd|debug' = > \ $ debug ,
'h|help' = > \ $ help ,
2017-11-09 07:07:15 +03:00
'o|output-raw=s' = > \ $ output_raw ,
'i|input-raw=s' = > \ $ input_raw ,
'suppress-dmesg' = > \ $ suppress_dmesg ,
'squash-by-path' = > \ $ squash_by_path ,
'squash-by-filename' = > \ $ squash_by_filename ,
'raw' = > \ $ raw ,
2017-12-07 05:53:41 +03:00
'kernel-config-file=s' = > \ $ kernel_config_file ,
2018-01-29 07:00:16 +03:00
'32-bit' = > \ $ opt_32bit ,
'page-offset-32-bit=o' = > \ $ page_offset_32bit ,
2017-11-06 08:19:27 +03:00
) or help ( 1 ) ;
help ( 0 ) if ( $ help ) ;
2017-11-09 07:07:15 +03:00
if ( $ input_raw ) {
format_output ( $ input_raw ) ;
exit ( 0 ) ;
}
if ( ! $ input_raw and ( $ squash_by_path or $ squash_by_filename ) ) {
printf "\nSummary reporting only available with --input-raw=<file>\n" ;
printf "(First run scan with --output-raw=<file>.)\n" ;
exit ( 128 ) ;
}
2018-01-29 07:00:16 +03:00
if ( ! ( is_supported_architecture ( ) or $ opt_32bit or $ page_offset_32bit ) ) {
2017-11-09 07:19:40 +03:00
printf "\nScript does not support your architecture, sorry.\n" ;
printf "\nCurrently we support: \n\n" ;
foreach ( @ SUPPORTED_ARCHITECTURES ) {
printf "\t%s\n" , $ _ ;
}
2018-01-06 01:24:49 +03:00
printf ( "\n" ) ;
2017-11-09 07:19:40 +03:00
2018-01-29 07:00:16 +03:00
printf ( "If you are running a 32-bit architecture you may use:\n" ) ;
printf ( "\n\t--32-bit or --page-offset-32-bit=<page offset>\n\n" ) ;
2018-01-06 01:24:49 +03:00
my $ archname = `uname -m` ;
printf ( "Machine hardware name (`uname -m`): %s\n" , $ archname ) ;
2017-11-09 07:19:40 +03:00
exit ( 129 ) ;
}
2017-11-09 07:07:15 +03:00
if ( $ output_raw ) {
open my $ fh , '>' , $ output_raw or die "$0: $output_raw: $!\n" ;
select $ fh ;
}
2017-11-06 08:19:27 +03:00
parse_dmesg ( ) ;
walk ( @ DIRS ) ;
exit 0 ;
sub dprint
{
printf ( STDERR @ _ ) if $ debug ;
}
2017-11-09 07:19:40 +03:00
sub is_supported_architecture
{
2018-01-29 07:00:16 +03:00
return ( is_x86_64 ( ) or is_ppc64 ( ) or is_ix86_32 ( ) ) ;
}
sub is_32bit
{
# Allow --32-bit or --page-offset-32-bit to override
if ( $ opt_32bit or $ page_offset_32bit ) {
return 1 ;
}
return is_ix86_32 ( ) ;
}
sub is_ix86_32
{
2018-02-19 05:23:44 +03:00
state $ arch = `uname -m` ;
2018-01-29 07:00:16 +03:00
chomp $ arch ;
if ( $ arch =~ m/i[3456]86/ ) {
return 1 ;
}
return 0 ;
2017-11-09 07:19:40 +03:00
}
2018-01-29 06:33:49 +03:00
sub is_arch
2017-11-09 07:19:40 +03:00
{
2018-01-29 06:33:49 +03:00
my ( $ desc ) = @ _ ;
my $ arch = `uname -m` ;
chomp $ arch ;
if ( $ arch eq $ desc ) {
return 1 ;
}
return 0 ;
}
2017-11-09 07:19:40 +03:00
2018-01-29 06:33:49 +03:00
sub is_x86_64
{
2018-02-19 05:23:44 +03:00
state $ is = is_arch ( 'x86_64' ) ;
return $ is ;
2017-11-09 07:19:40 +03:00
}
sub is_ppc64
{
2018-02-19 05:23:44 +03:00
state $ is = is_arch ( 'ppc64' ) ;
return $ is ;
2017-11-09 07:19:40 +03:00
}
2017-12-07 05:53:41 +03:00
# Gets config option value from kernel config file.
# Returns "" on error or if config option not found.
sub get_kernel_config_option
{
my ( $ option ) = @ _ ;
my $ value = "" ;
my $ tmp_file = "" ;
my @ config_files ;
# Allow --kernel-config-file to override.
if ( $ kernel_config_file ne "" ) {
@ config_files = ( $ kernel_config_file ) ;
} elsif ( - R "/proc/config.gz" ) {
my $ tmp_file = "/tmp/tmpkconf" ;
if ( system ( "gunzip < /proc/config.gz > $tmp_file" ) ) {
2018-10-23 02:51:08 +03:00
dprint ( "system(gunzip < /proc/config.gz) failed\n" ) ;
2017-12-07 05:53:41 +03:00
return "" ;
} else {
@ config_files = ( $ tmp_file ) ;
}
} else {
my $ file = '/boot/config-' . `uname -r` ;
chomp $ file ;
@ config_files = ( $ file , '/boot/config' ) ;
}
foreach my $ file ( @ config_files ) {
2018-10-23 02:51:08 +03:00
dprint ( "parsing config file: $file\n" ) ;
2017-12-07 05:53:41 +03:00
$ value = option_from_file ( $ option , $ file ) ;
if ( $ value ne "" ) {
last ;
}
}
if ( $ tmp_file ne "" ) {
system ( "rm -f $tmp_file" ) ;
}
return $ value ;
}
# Parses $file and returns kernel configuration option value.
sub option_from_file
{
my ( $ option , $ file ) = @ _ ;
my $ str = "" ;
my $ val = "" ;
open ( my $ fh , "<" , $ file ) or return "" ;
while ( my $ line = <$fh> ) {
if ( $ line =~ /^$option/ ) {
( $ str , $ val ) = split /=/ , $ line ;
chomp $ val ;
last ;
}
}
close $ fh ;
return $ val ;
}
2017-11-06 08:19:27 +03:00
sub is_false_positive
{
2017-11-08 03:01:59 +03:00
my ( $ match ) = @ _ ;
2018-01-29 07:00:16 +03:00
if ( is_32bit ( ) ) {
return is_false_positive_32bit ( $ match ) ;
}
# 64 bit false positives.
2017-11-08 03:01:59 +03:00
if ( $ match =~ '\b(0x)?(f|F){16}\b' or
$ match =~ '\b(0x)?0{16}\b' ) {
return 1 ;
}
2017-11-06 08:19:27 +03:00
2017-12-07 04:33:21 +03:00
if ( is_x86_64 ( ) and is_in_vsyscall_memory_region ( $ match ) ) {
return 1 ;
2017-11-08 03:01:59 +03:00
}
2017-11-06 08:19:27 +03:00
2017-11-08 03:01:59 +03:00
return 0 ;
2017-11-06 08:19:27 +03:00
}
2018-01-29 07:00:16 +03:00
sub is_false_positive_32bit
{
my ( $ match ) = @ _ ;
state $ page_offset = get_page_offset ( ) ;
if ( $ match =~ '\b(0x)?(f|F){8}\b' ) {
return 1 ;
}
if ( hex ( $ match ) < $ page_offset ) {
return 1 ;
}
return 0 ;
}
# returns integer value
sub get_page_offset
{
my $ page_offset ;
my $ default_offset = 0xc0000000 ;
# Allow --page-offset-32bit to override.
if ( $ page_offset_32bit != 0 ) {
return $ page_offset_32bit ;
}
$ page_offset = get_kernel_config_option ( 'CONFIG_PAGE_OFFSET' ) ;
if ( ! $ page_offset ) {
return $ default_offset ;
}
return $ page_offset ;
}
2017-12-07 04:33:21 +03:00
sub is_in_vsyscall_memory_region
{
my ( $ match ) = @ _ ;
my $ hex = hex ( $ match ) ;
my $ region_min = hex ( "0xffffffffff600000" ) ;
my $ region_max = hex ( "0xffffffffff601000" ) ;
return ( $ hex >= $ region_min and $ hex <= $ region_max ) ;
}
2017-11-06 08:19:27 +03:00
# True if argument potentially contains a kernel address.
sub may_leak_address
{
2017-11-08 03:01:59 +03:00
my ( $ line ) = @ _ ;
2017-11-09 07:19:40 +03:00
my $ address_re ;
2017-11-06 08:19:27 +03:00
2017-11-08 03:01:59 +03:00
# Signal masks.
if ( $ line =~ '^SigBlk:' or
2017-11-14 01:25:11 +03:00
$ line =~ '^SigIgn:' or
2017-11-08 03:01:59 +03:00
$ line =~ '^SigCgt:' ) {
return 0 ;
}
2017-11-06 08:19:27 +03:00
2017-11-08 03:01:59 +03:00
if ( $ line =~ '\bKEY=[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' or
$ line =~ '\b[[:xdigit:]]{14} [[:xdigit:]]{16} [[:xdigit:]]{16}\b' ) {
2017-11-06 08:19:27 +03:00
return 0 ;
2017-11-08 03:01:59 +03:00
}
2017-11-06 08:19:27 +03:00
2017-12-07 06:40:29 +03:00
$ address_re = get_address_re ( ) ;
2018-03-02 00:42:59 +03:00
while ( $ line =~ /($address_re)/g ) {
2017-11-08 03:01:59 +03:00
if ( ! is_false_positive ( $ 1 ) ) {
return 1 ;
}
}
2017-11-06 08:19:27 +03:00
2017-11-08 03:01:59 +03:00
return 0 ;
2017-11-06 08:19:27 +03:00
}
2017-12-07 06:40:29 +03:00
sub get_address_re
{
2018-01-29 07:00:16 +03:00
if ( is_ppc64 ( ) ) {
2017-12-07 06:40:29 +03:00
return '\b(0x)?[89abcdef]00[[:xdigit:]]{13}\b' ;
2018-01-29 07:00:16 +03:00
} elsif ( is_32bit ( ) ) {
return '\b(0x)?[[:xdigit:]]{8}\b' ;
2017-12-07 06:40:29 +03:00
}
2018-01-29 07:00:16 +03:00
return get_x86_64_re ( ) ;
2017-12-07 06:40:29 +03:00
}
sub get_x86_64_re
{
# We handle page table levels but only if explicitly configured using
# CONFIG_PGTABLE_LEVELS. If config file parsing fails or config option
# is not found we default to using address regular expression suitable
# for 4 page table levels.
state $ ptl = get_kernel_config_option ( 'CONFIG_PGTABLE_LEVELS' ) ;
if ( $ ptl == 5 ) {
return '\b(0x)?ff[[:xdigit:]]{14}\b' ;
}
return '\b(0x)?ffff[[:xdigit:]]{12}\b' ;
}
2017-11-06 08:19:27 +03:00
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
{
2018-02-19 03:03:37 +03:00
my ( $ path ) = @ _ ;
2017-11-06 08:19:27 +03:00
2018-02-19 03:03:37 +03:00
foreach ( @ skip_abs ) {
2017-11-06 08:19:27 +03:00
return 1 if ( /^$path$/ ) ;
}
my ( $ filename , $ dirs , $ suffix ) = fileparse ( $ path ) ;
2018-02-19 03:03:37 +03:00
foreach ( @ skip_any ) {
2017-11-06 08:19:27 +03:00
return 1 if ( /^$filename$/ ) ;
}
return 0 ;
}
2017-11-09 07:37:06 +03:00
sub timed_parse_file
{
my ( $ file ) = @ _ ;
eval {
local $ SIG { ALRM } = sub { die "alarm\n" } ; # NB: \n required.
alarm $ TIMEOUT ;
parse_file ( $ file ) ;
alarm 0 ;
} ;
if ( $@ ) {
die unless $@ eq "alarm\n" ; # Propagate unexpected errors.
printf STDERR "timed out parsing: %s\n" , $ file ;
}
}
2017-11-06 08:19:27 +03:00
sub parse_file
{
my ( $ file ) = @ _ ;
if ( ! - R $ file ) {
return ;
}
2018-02-19 02:22:15 +03:00
if ( ! - T $ file ) {
return ;
}
2017-11-06 08:19:27 +03:00
open my $ fh , "<" , $ file or return ;
while ( <$fh> ) {
2021-09-30 01:02:18 +03:00
chomp ;
2017-11-06 08:19:27 +03:00
if ( may_leak_address ( $ _ ) ) {
2021-09-30 01:02:18 +03:00
printf ( "$file: $_\n" ) ;
2017-11-06 08:19:27 +03:00
}
}
close $ fh ;
}
2018-03-02 00:49:55 +03:00
# Checks if the actual path name is leaking a kernel address.
sub check_path_for_leaks
{
my ( $ path ) = @ _ ;
if ( may_leak_address ( $ path ) ) {
printf ( "Path name may contain address: $path\n" ) ;
}
}
2017-11-06 08:19:27 +03:00
# Recursively walk directory tree.
sub walk
{
my @ dirs = @ _ ;
while ( my $ pwd = shift @ dirs ) {
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 ) ;
2018-02-27 07:02:57 +03:00
# skip /proc/PID except /proc/1
next if ( ( $ path =~ /^\/proc\/[0-9]+$/ ) &&
( $ path !~ /^\/proc\/1$/ ) ) ;
2018-02-19 03:03:37 +03:00
next if ( skip ( $ path ) ) ;
2018-03-02 00:49:55 +03:00
check_path_for_leaks ( $ path ) ;
2017-11-06 08:19:27 +03:00
if ( - d $ path ) {
push @ dirs , $ path ;
2018-02-19 03:03:37 +03:00
next ;
2017-11-06 08:19:27 +03:00
}
2018-02-19 03:03:37 +03:00
2018-10-23 02:51:08 +03:00
dprint ( "parsing: $path\n" ) ;
2018-02-19 03:03:37 +03:00
timed_parse_file ( $ path ) ;
2017-11-06 08:19:27 +03:00
}
}
}
2017-11-09 07:07:15 +03:00
sub format_output
{
my ( $ file ) = @ _ ;
# Default is to show raw results.
if ( $ raw or ( ! $ squash_by_path and ! $ squash_by_filename ) ) {
dump_raw_output ( $ file ) ;
return ;
}
my ( $ total , $ dmesg , $ paths , $ files ) = parse_raw_file ( $ file ) ;
printf "\nTotal number of results from scan (incl dmesg): %d\n" , $ total ;
if ( ! $ suppress_dmesg ) {
print_dmesg ( $ dmesg ) ;
}
if ( $ squash_by_filename ) {
squash_by ( $ files , 'filename' ) ;
}
if ( $ squash_by_path ) {
squash_by ( $ paths , 'path' ) ;
}
}
sub dump_raw_output
{
my ( $ file ) = @ _ ;
open ( my $ fh , '<' , $ file ) or die "$0: $file: $!\n" ;
while ( <$fh> ) {
if ( $ suppress_dmesg ) {
if ( "dmesg:" eq substr ( $ _ , 0 , 6 ) ) {
next ;
}
}
print $ _ ;
}
close $ fh ;
}
sub parse_raw_file
{
my ( $ file ) = @ _ ;
my $ total = 0 ; # Total number of lines parsed.
my @ dmesg ; # dmesg output.
my % files ; # Unique filenames containing leaks.
my % paths ; # Unique paths containing leaks.
open ( my $ fh , '<' , $ file ) or die "$0: $file: $!\n" ;
while ( my $ line = <$fh> ) {
$ total + + ;
if ( "dmesg:" eq substr ( $ line , 0 , 6 ) ) {
push @ dmesg , $ line ;
next ;
}
cache_path ( \ % paths , $ line ) ;
cache_filename ( \ % files , $ line ) ;
}
return $ total , \ @ dmesg , \ % paths , \ % files ;
}
sub print_dmesg
{
my ( $ dmesg ) = @ _ ;
print "\ndmesg output:\n" ;
if ( @$ dmesg == 0 ) {
print "<no results>\n" ;
return ;
}
foreach ( @$ dmesg ) {
my $ index = index ( $ _ , ': ' ) ;
$ index += 2 ; # skid ': '
print substr ( $ _ , $ index ) ;
}
}
sub squash_by
{
my ( $ ref , $ desc ) = @ _ ;
print "\nResults squashed by $desc (excl dmesg). " ;
print "Displaying [<number of results> <$desc>], <example result>\n" ;
if ( keys %$ ref == 0 ) {
print "<no results>\n" ;
return ;
}
foreach ( keys %$ ref ) {
my $ lines = $ ref - > { $ _ } ;
my $ length = @$ lines ;
printf "[%d %s] %s" , $ length , $ _ , @$ lines [ 0 ] ;
}
}
sub cache_path
{
my ( $ paths , $ line ) = @ _ ;
my $ index = index ( $ line , ': ' ) ;
my $ path = substr ( $ line , 0 , $ index ) ;
$ index += 2 ; # skip ': '
add_to_cache ( $ paths , $ path , substr ( $ line , $ index ) ) ;
}
sub cache_filename
{
my ( $ files , $ line ) = @ _ ;
my $ index = index ( $ line , ': ' ) ;
my $ path = substr ( $ line , 0 , $ index ) ;
my $ filename = basename ( $ path ) ;
$ index += 2 ; # skip ': '
add_to_cache ( $ files , $ filename , substr ( $ line , $ index ) ) ;
}
sub add_to_cache
{
my ( $ cache , $ key , $ value ) = @ _ ;
if ( ! $ cache - > { $ key } ) {
$ cache - > { $ key } = ( ) ;
}
push @ { $ cache - > { $ key } } , $ value ;
}