Skip to content

Padding

Padding

PKCS#1 v1.5

  • This Signature Function is deterministic and is constant for every input.

Original Bleichenbacher's Attack

This attack is abusing the parsing of the data when unencrypted.

Most ASN.1 parsers do not reject extra trailing data. While this is important for ASN.1 parsers it it not good for signatures. This allows the parser to ignore a lot of the signature. If you can a perfect cube root over the ignoring the RSA modulus completely it will be accepted.

How the padding works with signatures

00 01 [some non-zero bytes] 00 ASN.1 HASH

How the attack would work

00 01 [some non-zero bytes] 00 ASN.1 HASH GARBAGE

Other ASN.1 parsing vulnerabilities were found including optional parameters being ignored in parsing.

TLS Padding Oracle attack

If there is a way to figure out the difference between invalid padding and invalid encryption it is possible to attack the server.

Original Operation:
In the initial handshake, at some point, the client is supposed to generate a random key (the "pre-master secret"), encrypt it with the server's public key, and send it.

The server decrypts the value, obtains the pre-master secret, and then compute from that pre-master secret the keys used for symmetric encryption of the rest of the connection.

Using the standard for guidance, the client sends a ClientKeyExchange (which contains the encrypted pre-master secret), then a ChangeCipherSpec, then Finished; this last message is encrypted with the derived symmetric key and its contents are verified by the server.

Attack:

If the client sends a random sequence of bytes of the right length to the server instead of a properly encrypted pre-master secret, then the server will, most of the time, respond with an error message telling "I tried to decrypt your ClientKeyExchange contents, but this failed, there was not a proper padding in it".

However, by pure chance, it may happen that the random string, after applying the modular exponentiation, yields something which really looks like a pre-master secret with correct padding. In that case, the server will not complain about the ClientKeyExchange, but about the Finished message, which will be incorrectly encrypted.

This is the information the attacker wants: whether the sequence of bytes he sent would, upon decryption, look properly padded or not.

Since the Padding must start with 0x00 0x02 flowed by [some non-zero bytes] then a null byte and the message [here goes M]

If there random data can be decoded such that it matches this scheme than you don't get a padding error but you will not be able to negotiate a shared key.

OAEP

  • also known as RSA PKCS v2.1
  • Has a random number in the padding to make sure that repeat input has different output

Implementation

Decryption:

message_with_padding = RSAdecrypt(ciphertext)

zero_byte = message_with_padding[0]
seed_xored = message_with_padding[1:hash_length+1]
random_xored_with_message = message_with_padding[hash_length+1:]

Manger's Attack

An implementation OAEP is only vulnerable when you can there is a difference between an invalid decryption and a invalid Padding. This usually pops up when the decryption algorithm decrypts the ciphertext and checks the initial zero byte before verifying the whole message. This way you leak a single byte of the decrypted data. Using this data leakage you can modify a ciphertext that you want to decrypt and narrow down the message that is used using a binary search.

Source

If we have a ciphertext we are able to multiply the ciphertext by a constant number. The decryption will not error out and the decrypted message will also be the plain text times by the same constant number.

RSA Malleability:

\[ \begin{aligned} Enc(m) &= m^e \pmod{n} \\ &= c \\ \end{aligned} \\~\\ \begin{align*} x^e * c & = x^e * Enc(m) \\ & = (m * x)^e \pmod{n} \\ & = Enc(m * x) \\ \end{align*} \]

Because of OAEP we are able to have an oracle that is able to determine weather the decrypted value is >= 2^(8 * (key_length - 1)) for any ciphertext.

This reduces the number of possible plaintext messages that could produce that ciphertext. This can be done in less than the keysize.

Example Code:

N = public_mod
key_size = 2048

# 256 bytes for 2048 key_size
bytes_size = math.floor(math.log(256, public_mod))

# 255 bytes for 2048 key_size
B = 2 ** (8 * (bytes_size - 1))


def binary_search(start, end):
	while start != end:
	middle = (end - start) /2
	#...


def message_oracle(ciphertext):
	#Decrypt ciphertext
	plaintext = RSAdecrypt(ciphertext)
	#Check Inital Padding
	return plaintext[0] == '\x00'

def find_power_mult(ciphertext, oracle):
	#Find a test power that is greater then B and less than 2B
	for power in range(key_size):
		modified_ciphertext = (ciphertext * 2**power) % N

		#Send to Oracle and get info if it is greater than B.
		if oracle(modified_ciphertext):
			# This means that we now know this property B <= plaintext < 2B
			
			return power

#Make better by using binary search
def find_wrap_around(power_mult, ciphertext, oracle):
	#Now we want to find the wrap around number
	start = (B + N) // B
	offset_max = math.ceiling(N / B) 
	offset_start = 0
	offset_end = offset_max

	first_part = 2**power_mult

	# Do Binary search to narrow down the number of guesses
	while offset_start + 1 != offset_end:
		#Set the offset for the binary search
		offset = ((offset_end - offset_start)//2) + offset_start
		
		mult_test = (start + offset) * first_part
		modified_ciphertext = (mult_test * ciphertext) % N

		#Send to Oracle and get info if it is larger than B.
		if oracle(modified_ciphertext):
			# modified plaintext is greater than B
			# Set the end to the current offset
			offset_end = offset

		else:
			# modified plaintext is less than B
			# Set the start to the current offset
			offset_start = offset

	#If it is smaller than B we know the message has wrapped around.
	#We know that (start + x) * ciphertext >= B
	return offset_start

def binary_serach_message(wrap_offset, ciphertext, oracle):
	message_minimum = N // wrap_offset
	message_maximum = (N + B) // wrap_offset
	BB = 2 * B

	#Do another Binary search for the message
	while message_minimum > message_maximum:
		#Mostly Zero until close to the end of the message space
		i = ((2 * B // (message_maximum - message_minimum)) * message_minimum) // N

		# Number to multiply to test message 
		test_mult = i * N // message_minimum
		modified_ciphertext = (mult_test * ciphertext) % N

		#Send to Oracle and get info if it is larger than B.
		if oracle(modified_ciphertext):
			# modified plaintext is greater than B
			# Set the end to the current offset
			message_maximum = (i * N + B) // test_mult

		else:
			# modified plaintext is less than B
			# Set the start to the current offset
			message_minimum = (i * N + B) // test_mult
	return message_minimum

if __name__ == '__main__':
	ciphertext = b""

	# Leak information about the decrypted message through the oracle where the following is true
	# B <= (plaintext * 2**power) % N < 2B
	power_mult = find_power_mult(ciphertext, message_oracle)

	# Using the power number gained from the last function lets divide everything by 2.
	# This means that B/2 <= (plaintext * 2**power)/2 < B
	# Using these properties lets try to find a number that can be multiplied to make the plaintext greater than N. This is before the mod N and the resulting output will be less than B which we can detect with the oracle.
	wrap_offset = find_wrap_around(power_mult - 1, ciphertext, message_oracle)

	# We now know that B <= plaintext * (((B + N) // B) + offset)     * 2**power
	#         and that B >  plaintext * (((B + N) // B) + offset + 1) * 2**power
	# Lets rearrange the terms
	# plaintext <= (N) // (((B + N) // B) + offset)
	# plaintext >  (N + B) // (((B + N) // B) + offset)
	# Now we have a low bound and a high bound. Lets narrow this down more to a single message
	plaintext = binary_serach_message(wrap_offset, ciphertext, message_oracle)

Simplified OAEP (SAEP)

Base64

Base64 Malleability

Mutable Inputs have the same output. Source

Example:

>>> echo "QP==" | base64 -d | xxd
00000000: 40                                       @
>>> echo "QR==" | base64 -d | xxd
00000000: 41                                       A
>>> echo "QQ==" | base64 -d | xxd
00000000: 41                                       A
>>> echo "QS==" | base64 -d | xxd
00000000: 41                                       A
>>> echo "Qf==" | base64 -d | xxd
00000000: 41                                       A
>>> echo "Qg==" | base64 -d | xxd
00000000: 42                                       B