samedi 22 mars 2014

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 !

Aucun commentaire:

 
Also check me out on Mastodon