VERE - Rev-1 - Piethon
Published at Feb 15, 2025

You need to deobfuscate the code to see what’s going on. This exact code attached is running on the server listed below. Once you find out how to decipher the flag, interact with the server to get it. Automation is probably your friend ngl. Also this may take a HOT second…
nc 172.16.16.7 51358
Challenge Overview:
Approach
My first step was to open the given file and see if I could glean anything from it. What greeted me was:
#!/usr/bin/python3
__import__('mpmath').mp.dps=0x3e7+0b1
[a:=open('flag.txt',chr(114)).read(),j:=str,z:='bad!!',b:=__import__('random').randint(0,0x3e8),e:=print,e(b),c:=input,d:=c(),f:=exit,h:=ord,i:=range,k:=__import__('mpmath').mp.pi]
if d!='pi':
@e
@z.format
class A:pass
@f
class Input:pass
if c()!=j(k)[:b]:
@e
@z.format
class B:pass
@f
class Str:pass
for(g)in(a):
for(_)in(i(h(g))):
if c()!=j(k)[_]:
@e
@z.format
class C:pass
@f
class X:pass
@e
@chr(1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+2+1+1+1+1+1+1+1+1+1+1+1+1+1+1+2+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1+1).format
class D:pass
That’s a whole load of nonsense. There’s a lot of obfuscation of code here. Let’s break it down into smaller chunks and see if we can deobfuscate it.
[a:=open('flag.txt',chr(114)).read(),j:=str,
z:='bad!!',
b:=__import__('random').randint(0,0x3e8),
e:=print,e(b),c:=input,d:=c(),f:=exit,h:=ord,i:=range,
k:=__import__('mpmath').mp.pi]
This section at the top seems to create a series of variables named after letters of the alphabet. That makes things hard to follow when they start to reference each other, so I assigned them names based on what they seem to be doing. As I step through it, I can set values to things I actually understand. chr(114)
, for instance, is a python function that returns the character that represents the specified unicode, in this case, the letter r
. The variable z
is being set to the hex representation of the string bad!!
.
Other variables, such as e
, f
, h
, and i
are being set to represent the python functions print
, exit
, ord
(does the opposite of chr()
and converts the digit back to unicode), and range
specifically. Likewise, j
is being set to be str
, denoting a string for some variable it will likely interact with later in the code.
Still other variables are being set to interact with others, so at first they are hard to identify, but after renaming them, it’s easier to see what they do. For example, e(b)
doesn’t mean anything initially, but when you realize that e
is print
and b
is a random integer from 0 to 0x3e8 (hex for 1000), you can put together that this small function simply runs print(<random number from 0 - 1000>)
.
This section all beautified turned out like this:
[read_flag_txt:=open('flag.txt','r').read(),string_str:=str,bad_var:='bad!!',random_0_to_1000:=__import__('random').randint(0,1000),random_0_to_1000:=1,print_function:=print,print_function(random_0_to_1000),user_input_function:=input,user_input_function2:=user_input_function(),exit_function:=exit,char_to_hex:=ord,range_function:=range,pi_value:=__import__('mpmath').mp.pi]
Using this principle, we are able to refactor the rest of the code, replacing sections like if c()!=j(k)[:b]:
with something much easier to comprehend if user_input_function2!='pi':
. Here is the full beautified (and commented up) piethon.py
file:
#!/usr/bin/python3
__import__('mpmath').mp.dps=0x3e7+0b1
# Reads flag.txt and stores its contents in read_flag_txt.
# Assigns built-in functions (str, input, exit, ord, range) to custom variable names.
# Generates a random number (random_0_to_1000) between 0 and 1000 and prints it.
# Prompts the user for input (user_input_function2).
# Fetches the value of π (pi_value) with 1000 decimal places.
[read_flag_txt:=open('flag.txt','r').read(),string_str:=str,bad_var:='bad!!',random_0_to_1000:=__import__('random').randint(0,1000),random_0_to_1000:=1,print_function:=print,print_function(random_0_to_1000),user_input_function:=input,user_input_function2:=user_input_function(),exit_function:=exit,char_to_hex:=ord,range_function:=range,pi_value:=__import__('mpmath').mp.pi]
# If the user's input isn't "pi", it:
# Uses decorators (@print_function and @bad_var.format) on an empty class A, which does nothing.
# Calls exit_function(), terminating the script.
if user_input_function2!='pi':
@print_function
@bad_var.format
class A:pass
@exit_function
class Input:pass
# Takes another user input.
# Converts pi_value to a string and slices it up to random_0_to_1000 decimal places.
# If the user’s input does not match the first random_0_to_1000 digits of π, end
if user_input_function()!=string_str(pi_value)[:random_0_to_1000]:
@print_function
@bad_var.format
class B:pass
@exit_function
class Str:pass
# Loops through each character in flag.txt.
# For each character, its ASCII value (char_to_hex(character)) determines how many times the loop runs.
# For each iteration, it asks for user input and checks if it matches the corresponding digit of π.
for(character)in(read_flag_txt):
for(index)in(range_function(char_to_hex(character))):
if user_input_function()!=string_str(pi_value)[index]:
@print_function
@bad_var.format
class C:pass
@exit_function
class X:pass
@print_function
@chr(121).format # print "y"
class D:pass
Now that it’s readable, we can identify what the code does. This is largely a rehashing of what I wrote in my comments, so if reading it inline with the code makes it better for you to read, do that.
The first section sets up a lot of the script. It reads flag.txt
and stores its contents in read_flag_txt
. It then assigns built-in functions (str
, input
, exit
, ord
, range
) to custom variabl names. It generates a random number (random_0_to_1000
) between 0 and 1000 and prints it. Then it prompts the user for input (user_input_function2
). It also fetches the value of π (pi_value
) with 1000 decimal places from the library mpmath
.
The Filling (Get it? The meat of the script… it’s a… pie reference… sigh…)
The rest of the script seems to be split into 3 big if
statements.
The first if
statement checks for the user’s input to not be “pi”. It uses decorators (@print_function
and @bad_var.format
) on an empty class A
, which does nothing. It then calls exit_function()
, ending the script. This doesn’t give us the flag yet, so we can assume that we don’t want to get into this function, which fortunately is simple: we just need to provide the input “pi”.
Moving on to the second if
statement, it takes another user input, converts pi_value
to a string and slices it up to random_0_to_1000
decimal places. If the user’s input does not match the first random_0_to_1000
digits of π, the script ends. This is the same random number that was printed when we first connected to the machine through netcat, so we can just enter that as our second input.
The third if
statement is a little conceptually difficult to understand. It loops through each character in flag.txt
(This is a good sign, means we’re getting close!). For each character, its ASCII value (char_to_hex(character)
) determines how many times the loop runs. For example, based on the pattern for the other flags we can safely assume that the first five digits will be vere{
, so we can expect the first character v
to be looped over 118 times, and so on. For each iteration, it asks for user input and checks if it matches the corresponding digit of π. For instance, the first input should be 3
. If the server doesn’t respond, then that means that it is waiting for more. So the user inputs 3.
, then 3.1
, then 3.14
, and so on. When the server finally sends a response (just a “y” for some reason), that is how we know we reached the value of the first digit! It then waits for a response on the next digit, so we restart the process of sending one more digit of pi at a time, repeating until we’ve enumerated the entire flag.
Depending on how long the flag is, this could take many thousands of requests. It’s out of the question to manually proceed beyond the first two if
statements. My solve script is included at the bottom of this writeup, it simply automates this process.
Running the solve script takes several minutes. There are a LOT of requests (just over 4000) that need to be sent, and network jitter can cause the strict ordered expected input to fail. Tweaking the values a little, I found that a timeout of 0.01 second tends to be too fast for the server to handle, but it could mostly handle 0.02 second requests. I set it to be 0.03 seconds just to be safe.
We’re not done! I don’t know if I missed a part of the obfuscated script, or more likely it was the way my solve script handled determining a found character, but when I run the solve script, I am presented with the following output:
udqdzH^g/o2^x/t^`tsnl3s2c,sg0r^0/0^7817b801/`d7|
Hmm, well that doesn’t seem like a valid flag. Looking more closely at the first five characters, which I know to be vere{
, I noticed that the corresponding first five characters of the extracted flag are udqdz
. The two repeating d
’s caught my eye, since they are in the same location as the two repeating e
’s in vere{
. d
happens to be 1 character before e
, so I adjusted the extracted flag values by adding 1 to each, and resulted with something that looked much better!

I didn’t want to do this by hand, so here’s a quick secondary script that I used to parse it:
def convert_to_ascii(input_string):
# Split the input string into individual numbers
numbers = input_string.split()
# Shift each number by 1, convert it to an integer, and then to ASCII
ascii_characters = []
for num in numbers:
num = int(num) + 1 # Add 1 to each number
ascii_characters.append(chr(num)) # Convert the number to an ASCII character
# Join the list of characters into a single string and return it
return ''.join(ascii_characters)
# The flag!
input_string = "117 100 113 100 122 72 94 103 47 111 50 94 120 47 116 94 96 116 115 110 108 51 115 50 99 44 115 103 48 114 94 48 47 48 94 55 56 49 55 98 56 48 49 47 96 100 55 124"
# Call the function and print the result
result = convert_to_ascii(input_string)
print(result)
TL;DR: The challenge was a highly obfuscated program requiring 3 inputs (the last being repeated many times) in order to get the flag:
- Initial response of “pi”.
- Correct π digits for a randomly chosen length.
- For each flag character, send π digits up to its ASCII value.
The Flag
vere{I_h0p3_y0u_autom4t3d-th1s_101_8928c9120ae8}
Solution Script
import socket
import mpmath
# Set precision to 1000 decimal places
mpmath.mp.dps = 1000
pi_digits = str(mpmath.mp.pi) # Get π as a string
# Server details
HOST = "172.16.16.7"
PORT = 51358
def receive_data(s, buffer_size=1024):
"""Receives data from the socket until newline or EOF."""
data = b""
while True:
chunk = s.recv(buffer_size)
if not chunk:
break # Connection closed
data += chunk
if b"\n" in chunk:
break # Stop at newline
return data.decode().strip()
def solve_challenge():
extracted_flag = ""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
# Step 1: Receive the first prompt (random number request) and send "pi"
received_data = receive_data(s)
print(f"Received digits: {received_data}")
s.sendall(b"pi\n")
print("Sent 'pi'")
# Step 2: Receive a random number, convert it to int, and send π digits
received_number = received_data
print(f"Server requested {received_number} digits of π")
requested_pi_digits = int(received_number)
pi_to_send = pi_digits[:requested_pi_digits] + "\n"
print (pi_to_send)
s.sendall(pi_to_send.encode())
print(f"Sent π up to {requested_pi_digits} decimal places")
# Step 3: Extract the flag dynamically
# For each character in flag.txt, its ASCII value (char_to_hex(character)) determines how many times the loop runs.
# It wil wait to accept a digit of pi, starting at index 0 or "3", then increment up.
# The server will respond with either a number or no response.
# If there is no response, that means to go to the next index which is ".", and so on
# If the server responds with a number, that means that I had found the correct letter,
# This letter should be logged, and the process should repeat again from index 0
# Step 3: Extract the flag dynamically
pi_index = 0
s.settimeout(0.03) # Set a timeout for receiving data
while True:
digit_to_send = pi_digits[pi_index].encode() + b"\n"
ascii_value = ord(pi_digits[pi_index]) # Get ASCII value of the digit THIS DOESNT WORK
s.sendall(digit_to_send)
print(f"Sent π digit: {pi_digits[pi_index]} (Index {pi_index}) | Extracted flag so far: {extracted_flag}")
try:
response = receive_data(s).strip() # Strip spaces/newlines
print(f"Received: " + response)
except socket.timeout:
print("No response, trying next digit...")
pi_index += 1
continue
if len(response) == 1: # Response is a single character
# + pi_index
extracted_flag += " " + f"{pi_index}"
print(f"Extracted flag so far: {extracted_flag}")
if response == "}": # Stop if flag is complete
break
print(f"Found character: {response} (ASCII: {ord(response)})")
pi_index = 0 # Restart for the next character
else:
pi_index += 1 # Move to the next π digit
print(f"Final Flag: {extracted_flag}")
# Run the solver
solve_challenge()