Quién necesita Matplotlib si tenemos el Administrador de Tareas
El fin justifica los medios (?)
Obviamente mi objetivo no era simplemente dibujar senos en el Administrador
de Tareas aunque fue entretenido, si no que este fue el medio para
concretar mi fin: Aprender sobre hacking interno inyectando DLLs
Hacking interno: Sharing the space
Como comenté en un blog previo, el hacking externo es entre comillas “fácil de concretar”, ya que requiere mucho menos boilerplate, y la WinAPI provee las funciones necesarias para leer y modificar la memoria de un proceso externo sin mayor problemas, así que para cosas rápidas a veces es más fácil ir por este camino, pero lo que yo no sabía eran todas las ventajas que se tenían al hacer las cosas de forma interna.
Interno vs Externo
¿Cuál es la gracia de hacer las cosas internas vs externas? bueno, en verdad es un poco autoexplicativo:
-
Hacking Externo: Un proceso externo se encarga de leer y modificar la memoria del proceso objetivo a través de las funciones disponibles en la librería del sistema operativo (en el caso de WinAPI, principalmente
ReadProcessMemory
yWriteProcessMemory
). -
Hacking Interno: A través de una librería dinámica (por ejemplo, los
.dll
), se inyecta al proceso objetivo usando las funciones disponibles del sistema operativo, para que luego la librería misma se encargue de las modificaciones dentro del proceso, compartiendo el espacio de memoria.
Como se destacó en bold, lo relevante es que se comparte el mismo espacio de memoria con el proceso objetivo, lo que trae algunas ventajas como
- Leer/Modificar memoria a través de punteros (ya no es necesario llamadas API del sistema operativo, por lo que no hay interrupciones).
- Inyectar funciones propias.
- Ejecutar funciones del proceso objetivo.
- Compartir variables.
Y muchas más qué aun me falta por explorar, pero con eso ya tenemos bastantes ventajas por sobre el hacking externo. Vale destacar que no digo que con Hacking Externo no se pueda, más bien se complejiza bastante hacer lo mismo cuando convertir todo el procedimiento a hacking interno no es tan complejo.
Parte uno: la aguja
Algo que se podría considerar como desventaja del Hacking Interno es que ahora el todo el proceso consiste en dos piezas de software: el inyector y la inyección.
El inyector se encarga de forzar la carga de nuestra librería dinámica
(nuestra inyección) en el proceso objetivo, y esto se sigue haciendo
con llamadas a la API del sistema operativo. Hay varios inyectores disponibles
en internet, algunos enfocados a saltarse la detección de hacks para juegos,
y otros más simples; en este caso usaremos uno propio como ejercicio de
aprendizaje. Este programa es independiente de la inyección ya que el .dll
en sí tiene su propio entry point
que el sistema operativo se encargará de
ejecutar.
Usando nuestro ya amado lenguaje, Rust, el inyector quedaría algo así:
pub unsafe fn inject_dll(process: &Process, name: &str) {
use mem::transmute;
let dll_dir = CString::new(name).unwrap();
let dll_dir_s = dll_dir.as_bytes_with_nul().len();
// Se carga la libreria Kernel32, libreria que contiene LoadLibraryA,
// quien se encargara finalmente de cargar nuestro DLL.
let s_module_handle = CString::new("Kernel32").unwrap();
let module_handle = GetModuleHandleA(s_module_handle.as_ptr());
// Se obtiene un puntero de la funcion LoadLibraryA
let a_loadlib = CString::new("LoadLibraryA").unwrap();
let result = GetProcAddress(module_handle, a_loadlib.as_ptr());
// Se castea el puntero a una funcion para que se pueda pasar a
// CreateRemoteThread
let casted_function: extern "system" fn(LPVOID) -> u32 = transmute(result);
// Se asigna espacio en el proceso externo donde se escribira
// la direccion donde nuestro DLL esta ubicado en el disco
let addr = VirtualAllocEx(
process.h_process,
ptr::null_mut(),
dll_dir_s,
MEM_COMMIT,
PAGE_READWRITE,
) as DWORD_PTR;
let _dll_dir = dll_dir.into_bytes_with_nul();
process.write_aob(addr, &_dll_dir, true);
// Se ejecuta un thread en el proceso externo que solo consiste
// en ejecutar la funcion LoadLibraryA usando la direccion de nuestro
// DLL como argumento
let a = CreateRemoteThread(
process.h_process,
ptr::null_mut(),
0,
Some(casted_function),
addr as LPVOID,
0,
ptr::null_mut(),
);
FreeLibrary(module_handle);
}
Leerlo y entenderlo no es tan complejo, y con ese simple segmento de código,
ya estamos listos para comenzar a escribir nuestro DLL
.
Parte dos: la inyección
Cheat Engine is a debugger too
Increíblemente, Cheat Engine no es solo útil para juegos. Su habilidad
para encontrar valores, e instrucciones que los modifican lo hacen un excelente
debugger para programas en runtime
, y como ya he experimentado
previamente con esta maravillosa herramienta, era tiempo de comenzar a jugar
con algún objetivo, y en este caso fue el Administrador de Tareas.
El objetivo de este post no es mostrar el proceso de Cheat Engine, así
que eso queda como ejercicio para el lector, lo importante a saber
es que usando Ghidra y Cheat Engine, encontré la función que actualiza
los valores de la carga de todos los componentes de visualización de
taskmgr.exe
, y también como se calcula el índice de este arreglo,
con esto ya teníamos suficiente conocimiento para escribir nuestro interceptor
.
Linking is your friend!
Como ahora estamos trabajando con hacking interno, el proceso de linking
de compilación se vuelve mucho más relevante, ya que estamos en el mismo
espacio de memoria que el proceso objetivo, por lo tanto
podemos compartir variables usando las foreign function interface (FFI).
; interceptor.asm
EXTERN _end: qword
EXTERN my_arr: qword
PUBLIC get_values
.code
get_values PROC
lea rax,[my_arr]
; rdx contiene el indice del arreglo, y rax es el puntero base
; de nuestro propio arreglo en este caso, luego
; es reemplazado por el valor correspondiente del arreglo
mov rax,[rax + rdx*8 + 08]
; original code
; pero ahora rax contiene nuestro propio valor
mov [rcx + rdx*8 + 08],rax
jmp [_end]
get_values ENDP
END
El interceptor en este caso es bastante cortito, pero hay algo que no había
usado previamente, el keyword EXTERN
.
En el caso del hacking externo, el interceptor
se inyectaba en el espacio
de memoria del objetivo a través de las llamadas de la API del sistema operativo
como un arreglo de bytes. Ahora no es necesario hacer esto, ya que al compartir
el espacio de memoria, y Rust al hacer el linking automático del interceptor,
este queda automáticamente cargado en la memoria del proceso, por lo tanto nos
ahorramos ese paso.
Lo interesante es, como ahora está trabajando todo en el mismo espacio de
memoria (perdonen la redundancia), se pueden compartir variables.
En este caso, el código en assembly reconoce 2 variables externas
las cuales llamamos _end
, y my_arr
, variables que definiremos
en nuestro código en Rust
y cargo
se encargará de hacer el linking por
ti (hasta nunca Makefile).
Lo entretenido de esto es que ahora será mucho más fácil y directo
manipular el arreglo y que además, será fácil conseguir la dirección
de regreso del jump (_end
).
Manipulando las cosas desde Rust
/// Handy macro para exportar las variables facilmente,
/// es equivalente al extern pero para que Rust genere
/// símbolos públicos en vez de leerlos
macro_rules! export_var {
($($name:ident: $v:ty = $val:expr),*) => {
$(#[no_mangle] pub static mut $name: $v = $val;)*
}
}
export_var!{
_end: usize = 0,
my_arr: [f64; 120] = [0f64; 120]
}
// Dirección de la función en assembly para ser inyectada
extern "C" {
static get_values: u8;
}
Como se puede ver, a diferencia del Photo Mode, aquí también estamos
exportando variables (con la macro creada) para que en el momento en que
el compilador haga el linking
, le pase esas variables al código en assembly.
Con esto, será mucho más fácil manipular el arreglo para hacer cosas bonitas, como en el caso nuestro, generar una curva sinusoidal (probablemente se puede hacer desde assembly puro, pero para qué jaja)
Sin más preámbulo, el código principal en Rust quedó algo así
#[no_mangle]
pub unsafe extern "system" fn intercept_input(_: LPVOID) -> DWORD {
use winapi::um;
// El DLL encargado de generar los graficos para el taskmgr es
// chartv.dll
let _name = CString::new("CHARTV.dll").unwrap();
let mba = um::libloaderapi::GetModuleHandleA(_name.as_ptr()) as usize;
// Offset especifico donde haremos nuestra inyeccion
let target_addr = mba + 0x312E;
unsafe {
// actualizamos _end para que nuestro codigo en assembly
// sepa donde retornar despues de la inyección
_end = target_addr + 5;
// inyectamos en nuestro offset la instruccion en assembly,
// notar que hook_fun ya no necesita obtener los valores
// de la funcion como arreglo de bytes ya que esta esta
// en el mismo espacio de memoria
hook_fun(target_addr, &get_values as *const u8, 5);
}
// Dirty math para crear la sinusoidal
let mut t = 0f64;
loop {
for i in 0..my_arr.len() {
let _i = (i as f64)/10f64;
my_arr[i] = 50f64*(1f64 + (t + _i).sin())
}
t += 1e-5;
if t > 2.*3.14 {
t = 0f64;
}
}
return 1;
}
// Boilerplate que necesita Windows para instanciar nuestro DLL
#[no_mangle]
#[allow(non_snake_case)]
pub extern "system" fn DllMain(_: HINSTANCE, reason: DWORD, _: LPVOID) -> BOOL {
unsafe {
match reason {
winapi::um::winnt::DLL_PROCESS_ATTACH => {
winapi::um::processthreadsapi::CreateThread(
ptr::null_mut(),
0,
Some(intercept_input),
ptr::null_mut(),
0,
ptr::null_mut(),
);
}
_ => (),
};
}
return true as BOOL;
}
Conclusión
Existen demasiadas ventajas al hacer hacking interno, tantas que
probablemente de ahora en adelante me enfoque en hacer este tipo de hacking
para mis futuros Photo Mode
en distintos juegos. Fue un ejercicio entretenido
para aprender un poco sobre linking, FFI, y los DLL de Windows, y como ha
sido desde el principio del 2020, usar Rust ha sido un viaje muy entretenido.
Espero que como lector hayas podido aprender algo, si no, siempre puedes dejar un feedback de la escritura del post.
Adjunto otra función bonita graficada en el Administrador de Tareas también:
Y puedes leer el código fuente original acá.