This is a write-up for the “Password Here Please” reverse engineering challenge from MetaCTF CyberGames 2020, which was worth 325 points.

Challenge statement: I forgot my bank account password! Luckily for me, I wrote a program that checks if my password is correct just in case I forgot which password I used, so this way I don’t lock myself out of my account. Unfortunately, I seem to have lost my password list as well… Could you take a look and see if you can find my password for me? Part 3 requires some math skills. To solve it, think about what is being done by the exponentiation step. Try rewriting the large number in base 257.

We are given a small python program that validates the password. Here it is:

``````def ValidatePassword(password):

print("Woah, you're not even close!!")
return False

for c in range(0, int(pwlen / 3))])
if "".join([c for c in chunk1[::4]]) != '&e"3&Ew*':
print("You call that the password? HA!")
return False

chunk2 = [ord(c) - 0x1F if ord(c) > 0x60
else (ord(c) + 0x1F if ord(c) > 0x40 else ord(c))
for c in password[int(pwlen / 3) : int(2 * pwlen / 3)]]
ring = [54, -45, 9, 25, -42, -25, 31, -79]
for i in range(0, len(chunk2)):
if(0 if i == len(chunk2) - 1 else chunk2[i + 1]) != chunk2[i] + ring[i]:
print("You cracked the passwo-- just kidding, try again! " + str(i))
return False

chunk3 = password[int(2 * pwlen / 3):]
code = 0xaace63feba9e1c71ef460e6dbf1b1fbabfd7e2e35401440ac57e93bd9ba41c4fbd5d437b1dfab11fe7a1c6c2035982a71765fc9a7b32ccef695dffb71babe15733f5bb29f76aae5f80fff
valid = True
for i in range(0, len(chunk3)):
if(ord(chunk3[i]) < 0x28):
valid = False
code -= (257 ** (ord(chunk3[i]) - 0x28)) * (1 << i)

if code == 0 and valid:
return True
else:
print("Quite wrong indeed!")
return False

pass
``````

Right, lets reverse this!

## A High Level Overview

Since we have the luxury of having the source code at our disposal (and in a language as readable as Python!), let’s get an overview of what’s going on here. The easiest way to do so is to just run the validator. I’ve saved the program in a file called `validator.py`. Start with running

``````python validator.py
``````

Note that you’ll have to enter the password surrounded with quotes or python tries to evaluate the password as a symbol and crashes.

The program prompts us to enter the password. Let’s try something simple, like `"helloworld"`. It responds with a rather condescending `Woah, you're not even close!!`, following which we get another shot at entering the password.

Okay, not very nice. Let’s take a cursory glance at the code and get an idea of what wer’re dealing with.

There’s a `ValidatePassword` function which seems to do all the heavy lifting here, and it returns `True` if the password was accepted, and `False` otherwise (and boy, it returns `False` a lot!). This function is run in a `while` loop that runs until a valid password is provided (or until you stop the program with `Ctrl+c`).

``````print("Please enter password")
pass
``````

Now let’s start breaking down the `ValidatePassword` function. At a glance, you can tell that it applies various validations one after another, and if a validation fails, it returns `False`. We see references to `chunk1`, `chunk2` and `chunk3`, each of which are validated independently. And towards the start, there seems to be something that validates the length of some convoluted subsection of the password. That’s 4 parts in total!

Let’s work through these in order!

Note: Each stage of the reversing process is under it’s own heading. So if you’ve already figured something out, feel free to skip ahead. This is a long read!

## Stage 1: The Length of the Password

Our validation function kicks of with this bit:

``````if(len(password[::-2]) != 12 or len(password[17:]) != 7):
print("Woah, you're not even close!!")
return False
``````

It checks for two conditions, and if either one is satisfied, the password is invalid, and we get the `Woah, you're not even close!!` response we saw earlier. The second one looks easier, so we’ll start with that. The `password[17:]` part is python syntax for “get me all the elements in this iterable starting from index 17”. The condition is true if the length of the part of the password starting from the 17th character is not 7. Since we want this to be false, we realise that our password has a length of 17 + 7, which is 24!

The first part of the condition also checks for the same thing, but in a much more convoluted way. The `password[::-2]` is python speak for “Give me all the characters in this list, from start to finish, but start at the end and take 2 steps at a time”. Let’s break that down for the sake of clarity. Fire up a python interpreter using `python` and enter these commands:

``````# Define the password

# password[::-1] gets all the characters in the password, but starts from the end

# password[::-2] gets all the characters starting from the end, but skips every alternate character

``````

The condition is that `len(password[::-2])` should not be 12. Since we want it to be false, we realise that the length of the string returned by taking every alternate character of the password should be 12, which means that the length of the password should be `12 * 2 = 24`. Perfect!

Run `validator.py` again, but this time enter a string of length 24. I just entered `"aaaaaaaaaaaaaaaaaaaaaaaa"`.

This time the validation script says `You call that the password? HA!`. Sweet, we’ve gotten past the first stage!

## Stage 2: Deobfuscation and Inversion

Okay, here’s the code for the second stage:

``````pwlen = len(password)
for c in range(0, int(pwlen / 3))])
if "".join([c for c in chunk1[::4]]) != '&e"3&Ew*':
print("You call that the password? HA!")
return False
``````

At this point, we’ll have to do some computations. Most people would write a script to solve this, but I preferred an interactive method where I could try my ideas in an interpreter. So that’s what we’ll be doing here. Fire up a python interpreter using `python` and let’s get started!

It looks like this stage uses a subsection of the password to create the first chunk, `chunk1`. We see that it operates on the characters of the password from index 0 to `pwlen / 3`, where `pwlen` is the length of the password. Since we already know that the password is 24 characters long, `pwlen / 3 = 8`. The first chunk operates on the first 8 characters, so that’s what we have to figure out.

Let’s look at what `chunk1` looks like. In your interpreter, enter the code for finding `chunk1` verbatim:

``````password = "abcdefghijklmnopqrstuvwx"

# returns '7key6key5key4key3key2key1key0'
chunk1 = 'key'.join([chr(0x98 - ord(password[c])) for c in range(0, int(pwlen / 3))])
``````

Looking up the `chr` and `ord` functions tells us that `ord` converts a single character into it’s integer ordinal, and `chr` does the opposite - convert an integer ordinal into a single character.

Tip: You could google the `chr` and `ord` functions, but a real hacker would use the `help` function of the interpreter! Try typing `help(chr)` or `help(ord)` in the interpreter.

Okay, I lied. A real hacker would use `chr.__doc__` and `ord.__doc__`. ;)

So it looks like this is constructed by:

1. Converting each character of the password into an integer using `ord`
2. Subtracting each of these integers from `0x98`
3. Converting the resultant back into a character using `chr`
4. Constructing a string by putting the word “key” between each character returned

Whew! Okay, let’s touch this when we need to. Let’s look at the next part, which has our condition.

``````if "".join([c for c in chunk1[::4]]) != '&e"3&Ew*':
print("You call that the password? HA!")
return False
``````

Here, we use another string constructed from `chunk1` by taking every 4th character of `chunk1`, starting from the first one. This is what `chunk1[::4]` does. Type this into the interpreter:

``````"".join([c for c in chunk1[::4]]) # '76543210'
``````

It looks like… this removes the `key` between each character? That makes the whole adding “key” part from earlier pointless! Why would you ever do that?

Obfuscation. They’re trying to make our job harder and send us down rabbit holes. But it’ll take more than that to confuse us!

Let’s simplify this stage a little.

``````pwlen = len(password)
for c in range(0, int(pwlen / 3))])
if chunk1 != '&e"3&Ew*':
print("You call that the password? HA!")
return False
``````

Okay, that is so much easier to understand! All we need to do is find an 8 character string that, when transformed into `chunk1`, results in `&e"3&Ew*`.

In order to pull this off, let’s try and convert the `chunk1` that we expect back into the string. In other words, let’s write a function that can invert the transformation that is applied to obtain `chunk1`. It’s time for some reversing!

So how do we figure this out? For starters, let’s say that the integer returned by `ord(password[c])` is `x`. Then the transformation becomes `chr(0x98 - x)`. Let’s forget the `chr` for a second. The crux of the transform is the `0x98 - x`. Let’s say that this results in an integer `y`. We then have the equation `0x98 - x = y`. Now here’s the question: Given `y`, how do you find `x`? Yep, we can rewrite that as `x = 0x98 - y` and solve for `x`.

In order to convert that back to a character, we apply the `chr` function, thus resulting in `chr(0x98 - y)`. Remeber that `y` is the integer ordinal of a character from `chunk1`, so we need to convert a character `c` from `chunk1` into it’s ordinal before we use our little reverse-function. So, for a character `c` of `chunk1`, we can figure out the corresponing character of the password using `chr(0x98 - ord(c))`. This looks… familiar? This is exactly what we’re doing to calculate `chunk1` in the first place! This is a function whose inverse is… itself!

That makes our job easy. We don’t even need to write any code to figure out the first 8 characters. Since we know that chunk1 should be `&e"3&Ew*`, and that the function that calculates `chunk1` is its own inverse, we can just give `&e"3&Ew*` as an input to the function itself and figure out the first 8 chars of the password!

Let’s modify the `validator.py` file to print `chunk1`. Add this line right after `chunk1` is calculated:

``````print(chunk1)
``````

Let’s run `validator.py` again, but this time we’ll set the first 8 characters of the password to `&e"3&Ew*`, and the remaining 16 to something random like “aaaaaaaaaaaaaaaa”. Make sure that you’ve simplified the code as per the instructions above, and replaced the `"key".join` with `"".join`. Here’s the input you need:

``````'&e"3&Ew*aaaaaaaaaaaaaaaa'
``````

And that prints `r3verS!n`! So the the first 8 characters of the password must be `r3verS!n`. Let’s try this input next to confirm:

``````'r3verS!naaaaaaaaaaaaaaaa'
``````

And this time it tells us that `You cracked the passwo-- just kidding, try again! 0`! Looks like we’ve gotten past this stage and figured out the first 8 characters correctly!

## Stage 3: Reverse and Inverse

We move on to the next snippet, which deals with `chunk2`. Judging by the range in the list comprehension which creates `chunk2`, it looks like it operates upon the next 8 characters of our 24 character password.

``````chunk2 = [ord(c) - 0x1F if ord(c) > 0x60
else (ord(c) + 0x1F if ord(c) > 0x40 else ord(c))
for c in password[int(pwlen / 3) : int(2 * pwlen / 3)]]
ring = [54, -45, 9, 25, -42, -25, 31, -79]
for i in range(0, len(chunk2)):
if(0 if i == len(chunk2) - 1 else chunk2[i + 1]) != chunk2[i] + ring[i]:
print("You cracked the passwo-- just kidding, try again! " + str(i))
return False
``````

We see that the list comprehension being used to construct `chunk2` returns the result of some arithmetic operations on the ordinal value of each character (which is obtained using the `ord` function). So `chunk2` is a list of 8 integers.

And then there seems to be a list of seemingly random numbers called `ring` which is used to validate `chunk2`.

Finally, we have a for loop that iterates over the elements of `chunk2` and applies a condition to each of them in order to validate them. In essence, we will need the second group of 8 characters of the password to generate an array of integers `chunk2` that passes this validation. It makes sense that we want to know `chunk2` first (and also, this is a reversing challenge, hehehe), so let’s go in reverse and figure out the integer array `chunk2`.

Here is the condition (where `i` is the index):

``````(0 if i == len(chunk2) - 1 else chunk2[i + 1]) != chunk2[i] + ring[i]
``````

This might seem complex at first glance (especially because of the ternary operator), but it really isn’t. It just says that for all values of `i` except the last (i.e. except when `i` equals `len(chunk2) - 1`), `chunk2[i] + ring[i]` must equal `chunk2[i+1]` (i.e. the value at the next index of `chunk2`. For the last value of `i`, `chunk2[i] + ring[i]` must equal 0.

Since we know the exact value of `chunk2[i] + ring[i]` for the last element (`i=7`), we can start with that and work our way backwards. Lot’s of reversing here!

Fire up your python interpreter and let’s play with this. The comments will explain what’s happening.

``````# let's first grab the ring from the source code
ring = [54, -45, 9, 25, -42, -25, 31, -79]

# let's create a list of 8 integers to represent chunk2
# we initialize the elements of chunk2 to 0
chunk2 = [0 for i in range(8)] # [0, 0, 0, 0, 0, 0, 0, 0]

# we know that chunk2 + ring = 0
# therefore...
chunk2 = 0 - ring # [0, 0, 0, 0, 0, 0, 0, 79]

# we work our way backwards to calculate the rest of the elements of chunk2
# using the knowledge that since chunk2[i+1] = chunk2[i] + ring[i]
# chunk2[i] = chunk2[i+1] - ring[i]
for i in reversed(range(7)):
chunk2[i] = chunk2[i+1] - ring[i]

# print chunk2 (note that if you are in the interpreter, you can just enter the variable name instead of using the print function)
chunk2 # [72, 126, 81, 90, 115, 73, 48, 79]
``````

You can confirm that this is the right value of `chunk2` by placing this line of code right before the for loop that validates it and checking if you are able to get past this stage.

``````chunk2 = [72, 126, 81, 90, 115, 73, 48, 79]
``````

Okay, now that we know our target value of `chunk2`, it is time to figure out the characters that generate it. Time to reverse the part that generates `chunk2`!

This source code seems to make rather generous use of the ternary operator which (possibly intentionally) hurts readability. To make matters worse, we have a list comprehension! So let’s refactor the whole thing into a vanilla for loop which uses a function to get the right value for each char. Here’s what that might look like:

``````def chunk2_char(c):
if ord(c) > 0x60:
return ord(c) - 0x1F
else:
if ord(c) > 0x40:
return ord(c) + 0x1F
else:
return ord(c)

chunk2 = []
for c in password[int(pwlen / 3) : int(2 * pwlen / 3)]:
chunk2.append(chunk2_char(c))
``````

In this form, the code becomes far easier to analyse. Refactoring code to improve readability and mitigate obfuscation is more important than you might expect during reverse engineering.

Now lets try to invert the `chunk2_char` function.

Let’s start with the return values. The easiest one is the `return ord(c)` in the very end. This just converts the character to the integer ordinal, so to invert that, we can use the `chr` function.

For the condition where `ord(c) > 0x60`, the function returns `ord(c) - 0x1F`. To invert this, let’s say that `x = ord(c)`. We then solve the equation `x - 0x1F = y` for `x`, which gives us `x = y + 0x1F`. Since `x = ord(c)`, we can get `x` by using the inverse of the `ord` function, which, as you are well aware by now, is the `chr` function. Thus, the inversion of `ord(c) - 0x1F` becomes `chr(x)` which boils down to `chr(x + 0x1F)`.

By a similar chain of reasoning, you’ll find that the inverse of `ord(c) + 0x1F` is `chr(x + 0x1F)`.

Now let’s look at the conditions. In the original function, the first condition is `ord(c) > 0x60`. Let’s think of this as follows: let’s say that our inverse function takes an integer ordinal `x` and returns a character `c`. When we apply the original function to `c`, we should get `x` back. So when we check whether `ord(c) > 0x60` is true in the original function, the `c` here is the value returned by our inverse function. Which means that for each of the conditions, we can replace the `ord(c)` with the integer ordinal of the value returned if that condition is true.

For example, if the first condition (`ord(c) > 0x60`) is true, the returned value is `chr(x + 0x1F)`. Replacing the `c` with this value, we get the condition `ord(chr(x + 0x1F)) > 0x60`. But we can simplify this knowing that the `ord` function is the inverse of the `chr` function. So `ord(chr(x)) = x`. Applying this, we get the inverse condition `x + 0x1F > 0x60`. We can rearrange terms and make this `x > 0x60 - 0x1F` which simplifies to `x > 65` (you can do all these calculations in your interpreter).

By using similar reasoning, we figure out that the inverse of the second condition is `if x - 0x1F > 0x40`, which simplifies to `x > 95`.

Armed with the inverses of each of the pieces, we can construct our inverse function as follows:

``````def inverse_chunk2_char(x):
if x > 65:
return chr(x + 0x1F)
else:
if x > 95:
return chr(x - 0x1F)
else:
return chr(x)
``````

And apply it to the `chunk2` value we calculated earlier like this:

``````result = ''.join(list(map(inverse_chunk2_char, chunk2)))

# if you don't understand the map syntax very well, this list comprehension also works
result = ''.join([inverse_chunk2_char(x) for x in chunk2])
``````

But if you actually look at the value of `result`, you’ll see that though it looks like we got some things right, some of the values are non-printable. Looks like there is an issue with our inverse function.

The issue lies in the order in which we check our conditions. Think about what would happen for the value `x = 126` (the second item in the `chunk2` array). The first condition checks out because obviously `126 > 65`. But so does the second condition (`126 > 95`). In fact, if you think about it, you’ll realise that the first condition is true whenever the second condition is because 95 > 65. So we don’t ever reach the second condition when the first one is true. Even when you need to. To fix this, we just reorder things a little (note that all conditions are independent, so we don’t need the nested if here anyways).

``````def inverse_chunk2_char(x):
if x > 95:
return chr(x - 0x1F)
elif x > 65:
return chr(x + 0x1F)
else:
return chr(x)
``````

We now retrieve the 8 characters we need using

``````result = ''.join(list(map(inverse_chunk2_char, chunk2)))
# 'g_pyTh0n'
``````

That looks legit. So the first 16 characters of our password seem to be `r3verS!ng_pyTh0n`. Reversing python huh?

Entering `'r3verS!ng_pyTh0naaaaaaaa'` in the password prompt results in `Quite wrong indeed!`. Nice, we’ve gotten past this stage!

## Stage 4: Doin' the Math

We’re finally at the last stage. Let’s look at what we have:

``````chunk3 = password[int(2 * pwlen / 3):]
code = 0xaace63feba9e1c71ef460e6dbf1b1fbabfd7e2e35401440ac57e93bd9ba41c4fbd5d437b1dfab11fe7a1c6c2035982a71765fc9a7b32ccef695dffb71babe15733f5bb29f76aae5f80fff
valid = True
for i in range(0, len(chunk3)):
if(ord(chunk3[i]) < 0x28):
valid = False
code -= (257 ** (ord(chunk3[i]) - 0x28)) * (1 << i)

if code == 0 and valid:
return True
else:
print("Quite wrong indeed!")
return False
``````

This stage follows the same pattern of taking an 8 character long chunk and applying some validations on it. If these pass, you will (presumably) get the coveted `Password accepted!`.

We start with a really, really long hexadecimal number called `code`. And the condition to make it work is quite straightforward: `code` must equal 0 and `valid` must be `True`.

All the processing happens in this for loop:

``````valid = True
for i in range(0, len(chunk3)):
if(ord(chunk3[i]) < 0x28):
valid = False
code -= (257 ** (ord(chunk3[i]) - 0x28)) * (1 << i)
``````

For starters, we are looking at the `ord` values of the characters. So we’re dealing with numbers here.

The first thing is a condition that checks if `ord(c) < 0x28` (where `c` is a character of chunk3), and if so, sets `valid` to `False`. We don’t want that, so we obviously don’t want to trigger this condition. It is thus obvious that all the characters of this chunk will have integer ordinals greater than 0x28. Easy enough.

The next line does some strange maths. In each iteration, it subtracts from the current value of `code` the result of this strange calculation: `(257 ** (ord(chunk3[i]) - 0x28)) * (1 << i)`. In the end, the value of `code` must be 0. Since we are dealing with 8 iterations (corresponding to the last 8 characters of the password), we are doing this subtraction 8 times. In other words, we are subtracting 8 values from `code`, and this must result in a `0`. Let’s say that these values are `x0` to `x7`. We can then express the intent of the for loop and condition as this equation:

``````code - x0 - x1 - x2 - x3 - x4 - x5  -x6 -x7 = 0
=> code = x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7
=> x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 = code
``````

Where `x0...x7` are the various values of the convoluted expression `(257 ** (ord(chunk3[i]) - 0x28)) * (1 << i)`.

It’s time to break this expression down!

To make things easier, let’s say that `ord(chunk3[i]) - 0x28 = yi`. This is easy to invert and lets us focus on the right things. The expression then becomes `(257 ** yi) * (1 << i)`. That’s much better.

The `(1 << i)` represents a bitwise left shift. If you don’t know how these work, I suggest doing a google search at this point. All you need to know for this part is that a bitwise left shift by `i` places is equivalent to multiplying the given number by `2^i`. Since we are shifting `1` in all the cases, the `(1 << i)` turns into `1 * 2^i` which is simply `2^i`. Finally, just to use more comfortable mathematical notation, let’s replace the python exponentiation operator `**` with the more familiar `^`. We now get something that looks a lot more manageable:

``````(257 ^ yi) * (2 ^ i)
``````

Our problem now reduces to finding 8 values (`y0 to y7`) such that plugging each of them into the above expression and summing them up results in `code`. That’s troublesome. We have 8 unknowns and only one equation. And it isn’t even a polynomial one!

I was stuck at this point for a bit, but that’s when the hint from the challenge came to my rescue! To recap, “Part 3 requires some math skills. To solve it, think about what is being done by the exponentiation step. Try rewriting the large number in base 257.”

Yes! We can convert this to base 257! To understand why, let’s take a quick look at what it means to be a number in a certain base `b`. Let’s say we have the octal number 176. Let’s convert this into the more familiar base 10 (decimal). To do this, we run the following calculation (your interpreter is a good place to play with this):

``````6 * (8 ** 0) + 7 * (8 ** 1) + 1 * (8 ** 2)
# 126
``````

There is a pattern here. Starting from the end, we take each digit and multiply it by the base raised to the power of the position of the digit (starting with 0, which is the position of the last digit).

So if we convert `code` into base 257, we will have a resultant that looks something like:

``````a * (257 ** 0) + b * (257 ** 1) + .... + x * (257 ** k)
``````

Where `k` is one less than the number of digits in base 257 that code has. This matches with the pattern we saw in the previous expression! Let’s try this and see where it takes us.

I found this base conversion python script online which was well written and up to the task, and the article accompanying it is quite good too.

Copy the code from the article into a python file and name it `bconvert.py`. Since we need to get a list of what would be the digits in base 257, we’ll take a quick look at the code and print this list out. It turns out that this is quite simple. Most of the work happens in the `convert_number_system` function. Right after the main `while` loop (the `while sum_base_10 > 0` condition) completes, add this line:

``````print(remainder_list)
``````

Run the converter using `python bconvert.py`. Note that this one requires python3. When asked for the base to convert from, enter 16 (since `code` is in hexadecimal). For the base to convert to, enter 257. Finally, for the number to convert, enter the value of `code`.

The script should print a list containing our digits (ignore the final output; you don’t have 257 digits to represent base 257) that looks like this (note that you’ll see an array of strings, but I’ve converted it into numbers):

``````[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 8]
``````

This is interesting! It looks like most of our “digits” are zero! We also seem to have seven non-zero digits: 32, 4, 64, 17, 2, 128, and 8. You’ll have to take a quick look at the base conversion script to realise that it conveniently prints out the digits from the end. So the digit at index 0 of this list is the last digit, and so on. Recall that we can convert this into the familiar decimal system by multiplying each of these digits with `257 ^ index`.

Let’s quickly find the indices of the interesting non-zero digits. Here’s a Python one liner. We’ll also make a new list with the non-zero digits:

``````digits = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 8]

non_zero_digits = filter(lambda x: x, digits)
# [32, 4, 64, 17, 2, 128, 8]

indices = [digits.index(i) for i in non_zero_digits]
# [30, 39, 45, 55, 62, 70, 74]
``````

Since most of our numbers are zero, `0 * (257 ^ index)` will just be 0, so we can ignore them. We can now express `code` mathematically as:

``````code = (257 ^ 30) * 32 + (257 ^ 39) * 4 + (257 ^ 45) * 64...
``````

We immediately see that all the digits except 17 are powers of two, and they are being multiplied by something that is in the form `257 ^ yi`! This looks exactly like our equation from earlier:

``````(257 ^ yi) * (2 ^ i)
``````

Except we have two problems:

1. There are 8 terms in our equation (for 8 characters of the password), but we only have 7 non-zero digits.
2. The 17 is problematic because it isn’t a power of 2.

The solution to both of these issues is the same. It is the subtle realization that `17 = 16 + 1`!

So we can rewrite the term of the equation involving the digit 17 as `(257 ^ 55) * 17 = (257 ^ 55) * 16 + (257 ^ 55) * 1`. We now have exactly 8 terms, all of which are in the form `(257 ^ yi) * (2 ^ i)`. Perfect!

We can now write each of the 8 digits we have in the form `(257 ^ yi) * (2 ^ i)`, so let’s do that. I’ve ordered this based on increasing values of i:

``````(257 ^ 55) * (2 ^ 0) # 1 = 2 ^ 0, the index corresponding to 1 is the one for 17, which is 55
(257 ^ 62) * (2 ^ 1) # 2 = 2 ^ 1
(257 ^ 39) * (2 ^ 2) # 4 = 2 ^ 2
...
(257 ^ 70) * (2 ^ 7) # 128 = 2 ^ 7
``````

With this ordering, we can construct a list of `yi` values, which are just the powers that `257` is raised to.

Now recall that we had set `yi = ord(chunk3[i]) - 0x28` earlier. To get `chunk3[i]` values, we need to invert this function. This is easy if you’ve gotten past the previous stage. We realise that `chunk3[i] = chr(yi + 0x28)`. Knowing this, we can construct `chunk3` as follows:

``````# Our list of yi values
yis = [55, 62, 39, 74, 55, 30, 45, 70]

chunk3 = [chr(yi + 0x28) for yi in yis]
# ['_', 'f', 'O', 'r', '_', 'F', 'U', 'n']

# We convert the character list into a string
And we have the last 8 characters! You can enter this in the password prompt to finally feel the euphoria of `Password accepted!`.
``````'r3verS!ng_pyTh0n_fOr_FUn'
The flag is `MetaCTF{r3verS!ng_pyTh0n_fOr_FUn}`!