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 !