Resultados

Para captar tu atención, acá están los resultados de un Photo Mode implementado en Rust.

Habiendo terminado el Yakuza 0 (what a great game) y el Yakuza Kiwami (not so great tbh), naturalmente, el siguiente juego era Yakuza Kiwami 2.

Yakuza Kiwami 2 es conocido por tener un rendimiento no muy bueno, incluso un modder por el nombre de Silent hizo un parche para mejorar el frame pacing, donde se dio cuenta que para limitar el framerate usaron un sleep (sleep nunca es confiable debido a que depende del scheduler de Windows), sin embargo, a pesar de estos problemas, el juego se ve increible. Quedé inmediatamente impresionado por los gráficos, tiene un look que a mi por lo menos me atrae bastante.

Lamentablemente, a pesar de sus gráficas impresionante, el juego no trae un “Photo Mode” (en mi opinión, esto debería ser estándar a estas alturas), sólo se pueden tomar capturas cuando estás navegando libremente por la ciudad, pero esto limita inmediatamente la opción de obtener increíbles capturas en batallas por ejemplo, o tener panorámicas de la bella ciudad de Kamurocho.

Obviamente, como he estado investigando sobre el reverse engineering, esto no se podía quedar así

Introducing a new safe friend: Rust

Como ya mencioné, le tengo cierto cariño a los lenguajes compilados, me gusta lo eficiente que son, y lo fácil que es manipular la memoria, sin embargo, este es un poder un tanto “peligroso”, porque es facil apuntar a cosas que no existen, o hacer aritmética de punteros mal.

Habiendo amado C, y probado C++, me encontré con una nueva alternativa: Rust.

Izquierda: C++, Derecha: Rust

Existen otras alternativas a los lenguajes compilados, C#, Go, Rust, etc. todas con sus propios beneficios y defectos, pero Rust me llamaba la atención por ser conocido como un safe language, y por eso siempre lo tuve al ojo, pero nunca me dediqué a explorarlo, hasta que obviamente, la necesidad de programar en lenguajes compilados nació con el aprendizaje de esta área, así que ya era buen momento para meterle mano po'

Primeros pasos en Rust: memory-rs

Para crear un entorno de trabajo cómodo, lo primero es crear una librería que permita facilitar el manejo de memoria en Windows. En este sentido, la API es bastante straightforward, se necesita un HANDLE para el proceso, y teniendo la dirección de memoria base del módulo (en este caso, del EXE), ya es suficiente para comenzar a manipular el proceso. Bajo este esquema, lo más natural es crear una estructura que contenga esta información como base para no tener que guardarlas en variables cada vez que se vayan a utilizar, y además, implementar un par de métodos de cosas que se hacen frecuentemente para reducir la redundancia.

Bajo esta necesidad, el primer paso fue crear la librería memory-rs, la cual me permite hacer algunas cosas de forma bastante más cómoda:

let process = Process::new("YakuzaKiwami2.exe")
	.expect("El proceso no se pudo abrir");

// autocasteo de variable segun el tamaño
let x_pos = process.read_value::<f32>(addr);

// Se puede escribir un array directamente a la memoria
process.write_value::<[f32; 2]>(addr, [0., 0.]);

// Escribir array of bytes
let aob: Vec<u8> = vec![0xC0, 0xFF, 0xEE];
process.write_aob(addr, &aob);

Sin embargo, la función más interesante, es la que me permite inyectar shellcodes a los procesos, por ejemplo, tengo el siguiente código:

.data

PUBLIC get_controller_input
PUBLIC get_controller_input_end

;; Intercept the controller input
get_controller_input PROC
  push rax
  ; the pointer to the controller structure is stored in the head
  ; of the stack, but since we pushed rax and the return address,
  ; we need to add 0x8 + 0x8 to the RSP
  mov rax,[rsp+10h] 
  push rbx
  lea rbx,[get_controller_input + 200h]
  mov [rbx],rax
  pop rbx
  pop rax

  ; original code
  test eax,eax
  mov rax,[rsp+108h+8h] ; adjusted stack offset
  ret
get_controller_input_end::
get_controller_input ENDP

Que intercepta el puntero de la estructura del control (o joystick) y lo guarda en pos_injection + 0x200, en Rust puedo facilmente inyectarlo con


extern {
	static get_controller_input: u8;
	static get_controller_input_end: u8;
}

fn main() {
	...
	// guardo la direccion de inyeccion, para despues poder acceder al
	// valor interceptado
	let p_shellcode = unsafe {
		process.inject_shellcode(entry_point, size_origin_addr,
			&get_controller_input as *const u8,
			&get_controller_input_end as *cosnt u8)
	};

	// guardo el puntero al control
	let p_controller = process.read_value::<usize>(p_shellcode + 0x200);
}

Y por lo tanto, como se puede notar, esta librería me facilita bastante la escritura de scripts en assembly, lo cual se hace con bastante frecuencia.

Mejorando lo que ya se tenía: freecam anteriores

Para el caso de Yakuza 0 y Yakuza Kiwami, las herramientas de freecam eran bastante… primitivas, ya que el movimiento no era natural, si no que se movía en base a los ejes $(X, Y, Z)$, lo cual obviamente no es intuitivo ya que depende de como está hecho el mapa y mucha gente se quejaba de eso (totalmente entendible).

Luego de largas jornadas de debugging con el amado Cheat Engine, logré encontrar la función global que maneja las cámaras para el caso del Yakuza Kiwami 2, con un gran detalle: las estructuras no contenían ni Pitch ni Yaw, si no que eran 6 valores: Punto 3D donde se enfoca, y Punto 3D a la posición de la cámara (de ahora en adelante, focus y position respectivamente).

¿La solución? back to Cálculo 3.

Mate no sirve pa ná, except some times it does

La forma más natural de navegación es con WASD y mouse, donde W, S mueven en dirección de donde se mira, y A, D en dirección lateral, y obviamente con el mouse se puede cambiar donde se apunta. ¿El problema? ambos inputs por separado solo entregan coordenadas en dos dimensiones, donde W y S entregan un $\Delta x$, A y D un $\Delta y$, y el mouse $x \wedge y$.

Coordenadas esféricas: boyoyoi

Como se tienen dos puntos 3D en el espacio (el de Focus y el de Position) es fácil imaginar la situación como una esfera, donde la posición de la cámara corresponde al centroide, y la posición del Focus corresponde a un punto en el cascarón de la esfera. Habiendo aclarado esto, un vector en coordenadas esféricas está compuesto de la siguiente manera:

$$ (r, \theta, \phi) \in [0, R] \times [0, 2\pi] \times [0, \pi] $$

¿Por qué estamos haciendo esto? Porque en las coordenadas esféricas el $\theta$ corresponde a la visión 360º en horizontal, y $\phi$ corresponde a la visión en 90º vertical, y con estos valores, podemos mapear las coordenadas $x$ e $y$ que se reciben del mouse a la dirección en donde se mira.

¿Como se hizo al final? Se calculó un vector que llamaremos $\overrightarrow{R} = F - P$ y la velocidad de movimiento del mouse con un pequeño delta de tiempo ($\Delta x$ e $\Delta y$), definiendo así un nuevo $\overrightarrow{R}_n$ tal que:

$$ \overrightarrow{R}_n = (r, \theta + \Delta x, \phi + \Delta y) $$

Luego, el vector $\overrightarrow{R}_n$ se cambia a coordenadas cartesianas para obtener un nuevo $F$ y $P$ de tal manera que:

$$ F_n = P + \overrightarrow{R}_n + \Delta p \cdot \overrightarrow{R} \\ P_n = P + \Delta p \cdot \overrightarrow{R} $$

donde $\Delta p$ corresponde al movimiento del jugador con WS. Listo, con eso tenemos una manera de manipular las coordenadas de forma intuitiva! El código se ve algo así como:

fn calc_new_focus_point(cam_x: f32, cam_z: f32,
		cam_y: f32, speed_x: f32, speed_y: f32) -> (f32, f32, f32) {

		// use spherical coordinates to add speed
		let theta = cam_z.atan2(cam_x) + speed_x;

		let phi = (cam_x.powi(2) + cam_z.powi(2)).sqrt().atan2(cam_y) +
				speed_y;

		let r = (cam_x.powi(2) + cam_y.powi(2) + cam_z.powi(2)).sqrt();

		let r_cam_x = r*theta.cos()*phi.sin();
		let r_cam_z = r*theta.sin()*phi.sin();
		let r_cam_y = r*phi.cos();

		(r_cam_x, r_cam_z, r_cam_y)
}

...

self.f_cam_x = self.p_cam_x + r_cam_x + dp_forward*r_cam_x +
	dp_sides*r_cam_z;
self.f_cam_z = self.p_cam_z + r_cam_z + dp_forward*r_cam_z -
	dp_sides*r_cam_x;
self.f_cam_y = self.p_cam_y + r_cam_y + dp_forward*r_cam_y +
	dp_up*r_cam_y;

self.p_cam_x = self.p_cam_x + dp_forward*r_cam_x + dp_sides*r_cam_z;
self.p_cam_z = self.p_cam_z + dp_forward*r_cam_z - dp_sides*r_cam_x;
self.p_cam_y = self.p_cam_y + dp_forward*r_cam_y + dp_up*r_cam_y;

Cabe destacar que se sumaron unos deltas extras para el movimiento lateral y el movimiento en el eje y.

Devuelta al código

Después de haber hecho esa matemagia (quizás es básica, pero hey, I’m proud) lo único que faltaba era escribir un par de shellcodes para interceptar los valores guardados y reescribirlos a la fuerza y así fue como hice la primera versión del freecam

De Freecam a Photo Mode

La historia no termina allí. De seguro es cool poder explorar el mundo libremente del personaje, pero las batallas y el ragdoll del juego es tan genial que simplemente no me pude detener allí, así que nació un nuevo objetivo: Hacer un “Photo Mode” real, donde se pueda pausar y explorar la escena.

Comprobar que fuera factible no fue tan complejo, ya que me di cuenta que si pausaba el juego (i.e. me iba al menú), podía seguir moviendo la cámara y los personajes seguían quietos, lo cual me motivó a seguir ese approach.

Luego de largas jornadas de búsqueda en Cheat Engine, encontré la variable que pausaba el juego, ¿El problema? si pausaba el mundo, y salía del menú, todo tipo de input quedaba bloqueado, por lo tanto no había forma de despausar el mundo, a pesar de que yo modificase esa flag nuevamente.

Por casualidad, un par de bytes más al lado del pause flag, estaba la flag que verificaba si el juego estaba en primer plano, y por pura suerte, cuando esa flag cambiaba el juego se pausaba y volvía a otorgar control, por lo tanto la solución fue forzar esa flag a cambiar por un pequeño delta de tiempo para que el juego volviese a forzar la pausa y aceptara input:

fn trigger_pause(process: &Process, addr: usize) {
    if addr == 0x0 { return; }
    process.write_value::<u8>(addr, 0x1);
    thread::sleep(Duration::from_millis(20));
    process.write_value::<u8>(addr, 0x0);
}

Bastante hacker si me preguntan a mi :P

Como toque final, había que ocultar la UI del juego, que básicamente se reducía a nopear instrucciones, así que nada tan complejo.

Retoques finales

Imagen que sale al inicio del juego

Finalmente, como A real Yakuza use a Gamepad, evidentemente el juego estaba diseñado para ser jugado con un control, por lo tanto, era un poco molestoso estar obligado a ocupar el teclado para usar el Photo Mode, así que con un poco más de experiencia, se me ocurrió la siguiente solución:

  1. Revisar los DLL que carga el ejecutable; para Windows, el DLL que maneja los controles es XINPUT
  2. Obtener todas las llamadas a las funciones GetState de XINPUT
  3. Revisar las estructuras que se modifican durante esas llamadas
  4. Escribir un sniffer que obtendrá el puntero a la estructura del control
  5. Leer ese input y manejarlo

Y pam! obtuve el input del joystick, así que ahora el control es mucho más natural!

Conclusiones

Definitivamente, este es uno de los hacks más entretenidos que he hecho, ya que pude aprender un poco de Rust, mejorar los conocimientos de manejo de memoria, y hacer un freecam realmente usable.

El proyecto tuvo una excelente recepción en internet, alcanzando 70 estrellas en GitHub y estando entre los top posts del subreddit de Rust!

Recepción en Reddit
Recepción en GitHub

Además, ahora puedo hacer hermosas capturas de un juego que se ve realmente hermoso.