Chiffrement des mots de passe Netscreen (2/3) - Désassemblage de la ROM

Thu 03 January 2008 by JB

Le boîtier NetScreen contient un processeur Intel IXP425 cadencé à 400MHz. C'est un processeur utilisé pour l'embarqué, et il est optimisé pour les applications réseau. Il est équipé notamment d'un co-processeur accélérant les procédures de chiffrement. C'est un processeur XScale (architecture ARM), reconnu par IDA. Le système d'exploitation est un système propriétaire, ScreenOS, développé par Juniper. Il est stocké sur une mémoire Flash de 32Mo. Il n'y a pas de documentation technique disponible, la seule source d'information étant le manuel d'utilisation.

Récupération du système d'exploitation

Le boîtier peut être mis à jour par l'interface Web de NetScreen. Un firmware est uploadé, et le boîtier commence sa mise à jour. Plutôt que de dumper directement le contenu de la mémoire Flash, nous avons préféré travailler sur ces programmes de mise à jour.

upload.png Fig. 1 - Interface de mise à jour

Le fichier de mise à jour vers la version 5.2 du système s'appelle ns5gt.5.2.0r2 et fait environ 7Mo. Il n'a pas un en-tête connu. On peut supposer qu'il s'agit d'un fichier exécutable, et que son format n'est pas très évolué. On extrait les chaînes de caractère qu'il contient pour avoir une première idée de son fonctionnement.

$ strings ns5gt-5.4.0r3a.0 > firmware-strings.txt

Le programme contient seulement une quarantaine de chaînes intéressantes, toutes en début de fichier. On déduit assez rapidement qu'il s'agit d'un stub de décompression gzip. Parmi les chaînes on trouve notamment :

bad pack level
insufficient memory
%s: %s: unknown method %d -- get newer version of gzip
%s: %s is encrypted -- get newer version of gzip
%s: %s is a a multi-part gzip file -- get newer version of gzip
%s: %s has flags 0x%x -- get newer version of gzip

Le programme contient donc certainement une archive au format gzip, correspondant au système d'exploitation, qui sera décompressée en mémoire puis écrite sur la mémoire Flash. D'après les messages, on suppose que cette archive n'est pas chiffrée.

firmware-format.png Fig. 2 - Sections dégagées du firmware

On ouvre le programme dans un éditeur hexadécimal. Plusieurs zones peuvent être dégagées immédiatement, séparées par de larges zones remplies de 0. Notre hypothèse est a priori bonne : la fin du fichier ressemble fortement à une archive gzip. Les premiers octets de la dernière zone sont :

00019400  54 E9 08 08 05 9D CA 45 02 03 6E 73 35 67 74 2E   T.........ns5gt.
00019410  64 00 CC 5A 7D 70 5C D5 75 BF 77 F7 49 5A C9 CF   d..Z}p\\.u.w.IZ..
00019420  D2 D3 4A 36 FE 10 EC 13 B6 1B 85 B1 CC B3 2C A8   ..J6..........,.
00019430  CA 48 B0 18 D2 3A 63 07 D6 7C 15 5A 17 04 38 C4   .H...:c..|.Z..8.

Après avoir extrait l'archive du firmware, on essaie de la décompresser avec gzip. Mais ça ne marche pas.

$ gzip -d firmware-extracted.gzgzip: firmware-extracted.gz: not in gzip format

Les deux premiers octets de l'archive ont été modifiés. Ils ont une valeur fixe et servent de marqueur pour vérifier le format de l'archive. En regardant les sources de gzip on repère ces deux octets fixes :

#define   GZIP_MAGIC     "\037\213" /* Magic header for gzip files, 1F 8B */

On remplace les deux premiers octets \x54\xE9 par \x1F\xB8. Cette fois l'archive est bien décompressée. Elle contient un seul fichier ns5gt.d.

Format des fichiers exécutables

Le fichier est désassemblé directement avec IDA. On a supposé que le système était mappé en mémoire à l'adresse 0. Les portions de code sont très bien désassemblées, même si une très grande partie du code n'est pas explorée.

En revanche, les pointeurs vers les zones de données ont des valeurs incohérentes : ils pointent parfois vers rien, parfois au milieu de chaînes de caractères...

ROM:00009BA8                 LDR     R0, =(aWriteCertConte+0xC)
...                                           ; pointe en milieu de chaîne
ROM:00B25A2C aWriteCertConte DCB "write CERT contents bad length, got<%d>.",0xA,0
ROM:00B25A2C                                         ; DATA XREF: ROM:off_9BF8o

Bref, il y a un problème. Le système n'est certainement pas mappé à l'adresse 0. Étudions l'en-tête du fichier ns5gt.d et comparons-le au programme de mise à jour du système :

Fichier de mise à jour (Taille : 7 241 763 octets)

00000000  EE 16 BA 81 00 12 0C 12 00 00 00 20 02 80 00 00   ..º........ ....
00000010  00 6E 7F D4 10 EC 00 50 29 00 80 00 8C 00 D7 66   .n.Ô.ì.P)......f
00000020  EB 00 00 FE E1 A0 00 00 00 00 00 00 00 00 00 00   ë...á ..........

Fichier ns5gt.d (Taille : 18 212 916 octets)

00000000  EE 16 BA 81 00 01 01 10 00 00 00 20 00 08 00 00   ..º........ ....
00000010  01 15 E8 14 00 00 00 00 00 00 00 00 E5 90 9D EF   ..è.........å...
00000020  E5 9F D0 4C E2 8D DD 0F E3 A0 30 00 E5 8D 30 00   å..Lâ...ã 0.å.0.

Quelques déductions sont rapidement faisables :

  • les 4 premiers octets sont identiques. Il s'agit d'une constante servant à vérifier le format du fichier.
  • les octets 9 à 12 sont identiques (0x20). Mieux : si on ajoute cette valeur aux octets 17 à 20 pour les deux fichiers, on obtient la taille totale du fichier. Dans le cas du fichier de mise à jour il y a une légère différence, mais si on supprime ces octets supplémentaires de l'archive elle est toujours valide. C'est certainement une signature, empêchant d'uploader un fichier non signé ou modifié sur le boîtier.
  • entre ces deux valeurs, du 13au 16octet, se trouve certainement l'adresse virtuelle à laquelle charger le contenu du programme.

On peut penser à un format de fichier assez simple : un en-tête de 32 octets, composé de :

  • MagicValue, valeur fixe de 32 bits (0xEE16BA81) ;
  • Options, 4 octets d'options ;
  • SizeOfHeader la taille de l'en-tête ;
  • VirtualAddress, adresse à partir de laquelle charger le contenu du programme dans la mémoire virtuelle ;
  • SizeOfRawData, taille du programme ;
  • des octets inconnus ;
  • le contenu du programme, à partir de l'octet SizeOfHeader.

On recharge le programme dans IDA, mais cette fois à l'adresse VirtualAddress à partir de l'octet SizeOfHeader. Cette fois les pointeurs sont corrects. L'exemple précédent dans le listing donne ici :

ROM:00089B88                 LDR     R0, =aErrorInMutex_2
...
ROM:00089BD8 off_89BD8       DCD aErrorInMutex_2     ; DATA XREF: sub_89AE0+A8r
ROM:00089BD8                                         ; "error in mutex_unlock(), not locked."

Forcer le désassemblage

Comme dit plus haut, le code n'est pas entièrement exploré directement par IDA. Loin de là : moins de 1% des fonctions sont effectivement analysées. On peut forcer l'analyse manuellement, mais c'est extrêmement fastidieux. Il est préférable de créer un script forçant cette analyse.

Le début et la fin des procédures sont toujours les mêmes : la pile et d'autres registres sont sauvegardés au début, puis restaurés en fin de fonction.

ROM:00089AC8             sub_89AC8              ; CODE XREF: sub_24E4C0+38p
ROM:00089AC8ROM:00089AC8             oldR11          = -0xC
ROM:00089AC8             oldSP           = -8
ROM:00089AC8             oldLR           = -4
ROM:00089AC8
ROM:00089AC8 E1 A0 C0 0D  MOV     R12, SP
ROM:00089ACC E9 2D D8 00  STMFD   SP!, {R11,R12,LR,PC} ; sauvegarde des registres                                                       ; dans la pile
ROM:00089AD0 E2 4C B0 04  SUB     R11, R12, #4
...
ROM:00089ADC E9 1B A8 00  LDMDB   R11, {R11,SP,PC} ; restauration
ROM:00089ADC             ; End of function sub_89AC8

Les 4 octets correspondant à un MOV R12, SP sont donc un bon marqueur pour détecter le début de chaque procédure. On peut donc parcourir le programme à la recherche de ces marqueurs et créer une fonction chaque fois qu'on en rencontre un.

Dans le cas où la procédure n'a pas besoin d'utiliser la pile, la procédure commence directement et se termine par un RET. Dans ce cas on ne pourra pas la détecter avec notre script mais IDA analysera ces fonctions si elles sont appelées par une fonction déjà analysée.

Le script forçant l'analyse est très simple. Voici son code :

auto ea;
ea = ScreenEA();
ea = FindBinary(ea, SEARCH_DOWN, "E1 A0 C0 0D");
if(GetFunctionName(ea) == "")
{
    MakeFunction(ea, BADADDR);
    Message("New function: %x\n", ea);
}
ea = ea + 4;
while ((ea = FindBinary(ea, SEARCH_DOWN, "E1 A0 C0 0D")) != -1)
{
    if(GetFunctionName(ea) == "")
    {
        MakeFunction(ea, BADADDR);
        Message("New function: %x\n", ea);
    }
    ea = ea + 4;
}

Après le lancement du script, la quasi-totalité du programme est correctement désassemblée. On peut maintenant se lancer dans la recherche des fonctions qui nous intéressent.