Game Server Encryption

Diskussion und Informationen über UO:KR
Antworten
Nachricht
Autor
Murzelpurzel

Game Server Encryption

#1 Beitrag von Murzelpurzel » 16 Jul 2008 01:06

I did some reversing over the last two days and reversed how the game server encryption of UO:KR works.

As was said before, UO:KR uses AES/CFB/NoPadding for game server encryption. What was not yet known was the meaning of the E3 and E4 packets and how the encryption key is calculated.

First off, the structure of the 0xE3 packet:

Code: Alles auswählen

byte[1] packet cmd (0xe3) 
byte[2] packet length 
byte[4] length base
byte[length base] base
byte[4] length prime
byte[length prime] prime
byte[4] length public key
byte[length public key] public key
byte[4] unkD (0x00000020) 
byte[4] length iv
byte[length iv] iv 
Then the 0xE4 structure:

Code: Alles auswählen

byte[1] packet cmd (0xe4) 
byte[2] packet length
byte[4] length public key
byte[length public key] public key
Now for the details:
Ultima Online now uses strong cryptography for game server encryption (Why not for login?). Diffie-Hellman-Merkle is used to exchange a symmetric cipher key in a secure way. The 0xE3 packet is the first key agreement packet and contains the information required by the client to obtain the secret session key.

The base for diffie hellman is represented as an ASN.1 BER encoded integer (The original dump said 02 01 03, which is just 3 encoded using the BER).

The prime is also represented as a BER encoded integer.

The public key is just the binary representation of the public key. No BER encoding here. (This is a peculiarity of the Crypto++ interface for DH).

The last unknown element could possibly be the AES key size although I don't know why it is included in the packet...

After receiving 0xE3, the client generates his private and public key from the prime/base received by the server. (He chooses a random number for this). He then proceeds to calculate the secret session key and sends his public key to the server (packet 0xE4).

The client has a hardcoded prime, base, public and private key on the stack of the initialization function and proceeds to compute a diffie hellman session key from this information. The values currently used in the client are:
Base: 3 (As in the dynamic data)
Prime: 00 c7 77 96 c9 ea 6a 9e 9f 71 a7 27 19 d6 77 80 43
Private Key: 00 00 00 00 00 00 00 00 02 B3 43 65 0B 45 D4 AA
Public Key: 72 0E EF C3 38 13 27 5A 18 F8 AB 8A 24 68 CE 62

The resulting static session key is: 26 5D E0 9A D8 C9 1F 51 8B 62 6D 16 72 4B 83 A3

To get the AES encryption key, SHA256(static secret | dynamic secret) is computed and used directly as the 256bit key for AES as mentioned above.

If you need a proof of concept implemented in Java, feel free to ask.

Kons

#2 Beitrag von Kons » 16 Jul 2008 12:28

omg you're great :)
i'll read carefully after exams session finishes

edit: can you look at packet 0xF2 ?
it's something like global counter

structure is :

Code: Alles auswählen

byte f2
dword 0x116
word A
word B
dword 0x116
word A
word C
dword 0x116
word A
word C
it's always sent by server without request

Murzelpurzel

#3 Beitrag von Murzelpurzel » 16 Jul 2008 17:02

Kons hat geschrieben:omg you're great :)
i'll read carefully after exams session finishes

edit: can you look at packet 0xF2 ?
it's something like global counter

structure is :

Code: Alles auswählen

byte f2
dword 0x116
word A
word B
dword 0x116
word A
word C
dword 0x116
word A
word C
it's always sent by server without request
I successfully found the corresponding packet handler...

It's doing quite a lot of 64 bit integer arithmetic and seems to access the system time... My personal guess is that it's a packet that synchronizes the clients time with the server time (or something like that).

But I have to investigate further.

ps: can you give me the *full* packet? (Meaning: No placeholders)

pps: Is it possible that the client sends a 0xF1 packet after it receives 0xF2? At least the packet handler for 0xF2 suggests that.

TwoUnknownGuys

Re: Game Server Encryption

#4 Beitrag von TwoUnknownGuys » 17 Jul 2008 11:26

Murzelpurzel hat geschrieben:I did some reversing over the last two days and reversed how the game server encryption of UO:KR works.

...
Congrats!!! At least someone finished the job (too little time on our hands). Now the servers should be able to allow encrypted connection too :)


p.s.: If you have time do the POF in Java so every server community can use it to write theyr implementation :)

Murzelpurzel

#5 Beitrag von Murzelpurzel » 17 Jul 2008 12:22

I still have to write a complete implementation for my little test project but here is what I used to test the encryption on the data the client sent after the 0xE4:

Code: Alles auswählen

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Formatter;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AESCFBTest {

	public static byte[] sha256(byte[] in) throws NoSuchAlgorithmException {

		MessageDigest digest = MessageDigest.getInstance("sha-256");
		return digest.digest(in);
	}

	public static byte[] parseHex(String str) {

		String[] parts = str.split(" ");
		byte[] res = new byte[parts.length];
		for (int i = 0; i < parts.length; ++i) {
			res[i] = (byte) (Integer.valueOf(parts[i], 16) & 0xFF);
		}
		return res;
	}

	// The 0x91 packet the client sends after 0xE3 (encrypted). Sent by the
	// client.
	private static final byte[] ciphertext = parseHex("...");

	// The payload of the 0xE4 packet. Sent by the client. Prefix this with 00 if BigInteger complains about negative
	// numbers
	private static final byte[] rawOtherPublic = parseHex("...");

	// Initialization Vector. Always 16 byte. Random. Sent by server.
	private static final byte[] rawIv = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9,
			10, 11, 12, 13, 14, 15, 16 };

	public static void main(String[] args) throws Exception {

		Formatter f = new Formatter(System.out);

		/**
		 * The following values are chosen by the server and may differ.
		 */
		BigInteger g = BigInteger.valueOf(3);
		BigInteger p1 = new BigInteger(
				new byte[] { 0x00, (byte) 0xF6, 0x19, 0x45, (byte) 0xEA,
						(byte) 0x54, (byte) 0x89, (byte) 0xB0, (byte) 0xB6,
						(byte) 0xF6, (byte) 0x2E, (byte) 0x24, (byte) 0xD4,
						(byte) 0x6D, (byte) 0xE5, (byte) 0x12, (byte) 0x9B });
		BigInteger privateKey = new BigInteger(new byte[] { 0x02, (byte) 0xB3,
				(byte) 0x43, (byte) 0x65, (byte) 0x0B, (byte) 0x45,
				(byte) 0xD4, (byte) 0xAA });
		@SuppressWarnings("unused")
		BigInteger publicKey = g.modPow(privateKey, p1);

		/*
		 * Here you would send 0xE3 with (BEREncode(g), BEREncode(p1),
		 * publicKey.toByteArray(), 0x20, IV) to the client
		 */

		/*
		 * Here we continue after receiving 0xE4 from the client.
		 */
		BigInteger otherPublic = new BigInteger(rawOtherPublic);

		// Calculate the shared session secret
		BigInteger realKey = otherPublic.modPow(privateKey, p1);
		byte[] rawRealKey = realKey.toByteArray();
		System.out.print("DYNAMIC SECRET: ");
		for (byte b : rawRealKey) {
			f.format("%02X ", b);
		}
		System.out.println('\n');

		System.out
				.println("-------------------------------------------------------------------------------");

		// The following values are harcoded in the client
		BigInteger staticPublicKey = new BigInteger(
				parseHex("72 0E EF C3 38 13 27 5A 18 F8 AB 8A 24 68 CE 62"));
		BigInteger staticPrivateKey = new BigInteger(
				parseHex("00 00 00 00 00 00 00 00 02 B3 43 65 0B 45 D4 AA"));
		BigInteger p2 = new BigInteger(
				parseHex("00 c7 77 96 c9 ea 6a 9e 9f 71 a7 27 19 d6 77 80 43"));

		// This could also be included as a constant as the value is always the same.
		BigInteger staticSecret = staticPublicKey.modPow(staticPrivateKey, p2);

		System.out.print("STATIC SECRET: ");
		for (byte b : staticSecret.toByteArray()) {
			f.format("%02X ", b);
		}
		System.out.println('\n');

		System.out
				.println("-------------------------------------------------------------------------------");

		// Compute AES Key from staticSecret|dynamicSecret
		byte[] sha256Input = new byte[32];
		byte[] rawStaticSecret = staticSecret.toByteArray();
		System.arraycopy(rawStaticSecret, rawStaticSecret.length - 0x10,
				sha256Input, 0, 0x10);
		System.arraycopy(rawRealKey, rawRealKey.length - 0x10, sha256Input,
				0x10, 0x10);

		SecretKeySpec key = new SecretKeySpec(sha256(sha256Input), "AES");
		IvParameterSpec ivParam = new IvParameterSpec(rawIv);

		Cipher c = Cipher.getInstance("AES/CFB/NoPadding");
		c.init(Cipher.DECRYPT_MODE, key, ivParam);

		// Decode the value sent by the client
		byte[] plain = c.update(ciphertext);

		for (byte b : plain) {
			f.format("%02X ", b);
		}

		System.out.println('\n');
	}

}

Kons

#6 Beitrag von Kons » 07 Okt 2008 19:40

problem for Runuo is that BigInteger is not included in C# FrameNetWork 3.5 and BERConverter returns a longer value :)

Example:

OSI says : 00 00 00 03 02 01 03

Wrong Conversion:

Code: Alles auswählen

byte[] todisplay = BerConverter.Encode("i",value);
this makes right output: 2 1 3
but is wrong. Trying to Decode raise error! :)

Rigth Conversion:

Code: Alles auswählen

byte[] todisplay = BerConverter.Encode("{i}",value);
wrong output: 48 132 0 0 0 3 2 1 3
This is ok with .Decode(..) but not with our code.

BER coding Structure:
( of 48 132 0 0 0 3 2 1 3 )
48 132 0 0 0 - FIXED
3 - LENGTH of what is after
2 - TYPE ( 2 == integer )
1 - LENGTH of data
3 - DATA
In packet we are missing first part ( 48 132 ).

Prime:
( 00 00 00 13 02 11 00 B1 96 4C FF F6 DA 99 3E A5 7A 80 D6 64 14 11 5F ) is integer format too (TYPE == 02 ), but BERConverter function in C# does not support bigintegers and value is too biiig!

Antworten