CTR Bitflipping
August 22, 2019
No much to explain: encryption does not offer any protection against forgery.
– Spoiler Alert! –
We saw this in the CBC Bitflipping post and we will see it again here but this time it will be the CTR encryption mode our victim.
Setup
Recall from CBC Bitflipping post the scenario where we have a add_user_data
function that adds arbitrary data to users’ profiles:
>>> cfg = generate_config(random_state=seed, block_size=block_size,
... enc_mode='ctr',
... prefix = "comment1=cooking%20MCs;userdata=",
... posfix = ";comment2=%20like%20a%20pound%20of%20bacon")
>>> def add_user_data(m):
... assert ';' not in m and '=' not in m
... msg = B(cfg.prefix + m + cfg.posfix)
... return enc_ctr(msg, cfg.key, cfg.nonce)
On the server side there is a function that checks for admin role:
>>> def is_admin(c):
... msg = dec_ctr(c, cfg.key, cfg.nonce)
... return b'admin=true' in msg.split(b';')
We control the user’s data (userdata
field) but we cannot control the entire profile.
In particular, we cannot say that we have the administration role adding admin=true
.
Well… technically the check in add_user_data
is made with an assert
.
In Python these asserts are removed if the code is executed with the optimization flag so if the add_user_data
runs with the flag on we could inject anything.
So do not use assert
for any real check.
So, no, we cannot do this:
>>> add_user_data("somedata;admin=true")
<...>AssertionError
Bit flipping
In CBC Bitflipping post we saw that CBC does not offer any protection against forgery and how to break it.
In this post we will do the same but attacking the CTR mode.
First we will create our target plaintext and a padding plaintext. The former is the plaintext that we want to inject and the latter is the one that we are allowed to inject.
>>> target = B(';admin=true;')
>>> padding = 'A' * len(target)
Then we create our profile:
>>> ctext = add_user_data(padding)
Now, because CTR turns a block cipher into a stream cipher using xor, we can patch it trivially:
>>> patch = target ^ B(padding)
The only catch is that we don’t know where our padding is located so we don’t know where to patch.
For this we can use is_admin
as an oracle function, trying each position and knowing the correct one only when we get is_admin(..) == True
>>> l = len(patch)
>>> for i in range(len(ctext) - l + 1):
... cpatched = ctext[:i] + (ctext[i:i+l] ^ patch) + ctext[i+l:]
... assert len(cpatched) == len(ctext)
...
... if is_admin(cpatched):
... print("Priv escalated! Patch at %i" % i)
Priv escalated! Patch at 32
Broken! CTR bitflipping challenge unlock!
Related tags: cryptography, matasano, cryptonita, CTR, counter, forgery