We did a CTF at work.
I have to say it was loads of fun â Iâve never really participated in a CTF before, so it was really good to test my hacking skills against my new colleagues.
We had one instruction from my manager â âdonât let the interns beat youâ. I was determined to make sure that didnât happen, but I was also determined to share as much knowledge and excitement for InfoSec as possible. This meant that once or twice, I may have egged an intern on in the face of the fact that they were about to discover it anyway, and it just seemed like a really good way to keep them interested.
This is not that story.
This is about me turning the corner from knowing about a security failure, to understanding how it works. Letâs see if I can help you guys understand, too.
Thatâs the title of my blog, and thereâs not a whole lot of cryptography here. Itâs just a play on words, which was a little more relevant when I first started the blog back in 2005. So hereâs some crypto, at last.
Thereâs several aspects to cryptography that you have to get right as a developer:
Having tried to teach all of these to developers in various forms, I can tell you that the first one, which should be the simplest, is still surprisingly hard for some developers to master. Harder still for managers â the number of breach notifications that talk about passwords being âencryptedâ is a clear sign of this â encrypted passwords mean either your developers donât understand and implemented the wrong thing, or your manager doesnât understand what the developer implemented and thinks âencrypted sounds better than hashedâ, and puts that down without checking that itâs still technically accurate.
Key creation (so itâs not predictable), and storage (so it canât be found by an attacker) is one of those issues that seems to go perennially unsolved â Iâm not happy with many of the solutions Iâve seen, especially for self-hosted services where you canât just appeal to a central key vault such as is currently available in all good cloud platforms.
Picking correct algorithms is a moving target. Algorithms that were considered perfectly sound ten or twenty years ago are now much weaker, and can result in applications being much weaker if they arenât updated to match new understanding of cryptography, and processor and memory speed and quantity improvements. You can store rainbow tables in memory now that were unthinkable on disk just a decade or two ago.
Finally, of course, if all that wasnât enough to make cryptography sound really difficult (spoiler: it is, which is why you get someone else to do it for you), there are a number of ways in which you can mess up the way in which you use the algorithm.
There are a large number of parameters to set even when youâve picked which algorithms youâre using. Key sizes, block sizes, are fairly obvious â larger is (generally) better for a particular algorithm. [There are exceptions, but itâs a good rule of thumb to start from.]
There are a number of different modes available, generally abbreviated to puzzling TLAs â ECB, CFB, OFB, CBC, GCM, CTR, and so on and so forth. Itâs bewildering. Each of these modes just defines a different order in which to apply various operations to do things like propagating entropy, so that itâs not possible to infer anything about the original plaintext from the ciphertext. Thatâs the idea, at least. ECB, for instance, fails on this because any two blocks of plaintext that are the same will result in two blocks of ciphertext that are the same.
And if youâre encrypting using a block cipher, you have to think about what to do with the last block â which may not be a complete block. This requires that the block be filled out with âpaddingâ to make a full block. Even if youâre just filling it out with zeroes, youâre still padding â and those zeroes are the padding. (And you have to then answer the question âwhat if the last block ended with a zero before you padded it?â)
Thereâs a number of different padding schemes to choose from, too, such as âbit paddingâ, where after the last bit, you set the next bit to 1, and the remaining bits in the block to 0. Or thereâs padding where the last byte is set to the count of how many padding bytes there are, and the remaining bytes are set to 0 â or a set of random bytes â or the count repeated over and over. Itâs this latter that is embodied as PKCS#5 or PKCS#7 padding. For the purposes of this discussion, PKCS#7 padding is a generalised version of PKCS#5 padding. PKCS#5 padding works on eight-byte blocks, and PKCS#7 padding works on any size blocks (up to 256 bytes, presumably).
So, if you have a three-byte last block, and the block size is 16 bytes, the last block is ** ** ** 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d 0x0d (where â**â represents the last three bytes of data, and 0x0d represents the hexadecimal value for 13, the number of bytes in the padding). If your last block is full, PKCS#7 covers this by making you create an extra 16-byte block, with the value 0x10 (decimal 16) in every byte.
Itâs not at all unlikely that you wind up with the scenario with which we were presented in the CTF â a service that communicated with AES encryption, in CBC mode, using PKCS#7 padding. The fact that this was described as such was what tipped me off in the first place. This is the perfect setup for a Padding Oracle attack.
An Oracle is simply a device/function/machine/system/person that you send a request to, and get a response back, and which gives you some information as a result. The classical Oracles of Ancient Greece & Roman times were confusing and unhelpful at best, and thatâs really something we want from any cryptographic oracle. The term âRandom Oracleâ refers to a hypothetical system which returns random information to every query. A good cryptographic system is one that is indistinguishable from a Random Oracle.
Sadly, CBC with PKCS#7 padding is generally very different from a Random Oracle. It is a Padding Oracle, because it will tell us when the padding is correct or incorrect. And thatâs our vulnerability.
At this point, I could have done what one of my colleagues did, and download PadBuster, choosing parameters and/or modifying code, to crack the encryption.
ButâŠ Iâve been attacking this CTF somewhat âŠ non-traditionally, using tools other than the normal ones, and so I thought Iâd try and understand the algorithmâs weaknesses, and implement my own attack. I wrote it on the bus on my way into work, and was pleased to see when I got in that it worked â albeit slowly â first time.
When decrypting each block using CBC, we say that PN = DK(CN)âCN-1 â which is just a symbolic way of saying that the recipient Decrypts (with key âKâ) the current Ciphertext block (block N), and then XORs the result with the previous Ciphertext block (the N-1th block). Letâs also assume that weâre only decrypting those two blocks, N-1 and N, with N being the last block provided to the recipient.
In other modes, the padding check may not deliver the helpful information weâre looking for, but CBC is special. The way CBC decrypts data is to decrypt the current block of ciphertext (CN), which creates an intermediate block DK(CN). That intermediate block is combined with the previous ciphertext block, CN-1, to give the plaintext block, PN. This combining of blocks is done using the XOR (exclusive-or) operation, which has interesting properties any developer should be familiar with. Particularly, itâs important to note that XOR (represented here as âââ) is reversible. If XâY=Z, you know also that ZâY=X and ZâX=Y. This is one of the reasons the XOR operation is used in a lot of cryptographic algorithms.
If we want to change things in the inputs to produce a different output, we can really only change two things â the current and the previous block of Ciphertext â CN and CN-1. We should really only alter one input at a time. If we alter CN, thatâs going to be decrypted, and a small change will be magnified into a big difference to the DK(CN) value â all the bytes will have changed. But if we alter CN-1, just a bit, what we wind up with is a change in the plaintext value PN which matches that change. If we alter the 23rd bit of CN-1, it will alter the 23rd bit of PN, and only that one bit. Now if we can find what weâve changed that bit to, we can then figure out what that means we must have changed it from.
If we change the last byte of CN-1, to create CâN-1 (pronounced âC prime of N minus 1â) and cycle it through all the possible values it can take, the decryption will occur, and the recipient will reject our new plain text, PâN (âP prime of Nâ) because it is poorly formed â it will have a bad padding. With one (two, but Iâll come to that in a minute) notable exception. If the last byte of the plaintext decrypted is the value 0x01, itâs a single byte of padding â and itâs correct padding. For that value of the last byte of CâN-1, we know that the last byte of PâN is 1. We can rewrite PN = DK(CN)âCN-1 as DK(CN) = CN-1âPN â and then we can put the values in for the last byte: DK(CN) = CâN-1â0x01.
Letâs say, for illustrationâs sake, that the value we put in that last byte of CâN-1 was 0xa5, when our padding was accepted. That means DK(CN) = 0xa5 â 0x01 = 0xa4. Note the lack of any âprimeâ marks there â weâve figured out what the original value of the decrypted last byte was. Note that this isnât the same as the last byte of the plain text. No, we get that by taking this new value and XORing it with the original last byte of the previous block of ciphertext â thatâs CN-1. For illustration, letâs say that value is 0xc5. We calculate PN = DK(CN)âCN-1 = 0xa4â0xc5 = 0x61. Thatâs the lower case letter âaâ.
OK, so we got the first piece of plaintext out â the last byte.
[Remember that I said Iâd touch on another case? If CN is the original last block of ciphertext, it already contains valid padding! But not necessarily the 0x01 weâre trying to force into place.]
Almost the same process is used to get the next byte, with a couple of wrinkles. First, obviously, weâre altering the second-to-last byte through all possible values. Second, and not quite so obvious, we have to tweak the last byte once as well, because weâre looking to get the sequence 0x02 0x02 (two twos) to happen at the end of PâN. The last byte of CâN-1 to achieve this is simply the last byte of CâN-1 that we used to get 0x01, XORed by 0x03 (because thatâs 0x02 â 0x01). In our illustrative example, thatâs 0xa6.
Each time, you have to set the end values of the ciphertext block, so that the end of PâN will look like 0x03 0x03 0x03, 0x04 0x04 0x04 0x04, etc, all the way up to 0x10 âŠ 0x10 (sixteen 16s).
So hereâs the 200 lines or so that I wrote on the bus. I also wrote a test harness so that this would work even after the CTF finished and got shut down. Youâll find that in the same repo.
Iâve massaged the code so itâs easier to understand, or to use as an explainer for whatâs going on.
I plan on expanding this in a couple of ways â first, to make it essentially command-line compatible with âPadBusterâ, and second, to produce a graphical demo of how the cracking happens.
And in the next post, Iâm going to talk a little about how I optimised this code, so that it was nearly 15x faster than PadBuster.