For this challenge we’re given a network address where we can access what looks like a simple shell, and the source code to this shell, which is thankfully python code.
Looking at macsh.py we see a set of commands, some of which are privileged. We can
echo without privilege, and
Looking at the tag implementation, it looks like it creates and prints the MAC of the specified args, as long as the args don’t start with a priveleged command. Looking at the command parsing, it looks like commands are expected to come in the format
TAG<|>COMMAND, and the command will not execute unless the tag matches the mac of the command. The
tag command, however, skips the check.
The goal here is obvious: Take advantage of
tag to get the MAC of a privileged command, so that I can find and eventually dump the flag file. Let’s see how the crypto is implemented.
ECB mode! That means that each block is encrypted individually! The order and the neighbouring blocks don’t matter. This is easy: Just tag something that has a privileged command within its own block! Then snip out that part, and provide it on its own.
There’s a snag here: tag gives you a MAC, not the encryption of each block. Their mac combines all of the encrypted blocks by
xoring them together. We can easily get rid of this by taking advantage of this property of xor:
A ^ A = 0
A ^ B ^ A = B
What this means is that if we can get the tag of a good block (A), and then the tag of the same good block plus a privileged block (A^B) we can xor these together and end up with the tag of just the privileged block!
I write some code to do this and… fail. Time to run this locally and add some debugging prints.
Notice that in the
fmac function there are calls to
to_blocks. This pads the input so that its length is a multiple of the block size (16 bytes) as also adds a padding block or two. Adding some prints we can see what’s happening:
This throws a bit of a wrench in our gears: If we tag
GOOD|PRIV, because its length will be different than
GOOD, the extra blocks will not be the same, meaning we cannot just ‘snip out’ the good part. We need the length to be the same. Let’s try that:
That’s more like it! Now we have A^B^PAD, A^PRIV^PAD, but we want PRIV^SPAD, where SPAD is the padding of a short block (Since sending the privileged command on its own will be three blocks instead of four). We can use a combination of short and long commands to get what we want.
PRIV^SPAD = A^PAD ^ PRIV^PAD ^ A^SPAD
Now the two ‘A’ bogus commands cancel out, the two long pads cancel out, and we’re left with a privileged command with a short pad!
This is where I hit another snag: When debugging I noticed that the order of the blocks matter. That’s not the ECB I know! Turns out I missed an important part of
C.encrypt(xor(b, f(k0, i))). What does
Ah! The key is being rotated by the block index! Shit! That means that the PRIV block I put at index 1 won’t be MACed with the same key at index 0! How do we solve this?
If you think about this key rotation you’ll realize that there are effectively 128 versions of this key, since there are 128 bits in the key. What happens when we’re on the 129th block? The key gets rotated all the way around… back to it’s original value! This means that for PRIV to work properly, I need it to be MACed in the 129th block. This is easy to do! I just need to lengthen A so that it’s 128 blocks long! The same math works out to cancel out PAD and A.