GreyCTF 2025

Formula739137

MISC | Finals

So we are given with this nice image of a Mercedes W16.

To begin with, let's first understand that a PNG file is made up of chunks.

Here are some common chunk types that are good to know:

  • IHDR - Image header

  • IDAT - Image data

  • IEND - Image end marker

Every chunk follows a unified structure:

Running PNG-Check on the file reveals suspicious IDAT chunks of 9 bytes at the end of the image, as shown below.

Let's extract these chunks and take a quick look.

First Sus Chunk Offset = 0x160f2d = 1445677

1445677 - 4 = 1445673 (to include the length since the offset is where the Type string is detected)

For easier viewing, let's just list it in 21 bytes:

  • Data length = 9

  • Then Total chunk size = 4 + 4 + 9 + 4 = 21 bytes

It is observed that the flag is found by concatenating the last byte of each chunk from offset 0x0000508e onwards.

After solving the challenge, I was told that there was an easter egg in the challenge, and here you go:

└─$ grey{m3rced3s_1s_my_f4v_team!}

Reversing 101

EZPZ | Qualifier

The given binary was decompiled and analyzed, revealing three primary functions:

  • Function a : A length function that calculates the size of the input.

  • Function b : A key generation function that produces the internal RC4 key.

  • Function c : The RC4 encryption/decryption function.

The overall flow is as follows: the input is first processed by Function a to check that its length is exactly 5 characters, then encrypted by Function c using the key generated from Function b. The result is then compared against the encrypted byte array:

To obtain the return value of Function b using GDB, follow these steps:

  1. Launch GDB with the binary: gdb ./chal

  2. Set a breakpoint at the start of Function B: b *0x00401217

  3. runs until Function B returns: finish

  4. print the returned key value:

Note:

  • On x86 (32-bit), the return value is stored in the eax register.

  • On x86-64 (64-bit), the return value is stored in rax.

Then decrypt the encrypted byte array by running the code below.

Notsus.exe

Forensics | Qualifier

Insert Guessy forensics challenge description here

We are given a password-protected ZIP file that contains two files:

  • not_sus.exe

  • flag.txt.yorm

Initial attempts to brute-force the ZIP using pkcrack didn’t work. So, I shifted focus to another common ZIP attack: the known-plaintext attack.

A great reference for this type of attack is this write-up from IEEE VIC-3 CTF 2024.

The idea behind a known-plaintext attack on ZIP files is that you know some part of the original content, and you can use that to recover the decryption key.

In this case, the .yorm file type was unfamiliar, so I focused on the not_sus.exe instead.

Most Windows executables start with a typical DOS MZ header containing the following:

Offset
Bytes
Meaning (GPT Generated)

0x00

0x4D 0x5A

"MZ" signature β€” Magic number for DOS/Windows executables.

0x02

0x90 0x00

Bytes on last page β€” 0x0090 = 144 bytes

0x04

0x03 0x00

Pages in file β€” File is 3 * 512 = 1536 bytes total.

0x06

0x00 0x00

Relocation entries β€” Number of relocation entries (0 here).

0x08

0x04 0x00

Header size β€” In 16-byte paragraphs. 0x0004 = 4 * 16 = 64 bytes

0x0A

0x00 0x00

Minimum extra paragraphs needed β€” Typically zero.

0x0C

0xFF 0xFF

Maximum extra paragraphs needed β€” 0xFFFF indicates β€œas much memory as available”.

0x0E

0x00 0x00

Initial SS (stack segment) β€” Relative to the load segment (0 here).

Using this known header as plaintext, I ran a known-plaintext ZIP attack using bkcrack

After decrypting the ZIP, we now have:

  • not_sus.exe

  • flag.txt.yorm β€” contents look like gibberish, likely encrypted or encoded.

Running not_sus.exe through Detect It Easy (DIE) reveals that it’s a PyInstaller-packed Python executable.

DetectItEasy

We can unpack the executable with PyInstxtractor and decompiling the obtained .pycwith PyLingual

Looks like another RC4.

Recover the flag :

└─$ grey{this_program_cannot_be_run_in_dos_mode_hehe}

SGRPC

Web | Qualifier

Can't get hacked if they can't reach it.

This challenge revolves around a gRPC server written in Go, exposing two main RPC methods via the Flag service:

  • Hello

  • GetFlag

The server also includes a custom implementation of gRPC Server Reflection with some restrictions enforced in customreflect.go.

Based on the provided source code, the GetFlag RPC requires three specific conditions to be met in the request in order to get the flag.

To identify the required fields, we can enumerate using available gRPC Server Reflection. After some trial-and-error :

Base64 decoding the first file descriptor reveals it's a protoset for gRPC, but it’s not valid.

So let’s try decoding it manually to see what’s actually inside.

With this, we can reconstruct the .proto file ourselves, which we can then use to generate a valid protoset.

└─$ grey{r3fl3ct_th3_sch3m4}

Meowware

Rev | Qualifier | DidNotSolve

We were given two files: client (an executable) and client.pcap (containing encrypted traffic).

When we run the binary, it produces no output:

From this output, we can see that:

  • The binary uses x86-64 architecture (AMD64, Intel 64-bit)

  • It's statically linked

  • It's stripped

By using strace to trace the system calls, we were able to identify that:

  1. The binary is packed with a custom upx (notice the "upX" and "upx" memfd names)

  2. It unpacks itself during runtime

  3. It creates in-memory files (memfd_create), writes unpacked content to them, and executes them via mmap

Since we know the binary dumps the unpacked executable into in-memory files, we can try to dump the memory during runtime to obtain the unpacked binary.

From the strace output, we saw:

  • Start address: 0x400000

  • Size: 1062920 bytes = 0x103000 in hex

Last updated