Novedades

Guia Comparativa: JMP vs CALL, PUSH y RET en CodeCave



c0de

Administrador
Desde
19 Abr 2020
Mensajes
329
1587858464788.png
¿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).

1587858553197.png

¿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?

Por favor, Acceder o Regístrate ¡para ver el contenido de la cita!

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

1587858801712.png

1587858817345.png

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]:

1587876967894.png

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.

Por favor, Acceder o Regístrate ¡para ver el contenido de la cita!

Necesitamos encontrar el desplazamiento que hay entre la instrucción que disminuye la vida hasta nuestra rutina.

El código que resuelve esto:

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Siendo lpMemory la variable que contiene la dirección donde comienza nuestra rutina.

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Calculamos el desplazamiento que hay de la dirección, 004226BCh hacia la dirección contenida en lpMemory, el desplazamiento lo tenemos en EDX.

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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:

1587877247266.png

Lo primero que hacemos es reservar memoria donde podamos escribir; el puntero hacia dicho lugar en memoria lo contiene la variable lpMemory.

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Esto lo hacemos por seguridad ya que no todas las páginas de memoria tienen permiso de lectura o escritura.

1587877511339.png

Explicado en el Paso2.

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Pasamos el puntero de nuestro espacio de memoria reservada a EDX.

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Cambiamos los primeros opcodes FF 8B a 83 83, siendo los opcodes 83 83 = add [ebx…….

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

Seguimos armando la instrucción dándole el offset 00000454, y tendríamos: add [ebx + 00000454]

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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.

1587880345718.png

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++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

La siguiente dirección que se ejecutaría como si nada hubiera pasado:

1587882720846.png

PRIMERA PARTE - Resumen

En resumen esto fue lo que hicimos:

C++:
Por favor, Acceder o Regístrate para ver el contenido de los códigos!

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

1587882933851.png

1587885331810.png

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.

1587885353154.png

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

1587885455138.png


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:

1587885562008.png

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.

1587885638242.png

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
 
Última edición:
Atrás
Arriba