THM: Reversing ELF
Intro⌗
Reversing ELF is different from most of the other TryHackMe challenges I’ve written about here. I’ve recently been exploring reverse engineering and malware analysis, and will probably start practicing more “crackme” style CTFs and posting writeups as I learn.
This challenge is a really basic introduction to reversing Linux programs (ELFs) made up of 6 different mini challenges. Tools we’ll use to solve these include strings
, ltrace
, and a software reverse engineering tool suite from the NSA known as Ghidra. These are meant to be beginner friendly challenges, although basic knowledge of programming and C is necessary. We won’t be writing any code here, but in the later challenges we’ll read through decompiled C code to solve them.
Crackme1⌗
This one isn’t much of a challenge. All we have to do is run the program to get the flag.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme1
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme1
flag{not_that_kind_of_elf}
Crackme2⌗
This program is expecting the password to be passed as an argument:
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme2
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme2
Usage: ./crackme2 password
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme2 test
Access denied.
We can run strings crackme2
to analyze the binary for any strings it contains. There is one that stands out as a possible password:
And with that our access is granted:
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme2 super_secret_password
Access granted.
flag{if_i_submit_this_flag_then_i_will_get_points}
Crackme3⌗
For this challenge we also need to find the correct password to pass as an argument to the program.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme3
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme3
Usage: ./crackme3 PASSWORD
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme3 asdfasdf
Come on, even my aunt Mildred got this one!
And similarly, we’ll start our analysis again by running strings crackme3
.
The program contains a base64 encoded string. We can decode it to reveal the correct password.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ echo ZjByX3kwdXJfNWVjMG5kX2xlNTVvbl91bmJhc2U2NF80bGxfN2gzXzdoMW5nNQ== | base64 -d
f0r_y0ur_5ec0nd_le55on_unbase64_4ll_7h3_7h1ng5
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme3 f0r_y0ur_5ec0nd_le55on_unbase64_4ll_7h3_7h1ng5
Correct password!
Crackem4⌗
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme4
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme4
Usage : ./crackme4 password
This time the string is hidden and we used strcmp
This time we’re given a hint that we’ll need to up our game. There won’t be anything useful for us in the strings dump.
There is a nifty utility called ltrace
we can use to intercept any calls to functions in other libraries. From the manpage:
ltrace
is a program that simply runs the specified command until it exits. It intercepts and records the dynamic library calls which are called by the executed process and the signals which are received by that process. It can also intercept and print the system calls executed by the program.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ltrace ./crackme4 testing
__libc_start_main(0x400716, 2, 0x7ffdbe8bead8, 0x400760 <unfinished ...>
strcmp("my_m0r3_secur3_pwd", "testing") = -7
printf("password "%s" not OK\n", "testing"password "testing" not OK
) = 26
+++ exited (status 0) +++
We can the program is taking our input “testing” and calling strcmp()
from the string.h
library to compare it to another string, which happens to be the expected password.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme4 my_m0r3_secur3_pwd
password OK
Crackme5⌗
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme5
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme5
Enter your input:
1234
Always dig deeper
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ltrace ./crackme5
__libc_start_main(0x400773, 1, 0x7ffc216424c8, 0x4008d0 <unfinished ...>
puts("Enter your input:"Enter your input:
) = 18
__isoc99_scanf(0x400966, 0x7ffc21642380, 0, 0x7fa8a13d0f331234
) = 1
strlen("1234") = 4
strlen("1234") = 4
strlen("1234") = 4
strlen("1234") = 4
strlen("1234") = 4
strncmp("1234", "OfdlDSA|3tXb32~X3tX@sX`4tXtz", 28) = -30
puts("Always dig deeper"Always dig deeper
) = 18
+++ exited (status 0) +++
Running ltrace
here we can see this binary is doing a similar string comparison between our input and the password.
However, this time the function strncmp()
is being used, which accepts the maxinum number of characters to compare as a 3rd parameter.
In this case, the first 28 characters are being compared (which happens to be the full string).
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme5
Enter your input:
OfdlDSA|3tXb32~X3tX@sX`4tXtz
Good game
Crackme6⌗
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme6
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme6
Usage : ./crackme6 password
Good luck, read the source
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ltrace ./crackme6 6rian
__libc_start_main(0x400711, 2, 0x7fffd08fb618, 0x400760 <unfinished ...>
printf("password "%s" not OK\n", "6rian"password "6rian" not OK
) = 24
+++ exited (status 0) +++
This time neither strings
nor ltrace
are helpful to us, but we are given a hint that we should read the source.
To do this we’ll use Ghidra to disassemble and decompile the program. Then we can read the C source code to look for the password.
This is not a Ghidra tutorial so I’ll just quickly outline the steps to get started:
- Start Ghidra
- Create a new project
- File -> Import File and choose
crackme6
- Double click
crackme6
or drag it over the dragon icon to launch the code browser - When prompted, let Ghidra analyze the file
Now we can begin our analysis. From the Symbol Tree on the left side of the screen, look for main
under the Functions folder. Clicking on this will open the code for the entrypoint of the application in the decompiler pane on the right.
undefined8 main(int param_1,undefined8 *param_2)
{
if (param_1 == 2) {
compare_pwd(param_2[1]);
}
else {
printf("Usage : %s password\nGood luck, read the source\n",*param_2);
}
return 0;
}
On line 6 we see a function called compare_pwd()
which is taking in the password from the command line argument as a parameter. Let’s take a deeper look to see exactly what it’s comparing. We can double click the function name to jump to its code.
void compare_pwd(undefined8 param_1)
{
int iVar1;
iVar1 = my_secure_test(param_1);
if (iVar1 == 0) {
puts("password OK");
}
else {
printf("password \"%s\" not OK\n",param_1);
}
return;
}
Okay, we’re a little closer. Looks like we need to inspect the my_secure_test()
function next.
undefined8 my_secure_test(char *param_1)
{
undefined8 uVar1;
if ((*param_1 == '\0') || (*param_1 != '1')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[1] == '\0') || (param_1[1] != '3')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[2] == '\0') || (param_1[2] != '3')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[3] == '\0') || (param_1[3] != '7')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[4] == '\0') || (param_1[4] != '_')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[5] == '\0') || (param_1[5] != 'p')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[6] == '\0') || (param_1[6] != 'w')) {
uVar1 = 0xffffffff;
}
else {
if ((param_1[7] == '\0') || (param_1[7] != 'd')) {
uVar1 = 0xffffffff;
}
else {
if (param_1[8] == '\0') {
uVar1 = 0;
}
else {
uVar1 = 0xffffffff;
}
}
}
}
}
}
}
}
}
return uVar1;
}
This function looks a bit daunting at first, but all it’s doing is iterating over each character of our input and comparing it one by one to the character in the same position in the password. We can simply concatenate all the characters (keeping them in order) to get the full password.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme6 1337_pwd
password OK
Crackme7⌗
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ chmod +x crackme7
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme7
Menu:
[1] Say hello
[2] Add numbers
[3] Quit
[>] 1
What is your name? 6rian
Hello, 6rian!
Menu:
[1] Say hello
[2] Add numbers
[3] Quit
[>] 2
Enter first number: 1
Enter second number: 1
1 + 1 = 2
Menu:
[1] Say hello
[2] Add numbers
[3] Quit
[>] 3
Goodbye!
In this program we’re presented with a couple of options in a menu. It’s not immediately obvious what we’re looking for, so let’s go ahead and analyze it with Ghidra. As always we’ll look for the main()
function to begin our analysis.
undefined4 main(void)
{
int iVar1;
undefined4 *puVar2;
byte bVar3;
undefined4 local_80 [25];
int local_1c;
int local_18;
int local_14;
undefined *local_10;
bVar3 = 0;
local_10 = &stack0x00000004;
while( true ) {
while( true ) {
puts("Menu:\n\n[1] Say hello\n[2] Add numbers\n[3] Quit");
printf("\n[>] ");
iVar1 = __isoc99_scanf(&DAT_08048814,&local_14);
if (iVar1 != 1) {
puts("Unknown input!");
return 1;
}
if (local_14 != 1) break;
printf("What is your name? ");
puVar2 = local_80;
for (iVar1 = 0x19; iVar1 != 0; iVar1 = iVar1 + -1) {
*puVar2 = 0;
puVar2 = puVar2 + (uint)bVar3 * -2 + 1;
}
iVar1 = __isoc99_scanf(&DAT_0804883a,local_80);
if (iVar1 != 1) {
puts("Unable to read name!");
return 1;
}
printf("Hello, %s!\n",local_80);
}
if (local_14 != 2) {
if (local_14 == 3) {
puts("Goodbye!");
}
else {
if (local_14 == 0x7a69) {
puts("Wow such h4x0r!");
giveFlag();
}
else {
printf("Unknown choice: %d\n",local_14);
}
}
return 0;
}
printf("Enter first number: ");
iVar1 = __isoc99_scanf(&DAT_08048875,&local_18);
if (iVar1 != 1) break;
printf("Enter second number: ");
iVar1 = __isoc99_scanf(&DAT_08048875,&local_1c);
if (iVar1 != 1) {
puts("Unable to read number!");
return 1;
}
printf("%d + %d = %d\n",local_18,local_1c,local_18 + local_1c);
}
puts("Unable to read number!");
return 1;
}
There’s a lot to look at here but immediately the giveFlag()
call on line 46 stands out!
Just above that we can see that the variable local_14
is being compared to the value 0x7a69
.
Further up on line 20 there is a scanf()
call which is what takes our menu selection and stores it in local_14
. So now all we need to do is convert 0x7a60
from hex to decimal to determine the secret menu option.
Ghidra can do this for us, too. Just right click on the value decimal and other notations.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme7
Menu:
[1] Say hello
[2] Add numbers
[3] Quit
[>] 31337
Wow such h4x0r!
flag{much_reversing_very_ida_wow}
Crackme8⌗
Last one! Using Ghidra again we can examine main()
to start.
undefined4 main(int param_1,undefined4 *param_2)
{
undefined4 uVar1;
int iVar2;
if (param_1 == 2) {
iVar2 = atoi((char *)param_2[1]);
if (iVar2 == -0x35010ff3) {
puts("Access granted.");
giveFlag();
uVar1 = 0;
}
else {
puts("Access denied.");
uVar1 = 1;
}
}
else {
printf("Usage: %s password\n",*param_2);
uVar1 = 1;
}
return uVar1;
}
Because this is main()
we know that param_1
is our argc
(argument count), and param_2
is argv
(list of arguments). The first argument is the name of our program itself, and param_2[1]
is the password input.
So we can make relabel a few things to make this easier to read.
int main(int argc, char *argv)
{
int inputAsInteger;
if (argc == 2) {
inputAsInteger = atoi(*(char **)(argv + 4));
if (inputAsInteger == -0x35010ff3) {
puts("Access granted.");
giveFlag();
inputAsInteger = 0;
}
else {
puts("Access denied.");
inputAsInteger = 1;
}
}
else {
printf("Usage: %s password\n",*(undefined4 *)argv);
inputAsInteger = 1;
}
return inputAsInteger;
}
On line 8 our password input is being run through atoi()
which is a C standard library function that takes a string as input and converts it to an integer.
On line 9 it is being compared to -0x35010ff3
and if equal, the flag is given. So we just need to convert that to a decimal and we’ll have our password.
┌──(brian㉿kali)-[/tmp/ReverseELF]
└─$ ./crackme8 -889262067
Access granted.
flag{at_least_this_cafe_wont_leak_your_credit_card_numbers}