Skip to content

Latest commit

 

History

History

crypto_300_custom_function

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Crypto 300 (crypto, 300p)

PL

ENG

Bardzo ciekawe zadanie dla nas, musieliśmy sporo z nim powalczyć, ale w końcu udało się rozwiązać zagadkę szyfru.

Otóż dostajemy taki szyfr:

320b1c5900180a034c74441819004557415b0e0d1a316918011845524147384f5700264f48091e45
00110e41030d1203460b1d0752150411541b455741520544111d0000131e0159110f0c16451b0f1c
4a74120a170d460e13001e120a1106431e0c1c0a0a1017135a4e381b16530f330006411953664334
593654114e114c09532f271c490630110e0b0b

Oraz kod którym go zaszyfrowano (w wersji przepisanej przez nas do pythona, nie jesteśmy fanami PHP):

def encrypt(plainText):
    space = 10
    cipherText = ""
    for i in range(len(plainText)):
        if i + space < len(plainText) - 1:
            cipherText += chr(ord(plainText[i]) ^ ord(plainText[i + space]))
        else:
            cipherText += chr(ord(plainText[i]) ^ ord(plainText[space]))
        if ord(plainText[i]) % 2 == 0:
            space += 1
        else:
            space -= 1
    return cipherText

Bardzo komplikuje wszystko ta ruszająca się space. Ale jej stan zależy tylko od najniższego bitu, co wykorzystaliśmy przy rozwiązywaniu zadania. Rozwiązywaliśmy je w dwóch częściach - najpierw odzyskaliśmy najniższe bity plaintextu, a później dopiero całe hasło.

Pierwsza próba złamania najniższych bitów wyglądała tak:

def verify(cipherText, guessedBits):
    space = 10
    for i in range(len(guessedBits)):
        if i + space < len(cipherText) - 1:
            if i + space >= len(guessedBits):
                return True
            if (ord(cipherText[i]) & 1) != ((ord(guessedBits[i]) & 1) ^ (ord(guessedBits[i + space]) & 1)):
                return False
        else:
            if space >= len(guessedBits):
                return True
            if (ord(cipherText[i]) & 1) != ((ord(guessedBits[i]) & 1) ^ (ord(guessedBits[space]) & 1)):
                return False
        if guessedBits[i] == '0':
            space += 1
        else:
            space -= 1
    return True


def decrypt(cipherText, guessedBits, i):
    if i >= len(cipherText):
        print 'ok:', guessedBits
        return
    if len(guessedBits) == 10:
        print (int(guessedBits, 2) / 1024.0) * 100, '%'
    if verify(cipherText, guessedBits):
        decrypt(cipherText, guessedBits + '0', i + 1)
        decrypt(cipherText, guessedBits + '1', i + 1)

Konkretnie, był to bruteforce z odcinaniem - sprawdzaliśmy wszystkie możliwości, i jeśli verify() się nie udał, wychodziliśmy ze sprawdzanej gałęzi

Niestety, było to dużo za wolne. Zaczeliśmy od mikrooptymalizacji - jeśli dobrze pamiętam, przyśpieszyło to wykonanie skryptu prawie 600 razy (!) (a używaliśmy już pypy do wszystkich testów i tak):

def verify(cipherText, guessedBits, length, guessed_len):
    space = 10
    for i in range(guessed_len):
        if i + space < length - 1:
            if i + space >= guessed_len:
                return True
            if (cipherText[i] & 1) != ((guessedBits[i] & 1) ^ (guessedBits[i + space] & 1)):
                return False
        else:
            if space >= guessed_len:
                return True
            if (cipherText[i] & 1) != ((guessedBits[i] & 1) ^ (guessedBits[space] & 1)):
                return False
        if guessedBits[i] == 0:
            space += 1
        else:
            space -= 1
    return True


def decrypt(cipherText):
    guessed_bits = [0] * len(cipherText)
    length = len(cipherText)
    i = 0
    orded_cipher = [ord(c) for c in cipherText]
    decrypt_r(orded_cipher, guessed_bits, i, length)


def decrypt_r(orded_cipher, guessedBits, i, length):
    if i >= length:
        print 'ok:', guessedBits
        return
    if i == 10:
        print (int(''.join(str(c) for c in guessedBits[:10]), 2) / 1024.0) * 100, '%'
    if verify(orded_cipher, guessedBits, length, i):
        guessedBits[i] = 0
        decrypt_r(orded_cipher, guessedBits, i + 1, length)
        guessedBits[i] = 1
        decrypt_r(orded_cipher, guessedBits, i + 1, length)

Wykonywało się już relatywnie szybko. Odpaliliśmy to na czterech rdzeniach u jednego z członków naszego zespołu.

Ale nie zapowiadało się to zbyt optymistycznie, więc spróbowaliśmy jeszcze zmienić podejście (viva la algorytmika!). Zamiast za każdym razem weryfikować całe hasło (i wychodzić w przód niepotrzebnie), odrzucać niemożliwe rozwiązania od razu:

def decrypt(cipherText):
    guessed_bits = ['?'] * len(cipherText)
    length = len(cipherText)
    i = 0
    orded_cipher = [ord(c) & 1 for c in cipherText]
    decrypt_r(orded_cipher, guessed_bits, i, length, 10)


def try_guess(orded_cipher, guessedbits, i, length, guess, space):
    guessedbits = list(guessedbits)
    guessedbits[i] = guess
    if i + space < length - 1:
        nextndx = i + space
    else:
        nextndx = space

    nextbit = orded_cipher[i] ^ guess
    if guess == 0:
        newspace = space + 1
    else:
        newspace = space - 1

    if guessedbits[nextndx] == '?' or guessedbits[nextndx] == nextbit:
        guessedbits[nextndx] = nextbit
        decrypt_r(orded_cipher, guessedbits, i + 1, length, newspace)

def decrypt_r(orded_cipher, guessedbits, i, length, space):
    if i >= length:
        print 'ok:', ''.join(str(c) for c in guessedbits)
        return
    if guessedbits[i] == '?':
        try_guess(orded_cipher, guessedbits, i, length, 0, space)
        try_guess(orded_cipher, guessedbits, i, length, 1, space)
    elif guessedbits[i] == 0:
        try_guess(orded_cipher, guessedbits, i, length, 0, space)
    elif guessedbits[i] == 1:
        try_guess(orded_cipher, guessedbits, i, length, 1, space)

Używamy tu swoistego triboola w tablicy guessedbits, czyli 0 oznacza "na pewno będzie tam 0", 1 oznacza "na pewno będzie tam 1", a ? oznacza ofc "nie wiadomo".

Pisanie i debugowanie tej wersji zajęło nam około godziny. A rozwiązanie dała dosłownie kilka sekund po uruchomieniu (poprzednia wersja ciągle się liczyła jeszcze).

Tak czy inaczej, poprawne rozwiązania (szukaliśmy tutaj najniższych bitów plaintextu, przypominam) były cztery:

sln = '1001011001110101110010100010110110010000010000100111010010111010010100111100001001110000011110010011110100101001010110010110101000110110100' sln = '1001011001110101110010100010110110000100010111010011010000011001001100111100111011110000000110100001110100101001010110010110101000110110100' sln = '0101011000010101110100001010000110100101001011010111000100111000010010000011001000110101001110101111110100001000110110101001010111111110100' sln = '0101011000010101110100001010000110100101001011000011000100111000110010001100011001110101000010100001110101100111000100101001010111001001011'

Otrzymaliśmy w ten sposób układ równań dla 140 zmiennych. Nasza pierwsza próba to było użycie gotowego solvera do rozwiązania tego układu. Przykładowo dla 4 rozwiązania:

from constraint import *
problem = Problem()

ODD = range(33, 128, 2) + [13]
EVEN = range(32, 128, 2) + [10]

problem.addVariable(0, ODD)
problem.addVariable(1, EVEN)
problem.addVariable(2, EVEN)
problem.addVariable(3, ODD)
problem.addVariable(4, EVEN)
# (...) snip
problem.addVariable(134, ODD)
problem.addVariable(135, EVEN)
problem.addVariable(136, ODD)
problem.addVariable(137, EVEN)
problem.addVariable(138, EVEN)

problem.addConstraint(lambda av, bv: av ^ bv == 0x32, (0, 10))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (1, 10))
problem.addConstraint(lambda av, bv: av ^ bv == 0x1c, (2, 12))
problem.addConstraint(lambda av, bv: av ^ bv == 0x59, (3, 14))
problem.addConstraint(lambda av, bv: av ^ bv == 0x0, (4, 14))
problem.addConstraint(lambda av, bv: av ^ bv == 0x18, (5, 16))
# (...) snip
problem.addConstraint(lambda av, bv: av ^ bv == 0x6, (133, 17))
problem.addConstraint(lambda av, bv: av ^ bv == 0x30, (134, 16))
problem.addConstraint(lambda av, bv: av ^ bv == 0x11, (135, 15))
problem.addConstraint(lambda av, bv: av ^ bv == 0xe, (136, 16))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (137, 15))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (138, 16))

print problem.getSolutions()

Niestety, solver wykonywał się wieki (nie mamy pewności czy wykonałby się w tym stuleciu). Dlatego, w czasie kiedy on się liczył, my zabraliśmy się za ręczne pisanie solvera:

import string
slv = [None] * len(sln)

def filling_pass(slv):
    while True:
        any = False
        space = 10
        for i in range(len(sln)):
            if i + space < len(sln) - 1:
                nx = i + space
            else:
                nx = space
            if sln[i] == '0':
                space += 1
            else:
                space -= 1
            if slv[i] is not None:
                sn = ord(slv[i]) ^ ord(ciph[i])
                if slv[nx] is None:
                    slv[nx] = chr(sn)
                    if (sn >= 32 and sn < 127) or sn == 10 or sn == 13:
                        any = True
                    else:
                        return False
                else:
                    if slv[nx] != chr(sn):
                        return False
        if not any:
            return True

def tryit(slvo, start):
    while slvo[start] is not None:
        start += 1
        if start >= len(slvo):
            print ''.join(' ' if c is None else '.' if ord(c) < 32 else c for c in slvo)
            return
        continue
    for c in string.printable:
        slv = list(slvo)
        slv[start] = c
        possible = filling_pass(slv)
        if possible:
            tryit(slv, start)

tryit(slv, 0)

I po raz drugi w tym zadaniu, to co liczyło się godziny/dni naiwnym programem, po poprawie algorytmu zadziałało w sekundy. Cieszy że mimo postępu komputerów jednak myślenie nad wydajnością ma czasami zastosowanie.

Konkretnie, nasze rozwiązanie działało na takiej zasadzie:

hasło = ['?'] * długość_hasła
while (nie wszystkie literki w haśle znane):
    for char in charset:
        podstaw char za kolejną literkę w haśle
        wywnioskuj na podstawie znanych ograniczeń jak najwięcej innych znaków (funkcja filling_pass). Jeśli wyjdzie sprzeczność, zaniechaj, inaczej rekurencyjnie powtarzaj.

Odpaliliśmy więc nasz algorytm:

C:\Users\xxx\Code\RE\CTF\2015-10-02 def\crypto300>python hackz.py
Cpgl5bpyy5qz{p5lz`5VAS5eb{pg;5[zb5\5}tcp5az5r|cp5lz`5a}p5gpbtgq5szg5tyy5a}|f5}tgq5bzg~5zg5xtlwp5r`pff|{r;5A}p5sytr5|f5vgleat{tylf|fJ|fJ}tgq
Izmf?hzss?{pqz?fpj?\KY?ohqzm1?Qph?V?w~iz?kp?xviz?fpj?kwz?mzh~m{?ypm?~ss?kwvl?w~m{?hpmt?pm?r~f}z?xjzllvqx1?Kwz?ys~x?vl?|mfok~q~sflvl@vl@w~m{
Qbu~'pbkk'chib'~hr'DSA'wpibu)'Ihp'N'ofqb'sh'`nqb'~hr'sob'ubpfuc'ahu'fkk'sont'ofuc'phul'hu'jf~eb'`rbttni`)'Sob'akf`'nt'du~wsfifk~tntXntXofuc
Rav}$sahh$`kja$}kq$GPB$tsjav*$Jks$M$lera$pk$cmra$}kq$pla$vasev`$bkv$ehh$plmw$lev`$skvo$kv$ie}fa$cqawwmjc*$Pla$bhec$mw$gv}tpejeh}wmw[mw[lev`
S`w|%r`ii%ajk`%|jp%FQC%urk`w+%Kjr%L%mds`%qj%bls`%|jp%qm`%w`rdwa%cjw%dii%qmlv%mdwa%rjwn%jw%hd|g`%bp`vvlkb+%Qm`%cidb%lv%fw|uqdkdi|vlvZlvZmdwa
Tgp{"ugnn"fmlg"{mw"AVD"rulgp,"Lmu"K"jctg"vm"ektg"{mw"vjg"pgucpf"dmp"cnn"vjkq"jcpf"umpi"mp"oc{`g"ewgqqkle,"Vjg"dnce"kq"ap{rvclcn{qkq]kq]jcpf
Ufqz#tfoo#glmf#zlv#@WE#stmfq-#Mlt#J#kbuf#wl#djuf#zlv#wkf#qftbqg#elq#boo#wkjp#kbqg#tlqh#lq#nbzaf#dvfppjmd-#Wkf#eobd#jp#`qzswbmbozpjp\jp\kbqg
Very well done you CTF pwner. Now I have to give you the reward for all this hard work or maybe guessing. The flag is cryptanalysis_is_hard
Wdsx!vdmm!enod!xnt!BUG!qvods/!Onv!H!i`wd!un!fhwd!xnt!uid!sdv`se!gns!`mm!uihr!i`se!vnsj!ns!l`xcd!ftdrrhof/!Uid!gm`f!hr!bsxqu`o`mxrhr^hr^i`se
Xk|w.ykbb.ja`k.wa{.MZH.~y`k| [email protected]{.zfk.|kyo|j.ha|.obb.zfg}.fo|j.ya|e.a|.cowlk.i{k}}g`i .Zfk.hboi.g}.m|w~zo`obw}g}Qg}Qfo|j

C:\Users\xxx\Code\RE\CTF\2015-10-02 def\crypto300>

Świetnie - zdobyliśmy flagę - cryptanalysis_is_hard

ENG version

Very interesting task for us, we had to put much effort into this by we finally solved it.

We get a ciphertext:

320b1c5900180a034c74441819004557415b0e0d1a316918011845524147384f5700264f48091e45
00110e41030d1203460b1d0752150411541b455741520544111d0000131e0159110f0c16451b0f1c
4a74120a170d460e13001e120a1106431e0c1c0a0a1017135a4e381b16530f330006411953664334
593654114e114c09532f271c490630110e0b0b

And code whcih was used to encode it (rewritten to Python since we're not fans of PHP):

def encrypt(plainText):
    space = 10
    cipherText = ""
    for i in range(len(plainText)):
        if i + space < len(plainText) - 1:
            cipherText += chr(ord(plainText[i]) ^ ord(plainText[i + space]))
        else:
            cipherText += chr(ord(plainText[i]) ^ ord(plainText[space]))
        if ord(plainText[i]) % 2 == 0:
            space += 1
        else:
            space -= 1
    return cipherText

The biggest issue here is the moving space. However its state depends only on the lowest bit of the plaintext character, which we exploited to solve the task. The solution was split into two parts - first we extracted lowest bits of the plaintext and after that we decoded the whole ciphertext.

First attempts to extract the lowest bits:

def verify(cipherText, guessedBits):
    space = 10
    for i in range(len(guessedBits)):
        if i + space < len(cipherText) - 1:
            if i + space >= len(guessedBits):
                return True
            if (ord(cipherText[i]) & 1) != ((ord(guessedBits[i]) & 1) ^ (ord(guessedBits[i + space]) & 1)):
                return False
        else:
            if space >= len(guessedBits):
                return True
            if (ord(cipherText[i]) & 1) != ((ord(guessedBits[i]) & 1) ^ (ord(guessedBits[space]) & 1)):
                return False
        if guessedBits[i] == '0':
            space += 1
        else:
            space -= 1
    return True


def decrypt(cipherText, guessedBits, i):
    if i >= len(cipherText):
        print 'ok:', guessedBits
        return
    if len(guessedBits) == 10:
        print (int(guessedBits, 2) / 1024.0) * 100, '%'
    if verify(cipherText, guessedBits):
        decrypt(cipherText, guessedBits + '0', i + 1)
        decrypt(cipherText, guessedBits + '1', i + 1)

This is a simple brute-force with prunning - we test all possibilities and if verify() failed we prune given branch.

Unfortunately this was too slow. We started with some optimization of the code - and if I remember correctly this resulted in 600 times faster execution (!) (and we were already running on pypy for all the tests):

def verify(cipherText, guessedBits, length, guessed_len):
    space = 10
    for i in range(guessed_len):
        if i + space < length - 1:
            if i + space >= guessed_len:
                return True
            if (cipherText[i] & 1) != ((guessedBits[i] & 1) ^ (guessedBits[i + space] & 1)):
                return False
        else:
            if space >= guessed_len:
                return True
            if (cipherText[i] & 1) != ((guessedBits[i] & 1) ^ (guessedBits[space] & 1)):
                return False
        if guessedBits[i] == 0:
            space += 1
        else:
            space -= 1
    return True


def decrypt(cipherText):
    guessed_bits = [0] * len(cipherText)
    length = len(cipherText)
    i = 0
    orded_cipher = [ord(c) for c in cipherText]
    decrypt_r(orded_cipher, guessed_bits, i, length)


def decrypt_r(orded_cipher, guessedBits, i, length):
    if i >= length:
        print 'ok:', guessedBits
        return
    if i == 10:
        print (int(''.join(str(c) for c in guessedBits[:10]), 2) / 1024.0) * 100, '%'
    if verify(orded_cipher, guessedBits, length, i):
        guessedBits[i] = 0
        decrypt_r(orded_cipher, guessedBits, i + 1, length)
        guessedBits[i] = 1
        decrypt_r(orded_cipher, guessedBits, i + 1, length)

This was already reasonably fast. We fired this four times for different prefixes on one machine.

But since we had to wait anyway, we decided to try a different approach (viva la algorithmics). Instead of verifying the whole password every time (by going forward) we rejected impossible solutions right away:

def decrypt(cipherText):
    guessed_bits = ['?'] * len(cipherText)
    length = len(cipherText)
    i = 0
    orded_cipher = [ord(c) & 1 for c in cipherText]
    decrypt_r(orded_cipher, guessed_bits, i, length, 10)


def try_guess(orded_cipher, guessedbits, i, length, guess, space):
    guessedbits = list(guessedbits)
    guessedbits[i] = guess
    if i + space < length - 1:
        nextndx = i + space
    else:
        nextndx = space

    nextbit = orded_cipher[i] ^ guess
    if guess == 0:
        newspace = space + 1
    else:
        newspace = space - 1

    if guessedbits[nextndx] == '?' or guessedbits[nextndx] == nextbit:
        guessedbits[nextndx] = nextbit
        decrypt_r(orded_cipher, guessedbits, i + 1, length, newspace)

def decrypt_r(orded_cipher, guessedbits, i, length, space):
    if i >= length:
        print 'ok:', ''.join(str(c) for c in guessedbits)
        return
    if guessedbits[i] == '?':
        try_guess(orded_cipher, guessedbits, i, length, 0, space)
        try_guess(orded_cipher, guessedbits, i, length, 1, space)
    elif guessedbits[i] == 0:
        try_guess(orded_cipher, guessedbits, i, length, 0, space)
    elif guessedbits[i] == 1:
        try_guess(orded_cipher, guessedbits, i, length, 1, space)

We use a tri-value-boolean in guessedbits, 0 for there is definitely 0, 1 for there is definitely 1 and ? for don't know.

It took us an hour to write this and debug but we got the results within a few seconds (while the previous version was still computing).

Anyway, the final correct sulutions (for lowest bits of the plaintext) were four:

sln = '1001011001110101110010100010110110010000010000100111010010111010010100111100001001110000011110010011110100101001010110010110101000110110100' sln = '1001011001110101110010100010110110000100010111010011010000011001001100111100111011110000000110100001110100101001010110010110101000110110100' sln = '0101011000010101110100001010000110100101001011010111000100111000010010000011001000110101001110101111110100001000110110101001010111111110100' sln = '0101011000010101110100001010000110100101001011000011000100111000110010001100011001110101000010100001110101100111000100101001010111001001011'

This way we got a set of equations with 140 variables. We tried to use a constraint-programming solver to solve it. For example for the bits number 4:

from constraint import *
problem = Problem()

ODD = range(33, 128, 2) + [13]
EVEN = range(32, 128, 2) + [10]

problem.addVariable(0, ODD)
problem.addVariable(1, EVEN)
problem.addVariable(2, EVEN)
problem.addVariable(3, ODD)
problem.addVariable(4, EVEN)
# (...) snip
problem.addVariable(134, ODD)
problem.addVariable(135, EVEN)
problem.addVariable(136, ODD)
problem.addVariable(137, EVEN)
problem.addVariable(138, EVEN)

problem.addConstraint(lambda av, bv: av ^ bv == 0x32, (0, 10))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (1, 10))
problem.addConstraint(lambda av, bv: av ^ bv == 0x1c, (2, 12))
problem.addConstraint(lambda av, bv: av ^ bv == 0x59, (3, 14))
problem.addConstraint(lambda av, bv: av ^ bv == 0x0, (4, 14))
problem.addConstraint(lambda av, bv: av ^ bv == 0x18, (5, 16))
# (...) snip
problem.addConstraint(lambda av, bv: av ^ bv == 0x6, (133, 17))
problem.addConstraint(lambda av, bv: av ^ bv == 0x30, (134, 16))
problem.addConstraint(lambda av, bv: av ^ bv == 0x11, (135, 15))
problem.addConstraint(lambda av, bv: av ^ bv == 0xe, (136, 16))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (137, 15))
problem.addConstraint(lambda av, bv: av ^ bv == 0xb, (138, 16))

print problem.getSolutions()

Unfortunately, it was taking a long time (and we're not sure if it would compute in this century). So in the meanwhile we attempted to make a custom solver:

import string
slv = [None] * len(sln)

def filling_pass(slv):
    while True:
        any = False
        space = 10
        for i in range(len(sln)):
            if i + space < len(sln) - 1:
                nx = i + space
            else:
                nx = space
            if sln[i] == '0':
                space += 1
            else:
                space -= 1
            if slv[i] is not None:
                sn = ord(slv[i]) ^ ord(ciph[i])
                if slv[nx] is None:
                    slv[nx] = chr(sn)
                    if (sn >= 32 and sn < 127) or sn == 10 or sn == 13:
                        any = True
                    else:
                        return False
                else:
                    if slv[nx] != chr(sn):
                        return False
        if not any:
            return True

def tryit(slvo, start):
    while slvo[start] is not None:
        start += 1
        if start >= len(slvo):
            print ''.join(' ' if c is None else '.' if ord(c) < 32 else c for c in slvo)
            return
        continue
    for c in string.printable:
        slv = list(slvo)
        slv[start] = c
        possible = filling_pass(slv)
        if possible:
            tryit(slv, start)

tryit(slv, 0)

And again for the second time in this task, what was taking hours in the naive implementation, took only seconds with a better algorithm. It's nice to think that even though we get more and more powerful computers, thinking about the computational complexity pays off sometimes.

The solver worked like this:

plaintext = ['?'] * plaintext_length
while (not all characters in plaintext are decoded):
    for char in charset:
        use char as next letter in password
		using known constraints try to figure out as much as possible of next characters in plaintext (filling_pass function). If there is a constraint violation break, otherwise repeat recursively.

This way we got:

C:\Users\xxx\Code\RE\CTF\2015-10-02 def\crypto300>python hackz.py
Cpgl5bpyy5qz{p5lz`5VAS5eb{pg;5[zb5\5}tcp5az5r|cp5lz`5a}p5gpbtgq5szg5tyy5a}|f5}tgq5bzg~5zg5xtlwp5r`pff|{r;5A}p5sytr5|f5vgleat{tylf|fJ|fJ}tgq
Izmf?hzss?{pqz?fpj?\KY?ohqzm1?Qph?V?w~iz?kp?xviz?fpj?kwz?mzh~m{?ypm?~ss?kwvl?w~m{?hpmt?pm?r~f}z?xjzllvqx1?Kwz?ys~x?vl?|mfok~q~sflvl@vl@w~m{
Qbu~'pbkk'chib'~hr'DSA'wpibu)'Ihp'N'ofqb'sh'`nqb'~hr'sob'ubpfuc'ahu'fkk'sont'ofuc'phul'hu'jf~eb'`rbttni`)'Sob'akf`'nt'du~wsfifk~tntXntXofuc
Rav}$sahh$`kja$}kq$GPB$tsjav*$Jks$M$lera$pk$cmra$}kq$pla$vasev`$bkv$ehh$plmw$lev`$skvo$kv$ie}fa$cqawwmjc*$Pla$bhec$mw$gv}tpejeh}wmw[mw[lev`
S`w|%r`ii%ajk`%|jp%FQC%urk`w+%Kjr%L%mds`%qj%bls`%|jp%qm`%w`rdwa%cjw%dii%qmlv%mdwa%rjwn%jw%hd|g`%bp`vvlkb+%Qm`%cidb%lv%fw|uqdkdi|vlvZlvZmdwa
Tgp{"ugnn"fmlg"{mw"AVD"rulgp,"Lmu"K"jctg"vm"ektg"{mw"vjg"pgucpf"dmp"cnn"vjkq"jcpf"umpi"mp"oc{`g"ewgqqkle,"Vjg"dnce"kq"ap{rvclcn{qkq]kq]jcpf
Ufqz#tfoo#glmf#zlv#@WE#stmfq-#Mlt#J#kbuf#wl#djuf#zlv#wkf#qftbqg#elq#boo#wkjp#kbqg#tlqh#lq#nbzaf#dvfppjmd-#Wkf#eobd#jp#`qzswbmbozpjp\jp\kbqg
Very well done you CTF pwner. Now I have to give you the reward for all this hard work or maybe guessing. The flag is cryptanalysis_is_hard
Wdsx!vdmm!enod!xnt!BUG!qvods/!Onv!H!i`wd!un!fhwd!xnt!uid!sdv`se!gns!`mm!uihr!i`se!vnsj!ns!l`xcd!ftdrrhof/!Uid!gm`f!hr!bsxqu`o`mxrhr^hr^i`se
Xk|w.ykbb.ja`k.wa{.MZH.~y`k| [email protected]{.zfk.|kyo|j.ha|.obb.zfg}.fo|j.ya|e.a|.cowlk.i{k}}g`i .Zfk.hboi.g}.m|w~zo`obw}g}Qg}Qfo|j

C:\Users\xxx\Code\RE\CTF\2015-10-02 def\crypto300>

Great, we got the flag - cryptanalysis_is_hard