Alexm
Entra

Utiliza tu e-mail o nombre de usuario y la contraseña para identificarte

Regístrate

Recibirá un correo electrónico con un enlace de activación. El último campo es opcional


🠉

Construir el ejecutable portátil más pequeño con lenguaje ensamblador

Aprended la estructura de un ejecutable portátil creando la aplicación más pequeña con nada más que lenguaje Assembly

Hubo un tiempo en el que tenía mucho rato libre y nada mejor que hacer que aprender las complejidades de los archivos ejecutables, así que me adentré en la madriguera del conejo hasta que logré crear la aplicación de Windows más pequeña posible usando solo NASM y alrededor de 400 líneas de código.

Fue un buen ejercicio - también me llevó mucho tiempo - que me ayudó a descubrir la estructura de un ejecutable portátil, conocimiento que me resultó útil más tarde, cuando empecé a probar la ingeniería inversa o cuando tuve que crear algunas herramientas de construcción en particular.

Eventualmente, terminé con un fragmento de código muy pequeño y quizás excesivamente documentado que muestra un mensaje de "Hola mundo"; el binario resultante no supera los 2,50 KB. Lo mismo creado con Microsoft Visual Studio (modo Release) tiene 9 KB. Por supuesto, eso se debe a que MSVC agrega sus propias instrucciones de inicio

Como dije, el código está muy comentado; a algunos puede que no les plazca. Esto es para que puedas entender qué hace exactamente cada línea. Es solo un fichero, pero podéis mover las constantes, definiciones y macros dentro de otro archivo que se puede incluir en el código fuente principal.


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;                                                                                 ;;
;; EJECUTABLE PORTÁTIL EN LENGUAJE ENSAMBLADOR                                     ;;
;;                                                                                 ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;; CONSTANTES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Número de bytes en la última página de 512 bytes que cargará DOS, que es el tamaño
; de todo el bloque de DOS, que no puede exceder de 512 bytes
$LAST_PAGE_BYTES            equ     EXE_HEADER - DOS_HEADER
; El contenido de cada sección comienza en una dirección múltiplo de este valor.
; En sistemas x86 es igual al tamaño de una página de memoria
$SECT_ALIGN          		equ     4096
; Cada sección de la imagen se alinea con este límite (un tamaño de sector de disco)
$FILE_ALIGN             	equ     512
; Dirección absoluta preferida donde se cargará la aplicación en la memoria (4 MB)
$PREFERRED_ADDRESS          equ     4194304
; Número de secciones dentro de la imagen
$SECTIONS_COUNT             equ     4
; Tamaño real de los encabezados
$HEADERS_SIZE               equ     HEADERS_END - DOS_HEADER
; El tamaño real de la sección de código
$CODE_SIZE                  equ     CODE_END - CODE
; Tamaño real de la sección de datos
$DATA_SIZE                  equ     DATA_END - INIT_DATA
; Tamaño real de las secciones de importados
$IMPORTS_SIZE               equ     IMPORTS_END - IMPORT_DATA
; Tamaño de la tabla de elementos importados
$IMPORTS_TABLE_SIZE         equ     IMPORT_DATA_END - IMPORT_DATA_START
; Tamaño de los datos no inicializados en la memoria
$UDATA_SIZE                 equ     16

;; DEFINICIONES
; Alinea con el límite especificado
%define Round(Number, Boundary)     (Number + Boundary - 1)/Boundary * Boundary
; Divide el inicio de la sección por la alineación
%define Divide(BaseAddress)			(BaseAddress - CODE)/$FILE_ALIGN
; Calcula la dirección virtual (RVA) relativa al principio de las secciones
%define RVA(BaseAddress)            Divide(BaseAddress) * $SECT_ALIGN + $SECT_ALIGN
; Obtener la RVA de la dirección base especificada
%define RVA(Adress, BaseAddress)    RVA(BaseAddress) + (Adress - BaseAddress)
; Tamaño de la imagen en la memoria (todo alineado a $SECT_ALIGN)             
%define ImageSize                   RVA(IMAGE_END) + Round($UDATA_SIZE, $SECT_ALIGN)

;; MACROS
; Agrega el valor en el primer argumento tantas veces como especifique el segundo
%macro Fill 2
	times %1 db %2
%endmacro

;; ENCABEZADOS EJECUTABLES
; Cualquier archivo PE válido comenzará con el encabezado DOS, por compatibilidad con versiones anteriores
DOS_HEADER:
	; Al principio añadiremos la firma MZ (Mark Zbikowski) para mantener la
	; compatibilidad con MS-D0S (16 bytes). Si la aplicación se ejecuta en DOS, 
	; mostrará un mensaje diciendo que la aplicación solo es compatible con Windows
	.MZSignature            db          "MZ"
	; El número de bytes en la última página de 512 bytes que cargará DOS. La última
	; página no suele contener exactamente 512 bytes, por lo que especificaremos el
	; tamaño exacto aquí, que es el tamaño del encabezado de DOS más la parte de 
	; código que se ejecutará en DOS para mostrar el mensaje de incompatibilidad
	.LastPageBytes          dw          $LAST_PAGE_BYTES
	; Páginas que se cargarán en la memoria DOS. Se trata de una sola página desde 
	; la que se cargarán solo los bytes especificados en LastPageBytes. El tamaño 
	; del módulo en DOS se determina mediante la siguiente fórmula:
	; ((TotalPages * 512) - (DOSHeaderSize * 16)) - LastPageBytes
	.TotalPages             dw          1
	; Número de reasignaciones de memoria. No hay reubicación de memoria, por lo que
        ; el número de reasignaciones es 0. En DOS podemos especificar una dirección 
        ; exacta para diferentes segmentos del archivo, pero en este caso no es necesario
	.Relocations            dw          0
	; Tamaño de encabezado MZ en párrafos (16 bytes). Inmediatamente después de esto
	; la siguiente parte es el DOS con código de 16 bytes que se ejecuta cuando la 
        ; aplicación está abierta en DOS;
	.DOSHeaderSize          dw          4
	; Cantidad mínima de párrafos que se deben cargar en la memoria además del código.
	; Si en la memoria no hay al menos tantos párrafos como se especifica aquí, 
        ; el programa no se ejecutará
	.MinimumParagraphs      dw          0
	; Máximo de párrafos que pueden ser cargados por DOS en la memoria, además del 
        ; código. El sistema proporcionará tanta memoria adicional como sea necesario.
        ; No lo limitaremos
	.MaximumParagraphs      dw          65535
	; Valor inicial del registro que almacena el segmento de las pilas de memoria. Este 
        ; valor se añade al segmento donde se carga el programa y el resultado se almacena 
        ; en el registro SS
        .SSRegister             dw          0
	; Valor inicial del registro que contiene el puntero de pila (su tamaño inicial)
	.SPRegister             dw          0x00B8
	; Checksum. Suma de comprobación invertida de todas las palabras del archivo. 
        ; Por lo general, Por lo general, el cargador lo ignora
	.Checksum               dw          0
	; Valor del puntero de instrucción inicial
	.IPRegister             dw          0
	; Valor inicial (relativo) del segmento de código
	.CSRegister             dw          0
	; La dirección de la tabla de reasignaciones (si existe). Esta dirección es 
        ; relativa al principio del fichero. Como este archivo no se ejecutará en 
        ; DOS, no hay reubicaciones. Especificamos esto asignándole el valor '40h'
	.RealocationsTable      dw          0x0040
	; Número de superposiciones. o usaremos superposiciones. Este es el único 
        ; y principal programa que se carga en la memoria
        .Overlays               dw          0
	; Rellena con 8 bytes
	.RezervedWords          dq          0
	; Identificador OEM
	.OEMIdentifier          dw          0
	; Información OEM
	.OEMInfo                dw          0
	; Agrega 10 bytes nulos
	Fill                    20,         0
	; Dirección de encabezado del PE, donde se ejecutará la aplicación en modo Windows
	.PEHeader               dd          EXE_HEADER

	; Una trozo de código para mostrar un mensaje sobre la incompatibilidad en el modo DOS
	DOS_PROGRAM:
		; Mover segmento de código en segmento de datos
		push               	cs
		; Almacena los datos después del código, para ahorrar espacio
		pop                	ds
		; Carga el puntero al mensaje que se mostrará en la pantalla
		mov                	dx,			DOSMessage - DOS_PROGRAM
		; Este es el argumento para la operación de salida del interruptor 21h
		mov                	ah,         9
		; Llama al interruptor y muestra el mensaje
		int                	21h
		; Argumento de salida para el mismo interruptor
		mov                	ax,         0x4C01
		; Salida
		int					21h

		; Mensaje a mostrar en DOS cuando se ejecuta este programa
		DOSMessage:
			db				"Este programa no fue creado para su sistema!"
			db				0Dh, 0Dh, 0Ah, '$'

		; Rellena con ceros hasta el total de 64 bytes de esta parte de DOS
		Fill               	64-$+DOS_PROGRAM, 0

; Aquí definiremos el encabezado PE, que valida el archivo como ejecutable portátil
EXE_HEADER:
	; Al igual que en el encabezado de DOS, comenzamos con una firma que lo identifica como PE
	.PESignature            db          "PE", 0, 0
	; TheEl tipo de procesador para el que va destinado este binario, en nuestro caso Intel i386
	.Microprocessor         dw          0x014C
	; Número de secciones en este fichero
	.SectionsCount          dw          $SECTIONS_COUNT
	; Fecha y hora de creación del encabezado
	.CreationDateHour       dd          __POSIX_TIME__
	; Puntero a la tabla de símbolos. Los símbolos ayudarán al compilador a identificar 
        ; diferentes elementos dentro del código fuente. No se necesita ahora
	.SymbolsTable           dd          0
	; Cuántos símbolos tenemos en la tabla. No hay tabla de símbolos
	.SymbolsCount           dd          0
	; El tamaño del encabezado adicional debajo de este
	.OptionalHeaderSize     dw          SECTIONS_HEADER_TABLE - OPTIONAL_HEADER
	; Características del archivo (ver referencia)
	.Characteristics        dw          0x0002|0x0004|0x0008|0x0100|0x0200

; Encabezado opcional (de hecho, obligatorio) definirá la estructura del ejecutable
OPTIONAL_HEADER:
	; Al igual que antes, comenzamos con un número que identifique este encabezado
	.OptHeaderSignature     dw          0x010B
	; La versión principal del vinculador. Este programa no tiene tal cosa
	.LinkerMajorVersion     db          0
	; La versión menor del enlazador
	.LinkerMinorVersion     db          0
	; El tamaño de las secciones de código de la imagen
	.CodeSize               dd          Round($CODE_SIZE, $SECT_ALIGN)
	; El tamaño de las secciones de datos inicializadas
	.DataSize               dd          Round($DATA_SIZE, $SECT_ALIGN)
	; El tamaño de las secciones de datos no inicializados (BSS). Estas secciones no ocupan
	; ningún espacio dentro del archivo, pero el sistema asignará la cantidad de memoria
        ; necesaria al ejecutar el programa
	.UdataSize              dd          Round($UDATA_SIZE, $SECT_ALIGN)
	; La dirección relativa del punto de entrada de la aplicación. Como se encuentra en la
	; primera sección de la memoria y comienza en $SECT_ALIGN, la dirección del punto
	; de entrada es la misma $SECT_ALIGN
	.EntryPoint             dd          RVA(CODE)
	; La dirección virtual relativa (RVA) de la sección de código
	.CodeBase               dd          RVA(CODE)
	; La RVA de la sección de datos
	.DataBase               dd          RVA(INIT_DATA)
	; La dirección preferida en la que se va a cargar la imagen. Para la imagen EXE, esto es
	; 0x00400000, para un fichero DLL es 0x1000000. Encima de esta dirección se agrega el
        ; código y la base de datos anterior
	.MemoryImageBase        dd          $PREFERRED_ADDRESS
	; Alineación de secciones. El valor predeterminado es 0x1000 (4096). Cada sección comienza 
	; en una dirección virtual múltiplo de este valor
	.SectionAlignment       dd          $SECT_ALIGN
	; Alineación de archivos. El valor recomendado es 0x0200. Cada sección dentro del archivo 
	; está alineada con este valor
	.FileAlignment          dd          $FILE_ALIGN
	; Indica la versión principal del sistema operativo mínimo aceptado. En nuestro caso,
	; esto puede ejecutarse en Windows 95 y versiones más recientes
	.MajorOSVersion         dw          4
	; La versión secundaria del sistema operativo
	.MinorOSVersion         dw          0
	; Versión principal de la imagen
	.MajorImageVersion      dw          0
	; Versión secundaria de la imagen
	.MinorImageVersion      dw          0
	; La versión principal del subsistema. El valor mínimo aceptado es 3
	.MajorSubsysVersion     dw          3
	; La versión secundaria del subsistema. El valor mínimo aceptado es 10
	.MinorSubsysVersion     dw          10
	; Versión WIN32, siempre 0
	.Win32Version           dd          0
	; Tamaño de la imagen del archivo, incluso encabezados y alineado con $SECT_ALIGN
	.ImgSize                dd          ImageSize
	; Tamaño de todos los encabezados, incluso los encabezados de las secciones, alineados 
        ; con $FILE_ALIGN
	.HeadersSize            dd          Round($HEADERS_SIZE, $FILE_ALIGN)
	; Suma de comprobación que valida la integridad de la imagen. Por lo general, 0
	.Checksum               dd          0
	; El subsistema utilizado para la interfaz de usuario, en nuestro caso, la consola
	.InterfaceSubsystem     dw          3
	; Características del DLL. No es DLL, por lo que 0
	.DLLAttributes          dw          0
	; La cantidad de memoria reservada por el sistema para la pila
	.ReservedStack          dd          4096
	; La cantidad inicial de memoria reservada por el sistema para la pila local
	.LocalStack             dd          4096
	; Tamaño inicial de la memoria libre
	.ReservedFreeMemory     dd          65536
	; Memoria inicial libre para la imagen
	.FreeMemory             dd          0
	; Puntero de depuración, obsoleto
	.DebuggingPointer       dd          0
	; Cantidad de  entradas en el DATA_TABLE. 16 se usa la mayoría de las veces
	.DataDirectories        dd          16
	; Dirección de la tabla de elementos exportados. Esto no es un archivo DLL, no se exporta nada
	.ExportTable            dd          0
	; El tamaño de la tabla de elementos exportados
	.ExportTableSize        dd          0
	; La dirección de la tabla de elementos importados, donde tenemos saltos elementos de 
        ; otros módulos
	.ImportTable            dd          RVA(KERNEL32_ITABLE, IMPORT_DATA)
	; Tamaño de la tabla de elementos importados
	.ImportTableSize        dd          $IMPORTS_TABLE_SIZE
	; 112 bytes reservados
	Fill                    112,        0

; Esta tabla contiene los encabezados de las secciones dentro de esta imagen y sus atributos
SECTIONS_HEADER_TABLE:
	; El encabezado de la sección de código, donde se encuentra el código ejecutable. 
        ; Por lo general, esta sección se llama texto, pero la llamaremos 'code'
	CODE_SECTION_HEADER:
		; Nombre de esta sección. No puede tener más de 8 caracteres
		.Name               db          ".code", 0, 0, 0
		; Tamaño virtual (en memoria) de esta sección. Si es mayor que el tamaño del disco, 
                ; se rellenará con 0
		.SectionSize        dd          Round($CODE_SIZE, $SECT_ALIGN)
		; RVA de esta sección
		.SectionStart       dd          RVA(CODE)
		; Tamaño real alineado de esta sección
		.RealSize           dd          Round($CODE_SIZE, $SECT_ALIGN)
		; Dirección de inicio real
		.StartAddress       dd          CODE
		; Dirección de la tabla de reubicación
		.RelocationsTable   dd          0
		; Puntero de fichero al principio de las entradas de número de línea de la sección.
		; Si no hay números de línea COFF, este valor es cero
		.LineNumbers        dd          0
		; Total dereubicaciones
		.RelocationsTotal   dw          0
		; El número de entradas de número de línea para la sección
		.LineNumbersTotal   dw          0
		; Características de esta sección. Se puede leer, escribir y ejecutar
		.Characteristics    dd          0x00000020|0x20000000|0x40000000|0x80000000

	; Encabezado de la sección de importaciones (consulta el primer encabezado para comentarios)
	IMPORT_SECTION_HEADER:
		.Name               db          ".idat", 0, 0, 0
		.SectionSize        dd          Round($IMPORTS_SIZE, $SECT_ALIGN)
		.SectionStart       dd          RVA(IMPORT_DATA)
		.RealSize           dd          Round($IMPORTS_SIZE, $FILE_ALIGN)
		.StartAddress       dd          IMPORT_DATA
		.RelocationsTable   dd          0
		.LineNumbers        dd          0
		.RelocationsTotal   dw          0
		.LineNumbersTotal   dw          0
		.Characteristics    dd          0x00000040|0x40000000|0x80000000

	; Encabezado de sección de datos inicializados
	DATA_SECTION_HEADER:
		.Name               db          ".data", 0, 0, 0
		.SectionSize        dd          Round($DATA_SIZE, $SECT_ALIGN)
		.SectionStart       dd          RVA(INIT_DATA)
		.RealSize           dd          Round($DATA_SIZE, $FILE_ALIGN)
		.StartAddress       dd          INIT_DATA
		.RelocationsTable   dd          0
		.LineNumbers        dd          0
		.RelocationsTotal   dw          0
		.LineNumbersTotal   dw          0
		.Characteristics    dd          0x00000040|0x40000000|0x80000000

	; Encabezado de sección de datos no inicializados
	NULL_DATA_HEADER:
		.Name               db          ".null", 0, 0, 0
		.SectionSize        dd          Round($UDATA_SIZE, $SECT_ALIGN)
		.SectionStart       dd          RVA(NULL_DATA)
		.RealSize           dd          0
		.StartAddress       dd          0
		.RelocationsTable   dd          0
		.LineNumbers        dd          0
		.RelocationsTotal   dw          0
		.LineNumbersTotal   dw          0
		.Characteristics    dd          0x00000080|0x40000000|0x80000000

	;; Aquí es donde terminan todas las declaraciones de encabezados
	HEADERS_END:

;; Alineación con el tamaño de la página
align   $FILE_ALIGN

;; SECCIÓN DE CÓDIGO
; Para procesadores de 32 bits
use32
; El RVA de las variables globales no inicializadas, de la sección BSS. La primera variable
; contendrá el controlador del dispositivo de salida
OutputHandler               equ RVA(NULL_DATA) + 0
; Esto almacenará la cantidad de caracteres que genera WriteConsoleW
WrittenChars                equ OutputHandler + 4

; Todo el código que se va a ejecutar está aquí
CODE:
	; En primer lugar, necesitaremos el controlador del dispositivo de salida
	call    GetOutputHandler
	; Agregua el mensaje que se mostrará a la pila
	push    dword $PREFERRED_ADDRESS + RVA(HelloMessage, INIT_DATA)
	; La cantidad de caracteres que se mostrarán
	push    13
	; Llama a la función que mostrará el texto
	call    ShowText

	; Termina la aplicación correctamente
	jmp     CloseApplication

	; Esta parte del código obtendrá un controlador y lo almacenará en OutputHandler
	GetOutputHandler:
		; El argumento que indica lo que queremos, que es un dispositivo de salida
		push                    -11
		; Ahora, llama a la función desde Kernel32.dll
		call                    dword [$PREFERRED_ADDRESS + RVA(F_GetStdHandle, IMPORT_DATA)]
		; Si el valor invertido es menor que 1, tenemos un problema y la aplicación debe salir
		cmp                     eax,    1
		; Guarda el registro BX para restaurarlo en caso de éxito
		push                    ebx
		; Si hay un error, forzaremos el final del proceso y el código de salida será 1
		mov                     ebx,    1
		; Salto a la función que finaliza la aplicación
		jl                      CloseApplication
		; Restaura el registro BX, antes de recibir el código de salida
		pop                     ebx
		; Guarda el controlador en BSS
		mov                     dword [$PREFERRED_ADDRESS + OutputHandler], eax
		; Vuelve al lugar desde el que se ha llamado a esta función
		ret                     4

	; Esta función mostrará un texto en la consola
	ShowText:
		; Guarda el registro EBP para restaurarlo más tarde
		push                    ebp
		; Mueve ESP en EBP para leer los argumentos recibidos
		mov                     ebp,    esp
		; Argumento NULL reservado para la función WriteConsoleW
		push                    0
		; Aquí es donde se guardará la cantidad de caracteres escritos
		push                    dword $PREFERRED_ADDRESS + WrittenChars
		; La cantidad de caracteres que se van a escribir, del segundo argumento
		push                    dword [ebp + 8]
		; El puntero al texto, del primer argumento de la pila
		push                    dword [ebp + 12]
		; El controlador al dispositivo de salida
		push                    dword [$PREFERRED_ADDRESS + OutputHandler]
		; Llama a la función que mostrará el texto
		call                    dword [$PREFERRED_ADDRESS + RVA(F_WriteConsoleW, IMPORT_DATA)]
		; Restaura EBP
		pop                     ebp
		; Vuelve y al mismo tiempo restaura la pila
		ret                     8

	; Esta función finalizará la solicitud y devolverá el código de salida en EBX
	CloseApplication:
		; Almacene el valor BX en la pila, es el código de salida
		push                    ebx
		; Llame a la función que terminará el proceso con el código en EBX
		call                    dword [$PREFERRED_ADDRESS + RVA(F_ExitProcess, IMPORT_DATA)]
		; Si esto no funcionó, la pila se restaura y simplemente regresamos
		pop                     ebx
		; Regresa
		ret

; La sección de código está alineada a 512 bytes
align   $FILE_ALIGN
CODE_END:

; Aquí tenemos todos los datos y funciones importadas de librerías dinámicas
IMPORT_DATA:
	; Esta es la biblioteca principal de Windows y proporcionará funciones esenciales
	KERNEL32_LIBRARY			db         'kernel32.dll', 0

	IMPORT_DATA_START:

	; Aquí describimos las características de la tabla de importaciones para la librería KERNEL32.DLL
	KERNEL32_ITABLE:
	    ; RVA de la tabla de búsqueda de importaciones
		.originalfthk		    dd         0
		; Marca de fecha de hora sin enlazar, se actualizará a la marca de fecha de fecha de la DLL
		.timedate               dd         0
		; Índice de la primera referencia de la cadena de transitarios
		.forwarder              dd         0
		; Nombre de la biblioteca que se va a importar
		.name                   dd         RVA(KERNEL32_LIBRARY, IMPORT_DATA)
		; RVA de la tabla de direcciones de importaciones
		.firstthunk             dd         RVA(KERNEL32_IMPORTED_FUNCTIONS, IMPORT_DATA)
		; El final de la tabla para la librería KERNEL32.DLL
		Fill                    20,        0

	; Aquí tenemos las direcciones de las funciones importadas
	KERNEL32_IMPORTED_FUNCTIONS:
		; Función para obtener el controlador para el dispositivo estándar especificado
		F_GetStdHandle:         dd         RVA(I_GetStdHandle, IMPORT_DATA)
		; Función que muestra un texto en la consola
		F_WriteConsoleW:        dd         RVA(I_WriteConsoleW, IMPORT_DATA)
		; Esta función cierra el proceso y devuelve un código de salida
		F_ExitProcess           dd         RVA(I_ExitProcess, IMPORT_DATA)
		; Byte reservado
		ReservedBytes           dd         0

	; Nombres de los elementos importados de la biblioteca anterior
	KERNEL32_IMPORTED_ELEMENTS:
		I_GetStdHandle:
			dw                  0
			db                  'GetStdHandle',  0
			align               2
		I_WriteConsoleW:
			dw                  0
			db                  'WriteConsoleW', 0
			align               2
		I_ExitProcess:
			dw                  0
			db                  'ExitProcess',   0
			align				2

	IMPORT_DATA_END:

align   $FILE_ALIGN
IMPORTS_END:

; Datos inicializados, normalmente constantes como mensajes de texto estáticos
INIT_DATA:
	; Mensaje de hola para mostrar
	HelloMessage:    			dw          __utf16__("¡Hola mundo!"), 0Ah
; Alineación a 512 bytes
align   $FILE_ALIGN
DATA_END:

; Sección BSS, con datos no inicializados
NULL_DATA:
; Fin de la imagen
IMAGE_END:

Construirlo es muy sencillo. Solo aseguraros de tener NASM instalado y agregado a la ruta del entorno. Ejecutad el siguiente comando

nasm main.asm -f bin -o app.exe

Esto creará un ejecutable x86 para procesadores de 32 bits. Algún día intentaré crear la versión de 64 bits y quiero agregar algunas funcionalidades más (como obtener los argumentos de la línea de comandos). Veamos a dónde va esto.

Podéis obtener más información sobre el formato PE aquí .