
Documentación y código de prueba de concepto para CVE-2022-24125 y CVE-2022-24126.
Se ha lanzado una nueva actualización del juego, la 1.15.1, para Dark Souls III el 25/08/2022, junto con la restauración de los servicios en línea. Esta actualización corrigió tanto CVE-2022-24125 como CVE-2022-24126, junto con una amplia variedad de otras posibles vulnerabilidades de seguridad presentes en la red P2P del juego (lecturas/escrituras fuera de límites). Además, se han corregido todos los exploits conocidos que permitían corromper la partida de otros jugadores. Muchos trucos menores comunes (p. ej., "cuchillo de maldición") que se podían encontrar a menudo durante el multijugador en línea también han sido parcheados.
Este repositorio contiene código de prueba de concepto y documentación para el exploit RCE más reciente que afecta a los juegos de FROM SOFTWARE, CVE-2022-24126. Aunque teóricamente es posible en otros juegos, la atención se centra en Dark Souls III, ya que es el juego en el que he realizado mi investigación. Por ahora solo existe código de prueba de concepto para Dark Souls III; se ha confirmado que la vulnerabilidad está presente en:
El código vulnerable también está presente en Sekiro (crédito: LukeYui), aunque no hay forma de activarlo. La presencia en Demon's Souls no se ha confirmado, pero es muy probable. Si bien la prueba de red cerrada se vio afectada por esto, la versión final de Elden Ring no lo está. De hecho, una enorme lista de fallos de red, lecturas/escrituras fuera de límites y exploits que permitían a los jugadores modificar los datos del juego de sus pares, presentes en Dark Souls III, han sido parcheados en Elden Ring. Felicitaciones a LukeYui por recopilar esta lista y a FROM SOFTWARE por actuar con rapidez. Me alegra decir que Elden Ring es, indiscutiblemente, el título de FROM SOFTWARE más seguro en cuanto al alcance del daño que los hackers pueden infligir.
Contrariamente a la creencia popular, esto NO es un exploit de red peer-to-peer. Está relacionado con el servidor de emparejamiento y, por lo tanto, es mucho más grave, ya que no necesitas participar en ninguna actividad multijugador para ser vulnerable debido a otra vulnerabilidad del servidor de emparejamiento (CVE-2022-24125).
Con un promedio de unos 20.000 jugadores concurrentes en los meses anteriores al cierre de los servidores, era claramente un problema que necesitaba una solución inmediata, especialmente ante la posibilidad de que estuviera en Elden Ring. Dado que FROM SOFTWARE no había actuado después de más de 40 días desde mi informe inicial con vídeos de prueba de concepto y documentación detallada del exploit (en la que se basa gran parte de este readme), decidí demostrar la existencia del exploit públicamente de manera benigna, con la esperanza de atraer la atención para que los desarrolladores lo abordaran, y funcionó.
La comprobación inadecuada de límites en un búfer de pila y en el campo de tamaño de datos durante el análisis de los datos de emparejamiento de NRSessionSearchResult permite a un atacante ejecutar código arbitrario. El desbordamiento de pila permite sobrescribir los dos bytes inferiores de vftable_ptr del objeto DLMemoryInputStream utilizado internamente por el lector de flujo, redirigiendo la ejecución a código vecino cuidadosamente elegido. La explotación inteligente de la estructura del objeto DLMemoryInputStream y de su campo de tamaño de datos permite entonces lograr la redirección de código arbitrario, con RCX apuntando a la dirección de nuestro paquete. A partir de ahí, se puede usar una serie de redirecciones de código mediante llamadas virtuales con diferentes desplazamientos (que ahora saltarán a las direcciones que escribimos en el búfer del paquete) para lograr la ejecución de código arbitrario.
Los vectores de distribución son lo que hace que este RCE en particular sea especialmente grave (más allá de ser ya un RCE). El exploit se transmite a través de solicitudes push de emparejamiento que contienen información de NRSessionSearchResult. Esto significa que el atacante puede apuntar a cualquiera que se una a su sesión en línea. En particular, para DS3:
PushRequestSummonSign)PushRequestAllowBreakInTarget)PushRequestVisit)PushRequestAcceptQuickMatch)Esto ya es bastante malo, pero el verdadero potencial se desbloquea con la solicitud RequestSendMessageToPlayers:
message RequestSendMessageToPlayers {
repeated uint32 player_ids = 1;
required bytes push_message = 2;
}
El anfitrión usa esta solicitud para enviar directamente el mensaje push PushRequestAllowBreakInTarget a los invasores, de modo que puedan obtener las coordenadas de aparición y unirse a su sesión P2P. Eso es todo. Esa es la única forma en que el juego usa esta solicitud.
Si bien el RCE no se traslada exactamente a todos los juegos, la idea central del exploit, que le da al atacante redirección de código arbitrario, es la misma. Si esto se puede lograr, es muy probable que se pueda encontrar una cadena de llamadas virtuales o una cadena ROP específica del juego. Este "primer paso" utiliza las siguientes vulnerabilidades:
Las solicitudes push de emparejamiento que contienen información de unión a sesión almacenan dicha información en un formato binario personalizado que consiste en una cadena de entradas de datos delimitadas por longitud. Cada entrada tiene el siguiente formato:
struct Entry
{
uint32_t type_or_id; // not sure, but probably a type (fixed length = 2, variable length = 1 ?)
uint32_t size;
uint8_t data[size];
}
La función del juego responsable de copiar los datos de estas entradas confía ciegamente en el campo de tamaño, lo que crea una lectura fuera de límites. Un cliente malintencionado puede abusar de esto estableciendo el campo de tamaño a valores como 0x7FFFFFFF, lo que hace que la asignación de memoria falle y el juego de la víctima se bloquee. Más tarde, este tamaño también se pasa al constructor de un DLMemoryInputSteam, que es una parte instrumental del exploit.
NRSessionSearchResultUna de las entradas en la estructura de datos descrita anteriormente es un objeto NRSessionSearchResult serializado. El analizador de estos datos primero analiza una lista de propiedades. Estas propiedades pueden ser enteros de 4 bytes, enteros de 8 bytes o cadenas anchas terminadas en nulo. A esta lista de propiedades le sigue el nombre de persona de Steam del anfitrión como una cadena ancha terminada en nulo y algunos datos adicionales que no son importantes para el exploit. Tanto esta función como el analizador de la lista de propiedades usan un búfer de pila de tamaño fijo para leer cadenas, y en ambos casos no se realiza ninguna comprobación de límites en el búfer. Aquí está el código del juego responsable de copiar el nombre del anfitrión (producido con el descompilador de Ghidra y luego limpiado):
size_t idx = 0;
wchar_t wchr = 0;
do {
// read_wchar() function at vftable index 17 of DLEndianStreamReader
wchr = stream_reader->read_wchar();
player_name_buff[idx] = wchr;
idx++;
} while (wchr != 0);
Esto conduce a un exploit de desbordamiento de búfer, que permite al atacante corromper la pila.
Para lograr la redirección de código arbitrario, usamos esto y el diseño de memoria de un objeto DLMemoryInputStream instanciado en la pila por la función que llama al analizador, y que es utilizado internamente por el lector de flujo:
struct DLMemoryInputStream {
uintptr_t* vftable_ptr; // Offset 0
size_t data_size; // Offset 4 (32bit) / 8 (64bit)
uint8_t* data_buffer; // Offset 8 (32bit) / 16 (64bit)
// Entries after the buffer are not important for the exploit
}
Dado que controlamos el campo data_size (Bug #1), se puede establecer en la dirección de memoria de pila del campo data_buffer. Esto tendrá éxito siempre que la dirección sea constante y no demasiado grande (DS3 cumple esos requisitos). Dado que el compilador coloca el búfer de pila en la parte superior del marco, el atacante puede usar Bug #2 para sobrescribir los dos bytes inferiores de vftable_ptr del DLMemoryInputStream. Por lo tanto, cuando el DLEndianStreamReader lea el siguiente carácter, llamará internamente al y el código será redirigido. Los 2 bytes dan suficiente margen para saltar a la función número 22 en la vftable de , que llama al sexto método virtual del objeto apuntado por su primer campo. En un proceso de 64 bits (es decir, Dark Souls III), se ejecutarían las siguientes instrucciones:
Consulta aquí para más detalles sobre estos 3 gadgets. Si para algún otro juego este método de llamada virtual no es un enfoque factible, la redirección de código arbitrario aún puede usarse para preparar un exploit ROP más tradicional.
Para ejecutar el código de prueba de concepto, primero debes tener un servidor al que conectarte. Si bien los servidores oficiales se han deshabilitado debido al exploit, puedes configurar uno privado usando ds3os. ds3os está diseñado para imitar el comportamiento del servidor comercial lo más fielmente posible, pero ya se han implementado parches de seguridad en este proyecto para corregir este exploit. Sin embargo, aún puedes configurar un entorno de pruebas compilando el proyecto tú mismo con las constantes SEND_MESSAGE_TO_PLAYERS_SANITY_CHECKS y NRSSR_SANITY_CHECKS establecidas en false en BuildConfig.h. Esto imita el comportamiento inseguro del servidor comercial. Sigue las instrucciones proporcionadas por ds3os para iniciar el juego y conectarte a tu servidor.
Una vez hecho esto y con tu juego conectado a los servidores, compila el código PoC e inicia el ejecutable Injector.exe. Inyectará una DLL que contiene el código del exploit en el proceso de Dark Souls III. Luego, esta DLL usará la función del juego que envía mensajes FRPG al servidor para entregar el exploit a tu propio cliente.
Para la prueba de concepto decidí usar un mensaje PushRequestVisit enviado mediante RequestSendMessageToPlayers. Esta es la versión más potente del exploit, en el sentido de que el juego del objetivo analizará inmediatamente los datos vulnerables después de recibirlos en todas las situaciones (incluso en el menú principal).
LEA RAX,[DAT_144786150]
RET
Este gadget se usa en el de 0x68. Necesitamos poner en RAX una dirección inferior pero bastante cercana a 144786998; esta es la más cercana.
MOV RDX,RAX
MOV R8,qword ptr [RCX]
CALL qword ptr [R8 + 0x68]
Para poder usar el gadget en el offset 0x68, necesitamos que la dirección del búfer de datos esté almacenada en RDX y que RCX permanezca igual. Esto logra precisamente eso.
Recomiendo revisar el código fuente del código de prueba de concepto, ya que tiene muchos comentarios que detallan la estructura del paquete. Si quieres ver lo que sucede en cada paso en tiempo real (¡deberías, es bastante genial!), puedes inyectar la DLL de la prueba de concepto mientras ejecutas el juego bajo un depurador con puntos de interrupción en las siguientes direcciones de interés:
140ca5960Esencialmente donde comienza el exploit. Esta función es responsable de analizar los datos de la lista de entradas delimitadas por tamaño del mensaje PushRequestVisit. Primero extrae cada entrada de la lista en diferentes vectores:
0x140ca59f8:
player_data_cpy_ptr = (std_vector *)VectorCopy2_140ca4ef0(&player_data_cpy,player_data);
FUN_140ca5010(player_data_cpy_ptr,&spawn_data,0x1c);
FUN_140ca5010(player_data_cpy_ptr,&unk,4);
FUN_140ca4fa0(player_data_cpy_ptr,&nrssr_data);
La función 140ca5010 comprueba los tamaños de las entradas, pero 140ca4fa0 es para entradas de tamaño variable y no realiza comprobaciones de validez en el campo de tamaño (Bug #1). Para lograr el exploit de redirección de código arbitrario descrito anteriormente, necesitamos establecerlo en 14F3B0. Esto causará una lectura fuera de límites de aproximadamente 1.3MiB, pero la página de memoria debería ser lo suficientemente grande para evitar violaciones de acceso.
140ca56b0Esta función es llamada por la anterior con nrssr_data como argumento. Crea el objeto DLMemoryInputStream en la pila, que luego se pasa como argumento al analizador NRSSR.
141955f50: ParseNRSessionSeachResultEl analizador de NRSessionSearchResult. Verifica la firma y los números de versión de NRSSR (14196a0f0), analiza la lista de propiedades (14196a260), el nombre del anfitrión (14195603a) y algo más de información (consulta rce.h)
14195603aBucle en la función anterior que copia de forma insegura el nombre del anfitrión (Bug #2). Aquí hay algunas direcciones que pueden ayudar a seguir lo que está sucediendo durante el desbordamiento del búfer:
14F128DLMemoryInputStream: 14F3A0DLMemoryInputStream después de la sobrescritura: 1439e8b30DLMemoryInputStream utilizado por el DLInputStreamReader: 0x181439e8b48MOV RCX,qword ptr [RCX + 0x8]
MOV RAX,qword ptr [RCX]
JMP qword ptr [RAX + 0x40]
Donde terminamos después de la primera redirección de código causada por la vftable del flujo de memoria sobrescrita. Aquí es donde comienza la cadena de redirecciones de llamadas virtuales.
Para Dark Souls III Ver. 1.15. El tamaño máximo teórico de la carga útil depende del diseño de la pila y, por lo tanto, variará según el juego y la versión. ↩
No puedo enfatizar lo horriblemente inseguro que es esto. Cualquier jugador puede básicamente hacerse pasar por el servidor de emparejamiento. Al usar esta solicitud para enviar el exploit a través de un PushRequestVisit, cualquier jugador en línea puede ser atacado de forma remota siempre que se conozca su ID de jugador. El atacante también puede enviar el exploit a toda la base de jugadores en línea muy rápidamente enviando múltiples solicitudes, cada una conteniendo una gran porción de posibles IDs de jugador.
DLMemoryInputStreamDLEndianStreamReaderMOV RCX,qword ptr [RCX + 0x8]
MOV RAX,qword ptr [RCX]
JMP qword ptr [RAX + 0x40]
Dado que RCX es un puntero al objeto DLMemoryInputStream, la primera instrucción escribe el campo data_size, que el atacante ha establecido mediante Bug #1 a una dirección de pila que apunta al campo data_buffer, en RCX. Las dos siguientes instrucciones redirigirán así la ejecución a la dirección de memoria que el atacante ha escrito en el desplazamiento 0x40 del búfer de datos. ¡Se ha logrado la redirección de código arbitrario! A partir de ahí, el atacante puede establecer una cadena de redirección de código que copie su carga útil en una región de memoria adecuada y la ejecute eligiendo código cercano a llamadas virtuales con diferentes desplazamientos, ya que el búfer ahora actúa como una tabla de métodos virtuales. Para la prueba de concepto de Dark Souls III encontré una configuración que solo requiere 3 gadgets para lograr RCE:
140e977001422be020140e40f15; Jumping here from the gadget at offset 0x40
MOV RBX,RDX
CMP R9,R8
; Never jumps, R9 != R8
JZ LAB_140e40f7a
MOV RAX,qword ptr [RCX]
MOV R8,qword ptr [RSP + 0x50]
MOV RDX,R9
MOV qword ptr [RSP + 0x30],RSI
; Call gadget at offset 18 (140e97700). Loads 144786150 into RAX
CALL qword ptr [RAX + 0x18]
MOV RSI,RAX
TEST RAX,RAX
; Never jumps, RAX is the data buffer addr.
JZ LAB_140e40f4d
CMP RBP,RDI
MOV RDX,RBX
MOV RCX,RAX
CMOVC RDI,RBP
MOV R8,RDI ; RDI is a stack address close to 14F3B0, so the memcpy succeeds
CALL memcpy
LAB_140e40f4d:
TEST RBX,RBX
; Never jumps as RBX == RDX == data buffer addr, nonzero.
JZ LAB_140e40f62
; We have our now fully control this RWE memory region due to the memcpy at 144786150. RCE has been achieved!
MOV RCX,qword ptr [DAT_144786998]
MOV RDX,RBX
MOV RAX,qword ptr [RCX]
CALL qword ptr [RAX + 0x68]
Este gadget hace casi todo por nosotros. Llama al offset 0x18 para obtener un puntero de destino para memcpy, copia nuestro paquete allí y luego llama a la función virtual en el offset 0x68 sobre el objeto estático en 144786998, que ahora controlamos por completo gracias a la llamada a memcpy. Dado que la cantidad de memoria corrompida por memcpy es grande y algunas regiones están siendo escritas constantemente por otros hilos del juego, el exploit primero carga una carga útil de «configuración» que se copia en una ubicación segura, suspende todos los demás hilos y vuelve a copiar nuestra carga útil real antes de saltar a ella. Consulta rce.h para más información.