Reverse Engineering Training
Inroduction
Cyber Talents - training reverse engineering ahram canadian competition.
Description
x64 stripped linux binary file given file called training with description of “ training, keep training.” Reverse it to get the flag
File
For downloading , Visit training .
Solution
The file is a x64 stripped linux binary At running, it reads from stdin and just prints back what I write So let’s load it to IDA I also used HexRays decompiler to get the pseudo-code of the functions For the main function we have
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
_QWORD *v3; // rax
char v4; // bl
__int64 v5; // rax
char v7; // [rsp+Fh] [rbp-A1h]
char v8; // [rsp+10h] [rbp-A0h]
char v9; // [rsp+30h] [rbp-80h]
char v10; // [rsp+50h] [rbp-60h]
char v11; // [rsp+70h] [rbp-40h]
unsigned __int64 v12; // [rsp+98h] [rbp-18h]
v12 = __readfsqword(0x28u);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v8, a2, a3);
sub_130A();
while ( 1 )
{
v3 = std::operator>><char,std::char_traits<char>,std::allocator<char>>(&std::cin, &v8);
if ( !std::basic_ios<char,std::char_traits<char>>::operator bool(v3 + *(*v3 - 24LL)) )
break;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v9, &v8);
sub_13DB(&v10, &v9);
v4 = 0;
if ( sub_1A6C(&v10, &unk_2046A0) )
{
std::allocator<char>::allocator(&v7);
v4 = 1;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v11, "correct", &v7);
}
else
{
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&v11, &v8);
}
v5 = std::operator<<<char,std::char_traits<char>,std::allocator<char>>(&std::cout, &v11);
std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v11);
if ( v4 )
std::allocator<char>::~allocator(&v7);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v10);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v9);
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v8);
return 0LL;
}
Here we have a C++ program, so we have other functions to allocate memory and copy data For now we can understand
1
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string
to be a data copy mechanism that copies the data from the second parameter to the first one And
1
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string
to be a memory free mechanism First let me explain the code line by line
- First it defines the different used variables and their types
- Assigns
v12
to be a 8-byte (qword) address at offset 0x28 from the register segment FS (not important for us) a2
anda3
are the parameters of the main function so this should be a default operation (not important for us)- Executes the function
sub_130A
- Initiates an infinity loop (can be broken from inside)
- Copies the address of pointer
v8
to the address of the stdin (now any input data will be at pointerv8
) - Checks if the previous operation returned properly if not it will break (not important for us)
- Copies the address of
v8
to the address ofv9
(now any input data will be at pointerv9
) - Executes sub_13DB with two pointers
v10
andv9
(our input data) - Assigns
v4
to be 0 - Executes
sub_1A6C
with two pointer&v10
and unknown pointer at 0x2046A0 and check for the return - If returned a non-zero value it allocates some memory and gives its address to pointer
v7
and assignsv4
to 1 - Copies the string
correct
to memory ofv11
- If returned zero, it copies data from
v8
(our input data) to pointerv11
- Copies address of
v11
(correct
or our input data) to be the address of stdout (prints&v11
) - Prints new line
- Free memory of
v11
- Free memory of
v7
ifv4
is non-zero (or when the previous condition is true) - Free memory of
v9
,v10
,v8
, and return
So simply it will read our input, make some check on it, if it passed it will print ‘correct’ if not it will print our input For the checking function sub_1A6C
we have
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_BOOL8 __fastcall sub_1A6C(__int64 a1, __int64 a2)
{
__int64 v2; // rbx
__int64 v3; // r12
__int64 v4; // rbx
__int64 v5; // rax
_BOOL8 result; // rax
v2 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a1);
result = 0;
if ( v2 == std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a2) )
{
v3 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::size(a1);
v4 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::data(a2);
v5 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::data(a1);
if ( !sub_18BF(v5, v4, v3) )
result = 1;
}
return result;
}
And for sub_18BF
1
2
3
4
5
6
7
8
9
10
int __fastcall sub_18BF(const void *a1, const void *a2, size_t a3)
{
int result; // eax
if ( a3 )
result = memcmp(a1, a2, a3);
else
result = 0;
return result;
}
Which seems to be a simple comparison function that makes sure the data and the size of the two pointers are the same So now we need to know the data at the pointer unk_2046A0
, but when I tried to dump it it was not initialised Pointer unk_2046A0
will be initialised at the runtime by some function To know where it will be filled with data in ida you can jump to its x-refernces (right click–>jump to xref) To find that it will be initialised by the function sub_178E
at which we have
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
unsigned __int64 __fastcall sub_178E(int a1, int a2)
{
char v3; // [rsp+17h] [rbp-19h]
unsigned __int64 v4; // [rsp+18h] [rbp-18h]
v4 = __readfsqword(0x28u);
if ( a1 == 1 && a2 == 0xFFFF )
{
std::ios_base::Init::Init(&unk_204680);
__cxa_atexit(&std::ios_base::Init::~Init, &unk_204680, &off_204008);
std::allocator<char>::allocator(&v3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
&unk_2046A0,
"IQHR}nxio_vtvk_aapbijsr_vnxwbbmm{",
&v3);
std::allocator<char>::~allocator(&v3);
__cxa_atexit(
&std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string,
&unk_2046A0,
&off_204008);
sub_19B4(&unk_204260);
__cxa_atexit(sub_261E, &unk_204260, &off_204008);
}
return __readfsqword(0x28u) ^ v4;
}
From the code we know that it will copy the string IQHR}nxio_vtvk_aapbijsr_vnxwbbmm{
to our unknown pointer Now I am pretty sure that the function sub_13DB
is the encryption function that takes two pointers &v10
and &v9
(our input) and it will encrypt our input and copy the result to &v10
to be checked again by sub_1A6C
Now for sub_13DB
we have
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
__int64 __fastcall sub_13DB(__int64 a1, __int64 a2)
{
int v2; // ebx
char *v3; // rax
signed int v4; // eax
char *v5; // rax
char *v6; // rax
__int64 v7; // rbx
int i; // [rsp+14h] [rbp-1Ch]
int v10; // [rsp+18h] [rbp-18h]
for ( i = 0; *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i); ++i )
{
v2 = *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
v10 = v2 + *sub_1A4C(&unk_204260, i);
v3 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
if ( sub_1929(*v3) )
{
v4 = 122;
}
else
{
v5 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
if ( sub_194C(*v5) )
v4 = 90;
else
v4 = *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
}
while ( v10 > v4 )
v10 -= 26;
if ( *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i) == 123 )
{
LOBYTE(v7) = 125;
}
else if ( *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i) == 125 )
{
LOBYTE(v7) = 123;
}
else
{
v6 = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
if ( sub_18FA(*v6) )
LOBYTE(v7) = v10;
else
v7 = *std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i);
}
*std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i) = v7;
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(a1, a2);
return a1;
}
You can understand :
1
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator[](a2, i)
to be just (&a2+i)
which is a2[i]
For sub_1A4C
, sub_1929
, sub_194C
, sub_18FA
, they are just small checking functions
Also we have this pointer unk_204260
which is also not initialised, jump to its x-references to find out the function that will fill it This function is sub_130A
which is hard to be reversed so we will debug (source code debugging) it to get the data at this pointer I set a break point at the line after the line it used in I used ida x64 remote linux debugger server on ubuntu x64 Dumped the data from it with GetManyBytes
idapython api function The dumped data was
1
['\x03\x00\x00\x00\x05\x00\x00\x00\x07\x00\x00\x00\x0b\x00\x00\x00\r\x00\x00\x00\x11\x00\x00\x00\x13\x00\x00\x00\x17\x00\x00\x00\x1d\x00\x00\x00\x1f\x00\x00\x00%\x00\x00\x00)\x00\x00\x00+\x00\x00\x00/\x00\x00\x005\x00\x00\x00;\x00\x00\x00=\x00\x00\x00C\x00\x00\x00G\x00\x00\x00I\x00\x00\x00O\x00\x00\x00S\x00\x00\x00Y\x00\x00\x00a\x00\x00\x00e\x00\x00\x00g\x00\x00\x00k\x00\x00\x00m\x00\x00\x00q\x00\x00\x00\x7f\x00\x00\x00\x83\x00\x00\x00\x89\x00\x00\x00\x8b\x00\x00\x00\x95\x00\x00\x00\x97\x00\x00\x00\x9d\x00\x00\x00\xa3\x00\x00\x00\xa7\x00\x00\x00\xad\x00\x00\x00\xb3\x00\x00\x00\xb5\x00\x00\x00\xbf\x00\x00\x00\xc1\x00\x00\x00\xc5\x00\x00\x00\xc7\x00\x00\x00\xd3\x00\x00\x00\xdf\x00\x00\x00\xe3\x00\x00\x00\xe5\x00\x00\x00\xe9\x00\x00\x00\xef\x00\x00\x00\xf1\x00\x00\x00\xfb\x00\x00\x00']
the fact that these bytes will be casted to be int32 and every int32 is 4 bytes, also we know that the bytes are in little-endian format so a bytes array like '\x03\x00\x00\x00'
is just 0x03 and so on So for now we have our key array to be
1
[0x03,0x05,0x07,0x0b,0x0d,0x11,0x13,0x17,0x1D,0x1F,0x25,0x29,0x2B,0x2F,0x35,0x3B,0x3D,0x43,0x47,0x49,0x4F,0x53,0x59,0x61,0x65,0x67,0x6B,0x6D,0x71,0x7F,0x83,0x89,0x8B,0x95,0x97,0x9D,0x0A3,0xA7,0xAD,0xB3,0xB5,0xbf,0xc1,0xc5,0xc7,0xd3,0xdf,0xe3,0xe5,0xe9,0xef,0xf1,0xfb]
So for this encryption function we have the output which is IQHR}nxio_vtvk_aapbijsr_vnxwbbmm{
and the key This is a kind of a rotation encryption function so I assumed that I will got the right flag if just passed the output as input again and so on to get the write flag Also I rewrited it in python so as we can decrypt our string I made this script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
key = [0x03,0x05,0x07,0x0b,0x0d,0x11,0x13,0x17,0x1D,0x1F,0x25,0x29,0x2B,0x2F,0x35,0x3B,0x3D,0x43,0x47,0x49,0x4F,0x53,0x59,0x61,0x65,0x67,0x6B,0x6D,0x71,0x7F,0x83,0x89,0x8B,0x95,0x97,0x9D,0x0A3,0xA7,0xAD,0xB3,0xB5,0xbf,0xc1,0xc5,0xc7,0xd3,0xdf,0xe3,0xe5,0xe9,0xef,0xf1,0xfb]
def enc(inp):
inp = list(inp)
for i in range(len(inp)):
current_char = ord(inp[i])
v17 = ord(inp[i]) + key[i]
if current_char > 96 and current_char <= 122:
v6 = 122
else:
if (current_char > 96 and current_char <= 122 or current_char > 64 and current_char <= 90) and not (current_char > 96 and current_char <= 122):
v6 = 90
else:
v6 = current_char
while v17 > v6: v17 -= 26
if current_char == 123:
v12 = 125
else:
if current_char == 125:
v12 = 123
else:
if current_char > 96 and current_char <= 122 or current_char > 64 and current_char <= 90:
v12 = v17
else:
v12 = current_char
inp[i] = chr(v12)
return ''.join(inp)
flag="IQHR}nxio_vtvk_aapbijsr_vnxwbbmm{"
while True:
if "FLAG" in flag:
print(flag)
break
flag = enc(flag)
` Flag is Captured ` » {well_keep_training_yourself}