Home Flare-On 10 Challenge 7 (Nuitka)
Post
Cancel

Flare-On 10 Challenge 7 (Nuitka)

This year’s Flare-On includes a Python challenge. Similar to the previous year, it can be exploited quite extensively, but it is slightly more intricate than a straightforward memory dump, as was the case last year. In this blog post, I will cover how to effectively solve this challenge and provide insights into dealing with Nuitka files in general.

Initial Impressions

When encountering a Nuitka file, many individuals may not know how to proceed.

Analyzing the assembly code within a native disassembler (such as x64dbg) can be a daunting task, especially when you realize that Python code converted to C code is not easy to comprehend. You can perform external inspections by using tools like Procmon to monitor system calls and disk access, or HTTPDebugger for analyzing network requests.

Nonetheless, the first step you should take is to determine how much of the original Python program remains. Any information you can extract will prove invaluable during the reverse engineering process. The tool of choice for this purpose is PyInjector. With PyInjector, you can effortlessly execute Python code within the executable’s Python runtime. Since Nuitka needs to preserve certain metadata in Python to enable library calls and other functionalities to function correctly, we can expose that metadata.

Attacking the Executable

Let’s begin by creating a file called code.py, which is what PyInjector searches for when seeking the code to execute. Our file structure will appear as follows:

1
2
3
4
5
└───flake
    │   demo_conf.txt
    │   flake.exe
    │   mail.txt
    │   code.py

To start some standard reconnaissance, we can execute print(dir()). This command will list all the available names in the current scope. You can perform this action by injecting the PyInjector DLL into the flake.exe process. To do this, you can utilize a tool like Process Hacker 2.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
['ACTIVE', 'ALL', 'ANCHOR', 'ARC', 'ARC4', 'BASELINE', 'BEVEL', 'BOTH', 'BOTTOM', 'BROWSE', 'BUTT', 'BaseWidget', 'BitmapImage', 'BooleanVar', 
'Button', 'CASCADE', 'CENTER', 'CHAR', 'CHECKBUTTON', 'CHORD', 'COMMAND', 'CURRENT', 'CallWrapper', 'Canvas', 'Checkbutton', 'Config', 'DISABLED',
'DOTBOX', 'Direction', 'DoubleVar', 'E', 'END', 'EW', 'EXCEPTION', 'EXTENDED', 'Entry', 'Event', 'EventType', 'FALSE', 'FIRST', 'FLAT', 'Food',
'Frame', 'GROOVE', 'Grid', 'HIDDEN', 'HORIZONTAL', 'INSERT', 'INSIDE', 'Image', 'ImageTk', 'IntVar', 'LAST', 'LEFT', 'Label', 'LabelFrame', 'Listbox',
'MITER', 'MOVETO', 'MULTIPLE', 'Menu', 'Menubutton', 'Message', 'Misc', 'N', 'NE', 'NO', 'NONE', 'NORMAL', 'NS', 'NSEW', 'NUMERIC', 'NW',
'NoDefaultRoot', 'OFF', 'ON', 'OUTSIDE', 'OptionMenu', 'PAGES', 'PIESLICE', 'PROJECTING', 'Pack', 'PanedWindow', 'PhotoImage', 'Place', 'RADIOBUTTON',
'RAISED', 'READABLE', 'RIDGE', 'RIGHT', 'ROUND', 'Radiobutton', 'S', 'SCROLL', 'SE', 'SEL', 'SEL_FIRST', 'SEL_LAST', 'SEPARATOR', 'SINGLE', 'SOLID',
'SUNKEN', 'SW', 'Scale', 'Scrollbar', 'Snake', 'Spinbox', 'Square', 'StringVar', 'TOP', 'TRUE', 'Tcl', 'TclError', 'TclVersion', 'Text', 'Tk',
'TkVersion', 'Toplevel', 'UNDERLINE', 'UNITS', 'VERTICAL', 'Variable', 'W', 'WORD', 'WRITABLE', 'Widget', 'Wm', 'X', 'XView', 'Y', 'YES', 'YView',
'__annotations__', '__builtins__', '__cached__', '__compiled__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'base64',
'btn_submit_clicked', 'builtins', 'canvas', 'change_direction', 'check_collisions', 'check_snake_length', 'constants', 'data', 'dataclass', 'enum',
'file', 'food', 'game_over', 'game_win', 'get_flag', 'getboolean', 'getdouble', 'getint', 'image_names', 'image_types', 'img_flare', 'img_flare_down',
'img_flare_left', 'img_flare_right', 'img_flare_up', 'json', 'lbl_player_score', 'lbl_top_player', 'mainloop', 'next_turn','open_new_top_player_window',
'os', 'random', 're', 'read_config', 'scale_window', 'score', 'shame', 'snake', 'strval_email', 'sys', 'wantobjects', 'wndw_root', 'xk']

When you run this code, you’ll see a bunch of names that have been defined. Since we can see traces of Tkinter, it is safe to assume that they used something like from tkinter import *, which rapidly populates the local names. The names written in all capital letters can be disregarded, as they are likely related to Tkinter. Some intriguing names include get_flag, game_win, and score. However, it remains uncertain whether these names are variables, functions, or something else. You can deduce their nature by conducting a straightforward print test.

1
2
print(get_flag)
# <compiled_function get_flag at 0x000001DF0E3D1310>

Upon execution, it becomes evident that get_flag is a compiled function. Let’s attempt to call it.

1
TypeError: get_flag() missing 1 required positional argument: 'xk'

An error message informs us that the function requires an argument called “xk,” which we recognize from when we listed all the names in scope. This argument corresponds to the variable name, as a quick check confirms:

1
2
print(xk)
# b'\x1b\xba\x8c\x1b'

This looks like a seemingly random byte string and could be used as a private key of some sort. Before delving deeper into the code, let’s attempt to call the function with xk as its argument:

1
2
print(get_flag(xk))
# [email protected]

And here is our flag… I was not expecting it to be this easy. Out of curiosity let’s look at the other interesting name we found, game_win. Assuming it’s a function, we can call it:

1
print(game_win())

Upon running this code, a Tkinter window pops up, revealing the flag. This approach turned out to be even easier than manually invoking the get_flag function.

Exposing Python bytecode

In the end, we successfully resolved it by making educated “guesses” regarding the required function and its arguments (the function names were quite helpful though :D). If we want to understand the inner workings of the get_flag function, we can attempt the conventional approach of disassembling the function:

1
2
3
4
import dis

dis.dis(get_flag)
# 

However, it appears that there is no output. The reason for this is quite evident: Nuitka converts the Python code into native code, which cannot be disassembled using Python’s disassembler. You might think this approach is stupid, but many times, you can still access the original Python bytecode. I discussed this in my previous post, “The Stereotype”, where I explained why and how this is achievable. This is also the reason why employing PyArmor and Nuitka as protectors simultaneously is illogical. The PyArmor layer enables us to effectively bypass Nuitka by extracting the Python bytecode.

Alternative solutions

Special thanks to GiveAcademy for discovering this clever method of executing Python code without the need for DLL injection. He found this trick while analyzing the program using Procmon, the process searches for a file named .flake.exe.py.

Procmon Screenshot

After creating this specific file, it appears that the program executes the code contained within it. The reason behind this behavior is not entirely clear, but I assume it’s a Tkinter quirk, as it also searches for a file named .Tk.py.

The script he created looked like this:

1
print(f'#{locals()["sys"].modules["flag"].get_flag(locals()["sys"].modules["__main__"].xk)}')

Effectively, he arrived at the same solution as I did by calling get_flag(xk), albeit he accesses these elements through the locals function.

EDIT: Mandiant (the creators of Flare-On) has published their official solution to the challenge, it’s safe to assume they didn’t know you could solve it like this ;D. You can read it here.

This post is licensed under CC BY 4.0 by the author.
Trending Tags