The x86 Assembler guide (2000-2001)
Around 2000 I got familiar enough with x86 assembly language to write a tutorial about it. It ran as a monthly series in an online newsletter under the title “Assembler by example” (in Romanian, “Assambler prin exemple”), from September 2000 through March 2001. It proved more popular than I expected, mostly because the later installments took apart the inner workings of a real DOS virus.
The whole series targeted 16-bit DOS on the x86, starting from the crudest tool available
(debug) and working up to TASM, with every example built as a tiny .com program. Below is
the full course, reproduced as it was originally published in Romanian. The separate source
listings for the example programs have been lost to time, so where an installment linked out
to one I have just noted that.
The eight monthly installments:
- Assambler prin exemple, September 2000: what assembly language is, and the
first program, HelloWorld.com, typed in by hand at the
debugprompt. - Assambler prin exemple, October 2000: that program explained line by line,
the
movinstruction, the register file, the segment:offset memory model, and the DOSint 21hservices. - Assembler by example III, November 2000: moving to TASM, then a command-line XOR encryption tool that reads its password out of the Program Segment Prefix.
- Assembler by example IV, December 2000: flags and conditional jumps,
PUSH/POPand the stack, and the design of a self-decrypting COM program. - Structura şi funcţionarea unui virus, January 2001: the anatomy of a real (circa 1993) COM-infecting virus, the delta-offset trick, self-decryption, and dead code to fool heuristic scanners.
- Structura şi funcţionarea unui virus II, February 2001: how the virus goes
memory-resident, hooks
int 21h, and infects COM files as they are executed. - Fonturi în MSDOS, March 2001: redefining character glyphs through
int 10hto build a custom bitmap font, ending with a mouse-pointer glyph. - Assembler By Example 8, March 2001: the finale, a 320x200 PCX image viewer that writes straight to video memory and sets the VGA palette by hand.
Assambler prin exemple (septembrie 2000)#
Se spune ca ori ce programator ‘adevarat’ trebuie sa fi programat cel putin o data in asamblor. Pentru cine n-a facut-o, are acum ocazia sa afle cum se face.
Ce este Assambler (asamblor)? Assamblerul este un Low Level Language, pentru a fi mai exacti, de nivelul cel mai de jos. Practic se da acces direct la resursele sistemului. In assambler nu se pot efectua decat operatii aritmetice simple (pentru care exista circuite electronice dedicate in procesor) citire si scriere pe porturi, accesarea directa a memoriei. A programa in assambler (fata de C de exemplu) este ca si cum ti-ai da toti angajatii din firma afara si te-ai apuca sa faci totul singur. Dezavantajele sunt multe, de exemplu trebuie sa-ti tii singur evienta adreselor, tipurilor de date si pointerilor. Codul este de obici absolut neportabil fiind foarte dependent de arhitectura interna a procesorului si a sistemului de operare (tot din acest motiv programarea in assambler nu se poate face numai cu ‘biblia’, cartea tehnica, la capatai). Avantajele sunt putine dar majore: codul generat este foarte mic si ruleaza rapid (cu exceptia cazului cand a fost prost gandit), stiti in ori ce moment ce se intampla!. Alt avantaj ar fi setul redus si clar de instructiuni, care face limbajul usor de invatat. Turbo Assambler, (TASM si probabil ca si MASM, creatia Microsoft, chiar daca n-am folosit-o niciodata, din motive religioase), care este un fel de compilator, rezolva si unele probleme legate de calcularea adreselor si generarea codului, facand programarea mai umana. Assamblerul se foloseste la crearea de programe mici unde viteza de executie este critica. Majoritatea virusilor sunt scrisi in assambler.
Primul Program Acest prim program, HelloWorld.com, va fi scris cu ajutorul programului ‘debug’ care intra in componenta dosului in mod standard si se gaseste si in Windows (eu am W98SE si tot l-am gasit). In viitor vom folosi doar TASM, dar momentan, pentru simplitate (si pentru a lua cunostinta cu trivialitatea acestui mod de programare), folosim debug. La linia de comanda scrieti ‘debug’. Va intampina un prompter prietenos: ‘-’ (pentru help tastati ‘?’ dar momentan e suficient sa transcrieti ce-i mai jos:
a <enter>Programul va arata o adresa de genul XYZK:0100 si va invita sa tastati (cu enter dupa fiecare instructiune):
mov dx, 010c
mov ah, 09
int 21
mov ax, 4c00
int 21
db 'Hello World!$'Acum este momentul sa dati un enter fara nici o instructiune (linie goala). programul revine la prompterul initial. Tastati:
r CXvi se raspunde, politicos ‘CX 0000’ si ‘:’ tastati
0019(acest numar nu corespunde in cazul in care ati decis sa folositi un mesaj mai lung… acest numar este numarul care corespunde linie-i goale pe care ati dat enter minus 100: XYZK:0119, atentie, totul este HEXA !!!) acum este momentul sa dati un nume programului dvs. si, ati ghicit, se face intr-un mod simplu si logic:
n HelloW.comsi acum comanda de scris pe HDD:
w(vi se raspunde prompt: ‘Writing 00119 bytes’) iar acum sa iesim:
qBuuuuun ! daca totul a mers bine, veti gasi in directorul curent un fisier Hellow.com. Acest fisier, daca este executat, va prezenta mesajul Hello World!. In caz ca n-ati pus semnul $ la sfarsitul mesajului, programul va scuipa pe ecran continutul memoriei, pana intalneste un simbol $. In numarul viitor urmeaza explicarea pas cu pas a programului!
Assambler prin exemple (octombrie 2000)#
In numarul trecut am ramas la programul:
0: mov dx, 010c
1: mov ah, 09
2: int 21
3: mov ax, 4c00
4: int 21
5: db 'Hello World!$'Explicarea liniilor:
linia 0: mov, mov vine de la move, a muta (instructiunile in assambler sunt de 2-4 litere) Instructiunea mov este de forma:
mov <destinatie>, <sursa>Efectul instructiunii mov este de a transfera valoarea desemnata de
mov ax, [bx]pune in registrul ax valoarea aflata la adresa absoluta de memorie DS:BX Din explicatia precedenta probabil ca aveti 2 nelamuriri:
- ce este un registru
- si de ce o adresa este de genul DS:BX …
Incep cu adresa: Memoria calculatorului de arhitectura 80x86 este impartita in SEGMENTI de memorie de la 0000 la FFFF, fiecare SEGMENT fiind impartit in ADRESE RELATIVE de la 0000 la FFFF. Deci cand avem o adresa de genul 12AB:010C, prima parte (12AB) reprezinta segmentul de memorie, iar a doua (010C) adresa relativa la segment. (Sper ca na-ti chiulit in clasa a VII-a cand ati invatat ca cifrele in HEX, baza 16, sunt de la 0 la f). Treaba cu segmentul de memorie, e mult mai complicata decat pare la prima vedere (si se complica si mai tare in programarea pe 32 de biti) dar mai lamurim pe parcurs.
Si acum ce sunt REGISTRII ?! Registrii sunt niste ‘variabile’ ale procesorului. Procesorul lucreaza rapid cu ele. Sunt primul lucru care o sa vi se para cu adevarat limitant la calculatorul dvs. pentru ca intotdeauna o sa va doriti sa mai aveti unul. In arhitectura 80x86, registri se clasifica in felul urmator:
Registrii de uz general: AX, BX, CX, DX Acesti registrii sunt fiecare de cate 16 biti (respectiv fiecare poate contine un numar de forma 1FAB). Acesti registrii ii puteti umple cu ce doriti dvs. dar pentru unele instructiuni ei au semnificatii speciale. Fiecare dintre acesti registrii este impartit in doi registrii de 8 biti si se pot apela individual folosind denumirile AH, AL pentru AX; BH, BL pentru BX … etc. AH reprezentand primii 8 biti (dintr-un numar de forma 1fab reprezinta 1f), H de la high si AL ultimi doi (deci AB din 1fab), l de la low…etc. Procesoarele de la 80386 in sus poseda niste registrii de 32 de biti notati cu EAX, EBX … acestia nu-i puteti folosi decat intr-un mod special, numit mod protejat, iar momentan n-o sa va loviti de ei…
Registrii indicatori de adresa SP si BP Registrii de 16 biti fiecare, indica de obicei adresa stivei SP:BP. Stiva este un fel de lada de gunoi a procesorului, unde acesta isi salveaza tot felul de date curente, gunoaie. Cand ne vom lovi de ea voi explica mai amanuntit acesti doi registrii si utilitatea stivei.
Registrii de index SI si DI Acesti registrii, de 16 bit fiecare, se folosesc pentru a explica procesorului cum sa acceseze date din alti segmenti de memorie decat cel curent. Se folosesc rar la vedere, dar sunt vitali. Registrii de segment CS, DS, ES, SS Registrii de 16 biti fiecare
- CS- contine adresa segmentului de memorie in care se afla codul (programul)
- DS- adresa segmentului unde se afla datele folosite de program
- ES- adresa unde se afla date auxiliare folosite de program
- SS- adresa segmentului curent de stiva
Mai exista un registru: IP (instruction pointer) care indica procesorului adresa relativa unde se afla urmatoarea instructiune care va fi executata. Cam acestia sunt registrii. Momentan trebuie sa tineti minte doar AX, BX, CX si DX. Pe parcurs ii vom folosi si pe ceilalti, dar acestia sunt cei mai utilizati. Buuun ! Deci e clar, In linia 0 incarcam valoare 010c in registrul dx… linia 1: In linia a doua facem cam acelasi lucru, respectiv incarcam valoarea 09 in ah. Dece imi place mie sa incarc valori aiurea in registrii, ramane de vazut mai tarziu. linia 2: Aici apare o instructiune noua: int 21. Aceasta instructiune executa o intrerupere de nivel 21.
Dar ce este o intrerupere ?! O intrerupere este un semnal care este declansat de un eveniment intern sau extern (hard sau soft). Trebuie sa priviti intreruperile ca niste functii. Aceste functii sunt apelate la diferite evenimente, de exemplu la apasarea unei taste (eveniment hard) este executata o intrerupere de nivel 9, care va lua din bufferul unde este stocat, codul tastei apasate si va actiona in consecinta. In cazul curent declansam un eveniment soft, apelant intreruperea 21. (in episodul in care voi descrie programele care raman rezidente in memorie, TSR, voi prezenta cum functioneaza sistemul de intreruperi si ce treburi dragute se pot face cu el)…
Acum, de ce apelez int 21 ?! Intreruperea 21 (numita si DOS services) este caracteristica sistemului de operare DOS si derivatilor acestuia, recursiv compatibili. Este o “multiplex interrupt”, asta insemnand ca este o functie cu mai multe subfunctii independente, apelabile fiecare individual. In int21, sistemul DOS ne pune la dispozitie o colectie de functii care executa cele mai uzuale functii si faciliteaza interactiunea programului cu mediul exterior si cu sistemul de operare. Ori ce actiune ce se poate executa cu int 21 (ca de exemplu afisarea unui text), poate fi executata si fara int 21, apeland doar la intreruperi programate in hard (dar scrierea unui string ar fi mult mai complicata, hardul stiind sa scrie doar un caracter o data).
Structura de apelare a int 21 Pentru ca int 21 este o colectie de functii, undeva trebuie sa indicam subfunctia dorita. La apelarea int 21, intotdeauna registrul ah contine numarul subfunctiei dorite. In ceilalti registrii trebuie stocate diferite valori, in functie de subfunctia apelata. Subfunctiile lui int 21 sunt multe, deci nu aveti cum sa le invatati la toate modul de apelare. (Eu le stiu doar pe cele mai uzuale) In mod sigur aveti nevoie de un manual unde sa le aveti descrise. Eu am unul electronic, cine si-l doreste, i-l pun la dispozitie cu cea mai mare placere, via email. In cazul nostru dorim sa afisam un text pe ecran. In acest scop folosim int 21, subfunctia 09. Aceasta valoare (09) o incarcam in AH. Manualul ne spune ca aceasta subfunctie se asteapta sa gaseasca in DS:DX pointer la stringul care dorim sa-l afisam. Asta inseamna ca in DS:DX trebuie pusa adresa de memorie unde incepe textul. La intrarea intr-un program de tip COM, registrul DS este setat de sistem la valoare segmentului in care se afla programul. Avand in vedere ca si textul se afla in acelasi segment, nu alteram acest registru. Ramane doar DX. In acesta am decis sa pun valoarea 010C. Aceasta este adresa la care incepe textul. Cum am calculat-o ? Teoretic trebue sa calculez lungimea codului si sa adaug 100h. Nu are rost sa explic cum se face acest lucru. In numarul urmator transcriu acest cod in TASM, care ne scapa de pacostea calcularii adreselor. (Cine e totusi curios cum se face, poate sa-mi dea un email). Ni se mai spune in documentatie ce textul afisat trebuie sa se termine in simbolul ‘$’ (care nu este afisat). Daca omiteti acest semn, va fi afisat continutul memoriei, pina cand se intalneste primul ‘$’. Dupa toate pregatirile apelam int 21, care isi face treaba constiincios. Nu putem verifica in nici un fel ca totul a mers bine, pentru ca functia nu returneaza nici o valoare, dar e bine de stiut ca sunt si functii care returneaza coduride eroare. Acum ne-am facut treaba si trebuie sa iesim cumva din program. Nu este indeajuns sa terminam codul. Cineva trebuie sa faca curat dupa noi in memorie si sa retransmita controlul sistemului de operare (plus sa inchida fisierele si sa faca multe altele). Treaba aceasta se poate face teoretic si manual dar ar lua multe pagini de cod degeaba. Mult mai simpla si mai facila este folosirea subfunctiei 4c din int 21. Deci, in linia 3 punem in ah valoarea 4c si in al 00. (mov ax, 4c00 este echivalent cu mov ah, 4c mov al, 00). Valoarea din al reprezinta valoarea pe care o returneaza programul sistemului de operare (in DOS teoretic ori ce program returneaza o valoare, aceasta valoare este utila in cazul scripturilor bat, cand dorim sa aflam daca un program a fost executat cu succes). In cazul nostru returnam valoarea 0 care in general inseamna OK. Apoi apelam int 21 pentru a pune capat executiei. In caz ca n-am fi executat aceasta secventa, procesorul ar fi interpretat toate datele din memorie ca si cod si probabil ca s-ar fi blocat (sau poate cine stie ?! ati fi gasit in mod miraculos a patra dimensiune). In linia 5 noi nu mai avem instructiuni ci avem textul de afisat. Procesorul ar interpreta si acest text ca si cod, dar nu ajunge niciodata la el, pentru ca exitul are loc in linia 4. Cam asta a fost cu ‘Hello World !’ Pentru nelamuriri, lucruri care nu le-am explicat indeajuns, sau sugestii, astept mailuri. In numarul viitor transcriem acest program in TASM (pentru a ne familiariza cu acest mediu de programare) + un program nou, mai complex si mai util.
Assembler by example III (noiembrie 2000)#
Motto: ‘Assembly is the language that before you can shoot yourself in the foot you have to invent the gun, hand, leg, and foot.’
Daca va mai amintiti, in numarul trecut, terminasem descrierea la Hello World! Acum vom transcrie programul in sursa pentru TASM. TASM vine de la TURBO assembler si este un compilator de assembler care ne ofera diferite facilitati, de la evidenta adreselor, pana la optimizarea codului (?!). Principalele avantaje sunt posibilitatea scrierii codului intr-un editor civilizat (nu ca la arhaicul debug) si calculare automata a adreselor de memorie. Momentan vom transcrie programul si vom da o explicatie scurta la fiecare linie. TASM este mult mai complex decat il voi prezenta aici, dar il voi prezenta mai pe larg pe parcurs. Iata codul:
.model tiny
.code
org 100h
start:
mov dx, offset text
mov ah, 09h
int 21h
mov ax, 4c00h
int 21h
text db 'Hello World!$'
end startParca nu exista modificari majore, nu ? Totusi iata ce a aparut:
.model tinyLinia aceasta se pune la inceputul codului si explica compilatorului ca programul care intentionam noi sa-l facem este un program care isi pastreaza toate datele (codul, stack, buffer, date suplimentare) intr-un singur segment de memorie. Toate programele com sunt model tiny (mai exista modelele small, medium, large, huge, dar acestea sunt pentru fisiere exe). Eu personal sunt de parere ca ceea ce nu incape intr-un tiny n-ar trebui programat in assembler, deci toate programele care le vom folosi ca exemple vor fi model tiny.
.codeAceasta linie explica compilatorului ca ceea ce va gasi el mai jos este codul programului si va trebui sa-l trateze ca atare.
org 100hAceasta linie explica compilatorului ca noi vom intentiona sa compilam respectivul program ca un fisier com. Fisierele com sunt incarcate de sistemul de operare direct in memorie (asta inseamna ca un fisier com contine exact imaginea programului din memorie in momentul executarii acestuia) intr-un segment liber de memorie, incepand cu adresa 0100h. Deci noi va trebui sa spunem compilatorului ca prima instructiune are adresa 0100h (si nu 0). Compilatorul necesita aceasta informatie pentru a calcula adresele la care se afla datele si adresele de salt absolut.
start... end startIndica inceputul si sfarsitul programului. Intre start si end start (sau ori ce alt label) trebuie sa se afle toate functiile, subfunctiile, codul, date…etc a programului. Apoi urmeaza codul. In cod, sigurul lucru ciudat exte acel mov dx, offset text. Daca ne uitam mai jos, vedem ca textul nostru a primit un label (text). Deci practic ceea ce am zis noi se traduce in: pune in DX adresa (offset se traduce mai degraba ca ’la ce distanta de mine’) textului text. Deci, in loc sa calculam noi unde se afla respectivul text (cum am facut pana acum) compilatorul face asta pentru noi. Acum cred ca am sa abandonez Hello World, pentru ca deja am invatat destule pe spinarea lui. Urmatorul program care il propun va face urmatoarele: va primi in linia de comanda 1 argument (o parola). Apoi va citi datele de la tastatura (fisier) si le va encripta folosind parola si va scrie pe ecran (fisier) rezultatul (vom privi tastatura ca un fisier si ecranul tot ca un fisier pentru ca intrarea si iesirea pot fi redirectionate). La acest program apar mai multe probleme. De exemplu, cum aflam ce a primit programul in linia de comanda: Poate va ganditi ca exista o functie speciala pentruasa cea… s-ar putea, dar eu nu o cunosc. Dar ceea ce cunosc este ca sistemul de operare creaza inainte sa execute un program un Program Segment Prefix lung de 100h bytes (h de la HEXA). N-am sa va spun ce contine acest PSP pentru ca gasesti aceste lucruri in carti de scoala. Ce am sa va spun este ca in byteul 80h se afla lungimea argumentului primit de program (normal, in hexa) si incepand cu byteul 81h (pana la 100h) se afla chiar argumentul. Deci ceea ce trebuie sa faca programul nostru pentru a afla argumentul cu care a fost lansat, este sa citeasca byteul 80h si sa citeasca atatea caractere din memorie, incepand cu 81h. O alta problema ar fi, cum se citesc fisierele (tastatura). (codul acestui exemplu s-a pierdut in timp) Ultima problema ar fi metoda de encryptare a fisierului. Sa nu va imaginat ca acum o sa aflati algoritmul blowfish sau ecryptarea PGP… Am decis sa folosesc o metoda ‘didactica’, care are si avantajul ca este reversibila folosind acelasi program (respecitv mai rulati programul o data pe fisierul criptat, cu aceeasi cheie si obtineti fisierul original). Aceasta metoda minune este metoda xor (‘SAU EXCLUSIV’) si se invata in clasa a 9-a (?!) capitolul logica matematica. Voi indica un tabel cu actiunea comenzii xor. (De mentionat ca xor acctioneaza la nivel de bit, deci tabelul se refera la biti):
sursa1 | 1 | 1 | 0 | 0 |
sursa2 | 0 | 1 | 0 | 1 |
------------------------
rezulta| 1 | 0 | 0 | 1 |Instructiunea XOR este definita asa:
XOR dest,src
exclusive OR (toggle dest bits which are 1s in src)
dest<-(dest ^ src)Cam asta ar fi totul. Acum codul sursa (pentru cei care chiar vor sa invete, sa incerce prima data sa faca o schema logica a programului si sa incerce sa o transcrie in assembler si unde nu stiu sa se uite la programul meu):
Mentiune speciala:
Am un stil destul de dezordonat de programare si acest program nu ar trebui considerat etalon de nici un fel. (codul sursa al programului s-a pierdut in timp) Cam asta ar fi un cod functional. Codul se compileaza (asambleaza) cu comanda ’tasm cod.asm’ care va genera un fisier obj iar apoi acest fisier se transforma in program com prin comanda ’tlink cod.obj /t’. Pentru a folosi programul pe fisiere, il vom apela asa:
program.com parola <infile >outfileLinia
Assembler by example IV (decembrie 2000)#
În numărul trecut prezentasem acel program de encryptare/decryptare. Dacă aţi înţeles cum funcţionează, totul este ok. Oricum, eu ţin neapărat să prezint elementele noi care apar în program. S-o luăm cu începutul… *** In lina 20 apare instrucţiunea JLE (Jump if less or equal). Procesoarele x86 au 8 biţi în memorie (un caracter) care sunt folosiţi ca flag-uri. Respectiv, în momentul executării unei comparaţii (şi alte instrucţiuni), aceşti biţi sunt setaţi sau nu (TRUE sau FALSE) in funcţie de rezultatul comparaţiei. Apoi, dacă executăm o instrucţiune ca cea de mai sus (JLE), procesorul analizează biţii de flag-uri şi acţionează în consecinţă. O instrucţiune gen JLE nu trebuie să succeadă imediat o instrucţiune CMP. Între ele pot fi oricâte instrucţiuni, cu condiţia să nu afecteze starea flag-urilor. Instrucţiunile de genul JC (Jump if Clear) pe care le întâlniţi după apelarea int 21h, verifică dacă a fost setat flag-ul de eroare. Ele au funcţia de a intercepta eventual erori în procesul de execuţie (un fel de trow/catch la C). *** În linia 43 apare instrucţiunea PUSH. Această instrucţiune salvează în stiva (despre care v-am vorbit in nr.2) conţinutul registrului care-l primeşte ca argument. Acesta instrucţiune se foloseşte împreună cu instrucţiunea POP care aduce din memorie ** ultima valoare salvata ** Deci instrucţiunile de genul: push CX pop AX au aceeaşi valoare ca si mov AX, CX. Stiva este un lucru foarte folositor dar se poate si umple. În general e de 255 de caractere dar depinde de sistem. Oricum, a umple stiva (sau a da pop când nu e nimic salvat în ea) e o metodă sigură de a bloca programul. În numărul acesta voi face o excepţie şi nu voi prezenta un program complet (printre altele şi pentru a va stimula să vă puneţi mintea la contribuţie pentru a va da seama dacă aveţi undeva neclarităţi). Oricum voi prezenta problemele principale legate de acest program. Iată la ce program m-am gândit: poate aţi observat ca în primul program (HelloWorld), mesajul era vizibil oricărui utilizator care s-ar uita la conţinutul fişierului binar. Ce facem dacă vrem sa creăm un program care să prezinte un mesaj care nu este vizibil la vizualizarea codului ? Sau mai bine, avem deja un program, care dorim să-l encryptam şi se va autodecrypta în momentul execuţiei ? În acest mod am şi proteja programul la eventuale încercări de dezasamblare (hackuire/spargere)…(nota: metoda prezentata aici este doar un exemplu…pentru o protecţie reala, ar trebui îmbunătăţit algoritmul de encryptare. Oricum, e mai bun decât nimic). Deci, programul se va manifesta cam aşa: Va primi la intrare un fişier COM. Îl va encrypta, şi ii va adăuga un cod de decryptare. Ceea ce obţinem este un program COM care va porni ceva mai greu dar va fi encryptat. Cum facem ca acest program să se autodecrypteze, cum adăugăm cod ? Codul de decryptare îl adăugăm pur şi simplu la sfârşitul programului encryptat. Pentru ca acest cod să fie executat înaintea oricărui altuia, trebuie să plasăm o instrucţiune jmp chiar la începutul codului, care va indica spre codul de decodare. ştiind că o instrucţiune jmp (jmp la adresa relativă şi nu absolută) ocupa trei bytes (e problema de experienţă), vom copia primi trei bytes originali ai programului la sfârşit şi îi vom înlocui cu jmp-ul de care avem nevoie. Treaba de a muta iarăşi bytesii originali la locul lor, revine codului de decodare. După ce acest lucru a fost făcut şi programul a fost decodat, nu trebuie decât sa executam un jmp la 100h (respectiv, unde începe programul com). Structura programului este simplă şi foloseşte elemente deja cunoscute. Funcţiile speciale de care aveţi nevoie sunt cele deja descrise. (codul sursă al acestui exemplu s-a pierdut în timp) Nu voi prezenta codul în numărul acesta, în schimb, în numărul viitor va apărea acest cod, dar extins. Anume: voi prezenta un virus. Singurul lucru care îl face virusul în plus fata de programul prezentat aici este faptul ca se propaga singur, fără ajutorul omului. Mai exact, îşi găseşte singur fişierele la care sa se ataşeze, fără să trebuiască să-i fie furnizate în linia de comandă.
Structura şi funcţionarea unui virus (ianuarie 2001)#
În acest număr voi prezenta structura şi funcţionarea unui virus (in acest număr şi in numărul viitor acesta şi următorul). În primul rând voi prezenta codul virusului (asm). Aici am o mare dorinţă către voi: nu vă apucaţi să compilaţi virusul, doar ca să-i arătaţi vecinului de sus că sunteţi mai deştept ca el. Oricum virusul datează de prin 1993 şi sper că este detectat de toţi antiviruşii posibili. Deci, codul, ca să avem despre ce vorbi! (codul complet al virusului s-a pierdut în timp) Codul este in totalitate comentat, deci n-ar trebui să fie o problemă să descifraţi (cu biblia aproape) ceea ce se întâmplă. oricum, iată câteva explicaţii: Dacă ne uităm la începutul codului, vom observa că programul îşi face o intrare destul de ciudată, cu un CALL (apel la subrutină) la linia următoare. Acest lucru este necesar din următorul motiv: mare problemă a viruşilor, este faptul că infectează fişiere de mărime variabilă. Din acest motiv, nu ştim niciodată o instrucţiune sau un buffer la ce adresă se află. Fiindcă avem nevoie de adresa la care începe virusul, executăm acest call. Apoi, trebuie să ştim că instrucţiunea call salvează în stack, adresa de la care sa executat call-ul. Următoarele instrucţiuni au unicul scop de a pune valoarea adresei in bp. Mai departe acest bp va fi folosit ca “delta” în ori ce apel la adresă absolută, respectiv orice adresă va fi indexată cu valoarea lui bp, practic lungimea programului infectat. În acest sens nu vom mai folosi instrucţiunea mov pentru a pune adresa absolută într-un registru şi apoi să-i adăugăm bp, ci vom folosi instrucţiunea LEA, care face cam acelaşi lucru. Apoi urmează o serie de instrucţiuni care vor decripta programul. Virusul se ataşează la fişier în formă ecryptată, dar pentru a fi executat, trebuie întâi decryptat. Problema care se pune aici, este că la prima rulare (în momentul când virusul este executat prima dată, fără să fie ataşat la un program) el se află în stare necryptată. De aceea el îşi compara primul byte al programului cu ceea ce ar trebui să fie în stare necryptată. În caz că află că este necryptat, sare peste procedura de decryptare. Procedura de cryptare este simplă, prin negarea (înlocuieşte 1 cu 0 şi invers) biţilor. Apoi urmează câteva secvenţe de cod care nu sunt executate niciodată (sau n-au nici un efect), unicul lor scop fiind de a induce în eroare antiviruşii care folosesc metoda heuristică de căutare a viruşilor. Următorul pas este să se verifice dacă virusul este activ în memorie. În acest scop, virusul apelează o subfuncţie a int 21, inexistentă în mod normal, dar care virusul si-o creează în momentul în care se încarcă în memorie (vom vedea mai jos). În caz că primeşte răspunsul aşteptat, decide că este deja activ, şi nu se mai încarcă o dată în memorie. Apoi el restaurează primii 3 bytes ai programului infectat şi îi transferă acestuia controlul. În caz că acesta constată că nu este deja activ în memorie, începe procesul lung şi complicat al transferului în memorie. O dată activ în memorie, virusul va infecta ori ce program rulat, care nu a fost infectat deja. Despre acest aspect vom discuta în numărul viitor. În acest director voi pune şi altele, când va fi nevoie.
Structura şi funcţionarea unui virus II (februarie 2001)#
A rămas în numărul trecut la problema încărcării virusului în memorie. Folosind subfuncţia 35h a int21h, virusul află vectorul întreruperii 21h. (codul acestui pas s-a pierdut în timp) Trebuie să ştiţi că memoria unui PC începe cu o zonă numită tabelă de întreruperi, practic o zonă în care există o listă de adrese. Aceste adrese reprezintă adresa codului unde este tratată acea întrerupere. Asta înseamnă că, în momentul când sistemul primeşte o întrerupere de nivel 0 (sau 1, 2, etc) acesta execută practic un jump la prima (sau a doua, etc.) adresa din tabela de întreruperi şi de acolo execută un jump la adresa indicată, numită vector de întrerupere. Deci programul nostru doreşte să afle unde se află codul care tratează întreruperea 21h. După ce află, salvează această adresă într-o zonă special rezervată la sfârşitul codului. Codul care urmează este greu de explicat şi oricum ar fi fără sens să explic cum face. Ceea ce face este să afle dimensiunile programului folosind MCB, un prefix al programului, creat de sistemul de operare, în momentul lansării acestuia în execuţie. Apoi, folosind subfuncţia 48h a int 21h, virusul îşi alocă memoria necesară pentru a rămâne rezident. (codul acestui pas s-a pierdut în timp) După ce a primit spaţiu în memorie, virusul se copiază în acel spaţiu, folosind instrucţiunile REP şi MOVSB. Cele două instrucţiuni fac parte din categoria String Operation Instructions şi au următoarele definiţii. (definiţiile şi codul s-au pierdut în timp) Acum, nu avem decât să modificăm tabela de întreruperi, astfel încât vectorul de la int21h să indice către codul nostru. În acest scop folosim subf 25h a int 21h. (codul acestui pas s-a pierdut în timp) Din acest moment, ori ce int21h care este executată pe sistem, va trece prima dată prin codul virusului nostru (nu tot codul ci doar secţiunea care este în memorie, şi anume de la “Handler…”, pentru că aşa am setat vectorul). Acum virusul restaurează primii 3 bytes ai programului original, şi îi transferă acestuia controlul. Secţiunea de cod care urmează acum ar putea fi considerată un alt program. Acesta este codul care rămâne rezident în memorie şi care se ocupă de infectare şi altele. Nu trebuie să scăpăm din vedere faptul că acest cod este executat de fiecare dată când un program apelează int21h. Deci să începem. În momentul în care este executat in21h, pe noi (virusul) ne interesează ce subfuncţie este apelată. În acest scop executăm o serie de comparaţii. Subfuncţiile care ne interesează sunt:
4bh - execută program
CAh - resident checkCelelalte două subfuncţii se referă la schimbarea directoarelor şi discurilor. Codul nu face nimic în momentul în care le întâlneşte, ele există doar pentru un upgrade viitor al virusului. Subf. CAh: Dacă este apelată aceasta subfuncţie, virusul ştie că o altă copie a sa de pe sistem, încearcă tocmai să afle dacă virusul este rezident sau nu. De aceea, virusul rezident răspunde punând în bh, Ca. Apoi este executat mai departe vechiul cod al int21 (ca şi în cazul în care nici una dintre subfuncţii nu este interceptată). (codul pentru subf. 4bh s-a pierdut în timp) Dacă se apelează această funcţie, înseamnă că sistemul de operare sau un alt program, doreşte să execute un program. Noi dorim să interceptăm acest program şi să-l infectăm. În primul rând salvăm toţi regiştrii cu care a fost apelată int21h, ca să-i putem restaura în momentul execuţiei vechiului int21h. În acest moment avem la adresa DS:DX un string care indică un program care trebuie să fie executat. Noi vom încerca să infectăm acest program înainte să fie executat. Întâi, cu subf 43h, citim atributele fişierului (readonly, etc) şi le modificăm pentru a putea accesa normal fişierul. Vechile atribute sunt salvate pentru a fi restaurate după infectare. Dar prima dată trebuie să aflăm dacă fişierul nu a fost infectat deja. Virusul foloseşte o metodă empirică: Află atributele fişierului (time-dat) cu subf. 57h. Dacă secundele de la atributele fişierului (respectiv data şi ora la care a fost modificat) sunt egale cu 58, atunci virusul trage concluzia ca fişierul este deja infectat şi îl lasă în pace. Dacă nu, urmează procesul de infectare. Fişierul este deschis cu subfn 3dh, pentru citire-scriere. Acum începe infectarea. Mutăm pointerul de citire la sfârşit, folosind 42h. Astfel aflăm dimensiunile programului de infectat. Această dimensiune ne este necesară pentru a calcula jmp-ul care trebuie scris la începutul fişierului. Apoi mutăm pointerul înapoi la început şi citim primii 3 bytes, cei care trebuie înlocuiţi cu jmp-ul. Un lucru am uitat să facem: nu ştim dacă fişierul pe care lucrăm e com sau exe… Pentru că noi suntem un virus care infectează doar com-uri. În acest scop, comparăm octeţii citiţi cu MZ. Dacă găsim MZ, atunci fişierul este un exe, nu avem ce-i face, restaurăm atributele şi ieşim. (Notă istorică: toate fişierele EXE încep cu MZ. MZ vine de la Mark Zbigovski, unul dintre designerii DOS). Dacă totuşi fişierul nostru este un com, calculăm distanţa la care trebuie făcut jmp-ul şi îl scriem la începutul fişierului. Apoi luăm codul virusului, aflat în memorie, şi jonglând cu el, îl copiem într-un alt spaţiu şi îl encryptăm, tot în afară de primii bytes de decryptare. Observăm că procedura de encyptare este complementară cu cea de decryptare de la începutul virusului. Acum avem de două ori virusul în memorie: o copie care rulează şi o copie care este ecryptată. Nu ne mai rămâne decât să ataşăm această a doua copie la programul original. Acum fişierul este infectat cu succes. Nu ne mai rămâne decât să restaurăm atributele originale ale fişierului, regiştrii cu care a fost apelată int21h şi să transferăm controlul procedurii vechi de tratare a întreruperii. Cam acesta este un virus. După cum vedeţi, este un program complex, care cere o cunoaştere detaliată a sistemului de operare şi a assembler-ului. Majoritatea viruşilor sunt exemple de programare. În numărul viitor părăsim tărâmul codului ascuns şi ne vom ocupa un pic de grafica în Assembler.
Fonturi în MSDOS (martie 2001)#
Multa vreme am crezut ca fonturile (si toate necazurile care deriva din ele) ar fi apanajul sistemelor grafice. Dar, surpriza, si in DOS se pot schimba fonturile. Mai jos voi prezenta metoda: Folosind întreruperea 10h (video services) putem face multe efecte speciale. Una dintre subfunctiile acestei întreruperi este 1110H (=AX). Iata o explicatie mai amanuntita a metodei de apelare:
BH înaltimea fiecarui caracter (biti/ caracter ddefinit)
BL Blocul de fonturi care trebuie incarcat (EGA: 0-3; VGA: 0-7)
CX numarul de caractere care vor fi redefinite
DX Codul ASCII al primului caracter redefinit în tabelul de la ES:BP
ES:BP adresa definitiei fonturilorExplicatii suplimentare: BH înaltimea caracterului. Latimea caracterului este întotdeauna de 8 biti. BL pot fi definite pâna la 8 seturi de caractere. Având în vedere ca dorim sa modificam fontul activ, BL va fi 0. La adresa ES:BP vom pune un tabel cu descrierea fonturilor. Tabelul ar trebui sa aiba lungimea de BH*CX baiti. Definirea fonturilor se face in felul urmator:
=00000000=00
=00000000=00
___ =00111000=38
__ __ =01101100=6C
__ __ =11000110=C6
__ __ =11000110=C6
__ __ =11000110=C6
_______ =11111110=FE
__ __ =11000110=C6
__ __ =11000110=C6
__ __ =11000110=C6
__ __ =11000110=C6
=00000000=00
=00000000=00
=00000000=00
=00000000=00(caracterul A) devine urmatorul sir de baiti: 00,00,38,6C,C6,C6,C6,FE,C6,C6,C6,C6,00,00,00,00 Ideea în spatele acestei prezentari ar putea parea stupida. Dar are si unele utilitati: de exemplu Norton Utilities (4 DOS) foloseste cu succes o metoda de afisare semi grafica, si anume, redefinind unele caractere speciale creaza itemuri ca checkboxuri, dropdownlist etc. În numarul de astazi intentionez sa prezint demonstrativ un program care sa modifice fontul unui caracter cu un font care sa reprezinte o sageata de mouse. Astfel, când este initializat mouseul cu acel caracter, veti avea un pointer adevarat… Iata cam cum va arata pointerul nostru:
00000000 == 00
00000000 == 00
01000000 == 40
01100000 == 60
01110000 == 70
01111000 == 78
01111100 == 7C
01111110 == 7E
00011100 == 1C
00011100 == 1C
00001110 == 0E
00001110 == 0E
00000000 == 00
00000000 == 00
00000000 == 00
00000000 == 00Deci: 00, 00, 40, 60, 70, 78, 7C, 7E, 1C, 1C, 0E, 0E, 00, 00, 00, 00 Acum nu mai trebuie decât sa stim codul ASCII al caracterului care dorim sa-l modificam, care este 21 (!), si ne putem apuca de program. Programul este extrem de simplu, dar pentru a modifica întreg seul de caractere, trebuie depusa un pic de munca. Ca demonstratie exista un program care face asta. (codul acestui program s-a pierdut in timp)
Assembler By Example 8 (martie 2001)#
Acesta fiind ultimul număr din Assembler by Example, m-am gândit să public ceva special. Astfel, fiind la capitolul grafică, am făcut un program care afisează un fisier PCX pe ecran (în DOS mode). Programul este oarecum limitat, fiind capabil să afiseze numai fisiere de 320x200 pixeli. Codul este în totalitate comentat, deci ar trebui să îl întelegeti fără alte explicatii. (imaginea de exemplu, descrierea formatului PCX si codul sursă al programului s-au pierdut în timp) In primul rând verificam dacă fisierul PCX există. Apoi îl deschidem. Presupunând că totul a decurs normal, procedăm la decodarea lui. Pentru a intelege procesul de decodare al fisierului, trebuie să studiati cu atentie descrierea formatului PCX. Este mult mai simplu decât vă imaginati si este un exercitiu bun de programare (desi formatul este cam “nefericit”). Pentru a comunica cu dispozitivul de afisare folosim doua metode: copiem biti in memoria video (bit blitting, o metoda asa de veche incât la HC nu se mai folosea, fiind considerată lentă si învechită, dar s-a revenit la ea, e metoda preferată la animatie 3d) si a doua metodă este de a face out pe portul vga (3c8h) pentru a seta paleta de culori. (In rutina de setare a paletei de culori, putem introduce tot felul de functii de modificare a bitiilor, astfel vom obtine efecte ciudate, unele interesante. Dacă facem aceste transformări dinamic, putem obtine o imagine cu culori schimbătoare…) Apoi schimbăm din nou modul video in text 80x25 (standardul dos) si iesim. Cam acesta este tot programul. Acest program demonstrează puterea assembler-ului si tot odată poate constitui baza pentru alte aplicatii grafice sub dos (?!). Eu personal m-am distrat copios scriindu-l si apoi modificându-l. Printre altele am reusit să fac un mic player video sub dos, folosind repetat rutina de afisare. Rutina este însă asa de rapidă (lucru bun!) încât a trebuit să introduc temporizări între cadre (pe un Celeron la 300). Cam atât a fost ce am avut eu să vă spun despre assembler (sub DOS). Nu este nici pe departe totul, n-am discutat programarea pe 32 de biti/sub windows. Personal cred că assemblerul n-ar trebui folosit la “programare” sub windows ci doar la debuging. Sper sa pot realiza si o descriere a programarii sub windows, dar asta cel mai devreme peste 4 luni, dupa bac… Assemblerul rămâne in continuare metoda preferată în caz că se cere executare rapidă si consum mic de resurse. Îmi cer scuze deca am lasat lucruri “in aer”, mailu meu ramâne deschis pentru ori ce intrebare la care sper sa va pot raspunde…