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}