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:
0000000000400f7eUne routine particulièrement peu optimisée pour le code C suivant:: 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
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 statsOn 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 bestOn 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:
Enregistrer un commentaire