File: hash_attack.pm

package info (click to toggle)
libapache2-mod-perl2 2.0.13-2
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 12,016 kB
  • sloc: perl: 97,771; ansic: 14,493; makefile: 51; sh: 18
file content (187 lines) | stat: -rw-r--r-- 6,022 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# please insert nothing before this line: -*- mode: cperl; cperl-indent-level: 4; cperl-continued-statement-offset: 4; indent-tabs-mode: nil -*-
package TestPerl::hash_attack;

# if the rehashing of the keys in the stash happens due to the hash attack,
# mod_perl must not fail to find the previously cached stash entry (response
# and fixup handlers in this test). Moreover it must not fail to find
# that entry on the subsequent requests.
#
# the hash attack is detected when HV_MAX_LENGTH_BEFORE_REHASH keys find
# themselves in the same hash bucket on splitting (which happens when the
# number of keys crosses the threshold of a power of 2), in which case
# starting from 5.8.2 the hash will rehash all its keys using a random hash
# seed (PL_new_hash_seed, set in mod_perl or via PERL_HASH_SEED environment
# variable)
#
# Prior to the attack condition hashes use the PL_hash_seed, which is
# always 0.
#
# only in 5.8.1 hashes always use a non-zero PL_hash_seed (unless set
# to 0 via PERL_HASH_SEED environment variable or compiled without
# -DUSE_HASH_SEED or -DUSE_HASH_SEED_EXPLICIT

use strict;
use warnings FATAL => 'all';

use Apache::TestTrace;

use Apache2::Const -compile => 'OK';

use Math::BigInt;

use constant MASK_U32  => 2**32;
use constant HASH_SEED => 0; # 5.8.2: always zero before the rehashing
use constant THRESHOLD => 14; #define HV_MAX_LENGTH_BEFORE_(SPLIT|REHASH)
use constant START     => "a";

# create conditions which will trigger a rehash on the current stash
# (__PACKAGE__::). Relevant for perl 5.8.2 and higher.
sub init {
    my $r = shift;

    no strict 'refs';
    my @attack_keys = attack(\%{__PACKAGE__ . "::"}) if $] >= 5.008002;

    # define a new symbol (sub) after the attack has caused a re-hash
    # check that mod_perl finds that symbol (fixup2) in the stash
    no warnings 'redefine';
    eval qq[sub fixup2 { return Apache2::Const::OK; }];
    $r->push_handlers(PerlFixupHandler => \&fixup2);

    return Apache2::Const::DECLINED;
}

sub fixup { return Apache2::Const::OK; }

sub handler {
    my $r = shift;
    $r->print("ok");
    return Apache2::Const::OK;
}

sub buckets { scalar(%{$_[0]}) =~ m#/([0-9]+)\z# ? 0+$1 : 8 }

sub attack {
    my $stash = shift;

    #require Hash::Util; # avail since 5.8.0
    debug "starting attack (it may take a long time!)";

    my @keys;

    # the minimum of bits required to mount the attack on a hash
    my $min_bits = log(THRESHOLD)/log(2);

    # if the hash has already been populated with a significant amount
    # of entries the number of mask bits can be higher
    my $keys = scalar keys %$stash;
    my $bits = $keys ? log($keys)/log(2) : 0;
    $bits = $min_bits if $min_bits > $bits;

    $bits = ceil($bits);
    # need to add 3 bits to cover the internal split cases
    $bits += 3;
    my $mask = 2**$bits-1;
    debug "mask: $mask ($bits)";

    my $s = START;
    my $c = 0;
    # get 2 keys on top of the THRESHOLD
    my $h;
    while (@keys < THRESHOLD+2) {
        next if exists $stash->{$s};
        $h = hash($s);
        next unless ($h & $mask) == 0;
        $c++;
        $stash->{$s}++;
        debug sprintf "%2d: %5s, %08x %s", $c, $s, $h, scalar(%$stash);
        push @keys, $s;
        debug "The hash collision attack has been successful"
            if Internals::HvREHASH(%$stash);
    } continue {
        $s++;
    }

    # If the rehash hasn't been triggered yet, it's being delayed until the
    # next bucket split.  Add keys until a split occurs.
    unless (Internals::HvREHASH(%$stash)) {
        debug "Will add padding keys until hash split";
        my $old_buckets = buckets($stash);
        while (buckets($stash) == $old_buckets) {
            next if exists $stash->{$s};
            $h = hash($s);
            $c++;
            $stash->{$s}++;
            debug sprintf "%2d: %5s, %08x %s", $c, $s, $h, scalar(%$stash);
            push @keys, $s;
            debug "The hash collision attack has been successful"
                if Internals::HvREHASH(%$stash);
            $s++;
        }
    }

    # this verifies that the attack was mounted successfully. If
    # HvREHASH is on it is. Otherwise the sequence wasn't successful.
    die "Failed to mount the hash collision attack"
        unless Internals::HvREHASH(%$stash);

    debug "ending attack";

    return @keys;
}

# least integer >= n
sub ceil {
    my $value = shift;
    return int($value) < $value ? int($value) + 1 : int($value);
}

# trying to provide the fastest equivalent of C macro's PERL_HASH in
# Perl - the main complication is that the C macro uses U32 integer
# (unsigned int), which we can't do it Perl (it can do I32, with 'use
# integer'). So we outsmart Perl and take modules 2*32 after each
# calculation, emulating overflows that happen in C.
sub hash {
    my $s = shift;
    my @c = split //, $s;
    my $u = HASH_SEED;
    for (@c) {
        # (A % M) + (B % M) == (A + B) % M
        # This works because '+' produces a NV, which is big enough to hold
        # the intermidiate result. We only need the % before any "^" and "&"
        # to get the result in the range for an I32.
        # and << doesn't work on NV, so using 1 << 10
        $u += ord;
        $u += $u * (1 << 10); $u %= MASK_U32;
        $u ^= $u >> 6;
    }
    $u += $u << 3;  $u %= MASK_U32;
    $u ^= $u >> 11; $u %= MASK_U32;
    $u += $u << 15; $u %= MASK_U32;
    $u;
}

# a bit slower but simpler version
sub hash_original {
    my $s = shift;
    my @c = split //, $s;
    my $u = HASH_SEED;
    for (@c) {
        $u += ord;      $u %= MASK_U32;
        $u += $u << 10; $u %= MASK_U32;
        $u ^= $u >> 6;  $u %= MASK_U32;
    }
    $u += $u << 3;  $u %= MASK_U32;
    $u ^= $u >> 11; $u %= MASK_U32;
    $u += $u << 15; $u %= MASK_U32;
    $u;
}

1;

__END__
PerlModule       TestPerl::hash_attack
PerlInitHandler  TestPerl::hash_attack::init
# call twice to verify an access to the same hash value after the rehash
PerlFixupHandler TestPerl::hash_attack::fixup TestPerl::hash_attack::fixup