[misc/Digital Instrumentality Project] PolyU x NuttyShell Cybersecurity CTF 2025 Writeup

# Challenge Details
<table>
  <tr>
    <td>Name</td> <td>NuttyShell File Manager</td>
  </tr>
    <tr>
    <td>Author</td> <td>Karma</td>
  </tr>
  <tr>
    <td>Final Solves</td> <td>8</td>
  </tr>
  <tr>
  <td>Final Points</td> <td>487</td>
  </tr>
  <tr>
  <td>Description</td>
  <td>Episode 26 of Evangelion... but for computers! Files are losing their 'ego' and turning into LCL of 1s and 0s. All hope is not yet lost because the files are still separated by 'buffers' of 00s. Restore 'ego' of 3 files and 'carve' them out.</td>
  </tr>
</table>

# Introduction
We are given a weird file named **"Fanta_file_new"**, and the challenge description tells us to "carve" exactly $3$ files out...

Running `strings` on the file, we find that along with a bunch of random characters, there also exists delimeters "File ends right before this sentence" and "File starts right after this sentence"! We can use this to split the file into $3$ individual files.

Asking Gemini-2.0-Flash to make such python program to split files, it came up with this:
```py
def split_file(input_file, output_file_prefix="output", split_string="File ends right before this sentence"):
    """
    Splits an input file into multiple output files based on a delimiter string,
    handling potential decoding errors.

    Args:
        input_file (str): Path to the input file.
        output_file_prefix (str, optional): Prefix for the output file names. Defaults to "output".
        split_string (str, optional): Delimiter string to split the file by. Defaults to "File ends right before this sentence".
    """

    file_count = 1
    current_output_file = None

    try:
        with open(input_file, 'rb') as infile:  # Open in binary mode to handle all characters
            file_content = infile.read()

        # Split the binary content by the split string (encoded as bytes)
        split_string_bytes = split_string.encode('utf-8')  # Encode split string to bytes
        parts = file_content.split(split_string_bytes)

        # Iterate through the parts and write them to separate files
        for i, part in enumerate(parts):
            output_filename = f"{output_file_prefix}_{file_count}.txt"
            try:
                with open(output_filename, 'wb') as outfile:  # Open in binary write mode
                    # If it's not the first file, prepend the split string
                    if i > 0:
                        outfile.write(split_string_bytes)
                    outfile.write(part)  # Write the binary content directly
                print(f"File '{output_filename}' created successfully.")
                file_count += 1
            except Exception as e:
                print(f"Error writing to file '{output_filename}': {e}")

    except FileNotFoundError:
        print(f"Error: Input file '{input_file}' not found.")
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == '__main__':
    input_filename = "input.txt"  # Replace with your input file name
    split_file(input_filename)
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/1182cbf6-1ae0-41b4-be87-9dec9b552f0d.png)

We run it to get 3 perfectly split files...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/c4d6154d-b347-4b41-a6f2-4e572b955c02.png)

# The First File
Let's first take a look at `output_1.txt`! It seems to contain an IHDR header :thinking: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/46b05654-2d65-4ace-aed4-d129ea5c66a0.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/a5dcd99d-a086-4b9c-ad32-14dadd927171.png)

Google seems to suggest that it's a PNG file, however, it does seem to be missing some PNG file signature/magic bytes, and indeed, when we try to rename the file to `output_1.png`, the image doesn't load!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/68618f40-26e1-448c-ac44-b28ecbf80a18.png)

We can add back the signature in front of IHDR...

```py
with open("output_1.txt", "rb") as f:
    data = f.read()

png_signature = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])

ihdr_pos = data.find(b'IHDR')
chunk_start = ihdr_pos - 4

with open("recovered1.png", "wb") as f:
    f.write(png_signature + data[chunk_start:])
```

Now when we open the file, it indeed loads an image! However, the image doesn't seem to show any flags, so let's run it through my favourite stego tool for images: [AperiSolve](https://www.aperisolve.com/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/7303ecb6-37fc-40c8-a710-488928e42b93.png)

[At the bottom of the AperiSolve analysis](https://www.aperisolve.com/c57eed150f9dcc0e8a6bc83a677c187e), the zsteg tool seems to have recovered part of the flag!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/97a67494-6bd7-469f-b970-d8455de9705c.png)

First part: `PUCTF25{1st_Rule_of_fite_club_is_`

# The Second File
Looking into `output_2.txt`, the first line reads `mimetypeapplication/vnd.oasis.opendocument.text` after "File starts right after this sentence", which suggest that this is of the `.odt` format.

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/a5c7b286-cf8e-4d28-b6b8-9b33d931a499.png)

Being too lazy to do anything, I tried to run binwalk on `output_2.txt`, which succeeded!

```sh
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
DECIMAL                            HEXADECIMAL                        DESCRIPTION
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
393                                0x189                              ZIP archive, file count: 18, total size: 12780 bytes
---------------------------------------------------------------------------------------------------------------------------------------------------------------------
[+] Extraction of zip data at offset 0x189 completed successfully
--------------------
```

```brainfuck
extractions/output_2.txt.extracted
└── 189
    ├── Configurations2
    │   ├── accelerator
    │   ├── floater
    │   ├── images
    │   │   └── Bitmaps
    │   ├── menubar
    │   ├── popupmenu
    │   ├── progressbar
    │   ├── statusbar
    │   ├── toolbar
    │   └── toolpanel
    ├── content.xml
    ├── manifest.rdf
    ├── media-type
    ├── META-INF
    │   └── manifest.xml
    ├── meta.xml
    ├── settings.xml
    ├── styles.xml
    └── Thumbnails
        └── thumbnail.png
```

This looks quite interesting and promising at the same time, particularly the file `thumbnail.png` displays the following text:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/431a959a-044b-484f-bd26-995533741b00.png)

More interestingly, looking at `media-type` we find the following text:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/77d8dde2-f770-49bf-9af9-8fb967bb7b64.png)

This seems to be base64 but with lots of `09` in between! We can recognise it from the padding `=` and charset.

Removing the `09`s and decoding it from Base64, we don't get anything too interesting...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/fa029cc4-1823-410d-9ac4-46be92ea234e.png)

However, `thumbnail.png` mentions XOR, so let's try that too!

`Key = 09` seems to have decrypted the text, showing `to_nEvEr_w0rry_and_havE_funn_`!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/1822db25-d9b5-4714-bda4-665fbd3dd37f.png)

Second part: `to_nEvEr_w0rry_and_havE_funn_`

# The Third File
`output_3.txt` seems to be an mp4 file, as shown by the first line within it!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/fd1fed69-e483-4a22-81f6-35f805ee8617.png)

Recovering the mp4 by removing stuff in front of its file signature, we just find a video without anything particularly interesting...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/9069d109-e30b-4374-9b02-cc2779500d2b.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/72c3eec6-305b-45b0-87f5-3da4a8549f14.png)

Looking at `output_3.txt` in more detail, we find this very interesting piece of code...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4067930/a9535bd5-c60f-47d4-aa4f-d8ae21efc736.png)

It appears that we can just run the file to obtain the last part of the flag... As running knuth function in python was a bit too slow, we asked ChatGPT to recreate the function in c++, which returned `238837843`.

```cpp
#include <cstdint>
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>

uint32_t knuth(uint32_t seed) {
    uint32_t result = seed;
    for (uint32_t i = 1; i < 1000000000u; ++i) {
        // All operations wrap modulo 2^32 automatically on uint32_t overflow
        result = result * i + 3;
        result = result ^ (result >> 3);
        result = result * 2654435761u;
    }
    return result;
}

int main() {
    uint32_t r = knuth(95714287u);
    std::cout << r << std::endl;
}
```

Putting the result back into the python file, we get the last part of the flag!

```py
def flag():
    result = 238837843
    hex_string = hashlib.sha256(str(result).encode()).hexdigest()[:32]
    return hex_string

if __name__ == '__main__':
    print(str(flag())+'}')
```

Final part: `3601bc5bcef53e15254046faf34ef764}`

# Combining the parts

First part: `PUCTF25{1st_Rule_of_fite_club_is_`
Second part: `to_nEvEr_w0rry_and_havE_funn_`
Final part: `3601bc5bcef53e15254046faf34ef764}`

`PUCTF25{1st_Rule_of_fite_club_is_to_nEvEr_w0rry_and_havE_funn_3601bc5bcef53e15254046faf34ef764}`