All Articles

n00bzCTF 2023 Writeup

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

I participated in n00bzCTF 2023 as part of 0nePadding, and we placed 79th out of 855 teams.

image-20230614190703619

This time I only participated lightly, but I wanted to jot down the challenges where I learned something.

Table of Contents

MyPin(Rev)

I made a safe with a pin of only two digits.

Running the jar file provided as the challenge binary launched the following application.

image-20230613234101350

The class layout was as follows.

jar -tf My-pin.jar
>
META-INF/
META-INF/MANIFEST.MF
Mypin.class
PinButton.class
ResetButton.class
Secret.class

After checking the decompiled jar file, I found that the application does the following.

  • Each time a button is pressed, the variable cnt is incremented by 1, and the buttons can be used at most 9 times.
  • When a button is pressed, "0" or "1" is passed to the process function of the Secret class, updating part of the predefined mydata array.
  • The getData function of the Secret class has logic that generates some string based on the information in the mydata array.
  • There are 2**9 (=512) possible input patterns, so one of them should make the output of the getData function become the flag.

Once I had figured that out, I thought, “The rest will be easy if I rewrite the Java program in Python and brute-force it!” Unfortunately, even after trying every decompilation approach I could think of, the Python script I rewrote never behaved exactly the same as the original Java program.

Reference: GitHub - skylot/jadx: Dex to Java decompiler

In the end, it seems the proper solution was to take advantage of the fact that a jar file (or class file) can be called from your own Java program as a library.

First, extract the jar file into class files with the following command.

jar xf My-pin.jar

This gives you a file named Secret.class, so I wrote the following Java program to brute-force the flag by reusing the challenge binary’s Secret class.

public class Solve {
    public static void main(String[] args) {
        // 2**9 = 512
        for (int i = 0; i < 512; i++) {
            String pin = String.format("%9s", Integer.toBinaryString(i)).replace(' ', '0');
            Secret secret = Secret.getInstance();
            secret.resetInstance();
            for (char c : pin.toCharArray()) {
                secret.process(c);
            }
            String data = secret.getData();
            if(data.contains("n00bz")) {
                System.out.println("PIN: " + pin + ", Flag: " + data);
            }
        }
    }
}

Reference: n00bzCTF 2023 - MyPin :: Chocapikk’s blog

Place this as Solve.java in the same directory as Secret.class, then run the following command to recover the flag.

javac Solve.java ; java Solve

Reference: Java import giving error - Stack Overflow

EZrev(Rev)

Rev is EZ! Author: NoobHacker

Decompiling the provided class file yields the following code.

/*
 * Decompiled with CFR 0.150.
 */
import java.util.Arrays;

public class EZrev {
    public static void main(String[] arrstring) {
        int n;
        if (arrstring.length != 1) {
            System.out.println("L");
            return;
        }
        String string = arrstring[0];
        if (string.length() != 31) {
            System.out.println("L");
            return;
        }
        int[] arrn = string.chars().toArray();
        for (n = 0; n < arrn.length; ++n) {
            arrn[n] = n % 2 == 0 ? (int)((char)(arrn[n] ^ 0x13)) : (int)((char)(arrn[n] ^ 0x37));
        }
        for (n = 0; n < arrn.length / 2; ++n) {
            if (n % 2 == 0) {
                int n2 = arrn[n] - 10;
                arrn[n] = (char)(arrn[arrn.length - 1 - n] + 20);
                arrn[arrn.length - 1 - n] = (char)n2;
                continue;
            }
            arrn[n] = (char)(arrn[n] + 30);
        }
        int[] arrn2 = new int[]{130, 37, 70, 115, 64, 106, 143, 34, 54, 134, 96, 98, 125, 98, 138, 104, 25, 3, 66, 78, 24, 69, 91, 80, 87, 67, 95, 8, 25, 22, 115};
        if (Arrays.equals(arrn, arrn2)) {
            System.out.println("W");
        } else {
            System.out.println("L");
        }
    }
}

After that, you can recover the flag just by writing code that works backwards.

arr2 = [130, 37, 70, 115, 64, 106, 143, 34, 54, 134, 96, 98, 125, 98, 138, 104, 25, 3, 66, 78, 24, 69, 91, 80, 87, 67, 95, 8, 25, 22, 115]
arr = arr2.copy()

for i in range(len(arr)//2):
    if i % 2 == 0:
        n2 = arr[i]
        arr[i] = arr[len(arr)-1-i]
        arr[len(arr)-1-i] = n2
print(arr)

for i in range(len(arr)//2):
    if i % 2 != 0:
        arr[i] = arr2[i] - 30 
    else:
        arr[i] = arr2[len(arr)-1-i] + 10
        arr[len(arr)-1-i] = arr2[i] - 20

for i in range(len(arr2)):
    if i % 2 == 0:
        arr[i] = arr[i] ^ 0x13
    else:
        arr[i] = arr[i] ^ 0x37

for a in arr:
    print(chr(a), end="")

zzz(Rev)

Decompiling the ELF file provided as the challenge binary with Ghidra gave the following result.

image-20230621232054338

It looks like the read function is reading 0x1e bytes from fd=0, so we can tell that it receives data from standard input and passes it to the check function.

The check function decompiled as follows.

image-20230621233149319

At a glance, it looked like an angr challenge, so I tried the following template.

(Since the challenge name is zzz, maybe the intended solution was to solve the constraints with z3?)

import angr
proj = angr.Project("chall", auto_load_libs=False)
init_state = proj.factory.entry_state(args = ['chall'])
simgr = proj.factory.simgr(init_state)
simgr.explore(find=(0x401654), avoid=([0x4016ca,0x4011a9]))

# 出力
simgr.found[0].posix.dumps(0)

However, I could not recover the flag with the template above.

image-20230621233255024

I suspect that was because I had not specified any constraints in angr.

I was not very sure how to run angr with more detailed constraints, so I decided to use Z3Py instead, which was probably closer to the intended solution.

Based on Ghidra’s decompiled output, I created a solver with the following constraints and was able to obtain the flag.

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

# 独自の制約を追加
s.add(flag[0] == ord("n"))
s.add(flag[1] == ord("0"))
s.add(flag[2] == ord("0"))
s.add(flag[3] == ord("b"))
s.add(flag[4] == ord("z"))
s.add(flag[5] == ord("{"))
s.add(flag[0x1e-1] == ord("}"))
for i in range(0x1e):
    s.add(And(
        (flag[i] >= 0x21),
        (flag[i] <= 0x7e)
    ))

# 問題の制約
s.add(flag[0] >> 4 == 0x6)
s.add(flag[1] == flag[2])
s.add(And(
    ((flag[6] | flag[3]) == 0x7a),
    ((flag[6] & flag[3]) == 0x42)
 ))
s.add(flag[0x1c] == flag[4]) 
s.add(And(
    (flag[0x1d] * flag[5] == 0x3c0f),
    And(
        (flag[8] + flag[6] + flag[7] == 0x12e),
        (flag[7] * flag[6] - flag[8] == 0x2a8a)
    )
))

s.add(And(
    And(
        (flag[9] - flag[8] == 5),
        (flag[10] - flag[9] == 0x1b),
        (flag[10] ^ flag[0xb] == 0x20)
    ),
    And(
        (flag[0xc] == flag[0xf]),
        (flag[0xb] + flag[0xc] == 0xb4),
        (flag[0xc] + flag[0xd] == 0xb9)
    )
))

s.add(And(
    (flag[0xd] + flag[0xe] - flag[0x10] == flag[0xd]),
    And(
        (flag[0x11] + flag[0x10] == 0xd9),
        (flag[0x11] == flag[0xd])
    ),
    And(flag[0xe] + flag[0x10] == flag[0xe] * 2)
))

s.add(And(
        And(
            (flag[0x12] == ord('Z')),
            (flag[0x12] == flag[0x13]),
            ((flag[0x15] ^ flag[0x13] ^ flag[0x14]) == 0x7f)
        ),
        ((flag[0x14] ^ flag[0x15] ^ flag[0x16]) == flag[0x15]),
        (flag[0x15] == ord('_')),
        (flag[6] + flag[0x18] == 0xb4)
))

s.add(flag[0x18] + ~flag[0x17] == -0x21)
s.add(flag[0x19] == flag[9])
s.add(And(
    (flag[0x1b] + flag[0x1a] == 0xd4),
    (flag[0x1b] == flag[0x1c])
))

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

Summary

This time I participated only lightly.

I need to keep practicing.