martes, 30 de agosto de 2011

Hacer un ejecutable aun más pequeño

En el anterior post se mostraba cómo configurar Visual Studio para generar ejecutables pequeños de 1 kb. Es posible que un ejecutable sea todavía mas pequeño. Aunque ya hay que meterse en ensamblador y usar un editor hexadecimal.

Lo único que he encontrado al respecto es este artículo de Solar Eclipse.
http://www.phreedom.org/solar/code/tinype/

En él llega a un ejecutable de 97 bytes pero tras probarlo en diferentes versiones de Windows he constatado que solo funciona en versiones anteriores a Windows XP SP2, en esta versión y en posteriores no funciona, ni este ejecutable de 97 bytes, ni otros tantos del mismo artículo. También hay problemas con las versiones de Windows de 64 bits, donde el loader tiene comportamiento ligeramente diferente. Veámoslo.

En el artículo de Solar Eclipse, lo primero que hace es eliminar la librería Runtime de C para conseguir un ejecutable de 1k, como se hizo en el anterior artículo.

Lo siguiente que hace es reducir el valor de SectionAlignment mediante el parámetro /ALIGN:1 (Explicación en MSDN).

El problema es que VC++ 9.0 devuelve un error al intentar usar dicho parámetro:

Error    1    fatal error LNK1164: alineación de la sección 0x2 (2) superior al valor /ALIGN

El mínimo valor que deja usar es 4 y aun así modifica solo el valor de SectionAlignment dejándolo a 4, pero no modifica el valor de FileAlignment (como si hacía VC 6.0) para dejarlo también a 4.

image

Esto hace que el ejecutable no cumpla el estándar (Microsoft Portable Executable and Common Object File Format Specification), el cúal dice claramente:
”The section align must be greater than or equal to FileAlignment.”

“If the SectionAlignment is less than the architecture’s page size (0x1000), then FileAlignment must match SectionAlignment.”

En este caso en el que SectionAlignment es menor que 0x1000 FileAlignment también debe valer 4. Si cambiamos el valor de FileAlignment veremos como el ejecutable si funciona en todos los Windows >= Windows XP excepto en las versiones de x64 bits.

¿Por qué no funciona en las versiones de 64 bits? No se la razón exacta, solo se que para que funcione correctamente es necesario que SizeOfImage sea 0x1000 mayor de lo que debería ser (Extraño, si alguien sabe el por qué exacto que no dude en decirlo). Esto solo se da cuando SectionAlignment es menor que el page size.

Continuemos con el artículo de Solar Eclipse.

 

“Switching to assembly and removing the DOS stub”

El siguiente paso que toma para reducir el tamaño de un ejecutable es utilizar ensamblador, NASM. Es curioso que en el propio código en ensamblador del programa defina los campos de la cabecera PE. De este modo evita el uso del linker, que debería ser el que ensambla el ejecutable, crea la cabecera PE, las secciones, incluye los recursos, etc… Con esto logra una gran flexibilidad a la hora de generar un ejecutable con una cabecera personalizada.

Aunque de nuevo nos encontramos con problemas. Ya que en la primera revisión de 2009 de Nasm se introdujo el macro SECTALIGN. El código de Solar Eclipse, usa una variable con el mismo nombre, NASM nos da un error al ser una palabra reservada. Así que es necesario renombrar la variable SectAlign.

Este ejecutable, el de 356 bytes, no funciona en las versiones de 64 bits por lo que he explicado antes. Se puede solucionar cambiando esta línea:

dd round(filesize, sectalign) ; SizeOfImage

Por esta:

dd round(filesize, sectalign) + 0x1000 ; SizeOfImage

Así nos funcionará en todas las versiones de Windows >= XP tanto en 32 como en 64 bits.

tiny_356_mod.asm | tiny_356_mod.exe

 

“Collapsing the MZ header”

En este apartado superpone la cabecera PE con la cabecera MZ. Sitúa la cabecera Pe en el offset 4 haciendo que varios campos de ambas cabeceras se superpongan. El único campo importante de la cabecera MZ, aparte del magic number, es el e_lfanew, en el offset 0x3C. Este campo coincide con el campo de la cabecera PE, SectionAlignment. Da a ambos campos el valor de 4.

Para entender mejor esto en el desparecido blog de Ero Carrera se pueden ver algunos gráficos explicando los campos de este ejecutable.
http://web.archive.org/web/20081011023041/http://blog.dkbza.org/2007/03/tiny-and-crazy-pe.html

Como en el anterior apartado para que funcione en todos los Windows es necesario cambiar el valor del SizeOfImage.

tiny_296_mod.asm | tiny_296_mod.exe

 

“Removing the data directories”

Este apartado será el último que seremos capaces de realizar si queremos que el ejecutable funcione en todas las versiones de Windows.

La modificación que realiza aquí consiste en eliminar el directorio de datos, ya que no se están usando.

dd 0             ; NumberOfRvaAndSizes

Ahorrando así, 16x8=128 bytes.

Pero el ejecutable resultante no funciona en Windows XP SP2 y en siguientes versiones de Windows. El mensaje es curioso:

image
¿128 bytes demasiado extenso? :P

El problema en esta ocasión parece estar en que a pesar de que se ha puesto en el campo “NumberOfRvaAndSizes” un 0, el Loader espera que en disco esté toda la tabla de directorios, aunque esté vacía.

Así este ejecutable funciona:

image

Pero este otro no:

image

Fijaos que los últimos 4 ceros perteneces al Overlay del ejecutable. No pertenecen a ninguna sección, ni se ven reflejados en ningún campo del PE. Un tanto enigmático…

De este modo llegamos al ejecutable mas pequeño que funciona en todas las versiones de Windows superiores a XP, incluida.

; tiny.asm

BITS 32

;
; MZ header
;
; The only two fields that matter are e_magic and e_lfanew

mzhdr:
    dw "MZ"       ; e_magic
    dw 0          ; e_cblp UNUSED

;
; PE signature
;

pesig:
    dd "PE"       ; e_cp, e_crlc UNUSED       ; PE signature

;
; PE header
;

pehdr:
    dw 0x014C     ; e_cparhdr UNUSED          ; Machine (Intel 386)
    dw 0          ; e_minalloc UNUSED         ; NumberOfSections, 0
code:
    dd 0xC3582A6A ; e_maxalloc, e_ss UNUSED   ; TimeDateStamp UNUSED     ; Code (push byte 42, pop eax, ret)
    dd 0          ; e_sp, e_csum UNUSED       ; PointerToSymbolTable UNUSED
    dd 0          ; e_ip, e_cs UNUSED         ; NumberOfSymbols UNUSED
    dw opthdrsize ; e_lsarlc UNUSED           ; SizeOfOptionalHeader
    dw 0x103      ; e_ovno UNUSED             ; Characteristics

;
; PE optional header
;

filealign equ 4
sectalignn equ 4   ; must be 4 because of e_lfanew

%define round(n, r) (((n+(r-1))/r)*r)

opthdr:
    dw 0x10B      ; e_res UNUSED              ; Magic (PE32)
    db 8                                      ; MajorLinkerVersion UNUSED
    db 0                                      ; MinorLinkerVersion UNUSED
    dd round(4, filealign)                    ; SizeOfCode UNUSED
    dd 0          ; e_oemid, e_oeminfo UNUSED ; SizeOfInitializedData UNUSED
    dd 0          ; e_res2 UNUSED             ; SizeOfUninitializedData UNUSED
    dd code                                   ; AddressOfEntryPoint
    dd code                                   ; BaseOfCode UNUSED
    dd round(filesize, sectalignn)             ; BaseOfData UNUSED
    dd 0x400000                               ; ImageBase
    dd sectalignn ; e_lfanew                  ; SectionAlignment
    dd filealign                  ; FileAlignment
    dw 4                          ; MajorOperatingSystemVersion UNUSED
    dw 0                          ; MinorOperatingSystemVersion UNUSED
    dw 0                          ; MajorImageVersion UNUSED
    dw 0                          ; MinorImageVersion UNUSED
    dw 4                          ; MajorSubsystemVersion
    dw 0                          ; MinorSubsystemVersion UNUSED
    dd 0                          ; Win32VersionValue UNUSED
    dd round(filesize, sectalignn) + 0x1000; SizeOfImage
    dd round(hdrsize, filealign)  ; SizeOfHeaders
    dd 0                          ; CheckSum UNUSED
    dw 2                          ; Subsystem (Win32 GUI)
    dw 0x400                      ; DllCharacteristics UNUSED
    dd 0x100000                   ; SizeOfStackReserve UNUSED
    dd 0x1000                     ; SizeOfStackCommit
    dd 0x100000                   ; SizeOfHeapReserve
    dd 0x1000                     ; SizeOfHeapCommit UNUSED
    dd 0                          ; LoaderFlags UNUSED
    dd 0                          ; NumberOfRvaAndSizes UNUSED

;
; Data directories
;

    times 16 dd 0, 0 ; Empty Directory data, it's needed in Windows >= XP SP2
    dd 0, 0, 0, 0  ; Compatibility with x64 Windows

opthdrsize equ $ - opthdr

hdrsize equ $ - $$

filesize equ $ - $$

Una última cosa. Para que funcione en Windows de 64 bits requiere 16 bytes más a continuación de la tabla de directorios :|

tiny-Compatible.asm | tiny-Compatible.exe

268 bytes, esto es lo mínimo que puede ocupar un ejecutable que funcione en XP, Vista y W7 tanto en versiones de 32 como de 64 bits.

4 bytes de la cabera MZ + 24 bytes de la cabecera PE + 96 bytes de la cabecera PE Opcional + 128 bytes de la tabla de directorios (compatibilidad >= XP SP2) + 16 bytes (compatibilidad x64) = 268 bytes.

 

Documentación relacionada:

Después en el artículo para conseguir dejar el exe en 97 bytes superpone la tabla de secciones sobre la cabecera PE. Otra opción hubiese sido eliminar la tabla de secciones y utilizar un ejecutable sin ninguna sección. Esto está explicado en esta presentación de Alexander Liskin. PE: Specification vs. Loader.

Otro documento muy interesante en el que se lleva el formato PE es el white papper Undocumented PECOFF de ReversingLabs, presentado este año en la BlackHat USA.

 

Ha sido un artículo un poco pesado, con bastantes cosas “mágicas” que no se sabe muy bien por qué suceden. Para entender bien la implementación del loader de windows habría que realizar ingeniería inversa al mismo, ¿eso es legal?. Todas las demás conclusiones pueden estar equivocadas.

Si alguien logra un ejecutable menor de 268 bytes compatible con "todos" los Windows o hay alguna parte equivocada os animo a corregirme en los comentarios.
Saludos!

No hay comentarios:

Publicar un comentario