## Rexy (Reverse, 53 solves)#

Solved by fsharp

A Linux program and an encrypted flag are provided. Opening the program in Ghidra, we find that it reads the plaintext flag from a file before passing it into an encryption function that involves randomness from rand(), buffers that get allocated only to be freed up without being used, and base conversions.

Looking closer, some parts of this function are actually redundant and only exist to confuse us. The actual encryption algorithm is quite simple and can be written as:

def encrypt(pt):
ct = ""
pos = 0
for c in pt:
pos += 1
pos_squared = (pos ** 2) ^ 0x19
xord_c = ord(c) ^ pos_squared ^ 0x69b2 ^ 0x11f0b8
based_c = format(int(format(xord_c, 'o')), 'X')
ct += based_c
return ct


We write the decryption algorithm and get the flag:

def decrypt(ct):
pt = ""
pos = 0
for i in range(0, len(ct), 6):
pos += 1
pos_squared = (pos ** 2) ^ 0x19
based_c = ct[i : i + 6]
xord_c = int(str(int(based_c, 16)), 8)
c = chr(xord_c ^ pos_squared ^ 0x69b2 ^ 0x11f0b8)
pt += c
return pt

flag = decrypt(encrypted)
print(flag)


The plaintext is:

Congratulation! You got the flag:
+ASIS{W00twootW00t_HappyNewYear}+
Have fun and good luck new year!!


## Deserve (Reverse, 32 solves)#

Solved by harrier, grhkm

We are given an ARM binary that takes input from stdin and outputs an encoded message. By disassembling the binary, we see that the main function is located at 0xBE0. We decided to perform static analysis first, which in hindsight is not the best idea, but we will see why later.

The code can be splitted into two parts.

Here is the first part:
__int64 __fastcall sub_BE0(_BYTE *a1, unsigned __int64 *input_len) {
// ...
input = a1;
v3 = *input_len;
v5 = malloc(4 * input_len);
if ( !v5 )
sub_F60("malloc");
if ( v3 ) {
v6 = 0LL;
i = 0LL;
b_loc_table = (__int64 *)__ctype_b_loc();
v9 = 0;
do
{
cur_char = (unsigned __int8)input[i];
v3 = v6;
v11 = *b_loc_table;

// Check 1
if ( (*b_loc_table + 2LL * cur_char) & 8 )
{
++v9;
*(_BYTE *)(v5 + v6) = cur_char;
v6 = v9;
v3 = v9;
}

// Check 2
else if ( cur_char == 32 )
{
if ( v9 > 1
&& (v11 + 2LL * *(unsigned __int8 *)(v5 + v6 - 1)) & 0x200
&& (v5 + v6 - 2) == '+' )
{
v39 = *(unsigned __int8 *)(v5 + v6 - 1);
*(_BYTE *)(v5 + v6 - 1) = *(_DWORD *)(*(_QWORD *)__ctype_toupper_loc() + 4 * v39);
}
else
{
++v9;
*(_BYTE *)(v5 + v6) = '/';
v6 = v9;
v3 = v9;
}
}

else
{
if ( !input[i] )
goto LABEL_9;

// Check 3
v33 = strchr("@$_!\"#%&'()*+,-./:;<=>?\n", (unsigned __int8)input[i]); if ( v33 ) { v34 = v9 + 1; v9 += 2; *(_BYTE *)(v5 + v6) = '+'; v6 = v9; v3 = v9; *(_BYTE *)(v5 + v34) = v33 - (unsigned __int64)"@$_!\"#%&'()*+,-./:;<=>?\n" + 0x61;
}

else
{
// Check 4
v35 = strchr("[\\]^{|}~\t", v10);
if ( !v35 )
LABEL_9:
sub_F30("Invalid input! Sorry!!", v11);
v36 = v9 + 1;
v37 = v9 + 2;
v9 += 3;
*(_BYTE *)(v5 + v6) = '+';
v6 = v9;
*(_BYTE *)(v5 + v36) = '+';
v3 = v9;
*(_BYTE *)(v5 + v37) = v35 - (unsigned __int64)"[\\]^{|}~\t" + 0x61;
}
}
++i;
}
while ( i < len );
// ...
}


As annotated, there are multiple checks. The binary first checks (*b_loc_table + 2LL * cur_char) & 8, where b_loc_table is the returned pointer from __ctype_b_loc(). Looking into C header files, we see that the function __ctype_b_loc is defined inside ctype.h, where it has the following comment:

/*
These point into arrays of 384, so they can be indexed by any unsigned
char' value [0,255]; by EOF (-1); or by any signed char' value
[-128,-1).  ISO C requires that the ctype functions work for unsigned
char' values and for EOF; we also support negative signed char' values
for broken old programs.  The case conversion arrays are of int's
rather than unsigned char's because tolower (EOF) must be EOF, which
doesn't fit into an unsigned char'.  But today more important is that
the arrays are also used for multi-byte character sets.
*/

extern const unsigned short int **__ctype_b_loc (void) __THROW __attribute__ ((__const__));

#define __isctype(c, type) ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
#define isalnum(c)     __isctype((c), _ISalnum)

// LITTLE ENDIAN
#define _ISbit(bit) ((bit) < 8 ? ((1 << (bit)) << 8) : ((1 << (bit)) >> 8))

enum
{
_ISupper = _ISbit (0),        /* UPPERCASE.  */
_ISlower = _ISbit (1),        /* lowercase.  */
_ISalpha = _ISbit (2),        /* Alphabetic.  */
_ISdigit = _ISbit (3),        /* Numeric.  */
_ISxdigit = _ISbit (4),       /* Hexadecimal numeric.  */
_ISspace = _ISbit (5),        /* Whitespace.  */
_ISprint = _ISbit (6),        /* Printing.  */
_ISgraph = _ISbit (7),        /* Graphical.  */
_ISblank = _ISbit (8),        /* Blank (usually SPC and TAB).  */
_IScntrl = _ISbit (9),        /* Control character.  */
_ISpunct = _ISbit (10),       /* Punctuation.  */
_ISalnum = _ISbit (11)        /* Alphanumeric.  */
};


In short, its just a “character characteristic” lookup table, a C way to check isalpha / etc.

The above code is equivalent to something like this in pseudocode:

storage = ""
for all char c in input:
if c is alphabetic:
storage += c
else if c is space:
if previous two in storage is ~=+[a-zA-Z0-9]:
update the previous two to become upper case
else:
storage += "/"
else:
charset = "@$_!\"#%&'()*+,-./:;<=>?\n" if c in charset: index = charset.find(c) storage += f"+{index}" else: charset = "[\\]^{|}~\t" if not in charset: return error and exit storage += f"++{index}" if len(storage) mod 3 != 0: storage += "/" * (3 - len(storage) mod 3) storage = base64_decode(storage)  At first, I was so puzzled as I misread the first condition thinking it only checks for numeric characters (turns out it is checking for alphanumeric characters instead, so it make perfect sense), and this makes no sense if the input is a flag. And my teammate grhkm reminds me I should try base64 conversion first, and I see this: Compressin g/short/messages/is/essential+Nso/ASIS/has/developed/new/one+xThe/flag/for/t his/task/is+RASIS++ec0mprEs51nG+csHOr7+ct3xT+cmE5s49es+cASIS+d++g///  So clearly this is the flag, and we can just reverse the ++e etc syntax with the algorithm we reverse above. And this yields us the flag easily. ## RaaS-v1 (Web, 68 solves)# Solved by fsharp We’re given a URL and an archive file containing the source code for the webpage. The goal is to read the contents of /flag.txt by exploiting a vulnerability. The webpage allows us to request any webpage with a request method of our choice and with the ability to send any form data. The source code is: <?php if($_SERVER['REMOTE_ADDR'] == '127.0.0.1'){
die('curl :thonk:');
}

$url = 'http://localhost';$method = 'GET';
$formParams = []; if(isset($_GET['url'])){
$url =$_GET['url'];
}

if(isset($_GET['method'])){$method = $_GET['method']; } if(isset($_GET['formParams'])){
$formParams =$_GET['formParams'];
}

$cmd = 'curl ';$cmd .= '--proto -file ';
$cmd .= escapeshellarg($url).' ';
$cmd .= '-X ';$cmd .= escapeshellarg($method).' '; foreach($formParams as $key =>$value){
if(preg_match("/^\w+$/",$key)){
$cmd .= '-F ';$cmd .= escapeshellarg($key.'= '.$value);
}
}

## Basic (Forensics + Misc, 34 solves)#

Solved by fsharp

A file called basic.raw is given to us.

Opening it in a hex editor, it appears to be a corrupted Stata .dta file. Referring to a website that describes the file format and the sample file it references, we can notice 3 errors with the file and fix them manually as follows:

1. The beginning of the file should be replaced with <stata_dta><header><release>118</release><byteo.
2. The end of the file should be replaced with rls></strls><value_labels></value_labels></stata_dta>.
3. ‘Blank’ regions of the file should contain 0x00s. So, replace the 0xA3s in the varnames section with 0x00s.

Afterwards, we can read the file using the Pandas library. We get a few hundred rows of data, where each row contains 3 columns:

1. position: A number.
2. isflagchar: Is this a character for the flag?
3. md5charsalt: An MD5 hash.

The description of the md5charsalt variable given by the file is specifically md5(char + 'SALT'). I took it literally and tried to find the characters with the salt as SALT, but none were found.

Initially, I was confused with what the challenge was asking for. However, Mystiz looked up a few of those hashes on reverse MD5 websites and found that the salt was actually s4Lt. Thanks to his help and with a little more guessing, I was able to complete my script and solve this challenge:

from hashlib import md5

hashes = set()
flag = []

for (position, isflagchar, md5charsalt) in zip(df["position"], df["isflagchar"], df["md5charsalt"]):
if isflagchar == 'Y':
flag.append([position, md5charsalt])

hashes = list(hashes)
flag = [md5charsalt for [position, md5charsalt] in sorted(flag)]

for hash in hashes:
for c in range(32, 127):
s = "s4Lt" + chr(c)
if md5(s.encode()).hexdigest() == hash:
for i in range(len(flag)):
if flag[i] == hash:
flag[i] = chr(c)
break

print(''.join(flag))