Où comment décrypter le payload d’un packer qui XOR et qui Shifte (ou pas) sur du code.
Récemment je me suis fait humilier sur un crackme. Le concept était simple, une bonne partie du code était packé avec un XOR shifté , et la clef d’encryption étant bien sur le mot de passe du crackme. La fonction de décryptage était du genre (Au format AT&T, oui moi aussi j’aime pas !):
A l’appel de la fonction :
Dans EAX la taille de ce que je doit décrypter.
Dans ESI l’offset de la stringZ contenant le Password.
Dans EBX l’offset des data à décrypter
mov %esi,%ecx |
; offset du pwd dans ECX aussi |
|
LOOP1: |
cmp $0x0,%eax |
; test a t'on finis de décrypter ? |
je FINI |
; Si oui fini...on rentre |
|
cmpb $0x0,(%ecx) |
; Sinon est t'on au bout du password |
|
jne LOOP2 |
||
mov %esi,%ecx |
; Si on est au bout du password on se remet au début |
|
LOOP2: |
mov (%ecx),%dl |
|
xor %dl,(%ebx) |
; Decryptage via Xor data[j]=data[j] ⊕ password[i] |
|
addb $0xAB,(%ebx) |
; le Shift, data[j]=data[j] + 0xAB |
|
dec %eax |
; Ce qu'il reste a faire - 1 |
|
inc %ebx |
; j++ pour data[j] |
|
inc %ecx |
; i++ pour password[i] |
|
jmp LOOP1 |
||
FINI: |
ret |
Normalement, sur du code on cherche des aplats de 00 à décrypter, généralement c’est des réservations de buffers, il n’y a qu’a faire le shift, car un XOR avec un 0 ca donne la valeur avant le XOR, ici donc il suffisait ajouter 0xab à tous les octets et regarder ce qui est printable.
Mais là rien de rien ne sautait au visage. Je l’ai labouré, labouré et en fait la solution est ridicule. La solution est tellement simple quand on connait le truc que je ne peux pas vous laisser l’ignorer. allons y…
Tous code compilé en C, C++ ou autre language génère des champs de NOP entre les fonctions. C’est fait pour aligner les instructions en mémoire pour d’obscures raisons de performances. L’espace inutile est donc bourré d’instructions NOP (Pour les plus distraits NOP est une Instruction qui ne fait rien et qui prend un byte). Mais cela veut dire aussi qu’on se retrouve avec des gros aplat de NOP Note, l’instruction Nop fait une taille de 1 Byte et c’est 0x90 en hexa.
Allez un petit TP pour comprendre, Prenons un programme C très élaboré.
#include <stdio.h>
int main () {
printf("Hello World\n");
}
On le compile, et on ne regarde que la partie “code executable” du fichier ELF obtenu.
cyanide:/tmp$ gcc hello.c -o hello ; strip hello
cyanide:/tmp$ readelf hello -S | grep X
[12] .init PROGBITS 08048298 000298 000030 00 AX 0 0 4
[13] .plt PROGBITS 080482c8 0002c8 000040 04 AX 0 0 4
[14] .text PROGBITS 08048310 000310 00016c 00 AX 0 0 16
[15] .fini PROGBITS 0804847c 00047c 00001c 00 AX 0 0 4
W (write), A (alloc), X (execute), M (merge), S (strings)
Extractons ensuite juste les segments exécutables de ce formidable programme (Ça tombe bien ils sont contigus).
0x298 = 664 en décimal
0x30 + 0x40 + 0x16c + 0x1c0 = 0x39c donc 924 en décimal
cyanide:/tmp$ dd if=hello of=helloX bs=1 skip=664 count=924
924+0 records in
924+0 records out
924 bytes (924 B) copied, 0.00771259 s, 120 kB/s
Et cherchons y un champs de “NOP NOP”
cyanide:/tmp$ hexdump helloX -C | grep "90 90"
00000090 c4 83 04 08 e8 b7 ff ff ff f4 90 90 90 90 90 90 |................|
000000a0 90 90 90 90 90 90 90 90 55 89 e5 53 83 ec 04 80 |........U..S....|
00000140 ff c9 c3 90 90 90 90 90 55 89 e5 5d c3 8d 74 26 |........U..]..t&|
000001b0 5d c3 8b 1c 24 c3 90 90 55 89 e5 53 83 ec 04 a1 |]...$...U..S....|
000001e0 5d c3 90 90 55 89 e5 53 83 ec 04 e8 00 00 00 00 |]...U..S........|
Regardons le code maintenant
gdb$ x/30i 0x8048310
0x8048310 : xor %ebp,%ebp
0x8048312 : pop %esi
0x8048313 : mov %esp,%ecx
0x8048315 : and $0xfffffff0,%esp
0x8048318 : push %eax
0x8048319 : push %esp
0x804831a : push %edx
0x804831b : push $0x80483e0
0x8048320 : push $0x80483f0
0x8048325 : push %ecx
0x8048326 : push %esi
0x8048327 : push $0x80483c4
0x804832c : call 0x80482e8
0x8048331 : hlt
0x8048332 : nop
0x8048333 : nop
0x8048334 : nop
0x8048335 : nop
0x8048336 : nop
0x8048337 : nop
0x8048338 : nop
0x8048339 : nop
0x804833a : nop
0x804833b : nop
0x804833c : nop
0x804833d : nop
0x804833e : nop
0x804833f : nop
0x8048340 : push %ebp
0x8048341 : mov %esp,%ebp
On vois très bien le champ de 14 NOP d’affilés qui bourre la fonction _start jusqu’à la fonction destructor.
Idem derrière le code principal de notre très élaboré programme.
gdb$ x/25i 0x080483c4
0x80483c4 : push ebp
0x80483c5 : mov ebp,esp
0x80483c7 : and esp,0xfffffff0
0x80483ca : sub esp,0x10
0x80483cd : mov DWORD PTR [esp],0x80484a0
0x80483d4 : call 0x80482f8 0x80483d9 : leave
0x80483da : ret
0x80483db: nop
0x80483dc: nop
0x80483dd: nop
0x80483de: nop
0x80483df: nop
0x80483e0 : push ebp
0x80483e1 : mov ebp,esp
Rien que ce tout petit programme dispose de champs de NOP, Et l’histoire se répète chez tous le monde, cherchons du “NOP NOP” dans un hexdump de /bin/bash
cyanide:/$ hexdump /bin/bash -C | grep "90 90" | wc -l
241
Il y en a partout..241 champs, et du gros, là aussi des exemples 14 NOP d’affilés
cyanide:/$ hexdump /bin/bash -C | grep "90 90" | tail -n 10
000a4ad0 ff c9 c3 90 90 90 90 90 90 90 90 90 90 90 90 90 |................|
000a5840 89 45 c8 e9 08 fd ff ff 90 90 90 90 90 90 90 90 |.E..............|
000a5b40 c7 04 24 7d 00 00 00 e8 c4 c1 fe ff c9 c3 90 90 |..$}............|
000a6d90 ff ff 90 90 90 90 90 90 90 90 90 90 90 90 90 90 |................|
000a6f10 ff ff 90 90 90 90 90 90 90 90 90 90 90 90 90 90 |................|
000a70e0 39 d7 75 bf 2b 45 cc 1b 55 c8 eb b7 90 90 90 90 |9.u.+E..U.......|
000a7210 fa 83 c4 10 5e 5f 5d c3 90 90 90 90 90 90 90 90 |....^_].........|
000a7350 e9 1d ff ff ff 90 90 90 90 90 90 90 90 90 90 90 |................|
000a73c0 72 de 83 c4 0c 5b 5e 5f 5d c3 8b 1c 24 c3 90 90 |r....[^_]...$...|
000a73f0 f8 ff 75 f4 83 c4 04 5b 5d c3 90 90 55 89 e5 53 |..u....[]...U..S|
Et même sur de l’executable Windows cela est dispo. Par contre, je n’ai pas trouvé cette joyeusetée sur de l’executable osX… Etrange (Ou j’ai mal cherché).
Cela rend du coup l’exploitation plus simple. ⊕ c’est Xor hein…
Si la décryption d’un NOP est
(KEY⊕DATA)+0xAB = NOP
alors..
KEY = DATA⊕(NOP-0xAB)
Prenons un exemple concret pour les moins à l’aise sur un byte. Si j’ai crypté un NOP (0x90) avec la clef “A” (0x41) et que j’ai shifté préalablement mon NOP en soustrayant 0xab, ma data cryptée est 0xa4
Note, le “% 256” c’est juste pour dire a python de rester travailler sur du 8 bits et “^” c’est un xor.
L’encryption qui a été réalisée est :
>>> hex((((0x90 - 0xab ) % 256 ) ^ 0x41 ) %256)
'0xa4'
La décryption dans le crackme sera :
>>> hex((((0xa4 ^ 0x41 ) % 256 ) + 0xab ) %256)
'0x90'
Et donc si on applique ce que l’on a dit pour trouver la clef
>>> hex((((0x90 - 0xab ) % 256 ) ^ 0xa4 ) %256)
'0x41'
Moralité, c’est un crackme, on va donc xorer comme un boeuf les data cryptées avec (0x90 – 0xab), soit 0xe5 et on verra apparaitre en chaines ascii visible notre mot de passe. On ne gardera que les chaine de plus de 8 charactères.
On y vas…
cyanide:/tmp$ ./xor8.py dataxored.raw 0xe5 | strings -n 8 | sort | uniq -c | sort -n
…
4 @SsW0rDP@SsW0rD
5 +?@SsW0rD
5 +?@SsW0rDP@SsW0rD
5 80rDP@SsW0rD
5 P@SsW0rD
6 +?/SsW0rD
6 SsW0rDP@SsW0rD
7 0rDP@SsW0rD
101 sW0rDP@SsW0rD
Il ne faut pas sortir de Saint Cyr pour en déduire le bon mot de passe, de plus c’est un bête XOR il existe aussi des trucs pour déterminer la taille de la clef, mais cela fera parti d’un autre post.
Voila ce qui s’apparente en cryptanalyse à l’attaque sur le clair connu (mais bon… parler de cryptanalyse sur un XOR c’est quand même fortement pompeux)
Donc la prochaines fois que vous croiserez un cryptage par XOR sur du code dans un crackme ou un packer, tentez la décryption sur des zones de 00 ou des zones de 0x90 avant de vous fatiguer à debugger pour chercher où est caché la clef d’encryption..
A+
Merci pour ce post l’ami.
Bien utile et surtout bien expliqué !!