##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

pudel

[TUM CTF, 2015]

category: crypto

by f0rki

  • Category: Crypto
  • Points: 30
  • Solves: 15
  • Description:

Baby's 1st

pudel.tar.xz

Write-up

We received the code of the service, which is again written in python and rather short, and a pcap of a client interacting with the service. The service basically reads data from the socket and tries to decrypt it using AES in a self-implemented CBC mode. It will then check the padding and tell us whether the padding is bad or not. It concludes by printing kthx.

#!/usr/bin/env python3
import os, socket, binascii
from Crypto.Cipher import AES
key = open('key.bin', 'rb').read()
lsock = socket.socket()
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
lsock.bind(('', 1043))
lsock.listen(16)
while True:
  sock, addr = lsock.accept()
  print(addr, end = ' ')
  message = sock.recv(0x100)
  if len(message) < 32 or len(message) % 16:
    print('len')
    sock.send(b'len\n')
    continue
  blocks = [message[i : i + 16] for i in range(0, len(message), 16)]
  aes = AES.new(key, AES.MODE_ECB)
  prev_block = blocks[0]
  blocks = blocks[1:]
  message = b''
  for block in blocks:
    message += bytes(x ^ y for x, y in zip(aes.decrypt(block), prev_block))
    prev_block = block
  n = message[-1]
  if n not in range(1, 17) or any(message[-i] != message[-1] for i in range(1, n + 1)):
    print('bad')
    sock.send(b'bad\n')
    sock.close()
    continue
  message = message[:-n]
  print(binascii.hexlify(message).decode())
  sock.send(b'kthx\n')
  sock.close()

So there isn't much interaction going on, so it's rather obvious that this service acts as a padding oracle. Also the challenge is called Pudel, which is german for poodle, which is the name for a prominent SSL vulnerability. So our goal is to use the service as padding oracle and decrypt the encrypted data in the pcap file.

Padding oracle attacks are pretty cool because they allow you to guess the content of a ciphertext block byte by byte, which reduces the guess complexity in our case from pow(256, 16) to 256 * 16 == 4096, which is pretty good. The attack works in the following way. We start by guessing the last byte of the last plaintext block. To check whether the guess was correct we change the last byte of the previous ciphertext block to guess ^ c[-2][-1] ^ 0x01. Because of the way CBC works, this screws up the plaintext p[-2] and will XOR the value guess ^ 0x01 to the last byte of the last plaintext block p[-1]. If we guessed right guess ^ p[-1][-1] == 0 and the last byte of the decrypted plaintext will be 0x01. This means we have a correct padding at the end of the plaintext. If we have guessed wrong, there will be some other value and we get a bad padding error. We then continue guessing the next byte of p[-1] but aiming for a two byte padding with 0x02 bytes at the end.

This is all a little painful to implement, but fortunately there's a python module for that :) I used python-paddingoracle and it worked like a charm. After implementing the client that queries the service for the padding error, all that was left was to wait.

from pwn import *  # NOQA
import socket
from Crypto.Cipher import AES
from paddingoracle import BadPaddingException, PaddingOracle
HOST = "131.159.74.81"
PORT = 1043
# extracted from pcap
encdata = [0xcb, 0x3f, 0x73, 0xdd, 0x38, 0x76, 0x1b, 0x17,
           0x5c, 0x9b, 0x38, 0x07, 0xef, 0x00, 0x46, 0x9b,
           0xd0, 0x9e, 0x46, 0xc3, 0x5f, 0xcd, 0xec, 0x07,
           0xbd, 0x06, 0xab, 0x61, 0x51, 0x82, 0xce, 0x21,
           0x06, 0xc2, 0x02, 0x9f, 0xd5, 0xd0, 0x27, 0x4f,
           0x6c, 0x84, 0x5b, 0x5a, 0x58, 0xaa, 0xfa, 0x2f,
           0xca, 0x23, 0xfb, 0xbd, 0xdb, 0x5d, 0x1c, 0x49,
           0xb3, 0x56, 0x7d, 0x62, 0x97, 0xda, 0x7d, 0x69]
log.info("%d blocks of AES", len(encdata) // AES.block_size)
# split the blocks
bs = AES.block_size
iv = encdata[:bs]
blocks = [encdata[bs:2 * bs],
          encdata[2 * bs:3 * bs],
          encdata[3 * bs:]]
encdata_s = ""
for b in blocks:
    for c in b:
        encdata_s += chr(c)
class PadBuster(PaddingOracle):
    def __init__(self, **kwargs):
        super(PadBuster, self).__init__(**kwargs)
    def oracle(self, data, **kwargs):
        log.info(hexdump(data))
        #rem = remote(HOST, PORT)
        rem = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        rem.connect((HOST, PORT))
        rem.sendall("".join(chr(x) for x in data))
        line = rem.recv(5)
        #self.history.append(line)
        log.debug(line)
        if "len" in line:
            raise Exception("length of sent data is wrong!")
        if "bad" in line:
            raise BadPaddingException()
        rem.close()
        del rem
padbuster = PadBuster()
data = padbuster.decrypt(encdata_s, block_size=AES.block_size, iv=iv)
log.info(data)

After running the script for some time the flag hxp{ev3n_1n_2015_SSL_st1LL_c4nT_g3t_1t_r1ghT} appeared.

/writeups/ $

$