We didn’t have enough time and enough people to do CSAW 2015 seriously, but we did have fun on some of the challenges. Below are the two most memorable ones for me.
Reversing 500 - Wyvern
As soon as you start reversing this binary, the first thing you notice are those weird basic blocks everywhere:
What are those? Opaque predicates. Indeed, the branch taken by jne depends on the variables x3 and y4 but those are generated at runtime and always have the same value. This means the branch is already determined but you have to actually run the program to determine it. Here these opaque predicates are used in two ways:
- splitting basic block and adding false branches in between them
- obfuscating the control flow of real branches
Here it’s not too bad: the opaque predicates are always the same inside a function and the basic block are not too much shuffled. You can still deduce the order of the basic blocks and guess what a function is doing. This seems to be the basic flow of the program:
- read user input (the dragon secret) at 0x401ef5 in main()
- call the start_quest() (0x404350) function with the dragon secret as a C++ string
- inside start_quest() a global std::vector
(called hero) is initialized, then sanitize_input() (0x401cc0) is called with a copy of the dragon secret string
- inside sanitize_input() a whole lot of bullshit happens where the hero vector
is manipulated using the dragon string and transform_input() (0x4014b0) is
called with some other std::vector
- somehow the whole thing needs to return 0x1337
As you can see I still have a very rough idea of what the program is doing although I solved the challenge. But you only really need to spot two things.
The first one is the length check on the dragon secret string inside start_quest(). As you can see it’s compared with a static variable from the data segment but binary shifted right by two (probably some constants obfuscation) which evaluate to 28. Again it’s mixed with an opaque predicate but it’s pretty obvious: the input string should be 28 characters long.
(note: I annotated some stack variables, that’s not in the actual binary)
The loop increment inside sanitize_input(). This variable is used to index the input string (the dragon secret) at 0x402188. You can easily guess that there should be a loop going through all the characters, it’s supposed to check your input after all.
If you set a breakpoint at the start of sanitize_input() and at the loop increment you can validate two things: it will only execute sanitize_input() if the dragon secret is 28 characters long, it never steps on the loop increment. Why? Because it detects your dragon secret is invalid at the first character and breaks early. And that’s a flawed design because it means I can count the loop iterations to know how many characters I got right (note: you can also use Intel pin or another DBI framework to count the number of instructions executed).
The attack is the following: run the program on dragon secrets of 28 caracters but starting with a different first character each time, if the program breaks on the loop increment it means the character is correct and you can attack the next one. At the end you will have the complete dragon secret.
Of course this takes many tries and you have to automate. Here’s how to do it with GDB and its python API. This script implements a “counting breakpoint” using the GDB API. Passing it to the --command flag of GDB will return at the end the loop iteration count of sanitize_input().
Now this other script uses the first one to bruteforce the dragon secret.
It will take some time to run but in the end you will get the flag ;)
Crypto 500 - Bricks of Gold
No stegano involved, pure crypto, I swear.
First of all we have that crucial description:
We’ve captured this encrypted file being smuggled into the country. All we know is that they rolled their own custom CBC mode algorithm - its probably terrible.
I’m not going to explain what CBC is, just look it up if you don’t know. You should also have very quickly discovered at the end of the file this line:
So we have the IV, “CASH”, and thus also the block size: 32 bits. And that’s all we have. So how do we break an unkown crypto function?
Any good crypto should behave as a PRNG thus if we want to break it we should find something not random, a pattern, and understand why it exists. The best place to check in a file is always the beginning (because there’s usually a header there) and the end (that’s purely empirical).
And indeed at the start of the file there are those weird 32 bytes:
This should never happen in any serious crypto implementation.
If you ignore the first 2 bytes of each line, the only difference is in the 6th nibble (hex digit) of each block. And if you start looking at the binary encoding of those nibbles you will see there’s only one bit flipped in that nibble. What does this means?
Remember it’s CBC, each ciphertext block is used to xor the next block of plaintext. So if one bit is flipped in a ciphertext block, the same bit will be flipped in the plaintext block before it goes through the encryption function. And in a good cipher this should have a pseudo random effect on the output known as diffusion. Here the exact same bit is flipped at the output of the encryption function and this happens on 3 consecutive blocks.
This means there’s no diffusion. This means it’s a bit level substitution function. This means this is a xor cipher (the only binary operation which is bijective).
So we know it’s a CBC mode xor cipher (seriously, wtf) and we know the IV, now we need the key.
Again this is a file, and a file has a header which often contains a predictable magic number.
So what we can do is:
- guess a file format
- use the magic to compute the key on the first block
- decrypt the whole file and attempt to open it
- repeat those steps on the most common file format until it works
After a few tries, the pdf magic number worked. Here’s the python script:
Look at that beauty!
Anyway, thanks a lot to ISIS lab for those cool challenges ;)