TSA Cyber - Ditto Challenges
In this article I’ll be discussing the TSA Cyber Champion
Competition, and the challenges I faced during the competition. especially the interesting RE challenges that I encountered.
Introduction
TSA Cyber Champion is a national competition held by Kominfo supported by CISCO
Chall Introduction
Given a x86_64 ELF binary, when we execute it, it will ask for the FLAG, and i think we need to input the correct flag to get the flag. If not the program will exit with some error message. So, we can start static analysis on the binary, and see what the program does.
__int64 __fastcall main(int a1, const char **a2, char **a3)
{
printf("Enter the flag: ");
__isoc99_scanf("%s", s);
if ( strlen(s) == 27 )
{
...
}
else
{
puts("Invalid flag length :(");
return 1LL;
}
}
We can see that the input length must be 27, and if it is not, the program will exit. Next, we need to see what the program does if the input length is correct.
if ( strlen(s) == 27 )
{
for ( i = 0; ; ++i )
{
v4 = i;
if ( v4 >= strlen(s) )
break;
(*(&funcs_179C + i))(s[i]);
}
return 0LL;
}
.data:00000000000080E0 funcs_179C dq offset sub_17C4 ; DATA XREF: main+96↑o
int __fastcall sub_17C4(char a1)
{
int result; // eax
char *v2; // rax
__int16 v3; // [rsp+16h] [rbp-Ah] BYREF
unsigned int v4; // [rsp+18h] [rbp-8h]
unsigned int v5; // [rsp+1Ch] [rbp-4h]
v3 = 28587;
v5 = 0;
v4 = 0;
while ( v5 <= 1 )
{
// do some operations
*(&v3 + v5++) = ++v4;
}
result = a1;
if ( v3 == a1 )
{
v2 = md5(&v3);
return sub_1414(".c4ca4238a0b923820dcc509a6f75849b", v2);
}
return result;
}
From the above, the program will iterate through the input string, and call the function at funcs_179C
and pointing to sub_17C4
with the current character as the first argument. The function at sub_17C4
will do some operation and check if the cur character is equal to the generated character. After that it will create md5 for the char. Next it will call the function at sub_1414
with the md5 hash as the first argument and the md5 hash of the current character as the second argument.
int __fastcall sub_1413(const char *a1, const char *md5_input_char)
{
int result; // eax
char *file_name; // rax
char v4; // r12
unsigned __int64 v5; // rbx
size_t v6; // rax
_DWORD v7[4]; // [rsp+10h] [rbp-E0h] BYREF
__int64 v8; // [rsp+20h] [rbp-D0h]
__off_t offset; // [rsp+28h] [rbp-C8h]
size_t size; // [rsp+30h] [rbp-C0h]
_BYTE buf[40]; // [rsp+50h] [rbp-A0h] BYREF
__off_t v12; // [rsp+78h] [rbp-78h]
unsigned __int16 v13; // [rsp+8Ch] [rbp-64h]
unsigned __int16 v14; // [rsp+8Eh] [rbp-62h]
void *addr; // [rsp+90h] [rbp-60h]
unsigned __int64 v16; // [rsp+98h] [rbp-58h]
size_t v17; // [rsp+A0h] [rbp-50h]
unsigned __int64 v18; // [rsp+A8h] [rbp-48h]
char *s1; // [rsp+B0h] [rbp-40h]
void *ptr; // [rsp+B8h] [rbp-38h]
__int64 v21; // [rsp+C0h] [rbp-30h]
int fd; // [rsp+C8h] [rbp-28h]
int j; // [rsp+CCh] [rbp-24h]
size_t len; // [rsp+D0h] [rbp-20h]
int i; // [rsp+DCh] [rbp-14h]
result = open(dest, 0);
fd = result;
if ( result != -1 )
{
file_name = __xpg_basename(dest);
v21 = sub_1356(file_name);
read(fd, buf, 0x40uLL);
lseek(fd, (v14 << 6) + v12, 0);
read(fd, v7, 0x40uLL);
ptr = malloc(size);
lseek(fd, offset, 0);
read(fd, ptr, size);
lseek(fd, v12, 0);
for ( i = 0; i < v13; ++i )
{
read(fd, v7, 0x40uLL);
s1 = ptr + v7[0];
if ( !strcmp(s1, a1) )
{
v18 = v8 + v21;
v17 = size;
v16 = sysconf(30);
addr = (v18 - v18 % v16);
len = v18 % v16 + v17;
if ( len % v16 )
len += v16 - len % v16;
mprotect(addr, len, 7);
for ( j = 0; j < v17; ++j )
{
v4 = *(j + v18);
v5 = j;
v6 = strlen(md5_input_char);
*(j + v18) = md5_input_char[v5 % v6] ^ v4;
}
mprotect(addr, len, 5);
break;
}
}
free(ptr);
return close(fd);
}
return result;
}
Using GPT this is the point of this function:
- Opens a file and processes its header.
- Searches for a specific string (a1) within the file data.
- If it finds the string, it calculates an address, changes memory permissions, and then XOR-encrypts the content at the address using the md5_input_char string.
- After transforming the data, it resets memory protections, frees memory, and closes the file.
After that, we still confuse with this binary does, so we need to dynamically analyze the binary. Here im using gdb
to debug this challenge.
Set breakpoint to the 0x0000000000001A12 cmp edx, eax
on sub_17C4
function, and we can see that the program will compare the generated character with the current character. If it is equal, it will generate the md5 hash of the generated character.
Try to debug the program with ni
then we can see that it return to the main function and pass to the next character on call rdx
instruction. But the next function address is different from the previous function address. It try to access on offset 0x1a37
Notice that the segment is the md5
hash of the previous parameter that used in the sub_1413
function. Remember that the summary of the sub_1413
function is to open a file, search for a specific string, and then XOR the content of the file with the md5 hash of the current character. So, we can conclude that the program will decrypt the segment in sub_1413
function.
Solution
The solution is to decrypt for each segment in the binary using the md5 hash of the correct character.
But the problem is i dont know how to reverse the correct character. So i try to make a solver using gdb
api to solve this challenge. Here is the solver that i made:
import gdb
def disass():
return gdb.execute('x/200i $rip', to_string=True)
global flag
flag = []
def find_and_break_on_cmp():
disassembled_code = disass()
rip = int(gdb.parse_and_eval('$rip'))
# Iterate over the disassembled instructions to find 'cmp edx,eax'
for line in disassembled_code.split('\n'):
if 'cmp edx,eax' in line:
# Extract the address from the line
parts = line.split()
address = int(parts[0].rstrip(':'), 16)
print(f'Breakpoint at {hex(address)} (cmp edx,eax) found')
gdb.execute(f'break *{hex(address)}')
gdb.execute('continue')
rdx = int(gdb.parse_and_eval('$rdx'))
gdb.execute(f'set $rax = {rdx}')
flag.append(chr(rdx))
print(gdb.parse_and_eval('$rax'))
print(''.join(flag))
# input('Continue?')
return
print('No cmp edx,eax instruction found in the current disassembly range.')
# Clean up
gdb.execute('file ./chall')
rawinput = 'TSA{' + 'A' * (27 - 4)
gdb.execute('break-rva 0x179C')
gdb.execute(f'run <<< {rawinput}')
gdb.execute('continue')
gdb.execute('si')
for i in range(26):
find_and_break_on_cmp() #S 1
gdb.execute('continue')
gdb.execute('si')
So, the solve will set the breakpoint on the cmp edx, eax
instruction, and set the rax == rdx
then append the character to the flag. After that, the program will continue to the next character and do the same thing until the flag is complete.
My solver kinda bad because of my skill-ish on gdb scripting, but it works and i got the flag thanks god alhamdulillah 😭🙏.