A fost o vreme când aveam mult timp liber și nimic mai bun de făcut decât să mă apuc de învățat secretele fișierelor executabile, așa că m-am aprofundat în structura acestora până când am reușit să creez cea mai mică aplicație Windows posibilă folosind doar NASM și undeva la 400 de linii de cod.
A fost un exercițiu bun - și a luat ceva timp - care m-a ajutat să înțeleg structura unui executabil portabil, cunoștințe care mi-au venit la îndemână mai târziu, când am început să încerc marea cu degetul în domeniul ingineriei inverse sau când a trebuit să creez anumite instrumente de construcție.
În cele din urmă, m-am ales cu o bucățică de cod și poate excesiv de documentată, care arată un mesaj "Hello world"; binarul rezultat nu depășește 2,50 KB. Același lucru construit cu Microsoft Visual Studio (modul Release) are 9 KB. Desigur, acest lucru se datorează faptului că MSVC adaugă propriile instrucțiuni de pornire.
După cum am spus, codul este comentat la greu; unora s-ar putea să nu le placă. Am făcut asta ca să puteți înțelege ce face exact fiecare linie. Este un singur fișier, dar puteți muta constantele, definițiile și macrocomenzile în altul care poate fi inclus în sursa principală.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; ;;
;; EXECUTABIL PORTABIL IN ASSEMBLY ;;
;; ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; CONSTANTE ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Numărul de octeți din ultima pagină de 512 octeți care urmează să fie încărcată de
; DOS, și anume dimensiunea întregului bloc DOS, care nu poate depăși 512 octeți
$LAST_PAGE_BYTES equ EXE_HEADER - DOS_HEADER
; Conținutul fiecărei secțiuni începe de la o adresă care este un multiplu al acestei valori.
; Pe sistemele x86 este egal cu dimensiunea unei pagini de memorie
$SECT_ALIGN equ 4096
; Fiecare secțiune de imagine este aliniată la această limită (o dimensiune a sectorului de disc)
$FILE_ALIGN equ 512
; Adresa absolută preferată unde aplicația va fi încărcată în memorie (4MB)
$PREFERRED_ADDRESS equ 4194304
; Numărul de secțiuni în interiorul imaginii
$SECTIONS_COUNT equ 4
; Dimensiunea reală a anteturilor
$HEADERS_SIZE equ HEADERS_END - DOS_HEADER
; Dimensiunea reală a secțiunii de cod
$CODE_SIZE equ CODE_END - CODE
; Dimensiunea reală a secțiunii de date
$DATA_SIZE equ DATA_END - INIT_DATA
; Dimensiunea reală a secțiunii de importări
$IMPORTS_SIZE equ IMPORTS_END - IMPORT_DATA
; Mărimea tabelului de elemente importate
$IMPORTS_TABLE_SIZE equ IMPORT_DATA_END - IMPORT_DATA_START
; Dimensiunea datelor neinițializate din memorie
$UDATA_SIZE equ 16
;; DEFINITII
; Alinierea la limita specificată
%define Round(Number, Boundary) (Number + Boundary - 1)/Boundary * Boundary
; Împarte începutul secțiunii la aliniere
%define Divide(BaseAddress) (BaseAddress - CODE)/$FILE_ALIGN
; Calculează adresa virtuală relativă la începutul secțiunilor
%define RVA(BaseAddress) Divide(BaseAddress) * $SECT_ALIGN + $SECT_ALIGN
; Obține AVR-ul adresei de bază specificată
%define RVA(Adress, BaseAddress) RVA(BaseAddress) + (Adress - BaseAddress)
; Mărimea imaginii în memorie (totul aliniat la $SECT_ALIGN)
%define ImageSize RVA(IMAGE_END) + Round($UDATA_SIZE, $SECT_ALIGN)
;; MACROURI
; Adaugă valoarea primului argument de atâtea ori cât specifică al doilea argument
%macro Fill 2
times %1 db %2
%endmacro
;; ANTETURI ALE EXECUTABILULUI
; Orice fișier PE valid va începe cu antetul DOS, pentru compatibilitate retroactivă
DOS_HEADER:
; La început adăugăm semnătura MZ (Mark Zbikowski) pentru a reține compatibilitatea
; cu MS-D0S (16 biți). Dacă aplicația rulează în DOS (:D), se va afișa
; un mesaj despre faptul că aceasta nu este compatibilă decât cu Windows
.MZSignature db "MZ"
; Numărul de biți în ultima pagină de 512 biți pe care DOS o va încărca. Ultima
; pagină de obicei nu conține exact 512 biți, așa că vom specifica mărimea
; exactă aici, și anume mărimea antetului DOS plus porțiunea de cod caree va
; va fi executată în DOS pentru a afișa mesajul despre incompatibilitate
.LastPageBytes dw $LAST_PAGE_BYTES
; Paginile care vor fi încărcate în memoria DOS. Este vorba despre o singură
; pagină de unde se vor încărca doar biții specificați în LastPageBytes.
; Mărimea modulului în DOS se calculează folosind următoarea formulă:
; ((TotalPages * 512) - (DOSHeaderSize * 16)) - LastPageBytes
.TotalPages dw 1
; Numărul de realocări de memorie. Nu există relocări, așa că numărul de
; relocări este 0. În DOS putem specifica adresa exact pentru diferite segmente
; ale fișierului dar în acest caz nu este nevoie
.Relocations dw 0
; Mărime antet MZ în paragrafe (16 biți). Imediat după aceasta, următoare porțiune
; este antetul DOS cu code pe 16-biți care rulează când aplicația se deschide în ... DOS
.DOSHeaderSize dw 4
; Numărul minim de paragrafe ce trebuie încărcate în memorie pe lângă cod.
; Dacă în memorie nu există cel puțin atâtea paragrafe cât se specifică aici,
programul nu va rula
.MinimumParagraphs dw 0
; Numărul maxim de paragrafe care pot fi încărcate în memorie, în afară de cod.
; Sistemul va pune la dispoziție atâta memorie cât este necesar. Nu vom limita aceasta
.MaximumParagraphs dw 65535
; Valoarea inițială a registrului care stochează segmentul stivei de memorie. Această
; valoarea este adăugată segmentului în care se încarcă programul iar rezultatul
; este stocat în registrul SS
.SSRegister dw 0
; Valorea inițială a registrului conținând indicatorul stivei (mărimea sa inițială)
.SPRegister dw 0x00B8
; Sumă de verificare. Suma de verificare inversată a tuturor datelor din fișier.
; Este în general ignorată de încărcătorul de program
.Checksum dw 0
; Valoarea inițială a indicatorului de instrucțiuni
.IPRegister dw 0
; Valoarea (relativă) inițială a segmentului de cod
.CSRegister dw 0
; Adresa tabelului de realocări (dacă există). Această adresă este relativă la
; începutul fișierului. Cum fișierul nu va rula în DOS, acesta nu are realocări.
; Specificăm aceasta folosind valoarea '40h'
.RealocationsTable dw 0x0040
; Numărul de suprapuneri. Nu folosim suprapuneri. Acesta este singurul și
; principalul program ce va fi încărcat în memorie
.Overlays dw 0
; Umple cu 8 biți
.RezervedWords dq 0
; Identificator OEM
.OEMIdentifier dw 0
; Informații OEM
.OEMInfo dw 0
; A daugă 10 biți nuli
Fill 20, 0
; Adresa antetului PE, unde aplicația va rula în modul Windows
.PEHeader dd EXE_HEADER
; O porțiune de cod ce va afișa un mesaj despre incompatibilitatea în modul DOS
DOS_PROGRAM:
; Mută segmentul de cod în segmentul de date
push cs
; Stochează datele după cod, pentru a economisi spațiu
pop ds
; Încarcă indicatorul mesajului ce va fi afișat pe ecran
mov dx, DOSMessage - DOS_PROGRAM
; Acesta este argumentul pentru operația de afișare a întrerupătorului 21h
mov ah, 9
; Apelează întrerupătorul și afișează mesajul
int 21h
; Argument de ieșire pentru același întrerupător
mov ax, 0x4C01
; Ieșire
int 21h
; Mesaj de afișat în DOS când programul se execută
DOSMessage:
db "Acest program nu poate rula pe acest sistem!"
db 0Dh, 0Dh, 0Ah, '$'
; Umple cu zero până la totalul de 64 biți ai acestei porțiuni DOS
Fill 64-$+DOS_PROGRAM, 0
; Aici vom defini antentul PE, care va valida fișierul ca Portabil Executabil
EXE_HEADER:
; La fel ca în antetul DOS, vom începe cu o semnătură ce identifică acest fișier ca PE
.PESignature db "PE", 0, 0
; Tipul de procesor pentru acest program, în cazul nostru Intel i386
.Microprocessor dw 0x014C
; Numărul de secțiuni în fișier
.SectionsCount dw $SECTIONS_COUNT
; Data și ora creării antetului
.CreationDateHour dd __POSIX_TIME__
; Indicator la tabelul de simboluri. Simbolurile vor ajuta compilatorul să
; identifice diferitele elemente din codul sursă. Nu este necesar acum
.SymbolsTable dd 0
; Cât de multe simboluri există în tabel. Fără tabel de simboluri, nimic
.SymbolsCount dd 0
; Mărimea antetelor adiționale sub acesta
.OptionalHeaderSize dw SECTIONS_HEADER_TABLE - OPTIONAL_HEADER
; Caracteristici fișier (a se vedea referința)
.Characteristics dw 0x0002|0x0004|0x0008|0x0100|0x0200
; Antent opțional (de fapt, obligatoriu) ce va defini structura executabilului
OPTIONAL_HEADER:
; La fel ca înainte, vom începe cu numărul care identifică acest antent
.OptHeaderSignature dw 0x010B
; Versiunea majoră a linker-ului. Acest program nu are așa ceva
.LinkerMajorVersion db 0
; Versiunea minoră a linker-ului
.LinkerMinorVersion db 0
; Mărimea secțiunilor de cod din imagine
.CodeSize dd Round($CODE_SIZE, $SECT_ALIGN)
; Mărimea secțiunilor de data inițializate
.DataSize dd Round($DATA_SIZE, $SECT_ALIGN)
; Mărimea secțiunilor de date neinițializate (BSS). Aceste secțiuni nu ocupă
; nici un spațiu în fișier dar sistemul va aloca memoria necesară can va rula
; programul
.UdataSize dd Round($UDATA_SIZE, $SECT_ALIGN)
; Adresa relativă a punctului de intrare al aplicației. Cum acesta se găsește
; în prima secțiune din memorie și încee la $SECT_ALIGN, punctul de intrase este
; același ca $SECT_ALIGN
.EntryPoint dd RVA(CODE)
; Adresa virtuală relativă (AVR) a secțiunii de cod
.CodeBase dd RVA(CODE)
; AVR a secțiunii de date
.DataBase dd RVA(INIT_DATA)
; Adresa preferată pentur a încărca imaginea. Pentru imaginea EXE, aceasta este
; 0x00400000, pentru fișiere DLL 0x10000000. Deasupra acestei adrese sunt adăugate
; codul și datele de mai sus sunt adăugate
.MemoryImageBase dd $PREFERRED_ADDRESS
; Aliniere secțiune. Valoarea implicită este 0x1000 (4096). Fiecare secțiune începe
; la o adresă virtuală multiplu al acestei valori
.SectionAlignment dd $SECT_ALIGN
; Aliniere fișier. Valoarea recomandată este 0x0200. Fiecare secțiune din fișier este
; aliniată la această valoare
.FileAlignment dd $FILE_ALIGN
; Indică versiunea majoră a celui mai vechi sistem operativ acceptat. În cazul nostru,
; acest cod poate rula pe Windows 95 și versiuni ulterioare
.MajorOSVersion dw 4
; Versiunea minoră a sistemului operativ
.MinorOSVersion dw 0
; Versiunea majoră a imaginii
.MajorImageVersion dw 0
; Versiunea minoră a imaginii
.MinorImageVersion dw 0
; Versiunea majoră a subsistemului. Valoarea minimă acceptată este 3
.MajorSubsysVersion dw 3
; Versiunea minoră a subsistemului. Valoarea minimă acceptată este 10
.MinorSubsysVersion dw 10
; Versiune WIN32, totdeauna 0
.Win32Version dd 0
; Mărimea fișierului imaginii, incluzând anteturi aliniate la $SECT_ALIGN
.ImgSize dd ImageSize
; Mărimea tuturor anteturilor, incluzând anteturile secțiunilor, aliniate la $FILE_ALIGN
.HeadersSize dd Round($HEADERS_SIZE, $FILE_ALIGN)
; Sumă de verificare care validează integritatea imaginea. De obicei 0
.Checksum dd 0
; Subsistemul folosit pentru interfața cu utilizatorul, în cazul nostru, consola
.InterfaceSubsystem dw 3
; Caracteristici DLL. Niciun DLL, deci 0
.DLLAttributes dw 0
; Totalul de memorie rezervată de sistem pentru stivă
.ReservedStack dd 4096
; Totalul inițial de memorie rezervată de sistem pentru stiva locală
.LocalStack dd 4096
; Mărimea inițială a memoriei libere
.ReservedFreeMemory dd 65536
; Memoria inițială liberă pentru imagine
.FreeMemory dd 0
; Indicator pentru depanare, obsolet
.DebuggingPointer dd 0
; Numărul de intrări în the DATA_TABLE. 16 se folosește în cele mai multe cazuri
.DataDirectories dd 16
; Adresa tabelului de elemente exportate. Acesta nu este un DLL, nu se exportă nimic
.ExportTable dd 0
; Mărimea tabelului de elemente exportate
.ExportTableSize dd 0
; Adresa tabelului de elemente importate, unde avem legături cu elementele
; altor module
.ImportTable dd RVA(KERNEL32_ITABLE, IMPORT_DATA)
; Mărimea tabelului de elemente importate
.ImportTableSize dd $IMPORTS_TABLE_SIZE
; 112 biți rezervați
Fill 112, 0
; Acest table conține anteturile secțiunilor acestei imagini, dar și atributele acestora
SECTIONS_HEADER_TABLE:
; Antetul secțiunii de cod, unde codul executabil este localizat. De obicei, această
; secțiune se numește text, dar îi vom spune 'code'
CODE_SECTION_HEADER:
; Numele acestei secțiuni. Nu poate avea mai mult de 8 caractere
.Name db ".code", 0, 0, 0
; Mărimea virtuală (în memorie) a acestei secțiuni. Dacă este mai mare decât
; mărimea pe disc, va fi umplută cu 0
.SectionSize dd Round($CODE_SIZE, $SECT_ALIGN)
; AVR a acestei secțiuni
.SectionStart dd RVA(CODE)
; Mărimea reală aliniată a acestei secțiuni
.RealSize dd Round($CODE_SIZE, $SECT_ALIGN)
; Adresa reală de start
.StartAddress dd CODE
; Adresa tabelului de realocări
.RelocationsTable dd 0
; Un indicator de fișier la începutul intrărilor de numere de linie pentru secțiune.
; Dacă nu există numere de linie COFF, această valoare este zero
.LineNumbers dd 0
; Numărul de realocări
.RelocationsTotal dw 0
; Numărul de intrări de numere de rând pentru secțiune
.LineNumbersTotal dw 0
; Caracteristici secțiune. Poate fi citită, scrisă și executată
.Characteristics dd 0x00000020|0x20000000|0x40000000|0x80000000
; Antetul secțiunii de importări (vezi comentariile primului antet)
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
; Antetul secțiunii de date inițializate (vezi comentariile primului antet)
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
; Antetul secțiunii de date neinițializate (vezi comentariile primului antet)
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
;; Aici se încheie declarațiile tuturor antetelor
HEADERS_END:
;; Aliniere la mărimea paginii
align $FILE_ALIGN
;; SECȚIUNE COD
; Pentru procesoare pe 32 de octeți
use32
; AVR al variabilelor globale neinițializate, din secțiunea BSS. Prima variabilă
; va conține antetul dispozitivului de ieșire.
OutputHandler equ RVA(NULL_DATA) + 0
; Aici se va stoca numărul de caractere generat de WriteConsoleW
WrittenChars equ OutputHandler + 4
; Tot codul ce va fi executat se află aici
CODE:
; În primul rând, vom avea nevoie de un manipulator la consolă
call GetOutputHandler
; Adaugă mesajul de afișat în stivă
push dword $PREFERRED_ADDRESS + RVA(HelloMessage, INIT_DATA)
; Numărul de caractere care trebuie afișate
push 13
; Apelarea funcției care va afișa textul
call ShowText
; Închide aplicația în mod corect
jmp CloseApplication
; Această porțiune de cod va obține manipulatorul consolei și-l va stoca în OutputHandler
GetOutputHandler:
; Argumentul care indică ceea ce vrem, și anume un dispozitiv de ieșire
push -11
; Acum, apelează funcția din Kernel32.dll
call dword [$PREFERRED_ADDRESS + RVA(F_GetStdHandle, IMPORT_DATA)]
; Dacă valoarea inversată este mai mică decât 1, avem o problemă și aplicația trebuie să iasă
cmp eax, 1
; Stocare registru BX pentru a-l restabili în caz de succes
push ebx
; Dacă există o eroare, vom forța sfârșitul procesului, iar codul de ieșire va fi 1
mov ebx, 1
; Salt la funcția care încheie aplicația
jl CloseApplication
; Restabiliți registrul BX, înainte de a primi codul de ieșire
pop ebx
; Salvare manipulator în BSS
mov dword [$PREFERRED_ADDRESS + OutputHandler], eax
; Revenire la locul din care a fost apelată această funcție
ret 4
; Această funcție va afișa un text pe consolă
ShowText:
; Salvează registrul EBP pentru restaurare posterioară
push ebp
; Mută ESP în EBP pentru a citi argumentele primite
mov ebp, esp
; Argument rezervat NULL pentru funcția WriteConsoleW
push 0
; Aici se vor salva numărul de caractere scrise
push dword $PREFERRED_ADDRESS + WrittenChars
; Numărul de caractere ce vor fi scrise, de la al doilea argument
push dword [ebp + 8]
; Indicatorul textului, de la primul argument din stivă
push dword [ebp + 12]
; Manipulatorul dispozitivului de ieșire
push dword [$PREFERRED_ADDRESS + OutputHandler]
; Apelează funcția ce va afișa textul
call dword [$PREFERRED_ADDRESS + RVA(F_WriteConsoleW, IMPORT_DATA)]
; Restaurare EBP
pop ebp
; Revino și în același timp restaurează stiva
ret 8
; Această funcție va termina aplicația și va returna codul de ieșire din EBX
CloseApplication:
; Stochează valoarea lui BX în stivă, acesta este codul de ieșire
push ebx
; Apelează funcția care va ieși din aplicație cu codul din EBX
call dword [$PREFERRED_ADDRESS + RVA(F_ExitProcess, IMPORT_DATA)]
; Dacă nu a funcționat, stiva este restaurată și ne întoarcem
pop ebx
; Return
ret
; Secțiunea de cod este aliniată la 512 octeți
align $FILE_ALIGN
CODE_END:
; Aici avem toate datele și funcțiile importate din bibliotecile dinamice
IMPORT_DATA:
; Aceasta este biblioteca principală din Windows și va oferi funcții esențiale
KERNEL32_LIBRARY db 'kernel32.dll', 0
IMPORT_DATA_START:
; Aici descriem caracteristicile tabelului de importări pentru biblioteca KERNEL32.DLL
KERNEL32_ITABLE:
; AVR al tabelului de căutări de importări
.originalfthk dd 0
; Dată și timp, vor fi actualizate după data și ora din DLL
.timedate dd 0
; Indexul primei referințe a lanțului expeditorului
.forwarder dd 0
; Numele bibliotecii de importat
.name dd RVA(KERNEL32_LIBRARY, IMPORT_DATA)
; AVR al tabelul cu adrese de import
.firstthunk dd RVA(KERNEL32_IMPORTED_FUNCTIONS, IMPORT_DATA)
; Sfârșitul tabelului pentru biblioteca KERNEL32.DLL
Fill 20, 0
; Aici avem adresele funcțiilor importate
KERNEL32_IMPORTED_FUNCTIONS:
; Obține un controlador pentru dispozitivul standard specificat
F_GetStdHandle: dd RVA(I_GetStdHandle, IMPORT_DATA)
; Salt la funcția care afișează un text pe consolă
F_WriteConsoleW: dd RVA(I_WriteConsoleW, IMPORT_DATA)
; Această funcție închide procesul și returnează un cod de ieșire
F_ExitProcess dd RVA(I_ExitProcess, IMPORT_DATA)
; Octet rezervat
ReservedBytes dd 0
; Numele elementelor importate din librăria de mai sus
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:
; Date inițializate, de obicei constante precum mesajele de text statice
INIT_DATA:
; Mesaj de salut de afișat
HelloMessage: dw __utf16__("Salut, lume!"), 0Ah
; Aliniere la 512 octeți
align $FILE_ALIGN
DATA_END:
; Secțiunea BSS, cu date neinițializate
NULL_DATA:
; Sfârșitul imaginii
IMAGE_END:
Generarea fișierului binar este foarte simplă. Doar asigurați-vă că aveți NASM instalat și adăugat la calea de mediu. Executați următoarea comandă:
nasm main.asm -f bin -o app.exeAceasta va construi un executabil x86 pentru procesoarele pe 32 de biți. Într-o zi voi încerca să creez versiunea pe 64 de biți și vreau să adaug mai multe funcționalități (cum ar fi obținerea argumentelor din linia de comandă). Să vedem unde merge asta.
Puteți afla mai multe despre formatul PE aici.