Solving a (very) simple keygen challenge with Binary Ninja

Disclaimer: this post is meant for beginners to reverse engineering. If you have any experience with crackmes, chances are you know more than I do and this probably not worth your time.

So a couple of days ago, I purchased a license for Binary Ninja. It is a really neat reverse engineering tool. Before this, I had been using radare2 and the radare-based Cutter GUI. These are also great tools, and more than powerful enough for the challenge I will be discussing in this blogpost (hell, a more experienced reverse engineer than me could do it with just objdump).

But I thought Binary Ninja just seemed like a really cool program, and I wanted to play around with it a little bit. Besides, they offer a student license, and during black friday they gave away free credits for their merch store on any purchase.
I quite really like their business model, they're not free/open-source like radare2/cutter, but I would argue $300 is a reasonable for an enthousiastic hobbyist and the $75 student license is a great deal. This does only give you support for a year though, which means after a year you can still use the software, but you won't get any updates. But again, I think that is a reasonable compromise.
Vector35, the developers behind Binary Ninja, are also just much more sympathetic than some others (looking at you Hex-Rays). They engage much more with the community and generally just seem like really cool people.

But a bit about the tool itself: it might not be as powerful as IDA Pro, it is also a fraction of its price. It has a great Python API that allows users to script things and create their own plugins, and there are many useful plugins available that are made by the community.
One of the main selling points is the BNIL: Binary Ninja Intermediate Language. I haven't really looked into the specifics, but as I understand it, this is a family of intermediate languages (ranging from low-level to "high-level" C pseudocode) that not only makes it easier to quickly read through a disassembled binary, but also makes it easier to extend Binary Ninja with plugins or by adding support for unsupported architectures.

The challenge I will be solving is "glowwine", by Bkamp on crackmes.one. It is a simple keygen challenge: the program asks for a key, which is validated according to some rules (so there are multiple valid keys). Our goal is to figure out how the validation works and to write a script that generates valid keys.

By the way, this is what our high-level view looks like. As you can see, it is very good at "simplifying" code. But I am not a C developer, so the pointer stuff was a bit confusing to me, and I actually found the assembly code much easier to read step by step. Though the low-level view (basically assembly with Python-like syntax) and medium-level view were quite useful.

High-level view
Medium-level graph view

The top block just checks if we have entered a key, and if not exits the program. In the next block, we can see a call is made to strlen, let's take a closer look at that.
We can see here that it loads rbp-0x1o+0x8 (=rbp-0x8) into rax. We then load its value (which is our string pointer) into rdi, which is then passed as an argument to strlen. The return value of strlen is then compared to 5.

In this part, we skip one character (because we increase the pointer by one). Then on the 5th line we load one byte into eax. This one confused me a bit at first, but the movzx instruction just adds a bit of padding so that we can copy one byte over to a 32-bit register. This is then compared to 0x40 ('@' in ASCII), which means our second character has to be an @.

Here in the last part, we load characters into the al register like we did above, but this time, we add al to edx for the 3rd, 4th and 5th character. Then at the end, the sum of these values is compared to 0x12c (=300 in decimal). This means the sum of the ASCII values of our characters has to be 300.
So, the format of a valid key is `a@bcd`, where a is any character, and the sum of the ASCII values of b, c and d is equal to 300. An example of a valid key would be `A@ddd`, as the ASCII value of d is 100.

You can quickly open an ASCII table on Linux with the command man ascii

But this is easy enough to bruteforce with Python. In this case I'm not really going to bother with any optimizations, as it is a pretty short key with simple rules, so it shouldn't cost too much computing time. It is probably also a bad idea to use random number generation here, as that is quite compute-intensive and leads to duplicate checks, but again it doesn't really matter in this case (and random keys look cooler).

import random
from string import ascii_letters

ascii_nums = [ord(x) for x in ascii_letters]


def gen_key():
	a = b = c = 0
	while a + b + c != 0x12c:
		a = random.choice(ascii_nums)
		b = random.choice(ascii_nums)
		c = random.choice(ascii_nums)

	return random.choice(ascii_letters) + '@' + chr(a) + chr(b) + chr(c)


print(f'KEY: {gen_key()}')

And we can see that this does indeed work! For shits and giggles, I let the script generate 3.000.000 keys, which gives us about 81.016 unique keys. I think the total number of valid keys should indeed be somewhere around here because:
* A bit less than 48^3 = 110.592 different combinations, seems about right.
* 3.000.000 is a lot of keys. The "rarest" key was generated 15 times, while the most common one was generated 70 times. It's probably fair to assume we got (almost) all of them.
* I'm too lazy to do the proper math, so this is good enough.

While this was a pretty simple challenge and probably not really worth writing a whole blog about, I had a lot of fun doing it (and I gotta procrastinate for my midterms somehow, right?)
And although it is completely overkill for this challenge, Binary Ninja is an awesome tool to work with! And doing this challenge has taught me more about how to use it efficiently, with for example switching between the different intermediate language cracking-a-views.

There is one other thing I found while writing this blog post that I think encompasses pretty well what working with Binary Ninja is like. After finishing the challenge, I moved the binary and the "project database" (the analysis data and comments I added) over to another folder. When I reopened the project, Binary Ninja couldn't find the binary anymore.
After looking around a bit in the documentation however, I found the original_filename property in the FileMetaData class. So I opened up the built-in Python console and entered bv.file.original_filename = '/home/sam/hackety_hack/reverse-engineering/glowwine/glowwine', and saved the modified database. Boom, problem solved!
It would have been nice if the program prompted me to enter the new binary path, but on the other hand it was quite easily fixed thanks to Binary Ninja's amazing Python API.

Mastodon