segfault

by alxbl

NorthSec 2020 Writeup: After Party

This post outlines the solution to NorthSec 2020’s After Party challenge and includes both flags.

Context

You are given a zip file, which when unzipped contains the following:

afterparty.jar
afterparty.so
run.sh

Looking into the jar with unzip -l afterparty.jar, it contains the following files:

Archive:  afterparty.jar
Length     Date       Time    Name
---------  ---------- -----   ----
    63     2020-05-03 14:17   META-INF/MANIFEST.MF
    0      2020-05-03 14:17   prom/
    2024   2020-05-03 11:56   prom/Bootstrap.class
    149    2020-05-03 10:17   prom/IValidator.class
    1464   2020-05-03 14:17   prom/Main.class
    1637   2020-05-03 14:17   prom/Validator.class
---------                     -------
    5337                     6 files

Flag 1: Java Decompilation

The next step is to open the Java application in a decompiler to understand what it’s doing. I used jd-gui.

JD-GUI decompiler

The natural starting point is Main.main which is the application’s entry point. The code looks like the following:

package prom;

import java.io.BufferedReader;
import java.io.InputStreamReader;

public class Main {
  public static void main(String[] args) throws Exception {
    System.out.print("Password : ");
    BufferedReader buffer = new BufferedReader(
        new InputStreamReader(System.in)
    );
    String pass = buffer.readLine();
    pass = pass.strip();
    if (pass.equals("FLAG-XXXXXXXXXXXXXXXXXXXXX")) {
      System.out.println("You can enter the party !");
      return;
    }
    IValidator validator = Bootstrap.<IValidator>get(
        IValidator.class
    );
    if (validator.validate(pass)) {
      System.out.println("You can enter the secret place !");
    } else {
      System.out.println("You won't get in with that password !");
    }
  }
}

Right off the bat, we see on lines 8-16 that the password is being checked against a flag. That’s flag 1/2 in the challenge. Beyond that, though there is an extra validation step that checks whether you are allowed to the “secret” place.

Flag 2: JNI-assisted Validator

In order to understand this one, we need to dive into line 18: `Bootstrap.get(IValidator.class).

package prom;

import java.io.File;

public class Bootstrap extends ClassLoader {
  private static Bootstrap _inst = new Bootstrap();

  static {
    File f = new File(System.getProperty("java.class.path"));
    File dir = f.getAbsoluteFile().getParentFile();
    File mod = new File(dir, "afterparty.so");
    System.load(mod.getAbsolutePath());
  }

  public static <T> T get(Class<T> clazz) throws Exception {
    String name = clazz.getName();
    int pos = name.lastIndexOf(".") + 1;
    String beginning = name.substring(0, pos);
    String last = name.substring(pos);
    last = last.replaceFirst("I", "");
    Class<T> inst = _inst.findClass(String.valueOf(beginning) + last);
    return inst.newInstance();
  }

  public native Class findClass(String paramString) throws ClassNotFoundException;
}

We can see that the code is going to load afterparty.so and modify the class name to remove the I prefix (making it Validator) and then calls the Java Native Interface (JNI) function findClass. Looking at Validator.class, the decompiler is spitting out an internal error, and we can’t seem to read the code.

It looks like we have to dive into the shared object and look at what findClass is doing. To do that, we pop afterparty.so into Ghidra and locate the mangled findClass function. According to the JNI spec, the exported function name must be something like Java_package_class_method, so we filter the function list by Java and find Java_prom_Bootstrap_findClass. That looks right.

Exported JNI function

Now looking at the disassembly of this function is a bit messy because JNI exposed native functions to interact with the Java VM. Understanding all the native functions and API is time consuming and not necessary. We can guestimate by looking at the strings being called. Here’s the Ghidra decompiler output, with the interesting bits highlighted:

Reversing the findClass function

In step 1, the method java.io.Inputstream.getResourceAsStream is retrieving the class name from the .jar. Then, in step 2, the stream is read with InputStream.read, which returns the total number of bytes read (total_size). Finally, a buffer is allocated and then in step 3 there is a loop which seems to update a key and performs a XOR over the dst buffer. One of those calls after the malloc is likely performing a memcpy at the JVM level, so we just take a guess and run with it.

Getting this code to run is quite difficult since the Java Archive needs to be repacked and existing Java tooling makes it difficult to debug a .jar without having the original sources. Instead, we re-implement the simple decryption loop in Python and recover the decrypted .class:

with open('Validator.class', 'rb') as f:
    data = f.read()

CLASS = 'prom.Validator'
KEY = len(data) + 0x1264ec8
out = []
for i in range(len(data)):
    KEY = (KEY * 0xd + 0xbeb0 + len(CLASS)) & 0xFFFFFFFF
    b = data[i] ^ (KEY & 0xFF)
    out.append(b)

with open('out.class', 'wb') as o:
    o.write(bytes(out))

Lastly, we create a new afterparty.jar with the Validator.class replaced by our decrypted version and open the resulting archive in jd-gui to decompile the validation code:

package prom;

import java.util.regex.Pattern;
import prom.IValidator;

public class Validator implements IValidator {
  public boolean validate(String flag) {
    if (!flag.startsWith("FLAG-"))
      return false;
    if (flag.length() != 25)
      return false;
    Pattern pattern = Pattern.compile("^FLAG-[a-f0-9]+$");
    if (!pattern.matcher(flag).matches())
      return false;
    String rest = flag.substring(5);
    int code1 = rest.substring(0, 2).hashCode();
    int code2 = rest.substring(2, 4).hashCode();
    int code3 = rest.substring(4, 6).hashCode();
    int code4 = rest.substring(6, 8).hashCode();
    int code5 = rest.substring(8, 10).hashCode();
    int code6 = rest.substring(10, 12).hashCode();
    int code7 = rest.substring(12, 14).hashCode();
    int code8 = rest.substring(14, 16).hashCode();
    int code9 = rest.substring(16, 18).hashCode();
    int code10 = rest.substring(18, 20).hashCode();
    if (code1 + code2 + code3 + code4 + code5 + code6 + code7 + code8 + code9 + code10 != 22998)
      return false;
    if (code1 != 1821)
      return false;
    if (code2 != 1604)
      return false;
    if (code3 != 1802)
      return false;
    if (code4 != 1691)
      return false;
    if (code5 != 1867)
      return false;
    if (code6 != 3140)
      return false;
    if (code7 != 3186)
      return false;
    if (code8 != 3180)
      return false;
    if (code9 != 1570)
      return false;
    if (code10 != 3137)
      return false;
    return true;
  }
}

Oh, so it looks like we have to bruteforce some hashcodes two characters at a time in order to recover the flag. The important point here is to note the regular expression which limits the character set to a-f0-9. Failing to notice that will yield a lot of non-sensical flags because of hashcode collisions.

The easiest way to bruteforce this is in Java since there is no need to re-implement hashCode():

package afterp;

import java.util.regex.Pattern;
import java.util.ArrayList;
import java.util.Arrays;

public class AfterParty {
    public static String Good = "0123456789abcdef";
    public static void main(String[] args) {
        ArrayList<Integer> x = new ArrayList<Integer>(
            Arrays.asList(
                1821, 1604, 1802, 1691, 1867,
                3140, 3186, 3180, 1570, 3137
            )
        );

        var flag = "";
        var found = false;
        for (int c = 0; c < 10; ++c) {
            found = false;
            for (
                var b = 0;
                b < AfterParty.Good.length();
                ++b
            ) {
                for (
                    var a = 0;
                    a < AfterParty.Good.length();
                    ++a
                ) {

                    String match = Character.toString(
                        AfterParty.Good.charAt(b)) +
                        Character.toString(
                            AfterParty.Good.charAt(a)
                        )
                    );

                    if (match.hashCode() == x.get(c)) {
                        flag += match;
                        found = true;
                    }
                    if (found) break;
                }
                if (found) break;
            }
        }

        System.out.println("FLAG-" + flag);
        System.out.println(validate("FLAG-" + flag));
    }
}

I haven’t coded in Java since 2012, so please forgive my non-idiomatic Java. Suffice to say that the code works and we get the last flag of the track.

Cheers!