memory-rs: going internal & Yakuza Like A Dragon Photo Mode
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
jmp
s condicionales (ja
,jb
,je
, etc) ajmp
- 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.
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.
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 struct
s, y estos
extenderlos a través de implementaciones, y también existen trait
s 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).