All Articles

1337UP CTF 2023 Writeup

This page has been machine-translated from the original page.

I participated in the 1337UP CTF, which started on November 17, 2023, as part of 0nePadding, and we placed 32nd overall.

image-20231119161025504

There were so many challenges that we could not solve them all, but I will briefly write up the ones we did solve.

Table of Contents

Obfuscation(Rev)

I think I made my code harder to read. Can you let me know if that’s true?

After unpacking the challenge binary, I found the source code for the obfuscated challenge binary and an encrypted data file named output.

After compiling the obfuscated source once and decompiling it with Ghidra, I found that the main function performs the following processing.

undefined8 main(int param_1,long param_2)

{
  int __n;
  FILE *pFVar1;
  undefined8 uVar2;
  char *__s;
  
  if (param_1 != 2) {
    printf("Not enough arguments provided!");
                    /* WARNING: Subroutine does not return */
    exit(-1);
  }
  pFVar1 = fopen(*(char **)(param_2 + 8),"r");
  if (pFVar1 == (FILE *)0x0) {
    perror("Error opening file");
    uVar2 = 0xffffffff;
  }
  else {
    __n = o_0b97aabd0b9aa9e13aa47794b5f2236f(pFVar1);
    __s = (char *)malloc((long)(__n + 1));
    if (__s == (char *)0x0) {
      perror("Memory allocation error");
      fclose(pFVar1);
      uVar2 = 0xffffffff;
    }
    else {
      fgets(__s,__n,pFVar1);
      fclose(pFVar1);
      o_e5c0d3fd217ec5a6cd022874d7ffe0b9(__s,__n);
      pFVar1 = fopen("output","wb");
      if (pFVar1 == (FILE *)0x0) {
        perror("Error opening file");
        uVar2 = 0xffffffff;
      }
      else {
        fwrite(__s,(long)__n,1,pFVar1);
        fclose(pFVar1);
        free(__s);
        uVar2 = 0;
      }
    }
  }
  return uVar2;
}

The o_e5c0d3fd217ec5a6cd022874d7ffe0b9 function called from there looked like this.

void o_e5c0d3fd217ec5a6cd022874d7ffe0b9(long param_1,int param_2)
{
  int i;
  
  if (param_2 != 0x18) {
                    /* WARNING: Subroutine does not return */
    __assert_fail("o_8ce986b6b3a519615b6244d7fb2b62f8 == 24","chall.c",5,__PRETTY_FUNCTION__.0);
  }
  for (i = 0; i < 0x18; i = i + 1) {
    *(byte *)(param_1 + i) =
         *(byte *)(param_1 + i) ^
         (byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^
         0x37;
  }
  return;
}

From this, we can see that the result of encrypting the flag in the *(byte *)(param_1 + i) ^ (byte)*(undefined4 *)(o_a8d9bf17d390687c168fe26f2c3a58b1 + ((ulong)(long)i % 400) * 4) ^ 0x37 part is written to the output file.

Therefore, I was able to obtain the flag with the following solver.

import struct

obs = b'\x2a\x00\x00\x00\x4d\x00\x00\x00\x03\x00\x00\x00\x08\x00\x00\x00\x45\x00\x00\x00\x56\x00\x00\x00\x3c\x00\x00\x00\x63\x00\x00\x00\x32\x00\x00\x00\x4c\x00\x00\x00\x0f\x00\x00\x00\x0e\x00\x00\x00\x29\x00\x00\x00\x57\x00\x00\x00\x2d\x00\x00\x00\x3d\x00\x00\x00\x10\x00\x00\x00\x32\x00\x00\x00\x14\x00\x00\x00\x05\x00\x00\x00\x0d\x00\x00\x00\x21\x00\x00\x00\x3e\x00\x00\x00\x46\x00\x00\x00\x46\x00\x00\x00\x4d\x00\x00\x00\x1c\x00\x00\x00\x55\x00\x00\x00\x52\x00\x00\x00\x1a\x00\x00\x00\x1c\x00\x00\x00\x20\x00\x00\x00\x38\x00\x00\x00\x16\x00\x00\x00\x15\x00\x00\x00\x30\x00\x00\x00\x26\x00\x00\x00\x2a\x00\x00\x00\x62\x00\x00\x00\x14\x00\x00\x00\x2c\x00\x00\x00\x42\x00\x00\x00\x15\x00\x00\x00\x37\x00\x00\x00\x62\x00\x00\x00\x11\x00\x00\x00\x14\x00\x00\x00\x5d\x00\x00\x00\x63\x00\x00\x00\x36\x00\x00\x00\x15\x00\x00\x00\x2b\x00\x00\x00\x50\x00\x00\x00\x63\x00\x00\x00\x40\x00\x00\x00\x62\x00\x00\x00\x37\x00\x00\x00\x03\x00\x00\x00\x5f\x00\x00\x00\x10\x00\x00\x00\x38\x00\x00\x00\x3e\x00\x00\x00\x2a\x00\x00\x00\x53\x00\x00\x00\x48\x00\x00\x00\x17\x00\x00\x00\x47\x00\x00\x00\x3d\x00\x00\x00\x5a\x00\x00\x00\x0e\x00\x00\x00\x21\x00\x00\x00\x2d\x00\x00\x00\x54\x00\x00\x00\x19\x00\x00\x00\x18\x00\x00\x00\x60\x00\x00\x00\x4a\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5c\x00\x00\x00\x19\x00\x00\x00\x21\x00\x00\x00\x24\x00\x00\x00\x06\x00\x00\x00\x1a\x00\x00\x00\x0e\x00\x00\x00\x25\x00\x00\x00\x21\x00\x00\x00\x64\x00\x00\x00\x03\x00\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x00\x1f\x00\x00\x00\x1f\x00\x00\x00\x56\x00\x00\x00\x5c\x00\x00\x00\x3d\x00\x00\x00\x56\x00\x00\x00\x51\x00\x00\x00\x26\x00\x00\x00'
obs = [struct.unpack("<I", obs[i:i+4])[0] for i in range(0,len(obs),4)]

with open("output", "rb") as f:
    data = f.read()

for i in range(0x18):
    print(chr(data[i]^0x37^obs[i]),end="")

# INTIGRITI{Z29vZGpvYg==}

FlagChecker(Rev)

Can you beat this FlagChecker?

The Rust source code given as the challenge binary was as follows.

use std::io;

fn check_flag(flag: &str) -> bool {
    flag.as_bytes()[18] as i32 * flag.as_bytes()[7] as i32 & flag.as_bytes()[12] as i32 ^ flag.as_bytes()[2] as i32 == 36 &&
    flag.as_bytes()[1] as i32 % flag.as_bytes()[14] as i32 - flag.as_bytes()[21] as i32 % flag.as_bytes()[15] as i32 == -3 &&
    flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 * flag.as_bytes()[11] as i32 - flag.as_bytes()[20] as i32 == 5141 &&
    flag.as_bytes()[19] as i32 + flag.as_bytes()[12] as i32 * flag.as_bytes()[0] as i32 ^ flag.as_bytes()[16] as i32 == 8332 &&
    flag.as_bytes()[9] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[8] as i32 & flag.as_bytes()[16] as i32 == 113 &&
    flag.as_bytes()[3] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[5] as i32 + flag.as_bytes()[6] as i32 == 7090 &&
    flag.as_bytes()[21] as i32 * flag.as_bytes()[2] as i32 ^ flag.as_bytes()[3] as i32 ^ flag.as_bytes()[19] as i32 == 10521 &&
    flag.as_bytes()[11] as i32 ^ flag.as_bytes()[20] as i32 * flag.as_bytes()[1] as i32 + flag.as_bytes()[6] as i32 == 6787 &&
    flag.as_bytes()[7] as i32 + flag.as_bytes()[5] as i32 - flag.as_bytes()[18] as i32 & flag.as_bytes()[9] as i32 == 96 &&
    flag.as_bytes()[12] as i32 * flag.as_bytes()[8] as i32 - flag.as_bytes()[10] as i32 + flag.as_bytes()[4] as i32 == 8277 &&
    flag.as_bytes()[16] as i32 ^ flag.as_bytes()[17] as i32 * flag.as_bytes()[13] as i32 + flag.as_bytes()[14] as i32 == 4986 &&
    flag.as_bytes()[0] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[3] as i32 == 7008 &&
    flag.as_bytes()[13] as i32 + flag.as_bytes()[18] as i32 * flag.as_bytes()[2] as i32 & flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 == 118 &&
    flag.as_bytes()[0] as i32 % flag.as_bytes()[12] as i32 - flag.as_bytes()[19] as i32 % flag.as_bytes()[7] as i32 == 73 &&
    flag.as_bytes()[14] as i32 + flag.as_bytes()[21] as i32 * flag.as_bytes()[16] as i32 - flag.as_bytes()[8] as i32 == 11228 &&
    flag.as_bytes()[3] as i32 + flag.as_bytes()[17] as i32 * flag.as_bytes()[9] as i32 ^ flag.as_bytes()[11] as i32 == 11686 &&
    flag.as_bytes()[15] as i32 ^ flag.as_bytes()[4] as i32 * flag.as_bytes()[20] as i32 & flag.as_bytes()[1] as i32 == 95 &&
    flag.as_bytes()[6] as i32 * flag.as_bytes()[12] as i32 + flag.as_bytes()[19] as i32 + flag.as_bytes()[2] as i32 == 8490 &&
    flag.as_bytes()[7] as i32 * flag.as_bytes()[5] as i32 ^ flag.as_bytes()[10] as i32 ^ flag.as_bytes()[0] as i32 == 6869 &&
    flag.as_bytes()[21] as i32 ^ flag.as_bytes()[13] as i32 * flag.as_bytes()[15] as i32 + flag.as_bytes()[11] as i32 == 4936 &&
    flag.as_bytes()[16] as i32 + flag.as_bytes()[20] as i32 - flag.as_bytes()[3] as i32 & flag.as_bytes()[9] as i32 == 104 &&
    flag.as_bytes()[18] as i32 * flag.as_bytes()[1] as i32 - flag.as_bytes()[4] as i32 + flag.as_bytes()[14] as i32 == 5440 &&
    flag.as_bytes()[8] as i32 ^ flag.as_bytes()[6] as i32 * flag.as_bytes()[17] as i32 + flag.as_bytes()[12] as i32 == 7104 &&
    flag.as_bytes()[11] as i32 * flag.as_bytes()[2] as i32 + flag.as_bytes()[15] as i32 == 6143
}

fn main() {
    let mut flag = String::new();
    println!("Enter the flag: ");
    io::stdin().read_line(&mut flag).expect("Failed to read line");
    let flag = flag.trim();

    if check_flag(flag) {
        println!("Correct flag");
    } else {
        println!("Wrong flag");
    }
}

It seemed to be validating whether the input string matched the flag, and from the implementation it was pretty clear that it was intended to be solved with Z3.

So I used the following solver to obtain the flag.

from z3 import *
flag = [BitVec(f"flag[{i}]", 8) for i in range(22)]
s = Solver()

for i in range(22):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))

# INTIGRITI{
s.add(flag[0] == ord("I"))
s.add(flag[1] == ord("N"))
s.add(flag[2] == ord("T"))
s.add(flag[3] == ord("I"))
s.add(flag[4] == ord("G"))
s.add(flag[5] == ord("R"))
s.add(flag[6] == ord("I"))
s.add(flag[7] == ord("T"))
s.add(flag[8] == ord("I"))
s.add(flag[9] == ord("{"))
s.add(flag[21] == ord("}"))

s.add(flag[18] * flag[7] & flag[12] ^ flag[2] == 36)
s.add(flag[1] % flag[14] - flag[21] % flag[15] == -3)
s.add(flag[10] + flag[4] * flag[11] - flag[20] == 5141)
s.add(flag[19] + flag[12] * flag[0] ^ flag[16] == 8332)
s.add(flag[9] ^ flag[13] * flag[8] & flag[16] == 113)
s.add(flag[3] * flag[17] + flag[5] + flag[6] == 7090)
s.add(flag[21] * flag[2] ^ flag[3] ^ flag[19] == 10521)
s.add(flag[11] ^ flag[20] * flag[1] + flag[6] == 6787)
s.add(flag[7] + flag[5] - flag[18] & flag[9] == 96)
s.add(flag[12] * flag[8] - flag[10] + flag[4] == 8277)
s.add(flag[16] ^ flag[17] * flag[13] + flag[14] == 4986)
s.add(flag[0] * flag[15] + flag[3] == 7008)
s.add(flag[13] + flag[18] * flag[2] & flag[5] ^ flag[10] == 118)
s.add(flag[0] % flag[12] - flag[19] % flag[7] == 73)
s.add(flag[14] + flag[21] * flag[16] - flag[8] == 11228)
s.add(flag[3] + flag[17] * flag[9] ^ flag[11] == 11686)
s.add(flag[15] ^ flag[4] * flag[20] & flag[1] == 95)
s.add(flag[6] * flag[12] + flag[19] + flag[2] == 8490)
s.add(flag[7] * flag[5] ^ flag[10] ^ flag[0] == 6869)
s.add(flag[21] ^ flag[13] * flag[15] + flag[11] == 4936)
s.add(flag[16] + flag[20] - flag[3] & flag[9] == 104)
s.add(flag[18] * flag[1] - flag[4] + flag[14] == 5440)
s.add(flag[8] ^ flag[6] * flag[17] + flag[12] == 7104)
s.add(flag[11] * flag[2] + flag[15] == 6143)

while s.check() == sat:
    m = s.model()
    for c in flag:
        print(chr(m[c].as_long()),end="")
    print("")
    break

# INTIGRITI{tHr33_Z_FTW}

Anonymous(Rev)

Anonymous has hidden a message inside this exe, can you extract it?

Analyzing the file given as the challenge binary with ILSpy produced the following code.

image-20231118104419037

In this program, it extracts from an icon hardcoded inside the program a string that satisfies specific conditions and then Base64-decodes it.

So I copied a few suitable strings from around the middle of the icon and Base64-decoded them, which gave me the flag.

image-20231118104402809

imPACKful(Rev)

This program seems to be compressed but still can be executed, I wonder what could cause that..

The challenge binary was packed with UPX, so I unpacked it first.

image-20231118104558414

However, even after decompiling the unpacked program, the program itself did not seem to do anything at all.

image-20231118110146619

So I examined the sections of the unpacked program and found that a section named {N3v3R} had been registered there, and that turned out to be the flag.

image-20231118110137371

Can We Fix It(Rev)

We have extracted this payload that some malware tried to dump.. Seems dumping it directly from memory debased it so it can’t run. Can you fix it?

The file given as the challenge binary was a PE file that could not be executed for some reason.

Since Windows no longer seemed able to load it as a PE file, I proceeded with the analysis while comparing it with the image below and the results from CFF Explorer.

image-20231118150807553

Reference: PE Format - Win32 apps | Microsoft Learn

In the end, I found that incorrect alignment of the .text section was the reason the PE binary could not be executed.

image-20231118155837570

So I changed the .text section’s VA to 0x1000, saved the file, and successfully obtained the correct flag.

image-20231118155912481

Virtual RAM(Rev)

I wonder what the old man is talking about

The challenge provides a Game Boy ROM image as the binary.

image-20231118161636073

This ROM image can be run, debugged, and cheated with using the BGB GameBoy Emulator.

Reference: BGB GameBoy Emulator (current version: BGB 1.5.10)

Reference: [Reverse] WPI CTF 2022 - PokemonRematch | TeamRocketIST - Portuguese CTF Team

image-20231118220252864

When you talk to the old man on the game’s start screen, he tells you to check VRAM.

image-20231118220310677

So I checked the VRAM information and found that the flag string was embedded in tiles outside the visible screen.

image-20231118220325629

After cutting and pasting those together, I obtained the following string.

img

At first I could only read this as INTIGRITI{H3r0_0F_tIM3}, so I had a hard time getting it accepted. Then a teammate made the sharp observation that all of the vowels might be written as digits, which led us to identify the correct flag as INTIGRITI{H3r0_0F_t1M3}.

image-20231118220400153

Crack Me If You Can(Rev)

Can you slay goliath?

Looking at the EXE given as the challenge binary, I found that it was a .Net program.

image-20231119151731960

When I ran it, a GUI asking for some mysterious input appeared.

image-20231119151941971

When I decompiled it with ILSpy, I found that several fairly complicated-looking functions had been defined.

image-20231119152052196

The Main function decrypts encrypted byte data and retrieves methods via executingAssembly, so analysis seemed difficult in that form.

image-20231119152411152

So I used ExtremeDumper to extract the decrypted binary from the running process.

image-20231119154338120

Analyzing the extracted binary with ILSpy then made it possible to inspect the code for the GUI shown when the program starts.

image-20231119154629810

Reading the code in this Main class, I found that it checks whether the output obtained by passing the input string and the predefined byte array enc to the unkownMethod function matches the string WhatAreYouDoingToChallenge.

private void Button1_Click(object sender, EventArgs e)
{
    string @string = Encoding.UTF8.GetString(Resources.enc);
    if (Operators.CompareString(unkownMethod(TextBox1.Text, @string), "WhatAreYouDoingToChallenge", TextCompare: false) == 0)
    {
        Interaction.MsgBox("You Solve It", MsgBoxStyle.Information, "Nice");
    }
}

public string unkownMethod(string textToScramble, string password)
{
    StringBuilder stringBuilder = new StringBuilder(textToScramble.Length);
    int num = checked(textToScramble.Length - 1);
    for (int i = 0; i <= num; i = checked(i + 1))
    {
        int index = i % password.Length;
        char c = textToScramble[i];
        c = Strings.ChrW(c ^ password[index]);
        stringBuilder.Append(c);
    }
    return stringBuilder.ToString();
}

The unkownMethod function in turn calls another function named checked.

The code for the checked function was not included in ILSpy’s decompilation result, but judging from the later processing, it just XORs the input characters with enc, so it seemed safe to ignore.

I was able to obtain the flag with the following solver.

ans = "WhatAreYouDoingToChallenge"
with open("enc", "rb") as f:
    enc = f.read()

for i in range(len(ans)):
    print(chr(ord(ans[i])^enc[i%len(enc)]),end="")
    
# intigriti{You_Are_Amazing}

Escape(Game Hacking)

Your trapped inside a box. Can you escape it and do the reverse to get the flag?

The file given as the challenge binary was a Unity-made game program.

image-20231119111623490

Launching it shows a mysterious object surrounded by walls on all four sides.

For now, it seemed that escaping from this walled area would allow me to get the flag.

image-20231119111733734

At first, I thought I just needed to use CheatEngine to locate and tamper with the memory region that stores the coordinates while moving, but after repeated trial and error I unfortunately could not find the target memory region.

Next, as another approach, I considered modifying the game’s objects.

Reference: Reverse engineering unity game | tripoloski blog

Under Unity’s default settings, the compiled .Net program is placed at <Project>\Managed\Assembly-CSharp.dll, so I analyzed that with ILSpy.

Looking through the modules, I found a class named MyCharacterController.

image-20231119131415199

By patching this module, it seemed likely that I could obtain the flag by cheating in one of several ways: moving the object’s starting position outside the wall, deleting the wall objects, removing wall collision so I could pass through them, or making the character jump over the walls.

To tamper with the code, I reopened the program in dnSpy instead of ILSpy. (ILSpy might also be able to patch it, but I could not find the menu, so I used dnSpy.)

Looking at the character object’s UpdateVelocity method here, I found that the jump height is controlled there.

image-20231119145012962

So I added the following line to force an extremely high jump and then compiled the code.

image-20231119150144740

image-20231119150320357

Finally, I saved the file from Save All and launched the game.

image-20231119150344304

This made an ordinary jump turn into a super-high jump, letting me clear the wall.

image-20231119150122233

After getting over the wall, I was able to obtain the flag on the map.

image-20231119145857562

Over the Wire 1(Warmup)

I’m not sure how secure this protocol is but as long as we update the password, I’m sure everything will be fine

When I opened the provided pcap in Wireshark, I found that files were being transferred over FTP.

So I first saved the file as binary data.

image-20231117231323002

The file transferred over FTP (flag.zip) was a password-protected ZIP file, so next I looked for the password needed to extract it.

The password used during the FTP transfer was 5up3r_53cur3_p455w0rd_2022, so I tried extracting it with that password, but it failed.

220 pyftpdlib 1.5.9 ready.
USER cat
331 Username ok, send password.
PASS 5up3r_53cur3_p455w0rd_2022
230 Login successful.
SYST
215 UNIX Type: L8
PORT 192,168,16,131,179,47
200 Active data connection established.
LIST
125 Data connection already open. Transfer starting.
226 Transfer complete.
TYPE I
200 Type set to: Binary.
PORT 192,168,16,131,203,181
200 Active data connection established.
RETR flag.zip
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,132,11
200 Active data connection established.
RETR reminder.txt
125 Data connection already open. Transfer starting.
226 Transfer complete.
PORT 192,168,16,131,162,139
200 Active data connection established.
RETR README.md
125 Data connection already open. Transfer starting.
226 Transfer complete.
QUIT
221 Goodbye.

Reading the packets further, I found that a file containing the following message had also been received over FTP.

image-20231119162229252

Using this hint, I changed the password to 5up3r_53cur3_p455w0rd_2023, which allowed me to extract the ZIP file and obtain the flag.

Over the Wire 2(Warmup)

When I examined the provided pcap file, I found that images were being exchanged over SMTP.

image-20231117232037966

The first image was the following, but after checking it thoroughly I could not find anything suspicious.

image-20231117232149565

So I followed the packets further and found that another email was also sending an image.

image-20231117232800928

So I investigated this image next.

image-20231117232750719

Analyzing the file with zsteg gave me the flag.

image-20231117232841225

Summary

It was fun to be able to solve a lot of challenges again after a while.

It was also great to learn new things, such as dealing with packed .Net binaries and analyzing game programs.