Habiendo comenzado a experimentar con hacking interno a través de inyección de DLLs, y mientras esperaba la salida del juego Yakuza: Like A Dragon, aproveché de comenzar a preparar mi librería memory-rs para por fin comenzar a utilizar hacking interno, que por lo general conllevaba más trabajo preparar el boilerplate pero al final traía muchos más beneficios.

Evolución de memory-rs

A partir del 27 de octubre, comencé a trabajar casi todos los días en esta librería, porque cada vez me fascinaba más las posibles técnicas que se pueden utilizar al estar dentro del mismo espacio de memoria que el proceso objetivo, por lo que era necesario construir un toolkit para evitar ser redundante en futuras ocasiones.

El objetivo final de esta librería es facilitar la edición de memoria, que sirva para juegos es una consecuencia secundaria, pero en el estado actual, ésta permite fácilmente inyectar shellcodes o hacer function detouring, técnicas que se usan frecuentemente para expandir APIs privadas o en su defecto, a veces para desarrollar malware bastante básico.

Después de haber experimentado con distintas necesidades de modificación de memoria, concluí que era necesario implementar 3 estructuras esenciales que facilitan la modificación de software closed source para expandir su funcionalidad.

Detour, Injection & StaticElement

Estas 3 estructuras son las que defino como esencial para facilitar el game modding. Quizás después agregaré más (o quizás elimine alguna), pero por ahora, esta separación me ha funcionado bastante bien. Aquí un breve resumen de qué es lo que hace cada una:

  • Detour: Estructura que contiene un puntero a una parte original del código, un puntero a una función que se va a inyectar y opcionalmente un puntero a donde debe volver (en el caso de lo que se inyecte sea un shellcode, usualmente se salta de vuelta a donde estaba el código originalmente).
  • Injection: Estructura que contiene un puntero a una parte original del código, y un vector que contiene los bytes que se sobreescribirán sobre esa función original. Ésta estructura es útil cuando se necesitan nopear instrucciones o cuando se necesitan cambiar de jmps condicionales (ja, jb, je, etc) a jmp
  • StaticElement: Estructura que contiene un puntero a una dirección estática, y un valor, y este se sobreescribe cada vez que se llame a la función inject.

Lo interesante de estas 3 estructuras, es que fueron pensadas usando la estrategia RAII, por lo que al sacar la inyección, todo queda limpio como si nada hubiese pasado (ya sea porque se va fuera del scope o porque se saca el DLL).

Haciendo todo update-proof: scan_aob

Una de las grandes ventajas de usar hacking interno, es que se tiene acceso completo a la memoria del proceso (obviamente, los segmentos que son válidos, duh). Previamente, cuando necesitaba modificar algo haciendo todo externo, debía solicitar a través de una syscall a Windows que me entregue una copia de bytes del espacio de memoria que deseo leer. Ahora, simplemente todo se transforma en álgebra de punteros, lo cuál acelera bastante el proceso de obtención de datos ya que no hay intermediario.

Sabiendo esto, ahora era vital implementar una función que hace mucho tiempo quería hacer, debido a que me permitirá hacer parches update-proof: scan_aob. La gracia de dicha función, es que a partir de un patrón de bytes, te permite buscar dentro de segmentos de memoria (usualmente, escaneo el binario del ejecutable) y cuando se haga match del patrón, se retorna la dirección. Esto es útil porque por ejemplo, cuando se actualiza un juego, los offsets suelen cambiar al hacer la recompilación, porque basta que una estructura tenga un campo nuevo o que quizás un string sea un par de bytes más largo, y el parche ya no funcionará. Esto facilita bastante las cosas y además permite compatibilidad entre versiones (La freecam de Yakuza Like A Dragon funciona para Steam y para Microsoft Store sin hacer cambio alguno gracias a esto!).

// snip
let mut store_pref_detour = unsafe {
	let (size, func) = generate_aob_pattern![
		0x8B, 0x41, _, 0x89, _, 0x8B, 0x41, 0x24, 0x41, 0x89, 0x00, 0xC3
	];
	Detour::new_from_aob(
		(size, func),
		// proc_inf contiene información esencial del proceso
		&proc_inf,
        // store_preferred_res es el puntero a mi shellcode, el cual está
        // escrito en assembly
		auto_cast!(store_preferred_res),
        // En este caso el shellcode tiene un return, por lo cual no es
        // necesario inyectar un jmp back
		None,
		12,
        // La dirección encontrada por el aob_scan no necesita de un offset.
		None,
	)
	.or(Err("Couldn't find store_pref_addr"))?
};

store_pref_detour.inject();

Lo genial de esto, es que usando las macros de Rust, podemos generar una función lambda que lo único que hace es intentar hacer match cuando el patrón se cumple (los _ implican que puede haber cualquier byte en ese espacio), por lo tanto no hay que parsear strings (I’m looking at you, C++ 😉).

Ciertos usos de memory-rs

Bacán, te estoy vendiendo mi invento, pero, ¿de qué sirve? bueno, aquí hay ciertos ejemplos en los cuales mi librería me ha sido útil para acelerar el proceso de creación:

rtti-dumper

A veces los procesos contienen información en run-time de los tipos (Run Time Type Information, RTTI), y a veces es práctico comenzar a mirar desde ahí para hacer ciertas búsqueda de objetos en memoria. Es por esto, que creé un rtti-dumper, el cuál básicamente intenta encontrar las tablas de funciones virutales de objetos con información en run-time. Usando Rust, pude abusar de la Fearless Concurrency para crear algo multi-threaded, y sorprendentemente, el programa logra procesar ~60 GB/s! Gracias a que todo está en memoria, ésta se puede revisar múltiples veces y es extremadamente rápida, y no hay problemas al hacer acceso concurrente ya que todo es read-only.

Dumpeando las VFT de los RTTI contenidos en el Yakuza Kiwami 2, a ~60 GB/s

Red Dead Redemption 2 patch - range removal

Red Dead Redemption 2 es un juego con vistas fantásticas, creo que hasta ahora, no hay ningún juego que se le compare. Debido a esto, en el servidor de Discord que estoy, Frans Bouma había creado una Cheat Table para Cheat Engine (nos volvemos a encontrar ♥) donde implementaba el range-removal del modo foto del juego (es decir, quitaba la limitación de alejamiento de la cámara) y además habilitaba la opción de usar hot-sampling (básicamente si la ventana escala, la resolución también lo hace, ideal para tomar pantallazos de alta resolución). Debido a esto, aproveché de darle un uso a mi librería y pasar estas modificaciones a un parche (DLL) para que esta inyección se haga automáticamente ya que el modo fodo lo ocupo bastante, y usando la gran librería Ultimate-ASI-Loader, puedo crear un parche que se cargue automáticamente al abrir el juego.

Foto del RDR2 por mí

Películas en el taskmgr

Because, why not?

Ventajas de hacer el Photo Mode del Y:LAD con hacking interno

Para ir cerrando, déjame contarte de un par de ventajas que he obtenido gracias al hacer el photo mode del Yakuza: Like A Dragon usando memory-rs con inyección interna:

1. Update-proof

Como ya mencioné previamente, usando la función aob_scan, es mucho más probable que el parche sea update-proof debido a que los offsets pueden cambiando, dicho y hecho, así fue, el ejecutable ha sido actualizado 2 veces (y los offsets han cambiado según mis logs) y el Free-Cam ha funcionado sin ningún problema. Si pasase eso con alguna de mis previas freecam, tendría que buscar todo de nuevo con Cheat Engine, algo que es bastante consumidor de tiempo.

2. Rust structs

Oh boy, cuando se me ocurrió esto, quedé impresionado. Rust implementa un OOP bastante único, el cuál básicamente se resume en crear structs, y estos extenderlos a través de implementaciones, y también existen traits que se pueden implementar sobre structs existentes (algo así parecido como a las interfaces). ¿Por qué te cuento esto? por lo siguiente. En la memoria del juego, logré encontrar los valores esenciales del manejo de la cámara en el mismo, y se me ocurrió la brillante idea de castear esta información a un struct, y luego extender ese struct con mis métodos para modificarlo, y estoy demasiado orgulloso como quedó este approach. Esto se ve algo así:

// El repr(C) nos permite asegurar que el struct se compondrá de forma similar
// a como lo hacen los structs de C, con sus respectivos paddings
#[repr(C)]
struct GameCamera {
    // Por lo general siempre se ocupan vectores de tamaño cuatro debido a las
    // facilidades que otorga SSE2.
    pos: [f32; 4],
    focus: [f32; 4],
    rot: [f32; 4],
    // No conozco los valores que están acá, y se pueden castear como un
    // arreglo más alto, no ocupará más memoria porque esto ya existe ;)
    padding_: [f32; 0x8],
    fov: f32
}

// Esto es lo genial, puedo extender el struct GameCamera desde Rust, y luego
// aplicar estos métodos después de castear el puntero a mi struct personalizado,
// bastante genial eh?
impl GameCamera {
    pub fn consume_input(&mut self, input: &Input) {
        let r_cam_x = self.focus[0] - self.pos[0];
        let r_cam_y = self.focus[1] - self.pos[1];
        let r_cam_z = self.focus[2] - self.pos[2];

        let (r_cam_x, r_cam_z, r_cam_y) =
            Camera::calc_new_focus_point(r_cam_x, r_cam_z, r_cam_y,
                input.delta_focus.0, input.delta_focus.1);

        self.pos[0] = self.pos[0] + r_cam_x*input.delta_pos.1 +
            input.delta_pos.0*r_cam_z;
        self.pos[1] = self.pos[1] + r_cam_y*input.delta_pos.1;

        self.pos[2] = self.pos[2] + r_cam_z*input.delta_pos.1 -
            input.delta_pos.0*r_cam_x;

        self.focus[0] = self.pos[0] + r_cam_x;
        self.focus[1] = self.pos[1] + r_cam_y;
        self.focus[2] = self.pos[2] + r_cam_z;

        self.fov = input.fov;
    }
}

// También pude implementar el trait Debug de mi propio struct, que en el fondo
// imprimía de forma más bonita lo que ya estaba en memoria debido a las mismas
// facilidades mencionadas arriba
impl std::fmt::Debug for GameCamera {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let ptr = self as *const GameCamera as usize;
        f.debug_struct("GameCamera")
            .field("self", &format_args!("{:x}", ptr))
            .field("pos", &self.pos)
            .field("focus", &self.focus)
            .field("rot", &self.rot)
            .field("fov", &self.fov)
            .finish()
    }
}

// ... snip
unsafe {
    // casteamos el puntero a mi struct
	let gc = (g_camera_struct + 0x80) as *mut GameCamera;
    // Usamos el método implementado INTERNAMENTE en mi DLL sobre valores que
    // ya estan en memoria dentro del juego
	(*gc).consume_input(&input);

    // Podemos imprimir los valores de la GameCamera usando el trait Debug
	println!("{:?}", *gc);
}

¿Por qué es posible esto? Bueno, los arreglos en memoria no son nada más que valores contiguos en la RAM, y las struct son arreglos contiguos que “conceptualmente” están agrupados, entonces basta con saber el tamaño de estos objetos y se pueden castear a cualquier struct que tenga un formato parecido (por ejemplo, podría usar u32 en vez de f32, ya que tienen el mismo tamaño, pero los valores no tendrían sentido 😛). ¿Bastante cool eh?

3. Compatibilidad con Steam y MS Store version

Las freecam anteriores solo eran compatibles con la versión de steam (y si esta se actualiza, gg), en cambio esta, al usar scan_aob, y patrones seleccionados cuidadosamente (por ejemplo los jmp y los call se les removieron los offsets), es posible encontrar todas las funciones a modificar independientemente de la versión que estés ejecutando.

Finalizando

En fin, esta nota fue un vómito bastante rato, en conclusión, no me arrepiento de haber comenzado a hacer inyección interna, todo se hace más entretenido, los punteros son mucho más divertido de lo que pensaba y la memoria es solo un constructo social (heh). Si es que llegaste hasta acá, bacán, si te interesa este tópico, siempre me puedes encontrar en discord como etra#1337 o en twitter como etra0 (En facebook no soy muy activo).