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.
9 # Author: Michael Rash <mbr@cipherdyne.org>
16 use Getopt::Long 'GetOptions';
20 my $base64_decode = 1;
22 my $prefix = 'entropy';
23 my $file_to_measure = '';
24 my $run_fwknop_client = 0;
26 my $lib_dir = '../../lib/.libs';
27 my $fwknop_client_path = '../../client/.libs/fwknop';
29 my $enable_fwknop_client_gpg = 0;
30 my $spa_key_file = '../../test/local_spa.key';
34 my $openssl_salt = '0000000000000000';
35 my $openssl_mode = 'aes-256-cbc';
37 my %min_max_entropy = (
48 my @encrypted_data = ();
49 my @plaintext_data = ();
50 my @cross_pkt_data = ();
52 Getopt::Long::Configure('no_ignore_case');
53 die "[*] See '$0 -h' for usage information" unless (GetOptions(
54 'file-to-measure=s' => \$file_to_measure,
55 'base64-decode' => \$base64_decode,
56 'count=i' => \$packets,
57 'prefix=s' => \$prefix,
58 'run-fwknop-client' => \$run_fwknop_client,
59 'enc-mode=s' => \$enc_mode,
60 'gpg' => \$enable_fwknop_client_gpg,
61 'lib-dir=s' => \$lib_dir,
62 'Client-path=s' => \$fwknop_client_path,
63 'use-openssl' => \$use_openssl,
64 'openssl-salt=s' => \$openssl_salt,
65 'openssl-mode=s' => \$openssl_mode,
70 die "[*] Must execute --run-fwknop-client in --use-openssl mode"
71 if $use_openssl and not $run_fwknop_client;
73 &run_fwknop_client() if $run_fwknop_client;
81 open F, "> $prefix.dat" or die $!;
83 for my $str (@cross_pkt_data) {
85 my $entropy = &get_entropy($str);
87 # print F "$pos $entropy\n";
88 print F "$pos $entropy ### " . &hex_dump($str) . "\n";
90 if ($min_max_entropy{'min'}{'val'} == -1
91 and $min_max_entropy{'max'}{'val'} == -1) {
92 $min_max_entropy{'min'}{'val'} = $entropy;
93 $min_max_entropy{'min'}{'pos'} = $pos;
94 $min_max_entropy{'max'}{'val'} = $entropy;
95 $min_max_entropy{'max'}{'pos'} = $pos;
97 if ($entropy < $min_max_entropy{'min'}{'val'}) {
98 $min_max_entropy{'min'}{'val'} = $entropy;
99 $min_max_entropy{'min'}{'pos'} = $pos;
101 if ($entropy > $min_max_entropy{'max'}{'val'}) {
102 $min_max_entropy{'max'}{'val'} = $entropy;
103 $min_max_entropy{'max'}{'pos'} = $pos;
110 my $min = sprintf "%.2f", $min_max_entropy{'min'}{'val'};
111 my $max = sprintf "%.2f", $min_max_entropy{'max'}{'val'};
113 print "[+] Min entropy: $min at byte: $min_max_entropy{'min'}{'pos'}\n";
114 print "[+] Max entropy: $max at byte: $min_max_entropy{'max'}{'pos'}\n";
124 ### we've already gotten plaintext information from the fwknop client,
125 ### so encrypt this data with openssl and use it to re-write the
127 unlink $file_to_measure if -e $file_to_measure;
129 my @openssl_encrypted_data = ();
131 ### encrypt the plaintext and use it to re-write the -f file
132 for my $line (@plaintext_data) {
134 my $ptext_file = 'ptext.tmp';
135 my $enc_file = 'ptext.enc';
137 open F, "> $ptext_file" or die $!;
141 unlink $enc_file if -e $enc_file;
143 system "openssl enc -$openssl_mode -a -S $openssl_salt " .
144 "-in ptext.tmp -out ptext.enc -k fwknoptest000000";
146 my $base64_enc_data = '';
147 open F, "< $enc_file" or die $!;
150 $base64_enc_data .= $_;
154 push @openssl_encrypted_data, $base64_enc_data;
158 open F, "> $file_to_measure" or die $!;
159 for my $line (@openssl_encrypted_data) {
166 if ($file_to_measure) {
167 open IN, "< $file_to_measure" or die "[*] Could not open $file_to_measure: $!";
173 next unless $_ =~ /\S/;
176 if ($base64_decode) {
177 if (&is_base64($_)) {
180 if ($enable_fwknop_client_gpg) {
181 unless ($base64_str =~ /^hQ/) {
182 $base64_str = 'hQ' . $base64_str;
185 ### base64-encoded "Salted__" prefix
186 unless ($base64_str =~ /^U2FsdGVkX1/) {
187 $base64_str = 'U2FsdGVkX1' . $base64_str;
191 my ($equals_rv, $equals_padding) = &base64_equals_padding($base64_str);
192 if ($equals_padding) {
193 $base64_str .= $equals_padding;
195 my $str = decode_base64($base64_str);
197 if ($enable_fwknop_client_gpg) {
198 $str =~ s/^\x85\x02//;
200 $str =~ s/^Salted__//;
202 push @encrypted_data, $str;
204 push @encrypted_data, $_;
207 push @encrypted_data, $_;
212 last if $l_ctr == $packets;
216 ### hex dump encrypted data
217 open HEX, "> hex_dump.data" or die $!;
218 for my $line (@encrypted_data) {
219 print HEX &hex_dump($line), "\n";
223 print "[+] Read in $l_ctr SPA packets...\n";
227 sub run_fwknop_client() {
228 die "[*] Must set packets file with -f <file>" unless $file_to_measure;
229 die "[*] Must set packet count with -c <count>" unless $packets;
231 if (-e $file_to_measure) {
232 unlink $file_to_measure or die $!;
235 my $cmd = "LD_LIBRARY_PATH=$lib_dir $fwknop_client_path -A tcp/22 " .
236 "-a 127.0.0.2 -D 127.0.0.1 --get-key $spa_key_file " .
237 "-B $file_to_measure -b -v --test";
239 if ($enable_fwknop_client_gpg) {
240 $cmd .= ' --gpg-recipient-key 361BBAD4 --gpg-signer-key 6A3FAD56 ' .
241 '--gpg-home-dir ../../test/conf/client-gpg';
243 $cmd .= " -M $enc_mode";
245 $cmd .= " 2> /dev/null";
247 print "[+] Running fwknop client via the following command:\n\n$cmd\n\n";
249 for (my $i=0; $i < $packets; $i++) {
250 open C, "$cmd |" or die $!;
252 if (/Plaintext\:\s+(\S+)/) {
253 push @plaintext_data, $1;
265 ### calculate minimum length
266 for my $line (@encrypted_data) {
268 next unless $line =~ /\S/;
269 my $len = length($line);
273 if ($len < $min_len) {
281 sub build_data_slices() {
282 for my $line (@encrypted_data) {
283 my @chars = split //, $line;
285 for my $char (@chars) {
286 $cross_pkt_data[$c_ctr] .= $char;
287 last if $c_ctr == $min_len;
295 open F, "> $prefix.gnu" or die $!;
297 my $enc_str = $enc_mode;
298 $enc_str = 'gpg' if $enable_fwknop_client_gpg;
300 my $yrange = '[0:9]';
302 set title "SPA slice entropy (encryption mode: $enc_str)"
303 set terminal gif nocrop enhanced
304 set output "$prefix.gif"
307 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'}'
311 print "[+] Creating $prefix.gif gnuplot graph...\n\n";
312 system "gnuplot $prefix.gnu";
322 my $pid = open2(\*CHLD_OUT, \*CHLD_IN, 'ent');
328 ### Entropy = 5.637677 bits per byte.
329 if (/Entropy\s=\s(\d\S+)/) {
338 my $child_exit_status = $? >> 8;
343 sub base64_equals_padding() {
347 return 1, $padding if $msg =~ /=$/;
349 my $remainder = 4 - length($msg) % 4;
351 if ($remainder == 3) {
352 ### not possible for valid base64 data - should only have
353 ### pad with one or two '=' chars
357 unless ($remainder == 4) {
358 $padding .= '='x$remainder;
366 my @chars = split //, $data;
372 for my $char (@chars) {
374 $hex_part .= sprintf "%.2x", ord($char);
376 if ($char =~ /[^\x20-\x7e]/) {
379 $ascii_part .= $char;
383 return "$hex_part $ascii_part";
384 # return "$ascii_part";
390 ### check to make sure the packet data only contains base64 encoded
391 ### characters per RFC 3548: 0-9, A-Z, a-z, +, /, =
392 if ($data =~ /[^\x30-\x39\x41-\x5a\x61-\x7a\x2b\x2f\x3d]/) {
395 if ($data =~ /=[^=]/) {
402 print "$0 [options]\n";