samedi 22 mars 2014

CTF Writeup: Insomni'hack 2014: encoding

On fait face à un serveur web, disons http://insomni.hack/. Sur la page d'accueil, on trouve un lien vers la "Page d'administration", http://insomni.hack/control.php/admin, qui nous gratifie d'un beau "Please authenticate.".

On tape dans le robots.txt, on y découvre:

User-Agent: *
Disallow: /backup
Disallow: /

Aha. Le répertoire backup nous offre un listing, on y découvre le fichier suivant, qu'on sauvegarde dans encoded.php:
gzuncompress(str_rot13(base64_decode('a5yVkVhCwzAMgM/1rzDVDq00sQc7rYxdKBIXBt3jgtCUtd5NEZIqWoQQ6n8n3bRUVCfEMY4/+7N9O87THKCVsw3hCAuzKrTyTct2GC3C6NWNwpdsOJ0t59Gj+9butns3fgAtlnxx4myH/MXgDiGlpLLpwmMeAEC2U8+zXCY2/na90Qjd7dP1v8E5YL0AVyReQ43bTV9MohNYoZXk12maQZ3s71a00Qtdeol91CieQywT8o7/fkjQqVrdmBaAZnlIM2NXKGoz/7PrqWNw0vnGHIpGia5BCdecejArYza5n7iVw1K135t6vgienwSg+MxnnNq5tuHKIHO2i9fzhweZL3ZDECWoJTJw1YTOb6apkmVJith7sMf6J9igO8AnqfFOGpHUZRNNM8P18Bgqbnz3A8Wt1kk=')));
On utilise php pour évaluer cette expression facilement:
$ php -r 'echo(gzuncompress(str_rot13(base64_decode('a5yVkVhCwzAMgM/1rzDVDq00sQc7rYxdKBIXBt3jgtCUtd5NEZIqWoQQ6n8n3bRUVCfEMY4/+7N9O87THKCVsw3hCAuzKrTyTct2GC3C6NWNwpdsOJ0t59Gj+9butns3fgAtlnxx4myH/MXgDiGlpLLpwmMeAEC2U8+zXCY2/na90Qjd7dP1v8E5YL0AVyReQ43bTV9MohNYoZXk12maQZ3s71a00Qtdeol91CieQywT8o7/fkjQqVrdmBaAZnlIM2NXKGoz/7PrqWNw0vnGHIpGia5BCdecejArYza5n7iVw1K135t6vgienwSg+MxnnNq5tuHKIHO2i9fzhweZL3ZDECWoJTJw1YTOb6apkmVJith7sMf6J9igO8AnqfFOGpHUZRNNM8P18Bgqbnz3A8Wt1kk='))));'

$page = substr($_SERVER["REQUEST_URI"],0,13);
$adminPage =  substr($_SERVER["REQUEST_URI"], 13);
$error = null;


if ((string)$adminPage === "admin"){
        $error = 1;
} elseif ((string)$page != "/control.php/"){
        $error = 2;
} else {
        if ((string)$adminPage != (string)urldecode($adminPage)){
                $adminPage = urldecode($adminPage);
        } else {
                $error = 2;
        }
}

if ($adminPage != (string)urldecode($adminPage)){
        if ((string)urldecode($adminPage) === "admin"){
                echo "the flag is TODO";
        }else {
                $error = 2;
        }
} elseif ((string)$adminPage === "admin") {
        $error = 1;
}

switch ($error){
        case (1):
                echo "you need to authenticate";
                break;
        case (2):
                echo "404 Not Found";
                break;
        default:
                break;
}


Pour déclencher la branche de code qui révèle le flag, les conditions suivants doivent être remplies:
(string)adminPage === "admin" => FALSE
(string)adminPage != (string)urldecode(adminPage)  => TRUE

adminPage = urldecode(adminPage);

$adminPage != (string)urldecode($adminPage)) => TRUE
(string)urldecode($adminPage) === "admin" => TRUE
Par la première condition, on doit donc remplacer le chemin d'accès, "admin", par autre chose. La deuxième condition impose que la nouvelle chaine contienne au moins une séquence d'échappement interprétée par urldecode.
Après décodage, la 3e condition impose que la chaine contienne encore une fois une séquence d'échappement, et que cette séquence, une fois décodée une deuxième fois, soit égale à "admin".
On choisit donc un caractère au hasard, ici le "i". La séquence après le premier décodage sera donc "adm%69n". On passe ensuite cette chaine dans urlencode une deuxième fois, pour obtenir "adm%3069n".
Il suffit donc de visiter http://insomni.hack/control.php/adm%3069n, et hop !

CTF Writeup: Insomni'hack 2014: Leeeetcrypto

La donnée du problème consiste en une longue suite de nombres, séparés par des espaces.

52568362028447315123743948788053644459577411882456257531616135268537279675960870614430475573303102032918797133050913 52727929746007441720068082686756949179073655255531676829839480670880742558336965750100665996845187331498825064459186 201158324192401598932420659686637057233217007355201796046752494579206057840824741418635158750620395621617197903012628 28130137328108864733654457574649869546226965870669213292484603118885398856280073620722817153185715356950196877471039 80484565918589639155839282503441828047447393078860552265541334592733372319489211090676853869300071705818380141734553 327884935077457453113605219951670824692959814316520336632744473695392968704109006664884646461226287506677371413786816 71320589399616119392997612087604459808872826741986236180026121617993570092370114471863111200409714142046077511450033 222751634152401318446067973538013344568447280940195394615901147009774216683554748179893081969917861424112117226026325 28130137328108864733654457574649869546226965870669213292484603118885398856280073620722817153185715356950196877471039 80484565918589639155839282503441828047447393078860552265541334592733372319489211090676853869300071705818380141734553 139898042261876853301455727625774012475698643685395021760275568789054192063562822563098057540361718346301097569068285 290458508376731263921252055316868138470702882414424357263569140840448897025047660176505033785801971649974769416580828 11782118315742696825087904229204586109210704966361563810081222896447101063588246593812259079361925308894139998117784 39752644457197524816840681265913188543326108820355516805599867640545430987920927262238504378204278859183980221246355 301118090575689790044304143985657097761186152450376594357207609156541662901869839587261599850312495649532760148859125 80484565918589639155839282503441828047447393078860552265541334592733372319489211090676853869300071705818380141734553 222751634152401318446067973538013344568447280940195394615901147009774216683554748179893081969917861424112117226026325 76309999578481760278134186167064490139180297738713651148555706607615754191408415493954631393292274058258783002375297 312440510356542990832274139310717029606738136784317835051579720866508272813085512326169432510915628987511601361100755 312440510356542990832274139310717029606738136784317835051579720866508272813085512326169432510915628987511601361100755 76309999578481760278134186167064490139180297738713651148555706607615754191408415493954631393292274058258783002375297 11782118315742696825087904229204586109210704966361563810081222896447101063588246593812259079361925308894139998117784 39752644457197524816840681265913188543326108820355516805599867640545430987920927262238504378204278859183980221246355 71320589399616119392997612087604459808872826741986236180026121617993570092370114471863111200409714142046077511450033 11782118315742696825087904229204586109210704966361563810081222896447101063588246593812259079361925308894139998117784 39752644457197524816840681265913188543326108820355516805599867640545430987920927262238504378204278859183980221246355 301118090575689790044304143985657097761186152450376594357207609156541662901869839587261599850312495649532760148859125 71320589399616119392997612087604459808872826741986236180026121617993570092370114471863111200409714142046077511450033 39752644457197524816840681265913188543326108820355516805599867640545430987920927262238504378204278859183980221246355 301118090575689790044304143985657097761186152450376594357207609156541662901869839587261599850312495649532760148859125 11782118315742696825087904229204586109210704966361563810081222896447101063588246593812259079361925308894139998117784 71320589399616119392997612087604459808872826741986236180026121617993570092370114471863111200409714142046077511450033 231618262599702676973845832517759334021176525056072833529569269456564473604609355284495911481371096877656941135244359 139898042261876853301455727625774012475698643685395021760275568789054192063562822563098057540361718346301097569068285 76309999578481760278134186167064490139180297738713651148555706607615754191408415493954631393292274058258783002375297 28130137328108864733654457574649869546226965870669213292484603118885398856280073620722817153185715356950196877471039 301118090575689790044304143985657097761186152450376594357207609156541662901869839587261599850312495649532760148859125 11782118315742696825087904229204586109210704966361563810081222896447101063588246593812259079361925308894139998117784 231609473505895645459311620779424869497767062819966540784040891362418669395322439617108666258332519877710534676878307 139898042261876853301455727625774012475698643685395021760275568789054192063562822563098057540361718346301097569068285 80484565918589639155839282503441828047447393078860552265541334592733372319489211090676853869300071705818380141734553 222751634152401318446067973538013344568447280940195394615901147009774216683554748179893081969917861424112117226026325 181895177774870499268628883221214310423523857203344119754826804608066049315978476367118679395919668261312392452153631 231618262599702676973845832517759334021176525056072833529569269456564473604609355284495911481371096877656941135244359 231609473505895645459311620779424869497767062819966540784040891362418669395322439617108666258332519877710534676878307 205884448854259361864211375336005072036904149095927464741554161540888665349683555934701608904819901412468420531999589
-----BEGIN PUBLIC KEY-----
MEwwDQYJKoZIhvcNAQEBBQADOwAwOAIxCGVV/7bWB7CH0Z2UsUJgM/s6zW2UCiid
twBq/aXZxGfqX0gBgwwgAAAAAAAAAGFvjQIDAQAB
-----END PUBLIC KEY-----

On reconnait le format PEM standard de stockage de clefs publiques, et on utilise openssl pour obtenir les données de la clef:
$ openssl rsa -text -noout -pubin < leet
Public-Key: (388 bit)
Modulus:
    08:65:55:ff:b6:d6:07:b0:87:d1:9d:94:b1:42:60:
    33:fb:3a:cd:6d:94:0a:28:9d:b7:00:6a:fd:a5:d9:
    c4:67:ea:5f:48:01:83:0c:20:00:00:00:00:00:00:
    00:61:6f:8d
Exponent: 65537 (0x10001)

La clef est très petite, et pourra probablement être aisément factorisée. On pourrait aussi supposer, vu le grand nombre de ciphertexts, qui collent avec la taille de la clef, que chacun est une toute petite valeur (un byte, au hasard ?) et on pourrait construire un dictionnaire de petites valeurs chiffrées avec la clef publique. Mais comme le module est petit, attaquons-le directement. Pour ça, on télécharge et on compile gmp-ecm.
wget https://gforge.inria.fr/frs/download.php/32159/ecm-6.4.4.tar.gz
tar xvzf ecm-6.4.4.tar.gz
cd ecm-6.4.4
./configure
make

gmp-ecm lit les nombres en décimal, convertissons donc ce nombre en décimal:
$ sed -e 's/://g' < modulus |tr a-f A-F
86555FFB6D607B087D19D94B1426033FB3ACD6D940A289DB7006AFDA5D9C467EA5F4801830C2000000000000000616F8D
$ tr a-f A-F <<< "16 i 86555FFB6D607B087D19D94B1426033FB3ACD6D940A289DB7006AFDA5D9C467EA5F4801830C2000000000000000616F8D p" |dc
330813077170623813868916447047968257225466236033071266804905080468565157617117193466673715216384000000000000006385549

On passe ce nombre à gmp-ecm:
$ ./ecm 100000 <<< 330813077170623813868916447047968257225466236033071266804905080468565157617117193466673715216384000000000000006385549
GMP-ECM 6.4.4 [configured with GMP 5.1.3, --enable-asm-redc] [ECM]
Input number is 330813077170623813868916447047968257225466236033071266804905080468565157617117193466673715216384000000000000006385549 (117 digits)
Using B1=100000, B2=40868532, polynomial x^2, sigma=3677913806
Step 1 took 471ms
********** Factor found in step 1: 13331
Found probable prime factor of  5 digits: 13331
Probable prime cofactor 24815323469403931728221172233738523533528335161133543380459461440894543366372904768334987264000000000000000000479 has 113 digits

Parfait, les deux facteurs sont premiers selon toute vraisemblance. Supposons qu'une version minimale de RSA a été utilisée, calculons l'exposant de déchiffrement avec un petit programme Haskell:
-- Puissance modulaire rapide:
-- emod a e n = (a^e `mod` n)
emod _ 0 _ = 1
emod a e n | odd e     = (a * square (emod a (e `div` 2) n)) `mod` n
           | otherwise = square (emod a (e `div` 2) n) `mod` n

-- Algorithme d'Euclide étendu, pour résoudre l'équation de Bézout
-- euclid a b = (i,j,k) | a*i + b*j = k
euclid a b
  | b > a = let (j,i,k) = euclid b a in (i,j,k)
  | otherwise = euclid' (1,0,a) (0,1,b) where
      euclid' p@(i,j,k) q@(i',j',k')
        | k' == 0   = p
        | otherwise = let f = k `div` k'
                in euclid' q (i-i'*f, j-j'*f, k-k'*f)


n = 330813077170623813868916447047968257225466236033071266804905080468565157617117193466673715216384000000000000006385549
p = 13331
q = n `div` p

phi = (p-1)*(q-1)

e = 65537

-- Résoudre d*e + k*phi = g, pour g = PGCD(e,phi)
(d,k,g) = euclid e phi
-- On vérifie que g == 1

decrypt c = emod c (d `mod` phi) n

-- Les ciphertexts en dur, on découpe sur les espaces.
ciphers :: [Integer]
ciphers = map read $ words "5256...0913 5272792...31999589"


On teste dans GHCi:
*Main> map decrypt ciphers
[73,78,83,49,52,123,55,50,49,52,51,100,57,102,97,52,50,48,53,53,48,57,102,55,57,102,97,55,102,97,57,55,99,51,48,49,97,57,56,51,52,50,54,99,56,125]

Clairement un résultat intéressant, on dirait de l'ascii.
*Main Data.Char> map (chr.fromInteger.decrypt) c
"INS14{72143d9fa4205509f79fa7fa97c301a983426c8}"

Success !!!

CTF Writeup: Insomni'hack 2014: protocrypt

Au début du problème, nous avons à disposition 3 informations: une capture réseau, un programme client, et l'addresse du serveur.

capture.pcapng: pcap-ng capture file - version 1.0
client:         ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=04d200ccd619d8bc33a87567b74b2e3910515eaf, not stripped

Je décide de regarder la capture réseau en premier, et l'ouvre avec Wireshark. Il contient un seul flot tcp:

.zw.t]u.....t.]P]e{.W...k.......zk...z]..Z]gy.t.]..]z.]~{..]y.t.].w]wy.]....zwZ

Initelligible. Je décide de ne pas regarder le serveur, et commence de suite à analyser le binaire. Malheureusement, c'est un binaire 64bits, donc la version gratuire d'IDA ne l'analysera pas. Qu'a cela ne tienne, je l'attaque avec Objdump. Heureusement, le binaire n'a pas été strippé, et parmi les symboles, on voit deux routines, "encrypt" et "decrypt".
Le désassemblage de la routine "encrypt", commenté à la main:
0000000000400f7e :
  400f7e: 55                    push   rbp
  400f7f: 48 89 e5              mov    rbp,rsp

  // Save registers on stack
  400f82: 48 89 7d e8           mov    QWORD PTR [rbp-0x18],rdi  // char *destination;
  400f86: 89 75 e4              mov    DWORD PTR [rbp-0x1c],esi  // int length;
  400f89: 48 89 55 d8           mov    QWORD PTR [rbp-0x28],rdx  // int *key
  400f8d: c7 45 fc 00 00 00 00  mov    DWORD PTR [rbp-0x4],0x0   // int counter;
  400f94: c7 45 fc 00 00 00 00  mov    DWORD PTR [rbp-0x4],0x0


  // while (counter = 0; counter < length; counter++) {
  400f9b: e9 8f 00 00 00        jmp    40102f 


  //// rdx = &destination[counter]
  400fa0: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  400fa3: 48 63 d0              movsxd rdx,eax
  400fa6: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
  400faa: 48 01 c2              add    rdx,rax
  //// rax = &destination[counter]
  400fad: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  400fb0: 48 63 c8              movsxd rcx,eax
  400fb3: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
  400fb7: 48 01 c8              add    rax,rcx

  //// eax = *rax
  400fba: 0f b6 00              movzx  eax,BYTE PTR [rax]
  400fbd: 89 c1                 mov    ecx,eax
  400fbf: 48 8b 45 d8           mov    rax,QWORD PTR [rbp-0x28]
  400fc3: 8b 00                 mov    eax,DWORD PTR [rax]
  ///// eax ^= key[0]
  400fc5: 31 c8                 xor    eax,ecx
  ///// destination[counter] = eax
  400fc7: 88 02                 mov    BYTE PTR [rdx],al

  //// rdx = &destination[counter]
  400fc9: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  400fcc: 48 63 d0              movsxd rdx,eax
  400fcf: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
  400fd3: 48 01 c2              add    rdx,rax

  //// rax = &destination[counter]
  400fd6: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  400fd9: 48 63 c8              movsxd rcx,eax
  400fdc: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
  400fe0: 48 01 c8              add    rax,rcx

  //// esi = *rax
  400fe3: 0f b6 00              movzx  eax,BYTE PTR [rax]
  400fe6: 0f b6 f0              movzx  esi,al

  //// ecx = key[1]
  400fe9: 48 8b 45 d8           mov    rax,QWORD PTR [rbp-0x28]
  400fed: 48 83 c0 04           add    rax,0x4
  400ff1: 8b 00                 mov    eax,DWORD PTR [rax]
  400ff3: 89 c1                 mov    ecx,eax

  //// esi < ecx
  400ff5: d3 e6                 shl    esi,cl

  //// rax = &destination[counter]
  400ffb: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  400ffe: 48 63 c8              movsxd rcx,eax
  401001: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
  401005: 48 01 c8              add    rax,rcx

  //// edi = *rax
  401008: 0f b6 00              movzx  eax,BYTE PTR [rax]
  40100b: 0f b6 f8              movzx  edi,al

  //// ecx = 8 - key[1]
  40100e: 48 8b 45 d8           mov    rax,QWORD PTR [rbp-0x28]
  401012: 48 83 c0 04           add    rax,0x4
  401016: 8b 00                 mov    eax,DWORD PTR [rax]
  401018: b9 08 00 00 00        mov    ecx,0x8
  40101d: 29 c1                 sub    ecx,eax
  40101f: 89 c8                 mov    eax,ecx
  401021: 89 c1                 mov    ecx,eax

  //// edi < ecx
  401023: d3 ff                 sar    edi,cl

  //// *rdx = esi | edi
  401025: 89 f8                 mov    eax,edi
  401027: 09 f0                 or     eax,esi
  401029: 88 02                 mov    BYTE PTR [rdx],al

  // }
  40102b: 83 45 fc 01           add    DWORD PTR [rbp-0x4],0x1
  40102f: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
  401032: 3b 45 e4              cmp    eax,DWORD PTR [rbp-0x1c]
  401035: 0f 8c 65 ff ff ff     jl     400fa0 


  40103b: 90                    nop
  40103c: 5d                    pop    rbp
  40103d: c3                    ret    

Une routine particulièrement peu optimisée pour le code C suivant:
void encrypt(char *data, int length, int *key) {
    int counter = 0;
    for (counter = 0; counter < length; counter++) {
        data[counter] ^= key[0];
        data[counter] = (data[counter] << key[1]) | (data[counter] >> (8 - key[1]));
    }
}

Je vous épargne la routine de déchiffrement, tout aussi sous-optimisée, mais cohérente avec le reste.

Il n'y a que 256 valeurs effectives possibles pour key[0], et 8 pour key[1], soit 2048 clefs possibles au total. Le client, quand il est exécuté, demande une seule valeur de 4 chiffres comme clef. Le client et le serveur utilisent donc probablement la même pour communiquer. Nous allons bruteforcer la clef utilisée pour la capture.

Pour commencer, il nous faut des statistiques sur la probabilité d'apparition des bytes individuels dans le texte clair. Je commence par supposer que le protocole contient du texte.

Ne trouvant rien de probant sur internet, je commence par collecter des bouts de fichier texte un peu partout dans mon système:

find /home/max -name "*.txt" |head -n100 |xargs cat > bigrandomtextfile.txt

Puis j'utilise le fragment de Haskell suivant pour générer des statistiques:
import qualified Data.ByteString as B
import qualified Data.Map as M

main = do
        text <- B.readFile "textfiles"
        let stats = M.fromListWith (+) . (`zip` [1,1..]) $ [0..255] ++ B.unpack text
        print stats

On découpe le fichier en bytes avec B.unpack, on y ajout une fois chaque bytes de 0 à 255 pour ne pas avoir de 0 dans la map, et on affiche le résultat.
frequencyTable = M.fromList [(0,1),(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,144),(10,172065),(11,1),(12,10),(13,6359),(14,1),(15,1),(16,1),(17,1),(18,1),(19,1),(20,1),(21,1),(22,1),(23,1),(24,1),(25,1),(26,1),(27,1),(28,1),(29,1),(30,1),(31,1),(32,903319),(33,431),(34,1358),(35,460),(36,75),(37,511),(38,9356),(39,6813),(40,13377),(41,13489),(42,646),(43,4551),(44,12776),(45,9806),(46,47367),(47,179551),(48,138265),(49,30661),(50,55552),(51,48834),(52,28428),(53,45934),(54,24841),(55,26279),(56,38631),(57,25568),(58,48597),(59,9702),(60,227703),(61,2944),(62,227708),(63,1713),(64,1541),(65,7344),(66,27714),(67,22095),(68,20877),(69,21439),(70,6026),(71,3751),(72,3768),(73,11286),(74,776),(75,523),(76,9283),(77,4458),(78,13869),(79,7889),(80,10272),(81,239),(82,30229),(83,13744),(84,22801),(85,29150),(86,784),(87,2025),(88,459),(89,6076),(90,137),(91,5841),(92,53),(93,5841),(94,16),(95,282275),(96,36),(97,308952),(98,82152),(99,185842),(100,163847),(101,561599),(102,183557),(103,113944),(104,62272),(105,340221),(106,2968),(107,37168),(108,277212),(109,112984),(110,259352),(111,393856),(112,143240),(113,13026),(114,362535),(115,263072),(116,422578),(117,185756),(118,28394),(119,51245),(120,15040),(121,40527),(122,5384),(123,1),(124,4),(125,1),(126,8),(127,1),(128,253),(129,154),(130,133),(131,415),(132,106),(133,44),(134,34),(135,70),(136,173),(137,108),(138,113),(139,104),(140,60),(141,87),(142,102),(143,25),(144,178),(145,131),(146,27),(147,28),(148,293),(149,335),(150,72),(151,47),(152,57),(153,85),(154,61),(155,65),(156,44),(157,73),(158,51),(159,18),(160,605),(161,63),(162,66),(163,37),(164,23),(165,52),(166,30),(167,22),(168,32),(169,19),(170,46),(171,41),(172,11),(173,52),(174,10),(175,5),(176,72),(177,130),(178,74),(179,91),(180,40),(181,99),(182,34),(183,33),(184,395),(185,184),(186,47),(187,50),(188,85),(189,246),(190,89),(191,128),(192,1),(193,1),(194,14),(195,19),(196,3),(197,4),(198,1),(199,1),(200,1),(201,10),(202,6),(203,9),(204,3),(205,1),(206,454),(207,249),(208,220),(209,110),(210,1),(211,1),(212,1),(213,1),(214,1),(215,1),(216,1),(217,1),(218,1),(219,1),(220,1),(221,1),(222,1),(223,1),(224,413),(225,934),(226,1322),(227,5),(228,1),(229,1),(230,1),(231,1),(232,1),(233,1),(234,1),(235,1),(236,1),(237,1),(238,1),(239,2),(240,1),(241,1),(242,1),(243,1),(244,1),(245,1),(246,1),(247,1),(248,1),(249,1),(250,1),(251,1),(252,1),(253,1),(254,1),(255,1)]
Après avoir sauvegardé, avec wireshark, le dialogue chiffré dans un fichier "encrypted", j'utilise le programme Haskell suivant pour bruteforcer la clef:
type Key = (Word8, Int)

decrypt :: Key -> B.ByteString -> String
decrypt (k0, k1) = map (chr . fromIntegral . flip rotateL k1 . xor k0) . B.unpack

encrypt :: Key -> String -> B.ByteString
encrypt (k0, k1) = B.pack . map (xor k0 . flip rotateR k1 . fromIntegral . ord)

allKeys :: [(Word8, Int)]
allKeys = [ (k0, k1) | k0 <- [0..255], k1 <- [0..7] ]

score s = product [ frequencyTable M.! (ord c) | c <- s ]

bestDecryption = do
        f <- B.readFile "encrypted"
        let cipher = B.unpack f
        let clears = [ (k, decrypt k cipher) | k <- allKeys ]
        let best = last . sortBy (comparing (score.snd)) $ clears
        return best

On obtient k = (77,1), et le message déchiffré:
Enter password : Ple4s3_L37_m3_1nLogin OK. There is no flag here at the moment.
Pour tester, plutot que d'essayer de désassembler le main, j'utilise rapidement gdb pour comprendre le format de clef du programme client. Je mets un breakpoint sur decrypt, lance le programme avec diverses valeurs et inspecte les valeurs de la variable key[]. Le client accepte une clef de 4 chiffres au format "xxxy", x est k[0] en décimal, y est k[1].

J'essaie de lancer le client sur le serveur de production, avec la clef "0771", mais le client refuse, indiquant qu'il ne peut déchiffrer la réponse du serveur.
Je suppose donc que le client attend la chaîne fixe "Enter password : " avant de continuer, et visiblement, le serveur utilise une clef différente. Quelques essais avec netcat montrent que la clef change à chaque connexion. Nous allons donc implémenter un programme qui se connecte au serveur, lit la réponse, bruteforce la clef en live, sur le texte clair connu que nous avons, puis envoie le mot de passe connu avec la clef détectée, et lit la réponse.

password = "Ple4s3_L37_m3_1n"

witness = "Enter password : "

attack = do
        (addr:_) <- getAddrInfo Nothing (Just "10.13.37.101") (Just "21345")
        sock <- socket (addrFamily addr) Stream defaultProtocol
        connect sock (addrAddress addr)
        hello <- recv sock (length witness)
        case [ k | k <- allKeys, decrypt k hello == witness ] of
                [key] -> do
                        print $ "Key found : " ++ show key
                        sendAll sock (encrypt key password)
                        flag <- recv sock 2048
                        print $ "Received: " ++ decrypt key flag
                _ -> print "Could not find key :("


On exécute le programme:
"Key found : (27, 6)"
"Received: INS{blablablaflag}"

Merci à Delraich pour la relecture et les corrections !