Skip to content

Binary shrink


Description

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

Enoncé

After hearing about young computer problems, you have decided to become a computer shrink. Your first patient is a robot elf. "A little machine dream I keep having, " she says. "But when it is over, I always forget the end. I've captured the dream's program, but I don't dare look". Can you run the program for her? Are you able to figure out what's in her memory right before execution stops?

Pièce(s) jointe(s): - binary_shrink.zip


Solution détaillée

Reconnaissance

Ce défi est une suite du défi Embryobot. Nous commençons l'analyse en neutralisant certaines parties qui ne peuvent normalement pas être modifiées dans les fichiers ELF et en désassemblant le code.

L'hexdump est assez petit et nous commençons par examiner où le point d'entrée pointe (l'offset 18h pointe vers 8048009h). Si nous retirons la partie de l'adresse de base, nous pouvons voir que le programme saute à l'adresse 09h dans notre image.

00000000  7F 45 4C 46  02 01 01 00   00 E8 2C 00  00 00 13 37                                           .ELF......,....7
00000010  02 00 3E 00  01 00 00 00   09 80 04 08  00 00 00 00                                           ..>.............
00000020  40 00 00 00  00 00 00 00   00 00 00 00  00 00 00 00                                           @...............
00000030  00 00 00 00  40 00 38 00   01 00 5A 48  89 D0 EB 38                                           ....@.8...ZH...8
00000040  01 00 00 00  03 00 00 00   00 00 00 00  00 00 00 00                                           ................
00000050  00 80 04 08  00 00 00 00   00 80 04 08  00 00 00 00                                           ................
00000060  01 00 00 00  00 00 00 00   01 00 00 00  00 00 00 00                                           ................
00000070  00 10 00 00  00 00 00 00   48 81 C2 91  00 00 00 48                                           ........H......H
00000080  83 E8 0E 48  89 C6 31 C9   B1 56 48 89  F0 40 8A 30                                           ...H..1..VH..@.0
00000090  40 30 32 48  83 32 42 48   FF C2 48 FF  C0 E2 EE D6                                           @02H.2BH..H.....
000000A0  26 6C 76 23  28 38 76 2C   F5 0B 0E 04  1D 19 10 74                                           &lv#(8v,.......t
000000B0  26 23 04 36  0E 0E 1D 7B   84 19 07 76  06 0C 07 51                                           &#.6...{...v...Q
000000C0  11 3F C2 78  42 37 83 1A   03 FA 7C 78  6B 48 03 12                                           .?.xB7....|xkH..
000000D0  0A BD 85 4A  CB 9C 0A 72   90 AA 02 C4  97 E1 4B 83                                           ...J...r......K.
000000E0  0A BD 82 D1  8F C2                                                                            ......

Si nous désassemblons le binaire (cette fois-ci, le fichier nous indique qu'il s'agit d'un binaire 64 bits), le désassembleur ne s'aligne pas avec notre offset de saut 09h, nous devons donc commencer notre exploitation en remplaçant certains octets juste avant cet offset par des instructions NOP.

Exploitation

Nous utilisons objdump pour désassembler le fichier avec l'architecture appropriée :

$ objdump -D -Mintel,x86-64 -b binary -m i386 binary_shrink

0:   90                      nop
1:   90                      nop
2:   90                      nop
3:   90                      nop
4:   90                      nop
5:   90                      nop
6:   90                      nop
7:   90                      nop
8:   90                      nop
9:   e8 2c 00 00 00          call   0x3a
e:   13 37                   adc    esi,DWORD PTR [rdi]

Le programme appelle l'offset 3ah, donc nous continuons en remplaçant quelques octets par des instructions NOP.

39:   90                      nop             ; on nop ce byte
3a:   5a                      pop    rdx      ; pops addr de retour (804800eh) depuis la stack
3b:   48 89 d0                mov    rax,rdx  ; assigne addr retour à rax
3e:   eb 38                   jmp    0x78     ; jump à offset 78h

Ce snippet est utilisé pour obtenir une adresse valide avec la base d'image dans rdx et rax, puis saute à un autre offset. Cette fois, les instructions sont déjà alignées, donc le désassembleur nous donne la sortie correct.

78:   48 81 c2 91 00 00 00    add    rdx,0x91               ; ajoute 91h à rdx, rdx pointe maintenant à 9fh
7f:   48 83 e8 0e             sub    rax,0xe                ; soustrait 0eh de rax, rax pointe maintenant sur la base addr
83:   48 89 c6                mov    rsi,rax
86:   31 c9                   xor    ecx,ecx                ; ecx = 0
88:   b1 56                   mov    cl,0x56                ; set low byte de ecx, ecx = 56h
8a:   48 89 f0                mov    rax,rsi
8d:   40 8a 30                mov    sil,BYTE PTR [rax]     ; charge l'octet à partir du décalage de rax
90:   40 30 32                xor    BYTE PTR [rdx],sil     ; xor byte au décalage de rdx avec l'octet chargé
93:   48 83 32 42             xor    QWORD PTR [rdx],0x42   ; xor byte au décalage de rdx à 42h
97:   48 ff c2                inc    rdx                    ; increase rdx et rax
9a:   48 ff c0                inc    rax
9d:   e2 ee                   loop   0x8d                   ; reboucle sur 8dh si ecx n'est pas nul
9f:   d6                      (bad)           ; bad code

On s'aperçoit que cela ressemble à une boucle faisant une sorte de chiffrement XOR. En tout, 86 octets sont réécrits tandis que la cible (rdx) commence à l'offset 9fh et la source (rax) commence à l'adresse de base (où se trouve notre en-tête ELF). Le programme boucle sur 86 octets en les xorant avec des octets chargés et en xorant également l'octet cible avec 42h. Ce qui nous explique pourquoi les instructions suivantes n'ont pas été désassemblées correctement.

On peut alors développer un script qui nous permettra de décrypter le code :

data = bytearray(open("binary_shrink", "rb").read())

# add some bytes as the loop overflows our image quite a bit
for i in range(0,15):
    data.append(0)

# this is more or less 1:1 taken from our disassembly
rdx = 0xe
rax = rdx
rdx += 0x91
rax -= 0xe
rsi = rax
ecx = 0
cl = 0x56
rax = rsi

# encrypt the instructions
while True:
    sil = data[rax]
    data[rdx] ^= sil
    data[rdx] ^= 0x42
    rdx += 1
    rax += 1
    cl -= 1

    if cl == 0:
        break

open("binary_shrink_out", "wb").write(data)

Nous obtenons maintenant la séquence suivante :

78:   48 81 c2 91 00 00 00    add    rdx,0x91
7f:   48 83 e8 0e             sub    rax,0xe
83:   48 89 c6                mov    rsi,rax
86:   31 c9                   xor    ecx,ecx
88:   b1 56                   mov    cl,0x56
8a:   48 89 f0                mov    rax,rsi
8d:   40 8a 30                mov    sil,BYTE PTR [rax]
90:   40 30 32                xor    BYTE PTR [rdx],sil
93:   48 83 32 42             xor    QWORD PTR [rdx],0x42
97:   48 ff c2                inc    rdx
9a:   48 ff c0                inc    rax
9d:   e2 ee                   loop   0x8d                   ; la boucle comme auparavant
9f:   eb 21                   jmp    0xc2                   ; jump à c2h

...

c2:   80 3a 00                cmp    BYTE PTR [rdx],0x0     ; si [rdx] != 0
c5:   75 c1                   jne    0x88                   ; alors jump à 88h
c7:   58                      pop    rax                    ; sinon, pops '1' depuis la stack
c8:   41 b8 3e 3a 29 0a       mov    r8d,0xa293a3e          ; string pour '>:)\n'
ce:   41 50                   push   r8
d0:   48 ff c7                inc    rdi
d3:   48 89 e6                mov    rsi,rsp
d6:   48 31 d2                xor    rdx,rdx
d9:   b2 08                   mov    dl,0x8
db:   0f 05                   syscall

; exit program
dd:   48 31 c0                xor    rax,rax                ; rax = 0
e0:   48 ff c0                inc    rax                    ; rax = 1
e3:   90                      nop
e4:   cd 80                   int    0x80                   ; syscall exit

Les instructions suivantes configurent un autre appel système à 1 (écriture), une chaîne de longueur 8 (rdx = 8) est écrite sur stdout (rdi = 1), la chaîne est poussée sur la pile (rsi=rsp). La chaîne de sortie est un smiley, puis le programme se termine. La description indique que nous devrions vérifier la mémoire à la sortie du programme, donc utilisons radare2 et définissons un point d'arrêt quelque part avant que le programme ne se termine.

$ r2 -d binary_shrink
[0x08048009]> db 0x080480d9
[0x08048009]> dc
[+] SIGNAL 4 errno=0 addr=0x080480d9 code=2 si_pid=134512857 ret=0
[+] signal 4 aka SIGILL received 0 (Illegal instruction)
V!
Press `m`
View -> Hexdump

Ce qui nous mène facilement au flag :

FLAG : brck{4n_eLF_...S}