Helped a Homie with BITSCTF Using Python Internals

Tags: #programming #python

A random Sunday at home, I get a call from my friend skye (asli naam nhi likhne de rha (he is not letting me write his original name)). He was, as usual, fiddling with cybersec stuff, sparing time from his busy gaming schedule. Initially, I ignored the call, but then answered. He was participating in BITSCTF '25. He quickly asks me to have a look at a python file he sent me.

I, excited, opened the file, and I see the following Python one-liner:

# Python obfuscation by freecodingtools.org
                    
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));exec((_)(b'==QfWoizP8/vvPv/tu.........a lot of random stuff...........WiUxyW7lVwJe'))

aahhhhh... so much random characters

The goal was to find a flag.

Let's first understand what the code is doing:

  • First, a lambda function is being used. A lambda function allows definition of a function in a single line. It takes an argument (here __). It is followed by : which defines what the function should return. The function is assigned into a variable _.

  • __import__('zlib').decompress(...) is equivalent to the following, thanks to Python's metaprogramming functions:

import zlib
zlib.decompress(...)
  • Similarly, __import__('base64').b64decode(...) is equivalent to the following:
import base64
base64.decode(...)
  • __[::-1]), as everyone knows, reverses the string __ (which is the argument for the function).

  • ;: A lot of people don't know, but Python supports semicolons. It, like in any other languages, can be used to separate two statements. It's not necessary, and also not a standard, but is useful to make one-liners (as here two python statements are there in a single line)

  • exec(...): It treats strings as Python statements, for example exec("print('Hello World')") would treat the string as a Python statement and execute it, resulting in output Hello World.

  • The random string which is given is actually encoded string, given as a raw binary string (base64 takes binary strings by default). The _ function is thus called on the random string, passing the random string as the argument __.

In essence, the function first reverses the string passed to it, decodes it via base64, and then decompresses it via zlib. This is called on the random string.

I was feeling sus(picious) about the code as it is random string, and upon decoding, it could be any malicious code, and it is being run by exec. But, I took the riks, and executed the code. What output do I get?

Hello, World!

Looks like a normal "Hello World!", huh

I replaced the exec with a print to see what code is being executed, instead of seeing the "Hello World!" output of the code:

# Python obfuscation by freecodingtools.org
                    
_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));print((_)(b'==QfWoizP8/vvPv/tu.......................a lot of random stuff............................WiUxyW7lVwJe'))

Running this, I get the output:

b"exec((_)(b'=cE3wh5B//7.....a lot of random stuff..............xyWzlNwJe'))"

W-w-whaaaatttt? The output is also a new python code using that function. I can't just exec it again. I need to find the stuff encoded in it. So, I strip out the b"exec( at start and )" at the end, to retrieve the contained string, and not execute it. I add the print statement to display it. In essence, I executed this:

_ = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]));
print((_)(b'=cE3wh5B.........a lot of random stuff.....yWzlNwJe'))

The string passed in this is not the string given initially. This string is the one generated by the run of program.

Executing the above code, I get the output:

b"exec((_)(b'==AiELbHP4///....a lot of random stuff......Bt7jfZBVgcxyW0lVwJe'))"

aaaahhhhh.... another one

How long should this go? I visited the website given in comment in the initial program: freecodingtools.org. From the homepage, I found "Python Obfuscator", which says:

Python code is ran through this algorithm 100s of times, each time making it more secure and unreadable.

100s? aah. I wrote a for loop to automate this decoding the string.

But where to break the loop? The loop should break when the decoding is no longer possible, i.e. an exception is raised by any of the decoding functions base64decode or zlib.decompress

This is the loop I wrote:

decode_obfuscated = lambda __ : __import__('zlib').decompress(__import__('base64').b64decode(__[::-1]))
string = '==AiELbHP4///....a lot of random stuff......Bt7jfZBVgcxyW0lVwJe'

for i in range(100):
    try:
        temp = decode_obfuscated(string)
        string = temp[11:-3]
        
    except:
        print(temp)
        print(string)
        break
  • The string slicing [11:-3] strips out b"exec((_)(b' from start and '))" from end, to get the string of new iteration.
  • When the decompression or decoding is no longer possible, exception is raised and loop is terminated via 'break' statement.

Running the program:

b'# Online Python Compiler\n\nprint("Hello, World!")\n# BITSCTF{obfuscation_and_then_some_more_obfuscation_4a4a4a4a}'
b'thon Compiler\n\nprint("Hello, World!")\n# BITSCTF{obfuscation_and_then_some_more_obfuscation_4a4a4a'

Flag found:

obfuscation_and_then_some_more_obfuscation_4a4a4a4a, bye