Timecode

Times change you, and numbers.

Category: misc

Solver: frcroth, mp455

Flag: ENO{S0M3_J4V4_1NT3G3R5_4R3_C4CH3D}

When we connect to the host, we get a challenge:

Registered as user b6ee888b-6f24-4049-b0e2-ee227233973f

New Challenge (2024-03-20T19:57:49.535Z) 69 51 97 43 01 65

After trying out some values, sending the same numbers gives a cryptic response:

69 51 97 43 01 65 ‘85’ is not equal to ‘69’ ‘66’ is not equal to ‘51’ ‘79’ is not equal to ‘97’ ‘86’ is not equal to ‘43’ ‘127’ is not equal to ‘01’ ‘95’ is not equal to ‘65’ Challenge failed. Connection closed.

Let’s look at the code. The server is written in Java and we took some time to understand what it does. There was a file called “ObfuscationUtil.java”, which looked suspicious. We took the time to setup debugging of the app locally and looked at what was obfuscated.

public static Integer[] getArr() throws Exception {
        Class<?> c = Class.forName(ObfuscationUtil.decrypt(Constants.C));
        Field f = c.getDeclaredField(ObfuscationUtil.decrypt(Constants.F));
        f.setAccessible(true);
        return (Integer[]) f.get(c);
    }

Using dynamic debugging we checked which class was actually called here. It was IntegerCache. What is IntegerCache. It is a cache that maps an int primitive to a Java Integer boxed object [1].

The app would then go on to create a “mapping”, dependent on the time:

public static Pair getMapping(Instant timestamp) {
    long time = timestamp.toEpochMilli();
    
    List<String> challenge = challenge(String.valueOf(time));

    String[] mapping = new String[Constants.SIZE];

    for(int i = 0; i < Constants.SIZE; i++) {
        int diff = 0;
        int index = Math.abs((int) (time % 128) * (i + 1) * Constants.LEET * Constants.ANSWER ) % Constants.SIZE;
        while(!(mapping[(index + diff) % Constants.SIZE] == null)) {
            diff += 1;
        }
        mapping[(index + diff) % Constants.SIZE] = String.valueOf(i);
    }

    return new Pair(challenge.toArray(new String[0]), mapping);
}

then, this mapping would be applied on the IntegerCache:

public static void s(Pair pair) throws Exception {
    Integer[] arr = getArr();

    for(int i = Constants.SIZE; i < arr.length; i++) {
        arr[i] = new Integer(pair.getSecond()[i - Constants.SIZE]);
    }
}

(getArr() is the function above that returns the IntegerCache). This means that depending on the mapping, the IntegerCache was rewritten, resulting in boxing of int primitives to Integer objects would then lead to different results. This is done during the checking of the user input:

try {
    comparedObj = String.valueOf((Integer) Integer.parseInt(field));
} catch(NumberFormatException nfe) {
    this.out.println(String.valueOf(field) + " is not an Integer!");
    this.bye();
}

while(comparedObj.length() < pair.getFirst()[i].length()) {
    comparedObj = "0" + comparedObj;
}

if(!(comparedObj.equals(pair.getFirst()[i]))) {
    out.println("'" + comparedObj + "' is not equal to '" + pair.getFirst()[i] + "'");
    success = false;
}

Knowing all this we could now start solving the challenge. We know how the mapping is calculated, it uses the timestamp, which is also sent to because the challenge is nice to us. We could just reimplement the mapping in python and then reverse it since it is bijective. We need to solve 10 challenges and get the flag afterwards.

Script

from pwn import *
from datetime import datetime

def solve_challenge(io):
    io.recvline() # Registered as user 727d83e4-da40-4d01-84f4-0e488c5a07af
    io.recvline() # Empty Line
    raw_timestamp = io.recvline() # New Challenge (2024-03-14T11:04:41.955Z)
    timestamp = raw_timestamp.decode().split(" ")[2][1:-2]
    time = round(datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").timestamp() * 1000)
    mapping_dict = generate_mapping_inv(time)
    raw_challenge = io.recvline() # 11 15 41 48 29 57
    challenge = raw_challenge.decode().split(" ")
    challenge_mapped = [mapping_dict[int(x)] if int(x) < 128 else int(x) for x in challenge]
    io.sendline(" ".join(str(x) for x in challenge_mapped))


def main():
    io = connect("52.59.124.14", 5015)
    for i in range(10):
        print("Solving challenge ", i)
        solve_challenge(io)
    print(io.recvall().decode())

SIZE = 128
LEET = 1337
ANSWER = 42

def generate_mapping_inv(time_as_ms):
    mapping = ["NULL"] * SIZE

    for i in range(128):
        diff = 0
        index = abs((int) (time_as_ms % 128) * (i + 1) * LEET * ANSWER ) % SIZE
        while not (mapping[(index + diff) % SIZE] == "NULL"):
            diff += 1;
        mapping[(index + diff) % SIZE] = str(i)
    mapping_dict = {  int(mapping[i]) : i for i in range(128) }    
    return mapping_dict

if __name__ == '__main__':
    main()

[+] Receiving all data: Done (96B) [*] Closed connection to 52.59.124.14 port 5015 Success! All challenges have been solved. ENO{S0M3_J4V4_1NT3G3R5_4R3_C4CH3D}

More infos

[1] https://www.geeksforgeeks.org/java-integer-cache/