~$ Dissecting the Central-Infosec Python challenges

Posted on Apr. 20th, 2021.

Tags:InfoSecCTFWrite-Up


This is a write-up of the "07. Reversing: Code Analysis (CIS-WEBSRV01)" series of challenges in the context of the Central Infosec CTF.

The challenges consist of Python code to analyse and reverse, in order to find a series of flags. This write-up assumes just a basic amount of programming knowledge, and will walk you through most of the rest.

Summary


Challenge 1: Reading the script

The first challenge, located at the URL $MACHINE_IP/python-01, is a small python script that refreshes a few basics of string operations in Python, which you can see below.

```python str1 = "P" str2 = "7H0N" str3 = "[email protected]" str1 += "Y" str2 = "57R" print("Central-InfoSec" + "{" + str1 + "_" + str2 + "_" + str3 + "}") # Output: Central-InfoSec{???} ```

Let's break this down.

```python str1 = "P" str2 = "7H0N" str3 = "[email protected]" ```
Setting up the variables.

In the code above, we can see the declaration of a few variables (ie. boxes that have a name and that now contain a bit of information).

```python str1 += "Y" str2 = "57R" ```
Setting up the variables.

In this small bit, str1 has the character "Y" added to it, resulting in it having the value "PY". The content of str2 however, gets replaced with the value "57R".

```python print("Central-InfoSec" + "{" + str1 + "_" + str2 + "_" + str3 + "}") # Output: Central-InfoSec{???} ```

Concatenating the strings.

This last bit is the bit that does the work of showing the flag, which is Central-InfoSec{[email protected]}.


Challenge 2: Flipping the script

The second challenge, located at the URL $MACHINE_IP/python-02, is a small script that goes on about string replacement. The key is to go from the output ("Central-InfoSec{[email protected]_$47645558}") and reverse the code to find the input ("Central-InfoSec{???}").

```python flag = "Central-InfoSec{???}" flag = flag.replace("5","$",2) flag = flag.replace("2","4") flag = flag.replace("5","4",1) flag = flag.replace("8","58",1) print(flag) # Input: Central-InfoSec{???} # Output: Central-InfoSec{[email protected]_$47645558} ```

The way to deal with this is to reverse the steps taken to obfuscate the flag, which are each of the replace operations.

One thing to note is the optional third parameter that sometimes shows up, which according to the function signature (string.replace(oldvalue, newvalue, count)) is the amount of times the replacement is allowed to run on one string.

So to reverse this we are going to define the input as the original output, do every step in the opposite order and switch the oldvalue and newvalue parameters in each replace statement copied from the original code.

Setting up the variables.

To do this we're going to be writing the following script:

```python flag = "Central-InfoSec{[email protected]_$47645558}" flag = flag.replace("58","8",1) flag = flag.replace("4","5",1) flag = flag.replace("4","2") flag = flag.replace("$","5",2) print(flag) # Input: Central-InfoSec{[email protected]_$47645558} # Output: Central-InfoSec{[email protected]_55762558} ```

This means our original string was Central-InfoSec{[email protected]_55762558}, and is also the flag required for this challenge.


Challenge 3: Reversing the function

The third challenge, located at the URL $MACHINE_IP/python-03, is a small script that goes on about string replacement. The key is to go from the output ("Central-InfoSec{[email protected]_$47645558}") and reverse the code to find the input ("Central-InfoSec{???}").

```python def encodeNumbers(statement, num1, num2): number1 = str(num1) number2 = str(int(number1)+int(num2)) statement = statement.replace(number1,number2) return statement flag = "Central-InfoSec{???}" flag = encodeNumbers(flag,7,3) flag = encodeNumbers(flag,3,1) flag = encodeNumbers(flag,2,4) flag = encodeNumbers(flag,6,8) flag = encodeNumbers(flag,5,26) print(flag) # Input: Central-InfoSec{???} # Output: Central-InfoSec{4NC0D4D_NUM84R31_131140} ```

So, before doing anything, let's find out what this code does.

The first thing it does is define a function (which you can notice by the def keyword), named encodeNumbers. A function is a block of code you want to run multiple times without having to rewrite the code each time, and is quite useful in programming.

```python def encodeNumbers(statement, num1, num2): number1 = str(num1) number2 = str(int(number1)+int(num2)) statement = statement.replace(number1,number2) return statement ```

This function takes three parameters, the first (statement) being the flag, the second (num1) and third (num2) being numbers.

Its first step is defining a parameter named number1, to which the string representation of the num1 parameter is assigned.

Its second step is defining a parameter named number2, which is the string representation of the sum of number1 (but as a number, so basically num1) and the num2 parameter.

Its third step is replacing number1 string with the number2 string in the given flag.

Finally, it returns the flag. (When a function returns a value it allows this value to be assigned to a variable outside of the function itself)

Analyzing the function.

What this function is doing is replacing every occurence of num1 by the sum of num1 and num2.

So to reverse this function, we make it take the exact same numbers, but invert the order in the replace statement.

```python def decodeNumbers(statement, num1, num2): number1 = str(num1) number2 = str(num1 + num2) # Replaced with both numerical values for easier comprehension statement = statement.replace(number2,number1) return statement ```

We then do like before and reverse the order of operations in order to obtain:

```python flag = "Central-InfoSec{4NC0D4D_NUM84R31_131140}" flag = decodeNumbers(flag,7,3) flag = decodeNumbers(flag,3,1) flag = decodeNumbers(flag,2,4) flag = decodeNumbers(flag,6,8) flag = decodeNumbers(flag,5,26) print(flag) # Input: Central-InfoSec{4NC0D4D_NUM84R31_131140} # Output: Central-InfoSec{3NC0D3D_NUM83R5_15130} ```

We can check that this worked correctly by setting the result (Central-InfoSec{3NC0D3D_NUM83R5_15130}) as the original flag and rerunning the encoding, which will produce the original output.

This means our original string was Central-InfoSec{3NC0D3D_NUM83R5_15130}, and is also the flag required for this challenge.


Challenge 4: Flipping the encoding

The fourth challenge, located at the URL $MACHINE_IP/python-04, is a script that deals with function analysis. The key is to invert the function and go from the output ("Central-InfoSec{OPU==_DCUG==_64}") and reverse certain operations to find the input ("Central-InfoSec{???}").

```python def b64encode(s, n): return ''.join(chr((ord(char) - ord('a') + n) % 26 + 97) for char in s.lower()) flag1 = "Central-InfoSec{" flag2a = "???" flag2b = "_" flag2c = "????" flag2d = "_" flag2e = "64" flag3 = "}" flag2a = b64encode(flag2a,1).upper() + "==" flag2c = b64encode(flag2c,2).upper() + "==" print (flag1 + flag2a + flag2b + flag2c + flag2d + flag2e + flag3) # Input: Central-InfoSec{???}" # Output: Central-InfoSec{OPU==_DCUG==_64} ```

1. Working on the function

Like the previous challenge, let us start by looking at the function:

```python def b64encode(s, n): return ''.join(chr((ord(char) - ord('a') + n) % 26 + 97) for char in s.lower()) ```

This function takes two parameters, s (the flag) and n (a number). What it then does is a series of operations, which might be easier to understand if we expand it.

The first thing I wanted to touch on is that the ord and chr functions are both functions that are used to transform from an ASCII character to the ASCII index and vice-versa.

What is an ASCII character or an ASCII index? Well, since everything in computers deals primarily with numbers, if you want to write anything you need a reference table (that converts from numbers to characters). For the first 128 characters we call that table the ASCII table (as ASCII stands for American Standard Code for Information Interchange).

As such, the character 'a' is at ASCII index 97 (and is different to ASCII index 65, which is the character 'A'). So our ord function, if supplied with 'a' as a parameter, will return 97. Inversely, our chr function, if supplied with 97 as a parameter, will return 'a'.

Another thing you might notice is the % 26 operation. This modulus operation is used to make sure we don't go over 25 characters greater than the ASCII index of 'a', which is 'z'.

Additionally, the for char in s.lower() is what is known as a "list comprehension". It acts much like a traditional for loop but can be done on a single line. The .lower() method turns a string into it's lowercase variant.

Finally, the ''.join(...) is a function used to merge all the elements of a list into a string.

So we can rewrite our b64encode function like so:

```python def b64encode(s, n): result = [] # Start our list ascii_offset = ord('a') # Define our ASCII offset for char in s.lower(): # Start our loop new_value = ord(char) - ascii_offset # Remove the ASCII offset new_value = ord(char) + n # Add the defined offset new_value = new_value % 26 # Make sure it doesn't exceed 'z' new_value = new_value + ascii_offset # Add the ASCII offset result.append(chr(new_value)) # Add it to the list return ''.join(result) # Merge the list elements into a string ```

To reverse that function is now easier: We need to do much of the same, but instead of adding the defined offset, just substract it.

However, since the modulo doesn't have a very well defined behavior in Python when it comes to potential negative numbers, we will also add 26 for good measure before we do the modulo operation, just to be safe.

```python def b64decode(s, n): result = [] # Start our list ascii_offset = ord('a') # Define our ASCII offset for char in s.lower(): # Start our loop new_value = ord(char) - ascii_offset # Remove the ASCII offset new_value = ord(char) - n # Subtract the defined offset new_value = ord(char) + 26 # Add 26 to make the modulo work new_value = new_value % 26 # Make sure it doesn't exceed 'z' new_value = new_value + ascii_offset # Add the ASCII offset result.append(chr(new_value)) # Add it to the list return ''.join(result) # Merge the list elements into a string ```

If you want to rewrite this as a shorter version, you can always write:

```python def b64decode(s, n): return ''.join(chr((ord(char) - ord('a') - n + 26) % 26 + ord('a')) for char in s.lower()) ```

2. Working on the challenge
```python flag1 = "Central-InfoSec{" flag2a = "???" flag2b = "_" flag2c = "????" flag2d = "_" flag2e = "64" flag3 = "}" flag2a = b64encode(flag2a,1).upper() + "==" flag2c = b64encode(flag2c,2).upper() + "==" print (flag1 + flag2a + flag2b + flag2c + flag2d + flag2e + flag3) # Input: Central-InfoSec{???} # Output: Central-InfoSec{OPU==_DCUG==_64} ```

We have multiple elements here, so let us devise what values are stored in what variable at the end of the program.

Analyzing the assignments.

Here we can see that flag2a and flag2c are the ones that are affected by the encoding (lines 11 and 12 in the above code). The program first encodes a value, then adds "==" to the end of it.

So if we run our b64decode function with parameters "OPU" and 1, and once more with parameters "DCUG" and 2, we should obtain the original values for the flags.

As such "OPU" turns into "not" and "DCUG" turns into "base".

This makes the original flag be Central-InfoSec{not_base_64} (which would be correct, since the functions are operating more like a Caesar Shift Cipher).

Challenge 5: Dissassembling the operations

The fifth challenge, located at the URL $MACHINE_IP/python-04, is a script that deals with function analysis. The key is to invert the function and go from the output ("Central-InfoSec{OPU==_DCUG==_64}") and reverse certain operations to find the input ("Central-InfoSec{???}").

```python flag1 = "Central-InfoSec{" flag2a = "??_" flag2b = "?????????" flag2b = "C".join(flag2b.split("_"))[::-1] flag2c = "_?????"[::-1] flag2d = "8".join((flag2a + flag2b + flag2c).split("_"))[::-1] flag3 = "}" print(flag1 + flag2d + flag3); # Input: Central-InfoSec{???} # Output: Central-InfoSec{[email protected][email protected]} ```

This python script is a bit simpler in structure, but is a bit harder to analyze than our previous codes. Here we will go up from flag2d and find the original flag2a, flag2b and flag2c.

flag2d ends up as [email protected][email protected]. It is the result of a string flip operation ([::-1], which will reverse any list or string), which is done on the result of a '8'.join operation (which is the same as before, but instead of merging the elements outright, is going to add a '8' character at each interstice), which is done on the result of a join operation, which is done on the concatenation of flag2a, flag2b and flag2c.

It gets confusing fast, so let's break it down a little:

```python flag2d = "8".join((flag2a + flag2b + flag2c).split("_"))[::-1] # [email protected][email protected] flag2d_alpha = (flag2a + flag2b + flag2c) # Op4: "15_" + ? flag2d_bis = flag2d_alpha.split("_") # Op3: "[email protected][email protected]_" flag2d_ter = "8".join(flag2d_bis) # Op2: ["15", "[email protected][email protected]", ""] flag2d_quat = flag2d_ter[::-1] # Op1: "[email protected][email protected]" ```

If you read the operations in the opposite direction, and by comparing wit the original structure of flag2a ("??_") we have some idea as to what flag2a is: "15_".

flag2b and flag2c are a bit more complicated however.

We now know that flag2b + flag2c is "[email protected][email protected]_". If we look at the original structure of flag2c and the fact that it is reversed, we can find flag2c: We take the underscore ("_") and the 5 character preceding them ("[email protected]").
We then reverse this string to obtain: flag2c = "[email protected]?"

So now we only have flag2b left: "[email protected]".

Line 4 of our original code does the same thing with flag_2b as what earlier happened with flag_2d. It splits the string on the "_" character and merges it again with the "C" character at the interstices.

To reverse this we replace the "C" in our flag_2b by an "_", which gets us flag2b = "[email protected]_5147".

To finish this off, we assemble the contents of the flag, which becomes: "15_" + "[email protected]_5147" + "[email protected]?".

The final flag to finish this section off is "Central-InfoSec{[email protected][email protected]?}".


I hope you enjoyed reading this, as I've tried to make it as entry level into Python as possible. I'll be getting into more technical things in the 2 other write-ups, if that's more your speed!