¿Qué es “CodeCave”?
Una técnica muy usada en el Game Hacking, un CodeCave, se describe literalmente a su traducción, es hacer un hueco, túnel o cueva dentro del código normal, cambiando el flujo, hacia una rutina arbitraria, de esa manera se modifica el compartimiento normal, de una aplicación (en la demostración).
¿Qué veremos en esta charla?
Dos formas de hacer un CodeCave, la primera usando opcodes y reservando espacio en la memoria, esto puede ser muy tedioso si tenemos un código largo, la segunda es la manera fácil será saltar hacia nuestro propio espacio (para usar instrucciones), donde tendremos la rutina arbitraria.
Planteando el Problema
Ya mencionamos cómo funciona el CodeCave, pero, ¿en qué momento debemos usarlo?
Pasos Básicos
1er Paso: Obtener la dirección indicada que modificará el flujo hacía una rutina creada por nosotros.
2do Paso: Calcular el desplazamiento desde donde se hará el cambio de flujo, hasta nuestra rutina.
3er Paso: Crear la rutina que modificará el comportamiento arbitrariamente.
4to Paso: Regresar al flujo, como normalmente lo haría la aplicación, como si nada hubiera pasado.
Funcionamiento del CodeCave
En pocas palabras si por alguna acción la vida disminuye en 1, con el CodeCave podemos modificar el flujo, hacer que valla hacia nuestra rutina y en vez de disminuir, incrementará en 2 la vida, esto es en pocas palabras, el concepto del CodeCave.
PRIMERA PARTE (usando opcodes y reservando espacio)
1er Paso. Obtener la dirección indicada.
Primero necesitamos encontrar la dirección que disminuya la vida en 1. Para esto podemos usar un debugger cualquiera, o para mayor facilidad el CheatEngine (desde ahora CE), que fue concebido para estas labores.
1er Paso. Dirección/Address
Usaremos está instrucción ficticia y diremos que la instrucción que disminuye la vida es dec [ebx+00000454]:
Y Nuestro objetivo es cambiar la instrucción dec [ebx+00000454] (es la cual decrementa en 1 la vida) por add [ebx+00000454], 2 (es la cual sumará en 2 la vida).
2do Paso. Calculando el Desplazamiento
2do Paso. Calcular el desplazamiento hacia nuestra rutina arbitraria.
Necesitamos encontrar el desplazamiento que hay entre la instrucción que disminuye la vida hasta nuestra rutina.
El código que resuelve esto:
Siendo lpMemory la variable que contiene la dirección donde comienza nuestra rutina.
Calculamos el desplazamiento que hay de la dirección, 004226BCh hacia la dirección contenida en lpMemory, el desplazamiento lo tenemos en EDX.
Restamos los 5 bytes del JMP (JUMP, es el salto que va a cambiar el flujo hacia nuestra rutina) esta ocupa 5 bytes y la tenemos que restar. EDX = Desplazamiento u Offset.
3er Paso. Rutina
3er Paso: Crear la rutina que modificará el comportamiento arbitrariamente.
Esta Rutina contiene tanto la rutina que cambia una instrucción, así como el código que reserva espacio del HEAP, lo cual veremos en el siguiente código:
Lo primero que hacemos es reservar memoria donde podamos escribir; el puntero hacia dicho lugar en memoria lo contiene la variable lpMemory.
Especificamos el flag GMEM_MOVEABLE, este deja a Windows mover el bloque de memoria, para consolidar la memoria. La bandera (flag) GMEM_ZEROINIT le dice a GlobalAlloc que rellene el nuevo bloque de memoria reservado con ceros.
Cuando GlobalAlloc vuelve satisfactoriamente, EAX contiene el manejador (handle) al bloque de memoria reservado. Le pasamos este manejador (handle) a la función GlobalLock que nos devuelve un puntero al bloque de memoria.
Esto lo hacemos por seguridad ya que no todas las páginas de memoria tienen permiso de lectura o escritura.
Explicado en el Paso2.
A partir de aquí estamos modificando la instrucción dec [ebx + 00000454], por un JMP [lpMemory].
0E9h (Opcode) = JMP
EDX = Desplazamiento hacia la dirección allocada ([lpMemory]).
Como la instrucción dec [ebx + 00000454], tiene de tamaño 6 bytes, el JMP 5 bytes, hay una que sobra y esa la NOPeo.
Pasamos el puntero de nuestro espacio de memoria reservada a EDX.
Cambiamos los primeros opcodes FF 8B a 83 83, siendo los opcodes 83 83 = add [ebx…….
Seguimos armando la instrucción dándole el offset 00000454, y tendríamos: add [ebx + 00000454]
Esta última instrucción es el valor con que será sumando el primero operando, con esto tendríamos la instrucción de esta manera: add [ebx + 00000454], 2
Cambió la instrucción : dec [ebx + 00000454] por add [ebx + 00000454], 2
Muchos dirán porque ha puesto 00000454 del offset y no 54040000, y así para los demás opcodes, pues fácil “LittleEndian” tiene la culpa, es el formato en que se almacenan los datos, en palabras fáciles lo que hace el microprocesador es almacenar los valores pero al revés (Little Endian es ese formato).
Ejemplo: 004226BC - FF 8B 54040000 - dec [ebx+00000454] Donde el offset que ha sido almacenado es 54040000. Entonces si yo muevo el valor de 00000454, lo que hace el microprocesador es voltear ese valor a 54040000, y como estamos trabajando con opcodes, será interpretado como 00000454.
4to Paso. Regresando el Flujo
4to Paso: Regresar al flujo, como normalmente lo haría la aplicación, como si nada hubiera pasado.
Calculando el tamaño que ocupamos en la rutina arbitraria, obtendremos el desplazamiento desde el final del código arbitrario, hasta la address que ejecutaría normalmente el GAME.
Ocupamos 7 bytes en la anterior instrucción, y tenemos que sumarle 5 bytes del JMP del regreso del flujo, en total 12 bytes usaremos y la dirección final de la rutina está en EAX. EAX = Dirección final de la rutina arbitraria.
La siguiente dirección que se ejecutaría como si nada hubiera pasado:
PRIMERA PARTE - Resumen
En resumen esto fue lo que hicimos:
Hicimos un JMP, hacia nuestra rutina (CodeCave), ejecutamos la instrucción que modifica el comportamiento arbitrariamente, y regresamos el flujo, como si el comportamiento de la aplicación no hubiera cambiado, así nos evitamos del crash.
PRIMERA PARTE - Código
CODIGO
SEGUNDA PARTE (Usando nuestro propio espacio)
Después de hacer crear la primera parte, está segunda fue concebida al día siguiente luego de pensar porque hacer todo en opcodes y no usar mi propio espacio, quien sabe porque no se me ocurrió esto antes, o estaba tan absorto depurando el código anterior, que no tenía espacio para pensar en otra cosa, pero pensando que lo primero no es factible para códigos largos, así qué se concibió lo siguiente. Les pego el code, gran parte ya está explicado, los pasos son los mismos, lo único que cambia es el 3er Paso y 4to Paso, veamos como.
Segunda Parte - Código
3er Paso. Rutina
Lo que hacemos ya no es saltar a un espacio de memoria reservada desde el heap, por la api GlobalAlloc, si no saltar a nuestro propio espacio así nos evitamos de liar con opcodes e imaginarnos el dolor de cabeza si fuera un código largo.
Al usar nuestro propio espacio, podemos escribir la instrucción tal cual la deseamos.
¿Por qué pushad y popad?, Esto es por seguridad y buena costumbre ya que esto es un CodeCave y la rutina arbitraria que codeamos podría ser mucho más larga que esta, seguramente usaremos registros que necesitan preservar su valor inicial, y seguramente esta aplicación daría error si no hubiera preservado sus registros.
La idea es dejar intactos todos los registros y aunque solo usamos EBX. Ahora, ¿por qué esta en comentarios?, es que en los microprocesadores x64 los mnemónicos pushad y popad quedaron por decirlo así obsoletos, no son reconocidos ya que imagínense cuanto sería el tamaño que ocuparía en la pila, así que deberíamos usar otros métodos para preservar los registros que usemos.
4to Paso. Regresando el Flujo
Sí tanto hable de registros y preservarlos, ¿Por qué usa ESI, esto puede influir en algún error de la aplicación?, pudiera influir si antes no somos observadores, recordemos como está conformado las instrucciones ficticias que usamos para efecto del paper.
Esta vez ya no se calcula el desplazamiento dejamos ese trabajo al microprocesador hacienda esto:
A ESI, se mueve algún valor, así que no importa el valor de ESI en la dirección 004226BCh, porque en la siguiente instrucción va hacer modificada, así que lo usamos.
Este comentario, nos da otra idea de cómo hacer que salte hacia el flujo normal de la aplicación, por si por alguna razón no se modifica algún registro y necesitamos otra forma de regresar. Al estilo de un CALL, un PUSH y un RET, es una técnica muy usada y nos da una buena alternativa. Ocupa 6 bytes, un byte más que al usar el JMP.
Descarga en documento PDF
Creditos
Nox
Una técnica muy usada en el Game Hacking, un CodeCave, se describe literalmente a su traducción, es hacer un hueco, túnel o cueva dentro del código normal, cambiando el flujo, hacia una rutina arbitraria, de esa manera se modifica el compartimiento normal, de una aplicación (en la demostración).

¿Qué veremos en esta charla?
Dos formas de hacer un CodeCave, la primera usando opcodes y reservando espacio en la memoria, esto puede ser muy tedioso si tenemos un código largo, la segunda es la manera fácil será saltar hacia nuestro propio espacio (para usar instrucciones), donde tendremos la rutina arbitraria.
Planteando el Problema
Ya mencionamos cómo funciona el CodeCave, pero, ¿en qué momento debemos usarlo?
Please, Acceder or Registrarse to view quote content!
Pasos Básicos
1er Paso: Obtener la dirección indicada que modificará el flujo hacía una rutina creada por nosotros.
2do Paso: Calcular el desplazamiento desde donde se hará el cambio de flujo, hasta nuestra rutina.
3er Paso: Crear la rutina que modificará el comportamiento arbitrariamente.
4to Paso: Regresar al flujo, como normalmente lo haría la aplicación, como si nada hubiera pasado.
Funcionamiento del CodeCave


En pocas palabras si por alguna acción la vida disminuye en 1, con el CodeCave podemos modificar el flujo, hacer que valla hacia nuestra rutina y en vez de disminuir, incrementará en 2 la vida, esto es en pocas palabras, el concepto del CodeCave.
PRIMERA PARTE (usando opcodes y reservando espacio)
1er Paso. Obtener la dirección indicada.
Primero necesitamos encontrar la dirección que disminuya la vida en 1. Para esto podemos usar un debugger cualquiera, o para mayor facilidad el CheatEngine (desde ahora CE), que fue concebido para estas labores.
1er Paso. Dirección/Address
Usaremos está instrucción ficticia y diremos que la instrucción que disminuye la vida es dec [ebx+00000454]:

Y Nuestro objetivo es cambiar la instrucción dec [ebx+00000454] (es la cual decrementa en 1 la vida) por add [ebx+00000454], 2 (es la cual sumará en 2 la vida).
2do Paso. Calculando el Desplazamiento
2do Paso. Calcular el desplazamiento hacia nuestra rutina arbitraria.
Please, Acceder or Registrarse to view quote content!
Necesitamos encontrar el desplazamiento que hay entre la instrucción que disminuye la vida hasta nuestra rutina.
El código que resuelve esto:
C++:
Please,
Acceder
or
Registrarse to view codes content!
Siendo lpMemory la variable que contiene la dirección donde comienza nuestra rutina.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Calculamos el desplazamiento que hay de la dirección, 004226BCh hacia la dirección contenida en lpMemory, el desplazamiento lo tenemos en EDX.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Restamos los 5 bytes del JMP (JUMP, es el salto que va a cambiar el flujo hacia nuestra rutina) esta ocupa 5 bytes y la tenemos que restar. EDX = Desplazamiento u Offset.
3er Paso. Rutina
3er Paso: Crear la rutina que modificará el comportamiento arbitrariamente.
Esta Rutina contiene tanto la rutina que cambia una instrucción, así como el código que reserva espacio del HEAP, lo cual veremos en el siguiente código:

Lo primero que hacemos es reservar memoria donde podamos escribir; el puntero hacia dicho lugar en memoria lo contiene la variable lpMemory.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Especificamos el flag GMEM_MOVEABLE, este deja a Windows mover el bloque de memoria, para consolidar la memoria. La bandera (flag) GMEM_ZEROINIT le dice a GlobalAlloc que rellene el nuevo bloque de memoria reservado con ceros.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Cuando GlobalAlloc vuelve satisfactoriamente, EAX contiene el manejador (handle) al bloque de memoria reservado. Le pasamos este manejador (handle) a la función GlobalLock que nos devuelve un puntero al bloque de memoria.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Esto lo hacemos por seguridad ya que no todas las páginas de memoria tienen permiso de lectura o escritura.

Explicado en el Paso2.
C++:
Please,
Acceder
or
Registrarse to view codes content!
A partir de aquí estamos modificando la instrucción dec [ebx + 00000454], por un JMP [lpMemory].
0E9h (Opcode) = JMP
EDX = Desplazamiento hacia la dirección allocada ([lpMemory]).
C++:
Please,
Acceder
or
Registrarse to view codes content!
Como la instrucción dec [ebx + 00000454], tiene de tamaño 6 bytes, el JMP 5 bytes, hay una que sobra y esa la NOPeo.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Pasamos el puntero de nuestro espacio de memoria reservada a EDX.
C++:
Please,
Acceder
or
Registrarse to view codes content!
Cambiamos los primeros opcodes FF 8B a 83 83, siendo los opcodes 83 83 = add [ebx…….
C++:
Please,
Acceder
or
Registrarse to view codes content!
Seguimos armando la instrucción dándole el offset 00000454, y tendríamos: add [ebx + 00000454]
C++:
Please,
Acceder
or
Registrarse to view codes content!
Esta última instrucción es el valor con que será sumando el primero operando, con esto tendríamos la instrucción de esta manera: add [ebx + 00000454], 2
Cambió la instrucción : dec [ebx + 00000454] por add [ebx + 00000454], 2
Muchos dirán porque ha puesto 00000454 del offset y no 54040000, y así para los demás opcodes, pues fácil “LittleEndian” tiene la culpa, es el formato en que se almacenan los datos, en palabras fáciles lo que hace el microprocesador es almacenar los valores pero al revés (Little Endian es ese formato).
Ejemplo: 004226BC - FF 8B 54040000 - dec [ebx+00000454] Donde el offset que ha sido almacenado es 54040000. Entonces si yo muevo el valor de 00000454, lo que hace el microprocesador es voltear ese valor a 54040000, y como estamos trabajando con opcodes, será interpretado como 00000454.
4to Paso. Regresando el Flujo
4to Paso: Regresar al flujo, como normalmente lo haría la aplicación, como si nada hubiera pasado.
Calculando el tamaño que ocupamos en la rutina arbitraria, obtendremos el desplazamiento desde el final del código arbitrario, hasta la address que ejecutaría normalmente el GAME.

Ocupamos 7 bytes en la anterior instrucción, y tenemos que sumarle 5 bytes del JMP del regreso del flujo, en total 12 bytes usaremos y la dirección final de la rutina está en EAX. EAX = Dirección final de la rutina arbitraria.
C++:
Please,
Acceder
or
Registrarse to view codes content!
La siguiente dirección que se ejecutaría como si nada hubiera pasado:

PRIMERA PARTE - Resumen
En resumen esto fue lo que hicimos:
C++:
Please,
Acceder
or
Registrarse to view codes content!
Hicimos un JMP, hacia nuestra rutina (CodeCave), ejecutamos la instrucción que modifica el comportamiento arbitrariamente, y regresamos el flujo, como si el comportamiento de la aplicación no hubiera cambiado, así nos evitamos del crash.
PRIMERA PARTE - Código
CODIGO
SEGUNDA PARTE (Usando nuestro propio espacio)
Después de hacer crear la primera parte, está segunda fue concebida al día siguiente luego de pensar porque hacer todo en opcodes y no usar mi propio espacio, quien sabe porque no se me ocurrió esto antes, o estaba tan absorto depurando el código anterior, que no tenía espacio para pensar en otra cosa, pero pensando que lo primero no es factible para códigos largos, así qué se concibió lo siguiente. Les pego el code, gran parte ya está explicado, los pasos son los mismos, lo único que cambia es el 3er Paso y 4to Paso, veamos como.
Segunda Parte - Código


3er Paso. Rutina
Lo que hacemos ya no es saltar a un espacio de memoria reservada desde el heap, por la api GlobalAlloc, si no saltar a nuestro propio espacio así nos evitamos de liar con opcodes e imaginarnos el dolor de cabeza si fuera un código largo.

Al usar nuestro propio espacio, podemos escribir la instrucción tal cual la deseamos.
¿Por qué pushad y popad?, Esto es por seguridad y buena costumbre ya que esto es un CodeCave y la rutina arbitraria que codeamos podría ser mucho más larga que esta, seguramente usaremos registros que necesitan preservar su valor inicial, y seguramente esta aplicación daría error si no hubiera preservado sus registros.
La idea es dejar intactos todos los registros y aunque solo usamos EBX. Ahora, ¿por qué esta en comentarios?, es que en los microprocesadores x64 los mnemónicos pushad y popad quedaron por decirlo así obsoletos, no son reconocidos ya que imagínense cuanto sería el tamaño que ocuparía en la pila, así que deberíamos usar otros métodos para preservar los registros que usemos.
4to Paso. Regresando el Flujo

Sí tanto hable de registros y preservarlos, ¿Por qué usa ESI, esto puede influir en algún error de la aplicación?, pudiera influir si antes no somos observadores, recordemos como está conformado las instrucciones ficticias que usamos para efecto del paper.
Esta vez ya no se calcula el desplazamiento dejamos ese trabajo al microprocesador hacienda esto:

A ESI, se mueve algún valor, así que no importa el valor de ESI en la dirección 004226BCh, porque en la siguiente instrucción va hacer modificada, así que lo usamos.

Este comentario, nos da otra idea de cómo hacer que salte hacia el flujo normal de la aplicación, por si por alguna razón no se modifica algún registro y necesitamos otra forma de regresar. Al estilo de un CALL, un PUSH y un RET, es una técnica muy usada y nos da una buena alternativa. Ocupa 6 bytes, un byte más que al usar el JMP.
Descarga en documento PDF
Please,
Acceder
or
Registrarse
to view URLs content!
Creditos
Nox
Última edición: