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 y WriteProcessMemory).

  • 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á.