DEFCON2 (reverse, 200p, 6 solves)

description

DEFCON is the website controlling the security level of the room. The 5 different levels allows to warn the population of the risk of a potential nuclear war. Levels ranges from 5 (peacetime) to 1 (maximum alert). Try level 2.

solution

DEFCON website is pretty graphic website with lots of data, bet relevant part is "DEFCON LEVEL CONSOLE", where clicking on each level shows a popup with title "UNAUTHORIZED ACTION", text "The server requires a password for this action." and a single input field (password).

Using Chrome Developer Tools, inspecting button, following "Event Listeners" and searching defcon-2 in JavaScript files, shows how password is validated.

<div id="console">
	<button id="defcon-1">1</button>
	<button id="defcon-2">2</button>
	<button id="defcon-3">3</button>
	<button id="defcon-4">4</button>
	<button id="defcon-5">5</button>
</div>
async function validate() {
  if (modalLevel === "defcon-1") {
    return check1($("#modal-password").val());
  } else if (modalLevel === "defcon-2") {
    return await check2($("#modal-password").val());
  } else if (modalLevel === "defcon-3") {
    return check3($("#modal-password").val());
  } else if (modalLevel === "defcon-4") {
    return check4($("#modal-password").val());
  } else if (modalLevel === "defcon-5") {
    return check5($("#modal-password").val());
  } else {
    return false;
  }
}

Looking further at the level 2 validation in http://defcon.challs.malice.fr/static/js/scripts.js, reveals that it is using Doppio JVM (A Java Virtual Machine written in JavaScript) to run App class, providing password as argument.

function check2(pwd) {
    return new Promise((resolve, reject) => {
        const timeout = setTimeout(() => reject(new Error('Timeout while executing JVM')), 200000);
        new Doppio.VM.JVM({
            doppioHomePath: '/sys',
            classpath: ['/sys/classes'],
        }, function (err, jvm) {
            if (err) {
                return reject(err);
            }

            const process = BrowserFS.BFSRequire('process');
            process.initializeTTYs();
            process.stdout.on('data', function (data) {
                const messageStr = data.toString();
                if (messageStr.startsWith('result=')) {
                    process.removeAllListeners();
                    process.stdout.removeAllListeners();
                    resolve(messageStr.slice('result='.length) === 'true');
                    clearTimeout(timeout);
                }
            });

            jvm.runClass('App', [pwd], function (exitCode) {
                console.log('JVM started successfully');
                if (exitCode !== 0) {
                    reject(new Error('Script execution failed'));
                }
            });
        });
    });
}

To understand what App is doing, it must be decompiled. Using Command line Java Decompiler that is easy enough.

$ wget http://defcon.challs.malice.fr/static/java/classes/App.class
$ wget https://github.com/kwart/jd-cmd/releases/download/jd-cmd-0.9.2.Final/jd-cli-0.9.2-dist.tar.gz
$ tar -xvzf jd-cli-0.9.2-dist.tar.gz jd-cli.jar
x jd-cli.jar
$ java -jar jd-cli.jar App.class -od .
22:58:50.579 INFO  jd.cli.Main - Decompiling App.class
22:58:50.598 INFO  jd.core.output.DirOutput - Directory output will be initialized for path .
22:58:51.555 INFO  jd.core.output.DirOutput - Finished with 1 class file(s) and 0 resource file(s) written.

Looking at main function of decompiled App.java, reveals how password is validated. It is being run through 10 different functions and then compared to static base64-encoded string.

  public static void main(String[] paramArrayOfString)
  {
    byte[] arrayOfByte = paramArrayOfString[0].getBytes();
    arrayOfByte = base64_0(arrayOfByte);
    arrayOfByte = password_enchiffragement_1(arrayOfByte);
    arrayOfByte = base64_2(arrayOfByte);
    arrayOfByte = password_enchiffragement_3(arrayOfByte);
    arrayOfByte = base64_4(arrayOfByte);
    arrayOfByte = password_enchiffragement_5(arrayOfByte);
    arrayOfByte = base64_6(arrayOfByte);
    arrayOfByte = password_enchiffragement_7(arrayOfByte);
    arrayOfByte = password_enchiffragement_8(arrayOfByte);
    arrayOfByte = password_enchiffragement_9(arrayOfByte);
    
    boolean bool = Base64.getEncoder().encodeToString(arrayOfByte).equals("ZvhdxFZGZaN/M1IvAD8qaJI9UQqPluT45250BfOZPygRl68GGZqZ7QS+GTFUGcONT8r7cEkTh8gSgELMXdcURGPb25wfCIrVG6ptrLr6GJ9IpBeLP40Gu1VKwZJtdw75ud+LNgXop0KE4CGm8cCx6eqwnAFioKvBkJQEjQ==");
    System.out.println("result=" + bool);
  }

Looking at all of those functions, turns out that there are only 2 kind of functions, - password_enchiffragement_*, which simply xor's the argument and base64_*, which does similar things to base64 encoding, but with a twist.

  public static byte[] password_enchiffragement_9(byte[] paramArrayOfByte)
  {
(..)
    byte[] arrayOfByte = new byte[124];
    
    arrayOfByte[0] = ((byte)(paramArrayOfByte[5] ^ 0x8D));
    arrayOfByte[1] = ((byte)(paramArrayOfByte[98] ^ 0x4B));
    arrayOfByte[2] = ((byte)(paramArrayOfByte[123] ^ 0x36));
(..)
    arrayOfByte[123] = ((byte)(paramArrayOfByte[120] ^ 0x0));
    return arrayOfByte;
  }
  public static byte[] base64_6(byte[] paramArrayOfByte)
  {
    String str = "V-GCxZTakQFBs1oRUEtySWOiYJ0rwghfmpqub7394dejc58_AzIlDH6MNvLX2PnK";
    byte[] arrayOfByte1 = new byte[paramArrayOfByte.length + 3 - paramArrayOfByte.length % 3];
    for (int i = 0; i < paramArrayOfByte.length; i++) {
      arrayOfByte1[i] = paramArrayOfByte[i];
    }
    byte[] arrayOfByte2 = new byte[arrayOfByte1.length * 4 / 3];
    for (int j = 0; j < arrayOfByte1.length; j += 3)
    {
      arrayOfByte2[(j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[j] & 0xFF) >> 2));
      arrayOfByte2[(1 + j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[j] & 0xFF & 0x3) << 4 | (arrayOfByte1[(j + 1)] & 0xFF) >> 4));
      arrayOfByte2[(2 + j * 4 / 3)] = ((byte)str.charAt((arrayOfByte1[(j + 1)] & 0xFF & 0xF) << 2 | (arrayOfByte1[(j + 2)] & 0xFF) >> 6));
      arrayOfByte2[(3 + j * 4 / 3)] = ((byte)str.charAt(arrayOfByte1[(j + 2)] & 0xFF & 0x3F));
    }
    return arrayOfByte2;
  }

To reverse the the password_enchiffragement_* functions, the same xor function is applied, as reverse of xor is xor. To reverse base64_* functions, a brute force was chosen. It has a static str with uniquely ordered characters, which are re-arranged based on input. Each 3 input characters, after mathematical calculations, select 4 characters from str. So, to reverse it, take each 4 characters and find such 3 character combination, which fullfill mathematical conditions.

To automate this, as there are 10 functions, a Python script was written, which uses App.java to read conditions (xor bytes and magic str).

#!/usr/bin/env python
import io, re, base64

def solve_xor(c, text, level):
	text = base64.b64decode(text)
	r = [0] * len(text)
	start, srch = False, 'password_enchiffragement_{0}'.format(level)
	for line in c.split('\n'):
		if not start and srch in line:
			start = True
		if not start:
			continue
		if 'arrayOfByte' not in line:
			continue
		if 'return' in line:
			break
		mx = re.match('^.*?\[(\d+)\].*?\[(\d+)\].*?\s+\^\s+0x([0-9A-F]+).*$', line)
		if not mx:
			continue
		o1, o2, x = mx.groups()
		r[int(o2)] = chr(ord(text[int(o1)]) ^ int(x, 16))
	r = base64.b64encode(''.join(r))
	print(r)
	return r

def solve_b64(c, text, level):
	text, x = base64.b64decode(text), None
	start, srch = False, 'base64_{0}'.format(level)
	for line in c.split('\n'):
		if not start and srch in line:
			start = True
		if not start:
			continue
		if 'return' in line:
			break
		if 'str = ' in line:
			x = line.split('"')[1]
			break
	if x is None:
		return None
	r = ''
	for j in range(0, len(text), 4):
		p = text[j:j+4]
		c1, c2, c3, c4 = list(p) 
		p1, p2, p3, p4 = x.index(c1), x.index(c2), x.index(c3), x.index(c4)
		f = None
		for t1 in range(256):
			if f:
				break
			if (t1 & 0xff) >> 2 != p1:
				continue
			for t2 in range(256):
				if f:
					break
				if (t1 & 0xff & 0x3) << 4 | (t2 & 0xff) >> 4 != p2:
					continue
				for t3 in range(256):
					if (t2 & 0xff & 0xf) << 2 | (t3 & 0xff) >> 6 != p3:
						continue
					if (t3 & 0xff & 0x3f) != p4:
						continue
					f = chr(t1) + chr(t2) + chr(t3)
					break
		r += f
	r = r.replace('\x00', '')
	r = base64.b64encode(r)
	print(r)
	return r
	

with io.open('App.java') as fh:
	c = fh.read()

text = 'ZvhdxFZGZaN/M1IvAD8qaJI9UQqPluT45250BfOZPygRl68GGZqZ7QS+GTFUGcONT8r7cEkTh8gSgELMXdcURGPb25wfCIrVG6ptrLr6GJ9IpBeLP40Gu1VKwZJtdw75ud+LNgXop0KE4CGm8cCx6eqwnAFioKvBkJQEjQ=='

text = solve_xor(c, text, 9)
text = solve_xor(c, text, 8)
text = solve_xor(c, text, 7)
text = solve_b64(c, text, 6)
text = solve_xor(c, text, 5)
text = solve_b64(c, text, 4)
text = solve_xor(c, text, 3)
text = solve_b64(c, text, 2)
text = solve_xor(c, text, 1)
text = solve_b64(c, text, 0)

print(base64.b64decode(text))

Running this script, returns the flag.

$ ./solve.py
e4G8ZAvrRVdOf1eeg0NWIImWl8rF2jxX9Vr0IWtR+2hOAwQQnWElHUH6a5gLAmiluiQzRIsOY/4ytOMEM0DS6/W2zWHm5a1NDs162qmZTB6+caI7bNKfSkt6NcvZmBHBruiz7FuANgM4IVWchWjze0SV1oTckjcBjSRLaw==
hoh+op9JI0w5jAQzXHTdvQ3ClkIYoZJ8oOg8urc7DVJSzW2h+5fUSJql2caQxVmXVKXgeqMhtygXXEYYM4wPi4Hi0bdRI2yckyCkIUnkqd/Sv+x3LPjbsY/xZYFeseD1zvre3KUIZFmCINC9y1UYSvXUDYm90+x+u8aGjg==
WUV4clJ5c0VNRmFCSzZ6UHRYUjhucnJwemZNZXFyaERrWG1BcWd1YWVleEhRbk01dE9HZlVoQVNLdVpqVlkyamc1amZkOEdMMHhwNzAtZUtvNlduM0YtWUNrNDBqNmVYaERGMG5rdngtdmhUTTJsSjZSZVQtS0lYVDROVg==
YREbPTMR3KHL/2x9S7Pu+bbhxf3qibe0I7gwidjHqqE1J+3tSWCfQewU/jFrAY8rdtrfpuC6aEhlaBq/O2V+mKBYDIoar2q7e0Ka+I5EB5eG38zZ2PqGB/y7Go4=
TmxUT25DLXhnZ2FRZFdQU2J5SnNTdXdtMVRQUkw1bTQ5a0VFVUkyX3IyanliZXBtU3Jfa0NpVHFVUURiTkt2NmdtTWNQbWhuQmJTbkFyWUsyVFdDWTRid3RPZEI=
IfEYsqvQNNH8Ulgc+SRxc3LmkEgd0om5/DnnjPhCuhzS+KwmcuCDqbEBj87+Ire1NmpTgmGsA+csMuZrhElqZ5+LiYU=
dnFwdmpXcm1hUkphSmU2Y1RFMkRhekItYXhVZ1VOcUpSeDNpTE52aDdLUGlQREhCQjBmVnprcjkwZ3lPbnB4c1FRUVE=
gPJgZ6yv9WF9FkUn1EL+9w0f9VNrNsPFWVu3Csgl/Tz3z+j00xohwtyDxrHKSJVa
YTNoV1Z1MkNXYWxiUHU0S0gxbGJQdTQ4NHFZS2FlNEtNc3hsLU5PaFAyaFotbUJC
UnVObjFuOUo0djQxTko0djQ1Q3IxUHQxNXRoM2ZVdFVSMw==
RuNn1n9J4v41NJ4v45Cr1Pt15th3fUtUR3

Flag is RuNn1n9J4v41NJ4v45Cr1Pt15th3fUtUR3.