[test suite] AFL fuzzing README update
[fwknop.git] / extras / spa-entropy / spa-entropy.pl
1 #!/usr/bin/perl -w
2 #
3 # File: spa-entropy.pl
4 #
5 # Purpose: To measure cross-packet SPA entropy on a byte by byte slice basis
6 #          and produce gunplot graphs.  This is useful to measure SPA packet
7 #          randomness after encryption and verify that it high as one would
8 #          expect.
9 #
10 # Author: Michael Rash <mbr@cipherdyne.org>
11 #
12 # License: GPL v2
13 #
14
15 use MIME::Base64;
16 use IPC::Open2;
17 use Getopt::Long 'GetOptions';
18 use strict;
19
20 my $use_ent = 1;
21 my $base64_decode = 1;
22 my $packets = 0;
23 my $prefix = 'entropy';
24 my $file_to_measure = '';
25 my $run_fwknop_client = 0;
26 my $min_len = 0;
27 my $lib_dir = '../../lib/.libs';
28 my $fwknop_client_path = '../../client/.libs/fwknop';
29 my $enc_mode = 'cbc';
30 my $hmac_type = 0;
31 my $hmac_key_file = '../../test/conf/fwknoprc_default_hmac_base64_key';
32 my $enable_fwknop_client_gpg = 0;
33 my $gpg_recipient = '361BBAD4';
34 my $gpg_signer    = '6A3FAD56';
35 my $gpg_home_dir  = '../../test/conf/client-gpg-no-pw';
36 my $spa_key_file = '../../test/local_spa.key';
37 my $help = 0;
38
39 my $use_openssl = 0;
40 my $openssl_salt = '0000000000000000';
41 my $openssl_mode = 'aes-256-cbc';
42
43 my %min_max_entropy = (
44     'min' => {
45         'val' => -1,
46         'pos' => 0,
47     },
48     'max' => {
49         'val' => -1,
50         'pos' => 0,
51     }
52 );
53
54 my @encrypted_data = ();
55 my @plaintext_data = ();
56 my @cross_pkt_data = ();
57
58 Getopt::Long::Configure('no_ignore_case');
59 die "[*] See '$0 -h' for usage information" unless (GetOptions(
60     'file-to-measure=s' => \$file_to_measure,
61     'base64-decode'     => \$base64_decode,
62     'count=i'           => \$packets,
63     'prefix=s'          => \$prefix,
64     'run-fwknop-client' => \$run_fwknop_client,
65     'enc-mode=s'        => \$enc_mode,
66     'gpg-mode'          => \$enable_fwknop_client_gpg,
67     'gpg-recip=s'       => \$gpg_recipient,
68     'gpg-signer=s'      => \$gpg_signer,
69     'gpg-home=s'        => \$gpg_home_dir,
70     'hmac-mode'         => \$hmac_type,
71     'lib-dir=s'         => \$lib_dir,
72     'Client-path=s'     => \$fwknop_client_path,
73     'use-openssl'       => \$use_openssl,
74     'openssl-salt=s'    => \$openssl_salt,
75     'openssl-mode=s'    => \$openssl_mode,
76     'help'              => \$help,
77 ));
78 &usage() if $help;
79
80 die "[*] Must execute --run-fwknop-client in --use-openssl mode"
81     if $use_openssl and not $run_fwknop_client;
82
83 &run_fwknop_client() if $run_fwknop_client;
84
85 &read_data();
86
87 &get_min_len();
88
89 &build_data_slices();
90
91 open F, "> $prefix.dat" or die $!;
92 my $pos = 0;
93 for my $str (@cross_pkt_data) {
94
95     my $entropy = &get_entropy($str);
96
97 #    print F "$pos $entropy\n";
98     print F "$pos $entropy   ### " . &hex_dump($str) . "\n";
99
100     if ($min_max_entropy{'min'}{'val'} == -1
101             and $min_max_entropy{'max'}{'val'} == -1) {
102         $min_max_entropy{'min'}{'val'} = $entropy;
103         $min_max_entropy{'min'}{'pos'} = $pos;
104         $min_max_entropy{'max'}{'val'} = $entropy;
105         $min_max_entropy{'max'}{'pos'} = $pos;
106     } else {
107         if ($entropy < $min_max_entropy{'min'}{'val'}) {
108             $min_max_entropy{'min'}{'val'} = $entropy;
109             $min_max_entropy{'min'}{'pos'} = $pos;
110         }
111         if ($entropy > $min_max_entropy{'max'}{'val'}) {
112             $min_max_entropy{'max'}{'val'} = $entropy;
113             $min_max_entropy{'max'}{'pos'} = $pos;
114         }
115     }
116     $pos++;
117 }
118 close F;
119
120 my $min = sprintf "%.2f", $min_max_entropy{'min'}{'val'};
121 my $max = sprintf "%.2f", $min_max_entropy{'max'}{'val'};
122
123 print "[+] Min entropy: $min at byte: $min_max_entropy{'min'}{'pos'}\n";
124 print "[+] Max entropy: $max at byte: $min_max_entropy{'max'}{'pos'}\n";
125
126 &run_gnuplot();
127
128 exit 0;
129
130 sub read_data() {
131
132     if ($use_openssl) {
133
134         ### we've already gotten plaintext information from the fwknop client,
135         ### so encrypt this data with openssl and use it to re-write the
136         ### $file_to_measure
137         unlink $file_to_measure if -e $file_to_measure;
138
139         my @openssl_encrypted_data = ();
140
141         ### encrypt the plaintext and use it to re-write the -f file
142         for my $line (@plaintext_data) {
143
144             my $ptext_file = 'ptext.tmp';
145             my $enc_file   = 'ptext.enc';
146
147             open F, "> $ptext_file" or die $!;
148             print F $line;
149             close F;
150
151             unlink $enc_file if -e $enc_file;
152
153             system "openssl enc -$openssl_mode -a -S $openssl_salt " .
154                 "-in ptext.tmp -out ptext.enc -k fwknoptest000000";
155
156             my $base64_enc_data = '';
157             open F, "< $enc_file" or die $!;
158             while (<F>) {
159                 chomp;
160                 $base64_enc_data .= $_;
161             }
162             close F;
163
164             push @openssl_encrypted_data, $base64_enc_data;
165
166         }
167
168         open F, "> $file_to_measure" or die $!;
169         for my $line (@openssl_encrypted_data) {
170             print F $line, "\n";
171         }
172         close F;
173     }
174
175     my $fh = *STDIN;
176     if ($file_to_measure) {
177         open IN, "< $file_to_measure" or die "[*] Could not open $file_to_measure: $!";
178         $fh = *IN;
179     }
180
181     my $l_ctr = 0;
182     while (<$fh>) {
183         next unless $_ =~ /\S/;
184         chomp;
185
186         if ($base64_decode) {
187             if (&is_base64($_)) {
188                 my $base64_str = $_;
189
190                 if ($enable_fwknop_client_gpg) {
191                     unless ($base64_str =~ /^hQ/) {
192                         $base64_str = 'hQ' . $base64_str;
193                     }
194                 } else {
195                     ### base64-encoded "Salted__" prefix
196                     unless ($base64_str =~ /^U2FsdGVkX1/) {
197                         $base64_str = 'U2FsdGVkX1' . $base64_str;
198                     }
199                 }
200
201                 my ($equals_rv, $equals_padding) = &base64_equals_padding($base64_str);
202                 if ($equals_padding) {
203                     $base64_str .= $equals_padding;
204                 }
205                 my $str = decode_base64($base64_str);
206
207                 if ($enable_fwknop_client_gpg) {
208                     $str =~ s/^\x85\x02//;
209                 } else {
210                     $str =~ s/^Salted__//;
211                 }
212                 push @encrypted_data, $str;
213             } else {
214                 push @encrypted_data, $_;
215             }
216         } else {
217             push @encrypted_data, $_;
218         }
219
220         $l_ctr++;
221         if ($packets > 0) {
222             last if $l_ctr == $packets;
223         }
224     }
225
226     ### hex dump encrypted data
227     open HEX, "> hex_dump.data" or die $!;
228     for my $line (@encrypted_data) {
229         print HEX &hex_dump($line), "\n";
230     }
231     close HEX;
232
233     print "[+] Read in $l_ctr SPA packets...\n";
234     return;
235 }
236
237 sub run_fwknop_client() {
238     die "[*] Must set packets file with -f <file>" unless $file_to_measure;
239     die "[*] Must set packet count with -c <count>" unless $packets;
240
241     if (-e $file_to_measure) {
242         unlink $file_to_measure or die $!;
243     }
244
245     my $cmd = "LD_LIBRARY_PATH=$lib_dir $fwknop_client_path -A tcp/22 " .
246         "-a 127.0.0.2 -D 127.0.0.1 -B $file_to_measure -b -v --test";
247
248     if ($hmac_type) {
249         $cmd .= " --rc-file $hmac_key_file";
250     } else {
251         $cmd .= " --get-key $spa_key_file";
252     }
253
254     if ($enable_fwknop_client_gpg) {
255         $cmd .= " --gpg-recipient-key $gpg_recipient " .
256             "--gpg-signer-key $gpg_signer " .
257             "--gpg-home-dir $gpg_home_dir";
258     } else {
259         $cmd .= " -M $enc_mode";
260     }
261     $cmd .= " 2> /dev/null";
262
263     print "[+] Running fwknop client via the following command:\n\n$cmd\n\n";
264
265     for (my $i=0; $i < $packets; $i++) {
266         open C, "$cmd |" or die $!;
267         while (<C>) {
268             if (/Plaintext\:\s+(\S+)/) {
269                 push @plaintext_data, $1;
270                 last;
271             }
272         }
273         close C;
274     }
275
276     return;
277 }
278
279 sub get_min_len() {
280
281     ### calculate minimum length
282     for my $line (@encrypted_data) {
283         chomp $line;
284         next unless $line =~ /\S/;
285         my $len = length($line);
286         if ($min_len == 0) {
287             $min_len = $len;
288         } else {
289             if ($len < $min_len) {
290                 $min_len = $len;
291             }
292         }
293     }
294     return;
295 }
296
297 sub build_data_slices() {
298     for my $line (@encrypted_data) {
299         my @chars = split //, $line;
300         my $c_ctr = 0;
301         for my $char (@chars) {
302             $cross_pkt_data[$c_ctr] .= $char;
303             last if $c_ctr == $min_len;
304             $c_ctr++;
305         }
306     }
307     return;
308 }
309
310 sub run_gnuplot() {
311     open F, "> $prefix.gnu" or die $!;
312
313     my $enc_str = $enc_mode;
314     $enc_str = 'gpg' if $enable_fwknop_client_gpg;
315
316     my $yrange = '[0:9]';
317     print F <<_GNUPLOT_;
318 set title "SPA slice entropy (encryption mode: $enc_str)"
319 set terminal gif nocrop enhanced
320 set output "$prefix.gif"
321 set grid
322 set yrange $yrange
323 plot '$prefix.dat' using 1:2 with lines title 'min: $min \\@ byte: $min_max_entropy{'min'}{'pos'}, max: $max \\@ byte: $min_max_entropy{'max'}{'pos'}'
324 _GNUPLOT_
325     close F;
326
327     print "[+] Creating $prefix.gif gnuplot graph...\n\n";
328     system "gnuplot $prefix.gnu";
329
330     return;
331 }
332
333 sub get_entropy() {
334     my $data = shift;
335
336     my $entropy = '';
337
338     my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'ent');
339
340     print CHLD_IN $data;
341     close CHLD_IN;
342
343     while (<CHLD_OUT>) {
344         ### Entropy = 5.637677 bits per byte.
345         if (/Entropy\s=\s(\d\S+)/) {
346             $entropy = $1;
347             last;
348         }
349     }
350
351     close CHLD_OUT;
352
353     waitpid $pid, 0;
354     my $child_exit_status = $? >> 8;
355
356     return $entropy;
357 }
358
359 sub base64_equals_padding() {
360     my $msg = shift;
361     my $padding = '';
362
363     return 1, $padding if $msg =~ /=$/;
364
365     my $remainder = 4 - length($msg) % 4;
366
367     if ($remainder == 3) {
368         ### not possible for valid base64 data - should only have
369         ### pad with one or two '=' chars
370         return 0, $padding;
371     }
372
373     unless ($remainder == 4) {
374         $padding .= '='x$remainder;
375     }
376     return 1, $padding;
377 }
378
379 sub hex_dump() {
380     my $data = shift;
381
382     my @chars = split //, $data;
383     my $ctr = 0;
384
385     my $hex_part   = '';
386     my $ascii_part = '';
387
388     for my $char (@chars) {
389
390         $hex_part .= sprintf "%.2x", ord($char);
391
392         if ($char =~ /[^\x20-\x7e]/) {
393             $ascii_part .= '.';
394         } else {
395             $ascii_part .= $char;
396         }
397         $ctr++;
398     }
399     return "$hex_part $ascii_part";
400 #    return "$ascii_part";
401 }
402
403 sub is_base64() {
404     my $data = shift;
405
406     ### check to make sure the packet data only contains base64 encoded
407     ### characters per RFC 3548:   0-9, A-Z, a-z, +, /, =
408     if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) {
409         return 0;
410     }
411     if ($data =~ /=[^=]/) {
412         return 0;
413     }
414     return 1;
415 }
416
417 sub usage() {
418     print "$0 [options]\n";
419     exit 0;
420 }