ESTUDIO COLECTIVO DE DESPROTECCIONES
Última Actualizacion: 25/10/2001


Programa

Unpacker del PeCompact 1.41

W95 / W98 / NT

Descripción

Se trata del código que inyecta el compresor en los programas empaquetados con el, para que se puedan descomprimir en memoria.

Tipo

Packer

Tipo de Tutorial

[X]Original, []Adaptación, []Aplicación, []Traducción

Url

http://www.collakesoft.com

Protección

Encriptación/Compresión

Dificultad

1) Principiante, 2) Amateur, 3) Aficionado, 4) Profesional, 5) Especialista

Herramientas

OllyDbg 1.03, WinHex, PeBuild

Objetivo

Desentrañar cómo se descomprimen los programas comprimidos con PeCompact.

Cracker

Mr. Khaki

Grupo

www.whiskeykontekila.org

Fecha

20 de Septiembre de 2001

INTRODUCCION
La historia empezó a la hora de querer proteger un poco más mi primer crackme, aquel famoso que servía para obtener “registro” en el canal #crackers del Irc-Hispano. Si hubo por lo que me decidí a hacerlo con el PeCompact fue porque lo dejaba realmente compacto, y eso me gustó. A medida que la gente fue estudiando el crackme, y al ver sus respuestas cada vez me llamó más la atención. Alguno llegó a pensar que el crackme estaba comprimido en varias capas (¿?). Otros tenían auténticos problemas a la hora de desempaquetarlo, pues un script que había por ahí para el ProcDump no funcionaba.

A medida que la gente me iba diciendo todo esto, yo realmente descubría que mi crackme, atacarlo, no era tan difícil, es más, mi mayor empeño estaba dirigido al algoritmo del serial, que eso fue lo que trajo después de cabeza a muchos... mucho más que el antidebugging (por llamarlo de algún modo), o que el packer.

A la hora de estudiar cómo funciona el PeCompact (especialmente el unpacker que va inyectado en los exe's comprimidos) lo hice del siguiente modo. Cogí mi crackme, y lo comprimí con el PeCompact aplicándole diversas opciones: el algoritmo de compresión, la razón de compresión (ratio), algoritmo “fast” o “small”, etc... y después empecé a estudiarles 1 a 1.
AL ATAQUE

Lo primero que hice nada más comprimir mi crackme, fue echar una ojeada a las cabeceras (sí, cabeceras, porque no podéis olvidar que los PE también llevan cabecera MZ).

Lo primero que vi al ojear la cabecera del crackme comprimido con el PeCompact fue una especie de signatura (firma), que deja al inicio del fichero, concretamente en el offset 0CAh. La signatura consiste en un word (supuestamente un checksum?), y los caracteres “PECO” (figura 1). Y lo primero que intenté, fue cambiar esa signatura, y ver que el fichero comprimido seguía funcionando.... y así fue. Por tanto es una signatura que el compresor pone ahí, pero la podemos cambiar..... es decir, una signatura que no nos va a servir para identificar los archivos que están comprimidos con PeCompact.


Fig. 1. La signatura del compresor en la cabecera

Otra de las cosas que pronto se ven al analizar las cabeceras, es que las secciones de la cabecera PE la va nombrando como “.pecn” y “.rsrc”. Esto tampoco nos debería servir para identificar el compresor, pues si renombramos las secciones del PE, el unpacker no lo va a detectar, y eso provocará un despiste mayúsculo.

Así, pues, si queremos buscar una signatura real par el PeCompact, lo que tenemos que hacer es ojear el código ejecutable en la zona del Entry Point, es decir, el código que supuestamente tiene que descomprimir el exe comprimido. Esto es un problema, pues por la cantidad de opciones que tiene el PeCompact pronto supuse que tendría varios unpackers internos, así que fui mirando todas las posibilidades. Realmente la única diferencia está en si se usa el algoritmo aPLiB o si se usa el JCALG1. Yo todo el estudio lo he hecho sobre el algoritmo aPLiB, aunque seguramente el aspecto del unpacker para el JCALG1 es igual, salvo la rutina propia que descomprime los datos.

Al mirar los ficheros en la zona donde sitúa el Entry Point, observé que los primeros 178 bytes son prácticamente iguales, salvo alguna ligera modificación. Y es partir de esa zona donde la rutina del JCALG1 se diferencia. Podríamos por tanto coger cualquier cadena un poco larga como signatura... especialmente que en su representación ASCII sea fácil de localizar. Hay una parte que es tremendamente diferenciadora: “SSSSX-p” (figura 2).


Fig. 2. Los 178 bytes primeros de la zona de Entry Point los podemos considerar como la verdadera signatura del unpacker del PeCompact.

Bien, pues vista esta parte interesante del unpacker, vamos a empezar a estudiar su código, y el modo en que va reconstruyendo el fichero original.

TRAZANDO CÓDIGO

Normalmente los unpackers trabajan del modo siguiente: desencriptan un pequeño bloque que es realmente el verdadero corazón del unpacker, desde ese kernel del unpacker desempacan todo el código original del exe, alineándolo en memoria, desempacan la tabla de importaciones, y construyen la IAT (Import Address Table / Tabla de Direcciones de Importación),este trabajo normalmente lo hace el loader de Windows al cargar el exe en memoria, finalmente obtiene el Entry Point original del programa (OEP), y salta a ese lugar para ejecutar el fichero reconstruido en memoria.

00404600 >EB 06            jmp short FAST_APL.00404608
00404602 68 00100000       push 1000
00404607 C3                retn

Estas son las 3 primeras líneas de código del unpacker de PeCompact. Sencillo, ¿verdad?. Un jmp incondicional, un push y un ret. Si nos olvidamos del jmp, ya sabemos lo que haría esto... al meter 1000 en la pila, y provocar un ret, el ultimo dato de la pila (1000) se tomaría como dirección de retorno... y sería como poner un jmp 1000. Como dato ya preeliminar, decir que el número que acompaña al push es el RVA del OEP. Si a ese numero le sumamos la ImageBase, ya tendríamos el OEP (vaya con el PeCompact!!)

00404608 9C                pushfd
00404609 60                pushad
0040460A E8 02000000       call FAST_APL.00404611
0040460F 33C0              xor eax,eax
00404611 8BC4              mov eax,esp
00404613 83C0 04           add eax,4
00404616 93                xchg eax,ebx
00404617 8BE3              mov esp,ebx
00404619 8B5B FC           mov ebx,dword ptr ds:[ebx-4]
0040461C 81EB 0FA04000     sub ebx,40A00F
00404622 87DD              xchg ebp,ebx
00404624 8B85 A6A04000     mov eax,dword ptr ss:[ebp+40A0A6]
0040462A 0185 03A04000     add dword ptr ss:[ebp+40A003],eax
00404630 66:C785 00A04000 >mov word ptr ss:[ebp+40A000],9090
00404639 0185 9EA04000     add dword ptr ss:[ebp+40A09E],eax
0040463F BB C3110000       mov ebx,11C3
00404644 039D AAA04000     add ebx,dword ptr ss:[ebp+40A0AA]
0040464A 039D A6A04000     add ebx,dword ptr ss:[ebp+40A0A6]
00404650 53                push ebx
00404651 53                push ebx
00404652 53                push ebx
00404653 53                push ebx
00404654 58                pop eax
00404655 2D 70A04000       sub eax,40A070
0040465A 8985 71A04000     mov dword ptr ss:[ebp+40A071],eax
00404660 5F                pop edi
00404661 8DB5 70A04000     lea esi,dword ptr ss:[ebp+40A070]
00404667 B9 55040000       mov ecx,455
0040466C F3:A5             rep movs dword ptr es:[edi],dword ptr ds:[esi]
0040466E 5F                pop edi
0040466F C3                retn

Bien, analicemos ahora todo este bloque de código hasta el retn. Con las 2 primeras instrucciones guarda los valores de los registros en la pila, y con el call siguiente lo que hace es, meter en la pila el valor 40460F (dirección de retorno), y saltar a 404611. Desde 404611 hasta 404619 no hace más que recuperar el dato que hay en la pila poniéndolo en el registro EBX, y resituar el valor de ESP. Aquí viene un tema un poco complejo... fijaos que el unpacker se va a cargar en diferentes zonas de memoria, dependiendo de la longitud que ocupe el programa previamente empaquetado. Ello obliga a que todos los offsets sean relativos (código relocalizable), pero casi todas las instrucciones de tipo mov usan direcciones absolutas, por eso los unpackers lo que hacen es utilizar un registro (normalmente ebp), como índice para acceder a los datos que estarían en falsas posiciones relativas.

Fijaos que a ese calor de retorno que ha sacado de la pila (40460F) después le resta 40A00F, de este modo obtiene en EBP un valor negativo, que después lo usa como índice para acceder a las variables que están en direcciones absolutas.

Por ejemplo: mov eax,dword ptr ss:[ebp+40A0A6] , al utilizar un EBP negativo, está accediendo realmente a la dirección 4046A6.

En el unpacker inicialmente hay 3 pequeñas rutinas que no están ni encriptadas ni comprimidas: la inicial que estamos analizando ahora, la que desempaca el kernel del unpacker, y la que contiene el algoritmo de descompresión. Junto a estas 3 pequeñas rutinas se sitúan 5 datos necesarios: la dirección real donde se encuentra la rutina de descompresión (la suele situar al final de la sección de la tabla de importación), la ImageBase del fichero original, la ImageSize del fichero original, y la RVA de la sección que contiene el unpacker.

Concretamente en 4046A6 lo que está cogiendo es la ImageBase.

00404624 8B85 A6A04000     mov eax,dword ptr ss:[ebp+40A0A6]
0040462A 0185 03A04000     add dword ptr ss:[ebp+40A003],eax
00404630 66:C785 00A04000 >mov word ptr ss:[ebp+40A000],9090
00404639 0185 9EA04000     add dword ptr ss:[ebp+40A09E],eax

Estas líneas son muy curiosas, pues parchean el Entry Point del unpacker. Con “9090” está nopeando el primer jmp, y con los add lo que hace es sumar la ImageBase al RVA del OEP que está en el push. Con esto lo que ocurre es que la entrada del unpacker se queda parcheada de este modo:

00404600 >90               nop
00404601 90                nop
00404602 68 00104000       push 401000
00404607 C3                retn

Si en este momento nosotros hacemos un dump de la memoria a un fichero, el unpacker ya no funcionará, pues estamos provocando que salte al OEP (esto es una gran ventaja, pues a la hora de desempaquetar a mano nos va a evitar tener que modificar el Entry Point por el OEP.

0040463F BB C3110000       mov ebx,11C3
00404644 039D AAA04000     add ebx,dword ptr ss:[ebp+40A0AA]
0040464A 039D A6A04000     add ebx,dword ptr ss:[ebp+40A0A6]
00404650 53                push ebx
00404651 53                push ebx
00404652 53                push ebx
00404653 53                push ebx
00404654 58                pop eax
00404655 2D 70A04000       sub eax,40A070
0040465A 8985 71A04000     mov dword ptr ss:[ebp+40A071],eax

En esta zona se vuelve a calcular un nuevo EBP relativo, para poder trabajar más adelante con offsets y tomar valores de las variables. En EBX va sumando 11C3, le suma la ImageSize del fichero original, y también la ImageBase. Con estas sumas calcula una dirección en una zona de memoria donde va a realojar código ejecutable. Con el mov dword ptr ss:[ebp+40A071],eax, está modificando código en tiempo de ejecución que ahora veremos.

00404660 5F                pop edi
00404661 8DB5 70A04000     lea esi,dword ptr ss:[ebp+40A070]
00404667 B9 55040000       mov ecx,455
0040466C F3:A5             rep movs dword ptr es:[edi],dword ptr ds:[esi]
0040466E 5F                pop edi
0040466F C3                retn

Finalmente, realoja parte del código en una zona de memoria que está libre. (parte del código que ha realojado corresponde al kernel del unpacker. Finalmente, con el retn salta al código que acaba de realojar... (usa muchos datos tomados de la pila, que los guardó con todos los push ebx).

Ahora analizaremos el trozo de código que ha realojado, y que también ha modificado en tiempo de ejecución.

00404670 BD 0000000        mov ebp, 0
00404675 57                push edi
00404676 5E                pop esi
00404677 83C6 42           add esi,42
0040467A 81C7 53110000     add edi,1153
00404680 56                push esi
00404681 57                push edi
00404682 57                push edi
00404683 56                push esi
00404684 FF95 9EA04000     call dword ptr ss:[ebp+40A09E]
0040468A 8BC8              mov ecx,eax
0040468C 5E                pop esi
0040468D 5F                pop edi
0040468E 8BC1              mov eax,ecx
00404690 C1F9 02           sar ecx,2
00404693 F3:A5             rep movs dword ptr es:[edi],dword ptr ds:[esi]
00404695 03C8              add ecx,eax
00404697 83E1 03           and ecx,3
0040469A F3:A4             rep movs byte ptr es:[edi],byte ptr ds:[esi]
0040469C EB 14             jmp short FAST_APL.004046B2

Se trata de un bloque muy pequeño, pero muy importante. Es precisamente el que se encarga de desempaquetar el kernel del unpacker, y pasarle a él el control. Este bloque se encuentra duplicado, pues se ha realojado en otra zona de memoria (después veremos gráficamente qué galimatías ha hecho el unpacker). El mov ebp,0 se ha modificado previamente en tiempo de ejecución, justo antes de realojarlo en otra zona de memoria, de este modo el 0 lo cambia por un numero calcula que nuevamente lo usa como índice para referirse a offsets de variables. A la entrada de esta rutina, edi contiene la dirección donde ha sido realojada, por lo que se pasa a esi (push edi, pop esi), y se le suma 42 (longitud en bytes de este bloque de código, más 5 dwords que son variables que ya se han usado anteriormente). A edi le suma 1153, colocándolo en una zona de memoria libre y amplia. El call, es una llamada a la rutina que contiene el algoritmo de descompresión, y como parámetros solo hay que pasarla en esi la dirección del código comprimido, y en edi la dirección del buffer donde va a colocar el código descomprimido. El algoritmo de descompresión devuelve en eax la longitud de bytes descomprimidos.

Lo que hace después del call es tapar el código comprimido del kernel del unpacker con el código que hay descomprimido ya en el buffer. Y a continuación, mediante el jmp, salta al kernel de unpacker, que ya está descomprimido y realojado (figura 3).


Fig. 3. Gráfica con todos los bloques de código y buffers que mueve.

Según la gráfica anterior, podríamos hacer un breve resumen: En “Inicializaciones y parcheo”, el código simplemente parchea el Entry Point, poniendo en él un salto al Original Entry Point, y copia la memoria que hay en 404670-404A36 en 4061C3, saltando después a ejecutar el bloque de “Desempaca el Kernel”. El bloque “Desempaca el Kernel” sencillamente desempaca el “Kernel del unpacker empaquetado” en “Buffer para desempacar el Kernel”, y acto seguido sobrescribe “Kernel de unpacker empaquetado” con lo que ha desempacado en “Buffer para desempacar el Kernel”. Finalmente se haría un salto a la zona “Kernel del unpacker empaquetado”, que ahora tiene el kernel de unpacker desempacado.

Veamos pues el contenido del kernel del unpacker, es decir, el corazón real del unpacker.

 

ESTO SE PONE SERIO

Bien, si hemos entendido hasta aquí todo lo que hemos ido trazando, veremos que aún falta lo más complicado del unpacker. Pensemos un poco cómo es un fichero PE, cómo se estructura el fichero cargado en memoria, y qué significa la palabra “empaquetar” (pack). Debido a la propia estructura PE, y debido al alineamiento que existe dentro del fichero, hay muchos bytes que se pierden, es decir, espacio en el fichero que realmente no es útil y suele estar rellenada con 0.

El packer, por tanto, debe quitar todo ese espacio, para que realmente esa información inútil no ocupe espacio en el fichero final comprimido. Por tanto el unpacker debería reorganizar toda esa información inútil que aunque no esté comprimida en el fichero final, sí que debe mantenerla.

Aquí ha salido una palabra que va a ser clave para entender cómo funciona el loader y el unpacker, a saber: alineamiento. Si estamos acostumbrados a trabajar con ficheros PE, sabremos que estos están formados por secciones.

Cada sección es un bloque de datos, que tienen una cabecera especificando las características de cada sección. Dentro de esas características hay: Physical RVA, Physical Size, Virtual RVA, y Virtual Size. Los datos “physical” hacen referencia a los offset del fichero. Así pues, si una sección tiene Physical RVA igual a 400h, significa que los datos de esa sección empiezan en el offset 400h del fichero.

Los datos “Virtual” hacen referencia a dónde se organizará esa sección en el mapeado de memoria (empezando siempre desde la ImageBase). Así, si la sección anterior tiene Virtual RVA igual a 3000h, significa que esa sección se cargaría en la zona de memoria ImageBase+3000h (figura 4). Y aquí viene el meollo del alineamiento... Si el fichero tiene un alineamiento de 200h, significa que las secciones tienen que tener un tamaño que sea múltiplo de 200h. Así si una sección tiene un Physical RVA de 500h y un Physical Size de 35h, realmente ocuparía 200h, ya que por el alineamiento la siguiente sección debería empezar en 500h+200h. Si su Physical Size fuera de 235h, ocuparía 400h (el número más próximo múltiplo de 200h). El espacio “montante” que se añade para alinear el fichero normalmente se rellena con 0.

Y ahora viene el problema real.... normalmente el alineamiento del fichero suele ser muy pequeño. Pero no ocurre lo mismo con el alineamiento de memoria (Object Alignment), que normalmente suele ser de 1000h. Normalmente los compiladores y el linker suelen asignar bloques consecutivos para posicionar cada sección, aunque siempre respetando el alineamiento.


Fig. 4. Esquema de carga de una sección del fichero a la memoria.

Bueno, pues visto esto... veamos ahora el kernel del unpacker.

00406205 8BB5 A6A04000  mov esi,dword ptr ss:[ebp+40A0A6]
0040620B 56             push esi
0040620C 03B5 AEA04000  add esi,dword ptr ss:[ebp+40A0AE]
00406212 57             push edi
00406213 83C6 14        add esi,14
00406216 03B5 11A64000  add esi,dword ptr ss:[ebp+40A611]
0040621C 8DBD 15A64000  lea edi,dword ptr ss:[ebp+40A615]
00406222 B9 06000000    mov ecx,6
00406227 F3:A5          rep movs dword ptr es:[edi],dword ptr ds:[esi]
00406229 5F             pop edi
0040622A 6A 04          push 4
0040622C 68 00100000    push 1000
00406231 FFB5 2DA64000  push dword ptr ss:[ebp+40A62D]
00406237 6A 00          push 0
00406239 FF95 1DA64000  call dword ptr ss:[ebp+40A61D]
0040623F 8BF8           mov edi,eax
00406241 5B             pop ebx
00406242 019D 38A34000  add dword ptr ss:[ebp+40A338],ebx
00406248 8DB5 43A64000  lea esi,dword ptr ss:[ebp+40A643]
0040624E 60             pushad
0040624F 8D85 C3AD4000  lea eax,dword ptr ss:[ebp+40ADC3]
00406255 8B95 A6A04000  mov edx,dword ptr ss:[ebp+40A0A6]
0040625B FFD0           call eax
0040625D 61             popad
0040625E 57             push edi
0040625F AD             lods dword ptr ds:[esi]
00406260 0BC0           or eax,eax
00406262 74 6C          je short FAST_APL.004062D0
00406264 8BD0           mov edx,eax
00406266 0395 A6A04000  add edx,dword ptr ss:[ebp+40A0A6]
0040626C AD             lods dword ptr ds:[esi]
0040626D 56             push esi
0040626E 8BC8           mov ecx,eax
00406270 57             push edi
00406271 52             push edx
00406272 8BF2           mov esi,edx
00406274 8B85 15A64000  mov eax,dword ptr ss:[ebp+40A615]
0040627A 8B9D 19A64000  mov ebx,dword ptr ss:[ebp+40A619]
00406280 E8 910A0000    call FAST_APL.00406D16
00406285 5A             pop edx
00406286 5F             pop edi
00406287 52             push edx
00406288 57             push edi
00406289 FF95 9EA04000  call dword ptr ss:[ebp+40A09E]
0040628F 0BC0           or eax,eax
00406291 74 07          je short FAST_APL.0040629A
00406293 8BC8           mov ecx,eax
00406295 5E             pop esi
00406296 5F             pop edi
00406297 EB C5          jmp short FAST_APL.0040625E

El bloque anterior la que hace es ir descomprimiendo cada sección con nombre “.pecn”. El modo en que accede a ellas es mediante una tabla interna, por lo que no es necesario que las secciones conserven ese nombre.

Veamos cómo lo hace:
Desde 406205 hasta 406229 lo que hace es copiar las direcciones reales de las funciones de la tabla de importación a una tabla propia y por tanto más accesible por el unpacker, y que posteriormente van ser utilizadas. De este modo tienes una tabla de direcciones de las APIs y no tiene que estar buscándolas en la IAT del fichero comprimido. Las direcciones que carga de la IAT son las de las siguientes funciones (en el mismo orden que él las carga): LoadLibraryA, GetProcAddress, VirtualAlloc, VirtualFree, ExitProcess, GetModuleHandleA.

Un problema de las rutinas que tienen que descomprimir código es que se necesita un buffer donde descomprimir, por lo que necesita crear un buffer. Para crearlo llama a la API VirtualAlloc, haciéndolo del modo siguiente: desde 40622A hasta 406237 mete los argumentos en la pila (en orden inverso ;o), para posteriormente hacer un call al address directo de la API (que se encuentra en la tabla que acaba de crear antes). Los argumentos pasados son: 0, una variable que se encontraba comprimida con el kernel, 1000h, y 4. Veamos cuáles son los argumentos de esa API:

LPVOID VirtualAlloc(
      LPVOID lpAddress, // address of region to reserve or commit
      DWORD dwSize, // size of region
      DWORD flAllocationType, // type of allocation
      DWORD flProtect // type of access protection
);

lpAddress si tiene valor NULL (0), deja que el propio sistema busque una zona de memoria, y lo asigne, devolviéndonos en eax la dirección de inicio del bloque alojado. Como dwSize se le ha pasado la variable que estaba comprimida con el unpacker, por lo que presumiblemente debe ser el tamaño máximo que tiene que descomprimir. Como flAllocationType se le ha pasado un valor fijo, 1000h, que si miramos en las definiciones de windows.inc corresponde a MEM_COMMIT, y como flProtect se le ha pasado 4, que mirando en windows.inc corresponde a PAGE_READWRITE. Por tanto aquí se asigna un bloque de memoria para lectura y escritura de un tamaño en bytes suficientemente grande como para servir de buffer a cada sección (luego veremos que esto no es del todo cierto, porque el buffer se usa de almacenamiento temporal y no como buffer para descomprimir en él).

A partir de este momento lo que hace es ir descomprimiendo cada sección sobre sí misma. De este modo deja todo el trabajo sucio al propio sistema. Es decir, a cada sección le asigna en el fichero comprimido un VirtualSize suficiente como para poder alojar ahí el código comprimido. De este modo es el loader de Windows quien asigna la memoria. El modo en que va a descomprimir las secciones es el siguiente: primero copia la sección comprimida en el buffer y después descomprime el buffer sobre la propia sección (figura 5).


Fig. 5. Los 2 pasos que hace con cada sección para descomprimirla.

Analicemos esto sobre el código:

Después del call que llama a VirtualAlloc hace una de las operaciones más importantes en el unpcker... parchea las últimas instrucciones del unpacker, sumando al RVA del OEP la ImageBase. Justamente con este comando add dword ptr ss:[ebp+40A338],ebx cambia el push que contiene el RVA del OEP. Justo después de este push ejecuta un retn, es decir, que en el push está realmente el OriginalEntryPoint.

El PeCompact permite comprimir los archivos incluyendo diversos plugins. Existen varios tipos de plugins... encrypt, decrypt, pre-operative, post-operative, GPA (GetProcAddress Hook), y ExtraData. Justo después de haber alojado el buffer en memoria, haber situado el OEP, y antes de empezar a desempaquetar las secciones debería ejecutar el plugin pre-operative. Esto lo hace desde 406248 hasta 40625D. Fijémonos que antes del call eax guarda todos los registros en la pila, para recuperarlos después del call. Si no existe ningún plugin, eax apuntará a una parte del unpacker que está rellenada con 0C3h (retn), de tal modo que estas lineas no hacen nada.

A partir de ese momento la rutina sitúa esi apuntando a una tabla que tiene todas las RVA de las secciones que tienen datos comprimidos, y la longitud (en bytes) de cada sección (longitud real, no alineada). Y mediante un bucle que se van leyendo los datos de esa tabla, hasta que encuentre una RVA que sea 0. Un dato importante de la rutina que tiene el algoritmo de descompresión es que si hay un error del código empaquetado en eax devuelve 0, mientras que si no hay errores devuelve la longitud en bytes de lo que ha descomprimido.

Veamos este bucle parte a parte:

0040625F AD             lods dword ptr ds:[esi]
00406260 0BC0           or eax,eax
00406262 74 6C          je short FAST_APL.004062D0

Este es el inicio del bucle. Lee un dato de la tabla que tiene las RVA de las secciones empaquetadas (la llamaremos por ejemplopecRVA_list). Si el dato leído es 0, terminaría el bucle, saltando a 4062D0, si el RVA leído no es 0, entra en el bucle. La tabla pecRVA_list es una tabla de dwords que tendría 2 dword por cada sección comprimida, en el primero estaría la RVA de la sección, y en el segundo dword estaría la longitud en bytes de código comprimido en la sección.

00406264 8BD0           mov edx,eax
00406266 0395 A6A04000  add edx,dword ptr ss:[ebp+40A0A6]
0040626C AD             lods dword ptr ds:[esi]
0040626D 56             push esi
0040626E 8BC8           mov ecx,eax
00406270 57             push edi
00406271 52             push edx

En este momento mete en edx el RVA que ha leído de la tablapecRVA_list, para sumarle la ImageBase (add edx,dword ptr ss:[ebp+40A0A6]), a continuación carga en eax el siguiente dword de la pecRVA_list, que correspondería a la longitud en bytes de datos empaquetados en esa sección, pasándolo a eax. Finalmente guarda esi (apunta al siguiente dato en pecRVA_list) y edx (dirección de la sección) en la pila.

A continuación carga en eax y en ebx 2 valores de la tabla que creó anteriormente desde la IAT. Concretamente carga en eax la dirección de LoadLibraryA y en ebx la dirección de GetProcAddress, llamando posteriormente a 406D16. Hemos de tener en cuenta que justamente después de este call está el call a la rutina que tiene el algoritmo de descompresión. Si nos fijamos, hemos dicho que uno de los plugins es de tipo Encrypt... pues aquí nos encontramos justamente con el plugin Decrypt (:o). En la dirección 406D16 de hecho no hay más que una pequeña rutina que simplemente copia los datos de la sección empaquetada al buffer que hemos creado con VirtualAlloc. Una rutina, eso sí, seguida por un motón de 0C3h (retn).

00406D16 8BC1           mov eax,ecx
00406D18 C1F9 02        sar ecx,2
00406D1B F3:A5          rep movs dword ptr es:[edi],dword ptr ds:[esi]
00406D1D 03C8           add ecx,eax
00406D1F 83E1 03        and ecx,3
00406D22 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
00406D24 C3             retn

Finalmente, después de llamar a la rutina 406d16 que desencripta con el plugin (si existe), y copia la sección en el buffer, se llama a la rutina que tiene el algoritmo de descompresión. Esta rutina, recordemos, devuelve eax a 0 si hay algún error, y si no hay error devuelve en eax la longitud en bytes de lo que se ha descomprimido. Si por alguna extraña circunstancia se produce un error al descomprimir los datos de las secciones, se llama a una rutina que apenas comentaré, pues con los comentarios que añade el propio OllyDbg creo que queda muy claro qué es lo que hace.

0040629A 8D9D 97A54000  lea ebx,dword ptr ss:[ebp+40A597] ; EBX='USER32.DLL'
004062A0 53             push ebx
004062A1 FF95 15A64000  call dword ptr ss:[ebp+40A615] ; LoadLibraryA
004062A7 8D9D A2A54000  lea ebx,dword ptr ss:[ebp+40A5A2] ; MessageBoxA
004062AD 53             push ebx
004062AE 50             push eax
004062AF FF95 19A64000  call dword ptr ss:[ebp+40A619] ; GetProcAddress
004062B5 8D9D B8A54000  lea ebx,dword ptr ss:[ebp+40A5B8] ; 'This executable is corrupt![...]'
004062BB 8D8D EEA54000  lea ecx,dword ptr ss:[ebp+40A5EE] ; 'Authentification Check Failure'
004062C1 6A 10          push 10
004062C3 51             push ecx
004062C4 53             push ebx
004062C5 6A 00          push 0
004062C7 FFD0           call eax ; MessageBoxA
004062C9 FFA5 25A64000  jmp dword ptr ss:[ebp+40A625] ; ExitProcess

Y una vez acabado el bucle, saltaría a 40625E, donde se lee una nueva RVA de pecRVA_list, y si no es 0 volvería a ejecutar el bucle. Cuando encuentra una RVA que es 0, interpreta que es el final de la lista, y continua en 4062D0.

En la dirección 4062D0 nos encontramos una rutina muy similar a la que acabamos de ver. Una de las opciones al empaquetar un fichero con el PeCompact es unir varias secciones en una sola. De este modo podemos reducir el tamaño de alineamiento del fichero original, y lo que hace el unpacker en esta rutina que vamos a analizar ahora es exactamente eso, reordenar las secciones que han sido empaquetadas juntas en cada sección.

En 4062D0 encontramos el siguiente bloque de código:

004062D0 58             pop eax
004062D1 8DB5 C3A64000  lea esi,dword ptr ss:[ebp+40A6C3]
004062D7 AD             lods dword ptr ds:[esi]
004062D8 0BC0           or eax,eax
004062DA 74 74          je short FAST_APL.00406350
004062DC 0385 A6A04000  add eax,dword ptr ss:[ebp+40A0A6]
004062E2 8BD8           mov ebx,eax
004062E4 AD             lods dword ptr ds:[esi]
004062E5 0385 A6A04000  add eax,dword ptr ss:[ebp+40A0A6]
004062EB 8BD0           mov edx,eax
004062ED AD             lods dword ptr ds:[esi]
004062EE 8BC8           mov ecx,eax
004062F0 57             push edi
004062F1 56             push esi
004062F2 8BF3           mov esi,ebx
004062F4 57             push edi
004062F5 51             push ecx
004062F6 8BC1           mov eax,ecx
004062F8 C1F9 02        sar ecx,2
004062FB F3:A5          rep movs dword ptr es:[edi],dword ptr ds:[esi]
004062FD 03C8           add ecx,eax
004062FF 83E1 03        and ecx,3
00406302 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
00406304 59             pop ecx
00406305 5E             pop esi
00406306 8BFA           mov edi,edx
00406308 8BC1           mov eax,ecx
0040630A C1F9 02        sar ecx,2
0040630D F3:A5          rep movs dword ptr es:[edi],dword ptr ds:[esi]
0040630F 03C8           add ecx,eax
00406311 83E1 03        and ecx,3
00406314 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
00406316 5E             pop esi
00406317 AD             lods dword ptr ds:[esi]
00406318 8BC8           mov ecx,eax
0040631A 8BD0           mov edx,eax
0040631C 33C0           xor eax,eax
0040631E C1F9 02        sar ecx,2
00406321 F3:AB          rep stos dword ptr es:[edi]
00406323 03CA           add ecx,edx
00406325 83E1 03        and ecx,3
00406328 F3:AA          rep stos byte ptr es:[edi]
0040632A 8B7E F0        mov edi,dword ptr ds:[esi-10]
0040632D 03BD A6A04000  add edi,dword ptr ss:[ebp+40A0A6]
00406333 8B4E F4        mov ecx,dword ptr ds:[esi-C]
00406336 038D A6A04000  add ecx,dword ptr ss:[ebp+40A0A6]
0040633C 2BCF           sub ecx,edi
0040633E 8BD1           mov edx,ecx
00406340 C1F9 02        sar ecx,2
00406343 F3:AB          rep stos dword ptr es:[edi]
00406345 03CA           add ecx,edx
00406347 83E1 03        and ecx,3
0040634A F3:AA          rep stos byte ptr es:[edi]
0040634C 5F             pop edi
0040634D EB 88          jmp short FAST_APL.004062D7

Prácticamente todo el bloque es un bucle, concretamente desde 4062D7 hasta 40634D es el bucle que va reorganizando cada sección que se ha descomprimido. El modo de hacerlo es sencillo. Al entrar en 4062D0 en la pila ha quedado como último dato la dirección donde está el buffer que hemos alojado en memoria previamente. Y en 4062D1, con posiciona esi sobre una tabla de dwords que contiene los datos para reorganizar todas las secciones (a esta tabla la llamaremos mergedObjects_list). En esta lista se encuentran 4 dwords por cada sección que se ha mezclado en cada sección. Explicaré esto un poco más detalladamente.

Lo que hace el PeCompact es coger las secciones originales, y quitándolas la alineación propia de fichero, colocarlas secuencialmente, empaquetarlas, y guardar esa información en una sola sección. El problema es que para reorganizar eso de nuevo necesita saber, en qué posición y qué longitud tiene cada sección una vez desempaquetada esa sección, y qué RVA (y su alineamiento) le corresponde a cada sección de las que ha mezclado en esa sección. Todos esos valores los guarda en la tabla mergedObjects_list.

En 4062D7, carga un dword de la tabla mergedObjects_list, y si es 0, sale del bucle saltando a 406350. Si no es 0, a ese valor le suma la ImageBase., metiendo su valor en ebx. Lee un nuevo dword de la tabla, al cual le suma la ImageBase también y mete ese valor en edx. Si a esos valores les suma la ImageBase podemos suponer ya que son RVA's (Relative Virtual Addresses). En 4062ED carga de la mergedObjects_list un nuevo dword, cuyo valor lo guarda en ecx.

En la zona de 4062F0 a 406305 lo que hace es copiar memoria al buffer. Pero ¿qué es lo que copia? Bien sencillo. Hemos dicho que el PeCompact mezclaba las secciones secuencialmente en una sola sección. Pues bien, lo que hace es copiar en el buffer una de esas secciones que está mezcla en la sección ya desempaquetada. Para ello ha tomado varios valores de megedObjects_list, concretamente ha tomado un RVA que indica dentro de la sección ya desempaquetada dónde empieza la sección; el siguiente dato que ha leído de mergedObjects_list ha sido el VirtualRVA real que le correspondía a esa sección, para realojarla en memoria; y el siguiente dato que ha leído de mergedObject_list es la longitud en bytes que ocupa la sección desempaquetada (tamaño que no se corresponde al PhysicalSize de la sección original, pues aquí quita ceros finales, y otras cosas que son innecesarias).

En 406317 se lee otro dword de mergedObjects_list, que es usado como longitud en bytes a rellenar con ceros (0h), después de realojar la sección en su VirtualRVA original. Si nos fijamos todo este proceso se podría hacer sin usar el buffer como intermediario en los movimientos de memoria, pero se hace para que no se solape en ningún momento la sección que tiene todas las secciones mezcladas, con cada una de las secciones que se va colocando. Finalmente, desde 40632A hasta 40634C lo que se hace es poner a 0 la sección mezclada. Para que en ningún momento haya solapamiento, ni se sobrescriba ninguna sección el unpacker las va realojando empezando por la sección cuyo RVA es más alto (está más abajo en la memoria). (figura 6).


Fig. 6. En este diagrama se muestran los significados de cada valor tomado de la tabla mergedObjects_list. En la lista se guardan en el siguiente orden: RVAMerged, RVAOriginal, Longitud, Alineo. Hay que tener en cuenta que cada sección (sección1, sección2 y sección3) necesitarían tomar estos datos de la tabla.

Según lo que acabamos de ver daría la sensación que una vez realojadas todas las secciones en sus RVA originales podríamos volcar el proceso que está en memoria ya directamente sobre un fichero, pues una de las secciones que se han desempaquetado ha sido la que contiene la tabla de importaciones. Sin embargo esto no es del todo correcto, pues una de las cosas que hace el PeCompact antes de empaquetar el código, es modificar los argumentos de los opcodes que son de tipo call y jmp. Por tanto, una vez desempaquetadas las secciones debe rastrear el código, para restablecer los argumentos de esos opcodes. Eso es precisamente lo que hace a partir de 406350. Otra de las cosas que está pendiente por hacer, es rellenar la IAT (ImportAddressTable), ya que esto normalmente lo hace el loader de Windows, y en esta ocasión el loader sólo ha llenado la IAT del unpacker, pero no de la Tabla de Importaciones que estaba empaquetada, y que era la original del fichero.

Analicemos, pues, lo que hace a partir de 406350:

00406350 57             push edi
00406351 8BBD D7A44000  mov edi,dword ptr ss:[ebp+40A4D7]
00406357 03BD A6A04000  add edi,dword ptr ss:[ebp+40A0A6]
0040635D 8B8D DBA44000  mov ecx,dword ptr ss:[ebp+40A4DB]
00406363 33D2           xor edx,edx
00406365 33DB           xor ebx,ebx
00406367 33F6           xor esi,esi
00406369 03FE           add edi,esi
0040636B 03DE           add ebx,esi
0040636D 49             dec ecx
0040636E 74 72          je short FAST_APL.004063E2
00406370 78 70          js short FAST_APL.004063E2
00406372 66:8B07        mov ax,word ptr ds:[edi]
00406375 2C E8          sub al,E8
00406377 3C 01          cmp al,1
00406379 76 38          jbe short FAST_APL.004063B3
0040637B 66:3D 1725     cmp ax,2517
0040637F 74 51          je short FAST_APL.004063D2
00406381 3C 27          cmp al,27
00406383 75 0A          jnz short FAST_APL.0040638F
00406385 80FC 80        cmp ah,80
00406388 72 05          jb short FAST_APL.0040638F
0040638A 80FC 8F        cmp ah,8F
0040638D 76 05          jbe short FAST_APL.00406394
0040638F 47             inc edi
00406390 43             inc ebx
00406391 EB DA          jmp short FAST_APL.0040636D
00406393 B8 8B470290    mov eax,9002478B ;Código ofuscado
00406398 90             nop ; Realmente es :
00406399 90             nop ; db B8
0040639A 90             nop ; mov eax,dword ptr ds:[edi+2]
0040639B 90             nop ; Fijarse bien en la linea que lo invoca
0040639C 90             nop ; desde 40638D
0040639D 90             nop
0040639E 90             nop
0040639F 90             nop
004063A0 90             nop
004063A1 90             nop
004063A2 90             nop
004063A3 90             nop
004063A4 2BC3           sub eax,ebx
004063A6 8947 02        mov dword ptr ds:[edi+2],eax
004063A9 BE 06000000    mov esi,6
004063AE 83E9 05        sub ecx,5
004063B1 EB B6          jmp short FAST_APL.00406369
004063B3 8B47 01        mov eax,dword ptr ds:[edi+1]
004063B6 3C 01          cmp al,1
004063B8 75 D5          jnz short FAST_APL.0040638F
004063BA 66:C1E8 08     shr ax,8
004063BE C1C0 10        rol eax,10
004063C1 86C4           xchg ah,al
004063C3 2BC3           sub eax,ebx
004063C5 8947 01        mov dword ptr ds:[edi+1],eax
004063C8 BE 05000000    mov esi,5
004063CD 83E9 04        sub ecx,4
004063D0 EB 97          jmp short FAST_APL.00406369
004063D2 0157 02        add dword ptr ds:[edi+2],edx
004063D5 BE 06000000    mov esi,6
004063DA 83EA 04        sub edx,4
004063DD 2BCE           sub ecx,esi
004063DF 41             inc ecx
004063E0 EB 87          jmp short FAST_APL.00406369
004063E2 5F             pop edi
004063E3 E8 8D010000    call FAST_APL.00406575
004063E8 68 00400000    push 4000
004063ED 6A 00          push 0
004063EF 57             push edi
004063F0 FF95 21A64000  call dword ptr ss:[ebp+40A621]
004063F6 E8 97000000    call FAST_APL.00406492
004063FB 73 79          jnb short FAST_APL.00406476
004063FD 8D9D 97A54000  lea ebx,dword ptr ss:[ebp+40A597] ; USER32.DLL
00406403 53             push ebx
00406404 FF95 15A64000  call dword ptr ss:[ebp+40A615] ; LoadLibraryA
0040640A 8985 D7A44000  mov dword ptr ss:[ebp+40A4D7],eax
00406410 8D9D AEA54000  lea ebx,dword ptr ss:[ebp+40A5AE] ; wsprintfA
00406416 53             push ebx
00406417 50             push eax
00406418 FF95 19A64000  call dword ptr ss:[ebp+40A619] ; GetProcAddress
0040641E 8D9D C3A74000  lea ebx,dword ptr ss:[ebp+40A7C3]
00406424 53             push ebx
00406425 83BDE7A4400001 cmp dword ptr ss:[ebp+40A4E7],1 ; Es nombre u ordinal?
0040642C 74 08          je short FAST_APL.00406436
0040642E 8D8D 45A54000  lea ecx,dword ptr ss:[ebp+40A545] ; 'Procedure couldn't be found'
00406434 EB 06          jmp short FAST_APL.0040643C
00406436 8D8D 01A54000  lea ecx,dword ptr ss:[ebp+40A501] ; 'Ordinal couldn't be located'
0040643C 8B95 DFA44000  mov edx,dword ptr ss:[ebp+40A4DF] ; Funcion del error
00406442 8BBD E3A44000  mov edi,dword ptr ss:[ebp+40A4E3] ; Modulo del error
00406448 57             push edi
00406449 52             push edx
0040644A 51             push ecx
0040644B 53             push ebx
0040644C FFD0           call eax ; wsprintfA
0040644E 8D9D A2A54000  lea ebx,dword ptr ss:[ebp+40A5A2] ; MessageBoxA
00406454 53             push ebx
00406455 FFB5 D7A44000  push dword ptr ss:[ebp+40A4D7]
0040645B FF95 19A64000  call dword ptr ss:[ebp+40A619] ; GetProcAddess
00406461 5B             pop ebx
00406462 8D8D EBA44000  lea ecx,dword ptr ss:[ebp+40A4EB] ; 'Entry point not found'
00406468 6A 10          push 10
0040646A 51             push ecx
0040646B 53             push ebx
0040646C 6A 00          push 0
0040646E FFD0           call eax ; MessageBoxA
00406470 FFA5 25A64000  jmp dword ptr ss:[ebp+40A625] ; ExitProcess
00406476 8BB5 15A64000  mov esi,dword ptr ss:[ebp+40A615]
0040647C 8BBD 19A64000  mov edi,dword ptr ss:[ebp+40A619]
00406482 E8 8F0C0000    call FAST_APL.00407116
00406487 61             popad
00406488 9D             popfd
00406489 50             push eax
0040648A 68 00104000    push FAST_APL.00401000
0040648F C2 0400        retn 4

En primer lugar esta rutina carga en el registro edi la RVA y la longitud la sección que antes de ser empaquetada fue semiencriptada, modificando los argumentos de los opcodes call y jmp. Debido a que toma el valor de la RVA, debe añadirla la ImageBase. La longitud del código lo almacena en el registro ecx, que usará como contador. A partir de este momento se entra en un bucle que sólo sale cuando ecx es menor o igual a 0. Dentro del bucle hay que observar que edi es usado como puntero que apunta al código que se tiene que examinar, esi se usa como un offset para resituar más rápidamente el puntero. Los registros ebx y edx se usan para cálculos de posiciones relativas, y con ellos se van a ir modificando los argumentos de los opcodes call y jmp. Concretamente el registro ebx lleva la posición relativa que se está leyendo. El registro eax se usa para cargar ahí el código que se examina, word a word.

En 406372 se carga en eax secuencialmente cada uno de los word. Y a partir de este momento se chequea si ese word contiene alguno de los opcodes que nos interesa modificar.

De 406375 a 406379 se mira si el opcode es E8h o E9h, es decir call o jmp long. Si es uno de estos dos se salta a 4063B3, donde invierte el orden del argumento (dirección), y le resta la posición relativa actual.

En 40637B comprueba si el opcode es 2517h+E8h= 25FFh (= jmp dword ptr[NN]). Lo que hace en este caso es saltar a 4063D2 donde le suma al argumento (NN) el valor del registro ebx, este registro se inicializa a 0, y cada vez que es usado se le resta 4. Por tanto la primera vez que ocurre encuentra este opcode su argumento permanece inalterado, pero la siguiente vez se le resta 4, la siguiente 8, la siguiente 0Ch, etc... Este opcode aparece casi de modo exclusivo para indexar los valores de la IAT.

De 406381 a 40638D se comprueba si el opcode es un salto relativo, verificando para ello que el opcode sea de tipo 0F80h .. 0F8Fh, los cuales corresponden a: jo, jno, jb, jnb, je, jne, jbe, ja, js, jns, jpe, jpo, jl, jge, jle, jg (todos ellos de tipo long). Al ser de dipo long, el argumento es una dirección realtiva de 32bits, igual que con call y con jmp, aunque en esta ocasión los argumentos no están invertidos, y por ello lo único que hace es restarles el valor de ebx.

Una vez rastrea todo el código lo último que le queda por hacer es construir la tabla de la IAT, tarea que normalmente hace el loader. Sin embargo, al salir de este último bloque, en 4063E2 y 404063E3 hace un call. Uno de los plugins que vimos al inicio que tenía como opción el PeCompact era el pre-operative, pero también tiene uno post-operative. Precisamente es este call el encargado de llamar al plugin post-operative. Si no hay ningún plugin post-operative el call salta a una zona que está rellenada con 0C3h (retn). Inmediatamente, al retornar del call llama a en 4063F0 a VitualFree, liberando el buffer de memoria que fue creado anteriormente.

El siguiente call que aparece llama a una rutina que se encuentra en 406492. Esta rutina es la encargada de rellenar la IAT. Antes de entrar en su análisis adelantar solamente que la rutina devuelve el flag carry a 1 si hubo algún error al cargar la IAT, y si no hay ningún error vuelve con el flag carry a 0. Si hay algún error en la construcción de la IAT se ejecuta todo el código comprendido entre 4063FD y 406470, que sencillamente muestra un MessageBoxA con el módulo y la función API donde se ha producido el error (normalmente se debe a que no se puede encontrar esa función en ese módulo), llamando finalmente a ExitProcess (ver los comentarios del código añadidos por OllyDbg para clarificar la rutina).

Si no ha habido ningún error al cargar la IAT nuevamente se llama a una rutina de carga de pulgins, situando previamente en esi y en edi LoadLibraryA y GetProcAddress. Finalmente, recupera todos los registros mediante popad y popfd. En 406489 mete en la pila el valor de eax pasado por el loader (normalmente es el EntryPoint), y el OEP (que ha sido modificado anteriormente), para salta finalmente a ese OEP mediante el retn 4. Realmente el meter eax en la pila no sirve de nada, pues el retn 4 lo saca al saltar al OEP.

Analicemos ahora la rutina que construye la IAT. Antes de analizar qué hace esta rutina deberíamos entender qué es y como funciona la Import Table. Muy por encima decir que hay 2 tipos de tablas para las importaciones. Una tabla que la Import Table Address (Tabla de direcciones de importaciones), y la Import Table como tal (Tabla de Importaciones). En la segunda hay información sobre qué módulos necesita el fichero exe, y qué funciones de esos módulos utiliza el fichero exe. La primera tabla (IAT) inicialmente en el fichero está vacía, ya que es rellenada/construida por el loader del sistema. La estructura de la Import Table es muy sencilla, ya que consta de 20 bytes por cada módulo que importa, añadiendo como marcador de final otros 20 bytes puestos a 0.

Aunque en winnt.h existen multitud de estructuras definidas que hacen referencia a la Import, no hay ninguna que defina esta estructura. Los nombres y la definición lo he tomado de un documento de Johannes Plachy, por parecerme bastante clarificador.

Typedef struct tagImportDirectory
{
     DWORD dwRVAFunctionNameList;
     DWORD dwUseless1;
     DWORD dwUseless2;
     DWORD dwRVAModuleName;
     DWORD dwRVAFunctionAddressList;
}

Este tag/estructura, se repite consecutivamente por cada uno de los módulos que el fichero exe necesite. Es esta estructura la que marca el tamaño de la Import. Es decir, si hay 4 módulos a importar, corresponderían 20 bytes de estructura a cada módulo, 20 * 4 = 80 bytes, a esto habría que sumarle una estructura de 20 bytes puestos a 0 (que es la que marca el final), y por tanto sería 80 + 20 = 100 bytes. Por tanto el tamaño de Import para ese fichero sería de 100 bytes (64h).

dwRVAModuleName es la RVA que apunta al nombre del módulo que se debe cargar (el nombre del DLL). El primer valor que aparece en la estructura es la RVA que apunta a una lista de RVAs que a su vez apuntan a los nombres de las funciones que tiene que importar de este módulo. En este punto hay que tener cuidado, pues mientras que dwRVAModuleName apunta directamente al primer carácter del nombre del módulo, los RVAs de los nombres de las funciones que hay en la lista no apuntan al primer carácter, sino a 2 caracteres antes. Finalmente, dwRVAFunctionAddressList apunta a la zona de la IAT donde se deben poner las direcciones reales que le corresponden a cada función (figura 7).

Pues bien, esta tarea, que normalmente la hace el loader del sistema, es la que hace ahora la rutina que vamos a analizar.


Fig. 7. Esquema de la estructura que forma la ImportTable. Obviamente cada módulo asigna una zona distinta dentro de la ImportAddressTable.

La rutina que tiene que hacer todo este trabajo en el unpacker del PeCompact hemos visto que es llamada en último lugar, y que empieza en 406492. Veamos su aspecto...

00406492 8BB5 37A64000      mov esi,dword ptr ss:[ebp+40A637]
00406498 0BF6               or esi,esi
0040649A 74 18              je short FAST_APL.004064B4
0040649C 8B95 A6A04000      mov edx,dword ptr ss:[ebp+40A0A6]
004064A2 03F2               add esi,edx
004064A4 E8 0F000000        call FAST_APL.004064B8
004064A9 72 0B              jb short FAST_APL.004064B6
004064AB 83C6 14            add esi,14
004064AE 837E 0C 00         cmp dword ptr ds:[esi+C],0
004064B2 75 F0              jnz short FAST_APL.004064A4
004064B4 F8                 clc
004064B5 C3                 retn
004064B6 F9                 stc
004064B7 C3                 retn
004064B8 C785 0DA64000 000> mov dword ptr ss:[ebp+40A60D],0
004064C2 8B0E               mov ecx,dword ptr ds:[esi]
004064C4 8B7E 10            mov edi,dword ptr ds:[esi+10]
004064C7 0BC9               or ecx,ecx
004064C9 75 02              jnz short FAST_APL.004064CD
004064CB 8BCF               mov ecx,edi
004064CD 03CA               add ecx,edx
004064CF 03FA               add edi,edx
004064D1 8B46 0C            mov eax,dword ptr ds:[esi+C]
004064D4 0BC0               or eax,eax
004064D6 0F84 95000000      je FAST_APL.00406571
004064DC 03C2               add eax,edx
004064DE 51                 push ecx
004064DF 52                 push edx
004064E0 8985 E3A44000      mov dword ptr ss:[ebp+40A4E3],eax
004064E6 50                 push eax
004064E7 FF95 29A64000      call dword ptr ss:[ebp+40A629]
004064ED 5A                 pop edx
004064EE 59                 pop ecx
004064EF 0BC0               or eax,eax
004064F1 0F84 7C000000      je FAST_APL.00406573
004064F7 8985 33A64000      mov dword ptr ss:[ebp+40A633],eax
004064FD 8B19               mov ebx,dword ptr ds:[ecx]
004064FF 83C1 04            add ecx,4
00406502 0BDB               or ebx,ebx
00406504 74 6B              je short FAST_APL.00406571
00406506 8BC3               mov eax,ebx
00406508 F7C3 00000080      test ebx,80000000
0040650E 74 18              je short FAST_APL.00406528
00406510 81E3 FFFF0000      and ebx,FFFF
00406516 C785 E7A44000 010> mov dword ptr ss:[ebp+40A4E7],1
00406520 899D DFA44000      mov dword ptr ss:[ebp+40A4DF],ebx
00406526 EB 14              jmp short FAST_APL.0040653C
00406528 C785 E7A44000 000> mov dword ptr ss:[ebp+40A4E7],0
00406532 03DA               add ebx,edx
00406534 43                 inc ebx
00406535 43                 inc ebx
00406536 899D DFA44000      mov dword ptr ss:[ebp+40A4DF],ebx
0040653C 51                 push ecx
0040653D 52                 push edx
0040653E 80BD C3A74000 C3   cmp byte ptr ss:[ebp+40A7C3],C3
00406545 74 14              je short FAST_APL.0040655B
00406547 53                 push ebx
00406548 FFB5 33A64000      push dword ptr ss:[ebp+40A633]
0040654E FFB5 19A64000      push dword ptr ss:[ebp+40A619]
00406554 E8 BD030000        call FAST_APL.00406916
00406559 EB 0D              jmp short FAST_APL.00406568
0040655B 53                 push ebx
0040655C FFB5 33A64000      push dword ptr ss:[ebp+40A633]
00406562 FF95 19A64000      call dword ptr ss:[ebp+40A619]
00406568 5A                 pop edx
00406569 59                 pop ecx
0040656A 0BC0               or eax,eax
0040656C 74 05              je short FAST_APL.00406573
0040656E AB                 stos dword ptr es:[edi]
0040656F EB 8C              jmp short FAST_APL.004064FD
00406571 F8                 clc
00406572 C3                 retn
00406573 F9                 stc
00406574 C3                 retn

Aunque parece una rutina larga, se trata de una pequeña rutina que un par de bucles, que se encargan de ir cargando los módulos en memoria con LoadLibraryA, y posterior ir rastreando la lista de funciones y cargándolas en la IAT mediante GetProcAddess. Bueno, veamos por partes cómo lo hace.

En primer lugar lo que hace es usar esi como un puntero apuntando a la tabla de importaciones. El valor RVA de la Import Table lo toma en 406492, y es un valor que nos va a ser tremendamente útil a la hora de reconstruir el fichero que obtengamos al volcar la memoria.

Una vez toma la dirección de la Import Table, inicia un bucle pequeño que comprende de 4064A4 a 4064B7. En este bucle lo que hace es ir cogiendo del inicio de la Import Table los tags/estructuras, terminando el bucle cuando alguna de ellas tenga dwRVAModuleName (esi+0C) a 0. El el bucle se llama a la subrutina 4064B8, que ahora veremos qué hace. Solo saber que esa subrutina devuelve el flag carry puesto a 1 si ha habido un error.

La subrutina 4064B8 es la que se tiene que encargar de leer dwRVAModuleName, dwRVAFunctionNameList e ir cargando las funciones en dwRVAFunctionAddressList .Veamos cómo lo hace.

En 4064C2 y 4064CB hace que ecx sea dwRVAFunctionNameList, y que edi sea igual a dwRVAFunctionAddressList. Si por cualquier causa dwRVAFunctionNameList es 0, hace que ecx sea igual a dwRVAFunctionAddressList. A continuación a ambos valores les suma la ImageBase, para calcular su dirección real.

Lo siguiente es cargar el dwRVAModuleName en el registro eax. Si por cualquier causa extraña este valor fuera 0, salta a 406571 poniendo el flag carry a 0, es decir, no hay error (este chequeo no tiene mucho sentido, pues este chequeo se hace en el bucle principal que lee las estructuras). A este RVA también le suma la ImageBase que está en edx, para obtener la dirección real.

Debido a que el unpacker si hay un error debe mostrar información, debe guardar qué módulo se está tratando en cada momento. Eso lo hace guardando ciertos valores en una zona de memoria. En 4064F7, por ejemplo, guarda el registro eax (que en ese momento está apuntando al nombre del módulo), de tal modo que si después se produce un error, se tomará información de ese valor guardado (ver código de las direcciones 406436 a 40644C anteriormente explicado). Después mediante GetModuleHandleA toma el valor de esa librería, si existe algún problema con esta API deja eax a 0, saltando a 406573, donde pone el flag carry a 1 (error).

Una vez hecho esto se inicia un nuevo bucle que va rastreando los RVA que hay en dwRVAFunctionNameList+ImageBase. En el momento que encuentra un RVA que sea 0 interpreta que la lista de RVAs a los nombres de funciones ha terminado. Con cada RVA calcula la dirección real del nombre de la API, para obtener su dirección y ponerla en la IAT.

004064FD 8B19      mov ebx,dword ptr ds:[ecx]
004064FF 83C1 04   add ecx,4
00406502 0BDB      or ebx,ebx
00406504 74 6B     je short FAST_APL.00406571

Enebx mete la RVA de la lista de RVAs que apuntan a los nombres de funciones, posiciona ecx al próximo RVA de la lista, y si el RVA leído es 0, salta a 406571, donde pone el flag carry a 0 (no error).

Lo siguiente que se hace con el RVA leído de la lista chequear si el bit 31 está a 1 o a 0.

00406508 F7C3 00000080   test ebx,80000000
0040650E 74 18           je short FAST_APL.00406528

Si el bit 31 está a 0 se interpreta que es un Import por nombre, y si el bit 31 está a 1 se entiende que el Import se hace por ordinales. Si es por ordinales mete un 1 en[ebp+40A4E7], y si es por nombres, mete un 0. Esta variable se usa en caso de error, para mostrar el mensaje de error (error en la función %s, o erro en el ordinal %d). Si la importación se hace por ordinales mete el valor del ordinal (limitado a FFFFh) en ebp+40A4DF, que es la variable usada también para imprimir la cadena de error si lo hubiere.

Si es por nombre, salta a la dirección 406528, donde calcula la dirección real sumando al RVA la ImageBase. A continuación mediante dos inc ebx posiciona el puntero en el primer carácter del nombre de la función (hay que recordar que la tabla de RVA apuntando a los nombres de las funciones no apuntan realmente al primer carácter, sino 2 bytes antes). Una vez posiciona bien el puntero lo guarda en [ebp+40A4DF] (esto es para poder mostrar el mensaje de error si se diera el caso).

00406510 81E3 FFFF0000      and ebx,FFFF ;ordinales limitados a 0FFFFh
00406516 C785 E7A44000 010> mov dword ptr ss:[ebp+40A4E7],1 ;importaciones por ordinales
00406520 899D DFA44000      mov dword ptr ss:[ebp+40A4DF],ebx ;ordinal actual que se está tratando
00406526 EB 14              jmp short FAST_APL.0040653C
00406528 C785 E7A44000 000> mov dword ptr ss:[ebp+40A4E7],0 ;importaciones por nombre de función
00406532 03DA               add ebx,edx
00406534 43                 inc ebx
00406535 43                 inc ebx ;posiciona el puntero en el primer char
00406536 899D DFA44000      mov dword ptr ss:[ebp+40A4DF],ebx

Antes de cargar la dirección real de la función API con GetProcAddess, debemos fijarnos que uno de los plugins del PeCompact es el GPA, es decir, un hook para el GetProcAddess. Eso es precisamente lo que se hace en la zona comprendida desde 40653E hasta 406559.

0040653E 80BD C3A74000 C3  cmp byte ptr ss:[ebp+40A7C3],C3
00406545 74 14             je short FAST_APL.0040655B
00406547 53                push ebx
00406548 FFB5 33A64000     push dword ptr ss:[ebp+40A633]
0040654E FFB5 19A64000     push dword ptr ss:[ebp+40A619]
00406554 E8 BD030000       call FAST_APL.00406916
00406559 EB 0D             jmp short FAST_APL.00406568

En este pequeño bloque lo que se hace es chequear si existe el plugin instalado (mira si la primera instrucción de la rutina del plugin es 0C3h (retn). Si no es retn el primer opcode de esa rutina mete 3 datos en la pila, y salta a la rutina del plugin, que se encuentra en 406916. Los 3 datos que mete en la pila antes de llamar al plugin son: Handle, Handle del móudlo, y ebx (que contiene el ordinal o el puntero al nombre de la función a cargar). Al volver del plugin con el jmp 406568 salta el siguiente bloque, que es la rutina normal de GetProcAddess.

La rutina normal de GetProcAddess es la siguiente:

0040655B 53              push ebx
0040655C FFB5 33A64000   push dword ptr ss:[ebp+40A633]
00406562 FF95 19A64000   call dword ptr ss:[ebp+40A619]

Consiste tan solo en dospush, que meten en la pila los argumentos para llamar después a la API GetProcAddess. Los argumentos que mete en la pila son el handle del módulo, y el puntero al nombre de la función (u ordinal de la función).

Finalmente la rutina chequea el valor devuelto por el plugin o por GetProcAddess en eax. Si eax ha vuelto con un valor igual a 0 significa que se ha producido un error al cargar esa función, saliendo de todo el bucle y poniendo el flag de carry a 1 (stc). Si no ha habido error eax contiene la dirección de la API, la cual mediante stos dword ptr es:[edi] es guardada en la posición que le corresponde de la IAT.

Y esto sería lo que hace todo el bucle que reconstruye la IAT.

 

CONCLUSIONES

Bien, quizás esta sea la parte que muchos pensabais que iba a hacer al principio. Explicar rápidamente cómo se hace un dump de un fichero comprimido con este packer, y como reconstruirlo para que sea operativo.

Para reconstruirlo lo ideal sería poder hacer un volcado una vez haya desempaquetado toda la sección con la Tabla de Importaciones, pero antes de que genere los datos con la IAT. Sin embargo sí que deberíamos tener la información de dónde ha desempacado la Tabla de Importaciones.

La idea es bien sencilla. Si nos fijamos bien, todas las secciones se encuentran desempaquetadas al llegar al call que genera la IAT. En mi ejemplo ese call está en 4063F6. Se trata de un call que es fácilmente localizable, pues justo después tiene un salto condicional si no hay carry (jnc/jnb). El salto va inmediatamente a la rutina que llama al plugin post-operative, y al salto que nos lleva al OEP.

La idea para el dump sería detener el packer justo en ese call a la rutina que genera la IAT, y no ejecutar la llamada a esa rutina (podemos nopear el call). Lo que sí debemos ejecutar antes de hacer el volcado es el call a la rutina del plugin post-operative. Por tanto deberíamos hacer el dump del proceso cuando estemos situados sobre el popad. Antes de hacer el volcado debemos mirar el dword que hay en la dirección ebp+40A637. De esa dirección es donde lee la rutina que genera la IAT la RVA de la Tabla de Importaciones. Es decir, de esa dirección obtenemos la RVA de la Tabla de Importaciones. Para calcular su longitud es sencillo. Y podemos hacerlo antes de dumpear el proceso.

Bien, pues apuntamos esa RVA, y sobre el popad podemos parchear el famoso jmp eip. Los que uséis el OllyDbg tenéis la ventaja que podéis volcar sin necesidad de parchear ;o). Y una vez volcado tendremos que editar la cabecera PE. En la parte de “Data Directory” de la cabecera PE debéis editar la variable “Import”, poniendo ahí la RVA que habéis obtenido leyendo el dword de ebp+40A637.

Para calcular la longitud de la Import es sencillo. Debeis mirar los datos que corresponden a la Import. Una vez vistos, hay que ir mirándolos de 20 en 20 bytes. Y cuando encontréis un bloque de 20 bytes que sean todos 0, ese es el final del Import. La longitud también debe contar esos 20 bytes a 0. Por tanto la longitud mínima de una Import es de 20 bytes (todos a 0). Para calcular la longitud de la Import podéis volver a leer la parte de este tutorial donde he explicado la estructura de la Tabla de Importaciones.

 

PARA LOS ERUDITOS: EL ALGORITMO DE DESCOMPRESIÓN aPLiB

Cuando empecé a mirar los algoritmos que usaba el PeCompact en la compresión observé que había una pega principalmente: Jeremy Collake (el autor del packer) había usado 2 algoritmos distintos: el suyo propio (JCALG1), y otro algoritmo de Joergen Ibsen (aPLiB). El JCALG1 no es ningún problema, pues es un algoritmo abierto, del cual J. Collake ha publicado fuentes, pero la pega principal estaba en el aPLiB, pues J. Ibsen ha publicado una librería que es pública, para comprimir y descomprimir con su algoritmo, pero no hay fuentes (por tanto no es un algoritmo abierto). Así pues me puse manos a la obra, para desentrañar los misterios del algoritmo aPLiB.

Primero deberíamos entender qué es lo que hace un empaquetador de datos. Cosa, que imagino todos sabemos, es que todos los algoritmos de compresión intentar quitar los datos que se van repitiendo, sustituyéndolos por un código de control que indica que es un dato repetido que ya se ha almacenado en otro lugar (ese dato de control debe ocupar menos que el propio dato). Esto, que en teoría es muy bonito, en la práctica es una lucha constante por encontrar un algoritmo que ocupe poco, que consuma pocos recursos en memoria, que no tenga procesos muy reiterativos, y encima que tenga un buen ratio de compresión.

El modo en que esto se hace en el aPLiB es sencilla. Podemos decir que hay unos datos que los consideraremos como datos propiamente, y otros que son datos de control. El packer lo que hace es ir intercalando esos datos de control entre los datos propiamente... Ahora veremos como funciona el algoritmo de descompresión, y lo entenderemos mucho mejor.

He de advertir que prácticamente todo el código ASM del algoritmo consiste en comprobaciones de flags, y de rotaciones. A decir verdad cuando vi el código desensamblado fui yo el primer sorprendido... solo me vino un pensamiento: “¡¡¿¿esto es la rutina??!!”

Bueno, pues veamos su aspecto:

004090E2 C8 000000  enter 0,0
004090E6 55         push ebp
004090E7 8B75 08    mov esi,dword ptr ss:[ebp+8]
004090EA 8B7D 0C    mov edi,dword ptr ss:[ebp+C]
004090ED FC         cld
004090EE B2 80      mov dl,80

Estas primeras líneas lo que hacen es inicializar los valores. En esi sitúa la dirección que contiene los datos empaquetados, y en edi la dirección donde se tienen que desempaquetar. Después limpia el flag de dirección, y mete 80h en dl (este dato es lo que va a usar como contador de 8 bits - 80h = 10000000b)

004090F0 8A06       mov al,byte ptr ds:[esi]
004090F2 46         inc esi
004090F3 8807       mov byte ptr ds:[edi],al
004090F5 47         inc edi
004090F6 02D2       add dl,dl
004090F8 75 05      jnz short FAST_APL.004090FF

El primer dato es copia desde esi a edi literalmente, es decir, el primer byte es interpretado como dato literal que se tiene que copiar. Después se actualiza el puntero edi para seguir descomprimiendo. La suma del registro dl consigo mismo es como si rotásemos ese registro de 8 bits a la izquierda. Hay que tener en cuenta que esta primera vez al sumar 10000000b con 10000000b se produce un resultado de 00000000b, poniendo el flag carry a 1.

004090FA 8A16       mov dl,byte ptr ds:[esi]
004090FC 46         inc esi
004090FD 12D2       adc dl,dl
004090FF 73 EF      jnb short FAST_APL.004090F0

Si al rotarse el registro dl se observa que su valor pasa a ser 0, se entiende que se ha leído un código completo de control. Los códigos de control son bytes, que después son interpretados bit a bit.

Bien, analicemos esta operación. Inicialmente el registro dl vale 80h, pero al hacer una suma sobre sí mismo provoca que el carry pase a valer 1 (es lo que va a usar como marca de que ha leído 8 bits) y que el registro en sí pase a valer 0. En esta situación lo que hace el algoritmo es leer un byte del código empaquetado, interpretándolo como un código de control. A ese código de control le rota a la izquierda (con adc), de tal modo que el bit 7 de dl para a ser el carry actual, y el carry anterior es puesto en el bit 0. Ese carry anterior metido en el bit 0 es lo que se usará como marca para saber que se han leído los 8 bits que contiene el código de control cargado en dl.

En este primer bloque, desde 4090F0 hasta 4090FF lo que se hace es leer un bit del código de control (de izquierda a derecha). Si ese bit es 0, interpreta que debe ser leído un byte como dato, y añadido a la cadena desempaquetada (si el bit es 0 se produce el salto de 4090FF).

Por tanto, si tenemos 3 bytes que deben ser leídos y copiados de este modo, tenemos que realmente no ocuparían 24 bits (8 bits cada uno), sino que además hay que añadir un bit “0” a cada de uno, que es el que indica que deben ser copiados de este modo... es decir, que 24 bits serían interpretados con 27 bits. Este sería el caso más nefasto, es decir, que todos los bytes sean diferentes, y deban ser leídos como argumentos.

El peor de los casos sería una secuencia a comprimir de 255 bytes (2040 bits), cuyos valores fueran (1,2,3,4...255). En ese caso cada byte se debería guardar como argumento literal, lo que implicaría que a cada byte se le añadiera el código de control “0”... dicha secuencia al “comprimirla” ocuparía los 2040bits, más 255 bits más de códigos de control. El byte 0 no lo he añadido a posta, ya que se codifica siempre con 7 bits.

Hemos de tener en cuenta que la lectura de cada bit del código de control se hace pasando ese bit por el flag carry. Y por cada lectura de un bit hay que leer si es el último y está a 1 (la marca que hemos metido con 80h para marcar el final del código de control), ya que en este caso hay que leer un nuevo código de control.

Cada vez que veamos un bloque similar a este...

00409101 02D2       add dl,dl
00409103 75 05      jnz short FAST_APL.0040910A
00409105 8A16       mov dl,byte ptr ds:[esi]
00409107 46         inc esi
00409108 12D2       adc dl,dl

...podemos traducirlo como que se ha leído un nuevo bit del código de control, y se chequea por si es el último.

0040910A 73 4A      jnb short FAST_APL.00409156

Hasta este punto se ha leído un nuevo bit (el previo leído es “1”), y ahora la comparación hace que si el segundo bit leído es “0” se salte a 409156... si el segundo bit leído es “1” se continúa la ejecución del programa.

0040910C 33C0           xor eax,eax
0040910E 02D2           add dl,dl
00409110 75 05          jnz short FAST_APL.00409117
00409112 8A16           mov dl,byte ptr ds:[esi]
00409114 46             inc esi
00409115 12D2           adc dl,dl
00409117 0F83 E1000000  jnb FAST_APL.004091FE

Aquí se mira el valor del tercer bit leído. Si es 0 (leído en total “110” se salta a 4091FE), y si es 1 (leído “111”) se continúa la ejecución.

0040911D 02D2           add dl,dl
0040911F 75 05          jnz short FAST_APL.00409126
00409121 8A16           mov dl,byte ptr ds:[esi]
00409123 46             inc esi
00409124 12D2           adc dl,dl
00409126 13C0           adc eax,eax ;primer bit del argumento
00409128 02D2           add dl,dl

A continuación se va a ejecutar este trozo de código 4 veces, para leer 4 bits consecutivos, y metiéndolos en eax. De este modo podemos decir que se ha leído “111” del código de control, y 4 bits más como argumento que se guarda en eax.

0040912A 75 05          jnz short FAST_APL.00409131
0040912C 8A16           mov dl,byte ptr ds:[esi]
0040912E 46             inc esi
0040912F 12D2           adc dl,dl
00409131 13C0           adc eax,eax ;segundo bit del argumento
00409133 02D2           add dl,dl
00409135 75 05          jnz short FAST_APL.0040913C
00409137 8A16           mov dl,byte ptr ds:[esi]
00409139 46             inc esi
0040913A 12D2           adc dl,dl
0040913C 13C0           adc eax,eax ;tercer bit del argumento
0040913E 02D2           add dl,dl
00409140 75 05          jnz short FAST_APL.00409147
00409142 8A16           mov dl,byte ptr ds:[esi]
00409144 46             inc esi
00409145 12D2           adc dl,dl
00409147 13C0           adc eax,eax ;cuarto bit del argumento
00409149 74 06          je short FAST_APL.00409151

Finalmente en el último bit leído y guardado en eax, se comprueba si el argumento es 0. Si el argumento es 0 se entiende que se debe añadir un byte 0 en la cadena desempaquetada (por tanto el byte 0 se codificaría de este modo: “111”, “0000”; es decir, ocupa 7 bits). Si el argumento es distinto de 0 es interpretado como un offset relativo y negativo desde el cual hay que tomar el dato de la cadena ya desempaquetada. Por ejemplo, si el código de control es “111”, y el argumento es “0110”, se entiende que que el dato que hay que añadir a la cadena desempaquetada es el mismo que hay en la cadena ya desempaquetada en la posición actual menos 0110b (=6h).

0040914B 57             push edi
0040914C 2BF8           sub edi,eax ;calcula la posicion actual-argumento
0040914E 8A07           mov al,byte ptr ds:[edi] ;lee el dato que hay ahí
00409150 5F             pop edi
00409151 8807           mov byte ptr ds:[edi],al ;añade el dato a la cadena desemp.
00409153 47             inc edi
00409154 EB A0          jmp short FAST_APL.004090F6 ;lee un nuevo bit de control

Si lo que se lee es el código de control “110” salta a 4091FE. Donde hace las siguientes operaciones:

004091FE 8A06           mov al,byte ptr ds:[esi]
00409200 46             inc esi
00409201 33C9           xor ecx,ecx
00409203 C0E8 01        shr al,1
00409206 74 12          je short FAST_APL.0040921A

Se lee en al un byte de las secuencia de datos comprimidos. Y mediante shr se saca el bit0 del registro al pasándolo al flag carry. Por tanto al pasa a ser un valor significativo de 7 bits. Si ese valor de al es 0, se interpreta que es el final de los datos empaquetados, saltando al final de la subrutina, que se encuentra en 40921A. Por tanto, la secuencia que indica que se ha llegado al final de los datos empaquetados sería “110”, situando en el siguiente byte un valor que será 0 ó 1 (posibles valores del valor binario “0000000x”)

00409208 83D1 02        adc ecx,2
0040920B 8BE8           mov ebp,eax
0040920D 56             push esi
0040920E 8BF7           mov esi,edi
00409210 2BF0           sub esi,eax
00409212 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[edi]
00409214 5E             pop esi
00409215 E9 DCFEFFFF    jmp FAST_APL.004090F6

Este ultimo bloque de código es sencillo. Mediante adc ecx,2 pone a ecx con un valor de 2+carry. Si seguimos examinando un poco veremos que al se usa como offset relativo para tomar el valor que hay que añadir a los datos desempaquetados, los cuales son copiados de esi a edi mediante un rep. Por tanto ecx es el counter del número de bytes que hay que copiar. Luego ya podemos interpretar la secuencia de control. La cual tendría esta forma “110”+byte_siguiente. Siendo el byte siguiente de este modo “dddddddc”. Los bits marcados con “d” indican el desplazamiento hacia atrás de la posición actual del desempaquetado de donde hay que coger los datos. Y el bit “c” indica el valor que hay que añadir a ecx (inicializado por defecto a 2). Por tanto, si c es 0 ecx vale 2, y si c es 1 ecx vale 3. Hay que tener en cuenta una cosa. Que el valor relativo que toma del argumento (el valor de 7 bits) queda también guardado en el registro ebp, este dato es importante. Pues el rango que guarda es porque da supuesta la posibilidad de que los datos dentro de ese rango se repitan nuevamente. Este valor de ebp se usa en otros códigos de control que ahora veremos. Fijémonos que este código de control con su argumento de 8 bits son en total 11 bits. En el peor de los casos es para copiar 2 bytes (16 bits), y en el mejor de los casos es para copiar 3 bytes (24 bits).

Resumiendo, ¿qué ocurre si el desempaquetador se encuentra el código de control “110”? Lee el siguiente byte, y eso es lo que toma como argumento. De ese argumento saca 1 bit que le indica si tiene que copiar 2 ó 3 bytes. Y de los 7 bits restantes del argumento saca el valor del desplazamiento relativo de donde tiene que copiar esos 2 ó 3 bytes.

Ahora veamos qué ocurre si el código de control no es “111”, ni “0”, ni “110”, sino “10”...

00409156 B8 01000000    mov eax,1 ;inicializa eax con 1
0040915B 02D2           add dl,dl
0040915D 75 05          jnz short FAST_APL.00409164 ;lee un 1 bit más
0040915F 8A16           mov dl,byte ptr ds:[esi]
00409161 46             inc esi
00409162 12D2           adc dl,dl
00409164 13C0           adc eax,eax ;el cual es añadido a eax
00409166 02D2           add dl,dl ;lee otro bit
00409168 75 05          jnz short FAST_APL.0040916F
0040916A 8A16           mov dl,byte ptr ds:[esi]
0040916C 46             inc esi
0040916D 12D2           adc dl,dl
0040916F 72 EA          jb short FAST_APL.0040915B ;si ese bit es 1 vuelve a 40915B

Bien, ahora viene una parte un poco complicada, pues hasta ahora hemos visto códigos de control de longitud fija, pero ahora entramos en códigos de control de longitud variable. Lo que hace en este punto el desempaquetador es, después de haber leído del código de control “10”, lee el siguiente bit, y lo añade a eax. Lee un bit más, el cual indica si debe continuar bits para eax, o si ya ha acabado. Si el bit que lee es 1, significa que aun quedan más bits por leer para eax, si el bit que lee es 0, indica que ya ha acabado.

Por tanto la forma de este código de control sería así: “10w1x1y...z0” De tal modo que cuando acaba la lectura de los argumentos, eax terminaría con la forma: “1wxyz” (siendo w,x,y, y z valores binarios). Por ejemplo, si se lee del código de control “10011100”, eax valdría “1010” (se toman los valores subrayados, acabando de tomar valores cuando acaba con 0.).

00409171 83E8 02        sub eax,2
00409174 75 28          jnz short FAST_APL.0040919E

Lo siguiente que se hace es mirar qué valor tiene eax. Para ellos a eax se le resta 2. Fijémonos que la longitud mínima del código de control anterior sería “10x0”. Ya que el bit x lo lee siempre para añadirlo a eax, y es después de leer ese bit cuando comprueba si el siguiente bit es el final (bit puesto a 0). Por tanto, si al llegar a este punto eax vale 2, es porque el código de control anterior ha sido necesariamente “1000”.

00409176 B9 01000000    mov ecx,1 ;inicializa ecx con 1
0040917B 02D2           add dl,dl ;lee un bit
0040917D 75 05          jnz short FAST_APL.00409184
0040917F 8A16           mov dl,byte ptr ds:[esi]
00409181 46             inc esi
00409182 12D2           adc dl,dl
00409184 13C9           adc ecx,ecx ;lo mete en ecx
00409186 02D2           add dl,dl ;lee un bit
00409188 75 05          jnz short FAST_APL.0040918F
0040918A 8A16           mov dl,byte ptr ds:[esi]
0040918C 46             inc esi
0040918D 12D2           adc dl,dl
0040918F 72 EA          jb short FAST_APL.0040917B ;si es 1 sigue leyendo para ecx

Este bloque es similar a lo que hemos visto antes para leer en eax, salvo que aquí los bits son leídos para ecx. Se van leyendo bits, y después se lee el siguiente. Si el siguiente está a 1, significa que hay que leer un bit más para ecx, y si está a 0 significa que ya se han leído todos los bits para ecx.

Por tanto la forma quedaría de este modo: “x1y1...z0”, pasando ecx a tener el valor “1xyz”.

00409191 56             push esi
00409192 8BF7           mov esi,edi
00409194 2BF5           sub esi,ebp
00409196 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
00409198 5E             pop esi
00409199 E9 58FFFFFF    jmp FAST_APL.004090F6

Este bloque final ya nos suena mucho... lo que hace es tomar el valor relativo del offset del registro ebp (este registro queda establecido por el código de control “110”), y usa ecx como contador del numero de bits que hay que copiar. Fijémonos que ahora como el valor ebp ya está establecido podemos pedir que copie 3 bytes gastando menos bits... “1000” sería el código de control mínimo. Después deberíamos indicarle cuántos bytes tiene que copiar. El mínimo son 2 (para que ecx valga 2 [10 en binario] deberíamos pasar como argumento 00), y el máximo podría ser lo que nosotros consideremos.... como valor máximo ecx podría tener un valor de 32 bits. Si nos fijamos por cada bit que metamos en ecx debemos añadir uno más de control (o 1 para indicarle que siga leyendo, o 0 para indicarle que ya ha leído todos). Es decir, que como ebp ya está establecido podemos reducir 2 bytes (16 bits) a 6 bits. Es decir, en lugar de repetir el comando “110” con el argumento de 8 bits, podríamos colocar este código de control, ya que el argumento de 8 bits de “110” quedó preservado en ecx, y en este caso solo tendríamos que indicarle el numero de bytes a copiar. En el peor de los casos sería representar ecx = 2, que nos ocuparía 2 bits... [en 6 bits representaríamos 16 bits] en el mejor de los casos sería representar que ecx = FFFFFFFFh, que nos ocuparía 64 bits [en 68 bits representaríamos 34.359.738.360 bits]).

La otra opción para este código de control es bien distinta. A este punto (40919E) se llega si eax no es 2 (y con el valor restado ya). El valor mínimo sería si el código de control fue “1010”, en cuyo caso a este punto eax llegaría con un valor igual a 1.

0040919E 48             dec eax
0040919F C1E0 08        shl eax,8

En este punto el valor que hemos leído del código de control como argumento que se pasa al registro a eax es rotado a la izquierda 8 bits. Hemos de tener en cuenta que a ese valor de eax se le ha restado ya 3, aunque realmente solo es significativo que se le ha restado 1, ya que eax es inicializado con “1”, lo cual significa que ese bit se da ya por supuesto a 1..

004091A2 8A06           mov al,byte ptr ds:[esi]
004091A4 46             inc esi

El valor de al es leído directamente como un argumento literal de 8 bits, con lo cual, mediante el argumento previo de eax

004091A5 02D2           add dl,dl ;lee un bit
004091A7 75 05          jnz short FAST_APL.004091AE
004091A9 8A16           mov dl,byte ptr ds:[esi]
004091AB 46             inc esi
004091AC 12D2           adc dl,dl
004091AE 13C0           adc eax,eax ;lo mete de argumento en eax
004091B0 8BE8           mov ebp,eax ;el valor lo guarda en ebp

Lo que se hace hasta aquí es sencillo. El valor que quiere recuperar como desplazamiento es un valor de 16 bits. Para ellos los 7 primeros los saca del argumento inmediato a “10”. Los 8 siguientes los lee como argumento literal, y el último bit lo lee también como argumento inmediato. Es un sistema un poco extraño, pero la verdad es que sí que es resultón, pues con este método el byte más significativo del número de 16 bits puede ser representado con apenas un solo bit (en el mejor de los casos), y con 14 (en el peor). Es decir, un número de 16 bits con este método queda representado con tan solo 10 bits en el mejor de los casos... pero con 23 bits en el peor...

004091B2 B9 01000000    mov ecx,1
004091B7 02D2           add dl,dl
004091B9 75 05          jnz short FAST_APL.004091C0
004091BB 8A16           mov dl,byte ptr ds:[esi]
004091BD 46             inc esi
004091BE 12D2           adc dl,dl
004091C0 13C9           adc ecx,ecx
004091C2 02D2           add dl,dl
004091C4 75 05          jnz short FAST_APL.004091CB
004091C6 8A16           mov dl,byte ptr ds:[esi]
004091C8 46             inc esi
004091C9 12D2           adc dl,dl
004091CB 72 EA          jb short FAST_APL.004091B7

Este ultimo bloque aun nos suena... pues usa el método de ir leyendo en ecx varios bits, que le van a indicar el counter de bytes que tenemos que copiar. La rutina es como las anteriores.. lee un bit, y lee el siguiente para ver si está a 0 o a 1. Si está a 1 sigue leyendo bits para ecx, y si está a 0 interpreta que ya no tiene que leer más bits para ecx.

004091CD 3D 007D0000    cmp eax,7D00
004091D2 73 1A          jnb short FAST_APL.004091EE
004091D4 3D 00050000    cmp eax,500
004091D9 72 0E          jb short FAST_APL.004091E9
004091DB 41             inc ecx
004091DC 56             push esi
004091DD 8BF7           mov esi,edi
004091DF 2BF0           sub esi,eax
004091E1 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
004091E3 5E             pop esi
004091E4 E9 0DFFFFFF    jmp FAST_APL.004090F6

El resto de la rutina es clara. Si 500h<=eax<7D00h se añade 1 al counter, y eax es interpretado como un desplazamiento relativo, desde el que se van a copiar los ecx bytes. Si eax es un valor mayor o igual a 7D00hh, se salta a 4091EE, donde sencillamente se le suman 2 al numero de bytes que hay que copiar, caso que también ocurre cuando eax es menor o igual a 7Fh.

004091E9 83F8 7F        cmp eax,7F
004091EC 77 03          ja short FAST_APL.004091F1
004091EE 83C1 02        add ecx,2
004091F1 56             push esi
004091F2 8BF7           mov esi,edi
004091F4 2BF0           sub esi,eax
004091F6 F3:A4          rep movs byte ptr es:[edi],byte ptr ds:[esi]
004091F8 5E             pop esi
004091F9 E9 F8FEFFFF    jmp FAST_APL.004090F6

En cualquier otro caso (si 500h>eax>7Fh) se copian el número de bytes indicado por ecx.

Bien, pues esto es todo... como veis es un buen galimatías de bits, en donde se ha medido milimétricamente todo... desde la longitud de los códigos de control, hasta la longitud máxima y mínima de un argumento variable. Si estáis pensando en hacer algún algoritmo de compresión, este es uno que como veis es bien sencillo de implementar, e incluso os puede dar pistas para mejorarlo.... ¿te atreves?

Toda la información contenida en este documento es meramente educativa. En ningún momento se ha pretendido violar ningún derecho ni ninguna ley de protección de datos. Los algoritmos aPLiB y JCALG1 son propiedad intelectual de sus autores. PeCompact es un producto cuyo copyright pertenece a Jeremy Collake.

 

[ Entrada | Documentos Genéricos | WkT! Web Site ]

[ Todo el ECD | x Tipo de Protección | x Fecha de Publicación | x Orden Alfabético ]

(c) Whiskey Kon Tekila [WkT!] - The Original Spanish Reversers.
Si necesitas contactar con
nosotros , lee esto antes e infórmate de cómo puedes ayudarnos