TUCTF was a capture the flag
organized by the University of Tulsa.
I participated with a friend and we even managed to score first place in the high
school bracket.
WoO
Hmm. Opening the binary in radare shows some immediately suspicious calls to scanf(“%s”).
It also shows that the structs containing the data about the animals are stored on
the heap and it also reveals the struct layout.
There is also a secret function named pwnMe, which gets called when the number
0x1337 (4919) gets passed as a menu option and a function named l33tH4x0r (not called anywhere)
which loads the flag from a file on the target system and prints it.
Opening up the pwnMe function in gdb shows, that it loads the index of the last bear
struct (which gets set in the makeBear function), checks if the type of the bear
is equal to 3 and if it is, it loads a function pointer from the start of the struct
and then jumps to anywhere the pointer points to.
The first problem is the bear type — we can choose either 1 or 2, but not 3. However,
looking at the struct layout it looks like this can easily be bypassed by overflowing
the bear name scanf(“%s”) as the type is placed right after it.
Next the default 0xdeadbeef pointer needs to be overwritten, it is before the
name, so overflowing the bear name won’t help.
Notice that the structs are arranged after one another on the heap - this is
what the exploit relies on.
Okay, so the plan is as follows:
Load first two animals, does not matter which ones
Load a bear, overwrite its type with 3
Free the struct right before the bear struct
Overwrite the function pointer at the start of the bear struct using the scanf overflow by allocating another animal (this allocation should be put where the struct deleted in step 3. was)
The exploit then looks as follows:
WoO2-fixed
This is a bit more complex variant of the previous challenge, as the scanf overflow
is fixed, so the bear type cannot just be overwritten.
Returning to the bearOffset variable, notice that it does not get cleaned when the bear gets deleted.
Another important thing to notice is the difference in struct layout between
the bear struct and the other animals — the space in the bear struct occupied
by the function pointer is reused in the other animal structs for the name string.
This leads to the followint reuse-after-free scenario:
The bear struct gets loaded
The bear struct gets freed
Another struct takes its place, with animal type 3 and the target
function pointer as its name.
Especially Good Jmps
The challenge description also says that ASLR is enabled on the remote server.
Looking at how the name is loaded:
Okay, so we have full control over the stack and DEP is disabled, however, as the stack
location is random, we cannot just load a shellcode and then jump to it as its address
is unknown.
Note that the binary is not PIE, so at least all the functions which get loaded from libc
and the main application code is is accessible using constant addresses.
Let’s run a debugger and put a breakpoint at return from the main function
(which is where we first are able to get control over the execution).
Nice, so there is a stack pointer on the stack, now we need to leak it. To do that,
a fragment of the “Hello %s, %d is an odd number!” can be used (remember, the binary
is not position independent, so this will stay on a constant address) and an address
of the printf function from the PLT.
In the first round of the exploit, the stack will be arranged as follows:
Pointer to printf in the PLT
main address (as we need to run main one more time after leaking the pointer)
Pointer to “%d is an odd number”
Some stack pointer
This will cause the server to output the stack pointer, the second round is then
easy — just load a pointer to the shellcode and the shellcode itself.
Reverse your Rage
Opening up the file in IDA immediately shows that it was written in C++. This is
as good as an obfuscation, especially because this is the first time I am reversing
anything written in it.
The task is to figure out what input to the analyzeMeow function will make
it return true. The important parts of the function are here:
The first part is easy to puzzle out — compare every character of the input at an odd index
to the characters stored in mewOne. This leads to:
After staring at the second part for a bit, I decided to take a more straigtforward approach:
The point at which the comparison fails is at the mov [rbp-25h], 0 line. The current
character index is stored at [rbp-0x2c]. This means that the flag can easily be guessed
character-by-character by setting a breakpoint to
trigger when the comparison fails and checking whether the counter incremented beyond
the currently guessed character.
Altough this could very likely be easily implemented by scripting gdb directly,
there was not enough of time for me to learn that, so I just took a
semi-automatic approach:
And in another window:
Secure Auth
Okay, here we have an RSA signing oracle (it obviously won’t sign the requested
message directly). However, this oracle is not using proper padding, which has
a lot of problems.
First, the public key needs to be recovered,
this can be done using a pair of two signatures,
\((m_1, s_1),\ (m_2, s_2)\):
This also means guessing the public exponent, which turns out to be the
commonly used \(65537\). Altough \(\gcd(k_1, k_2)\) is not guaranteed to be \(1\)
it is easy to just pick new message pairs until it is (the public key then
can be verified by getting the server to sign it, as \(N^d \bmod N = 0\)).
Now onto the signature forgery. We first need to pick a constant \(k\) such as
that the message (\(m_b\)) is evenly divisible by it. Signatures for \(k m_b\) and \(k\) can
then be used to create a valid signature for \(m_b\).
\[\begin{align}
s_k &= k^d \bmod N \\
s_m &= (km_b)^d \bmod N \\
&\Rightarrow \\
s_b &= s_m s_k^{-1} \bmod N
\end{align}\]
Hash n Bake
So, here we are given \(hash(xor(k, m_1))\) and \(m_2\) and need to compute
\(hash(xor(k, m_2))\). As it looks like, we need to inverse the hash function
and then xor PLAIN_1 with the result to get KEY.
Looking at the hash function, there is an important observation to make:
The last bit of the result is flipped (compared to CONST) if and only if the
message got XORed in the final round.
After un-xoring it, the same applies to the
previous bit and so on, so the inverse hash function looks as follows:
Another important observation is, that the first bit of CONST2 is 1 – this means
that the message can safely be initialized to zeroes, as that is what it ends up
being after the hash function runs forward.
To make this a bit more clear, let’s take a look at some intermediate values:
(exclamation marks show when the result has been xored)