A small CTF-like challenge

This challenge was sent as part of an ITT. It covered a few basic CTF-like tasks. A single file named Annexe.txt was provided, without any further explanation.

Level 1

The provided file contains a hexdump produced by the xxd command. This output can be reverted easily to recover the binary data:

$ < Annexe.txt xxd -r | file -
/dev/stdin: pcapng capture file - version 1.0

The capture file can be explored thanks to tools such as wireshark. Several HTTP requests are revealed by filtering and wireshark allows to get a human readable view of the exchanges.

Two things can be extracted:

  • an xxd output, once again;
  • a mysterious sentence.

 

The mysterious sentence

As spaces, colons and an exclamation marks are used, the encoding is probably some kind of rot13. This kind of encoding can be easily defeated:

# apt-get install bsdgames
$ for i in $( seq 26 ); do \
caesar $i <<< "Ib jlq ab mxppb mlro zbqqb bmobrsb bpq : xslrpabglrboylkzlroxdbofbkafkprojlkqxyib !" ; \
done
Jc kmr bc nyqqc nmsp acrrc cnpcstc cqr : ytmsqbchmscpzmlamspyecpgclbglqspkmlryzjc !
Kd lns cd ozrrd ontq bdssd doqdtud drs : zuntrcdintdqanmbntqzfdqhdmchmrtqlnmszakd !
Le mot de passe pour cette epreuve est : avousdejouerboncourageriendinsurmontable !
Mf npu ef qbttf qpvs dfuuf fqsfvwf ftu : bwpvtefkpvfscpodpvsbhfsjfoejotvsnpoubcmf !
Ng oqv fg rcuug rqwt egvvg grtgwxg guv : cxqwufglqwgtdqpeqwtcigtkgpfkpuwtoqpvcdng !
Oh prw gh sdvvh srxu fhwwh hsuhxyh hvw : dyrxvghmrxhuerqfrxudjhulhqglqvxuprqwdeoh !
Pi qsx hi tewwi tsyv gixxi itviyzi iwx : ezsywhinsyivfsrgsyvekivmirhmrwyvqsrxefpi !
Qj rty ij ufxxj utzw hjyyj juwjzaj jxy : fatzxijotzjwgtshtzwfljwnjsinsxzwrtsyfgqj !
Rk suz jk vgyyk vuax ikzzk kvxkabk kyz : gbuayjkpuakxhutiuaxgmkxoktjotyaxsutzghrk !
Sl tva kl whzzl wvby jlaal lwylbcl lza : hcvbzklqvblyivujvbyhnlyplukpuzbytvuahisl !
Tm uwb lm xiaam xwcz kmbbm mxzmcdm mab : idwcalmrwcmzjwvkwcziomzqmvlqvaczuwvbijtm !
Un vxc mn yjbbn yxda lnccn nyanden nbc : jexdbmnsxdnakxwlxdajpnarnwmrwbdavxwcjkun !
Vo wyd no zkcco zyeb moddo ozboefo ocd : kfyecnotyeoblyxmyebkqobsoxnsxcebwyxdklvo !
Wp xze op alddp azfc npeep pacpfgp pde : lgzfdopuzfpcmzynzfclrpctpyotydfcxzyelmwp !
Xq yaf pq bmeeq bagd oqffq qbdqghq qef : mhagepqvagqdnazoagdmsqduqzpuzegdyazfmnxq !
Yr zbg qr cnffr cbhe prggr rcerhir rfg : nibhfqrwbhreobapbhentrevraqvafhezbagnoyr !
Zs ach rs doggs dcif qshhs sdfsijs sgh : ojcigrsxcisfpcbqcifousfwsbrwbgifacbhopzs !
At bdi st ephht edjg rtiit tegtjkt thi : pkdjhstydjtgqdcrdjgpvtgxtcsxchjgbdcipqat !
Bu cej tu fqiiu fekh sujju ufhuklu uij : qlekituzekuhredsekhqwuhyudtydikhcedjqrbu !
Cv dfk uv grjjv gfli tvkkv vgivlmv vjk : rmfljuvaflvisfetflirxvizveuzejlidfekrscv !
Dw egl vw hskkw hgmj uwllw whjwmnw wkl : sngmkvwbgmwjtgfugmjsywjawfvafkmjegflstdw !
Ex fhm wx itllx ihnk vxmmx xikxnox xlm : tohnlwxchnxkuhgvhnktzxkbxgwbglnkfhgmtuex !
Fy gin xy jummy jiol wynny yjlyopy ymn : upiomxydioylvihwioluaylcyhxchmolgihnuvfy !
Gz hjo yz kvnnz kjpm xzooz zkmzpqz zno : vqjpnyzejpzmwjixjpmvbzmdziydinpmhjiovwgz !
Ha ikp za lwooa lkqn yappa alnaqra aop : wrkqozafkqanxkjykqnwcaneajzejoqnikjpwxha !
Ib jlq ab mxppb mlro zbqqb bmobrsb bpq : xslrpabglrboylkzlroxdbofbkafkprojlkqxyib !

In the third line there is a password that will be useful for the next step.

Grabbing the xxd output

One way to get the content is to rely on a boring copy/paste operation. A second way is to save the POST request content with Wireshark and to remove all unnecessary parts by hand.

Another way is to use one feature from tshark: the -z follow option. Applied to the exchange between 10.3.253.93:1259 and 10.3.253.25:80 and with a little command line magic, the
content can be retrieved without effort:

$ while read line ; do echo $line | xxd -r -p ; done \
<<< $( tshark -nlr ./network.pcap -qz "follow,tcp,raw,10.3.253.93:1259,10.3.253.25:80" \
| tail -n +7 | head -n -1 | sed 's/^\s\+//g' ) \
| egrep '^[0-9a-z]{8}: ' > form-data.bin

Once again, the original content gets rebuilt with the command:

$ < form-data.bin xxd -r > form-data.raw

The file isn’t of any known type. However the first strings reveal its nature.

$ file form-data.raw
form-data.raw: data
$ strings -a form-data.raw | head -5
CREATED_BY
aescrypt 3.13
/d{B
mrkG
vIFkWs

 

Decryption

The strings quickly direct to AES Crypt and its Python bindings. Using a tiny python script  with the previously retrieved password allows for decrypting the data.

$ pip3 install --user pyAesCrypt
$ python3 decrypt.py
Usage: decrypt.py <encrypted file> <password>
$ python3 decrypt.py form-data.raw avousdejouerboncourageriendinsurmontable > decrypted.bin
$ file decrypted.bin
decrypted.bin: gzip compressed data, last modified: Mon Sep 16 12:25:56 2019, from Unix, original size modulo 2^32 133120
$ zcat decrypted.bin | file -
/dev/stdin: POSIX tar archive (GNU)
$ zcat decrypted.bin | tar tv
-rw-r--r-- root/root 149 2019-09-16 14:20 instructions
-rwxr-xr-x root/root 16608 2019-09-16 14:17 challenge
-rw-r--r-- root/root 105316 2019-09-16 14:25 2.tar.gz.aes
-rw-r--r-- root/root 31 2019-09-16 13:48 msg.txt

Once decrypted, the message is an archive containing a few files including a binary called challenge.

Level 2

There is still no real indication of what to do, and the binary doesn’t help either.

$ ./challenge
Segmentation fault

A quick look at a disassembly of the program in Ghidra is a good way to understand its behavior.

The 0x1b = 27 first characters of the user-provided argument are processed byte per byte and compared to a reference array. If there is a match for all these characters, the argument is the expected one. This process can be reverted with some Python lines:

base = 0x28
encoded = [
    0x6b,
    0x97,
    0x97,
    0xa9,
    0xb0,
    0xb0,
    0xc0,
    0xc0,
    0xc5,
    0xb0,
    0xdc,
    0xd6,
    0xc6,
    0xf8,
    0xfd,
    0x105,
    0xde,
    0x10e,
    0x118,
    0xf0,
    0x11c,
    0x120,
    0x125,
    0x134,
    0x119,
    0x13d,
    0x12d,
    0x150
]
for e in encoded:
    print(chr(e - base), end='')
    base += 7
print()

Running it retrieves the password:

$ python3 ./reverse.py
Challenge***JustForCheckIfOk
$
$ ./challenge Challenge***JustForCheckIfOk
C'est le bon mot de passe !!!

This password should go along nicely with the encrypted file we found in the archive.

Level 3

As guessed, the password can be used to decrypt the AES-encrypted file.

$ python3 decrypt.py 2.tar.gz.aes ChallengeAbcJustForCheckIfOk > 2.tar.gz
$ tar xfv 2.tar.gz
msg.txt
encrypted1targz
encrypt.php

The encrypt.php file shows a simple encryption method with only one byte as key. The data is hex-encoded at the end. This last step can be reverted with xxd:

$ < encrypted1targz xxd -r -p > 1.tar.gz.enc

As the encrypted file is called encrypted1targz, one can guess that the original content starts with the GZIP magic number: \x1f\x8b. Since the content of 1.tar.gz.enc starts with \x6a\xfe, the one-byte key is equal to 0x6a ^ 0x1f = 0x75. This value can be checked with the second byte. Python can be used once again to retrieve the contents:

hkey = 0x75
with open('1.tar.gz', 'wb') as fout:
    with open('1.tar.gz.enc', 'rb') as fin:
        data = fin.read()
        for d in data:
            fout.write(bytes([ d ^ hkey ]))

The gates to the next level are now open.

$ zcat 1.tar.gz | tar tv
-rw-r--r-- root/root 31 2019-09-16 13:40 msg.txt
-rw-r--r-- root/root 91430 2019-09-16 13:39 cache.png

 

Level 4

For this level there is an image of a pirate flag. Playing with GIMP and colors reveal a curious stripe at the top of the picture:

Hidden data can be detected using zteg

# apt-get install rubygems
$ gem install --user-install zsteg
$ export PATH=$HOME/.gem/ruby/2.5.0/bin:$PATH
$ zsteg -a --no-color cache.png
imagedata .. file: shared library
b1,r,lsb,xy .. text: "Z\"RBZBrG[\"RBZbRW[\"vVZ\"jB["
b1,r,msb,xy .. text: "@djZDnBZ@J"
b1,g,msb,xy .. text: "JYKHLJKHLMK"
b1,rgb,lsb,xy .. text: "0x1f0x8b0x080x000x3b0x560x7f0x5d0x000x030x4b0xcd..."
b2,g,lsb,xy .. file: shared library
...

Since one of the results seems to contain some hexadecimal values let’s try to extract it.

$ zsteg -E 'b1,rgb,lsb,xy' cache.png > hex.txt
$ printf $( < hex.txt sed 's#0x#\\x#g' ) > hex.bin
$ file hex.bin
hex.bin: gzip compressed data, last modified: Mon Sep 16 09:30:35 2019, from Unix, original size modulo 2^32 10240

And there is our next step.

Level 5

In the archive, there is two files. One is another AESCrypt encrypted file and the other contains a simple hex string.

$ ls
encryptedInstruction msgatrouverbase64.aes
$ cat encryptedInstruction
052b61242135692a24693e203a3d24692b323d6e2d2c6e323c27372820356974610a21342a2134052b112c3a283d06282b21341d3b003f2f2f2a2b320b272427

The first thing to do is to decode the hex to get an ASCII string.

$ python3
>>> encr = open('encryptedInstruction').read()
>>> dec = bytes.fromhex(encr)
>>> print(dec)
b"\x05+a$!5i*$i> :=$i+2=n-,n2<'7( 5ita\n!4*!4\x05+\x11,:(=\x06(+!4\x1d;\x00?//*+2\x0b'$'"

Since this is clearly not any kind of readable instructions, it has to be decrypted still. In a previous step, a ROT scheme was used. But that does not give anything readable here. So the next most common thing to try would be a XOR.
However, even if there isn’t any information about the key, there are two distinct methods possible to avoid bruteforcing the entire possible key space.

First method: finding the key length

When looking at the decoded string, there appear to be a few pairs of characters that are repeated two or three times like 5i or !4 for instance.
At least most of these repetitions can be assumed to be the result of XORing the same cleartext and key letters. The probable length of the key can be found by looking at the positions of the repeating characters.

>>> from re import finditer as fi
>>> [i.start() for i in fi(b'5i', dec)]
[5, 29]
>>> [i.start() for i in fi(b'\$i', dec)]
[8, 14]
>>> [i.start() for i in fi(b'!4', dec)]
[34, 37, 49]
>>> [i.start() for i in fi(b'\+2', dec)]
[16, 58]

In this hypothesis, the number of characters between each occurrence must be a multiple of the length of the key, except maybe for a few coincidences. Fortunately here, they all seem to fit that supposition since the the difference between the positions of two occurrences is always a multiple of 3.

29 -  5 = 24 = 3 * 8
14 -  8 =  6 = 3 * 2
49 - 37 = 12 = 3 * 4
...

So we know that we have a key of only three characters. It could be easy to bruteforce the 255 ^ 3 = 16581375 possible keys but, given the context of this challenge, one specific key comes to mind: *** (masked for anonymity reasons).

from itertools import cycle
encr = open('encryptedInstruction').read()
dec = bytes.fromhex(encr)
print(''.join( [chr(i ^ j) for i, j in zip(dec, cycle(b'***'))]) )

The script produces the following output:

$ python3 method1.py
Le mot de passe est le suivant : CoucouLePetitHibouTuAvancesBien

And there is our password to decrypt the other file!

Second method: guessing part of the plaintext

Even without identifying the repetitions in the ciphertext, the key could still have been found by trying to guess some parts of the plaintext. Basing the guess on the instruction message found in the first pcap file, the text is likely to mention a password (NOTE: “password” in French is “mot de passe”).

When trying to decrypt this fragment at all the possible positions in the message, the key that would have been used to encrypt the fragment at this position can be extracted. This is possible because of the properties of the XOR operation, plain XOR key = cipher equates to plain XOR cipher = key.
It is then possible to decrypt the whole message using this key, and check whether or not it makes any sense. Here, we only used a small test to see if all characters were in the printable ASCII range.

from itertools import cycle
from string import printable

encr = open('encryptedInstruction').read()
dec = bytes.fromhex(encr)
guess = b'mot de passe'
for k in range(len(dec) - len(guess)):
    test_key = ''.join([chr(i ^ j) for i, j in zip(dec[k:], guess)]).encode()
    test_clear = ''.join([chr(i ^ j) for i, j in zip(dec, cycle(test_key))])
    if test_clear.isprintable():
        print(test_clear)

The script produces the following output:

$ python3 method2.py
Le mot de passe est le suivant : CoucouLePetitHibouTuAvancesBien
Hq|p$Zhno;y@wg9=.]<*f~)Rq}*|%Zh0*XfTg{)Q.~-~coAHf{)I>o>kdxlRF}9s

The password for AESCrypt is outputted without even needing to know the XORing key.
Obviously, this only works because the key is repeated an exact number of times across the guessed text (the length of the guessed plaintext is a multiple of the length of the key).
If it was not the case, it would have been necessary to print the test_keys and see that it was just *** repeated several times. The decryption would then be exactly the same as in the first method.
With the password, the other file can be decrypted using AESCrypt.

$ python3 decrypt.py msgatrouverbase64.aes CoucouLePetitHibouTuAvancesBien > msgatrouverbase64
$ cat msgatrouverbase64 | base64 -d
***{Ceci est le message à trouver ! Si vous avez ceci bravo vous avez réussi ! Verification : ***************************************}

And this is the end of this little challenge.