/
hashextension.py
executable file
·272 lines (243 loc) · 10 KB
/
hashextension.py
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#!/usr/bin/python3
# Calculate a hash extension from a given hash state.
#
# Copyright 2012 Thomas Skora <thomas@skora.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import re
import struct
import binascii
import urllib.parse
import base64
debug=False
def debugprint(msg):
if debug:
print(msg)
class sha1:
# the SHA1 hash algorithm according to RFC 3174
blocksize = 64
# ch. 6.1
H = [bytes.fromhex('67452301')] + [bytes.fromhex('EFCDAB89')] + [bytes.fromhex('98BADCFE')] + [bytes.fromhex('10325476')] + [bytes.fromhex('C3D2E1F0')]
K = [0x5A827999] * 20 + [0x6ED9EBA1] * 20 + [0x8F1BBCDC] * 20 + [0xCA62C1D6] * 20
# initialize length and iv
def __init__(self, iv=None, length=0):
if iv:
if not re.match('^[0-9a-f]{40}$', iv, re.I):
raise ValueError("IV for SHA1 must be a hex string of length 40")
for i in range(0,5):
o = i * 8
self.H[i] = bytes.fromhex(iv[o:o+8])
self.l = length
debugprint("sha1.__init__: object initialized with l=" + str(self.l) + " iv=" + str(binascii.hexlify(b''.join(self.H)), "ascii"))
# adds padding to message. if l is given it is used, else the l of the object instance is used.
# ul is the number of unknown bytes which prepend the current block. It is used to shorten the padding
# appropriately.
def pad(self, msg, ul=0, l=None):
if l == None:
l = self.l
debugprint("sha1.pad: length is " + str(self.l) + " msg=" + repr(msg) + " ul=" + str(ul))
if (len(msg) + ul) % 64 > 55: # padding doesn't fits in remaining block, add pad-only block
msg = msg + b'\x80' + b'\x00' * (64 - 1 - ((len(msg) + ul) % 64) + 56) + struct.pack(">Q", self.l * 8)
else: # padding fits in one block
msg = msg + b'\x80' + b'\x00' * (64 - 8 - 1 - ((len(msg) + ul) % 64)) + struct.pack(">Q", self.l * 8)
debugprint("sha1.pad: msg after padding=" + repr(msg) + " length=" + str(len(msg)))
return msg
# ch. 5
def f(self, t,B,C,D):
if t in range(0,20):
return (B & C) | ((~B) & D)
elif t in range(20,40):
return B ^ C ^ D
elif t in range(40,60):
return (B & C) | (B & D) | (C & D)
elif t in range(60,80):
return B ^ C ^ D
else:
raise IndexError
# ch. 3, circular left shift
def S(self, n, x):
return ((x << n) | (x >> (32 - n))) & 0xffffffff
# ch 6.1
def process_block(self, m):
# a. (converts chunks into int's since arithmetic operations follow)
w = [int.from_bytes(m[i:i+4], 'big') for i in range(0, 64, 4)] + [0] * 64
debugprint("sha1.process_block: a. w=" + repr(w))
# b.
for t in range(16, 80):
w[t] = self.S(1, w[t-3] ^ w[t-8] ^ w[t-14] ^ w[t-16])
debugprint("sha1.process_block: b. w=" + repr(w))
# c.
(a,b,c,d,e) = [int.from_bytes(x, 'big') for x in self.H]
debugprint("sha1.process_block: c. a=" + str(a) + " b=" + str(b) + " c=" + str(c) + " d=" + str(d) + " e=" + str(e))
# d.
for t in range(0, 80):
temp = (self.S(5, a) + self.f(t, b, c, d) + e + w[t] + self.K[t]) % 2**32
e = d
d = c
c = self.S(30, b)
b = a
a = temp
debugprint("sha1.process_block: d. t=" + str(t) + " a=" + str(a) + " b=" + str(b) + " c=" + str(c) + " d=" + str(d) + " e=" + str(e))
# e.
self.H[0] = ((int.from_bytes(self.H[0], 'big') + a) % 2**32).to_bytes(4, 'big')
self.H[1] = ((int.from_bytes(self.H[1], 'big') + b) % 2**32).to_bytes(4, 'big')
self.H[2] = ((int.from_bytes(self.H[2], 'big') + c) % 2**32).to_bytes(4, 'big')
self.H[3] = ((int.from_bytes(self.H[3], 'big') + d) % 2**32).to_bytes(4, 'big')
self.H[4] = ((int.from_bytes(self.H[4], 'big') + e) % 2**32).to_bytes(4, 'big')
debugprint("sha1.process_block: e. H=" + str(binascii.hexlify(b''.join(self.H)), "ascii"))
# continue calculation of given state
def add(self, m):
self.l += len(m)
m = self.pad(m)
assert len(m) % 64 == 0
for i in range(0, len(m), 64):
block = m[i:i+64]
debugprint("sha1.add: Processing block: " + repr(block) + " length: " + str(len(block)))
self.process_block(block)
# return binary representation of hash
def digest(self):
return b''.join(self.H)
def hexdigest(self):
return str(binascii.hexlify(self.digest()), "ascii")
##### main functions #####
def calchash(hashclass, hashstate, length, msg):
h = hashclass(hashstate, length)
h.add(msg)
return h.hexdigest()
def extendhash(hashclass, hashstate, unknown_length, previous, extension):
h = hashclass(hashstate, unknown_length + len(previous))
prevhashed = h.pad(previous, unknown_length)
newpayload = prevhashed + extension
h.l = len(prevhashed) + unknown_length
h.add(extension)
newhash = h.hexdigest()
return (newpayload, newhash)
##### main #####
argparser = argparse.ArgumentParser(
description = 'Perform a hash extension attack on a given hash state.',
fromfile_prefix_chars = '@'
)
argparser.add_argument(
'-m', '--mode',
default = 'extend',
choices = {'hash', 'extend'},
help = 'Mode of operation. hash=calculate hash from optionally given start state extend=calculate hash extension from given state (default: %(default)s)'
)
argparser.add_argument(
'-f', '-hash',
dest = 'hash',
default = 'sha1',
choices = {'sha1'},
help = 'Hash function used for calculations (default: %(default)s)'
)
argparser.add_argument(
'-e', '--extension', '--value',
required = True,
help = 'The extension for which the attack is performed. In hash mode the value which should be hashed.'
)
argparser.add_argument(
'-p', '--previous',
help = 'Previously hashed known value. Mandatory in hash extension mode.'
)
argparser.add_argument(
'-b', '--binary',
action = 'store_true',
help = 'Extension and previous value is given in hex format (e.g. deadbeef) and is converted into bytes.'
)
argparser.add_argument(
'-s', '--hashstate',
help = 'Start state of the hash function in hex format (e.g. deadbeef). In normal hash extension attacks this would be the hash from which the extension should be calculated. Mandatory in hash extension mode.'
)
argparser.add_argument(
'-u', '--unknown-length',
type = int,
default = 0,
help = 'Length of unknown value. Mandatory in hash extension mode.'
)
argparser.add_argument(
'-mu', '--max-unknown-length',
type = int,
default = 0,
help = 'Maximum length of unknown value. If this is given, hash extension iterates between unknown-length and max-unknown-length.'
)
argparser.add_argument(
'-l', '--length',
type = int,
default = 0,
help = 'Initialize hash function with length of already hashed value.'
)
argparser.add_argument(
'-of', '--output-format',
default = 'python',
choices = {'python', 'url', 'hex', 'base64'},
help = 'Output format of new payload. Possible values: python=Python repr(), url=URL encoded, hex=hexadecimal representation, base64=military-grade high security encryption (default: %(default)s)'
)
argparser.add_argument(
'-po', '--payload-output',
help = 'Output file for new payloads'
)
argparser.add_argument(
'-ho', '--hash-output',
help = 'Output file for new hash'
)
argparser.add_argument(
'-d', '--debug',
action = 'store_true',
help = 'Verbose debugging output.'
)
args = argparser.parse_args()
debug = args.debug
if args.mode == 'extend' and (args.previous == None or args.hashstate == None or args.unknown_length == 0):
argparser.error("Mode 'extend' requires previous, hashstate and unknown-length parameter!")
hashclass = eval(args.hash)
if args.binary:
args.extension = bytes.fromhex(args.extension)
if args.previous != None:
args.previous = bytes.fromhex(args.previous)
else:
args.extension = bytes(args.extension, "ascii")
if args.previous != None:
args.previous = bytes(args.previous, "ascii")
if args.mode == 'hash':
print(calchash(hashclass, args.hashstate, args.length, args.extension))
elif args.mode == 'extend':
if (args.max_unknown_length < args.unknown_length):
args.max_unknown_length = args.unknown_length + 1
pf = None
hf = None
if args.payload_output:
pf = open(args.payload_output, mode="w")
if args.hash_output:
hf = open(args.hash_output, mode="w")
for unknown_length in range(args.unknown_length, args.max_unknown_length):
(newpayload, newhash) = extendhash(hashclass, args.hashstate, unknown_length, args.previous, args.extension)
if args.output_format == 'python':
newpayload = repr(newpayload)
elif args.output_format == 'url':
newpayload = urllib.parse.quote(newpayload)
elif args.output_format == 'hex':
newpayload = str(binascii.hexlify(newpayload), "ascii")
elif args.output_format == 'base64':
newpayload = str(base64.b64encode(newpayload), "ascii")
print("New payload: " + newpayload)
if pf:
print(newpayload, file=pf)
print("New hash: " + newhash)
if hf:
print(newhash, file=hf)
if pf:
pf.close()
if hf:
hf.close()