Writeup StarHack CTF: Challenge - Chaos

Writeup StarHack CTF: Challenge - Chaos#

I participated in StarHack CTF 2025, and here is the write-up of the “Chaos” challenge that I solved.

This challenge was in the “Binary” category; we were asked to exploit a binary to get a flag in the format flag{...}. We had access to the compiled Linux binary and a netcat session to run the binary on the CTF servers.

Binary analysis#

First step I looked at basic information about the binary with the command file chaos to get some information:

Terminal window
chaos: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e2937d93ac55bc47823fba02aa98e4b407ea01a3, for GNU/Linux 3.2.0, not stripped

The binary is a 64-bit ELF for Linux (nice because I’m on NixOS ❄️), not stripped, which is good news because we can have function names.

I quickly check with the command strings if there are interesting strings:

Terminal window
Enter password:
Error reading input.
Password too short.
Executing VM...
Invalid password.
···
Welcome to the multi-stage VM challenge!
Can you decrypt and execute the hidden bytecode?
Patching bytecode with input...
:*3$"
UGCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0

We see that it is a Virtual Machine (VM) challenge, where we must decrypt and execute hidden bytecode. The program asks for a password, and there are error messages for input reading and if the password is too short.

If I search references for “flag” we find notably the print_flag function and the flag.txt file.

Terminal window
$ strings chaos | grep flag
flag.txt
Error: Could not read flag. Contact admin.
print_flag

Analysis with Ghidra#

I then use ghidra to analyze the binary more in depth.

NOTE

I don’t do CTFs much anymore and I don’t necessarily master all the tools.

ghidra along with other tools are part of the essentials for CTF challenges, that’s why I created a small nix-shell with basic tools for all categories. You can find it here:

cedev-1
/
Nix-CTF
Waiting for api.github.com...
00K
0K
0K
Waiting...

As we saw earlier with the file command, the binary is not stripped, so we have function names, which is super convenient. I directly find the main function in the Functions menu, which is the program’s entry point with the main logic.

undefined8 main(void) {
char *pcVar1;
size_t sVar2;
void *__ptr;
long lVar3;
undefined8 uVar4;
char local_488 [64];
undefined1 local_448 [1024];
undefined4 local_48;
void *local_40;
undefined4 local_38;
undefined1 *local_30;
int local_28;
setbuf(stdout,(char *)0x0);
setbuf(stdin,(char *)0x0);
setbuf(stderr,(char *)0x0);
print_banner();
__printf_chk(1,"Enter password: ");
pcVar1 = fgets(local_488,0x40,stdin);
if (pcVar1 == (char *)0x0) {
puts("Error reading input.");
uVar4 = 1;
}
else {
sVar2 = strcspn(local_488,"\n");
local_488[sVar2] = '\0';
sVar2 = strlen(local_488);
if (sVar2 < 3) {
puts("Password too short.");
uVar4 = 1;
}
else {
__ptr = malloc(0x17);
lVar3 = 0;
do {
*(byte *)((long)__ptr + lVar3) = (&encrypted_stage2)[lVar3] ^ 0xaa;
lVar3 = lVar3 + 1;
} while (lVar3 != 0x17);
puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0];
*(char *)((long)__ptr + 10) = local_488[1];
*(char *)((long)__ptr + 0x11) = local_488[2];
puts("Executing VM...\n");
local_48 = 0;
local_38 = 0;
local_28 = 0;
local_40 = __ptr;
local_30 = local_488;
vm_execute(local_448);
if (local_28 == 0) {
puts("Invalid password.");
}
free(__ptr);
uVar4 = 0;
}
}
return uVar4;
}

So now we will try to understand the program logic and follow the execution flow.

First interesting thing, we see there is a print_banner() function that displays at program start.

banner

We notice some information:

  • it’s a multi-stage VM
  • we must decrypt and execute the hidden bytecode

The code is relatively simple:

void print_banner(void)
{
puts(&DAT_004020f8);
puts(&DAT_004021a8);
puts(&DAT_004021e0);
putchar(10);
puts("Welcome to the multi-stage VM challenge!");
puts("Can you decrypt and execute the hidden bytecode?");
putchar(10);
return;
}

Password#

So classic start, buffer initialization, banner display, then it asks for a password.

pcVar1 = fgets(local_488,0x40,stdin);
if (pcVar1 == (char *)0x0) {
puts("Error reading input.");
uVar4 = 1;
}

Input is read with fgets and an error is printed if it fails.

Then it becomes more interesting, we check the password length:

sVar2 = strcspn(local_488,"\n");
local_488[sVar2] = '\0';
sVar2 = strlen(local_488);
if (sVar2 < 3) {
puts("Password too short.");
uVar4 = 1;
}

So the password must be at least 3 characters (we’ll keep that in mind for now).

We then allocate 23 bytes (0x17 in hexadecimal) — not sure yet why.

else {
__ptr = malloc(0x17);

And here we reach the interesting part, we loop over 23 (23 bytes) and for each byte we XOR with 0xaa from an encrypted_stage2 array.

so:

  • there is encrypted data somewhere in the binary (encrypted_stage2)
  • we decrypt it with XOR 0xaa
  • the result goes into the allocated memory

Even more interesting!

puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0];
*(char *)((long)__ptr + 10) = local_488[1];
*(char *)((long)__ptr + 0x11) = local_488[2];
  • the first character goes to position 3 of the decrypted bytecode
  • the second character to position 10
  • the third character to position 17 (0x11 in hexadecimal)

So my password directly modifies the code that will be executed!

puts("Executing VM...\n");
local_48 = 0;
local_38 = 0;
local_28 = 0;
local_40 = __ptr;
local_30 = local_488;
vm_execute(local_448);

We initialize some variables, then call vm_execute which will run the patched bytecode.

if (local_28 == 0) {
puts("Invalid password.");
}

Another interesting thing, if local_28 is equal to 0 after bytecode execution, we print “Invalid password”.

encrypted_stage2#

Now that I understand the mechanism, I need to find this encrypted_stage2.

encrypted_stage2 is defined as a global variable.

We can use gdb to extract these bytes directly from the program memory.

in gdb, we can do:

x/23xb &encrypted_stage2

small explanations:

  • x to examine memory
  • /23 to read 23 bytes
  • xb to display in hexadecimal (byte by byte)
  • &encrypted_stage2 is the address of the variable.

gdb-encrypted-stage2

Here is the output so in the left column we have memory addresses then on the right the 23 encrypted bytes.

Now that I extracted the encrypted data, I must decrypt them with XOR 0xAA as in the code.

Here is a small bash script to do that:

#!/bin/bash
encrypted="ab e8 ab aa af ac a5 ab 99 ab aa af ac a2 ab da ab aa af ac ab ad 55"
echo "$encrypted" | tr ' ' '\n' | while read byte; do
printf "%02x " $((0x$byte ^ 0xaa))
done
echo

xor-decrypt

Analysis of the decrypted bytecode#

Looking at this decrypted bytecode we notice a clear pattern:

Terminal window
01 42 01 00 05 06 ...
... 01 33 01 00 05 06 ...
... 01 70 01 00 ...

The positions that the program patches (3, 10, and 17) are initially 0x00.

Repetitive pattern: we see the sequence 01 XX 01 00 05 06 which repeats 3 times.

The VM logic seems to be a comparison. Just before each 0x00, we find the bytes 0x42, 0x33 and 0x70.

  • At position 1, we have 0x42. The VM probably expects this value at position 3.
  • At position 8, we have 0x33. The VM probably expects this value at position 10.
  • At position 15, we have 0x70. The VM probably expects this value at position 17.

The correct password must therefore be composed of the ASCII characters corresponding to these hex values:

  • 0x42 = ‘B’
  • 0x33 = ‘3’
  • 0x70 = ‘p’

The password is B3p.

Testing the solution#

Terminal window
echo "B3p" | nc 15.237.107.187 1029

flag

We get the flag!

Conclusion#

Here is the write-up for this challenge, I found the flag. For other StarHack CTF 2025 challenges I solved, I may publish them if I have time.

I finished 24th in the overall ranking, not bad for a CTF comeback!

If you want to practice on this challenge the public instance is no longer available, but I can still provide the binary!

If you have questions or suggestions, feel free to contact me.

Writeup StarHack CTF: Challenge - Chaos
https://blog.ce-dev.eu/posts/en/writeup-challenge-chaos-ctf/
Author
Cedev
Published at
2025-10-17
License
CC BY-NC-SA 4.0