Karpoff Spanish Tutor 2001

Programa:

USOS Y ABUSOS DE LA PILA EN SESIONES DE DEPURACIÓN


AUTOR Black Fenix   FECHA: 23/03/01

 

 INTRODUCCION

"La pila es un espacio de memoria reservado para la aplicación, en este lugar se almacenan los parámetros que se pasarán a las llamadas (CALLS) y las variables locales de estas. Su funcionamiento es muy simple, el último argumento que se almacena en la pila es el primero en salir, esto es lo que se llama una pila FIFO (First In First Out). Básicamente tenemos dos instrucciones que se encargan de sacar y empujar parámetros en la pila: PUSH (EMPUJAR) y POP (SACAR), existe alguna más pero van siempre precedidos del prefijo PUSH o POP. Los parámetros que se suelen empujar a la pila suelen ser direcciones de memória (punteros) o valores inmediatos. Se puede manejar la pila manualmente incrementando o decrementando los registros de 32 bits ESP (extended stack pointer) y EBP (extended stack base pointer), ESP siempre es un puntero a la cima de la pila (último parámetro empujado a la pila), y EBP es un puntero la la base de la pila (primer argumento que se empujo a la pila). EBP y ESP se combinan con el registro SS (stack segment para 16bits o stack selector en 32bits)."

 

La PILA

Para que podemos necesitar la pila en una sesión de trazado ?, pues simplemente para saber que parámetros se están pasando a determinada funcion (CALL) y así saber en que posición de la pila se encuentra para poder controlar las operaciones que se realizan sobre estos, a esto yo lo llamo stack tracking (seguimiento de la pila), pero para poder realizar este seguimiento debemos saber que no todas las funciones procesan los parámetros pasados de igual forma. Básicamente hay tres tipos de funciones que se pueden declarar:

_cdecl: Cuando se llama a una función declarada como _cdecl, esta función NO restaura el estado de la pila al regresar, y debemos realizar nosotros el reajuste manualmente. Veamos un ejemplo de función declarada como _cdecl:

void _cdecl MyFunc( char c, short s, int i, double f )
{
}

Para llamar a esta función haremos:

MyFunc('x',12,8192,2.7138);

El código en ensamblador sería así:

:00401030 push 4005B5DC // 2.71338 , el número decimal se pasa de dos veces
:00401035 push C63F1412 // 2.71338
:0040103A push 00002000 // 8192
:0040103F push 0000000C // 12
:00401041 push 00000078 // 'x'
:00401043 call 00401000
:00401048 add esp, 00000014 // reajustamos la pila, 5 argumentos * 4 dword cada uno = 20 = 14h
 

Todas las funciones declaradas como _cdecl, estarán precedidas de un guión bajo "_", p. ej: MyFunc quedaria _MyFunc. Una vez dentro de la función declarada como _cdecl, encontraremos los parámetros en las siguientes posiciones:

Con este esquema nos hacemos una idea de donde están los parámetros pasados, así como la dirección de retorno, que siempre es una dato muy interesante, sobre todo para localizar la dirección de memória desde donde se llamó a la función declarada como _cdecl. Otro dato importante es saber que los registros ECX y EDX no son utilizados por la función, por lo que los valores que contengan resultan irrelevantes. Como puede apreciarse en la cima de la pila se guarda la dirección de retorno que ocupa 4bytes (un DWORD) y a partir de aquí los parámetros. Es importante saber que al final de una función declarada como _cdecl, siempre encontraremos un ret sin parámetros (digo siempre que NO haya sido programado en ensamblador).

Para ver en SoftIce los parámetros, hariamos:

>? *esp
>? *(esp+4)

y así sucesivamente, a veces resulta más práctico utilizar los chivatos (WATCHES):

>watch *esp
>watch *(esp+4)

Si alguno de los parámetros fuera una dirección de memória que contiene un buffer o una cadena, podríamos ver su contenido con:

>d *(esp+desplazamiento)

Esto es de especial utilidad para pescar cadenas que puedan contener numeros de serie y bueno ya sabes ;)

Veamos ahora el segundo tipo de declaración:

_stdcall y thiscall (solo C++) : Cuando se llama a una función declarada como _stdcall, esta función restaura el estado de la pila antes de regresar, y nosotros no deberemos realizar ningún reajuste. Veamos el código en C y ensamblador:

void _stdcall MyFuncStdCall( char c, short s, int i, double f )
{
}

LLamariamos a la rutina con:

MyFuncStdCall('x',12,8192,2.7138);

Si desensanblasemos el código encontrariamos:

:0040104B push 4005B5DC // 2.71338
:00401050 push C63F1412 // 2.71338
:00401055 push 00002000 // 8192
:0040105A push 0000000C // 12
:0040105C push 00000078 // 'x'
:0040105E call 00401010 // llama a función declarada como _stdcall

Al final de la función llamada encontrariamos:
* Referenced by a CALL at Address: |:0040105E
:00401010 C21400 ret 0014 // reajusta pila automaticamente antes de regresar en el tamaño de los argumentos

Veamos ahora donde encontrariamos los parámetros:

Como podemos ver las posiciones de los parámetros son iguales que el tipo de llamada _cdecl, la única diferencia es que el registro ECX contiene el puntero a this, utilizado sólo en las funciones miembro de una clase de C++, el parámetro this es un puntero al objeto que llamó a su función miembro, sólo es válido para funciones miembro NO estáticas (static). El registro EDX no se utiliza y su contenido resulta irrelevante para la función. Es importante conocer que el modelo de llamada _stdcall es el utilizado por todas las funciones del API de Windows, y probablemente sea el más común de todos junto con el _cdecl. Una utilidad muy práctica es cuando ponemos un BPX sobre una función del API y queremos saber desde donde se llamó pero sin trazar el código hasta regresar, para obtener la dirección del código que llamó a la función del API, usando SoftIce:

> dd *esp

el primer dword que aparezca en la ventana de datos es la dirección que buscabamos, si ahora tecleamos

> what *esp

nos mostrará el módulo o ejecutable que llamó a esta función. Yo utilizo una macro que me muestra la dirección de retorno y los 4 primeros parámetros que se le pasan a la función, la definición de la macro en SoftIce es como sigue:

> macro getargs = "?*(esp);?*(esp+4);?*(esp+8);?*(esp+c);?*(esp+10);?*(esp+14)"

Ahora solo tenemos que teclear getargs para que la macro se ejecute. Va de fabula para hacer seguimientos de pila y tambien puede ser utilizada con las otras convenciones de llamada. Que os parece? Son o no son útiles las macros?

Veamos ahora el tercer tipo de declaración:

_fastcall: Cuando se llama a una función declarada como _fastcall, esta función pasa los parámetros a la función en los registros internos del procesador siempre que sea posible. Veamos la implementación en C.

void _fastcall MyFuncFastCall( char c, short s, int i, double f )
{
}

LLamariamos a la rutina con:

MyFuncFastCall('x',12,8192,2.7138);

Si desensamblaramos el código tendriamos:

:00401063 68DCB50540 push 4005B5DC // 2.71338
:00401068 6812143FC6 push C63F1412 // 2.71338
:0040106D 6800200000 push 00002000 // 8192
:00401072 BA0C000000 mov edx, 0000000C // 12, aquí se aprecia como se guarda en EDX y no en la pila
:00401077 B178 mov cl, 78 // lo msimo para el caracter 'X' pero en CL
:00401079 E8A2FFFFFF call 00401020 // llama a función declarada como _fastcall

Al final de la función llamada encontrariamos:
* Referenced by a CALL at Address: |:00401079
:00401020 C20C00 ret 000C // reajusta pila automáticamente, notese que solo se restaura el tamaño de los parámetros empujados

Veamos ahora donde encontrariamos los parámetros:

Como podemos ver las posiciones de los parámetros varian ya que se han utilizado los registros ECX y EDX para almacenar dos de los parámetros utilizados por la función.

Cabe mencionar también que según el compilador el prólogo de entrada y el epílogo de salida de una llamada puede variar, pero siempre se mantendrá la lógica básica de cada modelo de llamada.Aquí teneis el prólogo de entrada y el epílogo de salida de una función de 32bits típica en C:

push ebp ; Salva EBP
mov ebp, esp
; Establece la base de la pila en la cima, esto hará que los parámetros pasados sean accesibles usando EBP
sub esp, localbytes
; Reserva espacio para las variables locales
push <registers>
; Salva los registros oportunos

La variable localbytes representa el número de bytes necesarios en la pila para almacenar las variables locales, y la variable registers representa una lista de los registros que se deben salvar en la pila. Después de empujar los registros, ya se pueden añadir más datos a la pila. Notese que se copia el puntero a la cima de la pila en EBP (base de la pila), esto hará que los parámetros sean accesibles mediante el direccionamiento de EBP más el desplazamiento del parámetro que necesitemos saber. Por último tendremos el epílogo de la función:

pop <registers> ; Restaura registros
mov esp, ebp
; Restaura el puntero a la cima de la pila
pop ebp
; Restaura la base de la pila
ret
; y retorna

La pila siempre crece hacia abajo (desde direcciones de memoria altas a bajas), ya que la instrucción PUSH decrementa el ESP y POP lo incrementa. El puntero base (EBP) apunta al valor empujado de EBP. El area de variables locales comienza en EBP-2. Para acceder a cualquier variable local, calcula el desplazamiento desde EBP sustrayendo el valor apropiado de EBP.


Espero que con esto os sea más fácil trazar el código, un buen trazado de la pila puede suponer un gran ahorro de horas de trazado. Pues nada, poner a punto el SoftIce, preparad vuestros mejores BPM y BPX y a la caza.

You are inside Reversed Minds pages.

Por Black Fenix.


La información aquí vertida es exclusivamente para uso educacional, no puedo hacerme responsable del uso que se haga de esta, por lo que atiendo a la honradez de cada uno :), recuerda que debes comprar el software que utilices :)

 

 

 

Karpoff Spanish Tutor: Pagina dedicada a la divulgacion de informacion en Castellano, sobre Ingenieria Inversa y Programacion. Email "Colabora con tus Proyectos"
www.000webhost.com