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;
}