Reverse Engineering Loop Exercise
This writeup walks through a simple reverse engineering exercise from session 2 of Introduction to Reverse Engineering with Ghidra. We’re given a binary that expects an unknown key as a command line argument. We’ll use Ghidra to disassemble and decompile it, and then step through the code to figure out what the secret key is.
The binary for this exercise is loop-example-1
and can be downloaded here.
After we have imported the file into a Ghidra project and run the auto-analysis, we will find the program’s entry point (the main()
function) in the symbol tree on the left.
Clicking this will jump to where the main function begins in the listing pane in the middle, as well show the decompiled version of the C code in the decompiler pane on the right.
undefined8 main(int param_1,long param_2)
{
undefined8 uVar1;
size_t sVar2;
int local_14;
int local_10;
if (param_1 == 2) {
sVar2 = strlen(*(char **)(param_2 + 8));
if ((int)sVar2 == 0xf) {
local_10 = 0;
for (local_14 = 0; local_14 < 0xf; local_14 = local_14 + 1) {
if (('@' < *(char *)((long)local_14 + *(long *)(param_2 + 8))) &&
(*(char *)((long)local_14 + *(long *)(param_2 + 8)) < '[')) {
local_10 = local_10 + 1;
}
}
if (local_10 == 8) {
puts("Congratulations, access granted!\r");
}
else {
puts("Not quite what we\'re looking for ... maybe try again?\r");
}
uVar1 = 0;
}
else {
puts("Wrong length! Try again!\r");
uVar1 = 0xffffffff;
}
}
else {
puts("Please provide a string!\r");
uVar1 = 0xffffffff;
}
return uVar1;
}
Now that we’re looking at code, the first thing we’ll want to do is to right click on “main” in the decompile pane and edit the function’s signature. Since this is main()
we know it should be int main(int argc, char ** argv)
.
This will automatically update anywhere the code was referencing the generic param_1
and param_2
variables before, already improving the function’s readability slightly.
So the first thing we can see on lines 5-8 are a few local variables being declared on the stack. We’ll rename these later as we understand their purposes.
On line 10 we can see the program is expecting exactly 2 command line arguments when the program is executed by checking if (argc == 2)
. The first argument is always the name of the binary itself, and the 2nd will be the key we need in order to solve the challenge.
On line 11, the program calls strlen(argv[1])
to get the length of the key and stores that value in sVar2
. We can click on the variable and press the l
key to relabel it to something meaningful like key_length
.
Next it compares the key length to 0xf
which tells us the key must be 15 characters long. We can right click on the hex value and change it to the decimal representation instead.
key_length = strlen(argv[1]);
if ((int)key_length == 15) {
local_10 = 0;
for (local_14 = 0; local_14 < 0xf; local_14 = local_14 + 1) {
if (('@' < argv[1][local_14]) && (argv[1][local_14] < '[')) {
local_10 = local_10 + 1;
}
}
if (local_10 == 8) {
puts("Congratulations, access granted!\r");
}
else {
puts("Not quite what we\'re looking for ... maybe try again?\r");
}
iVar1 = 0;
}
The for loop inside that if statement is the main piece we need to reverse engineer in order to solve the challenge.
local_14
can be renamed to i
as it’s obviously just the counter variable the loop is using for control flow. It runs for each integer value less than 15, starting from 0. We know the key length is 15, so we can deduce that the program is likely iterating through all the characters in the key.
Inside the loop is a single if statement checking that the value of each character in the key is greater than '@'
and less than '['
. Characters are really just integers under the hood in C, so what this is really doing is comparing the ASCII value of argv[1][i]
against that of the @ and [ characters.
Checking an ASCII chart we see that between those characters are all the uppercase alphabetic characters A-Z.
If that condition is met, the value of local_10
is incremented by 1, so let’s relabel that to uppercase_count
.
Immediately after the for loop is another if statement that checks if exactly 8 uppercase characters were counted. If so, then the key is accepted!
The final version of the reversed C code looks like this:
int main(int argc,char **argv)
{
int iVar1;
size_t key_length;
int i;
int uppercase_count;
if (argc == 2) {
key_length = strlen(argv[1]);
if ((int)key_length == 15) {
uppercase_count = 0;
for (i = 0; i < 15; i = i + 1) {
if (('@' < argv[1][i]) && (argv[1][i] < '[')) {
uppercase_count = uppercase_count + 1;
}
}
if (uppercase_count == 8) {
puts("Congratulations, access granted!\r");
}
else {
puts("Not quite what we\'re looking for ... maybe try again?\r");
}
iVar1 = 0;
}
else {
puts("Wrong length! Try again!\r");
iVar1 = -1;
}
}
else {
puts("Please provide a string!\r");
iVar1 = -1;
}
return iVar1;
}
And it’s not too far off from the original source code.
As you can see, by stepping through the code line-by-line to understand exactly what the code is doing, we are able to piece it all together to crack the key.
As it turns out, there isn’t a specific key the program is checking for. Any key that is 15 characters long and contains 8 uppercase characters will be accepted.
┌──(brian㉿labtop)-[~/repos/hackaday-u/session-two/exercises]
└─$ ./loop-example-1 AAAABBBBccccddd
Congratulations, access granted!
┌──(brian㉿labtop)-[~/repos/hackaday-u/session-two/exercises]
└─$ ./loop-example-1 ccccdddAAAABBBB
Congratulations, access granted!
┌──(brian㉿labtop)-[~/repos/hackaday-u/session-two/exercises]
└─$ ./loop-example-1 tooshort
Wrong length! Try again!
┌──(brian㉿labtop)-[~/repos/hackaday-u/session-two/exercises]
└─$ ./loop-example-1 nouppercaseabcd
Not quite what we're looking for ... maybe try again?