|
| 1 | +--- |
| 2 | +created: 2025-01-11T01:17 |
| 3 | +updated: 2025-01-12T18:38 |
| 4 | +unsolved: true |
| 5 | +solves: 3 |
| 6 | +--- |
| 7 | + |
| 8 | +Another minecraft pcap challenge? |
| 9 | + |
| 10 | +After a bit of scanning, I realised that the communication is encrypted, keys were exchanged and `login_encryption_request` was triggered. |
| 11 | + |
| 12 | +My old writeup does not handle crypto so it won't work anymore. |
| 13 | +## packet reading. |
| 14 | + |
| 15 | +```python |
| 16 | +def packet_login_encryption_request(self, buff): |
| 17 | + p_server_id = buff.unpack_string() |
| 18 | + def unpack_array(b): return b.read(b.unpack_varint(max_bits=16)) |
| 19 | + p_public_key = unpack_array(buff) |
| 20 | + p_verify_token = unpack_array(buff) |
| 21 | + |
| 22 | + self.public_key = crypto.import_public_key(p_public_key) |
| 23 | + self.verify_token = p_verify_token |
| 24 | + print(f"login_encryption_request {p_server_id=} {len(p_public_key)=} {p_verify_token=}") |
| 25 | + print(f"{self.public_key.public_numbers()=}") |
| 26 | +``` |
| 27 | + |
| 28 | +After `login_encryption_request` and `login_encryption_response` the bytes are encrypted. |
| 29 | + |
| 30 | +Here we have the public key, but breaking RSA is basically impossible. |
| 31 | + |
| 32 | +``` |
| 33 | +self.public_key.public_numbers()=<RSAPublicNumbers(e=65537, n=130461568758849331505036135484014114043992644491371593716583161711577760699108564114633147451910541252566337707008956518324286452684332343041659860224701474509989481060885383984791170320441956230799736307044647353927078847073897040487341909288238283432126001878262186673758416165688244773338744035128369896631)> |
| 34 | +``` |
| 35 | + |
| 36 | +So I decided to look into the server's source code. |
| 37 | + |
| 38 | +## server source |
| 39 | + |
| 40 | +We can obtain the server's version via decompiling, `1.21.4`. |
| 41 | + |
| 42 | +Let's check the provided `server.jar` file with the official one. |
| 43 | + |
| 44 | +```bash |
| 45 | +$ sha256sum server.jar |
| 46 | +9429e99dc0cd0993ef1664549245d497ae1615bbee428d71bccbb0f35b20d026 server.jar |
| 47 | +$ sha256sum server.real.jar |
| 48 | +1066970b09e9c671844572291c4a871cc1ac2b85838bf7004fa0e778e10f1358 server.real.jar |
| 49 | +``` |
| 50 | + |
| 51 | +The hashes are different! |
| 52 | + |
| 53 | +Let's examine their keypair generation. |
| 54 | + |
| 55 | +```java |
| 56 | +protected void V() { |
| 57 | + l.info("Generating keypair"); |
| 58 | + |
| 59 | + try { |
| 60 | + this.ad = axx.b(); |
| 61 | + } catch (axy $$0) { |
| 62 | + throw new IllegalStateException("Failed to generate key pair", $$0); |
| 63 | + } |
| 64 | +} |
| 65 | +``` |
| 66 | + |
| 67 | +```java [axx.class] |
| 68 | +public static KeyPair b() throws axy { |
| 69 | + try { |
| 70 | + BigInteger var0; |
| 71 | + BigInteger var1; |
| 72 | + do { |
| 73 | + var0 = BigInteger.probablePrime(512, new SecureRandom()); |
| 74 | + var1 = var0.shiftRight(256).or(var0.mod(BigInteger.TWO.pow(256)).shiftLeft(256)); |
| 75 | + } while(!var1.isProbablePrime(100)); |
| 76 | + |
| 77 | + KeyFactory var2 = KeyFactory.getInstance("RSA"); |
| 78 | + return new KeyPair(var2.generatePublic(new RSAPublicKeySpec(var0.multiply(var1), new BigInteger("65537"))), var2.generatePrivate(new RSAPrivateKeySpec(var0.multiply(var1), (new BigInteger("65537")).modInverse(var0.subtract(BigInteger.ONE).multiply(var1.subtract(BigInteger.ONE)))))); |
| 79 | + } catch (Exception var4) { |
| 80 | + throw new axy(var4); |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Meanwhile the real minecraft server does this. |
| 86 | + |
| 87 | +```java [real/axx.class] |
| 88 | +public static KeyPair b() throws axy { |
| 89 | + try { |
| 90 | + KeyPairGenerator $$0 = KeyPairGenerator.getInstance("RSA"); |
| 91 | + $$0.initialize(1024); |
| 92 | + return $$0.generateKeyPair(); |
| 93 | + } catch (Exception $$1) { |
| 94 | + throw new axy($$1); |
| 95 | + } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +The server.jar we got has weak crypto! |
| 100 | + |
| 101 | +$$ |
| 102 | +\begin{align} |
| 103 | +p & = H_1 || H_0= H_1 \cdot 2^{256} + H_0 \\ |
| 104 | +q & = H_0 || H_1= H_0 \cdot 2^{256} + H_1 \\ |
| 105 | +N & = (H_1 \cdot 2^{256} + H_0) \cdot (H_0 \cdot 2^{256} + H_1) \\ |
| 106 | + & = H_1 \cdot H_0 \cdot 2^{512} + (H_1^2 + H_0^2) \cdot 2^{256} + H_0 \cdot H_1 |
| 107 | +\end{align} |
| 108 | +$$ |
| 109 | + |
| 110 | +## breaking the crypto |
| 111 | + |
| 112 | +*"By the ancient keys of cipher and code, I call forth Maximxls, the solver of enigmas, the breaker of chains! Let the digital winds carry my plea, and may your brilliance illuminate the darkest of cryptographic realms! Step forth, Maximxls, and let the challenges bow before your unyielding might!"* |
| 113 | + |
| 114 | +My teammate @Maximxls helped me. |
| 115 | + |
| 116 | +```python |
| 117 | +N=130461568758849331505036135484014114043992644491371593716583161711577760699108564114633147451910541252566337707008956518324286452684332343041659860224701474509989481060885383984791170320441956230799736307044647353927078847073897040487341909288238283432126001878262186673758416165688244773338744035128369896631 |
| 118 | +p=12392410804664940156372899354158974363722257799949007097165521217543677200745499651061566614970955433161042932829539349273403482979240433003830514342967791 |
| 119 | +q=10527537443298684008931726136394941222037097596968664524093225803422432813136644482794182361090071851369773768313662498322489166067545495287091879391949241 |
| 120 | +d=120444591875645494952655925316390343931394097303056872878249907675954230604079583620792507844085742381960209697767320965885087077828791803343693331139593837503325523117472420712626828421711064671479313284923248605961051041209084189016216843605796126868866948541735980213741309193588914265655575714268414627473 |
| 121 | +p*q == N: True |
| 122 | +``` |
| 123 | + |
| 124 | +## verification |
| 125 | + |
| 126 | +Let's verify the author's uuid with his username. |
| 127 | + |
| 128 | +``` |
| 129 | +login_start name='__toad_' |
| 130 | +uuid='25f1fa23-0d8e-4e57-810d-e45f60c3bb74' |
| 131 | +``` |
| 132 | + |
| 133 | +[Minecraft UUID / Username Converter](https://mcuuid.net/?q=__toad_) |
| 134 | + |
| 135 | +``` |
| 136 | +25f1fa23-0d8e-4e57-810d-e45f60c3bb74 |
| 137 | +``` |
| 138 | + |
| 139 | +Yay it is correct! |
| 140 | + |
| 141 | +## decoding |
| 142 | + |
| 143 | +I got stuck here for 1 entire day. |
| 144 | + |
| 145 | +I was able to salvage some info though. |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | +The chunks are intact in a ring pattern, not sure why. |
| 150 | + |
| 151 | +The centre dots are blocks changed in block events. |
| 152 | + |
| 153 | +I am forever getting bad packets, those packets start with bad lengths, causing the errors to cascade due to it consuming bytes of the next packet. |
| 154 | + |
| 155 | +For example, right after compression headers start, I have to manually remove some data or it won't parse correctly (packet id not recognized.) |
| 156 | + |
| 157 | +```python |
| 158 | +if direction == 'upstream' and lost == b'/\x80-\x9be\x13,r)\x0f_\xf4\x9bs\x1f\x12:brand\x07vanilla\x10\x00\x00\x05en_us\x16\x00\x01\x7f\x01\x01\x01\x00g\xf3\xce\x06\xef\x8a\xc7\x84\xf4\xf4\xfe\xc9\x9d\x9f\x9fjore\x061.21.4': |
| 159 | + recv_buff.add(b'\x10\x00\x00\x05en_us\x16\x00\x01\x7f\x01\x01\x01\x00g\xf3\xce\x06\xef\x8a\xc7\x84\xf4\xf4\xfe\xc9\x9d\x9f\x9fjore\x061.21.4') |
| 160 | + recv_buff.save() |
| 161 | +``` |
| 162 | + |
| 163 | +``` |
| 164 | +loading... kernel32 |
| 165 | +loading... kernel32 |
| 166 | +
|
| 167 | +--- upstream init --- s_set_protocol 0x0 |
| 168 | +handshake proto=769 localhost:25565 state=2 |
| 169 | +
|
| 170 | +--- upstream login --- s_login_start 0x0 |
| 171 | +login_start name='__toad_' |
| 172 | +
|
| 173 | +--- downstream login --- c_encryption_begin 0x1 |
| 174 | +login_encryption_request p_server_id='' len(p_public_key)=162 |
| 175 | +validate keys True |
| 176 | +p_verify_token.hex()='0ec1d269' |
| 177 | +
|
| 178 | +--- upstream login --- s_encryption_begin 0x1 |
| 179 | +
|
| 180 | +--- downstream login --- c_compress 0x3 |
| 181 | +login_set_compression threshold=256 |
| 182 | +
|
| 183 | +--- downstream login --- c_success 0x2 |
| 184 | +uuid='25f1fa23-0d8e-4e57-810d-e45f60c3bb74' |
| 185 | +name='__toad_' |
| 186 | +property_name='textures' |
| 187 | +property_value='ewogICJ0aW1lc3RhbXAiIDogMTczNDMwMjY5MjE3OSwKICAicHJvZmlsZUlkIiA6ICIyNWYxZmEyMzBkOGU0ZTU3ODEwZGU0NWY2MGMzYmI3NCIsCiAgInByb2ZpbGVOYW1lIiA6ICJfX3RvYWRfIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzUwMWY1YmE1YTM2OGYwNTQ2OGU2NzU4MDg2NDQ0ZmUzMzEwZmIxODkxMjczZjAwNGEwODRmMTA4OGRlMWVkZDQiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ==' |
| 188 | +property_signature='sYCm/hA/Dr7fX0N5lpFGfrAF2KM7pEs5XLnoVoSeby745lHmVLT7U6LYp4xd0dpPbM4WcYYiM5F5H92GMDz/HhXY9GJYTG03t3KKMPbzj2xoAw6VfyJ96BrBqgpldYb8s0nc6jQhj+d9pETmFXaVtb1Fu2tERJiOy5Ms7QvOMQmqg4eEU7LS+X3oFggQ3vMrCYt/di6jSPc2o0IK1nPLwxzDzY7sPZUq8RgbZIAiqxoU6vLoChUfzCg1g8DLloICEwKaZiLSriSiINcwnLlkG6yBeQ2lDcOh85KKJlU1CR33YqoLI/wp+zTDdB6qkiOghYmmhRlInW97agwpdMJUFFpd6ohPnO6cpnxu1R9u9b0eSUMtp32TVAtULlDRgsgk1rvY8qn+VpJafIkaLXKwIFUTnndy4jS0hbKDsYpVdLxNRmnzLRDWbTb8gaZ7RJLGnDIeWb3/S7pRTj1UxQVmHMweHlu5xvR3Z4AVmsF/5cxvJgDD0VRLAuPSCDOpyutHy+eIIBqLiyA8Xs2U79GXDnouJAP1flDFwgYXFQ0uTTem6wrsZlRxXZxX+LnlezV80jLlogTbpNxFLJV76gjQgl9i19RJ90rQPIvoLnOUGYY4iZioBjAiJrr5beI3AOPZ0DDNdU6JHwTE8bXDYgUkxodNMazuburfFdKqg3YwvYE=' |
| 189 | +
|
| 190 | +(data fix skipped 73 bytes) |
| 191 | +
|
| 192 | +--- upstream configuration --- s_settings 0x0 |
| 193 | +locale en_us |
| 194 | +view_distance 22 |
| 195 | +chat_mode 0 |
| 196 | +chat_colors True |
| 197 | +displayed_skin_parts 127 |
| 198 | +main_hand 1 |
| 199 | +enable_text_filtering True |
| 200 | +allow_server_listing True |
| 201 | +particle 0 |
| 202 | +
|
| 203 | +(data fix skipped 27 bytes) |
| 204 | +
|
| 205 | +--- downstream configuration --- c_feature_flags 0xc |
| 206 | +
|
| 207 | +--- downstream configuration --- c_select_known_packs 0xe |
| 208 | +downstream configuration KeyError losing 3069 bytes |
| 209 | +b'f>\x94K\x8a\xba#\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\... |
| 210 | +663e944b8aba239efb5a3f084fc8f44d090cf300a6a628e607d8b3e34b5cea... |
| 211 | +possible zip at 746 b'\x07\x16minecraft:trim_pattern\x12\x0eminecraft:bolt\x00\x0fminecraf' |
| 212 | +possible zip at 1304 b'\x07\x1aminecraft:painting_variant2\x0fminecraft:alban\x00\x0fmin' |
| 213 | +possible zip at 1773 b'\x07\x15minecraft:damage_type1\x0fminecraft:arrow\x00\x1bminecraf' |
| 214 | +possible zip at 2194 b'\x07\x18minecraft:banner_pattern+\x0eminecraft:base\x00\x10minecr' |
| 215 | +possible zip at 2527 b'\x07\x15minecraft:enchantment*\x17minecraft:aqua_affinity\x00\x1c' |
| 216 | +possible zip at 2912 b'\x07\x16minecraft:jukebox_song\x13\x0cminecraft:11\x00\x0cminecraft:' |
| 217 | +
|
| 218 | +--- downstream configuration --- c_registry_data 0x7 |
| 219 | +
|
| 220 | +--- downstream configuration --- c_tags 0xd |
| 221 | +``` |
| 222 | + |
| 223 | +`KeyError` means the packet id is not recognized. |
| 224 | + |
| 225 | +I don't know why that is the case, due to me fixing the data on previous packets this shouldn't happen but for some reason it does. |
| 226 | + |
| 227 | +I had to manually sieve through all the broken packets to guess where the next packet should start. |
| 228 | + |
| 229 | +``` |
| 230 | +loading... kernel32 |
| 231 | +varint(0) 102 0x66 b'>\x94K\x8a\xba#\x9e\xfbZ?\x08O\xc8\xf4M' ? |
| 232 | +varint(1) 62 0x3e b'\x94K\x8a\xba#\x9e\xfbZ?\x08O\xc8\xf4M\t' ? |
| 233 | +varint(2) 9620 0x2594 b'\x8a\xba#\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3' ? |
| 234 | +varint(3) 75 0x4b b'\x8a\xba#\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3' ? |
| 235 | +varint(4) 580874 0x8dd0a b'\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6' ? |
| 236 | +varint(5) 4538 0x11ba b'\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6' ? |
| 237 | +varint(6) 35 0x23 b'\x9e\xfbZ?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6' ? |
| 238 | +varint(7) 1490334 0x16bd9e b'?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07' ? |
| 239 | +varint(8) 11643 0x2d7b b'?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07' ? |
| 240 | +varint(9) 90 0x5a b'?\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07' ? |
| 241 | +varint(10) 63 0x3f b'\x08O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8' ? |
| 242 | +varint(11) 8 0x8 b'O\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3' ? |
| 243 | +varint(12) 79 0x4f b'\xc8\xf4M\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3' ? |
| 244 | +varint(13) 1276488 0x137a48 b'\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xea' ? |
| 245 | +varint(14) 9972 0x26f4 b'\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xea' ? |
| 246 | +varint(15) 77 0x4d b'\t\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xea' ? |
| 247 | +varint(16) 9 0x9 b'\x0c\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xeaX' ? |
| 248 | +varint(17) 12 0xc b'\xf3\x00\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9d' c_feature_flags |
| 249 | + array |
| 250 | +
|
| 251 | +varint(18) 115 0x73 b'\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9dx\xda' ? |
| 252 | +varint(19) 0 0x0 b'\xa6\xa6(\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9dx\xda' ? |
| 253 | +varint(20) 660262 0xa1326 b'\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9dx\xda\xaf\x92\x9c' ? |
| 254 | +varint(21) 5158 0x1426 b'\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9dx\xda\xaf\x92\x9c' ? |
| 255 | +varint(22) 40 0x28 b'\xe6\x07\xd8\xb3\xe3K\\\xeaX\x9dx\xda\xaf\x92\x9c' ? |
| 256 | +varint(23) 998 0x3e6 b'\xd8\xb3\xe3K\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a' ? |
| 257 | +varint(24) 7 0x7 b'\xd8\xb3\xe3K\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a' c_registry_data |
| 258 | + string array |
| 259 | +
|
| 260 | +varint(25) 158915032 0x978d9d8 b'\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a\xaf\xc7\x8d\x93' ? |
| 261 | +varint(26) 1241523 0x12f1b3 b'\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a\xaf\xc7\x8d\x93' ? |
| 262 | +varint(27) 9699 0x25e3 b'\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a\xaf\xc7\x8d\x93' ? |
| 263 | +varint(28) 75 0x4b b'\\\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a\xaf\xc7\x8d\x93' ? |
| 264 | +varint(29) 92 0x5c b'\xeaX\x9dx\xda\xaf\x92\x9c\xb8\x9a\xaf\xc7\x8d\x93j' ? |
| 265 | +``` |
| 266 | + |
| 267 | +## find all |
| 268 | + |
| 269 | +The crypto part is definitely working due to correct exchange of bytes right after the encryption packets. |
| 270 | + |
| 271 | +So instead I dumped all info to `client.all.txt` and `server.all.txt` and simply looked for all matching headers. |
| 272 | + |
| 273 | +For uncompressed data I couldn't find anything useful. |
| 274 | +### zlib header |
| 275 | + |
| 276 | +```python |
| 277 | +p = re.compile(rb'\x78[\x01\x5e\x9c\xda]') |
| 278 | +``` |
| 279 | + |
| 280 | +For each point I will run the following checks to ensure it is indeed a valid packet. |
| 281 | +Then I will handle it. |
| 282 | + |
| 283 | +```python |
| 284 | +s1, s2 = varint_lookback(by, st-1, 2) |
| 285 | +buff = Buffer(by[s2:]) |
| 286 | +# print('varints', by[s2:s1].hex(), by[s1:st].hex()) |
| 287 | +packet_len = buff.unpack_varint() |
| 288 | +buff = Buffer(buff.read(packet_len)) |
| 289 | +data_len = buff.unpack_varint() |
| 290 | +data = buff.read() |
| 291 | +data = zlib.decompress(data) |
| 292 | +if 'uoft' in data: |
| 293 | + print(data) |
| 294 | + exit() |
| 295 | +assert len(data) == data_len |
| 296 | +unzipped_out.append(data) |
| 297 | +buff = Buffer(data) |
| 298 | +pid = buff.unpack_varint() |
| 299 | +name = get_packet_name(pid, 'play', direction) |
| 300 | +pids.add(pid) |
| 301 | +fn = globals().get('packet_'+name, None) |
| 302 | +if fn: |
| 303 | + fn(buff) |
| 304 | +else: |
| 305 | + print() |
| 306 | + print(hex(pid), name) |
| 307 | + print(f"{packet_len=} {data_len=}") |
| 308 | + print(buff.buff[:50]) |
| 309 | +``` |
| 310 | + |
| 311 | +More chunks are salvaged, however a lot of data is still missing. |
| 312 | + |
| 313 | + |
| 314 | + |
| 315 | + |
| 316 | + |
| 317 | +The individual block update events are combined with data I got before and mod 100. |
| 318 | +(some positions are y=-2539520, which is obviously wrong but I included them just in case) |
| 319 | + |
| 320 | +## conclusion |
| 321 | + |
| 322 | +I couldn't solve it. |
| 323 | + |
| 324 | +I tried a lot of fixes but none of them work, the packets just keep getting misaligned. |
| 325 | + |
| 326 | +Maybe it is coz the library I share much of the serialisation logic with has discontinued and is stuck in version 1.19, or maybe more steps are needed in the capture phase, like some tcp packets should be discarded or something. |
| 327 | + |
| 328 | +Welp. |
0 commit comments