cRACKER's n0TES

Identificando Funciones, Argumentos, y Variables (Rhayader)


Éste es mi primer ensayo de desensamblado. Un documento bastante largo escrito para un newbie en listado muerto. Este ensayo no es un documento todo-en-uno. En este ensayo acometo sólo la identificación de funciones. Tuve la primera idea de escribir este ensayo, cuando vi el ensayo de 'The Sandman' sobre el listado muerto que encontré en "Invirtiendo Código Para Principiantes ". Si no lo has leído, te consejo que primero lo leas. Aunque el ensayo este dirigido hacia el listado muerto, alguna de la información también puede aplicarse a la aproximación viva. Espero que este ensayo te ayude en tu sesión desensamblando cualquier cosa ;) Y si tuvieras cualquier comentario, sugerencia, o si encuentras algún error en este ensayo, realmente apreciaría, que me lo mandaras por correo. De esa manera, podré revisar este ensayo para ofrecer un servicio mejor para todos nosotros.

Para mantenerlo simple, lo enfoqué favorablemente con un programa del lenguaje de programación 'C', que se ejecuta en entorno Win32. Hay dos razones, por lo qué hice esto. Primero, un programa Win32 escrito con el Lenguaje C será el programa más común que nos encontraremos y usaremos. Segundo, enfocando el modelo de memoria 32-bit, puedo evitar la complejidad del modelo de memoria segmentado.


Antes de entrar en la discusión sobre cómo una función del alto nivel se relaciona en el listado de Ensamblador, hablaremos primero de la pila y la instrucción de llamada. Después de todo, yo escribo este tutorial para un principiante en listado muerto. Si ya tuvieras bastante conocimiento sobre estos dos, puedes saltártelo, e ir a Identificando Función.

The Stack (La Pila)

La pila es un área de memoria usada por un programa para guardar variables temporales. Cuando empiezas un programa, el sistema operativo buscará un segmento de pila. Luego, él pondrá la dirección de segmento de pila en SS y (E)SP para apuntar al primer byte detrás del segmento de pila. Un compilador de lenguaje de alto nivel, también usaría este área para sostener algunos argumentos de la función y las variables locales.

La pila, tiene las propiedades de una pila de platos. Es decir, el último que pusiste, es el primero que tomas. Pero, a diferencia de una pila de platos que se sostiene muy bien por gravedad, la cima de la pila tiene una dirección más baja que la base de la pila. En otras palabras: la pila crece descendéntemente, como una pirámide. Cuando un valor se agrega a la pila, el indicador de la pila será disminuido.

También podrías querer saber, que hay dos maneras en las que un CPU almacena un valor en memoria.La CPU Intel es Little Endian (solo). Las palabras Little Endian se derivan de las palabras Little End In. Lo cuál significa, que el extremo pequeño (byte más bajo) de un valor multibyte se guardará primero. Por ejemplo, un valor de 0x1234, se guardará en memoria como 0x3412. De igual manera, un valor de 0x5678ABCD se guardará como 0xCDAB7856. Esta regla también se aplica a la pila. Normalmente, esto no será un problema. Pero, desde que nosotros pequeños crackers descarguemos la memoria dentro de nuestro debugger, necesitaremos saber esto y mentalmente poner en orden el valor que vemos en memoria.

Registros Relacionados con La pila

SS

Apunta al segmento de la pila del programa.

(E)SP

Indicador de la pila. Apunta al valor actual de la pila. Implícitamente cambiado por push, pop, call y ret.

(E)BP

Puntero base. Normalmente apunta al marco de pila actual para un procedimiento. Un compilador perfeccionado a veces puede usar (E)BP como un registro universal.

Instrucciones de la pila

Las instrucciones siguientes (no es una lista completa) permiten el uso de la pila para guardar o recuperar datos: PUSH, PUSHF (push flags), PUSHA (push all word register), PUSHAD (push all double word register), POP, POPF (pop flags), POPA (pop all word register), y POPAD (pop all double word register). Yo cubriré sólo la dos instrucción básica: PUSH y POP.

La sintaxis para PUSH es:


    PUSH fuente

La instrucción PUSH disminuirá el valor de (E)SP por el tamaño de la fuente, y copiará el valor de la fuente a [SS:(E)SP], borrando el valor anterior. En un programa 16-bit, la fuente debe ser un valor word. En programas 32-bit, la fuente puede ser word o dword. La fuente puede ser un registro (como EAX), una situación de memoria, o un valor inmediato.

La sintaxis para POP es:


    POP destino

La instrucción POP copiará (no moverá) el valor de SS:(E)SP, y entonces disminuirá (E)SP por el tamaño de destino. El destino puede ser un registro o una situación de memoria.

Observa cómo trabajan las dos instrucciones. Supón que tenemos ésta instrucción:


    push 1
    push esi
    push edi
    push 2
    ...
    pop  eax
    pop  edi
    pop  esi

Después de ejecutada toda la instrucción PUSH, la pila aparecerá así:

(valor de memoria más alto)
    higher memory value

                          1
                          esi
                          edi
             ESP ->    2
    lower memory value

(valor de memoria más bajo)

Y después de ejecutada toda la instrucción POP,

(valor de memoria más alto)
   higher memory value
                ESP ->    1
                          esi
                          edi
                             2
    lower memory value
(valor de memoria más bajo)

con,


    eax = 2
    esi = pushed esi
    edi = pushed edi

Espero que puedas seguir esta introducción rápida a la pila.


Identificando Funciones

Aunque los desensambladores modernos como W32DASM e IDA Pro marcarán una función, hay veces que no podrán encontrarlo. Por ejemplo una función que se dirige a través de un indicador. Por lo tanto, todavía necesitamos saber, cómo el compilador traduce una función del lenguaje de alto nivel en su equivalente en lenguaje ensamblador.

La Llamada ( CALL) y La instrucción RET

Muy básicamente, la instrucción call simplemente es un modo de transferir el control a otra parte de un programa. Simplemente como un jmp, o jcondicional. Pero a diferencia de un salto que es un traslado permanente de control, la llamada almacena la información de retorno, así cuando la rutina llamada termina por una instrucción de ret, el programa puede retroceder al "llamador" (parte del programa que realiza la llamada). Sabiendo esto, está claro para nosotros que el éxito de una instrucción de llamada depende de las mecánicas para guardar y recuperar la información de retorno. A menos que el objetivo de la llamada sea un TSS (task state segment) [ n. del t.: Estado de las tareas del segmento] o una tarea puerta, las mecánicas son bastante simples.

 

La rutina llamada vuelve a su llamador grabando la dirección de la instrucción de su llamador. La instrucción de llamada hace esto grabando el valor del registro (E)IP antes de que salte dentro de la rutina llamada. Como probablemente sabrás, (E)IP apunta a la instrucción que el CPU quiere ejecutar. Simplemente cambiando el registro (E)IP, puedes cambiar la manera en la que un programa se ejecutará. La instrucción de llamada graba este valor empujándolo hacia la pila. Ya que (E)IP se incrementa en cuanto una instrucción se saque, el valor que se empuja hacia la pila es el valor de (E)IP de la instrucción que sigue a la instrucción de llamada. En un programa 32-bit, toda llamada es una llamada cercana. En un programa 16-bit, donde a veces un traslado de intrasegmento se requiere, el programa necesita usar una llamada lejana. En una llamada lejana, el valor de CS:(E)IP necesita ser grabado. El registro de CS es empujado primero, entonces el valor de (E)IP es empujado sobre la pila. Cuando una rutina llamada se termina, la instrucción ret quitará el valor de (E)IP (y CS para una llamada lejana) dentro de (E)IP (y registro CS).

La sintaxis de la llamada es muy simple. Sin tener en cuenta el objetivo, la sintaxis es la misma:


    call address

 (Llamada dirección)

Un problema ocurre, cuando la llamada está usando una dirección indirecta. Por ejemplo, call edi o, call [ebx+0Dh]. En el primer caso, nosotros podemos buscar la instrucción que asigna edi en una dirección antes de la instrucción de llamada. Normalmente ésta será una dirección del API de Windows. Para el segundo caso, puedes usar una aproximación viva (usando debugger). Pon un breakpoint allí, y averigua el valor de ebx. O, una manera mejor, es equiparse con el conocimiento necesario para encontrarlo. Fravia+ ya escribió un ensayo excelente sobre la tabla de relocalización de llamada. Léelo. Todo lo que yo puedo agregar es, con IDA Pro, puedes utilizar su función Search In Core para buscar la tabla de relocalización de llamada. Hay veces, por supuesto, cuando el debugger es la única manera de buscarlo. Es casi imposible para nosotros encontrar a donde está refiriéndose la dirección en un programa de C++ que tiene toneladas de direcciones indirectas. Aquí es donde W32DASM tienen una ventaja sobre IDA Pro. Con su establecimiento en debugger, encontrar la dirección serán muy rápido.

Si una instrucción de llamada identifica un punto de entrada de función, entonces tendrá sentido para nosotros si la instrucción ret identifica un punto de retorno de función. Básicamente, la instrucción de ret devolverá al llamador colocando el apropiado (E)IP (y CS para una llamada lejana). Dependiendo del tipo de la llamada (y del objetivo), los ret pueden ser retn (retorno cercano), retf (retorno lejano), o iret (involucrando conmutación de tareas ). Una instrucción de ret sólo puede aparecer sin el sufijo n o f si se escribe dentro de un proc director. Buenos desensambladores, como IDA Pro que desmontan correctamente un programa en su código en ensamblador pondrá normalmente una instrucción ret dentro de un proc director. Entonces, es el tipo del proc (cercano o lejano) quien define el tipo de ret.

La instrucción ret puede especificar un parámetro numérico que identificará cuántos bytes deben quitarse de la pila después de que se ha hecho saltar la dirección de retorno. Si ves un código así:


    some_procedure proc near
        ...
        ...
        pop  edi
        pop  esi
        add  esp, 10h
        pop  bp
        ret  8
    SOME procedure endp

Asumiendo que some_procedure ha quitado correctamente todos los valores que puso a la pila, el estado del llamador será así:


    ...                                         ; esp apunta a xxxxxxxx
    push 32bit_variable                  ; esp apunta a 32bit_variable
    push another_32bit_variable      ; esp apunta a another_32bit_variable
    call some_procedure
    inc edi		                      ; la instrucción de ret nos traerá aquí
                                                ; y el puntero esp retrocede a xxxxxxxx
    ...

Determinar una salida de la función puede ser más complicado. Si el compilador perfeccionado esta encendido, puede haber varios lugares donde la función hace un ret a su llamador. Normalmente, siempre que una función termine, habrá un comienzo de otra función. Podemos verificar donde acaba una función, aun cuando tenga ret múltiples, mirando la instrucción que sigue a la instrucción ret. Cuando el desensamblador puede marcar la próxima función correctamente, esto probablemente no será un problema. Pero en caso de que él no lo marque apropiadamente, tendremos que buscar algo que se parezca a un prólogo de la función.

La función Prólogo y Epílogo

El prólogo normal generado por un compilador, será alguna variación de éstos:

Expresado en lenguaje ensamblador, se parecerá a esto (32-bit):


    push ebp                 ; Guarda el marco de EBP del llamador
    mov  ebp, esp          ; Prepara nuevo marco de EBP
    sub  esp, xxxx          ; xxxx es el número de bytes necesario para la variable local
    push esi	       ; Guarda el registro del llamador
    push edi
    ...

En un programas de C/C++, la función llamada debe conservar el siguiente registro: (E)SI, (E)DI, (E)BP, (E)SP, CS, DS, SS. Es decir, la función llamada no podría cambiar el valor del registro anterior. Si la función llamada necesitara éstos registros (ESI y EDI serán los más empleados normalmente), debe guardarlos primero dentro de la pila. Sin embargo, la función llamada puede usar: (E)AX, (E)BX, (E)CX, (E)DX, y ES libremente, sin la necesidad de guardarlos primero. El compilador los guardara en el prólogo.

 

Nota sobre ENTER y LEAVE:

Dos instrucciones especiales se agregaron en los 80286 y en procesadores posteriores acomodados a lenguajes de alto nivel que requieren un marco de pila al llamar subrutinas: ENTER y LEAVE. Si los 80286 o posteriores están habilitados, el compilador puede optar por usar esta instrucción. Busca:


    enter xxxx, 0h   ; xxxx es el número de bytes necesitado por la variable local
    push  esi
    push  edi
    ...


El número 0h, en la instrucción ENTER, es el número del nivel. En nivel 0, enter creará un marco de pila siguiendo estos pasos:


    push ebp
    mov  ebp, esp
    sub  esp, xxxx

Si el nivel esta sobre 0, ENTER guardará el (E)BP padre primero como un vínculo posterior, y el nivel superior (E)BP en orden, y acabando con el actual (E)BP. Esto hace fácil para un programa alcanzar la variable del nivel más alto. Cuando la función busca la variable de nivel más alto, busca un código similar a esto:


  mov esi, [ebp-4]                ; Obtiene el nivel más alto (E)BP.
                                        ; El próximo nivel (E)BP es [ebp-8].
  mov eax, ss:[esi-8]            ; Obtiene la primera variable de nivel más alto.
                                        ; Si la primera variable es valor 32-bit, la segunda
                                        ; variable estará en [ebp-C]

Es bastante raro encontrar un programa que usa ENTER en un nivel sobre 0. Un programa que este compilado con Clarión probablemente sea una excepción a esto. Como puedes ver por la descripción anterior, ENTER con nivel sobre 0 requiere más espacio en el marco de pila.

La instrucción LEAVE eliminara simplemente el marco de pila actual de la pila, restaurando el anterior (E)BP y (E)SP.

Es esencial que todos los valores puestos sean limpiados antes del retorno de la función a su llamador. De lo contrario, la instrucción ret hará saltar un valor equivocado a (E)IP. Por esta razón, una función epílogo se parecerá mucho al código prólogo. Sólo esta vez, hará lo inverso de lo que el prólogo hizo. Para nuestro ejemplo anterior, el código podría parecerse a esto:


    ....
    pop  edi
    pop  esi
    add  esp, xxxx
    pop  ebp
    ret

o, utilizando la instrucción LEAVE:


    ....
    pop  edi
    pop  esi
    leave
    ret

El código escrito anteriormente es de golpe un tipo completo, prólogo y epílogo. En el mundo real, alguno puede faltar. Si el programa no necesita ninguna variable en absoluto, el compilador puede que no se moleste en configurar un marco de pila. En un programa 32-bit, incluso cuando una función utiliza una variable local, el compilador aun podría no configurar un marco de pila en ebp. El modo de direccionar de 32-bit le permite al compilador usar ESP para dirigir argumentos y variables locales. Y, por supuesto, siempre es una posibilidad que el programa este escrito con lenguaje ensamblador. Con este lenguaje, todas las apuestas están cerradas. El programador tiene control completo.

Función llamada por MFC macro de trazado del mensaje
(Función called by MFC message maps macro)

Este material probablemente no es para alguien que sólo empieza su aventura en listado muerto. Sin embargo, puesto que muchos de los programa de estos días están usando MFC, y varios compiladores ya lo soportan (incluso Watcom y Symantec), yo pienso que es una cosa importante de saber. Puedes saltarte esto si quieres.

Hay varias maneras para encontrar la información. Usando un editor hexadecimal, un "dumpeador" para descargar la sección de .rdata, o, puedes usar la función Search In Core de IDA pro. También ayudara, si entiendes lo que es un trazado del mensaje. No lo discutiré aquí. Si no eres programador de MFC, puedes leer sobre eso en este artículo de Visual C++ Developer Journal's escrito por George Shepherd y Scot Wingo.

El trazado del mensaje que nosotros tenemos está después de AFX_MSGMAP_ENTRY. Está definido como:


    struct AFX_MSGMAP_ENTRY
    {
        UINT nMessage;
        UINT nCode;
        UINT nID;
        UINT nLastID;
        UINT nSig;
        AFX_PMSG pfn;
    };

El primer campo identifica el mensaje de Windows que está viniendo del sistema. La definición de los mensajes es igual que el SDK. El mensaje más importante para nuestro propósito es WM_COMMAND que es definido como 0111h, y es enviado por Windows cada vez que nosotros pulsamos el botón de un menú, o un botón. El segundo campo representa el código de WM_NOTIFY. El tercer campo es la ID del control de arranque, y el cuarto campo es la última ID del control. Si el control está en serie ( ej. grupo de botones de radio, menú en cascada), el primer artículo estará en nID, y el último artículo en nLastID. El quinto campo es la firma de la función para manejar el mensaje. Y el último campo es un indicador para la función del manipulador del mensaje. Sabiendo esto, ahora podemos localizar una función para un botón en particular o menú.

La primera cosa que necesitas hacer es extraer la fuente del ejecutable que estabas desmontando. Yo utilizo el editor de Visual C++ para hacer esto. Localiza el recurso en el que estabas interesado, con el, y toma nota de su ID. Si quieres seguir el ejemplo, tienes que desmontar un programa MFC. Hazlo con cualquier programa MFC.

Por ejemplo, quieres saber qué función utiliza un programa para su menú guardar. Mirando a través de tu editor de fuente, encuentras que la ID del menú Guardar del objetivo es 57603.

En IDA Pro, ve a la sección de .rdata (View, Segment, y selecciona .rdata de la lista). Entonces, usa la función "Search for Text in Core..." de IDA (Alt+B). Asegúrate que busca hacia "abajo." Si no es así, pulsa el botón cancel, y utiliza la tecla TAB para cambiar la dirección de la búsqueda. Teclea 57603 para buscar el string, pulsa el botón decimal, y pulsa el botón OK. IDA se detendrá en algo similar a esto:


....
0045C738 db 3\

Convierte el valor de la situación del cursor a un dword decimal. La manera más rápida de hacerlo es apretando "o" seguido por "h" ("o" lo convertirá a un offset dword, "h" convertirá el offset a un decimal dword. No es la manera correcta, pero funciona, y también más rápidamente :). Debes verlo como 57603. Si no es así, entonces continúa buscando. Si es así, entonces intenta convertir el valor de antes y después de él. Debes ver algo similar a esto:


....
0045C730 dd 111h
0045C734 dd 0
0045C738 dd 57603
0045C73C dd 57603
0045C740 dd 0Ch
0045C744 dd offset loc_423280
....

En offset 423280, encontrarás la función que manipula este mensaje. Pero, eso no es el final. Puesto que una ID similar podría ser usada varias veces, por clases diferentes, tendrás que continuar tu búsqueda hasta que ya no estés en el segmento de .rdata.

Después de que buscas todas las ocurrencias de este ID, puedes verificar que estabas mirando el lugar correcto examinando la sucesión de bytes circundante. Conviértelo de manera similar. Si estuvieras buscando un botón en un diálogo, entonces verás que la ID de otro botón que este en el mismo diálogo estará alrededor de él. Cuando buscas un menú, recuerda, si un programa tuviera clases diferentes de este derivadas de CView, probablemente habrá múltiples lugares donde la misma ID del menú podría aparecer. Experimenta con eso. Quizá algún día, en alguna parte, una herramienta como FRMSPY o EXE2DPR se desarrollará para este propósito (¿por ti, mi estimado amigo? ;). Pero hasta ese día, esta es la manera más rápida que yo conozco. Es aun más rápido que usando un debugger. Podrías utilizar el comando stack de SoftICE para buscarlo. Pero, todavía tendrías que comparar varios resultado de stack para encontrar la situación exacta. Yo todavía soy un principiante en MFC no obstante. Así, si sabes otra manera, por favor mándame un mail, así podremos incluirlo en estos tuts.

Función de Retorno de Valor

No hay mucho que decir sobre una función de retorno de valor. En un programa 32-bit, la función devuelve su valor en EAX. Un programa 16-bit usa AX para un valor 16-bit, y una combinación de DX:AX para un valor 32-bit. Si, no obstante, el programa está escrito en lenguaje ensamblador, la función puede devolver su valor donde quiera. Una costumbre común en ensamblador es, si la función es una función booleana, la función pondrá el flag de acarreo (CF) como apropiado. Si este es el caso, puedes mirar el código que sigue a la llamada para un JC o JNC.

Argumentos de la función

Antes de que nosotros nos metamos en la discusión sobre cómo podemos reconocer un argumentos de la función, debemos saber algún convenio para llamadas exteriores. Con la excepción del convenio de llamada para la llamada rápida (fastcall), el compilador pasará argumentos a una función de llamada en la pila. Saber convenios de llamadas diferentes, ayudará a que nosotros deduzcamos donde están los argumentos. Los convenios de llamadas dictan cómo se pasan argumentos en una función, en lenguaje ensamblador, y cómo la pila se limpiará cuando la función retorne. Debajo está la tabla de algunos convenios de llamadas.

Convenio de llamada

Paso del argumento

Mantenimiento de la pila

Distintivo del nombre (C sólo)

Notas

__pascal

Izquierda a derecha.

La función llamada quita sus propios argumentos de la pila.

Mándame un e-mail

Usado para casi toda función de exportación en Windows 16-bit.

__cdecl
(convenio de llamada en C)

Derecha a Izquierda.

La función llamada quita sus propios argumentos de la pila.

Prefijo Subrayar para el nombre de la función. Ex: _Foo.

Usado en CRT (librería runtime de C).

__stdcall

Derecha a Izquierda.

La función llamada quita sus propios argumentos de la pila.

Prefijo Subrayar para el nombre de la función, @ añadió seguido por el número de bytes decimales en la lista del argumento. Ex: _Foo@10.

Usado para casi toda función de exportación en Win32.

__fastcall

Primero se pasan dos argumentos DWORD en ECX y EDX, el resto se pasa de derecha a izquierda.

La función llamada quita sus propios argumentos de la pila.

Un @ es prefijado al nombre, @ añadido seguido por el número de bytes decimales en la lista del argumento. Ex: @Foo@10.

Debido a que usa un registro específico, sólo se aplica a los CPUs de Intel. Esto es el convenio de llamada predefinido para los compiladores de Borland (incl. Delphi).

thiscall

este puntero pone en ECX, los argumentos pasados de derecha a izquierda.

La función llamada quita sus propios argumentos de la pila.

Ninguno.

Usado automáticamente por código de C++.

naked

Derecha a Izquierda.

La función llamada quita sus propios argumentos de la pila.

Ninguno.

Sólo usado por VxDs.

Nota: Esta tabla está sacada del artículo de John Robbins en MSJ. Aunque el "Penguin guy" raramente cometió un error, yo aún verifico el resultado con mi compilador. Si quieres verificarlo con un compilador de C++, usa extern "C" para toda la función, para prevenir C++ name mangling. Mi compilador (MSVC 4.2) también limita la capacidad de convenio de llamada que yo puedo hacer. No tengo ninguna manera de declarar una función en modo Pascal. Si supieras cual es la función de distintivo del nombre en modo pascal, entonces por favor mándame un e-mail.

Explicaré las dos secciones más importantes para nuestro propósito: Pasando argumentos y Mantenimiento de la Pila. Comparado con, otras secciones que ya están escritas "en inglés":)

 

La sección Pasando Argumentos nos dice cómo el argumento será empujado hacia la pila antes de que el programa llame a la función. Si el argumento es empujado de izquierda a derecha, esa función será declarada como:


    some_function (0x1000, 0x2000, 0x3000);

parecería como:


    push 1000h
    push 2000h
    push 3000h
    call some_function

    ...

Similarmente, si los argumentos son empujados de derecha a izquierda, parecería como:


    push 3000h
    push 2000h
    push 1000h
    call some_function
    ...

Si el argumento es pasado usando el registro (e.j. modo fastcall):


    push 3000h
    mov  edx, 2000h
    mov  ecx, 1000h
    call some_function
    ...

 

No debes esperar que la secuencia empujada vengan juntos uno después de otro. Digamos, por ejemplo, que quieres asignar una memoria en el heap (*) de un programa Win32. Para hacer eso, nosotros podemos usar la función HeapAlloc. Pero, en lugar de creando otro heap, en cambio nosotros queremos usar el heap del proceso. Podrías codificarlo como:

(* n. del t.: Area especial en la memoria, que se utiliza para almacenar recursos importantes)


    LPVOID lpMem = HeapAlloc(GetProcessHeap(), 0, 1024);


Y, en el desensamblado, podría mostrarse como:

  push 400h              ; argumentos de HeapAlloc 
  push 0                 ; argumentos de HeapAlloc
  call ds:GetProcessHeap ; GetProcessHeap no requiere ningún argumento.Así,los dos
                         ; valor empujado antes, es ignorado por la función
  eax de push            ; empuja el ret del MANIPULADOR de memoria de GetProcessHeap.
  ds:HeapAlloc de call   ; ahora,los argumentos de HeapAlloc están completos,llámalo
  mov [ebp-20], eax      ; guarda el ret del indicador de LPVOID como variable local

    ...

Ahora, para la sección de Mantenimiento de Pila. ¿Te acuerdas todavía de nuestra discusión de la función de ret, dónde (E)SP es devuelto a su valor anterior, antes de que todo el argumento sea empujado? Bien, dicho simplemente, eso es lo qué es el mantenimiento de la pila. Quién será responsable de poner el valor de (E)SP.

Si la pila está mantenida por la función llamada, nosotros podríamos buscar al final de la función por un código como este (en este ejemplo la función es una función 32-bit y tiene tres argumentos pasados a ella, ajustados a 32-bit cada uno):

...............................; esto es un código 32-bit
.........pop edi
.........pop esi
.........add esp, 20h
.........ret 0Ch ..............; limpia la pila, quita 12 bytes de ella


Si este es el llamador responsable del mantenimiento de la pila, el código del llamador podría parecerse a esto:


    ...                    ; esto es un código 32-bit
    push 3000h
    push 2000h
    push 1000h
    call some_functions
    add  esp, 0Ch          ; limpia la pila, quita 12 bytes de ella
    ...

Pero, no debes esperar agregar esp, xxxx después de cada llamada.A veces, cuando la función llamada sólo tenía uno o dos argumentos, el compilador podría quitar simplemente la pila dentro del registro innecesario, como:


    ...                      ; esto es un código 32-bit
    push 1000h               ; sólo un argumento
    call little_functions
    pop edx		     ; limpia la pila, quita 4 bytes de ella.
                             ; edx contiene un valor innecesario
    mov edx, [ebp-20]        ; prepara edx para otro propósito

    ...

El mantenimiento de la pila nos ayuda a deducir cuántos argumentos, de la función que nosotros estábamos examinando, son usados (recuerda que en la sucesión empujada no siempre vienen uno después de otro). Si una función 32-bit quita 10h de la pila, entonces debe de haber tenido 4 argumentos. Un punto a recordar es: Todos los argumentos de la función de API Win32 son 32-bit.

Si examinas la sección de Mantenimiento de Pila, notarás eso sólo en los convenios de llamadas de C, que el llamador debe limpiar. Una de las razones es el hecho que en un programa de C una función puede tener un número variable de argumentos. La función printf es un ejemplo de esto. Desde que sea imposible para la función llamada saber por adelantado, cuántos argumentos se pasarán a la función, no podrá limpiar la pila dentro de la función. Por lo tanto el mantenimiento de la pila es cargado al llamador. Esto también es la razón, de por qué algunas API de Win32 usan convenios de llamadas de C en lugar de convenios de llamadas de stdcall. Podrías echar una mirada a un programa que llama a wsprintf o a una función similar.

Cuando desensamblas un programa, y quiere saber donde está puesto el argumento(s) para una función, la primera cosa que tendremos que deducir es que convenios de llamadas usa la función. Después de que nosotros nos figuramos que convenios de llamadas usa la función, el resto realmente es pan comido. Miremos un ejemplo realmente mundial (tú probablemente te cansarás con push 1000h por ahora :). Un procedimiento de Diálogo en Win32:


    LRESULT CALLBACK AboutProc(HWND hDlg, UINT msg,
                               WPARAM wParam, LPARAM lParam)

Después de que la función ha ejecutado su código prólogo (es decir después de mover ebp, esp ejecutado), la pila se parecerá a esto:

Contenido de la pila

Situación

Descripción

lParam

[EBP+14h]

Empujado por el llamador

wParam

[EBP+10h]

Empujado por el llamador

msg

[EBP+0Ch]

Empujado por el llamador

hDlg

[EBP+08h]

Empujado por el llamador

return EIP

[EBP+04h]

Empujado por instrucción CALL

EBP Previo

[EBP+00h]

Empujado por el código prologo

     

Si encontráramos un código en nuestra función que usa la dirección [EBP+08h], nosotros sabremos que es el que usa hDlg. Por lo tanto, nosotros podemos reemplazar [EBP+08h] con [hDlg]. Similarmente, nosotros podemos reemplazar [EBP+0Ch] con [msg]. El punto importante aquí es (debes recordar esto):

Cuando una función está usando un marco de pila (E)BP, el argumento de función tendrá un offset positivo desde (E)BP

Asimismo esto también es verdadero para código 16-bit. Sólo que, en código 16-bit, los argumentos son 16-bit de ancho, y si es una llamada lejana, el retorno de CS también será empujado a la pila. CS será empujado primero (en [BP+04h]), seguido por IP (en [BP+02h]).

Identificando argumentos (y variables también), saber algunas prácticas de programación en Win32 también es inmensamente útil. Por ejemplo, los programadores no lanzarán un manipulador probablemente. Si la rutina del llamador que estabas examinando llamó a la función de Win32 GetParent (), y pasó el resultado a una función que nosotros estábamos examinando, normalmente se quedará como hwndParent a lo largo de la función. Similarmente, un manipulador que es devuelto por la función CreateFile (), se quedará como un manipulador para el archivo. Si estas desensamblando para crackear, y la función del llamador pasó hwndParent para preparar la caja de diálogo del registro, probablemente no encontrarás la necesidad de examinar cómo se usará. En la práctica, esto evitara que examinemos alguna función que es llamada con este argumento. Por otro lado, un indicador puede lanzarse para otro tipo en la función. Esto no aparecerá en nuestro desensamblaje. Para el lenguaje ensamblador, 32-bit son 32-bit, o es un punto para un string o un indicador para la estructura. Así que, probablemente quieras tener cuidado sobre renombrarlo.

Ay, a veces no es tan simple. Con programas 32-bit, el compilador puede utilizar ESP para enviar argumentos, como [ESP+20h]. Cuando encuentras esto, debes tener mucho cuidado con renombrarlo. Porque el valor de ESP es cambiado siempre que un valor sea empujado o quitado de la pila. Un string que fue guardado en [ESP+20h] podría haber cambiado a [ESP+28h] después, cuando la función empujó dos DWORD a la pila. Hay afortunadamente para nosotros, un desensamblador como IDA Pro. En IDA Pro, pueden renombrarse argumentos y variables locales dentro del desensamblador. Y, si es dirigido con EBP o ESP, IDA Pro siempre renombrará el correcto.

Identificando Variables

Variables locales

Las variables locales de una función también son guardadas en la pila. Habrá veces, que el compilador usará los registros con dificultad para guardar las variables locales. Sin embargo, puesto que algunos registros son usados automáticamente en ciertas operaciones (como EDI en MOVSD, ECX en instrucciones que requieren contador, EAX en IDIV), el uso de registros para guardar las variables locales a lo largo de la función será bastante raro.

Una cosa importante a recordar es:

Cuando una función está usando un marco de pila (E)BP, la función de las variables locales tendrá un offset negativo desde (E)BP

Debes recordar sin embargo, que si la función usó ESP para dirigir los argumentos de función, también usará ESP para dirigir variables locales. Cuando la función usa ESP, ambos, argumentos y variables locales tendrán un desplazamiento positivo desde ella. Al examinar variables locales, el convenio de llamada usado por la función no tendrá ninguna diferencia. La excepción a esto es el convenios de llamada naked (*), donde, los mismos programadores escribirán la función en lenguaje ensamblador.
(*n.del t.: Desprotegida)

Determinar el tipo de las variables locales no es tan fácil como determinar argumentos. Al trabajar con argumentos, nosotros sabíamos si eran 16-bit o 32-bit, examinando la instrucción push. Determinando un tipo de variable local, nosotros tenemos que mirar cómo serán usadas las variables particulares. Después te diré, en este ensayo, cómo podemos examinar el tipo de variables locales. Por supuesto podemos determinar cuántos bytes son usados para las variables locales por la función. Cuando encontramos la instrucción sub esp, xxxx en el prologo de la función, xxxx son el número de bytes que la función requiere para las variables locales.

Variables Globales

Determinar las variables globales en una función es fácil. Las variables globales no necesitaron la ayuda de EBP o ESP para dirigirse. Así, si encontráramos una instrucción como:


    mov eax, [00421EB0]

sabemos que la función está dirigiéndose a una variable global. Sin embargo, determinar el tipo de las variables, será justo como determinar el tipo de variables locales. Tendrás que averiguar cómo nuestra función lo usa. Incluso cuando tu desensamblador intente convertirlo al valor correcto (basándose en la instrucción que el desensamblador encontró cuando las variables globales son dirigidas), no siempre debes confiar en él. Tu desensamblador simplemente está intentando ayudar. No reemplazará nuestros ojos, cerebro, y experiencias:) Al examinar un programa 16-bit, debes tener cuidado sobre renombrar una variable global. Tendrás que asegurarte primero de que el segmento (DS o a veces ES) está de hecho apuntando al correcto.

Descubrimiento del Tipo de Variables

Ya mencioné cómo es de útil el API de Win32 al determinar el tipo de argumentos de la función. Bien, también tiene exactamente el mismo poder para determinar el tipo de variables locales. Cuando estamos examinando qué usa una función de variables locales, Win32 API, debe ser lo primero que investiguemos en la función. A veces, sólo renombrando las variables que usa la función con Win32 API, nuestros listado desensamblado aparecerá mucho más despejado. Considera el siguiente recorte:


    ...
    push    offset 00423440
    lea     edx, [ebp-5B0h]
    push    edx
    push    offset 0041EC2C
    push    offset 0041EC34
    call    ds:WritePrivateProfileStringA
    ...

Volviendo dentro de nuestra documentación del fiel API, sabemos que WritePrivateProfileString es declarado como:


    BOOL WritePrivateProfileString(
        LPCTSTR lpAppName,     // indicador para el nombre de la sección
        LPCTSTR lpKeyName,     // indicador para el nombre de la tecla
        LPCTSTR lpString,      // indicador para string a agregar
        LPCTSTR lpFileName     // indicador para el nombre del fichero de inicialización
        );

Ahora sabemos que en offset 00423440, encontraremos un string literal para el nombre de fichero .INI, y la variable local [EBP-5B0h] será un string literal para el valor de la tecla a escribir. De igual manera, nosotros podemos reemplazar 0041EC2C y 0041EC34 con szKey y szSection respectivamente.

Si los argumentos, puede usarse para identificar un tipo de la variable, el valor de retorno también puede usarse identificando variables. Echa una mirada a este ejemplo:


    ...
    call    ds:_hread
    mov     [ebp-40h], eax
    ...

De la documentación de API, nosotros supimos que _hread devolverá el número de bytes leído. Por nuestra discusión, nosotros supimos que el valor de retorno estará en eax. Así, [EBP-40h] debe ser un valor dword que contiene la cuenta de lectura de los bytes.Probablemente querrás renombrarlo como dwRead. Podrían usarse muchas variables como variables temporales. Y cómo lo uses, probablemente cambiará en diferentes partes del programa. Por consiguiente, siempre debes verificar cómo esas variables particulares que renombraste son usadas a lo largo del programa. Si es usado de forma consistente, entonces de esa manera, tu suposición será correcta probablemente.

Recientemente, desensambladores como IDA Pro, toman esto en un grado superior. No sólo nos mostrará el API Win32. Con su tecnología FLIRT, IDA Pro nos mostrará también cuando la función que nosotros examinamos usa el CRT (librería runtime de C), clases de MFC, función de Delphi, o la función de VCL de Borland. Incluso cuando está estáticamente unido. Esto es una fortuna de información por supuesto. Si trabajas mucho con programas de Delphi, podrías querer bajarte la librería FLIRT para Delphi de la página de download de Peter Sawatzki .

Cuando la función no está usando un API conocido, las variables no pueden ser reconocidas fácilmente. Un tipo de variables que podrían reconocerse fácilmente son las variables booleanas. Cuando el código que estas examinando contiene una instrucción como:


    ...
    move eax, [ebp-40h]
    test eax, eax
    jz   loc_41453D
    ...

En [EBP-40h] probablemente encuentres una variable de tipo BOOL. Otro tipo que puede ser reconoció fácilmente, es un contador para un loop. En C, el código para loop normalmente será codificado como:



    for (i = 0; i < 1024; i++) { 
                 // procesando algunas variables 
        }; 

Por favor, examina cuidadosamente el siguiente recorte. Presta atención a cómo [EBP-164h] es usado:


        ...
        mov    dword ptr 
        [ebp-164h], 0                      ; Inicia [ebp-164h]
        jmp short loc_41453D               ; Empieza desde loc_41453D
loc_41452E:
        mov    ecx, 
        [ebp-164h]                         ; [EBP-164h] de nuevo
        inc    ecx                         ; incrementa
        mov    [ebp-164h], ecx             ; y guarda

loc_41453D:
        cmp    dword ptr [ebp-164h], 400h ; ¿Es [ebp-164h] > 1024 ?
        jnb    short loc_414567           ; Si mayor, salta
        ...                               ; Algunos códigos con los que trabajan
        ...                               ; otras variables
        ...                               ; Recorte para brevedad
        jmp    short loc_41452E           ; salto atrás

loc_414567:

        ...

¿Supiste cómo funciona? Tienes razón, [EBP-164h] es el contador. Si no eres un programador de C, probablemente no sabrás una cosa graciosa. Un programador de C, raramente usa un contador para un propósito diferente. Si encuentras uno para una instrucción dentro de tu desensamblaje, muchas veces puedes apostar que las mismas variables se usarán de nuevo para un contador cuando la función hace otro para un loop. Una cosa para lo que lo usarán los programadores, es para un índice probablemente para un array. El código de debajo es un recorte fuera de la misma rutina:


        mov    eax, [ebp-164h]
        mov    edx, ds:00423194[eax*4]

En el código anterior, [EBP-164h] contiene un índice para una variable global array en offset ds:00423194h. El tipo del array es un entero 32-bit. Eso es por qué el contador es incrementado por cuatro bytes (el código [eax*4]). Si el array es de un tipo CHAR (1 byte), el código se parecerá:


        mov    eax, [ebp-164h]
        mov    edx, ds:00423194[eax]

Si el índice y el array son variables locales, el código no podría estar tan claro como el código anterior. Un array de tipo DWORD probablemente se desensamblará como:


        mov    eax, [ebp-164h]
        mov    edx, [ebp+eax*4-40h]

En este código, podemos encontrar un array de tipo DWORD comenzando en [EBP-40h].


Supongo que esto será bastante por ahora. Hay muchas maneras, que un compilador puede generar código en ensamblador. No hay ninguna manera de escribir todo lo que encontré. Sigue practicando. Es la única manera segura. El desensamblado es impreciso. No siempre debes esperar reconocer todas las variables que utiliza una función. Aunque, prefiero el listado muerto al acercamiento vivo, siempre debemos recordar,que el desensamblado es simplemente otra opción que podemos usar. No tiene ningún sentido esperar por el desensamblador para desensamblar un programa de 3 MB, si podemos encontrar la información en segundos usando un debugger. Debes estar seguro de la razón por la que desmontas este programa en el primer lugar. Es demasiado fácil ser engañado por una función que no es importante para nosotros. Simplemente como eres atraído por una función vista dentro de tu debugger. En lugar de pegar con tu breakpoint, en cambio examinas la función. ¿Sólo para averiguar que es una llamada a GetLastError (recuerdas todavía esa experiencia? ;).


 

 

The cRACKER's n0tES esta dividido dentro de 12 partes principales:

 TX. Notas del Traductor
 00. INDICE
 01. Ensamblador para Cracker (CoRN2)
 02. SoftICE (Menú de arranque , Configuración, Comandos)
       
 1 Menú de arranque
       
 2 Configuración
       
 3 Comandos
 03. Breakpoints & Detalles de API de Windows
       
 1 Programas restringidos
       
 2 Cajas de diálogo
       
 3 Verificando el Tipo de unidad
       
 4 Acceso a archivos
       
 5 Acceso al Registro
       
 6 Cogiendo números de serie
       
 7 Accediendo a Tiempo & Fecha
       
 8 Generando ventanas
 04. Instrucciones de salto
 05. Instrucciones SET
 06. Tips & Trucos para Crackear
       
 1 Programas restringidos
       
 2 Dongles
       
 3 General
       
 4 Configuración de InstallSHIELD
       
 5 Protecciones con Archivo llave
       
 6 Pantallas molestas
       
 7 Límites de Runtime
       
 8 Serials
       
 9 Limites de Tiempo
       
10 Programas Visual BASIC
 07. Ventanas de Mensajes Para los Cracker
 08. Identificando funciones, Argumentos, y Variables (Rhayader)
 09. Los Sistemas de Protecciones de comerciales
       
 1 Armadillo
       
 2 C-Dilla SafeDISC
       
 3 SalesAgent
       
 4 SecuROM
       
 5 softSENTRY
       
 6 TimeLOCK
       
 7 VBox
 10. Bitmanipulation (Cruehead)
 11. Teoría general de Cracking
 12. FAQ


 +A. Cómo contactar conmigo
 +B. ¿Que es lo Nuevo?


 



The cRACKER's n0TES are Copyright 1998-2000 by TORN@DO of ID.
Todo los Derechos Reservados.
Traducido por
Revisado por X-Grimator.