xref: /linux/scripts/leaking_addresses.pl (revision 7832681b365f220151d1c33cc1a8891f10ecdb6f)
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