Skip to content

Instantly share code, notes, and snippets.

Last active Oct 30, 2015
What would you like to do?
CSAW 2015 wyvern

CSAW RE 500 - wyvern

tl;dr The program checks a key one character at a time, and aborts early if incorrect. This means it leaks information via the number of instructions executed.

wyvern_c85f1be480808a9da350faaa6104a19b: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/, for GNU/Linux 2.6.24, BuildID[sha1]=45f9b5b50d013fe43405dc5c7fe651c91a7a7ee8, not stripped

So, I first popped wyvern into IDA, and looked at main. The binary isn’t stripped, so with relatively little reverse engineering you can see that you need to make the start_quest() function return 0x1337, and that the start_quest() function takes in your input. If start_quest() returns 0x1337, then your input is the key.

win branch

Stylistically, this is a pretty basic crackme. Now we just need to reverse start_quest() to make it return the value we want!

Going into start_quest, we see a bunch of values being pushed to a stack. Following this code path, we see the program branches on the input length: it takes one path if the length - 1 is 28, and another if it’s not (I assumed, and verified dynamically that the length included the newline character, thus the -1).

length check

From here however, it became difficult to trace the execution flow. The program depends on quite a few variables whose values seem to be zero (and IDA claims they’re never written to), but that doesn’t make a lot of sense. So, I figured I’d approach the problem from the other side: trace back where the return comes from. Near the end of the function, I did just that:

(Return value is stored in eax, working backwards)

.text:404AC8 mov     eax, [rbp+ret]

.text:404ABA mov     [rbp+ret], eax

.text:404A7E mov     eax, [rax]
.text:404A7A mov     rax, [rbp+result_ptr]

(ret is -0x5c, result_ptr is -0x38)

So, this tells us a pointer to our result is going to be stored in rbp+result_ptr. Running xrefs on result_ptr shows that it gets written in a few spots, with code that looks like this:

.text:404760 mov     eax, legend
.text:404767 sar     eax, 2
.text:40476A mov     rcx, [rbp+result_ptr]
.text:40476E mov     [rcx], eax

or like this

.text:404F0B mov     rax, [rbp+result_ptr]
.text:404F0F mov     ecx, [rbp+sanitized_input_ptr]
.text:404F12 mov     [rax],ecx

(sanitized_input_ptr is -0x48)

These two blocks of code are in the two code paths I mentioned earlier: the former is in codepath if the input length is not 28 and the latter is in the code path where the input length is 28. I recognized the legend variable from the length check earlier, and knew its initial value. IDA doesn’t show any writes to legend, and running it dynamically indicates that the return is, for some sample inputs, 28. Based on this, I figured the input length for a valid key had to be 28 bytes.

If the key length is 28 bytes, the result of sanitize_input() is what gets returned. So, let’s look at that function:

sanitize cfg


Up until this point I had been doing most of the work statically, and just verifying my results dynamically when applicable. After seeing this function I decided to re-evaluate my approach. Additionally, the program hints towards this: “brute strength and magic is our only hope. Test your skill.” (and basically confirmed by the hints released by the organizers on the ctf page)

Originally I was thinking of some sort of z3 magic, but then I remembered an article written by Jonathan Salwan on using instruction counts to defeat crackmes that I had read recently:

The basic idea is that if your crackme processes data one character at a time, and aborts early, you can use instruction counts to brute force the correct key one character at a time. The above article explains the process in more detail, and is a good read if the concept doesn’t make sense.

Intel’s pin is the dynamic binary instrumentation (DBI) I’m the most familiar with, and it even comes with an instruction count pintool out of the box (ManualExamples -- you'll just need to compile it first). To use it, just run the program under the pintool, then look at the created file:

x@ctf:~/csaw-2015/wyvern$ ~/pin/pin-2.14-71313-gcc.4.4.7-linux/ -t ~/pin/pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-intel64/ -- ./wyvern_c85f1be480808a9da350faaa6104a19b
|    Welcome Hero       |

[!] Quest: there is a dragon prowling the domain.
        brute strength and magic is our only hope. Test your skill.

Enter the dragon's secret: AAAA

[-] You have failed. The dragon's power, speed and intelligence was greater.
x@ctf:~/csaw-2015/wyvern$ cat inscount.out
Count 1420450

This indicates that the program executed 1420450 instructions. So, using this I whipped up a simple shell script to start searching.


for n in $(seq 32 126)
        c=$(printf \\$(printf '%03o' $n)) # c = chr(n)
        echo "$testcase" > testfile

        ~/pin/pin-2.14-71313-gcc.4.4.7-linux/ \
                -t ~/pin/pin-2.14-71313-gcc.4.4.7-linux/source/tools/ManualExamples/obj-intel64/ \
                -- ./wyvern_c85f1be480808a9da350faaa6104a19b < testfile > /dev/null

        inscount=$(cat inscount.out | cut -d ' ' -f 2)
        echo "$testcase: $inscount"

This will iterate through the printable ascii characters, outputting the instruction count for each one.


Hm, it looks like the program did an extra 10k or so odd instructions when the first character was ‘d’. Modify the test key to be ‘d${c}AAAA….’ and run the script again…


This part could be automated obviously, but occasionally pin will hiccup and change instruction counts by 2 or 3. Plus, at this point it was late and I didn’t really feel like writing more scripts, so I mostly ran the code in the background while wasting time on irc. From here, it’s just repeating this process 28 times to brute force the entire key.

It’s worth noting here, this is just the list of things that ended up working. I spent a ton of time going down dead ends trying to figure out what the x and y values were that get referenced throughout the program were and based on the structure I was pretty sure this approach wasn’t going to work. I figured I should at least try it, because it’s a relatively fast thing to check and it ended up paying off.

Final key: dr4g0n_or_p4tric1an_it5_LLVM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment