To solve this challenge I’ve used one of the most famous tool to do reverse engineering Ghidra. For sure there are more ways to solve this challenge, expecially with dynamic analysis. I will solve this challenge with static analysis. First I’ll show how to reverse the algorithm with Ghidra and then I’ll rewrite the algorithm in python.
Initial Analysis #
First of all we need to download the binary from the htb page and unzip it. Then to undestand what we are dealing with we can use the command file
to check the type of the file.
┌──(marcocampione㉿kali)-[~/Desktop/CTF_files/HTB_files]
└─$ file impossible_password.bin
impossible_password.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ba116ba1912a8c3779ddeb579404e2fdf34b1568, stripped
We obtained some information about the file, it’s a 64-bit Linux ELF and it’s stripped. That means that it does not contain any debug information.
Now let’s run the file and see what happens.
┌──(marcocampione㉿kali)-[~/Desktop/CTF_files/HTB_files]
└─$ ./impossible_password.bin
* flag
[flag]
As we can see the program ask us to enter something after the *
. If we enter something we get the same string in square brackets. So we can assume that the program is checking if the string we entered is the same as the one the program is looking for.
We can also use the command strings
to see if there are some interesting strings in the file. We can often find some very interesting strings in there, like flags, passwords and other hints.
┌──(marcocampione㉿kali)-[~/Desktop/CTF_files/HTB_files]
└─$ strings impossible_password.bin
/lib64/ld-linux-x86-64.so.2
libc.so.6
exit
srand
__isoc99_scanf
time
putchar
printf
malloc
strcmp
__libc_start_main
__gmon_start__
GLIBC_2.7
GLIBC_2.2.5
UH-x
UH-x
=1
[]A\A]A^A_
SuperSeKretKey
%20s
[%s]
;*3$"
GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-11)
.shstrtab
.interp
.note.ABI-tag
.note.gnu.build-id
.gnu.hash
.dynsym
.dynstr
.gnu.version
.gnu.version_r
.rela.dyn
.rela.plt
.init
.text
.fini
.rodata
.eh_frame_hdr
.eh_frame
.init_array
.fini_array
.jcr
.dynamic
.got
.got.plt
.data
.bss
.comment
We can see that there is a string called SuperSeKretKey
that could be the password we are looking for. Let’s try it.
┌──(marcocampione㉿kali)-[~/Desktop/CTF_files/HTB_files]
└─$ ./impossible_password.bin
* SuperSeKretKey
[SuperSeKretKey]
** SuperSeKretKey
We are going in the right direction, but we are still not there. Indeed the program is asking us to enter another password, but the one we found is not working anymore.
Now let’s try to use ltrace
to see what the program is doing.
┌──(marcocampione㉿kali)-[~/Desktop/CTF_files/HTB_files]
└─$ ltrace ./impossible_password.bin
__libc_start_main(0x40085d, 1, 0x7ffcf0807168, 0x4009e0 <unfinished ...>
printf("* ") = 2
__isoc99_scanf(0x400a82, 0x7ffcf0807030, 0, 0* SuperSeKretKey
) = 1
printf("[%s]\n", "SuperSeKretKey"[SuperSeKretKey]
) = 17
strcmp("SuperSeKretKey", "SuperSeKretKey") = 0
printf("** ") = 3
__isoc99_scanf(0x400a82, 0x7ffcf0807030, 0, 0** SuperSekretkey
) = 1
time(0) = 1679079209
srand(0xd31bf645, 10, 0xd19e4f34, 0) = 1
malloc(21) = 0xe02ac0
rand(0xe02ac0, 21, 0, 0xe02ac0) = 0x4112912a
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac0, 94) = 0x2f546ca
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac1, 94) = 0x7f2af784
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac2, 94) = 0x627744bc
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac3, 94) = 0x9e97b38
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac4, 94) = 0x786022e7
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac5, 94) = 0x7172dc2f
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac6, 94) = 0x130120df
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac7, 94) = 0x17b9c2fc
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac8, 94) = 0x41cb3ec
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ac9, 94) = 0x25ea9fd9
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02aca, 94) = 0x184d9bd9
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02acb, 94) = 0x6ce9f739
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02acc, 94) = 0x70f068de
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02acd, 94) = 0x572caba3
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ace, 94) = 0x5ac296ec
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02acf, 94) = 0x43bca72e
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ad0, 94) = 0x1ef74ee2
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ad1, 94) = 0x60f7f821
rand(0x7fbdb4c08840, 0x7ffcf0806f94, 0xe02ad2, 94) = 0x65470193
strcmp("SuperSekretkey", "1go'E~t,yKxb4yd)ei*$") = 34
+++ exited (status 34) +++
__isoc99_scanf
is reading or input, then printf
is printing out the output.
printf("[%s]\n", "SuperSeKretKey"[SuperSeKretKey]
and then compare our input with the expected string SuperSeKretKey
using strcmp
.
strcmp("SuperSeKretKey", "SuperSeKretKey")
Since the two strings are equal we ere able to pass the first check. Now let’s see what happens when we enter for the second time the same password.
strcmp("SuperSekretkey", "1go'E~t,yKxb4yd)ei*$")
We can see that the program is comparing our input with a string that is not the same as the one we entered before. This is because the program is generating a random string using rand
and then compare it with our input. So the second password seems to be dynamically generated. Let’s try to understand how reversing the binary with ghidra.
Reversing with Ghidra #
Let’s open the binary with ghidra and see what we can find.
Main Function #
First we need to find the main function. We can do this by searching for the __libc_start_main
in the Symbol Tree.
As we can see in the pic the main function is called FUN_0040085d
. We can edit the name of the function by right clicking on it and then selecting Rename Function
. Let’s rename it to main
. Now let’s see what the main function is doing.
void main(void)
{
int iVar1;
char *__s2;
undefined local_48;
undefined local_47;
undefined local_46;
undefined local_45;
undefined local_44;
undefined local_43;
undefined local_42;
undefined local_41;
undefined local_40;
undefined local_3f;
undefined local_3e;
undefined local_3d;
undefined local_3c;
undefined local_3b;
undefined local_3a;
undefined local_39;
undefined local_38;
undefined local_37;
undefined local_36;
undefined local_35;
char local_28 [20];
int local_14;
char *local_10;
local_10 = "SuperSeKretKey";
local_48 = 0x41;
local_47 = 0x5d;
local_46 = 0x4b;
local_45 = 0x72;
local_44 = 0x3d;
local_43 = 0x39;
local_42 = 0x6b;
local_41 = 0x30;
local_40 = 0x3d;
local_3f = 0x30;
local_3e = 0x6f;
local_3d = 0x30;
local_3c = 0x3b;
local_3b = 0x6b;
local_3a = 0x31;
local_39 = 0x3f;
local_38 = 0x6b;
local_37 = 0x38;
local_36 = 0x31;
local_35 = 0x74;
printf("* ");
__isoc99_scanf(&DAT_00400a82,local_28);
printf("[%s]\n",local_28);
local_14 = strcmp(local_28,local_10);
if (local_14 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
printf("** ");
__isoc99_scanf(&DAT_00400a82,local_28);
__s2 = (char *)FUN_0040078d(0x14);
iVar1 = strcmp(local_28,__s2);
if (iVar1 == 0) {
FUN_00400978(&local_48);
}
return;
}
We can start to rename some the variables to better understand what the code is doing. We will obtain something like this.
void main(void)
{
int did_psw_match_2;
char *random_psw;
undefined local_48;
undefined local_47;
undefined local_46;
undefined local_45;
undefined local_44;
undefined local_43;
undefined local_42;
undefined local_41;
undefined local_40;
undefined local_3f;
undefined local_3e;
undefined local_3d;
undefined local_3c;
undefined local_3b;
undefined local_3a;
undefined local_39;
undefined local_38;
undefined local_37;
undefined local_36;
undefined local_35;
char user_input_psw [20];
int did_psw_match_1;
char *psw_lvl_1;
psw_lvl_1 = "SuperSeKretKey";
local_48 = 'A';
local_47 = ']';
local_46 = 'K';
local_45 = 'r';
local_44 = '=';
local_43 = '9';
local_42 = 'k';
local_41 = '0';
local_40 = '=';
local_3f = '0';
local_3e = 'o';
local_3d = '0';
local_3c = ';';
local_3b = 'k';
local_3a = '1';
local_39 = '?';
local_38 = 'k';
local_37 = '8';
local_36 = '1';
local_35 = 't';
printf("* ");
__isoc99_scanf("%20s",user_input_psw);
printf("[%s]\n",user_input_psw);
did_psw_match_1 = strcmp(user_input_psw,psw_lvl_1);
if (did_psw_match_1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
printf("** ");
__isoc99_scanf("%20s",user_input_psw);
random_psw = (char *)generate_psw(0x14);
did_psw_match_2 = strcmp(user_input_psw,random_psw);
if (did_psw_match_2 == 0) {
print_flag(&local_48);
}
return;
}
Some observations:
local_48
is used as an argument in theprint_flag
function.local_35
tolocal_48
contain single characters.
Assumpions:
local_35
tolocal_48
is containing our encrypted flag.- They are not individual variables. Since we are passing
local_48
as an argument to theprint_flag
function. It means thatlocal_48
is the a pointer / array.
Let’s re-type the local_48
variable. it is a byte array not only a byte.
Now the code will look like this.
void main(void)
{
int did_psw_match_2;
char *random_psw;
byte encrypted_flag [20];
char user_input_psw [20];
int did_psw_match_1;
char *psw_lvl_1;
psw_lvl_1 = "SuperSeKretKey";
encrypted_flag[0] = 'A';
encrypted_flag[1] = ']';
encrypted_flag[2] = 'K';
encrypted_flag[3] = 'r';
encrypted_flag[4] = '=';
encrypted_flag[5] = '9';
encrypted_flag[6] = 'k';
encrypted_flag[7] = '0';
encrypted_flag[8] = '=';
encrypted_flag[9] = '0';
encrypted_flag[10] = 'o';
encrypted_flag[11] = '0';
encrypted_flag[12] = ';';
encrypted_flag[13] = 'k';
encrypted_flag[14] = '1';
encrypted_flag[15] = '?';
encrypted_flag[16] = 'k';
encrypted_flag[17] = '8';
encrypted_flag[18] = '1';
encrypted_flag[19] = 't';
printf("* ");
__isoc99_scanf("%20s",user_input_psw);
printf("[%s]\n",user_input_psw);
did_psw_match_1 = strcmp(user_input_psw,psw_lvl_1);
if (did_psw_match_1 != 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
printf("** ");
__isoc99_scanf("%20s",user_input_psw);
random_psw = (char *)generate_psw(0x14);
did_psw_match_2 = strcmp(user_input_psw,random_psw);
if (did_psw_match_2 == 0) {
print_flag(encrypted_flag);
}
return;
}
Print Flag Function #
Now let’s look at the print_flag
function.
After renaming the variables we will get something like this.
void print_flag(byte *param_1)
{
int i;
byte *encrypted_flag;
i = 0;
encrypted_flag = param_1;
while ((*encrypted_flag != 9 && (i < 20))) {
putchar((int)(char)(*encrypted_flag ^ 9));
encrypted_flag = encrypted_flag + 1;
i = i + 1;
}
putchar(10);
return;
}
The program is looping over our encrypted flag. The two conditions to exit the loop are:
*encrypted_flag != 9
,9
is the ASCII code for the tab character.i < 20
Then we call putchar
and Xoring the current byte with 9
. After that increasing encrypted_flag
with 1 and increasing i
with 1.
Solving with Python #
Now that we have a better understanding of the program. Let’s write a python script to solve it.
#!/usr/bin/env python3
flag_characters = ['A',']','K','r','=','9','k','0','=','0','o','0',';','k','1','?','k','8','1','t']
xor_key = 9
flag = []
i = 0
while i < len(flag_characters):
xored = ord(flag_characters[i]) ^ xor_key
flag.append(chr(xored))
i += 1
flag_string = "".join(flag)
print "Flag is: {}".format(flag_string)
Solving with CyberChef #
We can also solve this challenge using CyberChef. The recipe is as follows: