2017 Flare-On Challenge writeups

flareon4 has just ended, and it was a blast! i had learned a lot in the past few weeks and the challenges really put my skills to the test. since the fireeye team has published the full writeup on their blog, i will only cover two challenges and introduce a few remarkable tools that were used to solve them

go

Challenge 8 - flair.apk

challenge 8 provide us with an android apk file — flair.apk, and we need to solve 4 mini challenges (Michael, Brian, Milton, Printer) in order to form AES key to decrypt the final flag bytes. to solve the challenges, i used JADX for the decompilation purposes, android emulator for emulation and the awesome frida framework for dynamic instrumentation

Michael

the first challenge is fairly straightforward. it can be easily solved upon decompiling com.flare_on.flair.Michael.checkPassword() method

micheal

first challenge answer: MYPRSHE__FTW

Brian

for the second challenge, there are multiple ways to solve it. you can either solve it statically by going through the xmls or you can solve it by overriding com.flare_on.flair.Brian.teraljdknh() method. i chose the latter option

as the method will compare the input value (first argument) and the answer (second argument), we will need to overload the method with our own method which will print the second argument passed to it. after loading frida server on the emulator, the hooking process were done using the following scripts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# hook.py
import frida
import sys

scriptname = sys.argv[1]
fd = open(scriptname, "r")
procname = sys.argv[2]

def on_message(message, data):
print(message['payload'])

session = frida.get_usb_device().attach(procname)
script = session.create_script( fd.read() )
fd.close()
script.on('message', on_message)
script.load()
sys.stdin.read()
1
2
3
4
5
6
7
8
9
10
11
// brian.js
"use strict";

Java.perform(function() {
var brian = Java.use("com.flare_on.flair.Brian");
brian.teraljdknh.overload("java.lang.String", "java.lang.String").implementation = function(v, m) {
var retval = this.teraljdknh(v, m);
send(m);
return retval;
}
});

running the script on our terminal will spit out the answer for the second challenge: hashtag_covfefe_Fajitas!

answer2

Milton

Upon decompiling com.flare_on.flair.Milton class, we can see that the “submit” button is disabled by default. we can enable it by giving 4 stars on the rating bar. next, we need to make sense of the checking method — com.flare_on.flair.Milton.breop()

breop

we can see that the method will return true if the return value of Stapler.neapucx(<our input>) and nbsadf() is the same. we can easily obtain the return value of nbsadf() by overriding the method with frida

1
2
3
4
5
6
7
8
9
10
11
// milton.js
"use strict";

Java.perform(function() {
var milton = Java.use("com.flare_on.flair.Milton");
milton.nbsadf.overload().implementation = function() {
var retval = this.nbsadf();
send(retval);
return retval;
}
});

nbsadf

then we can create the reverse implementation of Stapler.neapucx() method to get the answer: 10aea594831e0b42b956c578ef9a6d44ee39938d

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# decrypt.py
import numpy as np
from ctypes import c_byte
import sys

def neapucx(to_match):
arr = []
dic = {}
ans = ""

for n in list("0123456789abcdef"):
v = c_byte(int(n, 16) << 4).value
arr.append(v)
dic[v] = n

arr = np.array(arr)

for match in to_match:
n = arr[arr <= match].max()
remain = match - n
ans += "{:s}{:x}".format(dic[n], remain)

return ans

print neapucx([16, -82, -91, -108, -125, 30, 11, 66, -71, 86, -59, 120, -17, -102, 109, 68, -18, 57, -109, -115])

answer3

Printer

in the last challenge, we can see that most of the strings are obfuscated and were decrypted with Stapler.iemm() method. we can easily obtain the original string by intercepting Stapler.iemm() return value

1
2
3
4
5
6
7
8
9
10
11
// iemm.js
"use strict";

Java.perform(function() {
var stapler = Java.use("com.flare_on.flair.Stapler");
stapler.iemm.overload("java.lang.String").implementation = function(fGLJ) {
var retval = this.iemm(fGLJ);
send(fGLJ + " -> " + retval);
return retval;
}
});

iemm

we can see that the string equals is being decrypted. and upon crosschecking with the decompiled code, we know that the method is being called in com.flare_on.flair.Printer.cgHbC(). it will check whether Stapler.neapucx(<our input>) equals Stapler.poserw(tVvV). therefore, in order to obtain the answer we need to intercept the return value of Stapler.poserw()

1
2
3
4
5
6
7
8
9
10
11
// poserw.js
"use strict";

Java.perform(function() {
var stapler = Java.use("com.flare_on.flair.Stapler");
stapler.poserw.overload("[B").implementation = function(intr) {
var retval = this.poserw(intr);
send(retval);
return retval;
}
});

poserw

then we can use our previous implementation of neapucx() to obtain the last answer: 5f1be3c9b081c40ddfc4a0238156008ee71e24a4

1
print neapucx([95, 27, -29, -55, -80, -127, -60, 13, -33, -60, -96, 35, -127, 86, 0, -114, -25, 30, 36, -92])

answer4

once we submit all the answers, we will finally obtain the flag for challenge 8: `pc_lo4d_l3tt3r_gl1tch@flare-on.com`

flag

Challenge 10 - shell.php

challenge 10 is a cryptography challenge which requires a lot guessing and manual work. we are presented with a php file that includes encrypted data which are xored in a chained manner. thus, we need to recover the key used to decrypt the data

the algorithm

1
2
3
4
for ($i = 0; $i < 2268; $i++) {
$decrypted[$i] = chr((ord($decrypted[$i]) ^ ord($key[$i])) % 256);
$key .= $decrypted[$i];
}

based the decryption algorithm above, we can conclude that the first N (key length) character of the original text is xored with the key and the remaining part of it is xored with itself to form the encrypted data. the encryption algorithm can be represented with the following expression:

expression

recovering key length

1
2
$key = isset($_POST['key']) ? $_POST['key'] : "";
$key = md5($key) . substr(MD5(strrev($key)), 0, strlen($key));

in order to break the crypto, we need to know the length of the key which is used to encrypt the plaintext data. based on the piece of code above, the possible key length will be between 32 and 64. and seeing that it is derived from md5 sums, the key can only be formed with hexadecimal characters. equipped with all the information above, i wrote a “smart” bruteforcer using z3 theorem prover to determine the possible key length

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# brute_length.py
from z3 import *
from base64 import b64decode

# base string
base = "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"
base = b64decode(base)

def solve(length):
solver = Solver()
constraint = []
chars = [BitVec("%i" % i, 8) for i in xrange(length)]

# must be hexadecimal characters
for i in xrange(length):
constraint += [
Or(
And(chars[i] >= 48, chars[i] <= 57),
And(chars[i] >= 97, chars[i] <= 102)
)
]

for i in xrange(2268):
tmp = ord(base[i]) ^ (chars[i] % 256)
# new value must be printable chars or whitespace
constraint += [
Or(
And(tmp >= 32, tmp <= 126),
tmp == 10, tmp == 13
)
]
chars.append(tmp)

solver.add(constraint)
if solver.check() == sat:
print "possible key length =", length

for i in xrange(32, 65):
solve(i)

brute length

we find out that the length of the key used in the encryption is 64 bytes. thus, we can conclude that the original string used in the encryption routine is 32 byte long, which is really too large for anyone to bruteforce (string.printable32). therefore, we need to find a more feasible way to recover the original string

the possibilities

now that we have obtained the key length, we can modify the bruteforce script to spit out every possible characters at each offset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# possible.py
from z3 import *
from base64 import b64decode

# base string
base = "QAYLHhIJbzIQClFAQgQTFkNYbz5aUBQZURIXXBARFztifGQwPRdaaFkePE9KOjMWRBRdUhtDXkIXaGkpYTc6REs7UgB6AAAATQ1FUUlOU0dWEylVOSlzfDJUGwwaXAMCeWZeXnsHSztPGn0LXnp9DlAUWksWUloCH1JaJjV0c04cLApTdR9SWiY1dHNOB1Q4XQEEJF1Fc3QyQUhBSit3Enp9Vi1UB3lEHQAABRRQXg4LPTsOT1JHBl8tN0kEQVsMRUc6EE1HMDQqe3MIYxdgFU4YRh5bX2llARVCbFwZOAYRHQJhJX52dkpwd3MaZkpvE0hTYVcUdmgyYlx4MBI4JHJ5BzAyGx03LRx9AHUqLgkAcFUiLgwcFD4+FAY+Ij4dOzoLCwAYSgciNQ0NLS0QGw91FAQ0bgULMRQCFjxKCgAzIjoMMBsKGxlmGBJJBAoWEC5XDy5sEDcuHBADBz0kGRUAERw9CyMQFDkbGBUtMhUmcQwEAhsSBiEMCyhdVSUEJjZUHiUqGg8mahAYFAwbEDIqURg9dAohDz4xFwk+MRxdMQQHLi4QAgAABhAXEHMHKnUTGXEmMhc+CwouSwgvCARhdwQAHFsKInkWGC4qOwANIWQWOnUbHzMqMQF4MQcSWil3DisqMRErCz8wEj5RFS1TNjtbXjACHyEyDABdKQQ/aQoXFWR3GiIpARMsPSYUMAwUAgdHZBQGKS8FAAAoAQEfUho/egUxKD4xJBFXBx4QCCAiKFpwGzU+AA1IATEOLgsTFGZBLAQbMj8VO2M9BS5ZLwMuJmIEEwMMGQAuRyNbRQASAEclIHMyPCM4dgAKLjoJBgQmDmh7dV1DEBhzZ2AxPTgRTUIjHj8dGCdQKDo3Ug9vX3p6ZnhBa2QXVAtGFQBFfHZXKCZIb05rZ25EOnVfbWcuIx0PDWRdRXN0ewdQTDhZHQAAVwFCTn8JaHpMQUNFDEMPFQ5LT11KERh/Q0FSSEhFDEwzO2hJQG0RaCRjO3hnKBFBaVNuFwsQJxpkA0w4ZFp8YnIYZkplKH5wYmVESUIyRHNzJy8jL2RfLSoSBAwjCUxqHgQXFB4iVj8WBHQMAgROABMbeygeBT4KBQ0YCBU1AA4GAi0uAAIAIgoAMRs2MV0UFxcPPwslADAGITEqIRYPAw8HMQcRBA8iGlp1FQQEeCATLQsuGwcMCgc7MiwCORsfBT9cBgEQJhsABxMjGCYJFwI4BDkJNVsACzEUBzI7IRgfcg8KAyo5PBNYCAQSNnEABnEMdAwDDxMGKjElBD1uMQATdR0gBh8EGhB/CQ8DVy4HDTUACwZxMQAmcgkTABwBGnIfCAMTABgNH2oGFA9XIxFbB3Mfc38bIyMTBgUQTC0DLgsYMncYBgQKcBMUJR8gFgxBNxUvJTQSF3l/IzkEAxQtQiAQSBs9HTYDJwgrEwAOABggCl1JBiINNi4HBQh9Bi0EBwNcBAYHNBw/AQ89Oxo6YC0cEikqGBAufDAtAyAPAW8KAGULCRUDZSkHOwA1CiMTXxwESBgVEg8hAhY1ehsEHzMMPWt6dkVueVhIZVhtdTopM1lwTTk0AiMLJzQENQw5LHpybmtrbB1jZ09Yd3VscB5oSBQ8Ni5zVlpfZWg1T19aeThbHQ8NZF1Fc3R7B1BMOFkdAABXBUJOfwloekxBQ0UMQw8TCExIXUoRGH9DQVJISEUMTCEASl9KbRFIJGdmGGY8NEFpcgUoHzszFWglVzltRWtpdQBpYkNLVE5jY1NkcgoVTmoaKz4tW34VKwIiHSZUR0sCOFMuDzZ/Qw0GD0gJMhQjCgMfORFZKmUdByUyCRIlHhcOdhsyZRwJFXA6NwUJfwsJPhN9AQYACwgxGEEKZ1YoJBBDGCkEf04HFA9GBjV2EQ4sLg4ZAypFFxciDAMiOR8XLAgfBDtxJRBmHDIXCz1AFi4LYAccOiANITVpDWcNAARgOgAlPAAtAEtcfA8+ahINFzkOHWNTGhdxB2MEGhAQAxcFfxkoXyAIMhsnAQsldwIDBBwUIUEDDT86OA1Ifgk1Yw8IBzhFDwFmACgXLRwxGysmAw0pRzgEdRRYJhcfDTcBUQItLQ8DFyYIBw8/ECsDWBQpEhRjJBF7HwYQOhx8Azo1DxsXIQUQBwUKCQcxBzc9ByMqSRgJFQMLACh1AxAycRR3BVkLPBsZNiAJahglBwJzFgUqBCscYQwzDi8QSBQ+Rzk8ABwmIQd4Kg8qNRUHFEYiMXYtERYFKjEaAH4gEUspCzdVJR8bMhx5F1ljAxBkQQUJECUsBRYHMhobEx8BZgwtJQcIJSIUDAMWLQMjIi10AhYfAD8zFXQnFQBbEQErFHN0TnFlU0pMGHRPUic6dU1WNAoWJhA2JxUgFjYDbRcXb29pb2huZm1hZmVwZm1hZSogIW9Qa1wRVz89aVRkJSVUS0wTfn5STEVODE8OHFkUAExSFhMtHgAeREUHTAEqJQ4OCA4NREJVABoEFUxJWQpPDhxQAAAAAABZaSodAFMMSTJFDBgRC1doZFUpYQwfCQhPVAEYFwJNSkwNTSQjLSoAAEVDTARFXUQGLS4OCQpLKWFFWRstb0NIS0QHAUZLFk1+QUcfDBZVMCgOZW8FCg5YGwRQTAgLXEwLTRQvek9TVAJbXwEBUFEQG3lzFQlOR2VjRERFTkdDBg5NQgFEAC0CAkEVFQUaCwAAVEZBBwlXXk9SHBwECwQdG0RJWwAKTlZQX1gAAVJVGkEZHE1HSQBIHwUIEAsfAg1DU0VvQxYrHUNDEwVMNg1YQUxMSwoXHRJFUAAcVQceBQFUVA0JFVgfUBMFDQZNAk4PDAhYH1ZWAlYXDRkQWB9BURwechFJRVJTVlMGABAcBBENCwoNFlheEEweHFUHHgUBVFQNCRVYH1ATBQ0GTQJODwwIWB9WVgJWFw0ZEFgfVVEcHncfXFdFXEZCB0k9DV0QFFdZBQpMFwYeVEUZC01XB1UWFBkRHwIEAAgcCh8CPQkCEh8NSh4PEA4eGFsaGXoo"
base = b64decode(base)

def solve(offset):
solver = Solver()
constraint = []
poss = []
chars = BitVec("%i" % offset, 8)

# must be hexadecimal characters
constraint += [
Or(
And(chars >= 48, chars <= 57),
And(chars >= 97, chars <= 102)
)
]

tmp = chars
for i in xrange(offset, 2268, 64):
tmp = ord(base[i]) ^ (tmp % 256)
# new value must be printable chars or whitespace
constraint += [
Or(
And(tmp >= 32, tmp <= 126),
tmp == 10, tmp == 13
)
]

solver.add(constraint)
while solver.check() == sat:
val = solver.model()[chars].as_long()
poss.append( chr(val) )
solver.add(chars != val)

print offset, "=", poss

for i in xrange(64):
solve(i)

executing the script will produce the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# mkhdznfq @ ubuntu in ~/ctf/flareon/shell [11:46:30]
$ python possible.py
0 = ['c', 'd']
1 = ['e', 'b']
2 = ['0', '8', '4', '1', '5', '9', '3', '7', '2', '6']
3 = ['9']
4 = ['2', '5']
5 = ['8', '0', '2', '6', '3', '1', '4', '7']
6 = ['b']
7 = ['8']
8 = ['4']
9 = ['f', 'a']
10 = ['8', '0', '9', '4', '5', '2', '3', '6', '7']
11 = ['7', '8', '9', '1', '0', '4', '6', '2', '5', '3']
12 = ['b']
13 = ['9']
14 = ['3']
15 = ['0', '8', '9', '1', '2', '6', '4', '5', '7', '3']
16 = ['d', 'a', 'e', 'c', 'b']
17 = ['a', 'b', 'd', 'f', 'c', 'e']
18 = ['b']
19 = ['4', '3']
20 = ['3']
21 = ['6']
22 = ['0', '8', '9', '1', '4', '5', '2', '6', '7']
23 = ['1', '6']
24 = ['8']
25 = ['a', 'd', 'e', 'b', 'c', 'f']
26 = ['d', 'c']
27 = ['9']
28 = ['d']
29 = ['9']
30 = ['0', '3', '2', '1', '4', '5', '7', '6', '9']
31 = ['a', 'b', 'f', 'd', 'e', 'c']
32 = ['8', '0', '2', '6', '7', '3', '1', '5', '9']
33 = ['0', '4', '1', '9', '5', '2', '6', '3', '7']
34 = ['0', '7']
35 = ['c', 'd']
36 = ['f', 'd', 'e']
37 = ['7', '0']
38 = ['5', '2']
39 = ['0', '7']
40 = ['4', '8', '1', '5', '9', '2', '6', '3', '7']
41 = ['4', '0', '3', '7', '8', '9']
42 = ['a']
43 = ['f', 'a']
44 = ['c']
45 = ['0', '7']
46 = ['9']
47 = ['8', '1', '5', '9', '2', '6', '3', '7']
48 = ['d']
49 = ['0', '7']
50 = ['6', '1']
51 = ['7', '0', '1', '3', '4', '6', '2', '5', '9']
52 = ['f', 'a', 'b', 'c']
53 = ['a', 'd', 'e', 'b', 'c', 'f']
54 = ['d', 'b', 'f', 'a', 'c']
55 = ['d', 'a', 'e', 'b', 'f', 'c']
56 = ['0', '8', '9', '1', '7', '6', '5', '2', '3']
57 = ['0', '7']
58 = ['9']
59 = ['f', 'a']
60 = ['0', '1', '2', '6', '3', '7']
61 = ['a', 'f', 'b', 'c', 'e']
62 = ['a', 'f']
63 = ['c', 'd']

manual labour

this is the most tedious part of the challenge as we need to manually check each possibilities. to ease the process, i had designed a simple web interface to help me construct the key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?php 
// base64 blob
$data = base64_decode('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');
// one of the possible key
$key = "ce0928b84f87b930dab436018ad9d90a800cf75044afc098d067fadd009f0aac";
?>
<html>
<head>
<title>test</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script type="text/javascript">
function lock() {
var n = parseInt($("#offset").val());
$(".p" + n).css("color", "red").off('click');
$(".k" + n).css("color", "red").off('click');
}
function pk() {
var n = parseInt($("#offset").val());
var k = $("#pk").val();
$(".k" + n).text(k);
$(".p" + n).each(function(){
k = String.fromCharCode(parseInt($(this).attr("ori")) ^ k.charCodeAt(0));
$(this).text(k);
});
}
var cur_p = $(".p0"), cur_k = $(".k0");
$(document).ready(function() {
$(".c").on('click', function(e) {
var o = $(e.target).attr("id");
$("#offset").val(o);
cur_p.css("background-color", "");
cur_k.css("background-color", "");
cur_p = $(".p" + o);
cur_k = $(".k" + o);
cur_p.css("background-color", "yellow");
cur_k.css("background-color", "yellow");
});
});
</script>
</head>
<body>
offset: <input type="text" id="offset"> <input type="button" value="lock" onclick="lock()"><br>
possible key: <input type="text" id="pk"> <input type="button" value="set" onclick="pk()"><br>
key =
<?php
for ($i = 0; $i < 64; $i++) echo "<span class='c k$i' id='$i'>$key[$i]</span>";
echo "<br><br>";
$dec = "";
foreach (str_split($data) as $i => $cipher) {
if ($i < 63) $xor = chr(ord($cipher) ^ ord($key[$i]));
else $xor = chr(ord($cipher) ^ ord($dec[$i - 64]));
$dec .= $xor;
$n = $i % 64;
echo "<span class='c p$n' id='" . ($i % 64) . "' ori='" . ord($cipher) . "'>$xor</span>";
}
?>
</body>
</html>

php

above is the sample screenshot in the middle of the guessing decryption process. the fact that the decrypted data mostly consist of php and html script sure help sped up the process a lot. full key: db6952b84a49b934acb436418ad9d93d237df05769afc796d067bccb379f2cac

subchallenges

supplying the key to the decryption routine will produce the following script (it has been reformatted to make it easier to understand):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?php
$d='';
$key = "";
if (isset($_POST['o_o']))
$key = $_POST['o_o'];
if (isset($_POST['hint']))
$d = "www.p01.org";
if (isset($_POST['t'])) {
if ($_POST['t'] == 'c') {
$d = base64_decode('SDcGHg1feVUIEhsbDxFhIBIYFQY+VwMWTyAcOhEYAw4VLVBaXRsKADMXTWxrSH4ZS1IiAgA3GxYUQVMvBFdVTysRMQAaQUxZYTlsTg0MECZSGgVcNn9AAwobXgcxHQRBAxMcWwodHV5EfxQfAAYrMlsCQlJBAAAAAAAAAAAAAAAAAFZhf3ldEQY6FBIbGw8RYlAxGEE5PkAOGwoWVHgCQ1BGVBdRCAAGQVQ2Fk4RX0gsVxQbHxdKMU8ABBU9MUADABkCGHdQFQ4TXDEfW0VDCkk0XiNcRjJxaDocSFgdck9CTgpPDx9bIjQKUW1NWwhERnVeSxhEDVs0LBlIR0VlBjtbBV4fcBtIEU8dMVoDACc3ORNPI08SGDZXA1pbSlZzGU5XVV1jGxURHQoEK0x+a11bPVsCC1FufmNdGxUMGGE=');
$key = preg_replace('/(.)../', '$1', $key);
}
if ($_POST['t'] == 's') {
$d = base64_decode('VBArMg1HYn1XGAwaAw1GDCsACwkeDgABUkAcESszBEdifVdNSENPJRkrNwgcGldMHFVfSEgwOjETEE9aRlJoZFMKFzsmQRALSilMEQsXHEUrPg9ZDRAoAwkBHVVIfzkNGAgaBAhUU00AAAAAAAAAAAAAAAAASkZSVV0KDAUCHBFQHA0MFjEVHB0BCgBNTAJVX3hkAkQiFh8ESw0AG0M5MBNRGkpdWV4bVEEVdGJGRR9XGBgcAgpVCDAsCA0GGAVWBAwcBxQqKwRCGxgbVkJFR11IdHcbRFxOUkNNV0RAVXIKSgxCWk1aVkdGQVI8dxRTVl5CR0JLVAQdOStbXkRfXlxOFEULUCp2SFJIUlVGQlUtRhExMQQLJyMmIFgDTUQtYmZIRUAECB4MHhtWRHA9Dh0WSWZmWUEHHBUzYQ==');
$key = preg_replace('/.(.)./', '$1', $key);
}
if ($_POST['t'] == 'w') {
$d = base64_decode('DycdGg1hYjl8FURaAVZxPhgNOQpdMxVIRwNKc0YDCCsDVn5sJxJMHmJJOgArB1olFA0JHQN+TlcpOgFBKUEAA1M+RVUVDjsWEy8PQUEMV3IsSgJxCFY0IkJAGVY3HV9DbQsRaU1eSxl6IR0SEykOX2gnEAwZGHJHRU0OUn4hFUUADlw8UhRPNwpaJwlZE14Df1IRDi1HS30JFlZAHnRAEQ4tR0p9CRZXQB50LFkHNgNfEgROWkVLZV1bGHVbHyJMSRFZCQtGRU0bQAFpSEtBHxsLVEdaeEEUfCd2akdKYAFaJXBdT3BeHBRFV3IdXCV1PhsUXFUBBR5hXFwwdxsab1kECFoaM0FET2pEd2owBXpAC2ZAS11sMhVmJREWVlFyDV4ldFIdcUMBWlBbcl5CSGFTUCEPW08eEyYNSgJhYjl8Tk9BCUpvDxsAODBeLwUfE08AAAAAAAAAAAAAAAAAEXFkfV1wB0ctDRM=');
$key = preg_replace('/..(.)/', '$1', $key);
}
while(strlen($key) < strlen($d))
$key = $key.$key;
$d = $d ^ $key;
}
if (strlen($d))
echo $d;
else
echo '<form action="shell.php" method="post"><input type="hidden" name="o_o" value="'.$key.'"><input type="radio" name="t" value="c"> Raytraced Checkboard<br> <input type="radio" name="t" value="s"> p01 256b Starfield<br> <input type="radio" name="t" value="w"> Wolfensteiny<br><input type="submit" value="Show"/></form>';
?>

we can see that now we have 3 more base64-encoded blobs to decode. but with following the previous methods (recover key length, obtain candidates, etc), and with a few little tweaks, we will obtain 3 substrings that can be combined to form the final flag for challenge 10: `th3_xOr_is_waaaay_too_w34k@flare-on.com`

1
2
3
4
5
6
7
8
import sys

a = "t_rsaat_4froc"
b = "hx__ayowkleno"
c = "3Oiwa_o3@a-.m"

for i in xrange(13):
sys.stdout.write(a[i] + b[i] + c[i])

flag

Final words

all over, i think my methods on solving these challenges might seems a little bit overcomplex, but it is actually a good chance for me to actually familiarize myself with all the cool toys tools, as well as demonstrate their capalities and potential use cases in ctf-ish challenges. finally, i want to congratulate all the winners who managed to complete this year challenges. and also shout out to the challenge authors for their hard work developing each of the challenges while making it a fun experience for the players