Este README.md es una traducción del blog de David. David encontró los CVE's 1015 y 1016 en el kernel de Linux. Puedes visitar su página web para leer el documento original.
Aquí te dejo sus redes sociales:
Publicado el 2 de abril del 2022.
- CVE-2022-1015 permite realizar un acceso out-of-bounds (fuera del límites) causado por escasas validaciones de argumentos de entrada, puede derivar en la ejecución de código remoto y a una escalación de privilegios local.
- CVE-2022-1016 está relacionado a una pobre de inicialización de las variables alojadas en el stack, lo que puede ser usado para filtrar una larga variedad de datos del kernel al espacio del usuario (userspace).
Estos problemas deberían ser explotabes en las configuraciones por defecto de la versión más nueva de Ubuntu y de RHEL. Escribí mi prueba de concepto (PoC) del CVE-2022-1015 tomando como objetivo la versión del kernel 5.16-rc3 de Arch Linux.
Este documento está dirigido a las personas que tengan un conocimiento básico del kernel de Linux en términos de funcionalidad y seguridad. Traté de hacer que este documento sea amigable con las personas que carezcan de conocimientos con el stack de redes para hacerlo accesible a todo público.
Aquí está una guía de lectura:
- Si estás aquí simplemente para leer acerca de la vulnerabilidad, empiezan en la Sección 4
- Si también quieres un poco de contexto acerca del subsistema del kernel, empieza con la Sección 2
- Si estás interesado en un poco más de contexto adicional, lee todo el documento
A mediados de febrero, el programa de seguridad de Google anunció que continuarían su programa de recompensas kCTF, ofreciendo recompensas que llegan desde los $31,337 hasta 91,337 dólares por un exploit en el kernel de Linux que pueda escalar privilegios al usuario root desde procesos sin privilegios en un sandbox de nsjail.
Siendo un pobre estudiante, obviamente esto captó mi atención. Esta era mi primera vez buscando buscando una vulnerabilidad del "mundo real", pero en mis aventuras jugando CTF con mi equipo, me he familiarizado con el kernel de Linux en términos de seguridad. Después de horas y horas con muy poco cercano a nada de progreso (pero con mayor conocimiento acerca de Linux) logré encontrar algunas vulnerabilidades en el módulo de nf_tables
.
Tristemente, al final del día, me di cuenta de que este módulo no estaba presente en las reglas del kCTF de Google (por lo que no conseguí ninguna recompensa por estas dos vulnerabilidades). Pero obviamente, aún y así las reporté y escribí un exploit LPE (Escalado de Privilegios Local) para el CVE-2022-1015.
Bien, así que has decidido que vas a encontrar algunas vulnerabilidades en Linux. ¿Ahora qué? Linux es un proyecto gigantezco, y es bastante fácil no poder ver el bosque por los árboles (te enfocas tanto en los detalles que pierdes visión de lo que es realmente importante, no tienes una vista general de la situación). Para empeorar las cosas, muchas partes no está documentadas y necesitas leer un montón de código para poder entender lo que está pasando.
Yo comencé intentando tener una perspectiva detallada del modelo de seguridad de Linux. Encontrar un bug es una cosa; pero encontrar un buen bug es otra muy distinta. Después de todo, no todos los bugs están creados igual:
- Si un bug requiere privilegios root, no existe un límite de seguridad significativo (a menos de que el kernel module signing esté activado)
- Algunas cosas que se me vienen a la mente son muchos de los módulos de los sistemas de ficheros (virtuales). Solo el usuario root inicial puede montar estos sistemas de ficheros. La excepción recae en vfe que específica
FS_USERNS_MOUNT
, en cuyo caso puedes montarlos en el user namespace.
- Algunas cosas que se me vienen a la mente son muchos de los módulos de los sistemas de ficheros (virtuales). Solo el usuario root inicial puede montar estos sistemas de ficheros. La excepción recae en vfe que específica
- Si un no se puede acceder a un bug a través de las llamadas al sistema, probablemente no podrá ser explotable.
- Esto aplica a muchos de los drivers de hardware, ya que no tienes acceso físico a la máquina. Los drivers de red de bajo nivel todavía podrían ser un buen objetivo si es que puedes p. ej. enviar datos a través de bluetooth o 802.11.ac.
- Obviamente esto depende del escenario en el que te encuentres.
- Muchos bugs requieren
CAP_SYS_ADMIN
oCAP_NET_ADMIN
.- Los user namespaces (espacio de nombre) están activados por defecto así que esto no es un problema.
- De lo contrario primero tendrás que hacer un escalado de privilegios al namespace (espacio de nombre) del usuario root dentro de un contenedor.
- No todos los módulos estarán presentes en tu objetivo.
- Linux es un pedazo de software excepcional altamente configurable, por lo que todas las configuraciones pueden variar de una gran multitud de formas.
- La configuración del kernel usualmente se puede acceder desde
/proc/config.gz
. Los módulos pueden ser cargados en (=m) o compilados por separado y cargados en tiempo de ejecución (=y). - Puedes usar
/proc/modules
y/proc/kallsyms
, pero siempre son confiables, ya que los módulos pueden ser cargados dinámicamente en el kernel (p. ej.request_module
). - Si no estás seguro, escribe un pequeño programa que intente interactuar con el módulo.
Estas restricciones nos ayudan a saber los límites de los sistemas de archivos en los cuáles podemos buscar vulnerabilidades. Creo que es una buena idea tomarte tu tiempo tratando de planear tu ataque al objetivo que desees.
Ya he aprendido mi lección acerca del punto anterior. Como mencioné, el módulo nf_tables
no estaba cargado en la instancia que nos presentó kCTF. Pude haberme dado cuenta de esto desde un principio y ahorrarme la decepción :p. Por otra parte, probablemente no estarías leyendo este blog ahora mismo si me hubiese dado cuenta antes, supongo que las cosas salieron bien después de todo.
Una explicación por la cuál el COS, Google's container-optimazed Linux fork, no tuviera nf_tables
puede ser encontrada aquí y aquí.
Después de evaluar los puntos anteriormente mencionados, decidí que mi mejor ruta para comenzar probablemente sería mirar el código fuente de la red. Muchas de las funcionalidades interesantes allí necesitan CAP_NET_ADMIN
, pero como lo mencioné, esto en realidad no es un problema. Por el contrario, sospecho que los componentes que requieren capacidades especiales son por lo general menos seguros, ya que los desarrolladores del kernel pueden tener una falsa sensación de seguridad.
También hice el esfuerzo para escoger el sistema de ficheros del cuál quería conocer más; de esta forma, incluso si no encuentras ningún bug aún así podrás aprender un montón de cosas interesantes.
Investigué muchos sistemas de ficheros de red, pero no encontré nada importante. Después de navergar el subdirectorio net/
, y me encontré con el módulo nf_tables
. Parecía un poco complejo, así que decidí tomarme un tiempo para conocer acerca del mismo.
Netfilter (net/netfilter
) es un subsistema de ficheros de red bastante grande en el kernel. En resumen, netfilter coloca hooks a través de los módulos de red que otros módulos pueden registrar manejadores. Cuando se alcanza un hook, el control es delegado a esos manejadores, y pueden operar con su respectiva estructura de paquetes de red. Los manjeadores pueden aceptar, soltar y modificar paquetes.
Después de algunas horas de navegar el API de nf_tables
(net/netfilter/nf_tables_api.c
) para empezar a saber cómo funciona de una manera exacta, decidí echar un vistazo a la validación lógica a los registros que el usuario manda, y encontré algunos comportamientos sospechosos. Después de pensar si me estaba volviendo loco o no, escribí un pequño PoC (prueba de concepto) para intentar activar la vulnerabilidad que encontré: una vulnerabilidad conocida como OOB o fuera de los límites, que permite leer y escribir en la memoria stack.
Después de encontrar una manera para filtrar las direcciones del kernel, tomar control del puntero de memoria fue bastante fácil. Después de un poco de ROP (programación orientada al retorno), y la shell con privilegios root se convirtió en una realidad.
Cada vez que la rutina init
de una expresión necesita parsear un registro de un mensaje de usuario de netlink, la nft_parse_register_load
o nft_parse_register_store
rutina es llamada dependiendo de si es un registro fuente o un registro de destino. Añandí algunos comentarios:
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
-----
static unsigned int nft_parse_register(const struct nlattr *attr)
{
/* Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
-----
static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
/* We can never read from the verdict register,
* so bail out if the index is 0,1,2,3 */
if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
Las variantes *_store
son virtualmente idénticas, excepto que permiten escribir al verbdict bajo algunas condiciones.
Después de revisar la última validación, algo está realmente fuera de lugar aquí:
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
Esto parece ser un integer overflow, ¿no lo creen? Si podemos hacer que reg
contenga algún valor multiplicado multiplicado por 4 que genere un overflow cuando se le sume len
, podemos satisfacer las condiciones. En nft_parse_register_load
, el último byte valioso de reg
todavía está escrito al puntero u8 *sreg
, cayendo en nuestro nft_expr
que es usando posteriormente como un index.
*sreg = reg;
¿De verdad podemos? reg
es un enum nft_registers
en la validación de la rutina, de todas formas. Podemos pasar valores que tengan un rango entre 0x00000001
hasta 0xfffffffb
inclusive, el rango de nft_parse_register
; pero ¿será reg
un valor de 32 bits en nft_validate_register_load
? Se sabe que los compiladores podrían encoger los enum types si un tipo más pequeño puede representar todos los valores. Vamos a obtener una segunda opinión.
Obtenido del manual de GCC:
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
¿TL;DR? Depende del ABI y el posible grado de optimización. No pude encontrar ninguna evidencia concreta de si esta opción está activada por defecto en las builds de Linux.
Pero el ensamblador nunca miente. Vamos a echar un vistazo:
0000000000001b60 <nft_parse_register_load>:
1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5>
1b65: 55 push rbp
1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
1b69: 0f c8 bswap eax
1b6b: 89 c7 mov edi,eax
1b6d: 8d 48 fc lea ecx,[rax-0x4]
1b70: c1 e7 04 shl edi,0x4
1b73: 48 89 e5 mov rbp,rsp
1b76: c1 ef 02 shr edi,0x2
1b79: 83 f8 04 cmp eax,0x4
1b7c: 89 f8 mov eax,edi
1b7e: 0f 47 c1 cmova eax,ecx
1b81: 85 d2 test edx,edx
1b83: 74 13 je 1b98 <nft_parse_register_load+0x38>
1b85: 83 f8 03 cmp eax,0x3
1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38>
1b8a: 8d 14 82 lea edx,[rdx+rax*4]
1b8d: 83 fa 50 cmp edx,0x50
1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f>
1b92: 88 06 mov BYTE PTR [rsi],al
1b94: 5d pop rbp
1b95: 31 c0 xor eax,eax
1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea
1b9d: 5d pop rbp
1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde
1ba4: 5d pop rbp
1ba5: c3 ret
Las llamadas a funciones están alineadas bastante bien. Las operaciones importantes están en 1b8a
:
lea edx, [rdx+rax*4]
cmp edx, 0x50
ja 1b9f <nft_parse_register_load+0x3f>
mov BYTE PTR [rsi], al
rax
es el resultado de ntf_parse_register
, rdx
es len
proporcionada, y rsi
es el puntero sreg
. Ya nos hemos quitado de dudas.
nft_parse_register_store
muestra el mismo comportamiento. Mientras los registros vivan en el stack, nuestra vulnerabilidad OOB obviamente será relativa del stack. Esto es bueno, porque con un poco de suerte, podremos sobreescribir y retornar memoria directamente.
Para dar un ejemplo de una entrada vulnerable, un registro de 0xfffffffb
y una longitud de 0x20
, va a evaluar 0xfffffffb * 4 + 0x20 = 0x0c < 0x50
. Después de la validación (u8)0xfffffffb = 0xfb
será escrito a *sreg
.
Aunque hay un problema: ¿existen expresiones que nos permitan usar una longitud que pueda causar un overflow cuando se realice la suma? Después de un poco de investigación, encontré que nft_bitwise
y nft_payload
te permiten dar como entrada tu propia longitud, desde 0x00
hasta 0xff
. Muchas otras expresiones parecen tener longitudes estáticas que son muy pequeñas.
De momento esto se ve prometedor. El siguiente paso es tomar estos exploit primitives (capacidad génerica ganada durante un exploit) y usarlos.
Si podemos definar el tipo de poder que nos puede dar nuestro exploit, explotar esta vulnerabilidad debería ser más fácil. Así que, denme un poco de su paciencia porque vamos a ver un poco de aritmética.
Hay tres puntos que podemos usar para nuestro overflow para la multiplicación de registro, ya que esto es multiplicado por 4 = 2^2
: 2^32 - 1
, 2^31 - 1
y 2^30 - 1
(respectivamente 0xffffffff
, 0x7fffffff
, y 0x3fffffff
). Estos valores pueden ir decreciendo hasta que sumemos nuestra máxima longitud permitida, después de ser multiplicada por cuatro esto no resultará en un overflow. Otro punto a tomar en cuenta es que no podemos usar valores mayores a 0xfffffffb
, como se mencionó con anterioridad.
Dando una longitud específica, los valores byte menos significativos que pueden permitir un overflow usando esta longitud formarán nuestro intervalo de índices OOB que podemos usar.
Después de todo, no importa qué puntos de overflow sean usados. Toma por ejemplo los siguientes valores con un LSB (bit menos significativo) de 0xf0
:
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
De ahora en adelante, vamos a usar valores de registro cercanos a 0x7fffffff
.
Anteriormente hemos dado hablado de nft_payload
and nft_bitwise
. Algunas propiedades de estas expresiones son:
-
nft_payload
solo puede realizar escrituras OOB, mientras quenft_bitwise
puede realizar escrituras y lecturas OOB. -
nft_payload
puede hacer escrituras OOB hasta 0xff bytes de datos arbitrarios. -
nft_bitwise
realmente solo puede escribir hasta0x40
bytes de datos arbitrarios y puede leer solo0x40
bytes de datos que se encuentren en el stack del espacio del registrador .-
nft_bitwise
requiere unsreg
y undreg
, los cuáles necesitan pasar la validación con el mismo valor de longitud. -
Solo tenemos
0x40
bytes de espacio de registro, así que queremos o leer o escribir del registro de espacio, pero no podemos pasar la validación con una longitud mayor a0x40
.
-
Podemos usar un valor de longitud más grande para nft_bitwise
, pero significa que sreg
y dreg
necesitan estar fuera de los límites, lo cuál no sería muy útil para nuestros propósitos. Así que, por ahora trabajaremos con la longitud de 0x40
.
Teniendo todo esto en cuenta, ¿qué tipos de exploits podremos usar?
nft_bitwise
tiene una longitud máxima de 0x40
. Esto significa que el valor de registro multiplicado por cuatro debería ser al menos 0xffffffc0
. El valor más grande que podemos obtener multiplicando por cuatro es 0xfffffffb
, y ya que 0xfffffffb + 0x40 = 0x3b <= 0x50
esto pasará la validación.
0x7ffffff0 * 4 = 0xffffffc0
: el límite inferior es 0xf0
.
0x7fffffff * 4 = 0xfffffffb
: el límite superior es 0xff
.
Traduciendo a byte offsets:
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload
puede escribir fuera de límites a través de los offsets [0x304, 0x4ab]
desde struct nft_regs
.
Ahora que todo esto ya está aclarado, ¿qué es lo que en realidad está en el stack en estos offsets?
La rutina nft_do_chain
puede ser llamada a través de muchas rutas de código. Existen muchos factores que cambiarán la forma del stack antes del stack frame (marco de stack) de nft_do_chain
:
-
Ya sea que el chain hook sea un
input
ooutput
.- Si tenemos un chain hook configurado como
input
, el hook se activará en el contexto softirq del dispositivo de red respectivo con el stack softirq. - Si tenemos un chain hook configurado como
output
, el hook se activará en el contexto de syscall (llamada al sistema)send*
con el stack de syscall.
- Si tenemos un chain hook configurado como
-
El protocolo que estamos usando.
- Mandar un paquete IP bruto tendrá un call stack bastante diferente a p. ej. un paquete UDP.
Creo que puedes obtener una muchas variaciones de call stacks usando diferentes combinaciones de protocolos, interfaces y localicaciones de hooks. Por el momento estaremos usando un chain hook configurado como un output
con un paquete UDP.
Diseño del stack y los alcances fuera de límites en nft_do_chain cuando un paquete UDP enviado alcanza un hook configurado a output
Para poder crear un exploit estable primero tendremos que filtrar la dirección de la imagen del kernel.
La dirección de la imagen del kernel tiene 9 bits de entropía (medida de la incertidumbre existente ante un conjunto de mensajes, del cual va a recibirse uno solo), lo que significa que hay 512 diferentes posiciones en las cuales el kernel puede ser cargado. Dependiendo del escenario de tu ataque, hay una probabilidad de 1 en 512 de conseguir que el ataque funcione correctamente; pero sería mejor si pudiésemos conseguir un exploit más estable de esto.
El paso más sencillo es tratar de usar nuestra capacidad de lectura fuera de límites que nos consiguió nft_bitwise
para copiar algunos de los datos del stack a nuestros registros. Ya que el intervalo total que podemos leer tiene una longitud de0x7c
bytes, existe una probabilidad bastante buena de que la dirección del kernel esté ahí.
Alcance fuera de límites de nft_bitwise
¡Hoy es nuestro día! Existen dos:
gef➤ x/bx 0xffffffff815b49c1
0xffffffff815b49c1 <import_iovec+49>: 0xc9
gef➤ x/bx 0xffffffff819ac3ec
0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
Escribir esto a los registros es una cosa, pero extraerlos es otra. Después de investigar, parece que no existe una forma fácil para leer directamente los registros cuando nft_do_chain
está en ejecución.
En mi reporte original a security@k.o, me informaron de que la expresión nft_dynset
por un mantenedor de netfilter, que tiene soporte para dynamic sets que pueden actuar como una especie de base de datos que puede escribir y leer a través de diferentes nft_do_chain
ejecuciones. Aparentemente, el nft_payload
también tiene la capacidad de escribir al paquete por sí mismo, no me di cuenta de esto.
En su lugar, decidí continuar con mi side-channel attack. Debido a la naturaleza de nf_tables
, puedes causar efectos secundarios. De hecho, podrías decir que ni siquiera son efectos secundarios, sino efectos primarios.
Creando reglas que sueltan o aceptan el paquete basándose en el valor de la dirección de memoria del kernel que estamos copiando, poco a poco podemos deducir cuál es el valor examinando si los paquetes que enviamos también fueron recibidos.
- Crear un socket UDP que recibe paquetes en
127.0.0.1:9999
:
-
Debería recibir paquetes en un hilo diferente.
-
Un mensaje debería ser enviado de vuelta por cada paquete que reciba.
-
Agrega una regla que:
-
Copia la dirección del kernel a los registros con
nft_bitwise
. -
Usa
nft_cmp_expr
para comparar la dirección a una constante. -
Soltar un paquete si la comparación evaluada es verdadera.
-
-
Envía un paquete UDP a
127.0.0.1:9999
- Podemos determinar un poco de información acerca de la dirección del kernel basado en si recibimos un mensaje de vuelta.
-
Repite 2 y 3 con los valores adecuados hasta que tienes suficiente información para determinar la información por sí sola.
Todavía existen algunas advertencias. Por ejemplo, el paquete que recibimos también podría ser soltado sin ningún previo aviso. Para mitigar esto, podemos añadir una reducción de ruido, para la cual necesitaremos una base chain y una auxiliary regular chain.
Rule in base chain:
# | Expresión | Argumentos | Comentario |
---|---|---|---|
0 | nft_payload |
base=NFT_PAYLOAD_TRANSPORT_HEADER offset=offsetof(udphdr, dport) len=sizeof_field(udphdr, dport) |
Escribir el puerto de destino del paquete al registro 8. |
1 | nft_cmp_expr |
op=NFT_CMP_EQ sreg=8 data=9999 |
Equiparar el puerto destino a 9999 , y regresar NFT_BREAK si el resultado no es igual. |
2 | nft_payload |
base=NFT_PAYLOAD_INNER_HEADER offset=0 len=8 |
Escribir los primeros ocho bytes del paquete al registro 8. |
3 | nft_cmp_expr |
op=NFT_CMP_EQ sreg=8 data=0xdeadbeef0badc0de |
Comparar los primeros ocho bytes al valor mágico, y regresar NFT_BREAK si no es igual. |
4 | nft_immediate_expr |
verdict=NFT_JUMP chain=aux_chain |
Ya que la regla aún está evaluando, las condiciones deben coincidir, y llamar a nuestra auxiliary chain. |
Rule in auxiliary chain:
# | Expresión | Argumentos | Comentario |
---|---|---|---|
0 | nft_bitwise |
op=NFT_BITWISE_RSHIFT data=SHIFT_AMT dreg=OOB_OFFSET sreg=8 |
Escribe la dirección del kernel a los registros usando la lectura fuera de límites, cambiado por los bits de SHIFT_AMT para obtener el byte de la dirección deseada al registro correcto. |
1 | nft_cmp |
op=NFT_CMP_GT sreg=ADDRESS_OFFSET data=COMPARAND |
Comparar los byte de la dirección del kernel con COMPARAND , regresar NFT_BREAK si este resultado no es igual. |
2 | nft_immediate |
verdict=NFT_DROP | Soltar el paquete si la dirección byte es más grande que COMPARAND . |
Revisando el puerto destino y comparando los primeros ocho bytes interiores del header a un valor mágico, podemos activar los efectos secundarios para los paquetes que queramos.
Cambiando dinámicamente COMPARAND
podemos hacer una búsqueda binaria para encontrar el byte de la dirección del kernel por la 0(log(n))
vez. Cambiando dinámicamente SHIFT_AMT
a los próximos múltiplos de ocho podemos movernos al próximo byte de memoria y empezar de nuevo.
Un poco de código en python para filtrar la dirección de memoria. Lo gracioso es que fácilmente pude haber implementado esto en python. Recuerden que no siempre tienen que hacer sus exploits para una kernel en C :p
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''
def leak_byte(pos):
s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
s.settimeout(200) # 200ms debería ser más que suficiente
s.bind(("127.0.0.1", 1234))
# buscar los límites
low = 0, high = 255
while True:
mid = (low + high) // 2
# si encontramos el valor, lo regresamos
if low == high:
s.close()
return mid
set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)
# Enviar el paquete y activar la auxiliary chain
s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))
# El hilo secundario regresa a 127.0.0.1:1234
res = s.recvfrom(0x2000)
if not res:
'''
nuestro paquete fue soltado
ya que no se regresó nada en los 200ms
lo que significa que
byte to leak >= mid
el byte a filtrar es mayor o igual a mid (127)
'''
low = mid
else:
'''
[sanity check o prueba de cordura]
se usa para evaluar rápidamente si
el valor a calcular es siquiera posible
https://es.wikipedia.org/wiki/Prueba_de_cordura
'''
if res != b"MSG_OK":
print("Something went wrong")
return None
'''
Nuestro paquete fue aceptado, lo que
significa que
byte to leak < mid
byte a filtrar es menor a mid (127)
'''
high = mid - 1
leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]
Ahora que conseguimos el leak, la ejecución arbitraria de código debería ser muy fácil. La escritura fuera de límites de nft_payload
debería de poder escribir un ataque RoP en cadena para el stack, ¿cierto?
Nop. No tuvimos mucha suerte, al menos en este kernel en particular. La escritura fuera de límites de nft_payload
casi en su totalidad se alinea con el stack frame de la rutina de udp_sendmsg
. La dirección de udp_sendmsg
se encuentra en el offset +0x2f8
relativo a los registros, esta localización es muy baja como para ser alcanzada con nft_payload
o nft_bitwise
(podemos comenzar a escribir comenzando en el offset +0x304
, tan cerca...). La dirección inet_sendmsg
está localizada en el offset +0x4a8
. Técnicamente podemos alcanzarlo (y sobreescribir los tres bytes inferiores), pero hay un stack canary (ténica utilizada para la detección de un stack buffer overflow antes de que la ejecución de código malicioso pueda suceder) en la dirección +0x0458
que también necesitamos sobreescribir para lograr esto. Esto obviamente haría que el kernel crasheara, así que hacer esto no es una opción.
Logré usar este método en otra build del kernel, pero parece ser que tratar de hacer lo mismo para la kernel que estoy utilizando para este blog será un poco más difícil.
Ahora, tal vez podamos hacer un poco de contrived stack frame hacking para sobreescribir las variables locales en udp_sendmsg
. También podríamos intentar sobreescribir el verdict chain pointer, usando un valor de los registros p. ej. 0x7fffff00
(creo que esto podría ser una técnica genial; tomando en cuenta el reto).
Vamos a intentar cambiando la base chain hook que usamos. Estábamos usando una chain output
, ¿qué pasaría si la cambiamos a una de input
El diagrama del alcance fuera de límites en nft_do_chain si un paquete UDP enviado alcanza el input hook
¡Esto se ve un poco mejor! Podemos sobreescribir la dirección de retorno del frame de __netif_receive_skb_one_core
(offset +0x328
), que regresa hacia __netif_receive_skb
. Ya que está relativamente cerca a la altura del alcance fuera de límites de nuestro nft_payload
, podemos hacer que nuestro index OOB (alcance fuera de límites) apunte directamente a esta dirección de retorno, eludiendo el stack canary en el offset +0x310
. El offset +0x328
se traduce a index 0xca
.
Para activar la sobreescritura de la dirección de regreso, creamos un nuevo input
chain en la tabla, y le añadimos una regla con un nft_payload
que escribe 0xff
bytes desde el header interior del paquete hacia index 0xca
. Después mandamos un paquete con el payload, y boom.
🥳 🥳 🥳 🥳 🥳