Our new conspiracy theorist intern has blocked everyone from the coffee machine because he saw that aliens were trying to steal the “out of the world” secret recipe. Your mission is to unveil the secrets that lie behind his profound madness and teach him a javaluable lesson.

Category: Reversing

Solvers: t0b1, lmarschk

TL;DR

This challenges was very nice but also hell of a ride. The main thing being done here is to use the Java Native Interface (JNI) to run a JVM from native C++ code. Then the behaviour of functions like Character.valueOf or System.exit is altered to obfuscate what is being done. In the end it uses several mappings to encode the flag in the binary.

Writeup

We get a binary called coffe_invocation. Running the strings tool shows nothing but Java stuff (and the coffee ascii art). The file tool tells us that it is a 64-bit binary and that the symbols are stripped.

We execute the binary to see what is happening and see that Java is required to run the program. Thus we install Java and set the LD_LIBRARY_PATH accordingly.

Now we can run the binary for the first time and get the following output.

average coffee

We can see, that there is the [Redacted] option. However choosing this options yields Can't access secret coffee without providing the password. As we don’t know what the password might be, we decompile the bianry and find the main function. (We already renamed some functions for clarity)

main

JNI Introduction

We find the first interesting function call. The JNI_CreateJavaVM looks interesting. JNI is the “Java Native Interface” and the Oracle documentation describes it as follows.

The JNI is a native programming interface. It allows Java code that runs inside a Java Virtual Machine (VM) to interoperate with applications and libraries written in other programming languages, such as C, C++, and assembly.

That means, that this program, probably written in C++, uses a JVM to execute certain Java code and communicate with that. A typical call to that function looks like JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); which is how we renamed the function parameters.

As we haven’t worked with JNI before, we search the internet for some basic tutorials, to find out how the original C++ code looks like. We find this tutorial that shows us some basics. Creating a VM looks like this.

JavaVM *jvm;
JNIEnv *jvm_env;
long flag = JNI_CreateJavaVM(&jvm, (void**)&jvm_env, &vmArgs);

Afterwards we can use the jvm_env to find classes and methods and to call those methods directly from our native code like this (error handling is excluded here).

jclass jcls = jvm_env->FindClass("org/jnijvm/Demo");
jmethodID methodId = jvm_env->GetStaticMethodID(jcls, "greet", "(Ljava/lang/String;)V");
jstring str = jvm_env->NewStringUTF(10);
jvm_env->CallStaticVoidMethod(jcls, methodId, str);

Now that we have a basic understanding of the JNI we can continue reverse engineering the binary.

JNI function calls in decompiler

In the main function above we see a call to JavaPrintln (we called it like that). By looking into that function we can find the things we saw in the tutorial.

javaprintln

This helps us understanding what happens in that function and coming to the conclusion, that this effectively calls Javas println function. We can rename the variables accordingly, to give us a better understanding, as already done in the screenshot.

Sadly Ghidra does not know the JNI, which is why we can’t see any of the function calls on the jvm_env. But we can use the Java docs again here that give us a function table of the JNINativeInterface, which looks like this.

const struct JNINativeInterface ... = {
  NULL,
  NULL,
  NULL,
  NULL,
  GetVersion,

  DefineClass,
  FindClass,

  FromReflectedMethod,
  ...many more...
}

Each entry is 8 byte long, so jvm_env+0x30 must be the 7th entry. This is the FindClass function which fits perfectly with our assumptions above. We can use that as a reference point. Whenever we see an offset to the jvm_env we can find the corresponding function.

get_flag function

If we choose the [Redacted] option in the menu, the check for the program parameters is performed. This clearly shows us, that we have to pass the password as the parameter to the program via the CLI.

Continuing, the get_flag function follows. We will immediately see why we called it like that.

get_flag

It will execute the first verification, afterwards the second verification and if both passed with return code 0, the program prints the flag. Looking at the code we see, that the flag that is printed, is simply the parameter that we passed as a CLI argument. That means, if we find out what the checks do, we know how to make them pass and get the flag.

Verify1

We need to pass the first check first. Looking into the execute_verify1 function reveals a lot more and it is getting more and more interesting.

Shutdown re-mapping

The first thing being done is calling the following function.

void register_shutdown(long *jvm_env,undefined8 on_halt) {
  undefined8 shutdown_cls;

  _halt0_function_pointer = on_halt;
  shutdown_cls = (**(code **)(*jvm_env + 0x30))(jvm_env,"java/lang/Shutdown",jvm_env);
  (**(code **)(*jvm_env + 0x6b8))(jvm_env,shutdown_cls,&pt_halt0_struct,2);
  return;
}

This calls the RegisterNatives from the JNI. We can find the corresponding documentation. Using this function, we can register native methods. The register_shutdown method changes the functionality of the halt0 and runAllFinalizers function. The functionality of halt0 is changed to the function at the on_halt pointer. The functionality of runAllFinalizers is changed to do nothing at all, which means that no finalizers will be run.

halt0 is the native halt method, which is called after executing the System.exit(status_code) function in a Java program. By modifying this behaviour, we modify how the program terminates, and what happens afterwards. Normally halt0 would terminate the VM.

In execute_verify1 the halt0 function is remapped to a function that stores the return code in a global variable. In the end of execute_verify1 this value is returned.

Value re-mapping

We find the following two lines right after the register_shutdown call. Again, those functions are already renamed by us.

mapping = get_mapping(1);
remap_byte(jvm_env,mapping,mapping);

The get_mapping function simply returns a pointer to an array of bytes. The specific array is chosen based on the given parameter. As those arrays are all equal in size, we assume that it is some kind of a mapping and called the function like that.

Then the remap_byte function is called. It does the following.

remap_byte

We find our jvm again. Now certain functions are called. We again look at the Java docs about the Byte.valueOf function and find the following.

Returns a Byte instance representing the specified byte value. If a new Byte instance is not required, this method should generally be used in preference to the constructor Byte(byte), as this method is likely to yield significantly better space and time performance since all byte values are cached.

That reveals, that the Byte.valueOf function uses cached values. It returns a Byte object which is cached. We can change the value of that Byte object by using Object.value = some_value. That way we can change the cached byte mapping! As we can see in the decompiled code in line 18, the SetByteField function (offset 0x350) is called with value and the returned byte in the line above. The mapping is clearly changed by the program! Sadly Ghidra does not show the mapping parameter we know from the execute_verify1 function.

The cached values for the Short objects are also remapped in the next step using another mapping.

Classes in memory

Then we find the following lines.

class_base_address = get_verify_class(0,&some_value);
local_50 = (**(code **)(*jvm_env + 0x28)) (jvm_env,"Verify1",0,class_base_address,some_value & 0xffffffff);

The get_verify_class function catched us. Depending on the first parameter, it sets the class_base_address differently to a address in the binary. Looking through the bytes at those addresses we find the following sequence <init> ... Code ... LineNumberTable ... compareByte ... . This made us think that there might be a Java class in the binary. The second line in the code above proves that assumption. It calls the FindClass function and passes class_base_address.

The Java class must be inside the binary. By running binwalk on the binary, we can indeed extract the compiled Java classes. This yields:

$ binwalk -D='.*' ./coffee_invocation
DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 64-bit LSB shared object, AMD x86-64, version 1 (SYSV)
16704         0x4140          Compiled Java class data, version 55.0
17984         0x4640          Compiled Java class data, version 55.0

Calling the class

Once the class is loaded, several logic is applied to prepare the call of the main function of the Verify1 class.

The first 30 byte are taken from our CLI parameter using the Java String.substr(0,30) call. This is stored in an array at position 0. Afterwards another string, which is stored in the binary is loaded. It is also 30 bytes long. This string is stored in the same array at position 1. Then the main function of the Verify1 class is called with our array as parameter.

As we extracted the class file previously, we can decompile that to get the following code. The online tool javadecompiler.com turns out to be very valuable here. We choose CFR as the decompiler.

/*
 * Decompiled with CFR 0.150.
 */
public class Verify1 {
    private static boolean compareByte(Byte by, Short s) {
        return by.byteValue() == s.shortValue();
    }

    public static void main(String[] arrstring) {
        if (arrstring == null || arrstring.length != 2) {
            System.out.println("Verifying requires source and target");
            System.exit(1);
            return;
        }
        String string = arrstring[0];
        String string2 = arrstring[1];
        if (string == null || string2 == null) {
            System.out.println("Source and Target may not be null");
            System.exit(2);
            return;
        }
        if (string.length() != string2.length()) {
            System.out.println("Source and Target don't have the same length");
            System.exit(2);
            return;
        }
        System.out.println("Verifying user is of terrestrial origin...");
        for (int i = 0; i < string.length(); ++i) {
            if (Verify1.compareByte(Byte.valueOf((byte)string.charAt(i)), Short.valueOf((byte)string2.charAt(i)))) continue;
            System.out.println("=> User might be an alien!!!");
            System.exit(3);
            return;
        }
    }
}

We can see, that after each System.exit call, a return is called seperatly. As we’ve seen above, this is to properly terminate the program as the halt0 function no longer terminates the VM.

Solving Verify1

The logic is simple. It iterates over both strings we placed in the array above, s being our input and s2 being the string from memory. If any byte does not match, the function returns. For our input string s it uses the Byte.valueOf function, which uses the changed cached values. For the predefined string it uses the Short.valueOf function whose mapping has been changed as well.

Now we only have to reverse the mappings to get the correct first input. This is done by the following python script. (I omitted the mappings to save up space, you can find the whole code in solve.py)

def byte_str_to_array(s):
  return [int(a, 16) for a in s.split(' ')]

def solve1():
  bytes_mapping_str = '' # insert byte mapping from ghidra here
  short_mapping_str = '' # insert short mapping from ghidra here

  og_mapping = byte_str_to_array(og_mapping_str)
  bytes_mapping = byte_str_to_array(bytes_mapping_str)
  short_mapping = byte_str_to_array(short_mapping_str)

  desired_str = '75 39 30 0c 70 30 20 6b 30 75 30 0c 6b 30 20 26 61 30 0c 26 70 0c 2b 30 0c 39 21 7a 6b 3a'
  desired = byte_str_to_array(desired_str)

  needed = []

  for byte in desired:
    needed.append(chr(bytes_mapping.index(short_mapping[byte])))

  return ''.join(needed)

We get the output: th3_s3cr3t3_r3c1p3_1s_23_h0ur5. Feeding that to the program shows:

Verifying user is of terrestrial origin...
Source may not be null
No coffee for you!

Hurray. We passed the check, as we didn’t got the output => User might be an alien!!!.

After the Verify1 class is executed, the mappings of Byte and Short are returned to their normal mapping.

Verify2

Now that we solved the first verification check, we move on to the second. The second verification is very similar to the first one. Instead of changing the Byte and Short values, it is changing the values of the Characters. It also swaps the meaning of true and false, so true becomes false and false becomes true. Furthermore it takes the second 30 characters from our input string.

However, there is a difference here. In the beginning of the second verification, the register_shutdown function is called again. This time the function that halt0 is mapped to is of more interest. The function stores the exit code in the global variable just like done in execute_verify1 and remaps the characters afterwards. This can be done up to 15 times. We remember that our halt0 function does not terminate the Java program.

We decompile the Verify2 class and find the following.

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

public class Verify2 {
    private static boolean compareByte(Byte by, Short s) {
        System.out.println(by + " == " + s);
        return by.byteValue() == s.shortValue();
    }

    private static String complexSort(String string, Boolean bl) {
        Object[] arrobject = string.chars().mapToObj(n -> Character.valueOf((char)n)).toArray();
        if (bl.booleanValue()) {
            Arrays.sort(arrobject);
        }
        String string2 = Arrays.stream(arrobject).map(Object::toString).collect(Collectors.joining());
        return string2;
    }

    private static Boolean verifyPassword(String string, String string2) {
        return string.equals(string2);
    }

    public static void main(String[] arrstring) {
        if (arrstring == null || arrstring.length != 1) {
            System.out.println("Verifying requires source");
            System.exit(1);
            return;
        }
        String string = arrstring[0];
        if (string == null) {
            System.out.println("Source may not be null");
            System.exit(1);
            return;
        }
        if (string.length() % 2 != 0) {
            System.out.println("Length must be even");
            System.exit(1);
            return;
        }
        System.out.println("Verifying user has authorization...");
        for (int i = 0; i < string.length() / 2; ++i) {
            String string2;
            String string3 = string.substring(i * 2, i * 2 + 2);
            String string4 = Verify2.complexSort(string3, true);
            if (!string4.equals((string2 = Verify2.complexSort("Cr1KD5mk0_uUzQYifaGVqlN2B3wvpgPtSx6Odo{8hjJLHy9IXb4RnWZ}TAFEsMce7", false)).substring(i * 2, i * 2 + 2))) continue;
            System.exit(i + 3);
        }
        if (!Verify2.verifyPassword(string, "Tinfoil").booleanValue()) {
            System.out.println("Please enter the correct password");
            System.exit(2);
        }
    }
}

Again we find some checks to make sure that our input string is long enough. This time, the input array of the main function only contains one string, the second 30 bytes of our input.

Afterwards we enter the loop that verifies our authorization. It takes two characters of our input string and calls complexSort. That function simply maps each character to its value retrieved using valueOf (remember, this mapping has been changed). If the boolean value of bl is true, as the mapping has been swapped as well bl must be false, the array is sorted.

The string4, our two mapped bytes, are compared against the sorted string of Cr1KD5mk0_uUzQYifaGVqlN2B3wvpgPtSx6Odo{8hjJLHy9IXb4RnWZ}TAFEsMce7 after mapping each character. If that substrings match we call System.exit(i+3). Now the altered behaviour of halt0 comes to play. It remaps the character values, but does not halt the program. That way the loop will continue and compare the next two bytes of our input and the weird string.

Essentially, it is using 15 different character mappings, one for each pair of characters. Now we only have to reverse those mappings using the weird string and get the second part of our flag. We can solve it with the following python script. (I omitted the mappings to save up space, you can find the working code iin solve.py)

def chars_from_mapping(mapping):
  l = mapping.split(' ')
  char_mapping = [chr(int(a, 16)) for a in l]
  return char_mapping

def complexSort(s, mapping):
  mapped_chars = [mapping[ord(c)] for c in s]
  sorted_chars = sorted(mapped_chars)
  return ''.join(sorted_chars)

def solve2():
  mappings = [] # insert byte strings from the 15 mappings here
  desired = 'Cr1KD5mk0_uUzQYifaGVqlN2B3wvpgPtSx6Odo{8hjJLHy9IXb4RnWZ}TAFEsMce7'
  secret_recipe = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz{}'
  needed = []

  for i in range(0,15):
    mapping = chars_from_mapping(mappings[i])
    secret = complexSort(desired, mapping)
    c1 = secret[i*2]
    c2 = secret[i*2+1]
    c1_new = chr(mapping.index(c1))
    c2_new = chr(mapping.index(c2))
    needed.append(c1_new)
    needed.append(c2_new)

  return ''.join(needed)

Running the solve2 function gives us the second part of our flag. _str41ght_0f_r34d1ng_J4v4_d0cs.

All together

To verify that we have the correct flag, we feed the complete string to the program. And after literally 23 hours straight of reading the Java docs, we get the ironic flag. What a challenge! HTB{th3_s3cr3t3_r3c1p3_1s_23_h0ur5_str41ght_0f_r34d1ng_J4v4_d0cs}

flag