Menu

Assembly Programming Journal

Assembly Programming Journal, traduzione a cura di Little-John per la comunità italiana del
reverse engeneering (RingZer0)***  http://ringzer0.cjb.net  ***
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.                                                Ott/Nov 98
:::\_____\::::::::::.                                               Issue    1
::::::::::::::::::::::..........................................................
A S S E M B L Y   P R O G R A M M I N G   J O U R N A L
http://asmjournal.freeservers.com
asmjournal@mailcity.com
T A B L E   O F   C O N T E N T S
----------------------------------------------------------------------
Introduzione....................................................mammon_
"VGA Programming in Mode 13h".............................Lord Lucifer
"SMC Techniques: The Basics"...................................mammon_
"Going Ring0 in Windows 9x".....................................Halvar
Column: Win32 Assembly Programming
"The Basics"..............................................Iczelion
"MessageBox"..............................................Iczelion
Column: The C standard library in Assembly
"_itoa, _ltoa and _ultoa"...................................Xbios2
Column: The Unix World
"x86 ASM Programming for Linux"............................mammon_
Column: Issue Solution
"11-byte Solution"..........................................Xbios2
----------------------------------------------------------------------
+++++++++++++++++++++++ Sfida ++++++++++++++++++++
Scrivi un programma che visualizzi la sua command line in 11 bytes
----------------------------------------------------------------------
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::..............................................INTRODUZIONE
by mammon_
Benvenuti alla prima issue dell'Assembly Programming Journal. Il linguaggio Assembly è stato
oggetto di rinnovato interesse per molti programmatori; la ragione di ciò dovrebbe essere la
reazione al boom improvviso di programmi RAD-developed di bassa qualità (vedi Delphi, VB, ecc)
rilasciati come free/shareware negli anni scorsi. Il linguaggio Assembly è solido, veloce, e
spesso ben fatto – è un più difficile trovare programmatori inesperti che sviluppano in
assembler che non, diciamo, in Visual Basic.
La selezione degli articoli è qualcosa di eclettico e dovrebbe dimostrare il focus di questo
giornale: p.e., si rivolge alla comunità dei programmatori assembler, non un tipo particolare
di coding, come Win32, virus, o programmazione di demo. Siccome il magazine è appena nato e
molti dei suoi scopi possono sembrare poco chiari, dedicherò il resto di questa introduzione
alle domande più comuni che ho ricevuto via email per i diversi chiarimenti.
Quanto spesso sarà pubblicato il giornale?
------------------------------------
Salvo fatalità, ogni issue sarà rilasciata a mesi alterni.
Che tipo di articoli saranno accettati?
----------------------------------------
Qualsiasi cosa che abbia a che fare con il linguaggio assembly. Ovviamente repliche di materiale
già pubblicato in precedenza non sono necessarie se non quando migliorano o chiarificano il
materiale precedente. Il più sarà incentrato sugli instruction sets della famiglia Intel x86;
comunque il coding per gli altri processori è accettabile (però sarebbe davvero una gran
cortesia indicare un emulatore x86 per il processore riguardo al quale tu scrivi).
Personalmente sono alla ricerca di articoli sull'assembly language che mi interessano:
ottimizzazione del codice, demo/graphics programming, virus coding, asm coding per unix e altri
OS, e OS-internals.
Le demo (con il sorgente) e 'ASCII art' di qualità (per le copertine della issue, logo per gli
articoli, ecc) sono davvero benvenuti.
Per quale livello di esperienza nel coding è inteso il mag?
--------------------------------------------------------
Il giornale intende coinvolgere gli asm-coders di ogni livello. Ogni issue
conterrà per lo più tecniche e codice per beginners e intermediate, dato che saranno, per forza
di cose, di maggiore richiesta; comunque uno degli obiettivi dell'APJ è di includere abbastanza
materiale 'advanced' per poter interessare anche i "pro-coders".
Come sarà distribuito il mag?
--------------------------------
L'Assembly Programming Journal ha la sua propria web page
http://asmjournal.freeservers.com
che conterrà la issue rilasciata e un archivio delle issue precedenti. La pagina contiene anche
un guestbook e una disucssion board per gli articolisti e i lettori.
Un abbonamento via email può esser ottenuto inviando una email a
asmjournal@mailcity.com
con il subject "SUBSCRIBE"; a partire dalla prossima issue, l'Assembly Programming Journal sarà
inviato via email all'indirizzo da cui hai inviato la mail.
Wrap-up
-------
Questo è per lo più la "faq". Enjoy the mag!
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::...........................................FEATURE..ARTICLE
VGA Programming in Mode 13h
by Lord Lucifer
Questo articolo descriverà come programmare grafica VGA Mode 13h usando il linguaggio assembly.
Mode 13h è la modalità grafica 320x200x256, ed è veloce e molto conveniente dal punto di vista
del programmatore.
Il buffer video comincia all'indirizzo A000:0000 e finisce all'indirizzo A000:F9FF. Ciò
significa che il buffer è di 64000 bytes e che ogni pixel in mode 13h è rappresentato da un
byte.
E' facile settare il mode 13h e il buffer video con l'assembly language:
mov     ax,0013h        ; Int 10 - Video BIOS Services
int     10h             ; ah = 00 – Setta il Video Mode
mov     ax,0A000h       ; punta il segment register es a A000h
mov     es,ax           ; possiamo ora accedere al buffer video come
Alla fine del tuo programma, vorrai probabilmente ripristinare il text mode.
Ecco come:
mov     ax,0003h        ; Int 10 - Video BIOS Services
int     10h             ; ah = 00 – Setta il Video Mode
Accedere ad un pixel specifico nel buffer è anche molto semplice:
mul     320             ; moltiplica y per 320 per ottenere la riga
add     ax,bx           ; aggiungi questo alla x per otten. l'offset
mov     cx,es:[ax]      ; ora si può accedere al pixel x,y da es:[ax]
Hmm... questo era facile, ma quella moltiplicazione è un po' lenta e dovremmo sbarazzarcene.
Questo è pure facile da fare, semplicemente usando il 'bit shifting' invece della
moltiplicazione.
Shiftando un numero a sinistra è come moltiplicarlo per 2. Noi vogliamo moltiplicare per 320,
che non è una potenza di 2, ma 320 = 256 + 64, e 256 e 64 sono tutti e due potenze di 2.
Quindi una maniera più veloce di accedere ad un pixel è:
mov     cx,bx           ; copia bx in cx, per salvarlo temporaneam.
shl     cx,8            ; shift a sinistra per 8, che è come
shl     bx,6            ; ora shift sin. per 6, che è come
add     bx,cx           ; ora somma queste 2 insieme, che è come
add     ax,bx           ; infine aggiungi la coord x a questo valore
mov     cx,es:[ax]      ; ora si può accedere al pixel x,y da es:[ax]
Beh, il codice è un po' più lungo e sembra anche più complicato, ma posso garantire che è molto
più veloce.
Per disegnare i colori, usiamo una 'color look-up table' (tavola di riferimento colori, NdT).
Questa look-up table è un array di 768 campi (3x256).  Ogni indice della table è proprio
l'offset index*3. I 3 bytes ad ogni indice contengono i valori corrispondenti (0-63) al rosso,
al verde, e al blu.  Quindi il numero totale dei colori possibili è 262144.
Comunque, siccome la table è di soli 256 elementi, solo 256 colori differenti sono possibili in
un dato momento.
Il cambiamento della palette dei colori è realizzato attraverso le porte I/O della scheda VGA:
La Porta 03C7h è la Palette Register Read port (Porta di lettura)
LA Porta 03C8h è la Palette Register Write port (Porta di scrittura)
La Porta 03C9h è la Palette Data port (Porta dati)
Ecco come cambiare la palette dei colori:
mov     dx,03C8h        ; 03c8h = Palette Register Write port
out     dx,ax           ; scegli l'index
mov     dx,03C9h        ; 03c8h = Palette Data port
out     dx,al
mov     bl,al           ; setta il valore rosso
out     dx,al
mov     cl,al           ; setta il valore verde
out     dx,al
mov     dl,al           ; setta il valore blu
Questo è tutto. Leggere la palette del colore è quasi lo stesso:
mov     dx,03C7h        ; 03c7h = Palette Register Read port
out     dx,ax           ; scegli l'index
mov     dx,03C9h        ; 03c8h = Palette Data port
in      al,dx
mov     bl,al           ; prendi il valore rosso
in      al,dx
mov     cl,al           ; prendi il valore verde
in      al,dx
mov     dl,al           ; prendi il valore blu
Cosa abbiamo ora bisogno di sapere è la procedura per disegnare un pixel di un certo colore in
una certa locazione sul monitor.  E' molto facile dato ciò che già sappiamo:
mov     cx,bx           ; copia bx in cx, per salvarlo temporaneam.
shl     cx,8            ; shift a sinistra per 8, che è come
shl     bx,6            ; ora shift a sin. per 6, che è come
add     bx,cx           ; ora somma queste 2, che è come
add     ax,bx           ; infine aggiungi la coord x a questo valore
mov     es:[ax],dx      ; copia il colore dx nella memory location
Ok, ora noi sappiamo come gestire il Mode 13h, gestire il video buffer, disegnare un pixel, e
editare la color palette.
Il mio prossimo articolo tratterà il disegno di linee, l'utilizzo del vertical
retrace per rendering più uniformi, e ogni cosa che mi verrà in mente fino ad allora...
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::...........................................FEATURE..ARTICLE
SMC Techniques: The Basics
by mammon_
Uno dei vantaggi della programmazione in assembler è che hai piene facoltà di controllo
sull'applicazione: la 'ginnastica binaria' del codice di un virus dimostra ciò più di ogni
altra cosa. Uno dei "trucchi" utilizzati dai virus, che ha avuto poi seguito negli schemi di
protezione dei programmi, è il codice automodificante (SMC = self-modifyng code).
In quest'articolo non tratterò di virus polimorfici o di motori di mutazione (mutation engines);
non analizzerò nessuno schema di protezione in particolare, non considererò trucchi
anti-debugger/anti-disassembler, e non affronterò l'argomento della PIQ (Prefetch Instruction
Queue, ndt).
Questo articolo è solamente una prima trattazione riguardante il codice automodificante, per
coloro a cui il concetto risulta nuovo e da implementare.
Episodio 1: Cambiamento di opcode (opcode alteration)
-----------------------------------------------------
Una delle forme più pure del codice automodificante è il cambiare il valore di una istruzione
prima che sia eseguita... a volte come il risultato di una comparazione, e a volte per
nascondere il codice da occhi curiosi. Questa tecnica segue essenzialmente questo schema:
mov reg1, codice-da-sostituire
mov [indir-su-cui-scrivere], reg1
in cui 'reg1' può essere qualsiasi registro, e '[indir-su-cui-scrivere]' dovrebbe essere un
puntatore all'indirizzo da cambiare. Nota che il 'codice-da-sostituire' dovrebbe essere una
istruzione in formato esadecimale, ma posizionando il codice da qualche altra parte nel
programma -- in una subroutine non chiamata, o in un segmento diverso -- è possibile
semplicemente trasferire il codice compilato da una locazione ad un'altra attraverso
l'indirizzamento indiretto, come segue:
call changer
mov dx, offset [string]     ;questo sarà eseguito ma ignorato
label:    mov ah, 09                  ;questo non sarà mai eseguito
int 21h                     ;questo chiuderà il programma
....
changer:  mov di, offset to_write     ;carica l'indirizzo del codice da scrivere
mov byte ptr [label], [di]  ;scrivi il codice nella locazione 'label'
ret                         ;ritorna dalla chiamata
to_write: mov ah, 4Ch                 ;codice di fine programma
questa piccola routine farà chiudere il programma, anche se in un disassembler essa all'inizio
sembra essere una semplice routine di scrittura di stringa. Nota che combinando l'indirizzamento
con dei loops, intere subroutine -- anche programmi -- possono essere sovrascritti, e il codice
da scrivere -- che può essere presente nel programma come dati -- può essere criptato con un
semplice XOR per farlo sfuggire da un disassembler.
Il seguente è un programma in assembler per dimostrare il cambiamento dal "vivo" del codice;
esso chiede all'utente una password, poi cambia la stringa da scrivere a seconda che la password
sia corretta o meno.
.286
.model small
.stack 200h
.DATA
MaxKbLength  db 05h
KbLength     db 00h
KbBuffer     dd 00h
szGuessIt        db     'Care to guess the super-secret password?',0Dh,0Ah,'$'
szString1        db     'Congratulations! You solved it!',0Dh,0Ah, '$'
szString2        db     'Ah, damn, too bad eh?',0Dh,0Ah,'$'
secret_word      db     "this"
.CODE
start:
mov     ax,@data                ; setta il registro di segmento
mov     ds, ax                  ; uguale alla direttiva "assume"
mov     es, ax
call Query                      ; chiede all'utente la password
mov     ah, 0Ah                 ; funzione DOS 'Ricevi input
mov     dx, offset MaxKbLength  ; inizio del buffer
int     21h
call Compare                    ; confronta la password e cambia il
exit:
mov ah,4ch                      ; funzione 'Chiudi al DOS'
int 21h
Query            proc
mov  dx, offset szGuessIt       ; stringa di richiesta password
mov  ah, 09h                    ; funzione 'visualizza stringa'
int  21h
ret
Query            endp
Reply            proc
PatchSpot:
mov  dx, offset szString2       ; stringa 'hai fallito'
mov  ah, 09h                    ; funzione 'visualizza stringa'
int  21h
ret
Reply            endp
Compare            proc
mov     cx, 4                   ; num. di bytes nella password
mov     si, offset KbBuffer     ; inizio del buffer di input della
mov     di, offset secret_word  ; indirizzo della password corretta
rep cmpsb                       ; confronto password
or cx, cx                       ; sono uguali?
jnz     bad_guess               ; no, non applicare il patch
mov word ptr cs:PatchSpot[1], offset szString1  ;cambia in GoodString
bad_guess:
call Reply                      ; output della stringa per visualizzare
ret
Compare            endp
end     start
Episodio 2: Encryption (la traduzione di questo termine è proprio brutta, encriptazione, quindi
ho preferito lasciare l'originale inglese)
-------------------------------------------------
Senza dubbio l'encryption è la forma più comune di codice automodificante utilizzato oggigiorno.
E' utilizzata dai packers e dagli exe-encriptors o per comprimere o per nascondere codice, dai
virus per rendere oscuri i propri contenuti, dagli schemi di protezione per nascondere dati.
La forma base dell'encription può essere:
mov reg1, indir-da-sovrascrivere
mov reg2, [reg1]
manipola reg2
mov [reg1], reg2
dove 'reg1' sarebbe il registro contenente l'indirizzo (l'offset) della locazione da
sovrascrivere, e 'reg2' un registro temporaneo che carica i contenuti del primo e poi li
modifica attraverso operazioni matematiche (ROL) oppure logiche (XOR).
L'indirizzo da cambiare è caricato in reg1, il suo contenuto è poi modificato all'interno di
reg2, ed infine riscritto nella locazione originale ancora contenuta in reg1.
Il programma della sezione precedente può essere modificato in modo tale che esso decripti
la password sovrascrivendola (in tal modo questa rimane decriptata finchè il programma non è
terminato) prima cambiando la 'parola segreta' come segue:
secret_word      db     06Ch, 04Dh, 082h, 0D0h
e poi cambiando la routine di confronto, Compare, per cambiare la locazione della 'secret_word'
nel segmento dati:
magic_key        db     18h, 25h, 0EBh, 0A3h ;non molto sicura!
Compare            proc               ; Passo 1: decripta la password
mov     al, [magic_key]       ; metti il byte1 della maschera XOR in al
mov     bl, [secret_word]     ; metti il byte1 della password in bl
xor     al, bl
mov     byte ptr secret_word, al     ; cambia il byte1 della password
mov     al, [magic_key+1]     ; metti il byte2 della maschera XOR in al
mov     bl, [secret_word+1]   ; metti il byte2 della password in bl
xor     al, bl
mov     byte ptr secret_word[1], al  ; cambia il byte2 della password
mov     al, [magic_key+2]      ; metti il byte3 della maschera XOR in al
mov     bl, [secret_word+2]    ; metti il byte3 della password in bl
xor     al, bl
mov     byte ptr secret_word[2], al  ; cambia il byte3 della password
mov     al, [magic_key+3]      ; metti il byte4 della maschera XOR in al
mov     bl, [secret_word+3]    ; metti il byte4 della password in bl
xor     al, bl
mov     byte ptr secret_word[3], al  ; cambia il byte4 della password
mov     cx, 4                ;Passo 2: Comfonta le passwords...nessun
mov     si,offset KbBuffer
mov     di, offset secret_word
rep     cmpsb
or      cx, cx
jnz     bad_guess
mov     word ptr cs:PatchSpot[1], offset szString1
bad_guess:
call Reply
ret
Compare            endp
Nota l'aggiunta della locazione della 'magic_key' (chiave magica) che contiene la maschera XOR
per la password. Tutto ciò potrebbe essere stato eseguito in maniera più sofisticata con un
loop, ma con soli 4 byte il codice sopra velocizza il tempo di debugging (e, inoltre, il tempo
di scrittura della articolo ( e della traduzione, ndt !)).
Nota come la password è caricata, XORata, e riscritta un byte la volta; utilizzando codice a
32-bit, l'intera password (dword) può esser scritta, XORata e riscritta in un sol colpo.
Episodio 3: Giocherellando con lo stack
---------------------------------------
Questo è un trucco che ho imparato mentre decompilavo del codice di SunTzu. Ciò che accade qui
è abbastanza interessante: lo stack è spostato nel segmento codice del programma, cosicchè il
top dello stack punta al primo indirizzo da essere modificato (che, inoltre, dovrebbe essere
uno vicinissimo alla fine del programma per il modo in cui lo stack funziona); il byte a questo
indirizzo è POPato in un registro, manipolato, e PUSHato indietro nella sua locazione originale.
Lo stack pointer (SP) è poi decrementato in modo che l'indirizzo successivo da essere
modificato (1 byte più basso in memoria) è ora nel top dello stack.
In più, i byte sono XORati con una porzione del codice stesso del programma, anche per
camuffare il valore della maschera XOR. Nel codice seguente, ho scelto di utilizzare i byte
dallo Start: (200h quando è compilato) fino a -- ma non includendolo -- Exit: (214h quando è
compilato; Exit-1=213h).
Comunque, come nel codice originale di SunTzu ho mantenuto la sequenza "inversa" della maschera
di XOR sicchè il byte 213h è il primo byte della maschera di XOR, e il byte 200h ne è l'ultimo.
Dopo alcuni esperimenti ho capito che questo era il modo più facile per sincronizzare una patch
-- o un editor esadecimale -- al codice che manipola lo stack; dal momento che lo stack si
muove all'indietro (uno stack che si sposta in avanti è più un problema che una soluzione),
utilizzando una maschera XOR "inversa" che permetta a tutti e due i puntatori di file in un
patcher di essere INCati o DECati in sincronia.
Come mai questa è una issue? A differenza dei due esempi precedenti, la seguente non contiene
una versione criptata del codice da modificare. Questa contiene giusto il code di origine che,
quando compilato, risulta essere nei byte non criptati che sono elaborati attraverso la routine
di XOR, criptati, ed infine eseguiti (che, se hai seguito il discorso, si dimostrerà subito di
non esser buono... in ogni caso è un ottimo metodo per far crashare la VM di DOS (Virtual
Machine, ndt)).
Una volta che il programma è compilato bisogna o cambiare i byte da decriptare a mano, o
scrivere un patcher che faccia ciò per te. La prima è più conveniente, l'ultima è più sicura
e diventa una necessità se hai intenzione di conservare il codice. Nell'esempio seguente ho
incluso 2 CCh (int 3) nel codice prima e dopo la fine dei byte da decriptare; un patcher deve
solo ricercare questi due valori, contare i byte nel mezzo, ed infine eseguire lo XOR con i
byte tra 200h e 213h.
Ancora una volta, questo esempio è la continuazione di quello precedente. In questo ho scritto
una routine per decriptare per intero la  routine 'Compare' della sezione precedente XORandola
con i byte compresi tra 'Start' e 'Exit'. Ciò è eseguito settando il segmento di stack come
segmento di codice, poi settando il puntatore di stack uguale all'ultimo indirizzo di codice
(il più alto) da modificare. Un byte è POPato dallo stack (es. la sua locazione originale),
XORato, e PUSHato indietro nella sua posizione originale. Il byte successivo è caricato
decrementando il puntatore di stack. Quando tutto il codice è decriptato, il controllo è
restituito alla appena decriptata routine 'Compare' e l'esecuzione continua normalmente.
magic_key        db     18h, 25h, 0EBh, 0A3h
Compare            proc
mov cx, offset EndPatch[1]    ;inizio dell'indiriz-da-sovrascriv + 1
sub cx, offset patch_pwd      ;fine dell'indiriz-da-sovrascriv
mov ax, cs
mov dx, ss                    ;salva lo stack segment -- importante!
mov ss, ax                    ;setta lo stack segment come code segment
mov bx, sp                    ;salva lo stack pointer
mov sp, offset EndPatch       ;inizio dell'indiriz-da-sovrascriv
mov si, offset Exit-1         ;inizio dell'indiriz della maschera XOR
XorLoop:
pop ax                        ;prendi il byte-da-patchare in AL
xor al, [si]                  ;XORa al con la XorMask
push ax                       ;scrivi il byte-to-patch in memoria
dec sp                        ;carica il successivo byte-da-patchare
dec si                        ;carica il successivo byte della maschera
cmp si, offset Start          ;fine della maschera XOR?
jae GoLoop                    ;Se No, continua
mov si, offset Exit-1         ;reinizializza la maschera XOR
GoLoop:
loop XorLoop                  ;XORa il byte successivo
mov sp, bx                    ;ripristina lo stack pointer
mov ss, dx                    ;ripristina lo stack segment
jmp    patch_pwd
db     0CCh,0CCh              ;Identifcation mark: START
patch_pwd:                             ;Nessun cambiamento da qui
mov     al, [magic_key]
mov     bl, [secret_word]
xor     al, bl
mov     byte ptr secret_word, al
mov     al, [magic_key+1]
mov     bl, [secret_word+1]
xor     al, bl
mov     byte ptr secret_word[1], al
mov     al, [magic_key+2]
mov     bl, [secret_word+2]
xor     al, bl
mov     byte ptr secret_word[2], al
mov     al, [magic_key+3]
mov     bl, [secret_word+3]
xor     al, bl
mov     byte ptr secret_word[3], al
mov     cx, 4
mov     si, offset KbBuffer
mov     di, offset secret_word
rep cmpsb
or cx, cx
jnz     bad_guess
mov word ptr cs:PatchSpot[1], offset szString1
bad_guess:
call Reply
ret
Compare            endp
EndPatch:
db 0CCh, 0CCh                  ;Identification Mark: END
Questo genere di programmi è davvero difficile da debuggare. Per testarlo, ho sostituito 'xor
al, [si]' prima con 'xor al,00', che non encripta ed è utile per cercercare eventuali errori
nel codice, e poi con 'xor al, EBh', che mi ha permesso di verificare che venivano criptati i
byte corretti (non fa mai male dare una controllatina, dopotutto).
Episodio 4: Conclusione
------------------------
Tutto ciò dovrebbe dare le basi sul codice automodificante. Ogni programma che utilizza tali
tecniche sarà pieno di insidie.
La cosa più importante è avere il programma completamente funzionante prima di iniziare a
sovrascrivere parti del suo codice. Inoltre, crea sempre una applicazione che esegua l'inverso
di ogni routine di decriptazione/criptazione -- non solo per velocizzare la compilazione e il
test automatizzando l'encriptazione di codice che sarà decriptato durante l'esecuzione, ma anche per disporre di un buono strumento per controlli utilizzando un disassemblatore (es. encripta il codice, dissasemblalo, decripta il codice, disassemblalo, confrontalo). Infatti, è una buona idea mantenere la porzione di codice automodificante del tuo programma in un eseguibile diverso e testarlo sulla versione definitiva, finchè tutti i bug sono eliminati dalla routine di decriptazione, e solo allora aggiungi la routine di decriptazione al codice finale. I segni di riconoscimento CCh (codemarks) sono anch'essi estremamente utili.
Infine, esegui il debug con il debug.com per applicazioni DOS -- il debugger è veloce, piccolo,
e se crasha hai solo perso una finestra di DOS.
Esempi più complessi di codice automodificante può esser trovato nel codice di Dark Angel, il
motore Rhince, o in qualsiasi motore di mutazione utilizzato nei virus polimorfici.
Si ringrazia Sun-Tzu per la tecnica dello stack usata nella su applicazione ghf-crackme.
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::...........................................FEATURE..ARTICLE
Going Ring0 in Windows 9x
by Halvar Flake
Questo articolo fornisce una breve visione generale su due modi per andare al livello Ring0 in
Windows 9x in modo non documentato, sfruttando il fatto che in Win9x nessuna delle tavole di
sistema più importanti sono sulle pagine che sono protette da un accesso a basso privilegio
(low-privilege access).
Una conoscenza di base del Protected Mode e degli OS Internals è richiesta, fa' riferimento al
tuo Assembly Book per questo:-) Le tecniche presentate qui non sono assolutamente una maniera
buona/pulita per raggiungere un livello di privilegio più alto, ma siccome richiedono uno
sforzo di coding davvero piccolo, a volte sono più more piacevoli da implementare che non un
rigoroso VxD.
1. Introduzione
----------------
In tutti i Sistemi Operativi moderni, la CPU va in protected mode, sfruttando le caratteristiche
particolari di questo mode per implementare la virtual memory,
il multitasking ecc. Per gestire l'accesso alle risorse system-critical (e perciò per fornire
stabilità) un OS ha bisogno di livelli di privilegio, in modo che un programma non possa
repentinamente uscirsene dal protected mode ecc. Questi livelli di privilegio sono
rappresentati sulle CPU x86 (mi riferisco all'x86 intendendo il 386 e i seguenti) con dei
'Rings', in cui il Ring0 è quello con i privilegi più alti e il Ring3 è quello con i privilegi
più bassi.
In teoria, l'x86 può gestire 4 livelli di privilegio, ma Win32 ne usa solo 2, Ring0 come
'Kernel Mode' e Ring3 come 'User Mode'.
Dal momento che il Ring0 non è richiesto dal 99% delle applicazioni, in Win9x l'unico modo
documentato per usare le routine del Ring0 è attraverso i VxDs. Ma i VxDs, pur rappresentando
l'unica maniera stabile e raccomandata, sono faticosi da scrivere e grandi, quindi in un paio
di situazioni particolari, le altre vie per raggiungere il Ring0 possono tornare utili.
La CPU stessa amministra le transizioni di livello di privilegio in due modi: attraverso le
Exceptions/Interrupts e attraverso i Callgates. I Callgates possono essere inseriti nella LDT
o nella GDT, Interrupt-Gates si trovano nella IDT.
Noi trarremo profitto dal fatto che queste tables possono essere scritte liberamente dal Ring3
in Win9x (NON IN NT !).
2. Il metodo della IDT
-----------------
Se si verifica una exception (or is triggered), la CPU guarda nella IDT al descriptor
corrispondente. Questo descriptor dà alla CPU un Address e un Segment
a cui trasferire il controllo. Un Interrupt Gate descriptor assomiglia a questo:
--------------------------------- ---------------------------------
D D
1.Offset (16-31)             P P P 0 1 1 1 0 0 0 0 R R R R R   +4
L L
--------------------------------- ---------------------------------
2.Segment Selector               3.Offset (0-15)                0
--------------------------------- ---------------------------------
DPL == I 2 bits che contengono il Descriptor Privilege Level
P   == Il bit Present
R   == bits Reserved
La prima word (Nr.3) contiene la più bassa word dell'address a 32-bit dell'Exception Handler.
La word a +6 contiene la word di ordine più alto. La word a +2 è il selector del segment in cui
risiede l'handler.
La word a +4 identifica il descriptor come Interrupt Gate, contiene il suo
privilegio e il bit present. Ora, per usare la IDT per andare a Ring0, creeremo un nuovo
Interrupt Gate che punta alla nostra procedura Ring0, salveremo quello vecchio e lo
sostituiremo con il nostro.
Poi causeremo quella exception. Invece di passare il controllo all'handler proprio di Windows,
la CPU eseguirà il nostro codice di Ring0. Non appena abbiamo finito, ripristineremo il vecchio
Interrupt Gate.
In Win9x, il selector 0028h punta sempre ad un Segmento Ring0-Code, che si estende per il
completo intervallo dello spazio di indirizzamento di 4 GB. Useremo questo come nostro Segment
selector.
Il DPL deve essere 3, dal momento che stiamo chiamando dal Ring3, e il bit present deve essere
settato. Quindi la word a +4 sarà 1110111000000000b => EE00h. Questi valori possono essere
hardcodati nel tuo programma, noi dobbiamo solo aggiungere l'offset della nostra Procedura
Ring0 al descriptor. In quanto exception, ne dovresti usare preferibilmente una che si verifica
raramente, quindi non usare l'int 14h ;-)
Io userò l'int 9h, dal momento che (per quanto ne so) non è usato sul 486+.
Il codice di esempio segue (da compilare con il TASM 5):
-------------------------------- taglia qui -----------------------------------
.386P
LOCALS
JUMPS
.MODEL FLAT, STDCALL
EXTRN ExitProcess : PROC
.data
IDTR        df 0            ; Questo conterrà i contenuti del registro IDTR
SavedGate   dq 0            ; Salviamo il gate che posizionamo qui
OurGate     dw 0            ; Offset low-order word
dw 028h         ; Segment selector
dw 0EE00h       ;
dw 0            ; Offset high-order word
.code
Start:
mov      eax, offset Ring0Proc
mov      [OurGate], ax              ; Mette l'offset words
shr      eax, 16                    ; nel nostro descriptor
mov      [OurGate+6], ax
sidt     fword ptr IDTR
mov      ebx, dword ptr [IDTR+2]    ; carica il Base Address della IDT
add      ebx, 8*9                   ; Indirizzo del descriptor int9 in ebx
mov      edi, offset SavedGate
mov      esi, ebx
movsd                               ; Salva il vecchio descriptor
movsd                               ; nel SavedGate
mov      edi, ebx
mov      esi, offset OurGate
movsd                               ; Sostituisce il vecchio handler
movsd                               ; con il nostro, nuovo
int      9h                         ; Genera l'exception, quindi
mov      edi, ebx
mov      esi, offset SavedGate
movsd                               ; Ripristina il vecchio handler
movsd
call     ExitProcess, LARGE -1
Ring0Proc PROC
mov      eax, CR0
iretd
Ring0Proc ENDP
end Start
-------------------------------- taglia qui -----------------------------------
3. The LDT Method
-----------------
Un'altra possibilità per eseguire del Ring0-Code è quella di installare un cosiddetto callgate
o nella GDT o nella LDT. Sotto Win9x è un po' più facile usare la LDT, dato che i primi 16
descriptors in essa sono sempre vuoti, quindi qui fornirò il codice solo per questo metodo.
Un Callgate è simile ad un Interrupt Gate ed è usato per trasferire il controllo da un segmento
low-privileged ad un segmento high-privileged usando una istruzione CALL.
Il formato di un callgate è:
--------------------------------- ---------------------------------
D D                   D D D D
1.Offset (16-31)             P P P 0 1 1 0 0 0 0 0 0 W W W W   +4
L L                   C C C C
--------------------------------- ---------------------------------
2.Segment Selector               3.Offset (0-15)                0
--------------------------------- ---------------------------------
P   == Present bit
DPL == Descriptor Privilege Level
DWC == Dword Count, numero di argomenti copiati nello stack del ring0
Quindi tutto ciò che ci resta da fare è creare un callgate, scriverlo in uno dei primi 16
descriptors, poi effettuare una far call a quel descriptor per eseguire il nostro Ring0 code.
Codice di esempio:
-------------------------------- taglia qui -----------------------------------
.386P
LOCALS
JUMPS
.MODEL FLAT, STDCALL
EXTRN ExitProcess : PROC
.data
GDTR        df 0            ; Questo conterrà i contenuti del registro IDTR
CallPtr     dd 00h          ; Siccome stiamo usando il primo descriptor (8) ed
dw 0Fh          ; è posizionato nell'LDT e il livello di privilegio
OurGate     dw 0            ; Offset low-order word
dw 028h         ; Segment selector
dw 0EC00h       ;
dw 0            ; Offset high-order word
.code
Start:
mov      eax, offset Ring0Proc
mov      [OurGate], ax              ; Mette l'offset words
shr      eax, 16                    ; nel nostro descriptor
mov      [OurGate+6], ax
xor      eax, eax
sgdt     fword ptr GDTR
mov      ebx, dword ptr [GDTR+2]    ; carica il Base Address della GDT
sldt     ax
add      ebx, eax                   ; L'indirizzo del descriptor LDT in
mov      al, [ebx+4]                ; Carica il base address
mov      ah, [ebx+7]                ; della stessa LDT in
shl      eax, 16                    ; eax, fa riferimento al tuo manuale
mov      ax, [ebx+2]                ; del pmode per i dettagli
add      eax, 8                     ; Salta il NULL Descriptor
mov      edi, eax
mov      esi, offset OurGate
movsd                               ; Sposta il nostro callgate personale
movsd                               ; nella LDT
call     fword ptr [CallPtr]        ; Esegui la nostra procedura Ring0
xor      eax, eax                   ; Pulisci la LDT
sub      edi, 8
stosd
stosd
call     ExitProcess, LARGE -1
Ring0Proc PROC
mov      eax, CR0
retf
Ring0Proc ENDP
end Start
-------------------------------- taglia qui -----------------------------------
Bene, popolo questo è tutto per ora. Questo metodo può essere facilmente adattato per usarlo
con la GDT, che ti permetterà di salvare un po' di bytes nel caso tu abbia da fare una forte
ottimizzazione.
Ad ogni modo, usa questi metodi con cura, che NON funzioneranno su NT e non sono in generale
una maniera pulita o stabile per effettuare queste operazioni.
Credits & Thanks
----------------
Il metodo della IDT preso dal virus CIH & dall'esempio di Stone alla url
http://www.cracking.net.
Il metodo della LDT è fatto da me, ma senza l'aiuto di IceMan & The_Owl sarei ancora confuso,
quindi tutti i crediti vanno a loro.
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING
Win32 ASM: The Basics
by Iczelion
Tools necessari:
-Microsoft Macro Assembler 6.1x : il supporto MASM per la programmazione
Win32 comincia dalla versione 6.1. L'ultima versione è la 6.13 che
è una patch alla versione pecedente alla 6.11. Il Win98 DDK include
MASM 6.11d che può essere scaricato da Microsoft da
http://www.microsoft.com/hwdev/ddk/download/win98ddk.exe
Ma ti avviso, questo è mostruosamente grande, 18.5 MB. La patch per
MASM 6.13 può anche essere scaricata da
ftp://ftp.microsoft.com/softlib/mslfiles/ml613.exe
-Microsoft import libraries : Puoi usare le import libraries del
Visual C++. Alcune sono incluse nel Win98 DDK.
-Win32 API Reference : Puoi scaricarla dal sito della Borland:
ftp://ftp.borland.com/pub/delphi/techpubs/delphi2/win32.zip
Ecco una breve descrizione del processo di assembly.
MASM 6.1x è fornito di due tools esseziali: ml.exe e link.exe. ml.exe è l'assembler.
Esso crea dal sorgente in assembly (.asm) un file object (.obj). Un file object è un file
intermedio tra il codice sorgente e il file eseguibile. Esso ha bisogno dei fixups per gli
indirizzi, servizio fornito dal link.exe. Link.exe trasforma un file object in eseguibile
agendo su più piani, come aggiungere codice da altri moduli ai file object oppure fornendo
i fixups per gli indirizzi, aggiungendo le risorse, ecc.
Per esempio:
ml skeleton.asm    ---> questo crea skeleton.obj
link skeleton.obj  ---> questo crea skeleton.exe
Le righe sopra sono, naturalmente, delle esemplificazioni. Nel mondo reale, devi aggiungere
diversi switches a ml.exe e link.exe per personalizzare la tua applicazione. Inoltre ci saranno
diversi files che dovrai linkare con il file object per creare la tua applicazione.
I programmi per Win32 sono eseguiti in "protected mode" che e' disponibile sin
dai tempi degli 80286. Ma gli 80286 ormai sono storia. Percio' dovremo
relazionarci all' 80386 e i suoi discendenti. Windows esegue ogni singolo
programma Win32 in uno spazio virtuale separato e unico. Cio' significa che ogni
programma Win32 avra' i suoi propri 4 GB di address space. Ogni programma e'
solo nel suo address space. Cio' e' in contrasto con la situazione presente in
Win16. Tutti i programmi Win16 possono *vedersi* gli uni con gli altri. Questo
non accade in Win32. Tale caratteristica aiuta a ridurre la probabilita' che un
programma scriva sul codice/dati di un altro.
Il memory model (modello di memoria, NdT) e' parimenti drasticamente diverso dai
vecchi giorni del mondo a 16-bit. In Win32, non e' piu' necessario preoccuparsi
del modello di memoria o dei segmenti! C'e' solo UN modello di memoria: il Flat
memory model. Non esistono piu' segmenti da 64K. La memoria e' un largo e
continuo spazio di 4 GB. Questo significa anche che non bisognera' piu' giocare
con i segment registers. Potrete usare qualsiasi segment register per
indirizzare qualsiasi punto nello spazio di memoria. Questo e' un GRANDE aiuto
ai programmatori, ed e' cio' che rende la programmazione in assembly per Win32
semplice quanto quella in C.
We will examine a miminal skeleton of a Win32 assembly program. We'll add more
flesh to it later. Ecco lo scheletro del programma. Se non comprendete alcune parti del codice,
niente panico. Spieghero' ciascuna di esse in seguito.
.386
.MODEL Flat, STDCALL
.DATA

......
.DATA?

......
.CONST

......
.CODE
:

.....
end 
Ecco tutto! Analizziamo questo scheletro.
.386
Questa e' una direttiva per l'assembler, a cui diciamo di usare il set di
istruzioni 80386. Potreste anche usare .486, .586 ma la scelta piu' sicura e'
quella di utilizzare sempre .386.
.MODEL FLAT, STDCALL
.MODEL e' una direttiva per l'assembler che specifica il modello di memoria del
nostro programma. Sotto Win32, esiste un solo modello di memoria, il modello
FLAT. STDCALL comunica a MASM la convenzione nel passare i parametri. Questa
convenzione specifica l'ordine con cui i parametri verranno passati, da
sinistra-verso-destra o da destra-verso-sinistra, oltre a chi bilanciera' lo
stack frame dopo la chiamata di una call (procedura, NdT).
In Win16, ci sono due tipi di convenzioni di chiamata, C e PASCAL
La convenzione di chiamata C passa i parametri da destra a sinistra, cioe', il
parametro all'estrema destra e' PUSHato per primo. Il caller e' responsabile del
bilanciamento dello stack frame dopo la call. Ad esempio, volendo chiamare una
funzione denominata foo(int primo_param, int secondo_param, int terzo_param) con
la convenzione C, il codice assomiglierebbe a questo:
push  [terzo_param]              ; Pusha il terzo parametro
push  [secondo_param]            ; Seguito dal secondo
push  [primo_param]              ; E dal primo
call    foo
add    sp, 12                    ; Il caller bilancia lo stack frame
La convenzione PASCAL e' l'inverso di quella per il C. Essa passa i parametri da
sinistra a destra, e il callee e' responsabile per il bilanciamento della stack
dopo la call.
Win16 adotta la convenzione PASCAL poiche' produce codici piu' piccoli. la
convenzione C e' utile quando non si conosce il numero dei parametri che
verranno passati alla funzione, come nel caso di wsprintf(). Nel caso di
wsprintf(), la funzione non ha modo di determinare aprioristicamente il numero
di parametri che verrano pushati sulla stack, percio' non puo' bilanciare lo
stack frame.
STDCALL e' un ibrido tra le convenzioni C e PASCAL. Essa passa i parametri da
destra a sinistra ma il callee e' responsabile per il bilanciamento della stack
dopo la call. La piattaforma Win32 usa esclusivamente STDCALL. Eccetto in un
caso: wsprintf(). Dovete usare la convenzione C con wsprintf().
.DATA
.DATA?
.CONST
.CODE
Tutte e quattro le direttive sono cio' che viene definito SEZIONE. Non avete
segmenti in Win32, ricordate ? Ma potete dividere il vostro intero address space
in sezioni logiche. L'inizio di una sezione definisce la fine della sezione
precedente. Ci sono due gruppi di sezioni: dati e codice. Le sezioni dei dati
sono divise in 3 categorie:
.DATA    Questa sezione contiene i dati inizializzati del vostro programma.
.DATA?  Questa sezione contiene i dati NON inizializzati del vostro
programma. A volte capita di voler impegnare una parte di memoria (variabli,
NdT) senza inizializzarla. Questa sezione serve a tale scopo.
.CONST  Questa sezione contiene le costanti usate dal vostro programma. Le
costanti in questa sezione non potranno mai essere modificate dal vostro
programma. Sono semplicemente *costanti*.
Non e' necessario utilizzare tutte e tre le sezioni nel vostro programma.
Dichiarate solo la/e sezione/i che volete usare.
C'e' solo una sezione per il codice: .CODE. Qui e' dove le vostre istruzioni
risiedono. Ad esempio:
:
end 
...dove  e' una qualsiasi etichetta arbitraria usata per specificare
l'estensione del vostro programma. Entrambe le etichette devono essere
identiche. Tutto il vostro codice deve risiedere tra
 ed end 
Traduttore in lingua italiana : -NeuRaL_NoiSE
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING
MessageBox Display
by Iczelion
In questo tutorial, creeremo un programma per Windows completamente funzionante
che mostra un box con il messaggio "Win32 assembly is great!".
Windows prepara una grossa quantita' di risorse per i programmi Windows. Al centro di cio' c'e'
la Windows API (Application Programming Interface, Interfaccia per la Programmazione di
Applicazioni, NdT). La Windows API e' un'immensa collezione di utilissime funzioni contenute
in Windows stesso, pronte
ad essere usate da qualsiasi programma per Windows.
Queste funzioni risiedono in DLL (dynamic-linked libraries, librerie collegate dinamicamente
al programma, NdT) come kernel32.dll, user32.dll e gdi32.dll. Kernel32.dll contiene funzioni
API relative alla memoria e alla gestione dei processi. User32.dll controlla gli aspetti
dell'interfaccia utente del vostro programma.Gdi32.dll e' responsabile per le operazioni
grafiche. Oltre alle "principali tre", ci sono altre DLL che il vostro programma puo' usare,
ammesso che voi possediate abbastanza informazioni riguardo alla funzione API desiderata.
I programmi per Windows si linkano (collegano, NdT) dinamicamente a queste DLL,
in altre parole il codice per le funzioni API non e' incluso nell'eseguibile del
programma per Windows. Per comunicare al vostro programma dove trovare le
funzioni API desiderate al momento dell'esecuzione, dovrete accludere tale
informazione nel file eseguibile. L'informazione risiede nelle import libraries
(librerie importate, NdT). Dovrete linkare il vostro programma con le corrette
import libraries o esso non sara' capace di localizzare le funzioni API.
Esistono due tipi di funzioni API: Uno per ANSI e uno per Unicode. Il nome delle
funzioni API per ANSI e' postfissato con "A", ad esempio MessageBoxA. Quelle per
Unicode sono postfissate con "W" (per Wide Char, credo).
Windows 95 supporta nativamente ANSI e l'Unicode Windows NT. Ma la maggior parte delle volte,
utilizzerete un file di include che puo' determinare e selezionare le funzioni API appropriate
per la vostra piattaforma. Semplicemente riferitevi alla funzione API senza il postfisso.
Presentero' semplicemente lo scheletro del programma qui sotto. Lo riempiremo
successivamente.
.386
.model flat, stdcall
.data
.code
Main:
end Main
Ogni programma per Windows deve chiamare una funzione API, ExitProcess, quando
vuole uscire a Windows. In quest'ottica, ExitProcess e' equivalente a int 21h, ah=4Ch in DOS.
Ecco il prototipo per la funzione ExitProcess da winbase.h:
void WINAPI ExitProcess(UINT uExitCode);
-void significa che la funzione non restituisce nessun valore al caller.
-WINAPI e' un alias della convenzione di chiamata STDCALL.
-UINT e un tipo di dati, "unsigned integer", che e' un valore a 32-bits sotto
Win32 (e' un valore a 16-bits sotto Win16)
-uExitCode e' il codice a 32-bits di ritorno a Windows. Questo valore non e'
usato da Windows al momento.
Per chiamare ExitProcess da un programma in assembly, dovrete prima dichiarare
il function prototype (prototipo di funzione, NdT) per ExitProcess.
.386
.model flat, stdcall
ExitProcess     PROTO      ,:DWORD
.data
.code
Main:
INVOKE    ExitProcess, 0
end Main
Ecco tutto. Il vostro primo programma funzionante per Win32. Salvatelo come
msgbox.asm.
Presupponendo che ml.exe e' nella vostra path, assemblate msgbox.asm con:
ml  /c  /coff  /Cp msgbox.asm
/c dice a MASM di assemblare soltanto. Non invoca Link.
/coff dice a MASM di creare un file .obj in formato COFF.
/Cp dice a MASM di conservare le caratteristiche di formattazione
(maiuscole/minuscole) degli identificatori (variabili, NdT) dell'utente.
Quindi procedete con link:
link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm611\lib  msgbox.obj  kernel32.lib
/SUBSYSTEM:WINDOWS  dice a Link che tipo di eseguibile e' questo programma.
/LIBPATH: dice a Link dove sono le import
libraries. Sul mio PC, sono sotto c:\masm\lib
Adesso avete ottenuto msgbox.exe. Andate avanti, fatelo partire. Scoprirete che
non fa niente. Beh, non ci abbiamo ancora inserito niente di interessante. Ma e'
senza ombra di dubbio un programma per Windows. E osservate le sue dimensioni!
Sul mio PC, il file e' lungo 1,536 bytes.
La linea:
ExitProcess     PROTO      ,:DWORD
e' un prototipo di funzione. Voi dichiarate il nome della funzione seguito dalla
parola chiave "PROTO", una virgola, e la lista del tipo di dati dei parametri.
MASM usa il prototipo di funzione per controllare il numero e il tipo di
parametri della funzione.
Il miglior posto per i prototipi di funzione e' un file di include. Potete
creare un file di include pieno di prototipi di funzioni e strutture di dati
frequentemente usati e includerlo all'inizio del vostro programma asm.
Chiamate le funzioni API usando la parola chiave INVOKE:
INVOKE    ExitProcess, 0
INVOKE e' in pratica una specie di call specializzata. Essa controlla il numero
e il tipo di parametri e li pusha sulla stack seguendo la convenzione di
chiamata predefinita (in questo caso, stdcall). Usando INVOKE invece del normale
CALL, potete prevenire gli errori della stack derivanti da un passaggio di
parametri incorretto. Molto utile. La sintassi e':
INVOKE  espressione [,argomenti]
dove espressione e' un'etichetta o il nome di una funzione.
Successivamente, metteremo su una message box. La dichiarazione per questa
funzione e':
int WINAPI MessageBoxA(HWND hwnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
-hwnd e' l'handle della parent window (finestra-genitrice, NdT :)
-lpText e' un puntatore al testo che volete mostrare nella client area (l'area a
disposizione della message box, NdT)
-lpCaption e' un puntatore al titolo della message box
-uType specifica l'icona e il numero e tipo dei bottoni della message box
Sotto la piattaforma Win32, HWND, LPCSTR, e UINT sono tutti valori della
dimensione di 32 bits.
Modifichiamo msgbox.asm per includere la message box.
.386
.model flat, stdcall
ExitProcess      PROTO      ,:DWORD
MessageBoxA PROTO      ,:DWORD, :DWORD, :DWORD, :DWORD
.data
MsgBoxCaption  db "Iczelion Tutorial No.2",0
MsgBoxText       db "Win32 Assembly is Great!",0
.const
NULL     equ  0
MB_OK    equ  0
.code
Main:
INVOKE    MessageBoxA, NULL, ADDR MsgBoxText, ADDR MsgBoxCaption, MB_OK
INVOKE    ExitProcess, NULL
end Main
Assemblatelo così:
Assemble it by:
ml /c /coff /Cp msgbox.asm
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm\lib msgbox kernl32.lib user32.lib
Dovrete includere user32.lib nel parametro di Link, poiche' le informazioni per linkare
MessageBoxA risiedono in user32.lib
Vedrete una message box che mostra il testo "Win32 Assembly is Great!".
Diamo un'altra occhiata al codice.
Definiamo due stringhe terminate con zero (zero-terminated) nella sezione .data. Ricordate che
tutte le stringhe in Windows devono essere terminate con zero (ASCIIZ).
Definiamo due costanti nella sezione .const. Utilizziamo le costanti per
rendere piu' chiaro il codice.
Osservate i parametri della funzione MessageBoxA. Il primo parametro e' NULL.
Cio' significa che non c'e' nessuna finestra che *possiede* questa message box.
L'operatore "ADDR" e' usato per passare l'indirizzo dell'etichetta alla
funzione. Questo operatore è specifico di MASM. Non esiste un equivalente di TASM.
Funziona come l'operatore "OFFSET" ma con alcune differenze:
1. Non accetta le forward reference. Se vuoi usare "ADDR foo",
devi dichiarare "foo" prima di usare l'operatore ADDR.
2. Può essere usato con una variabile locale. Una variabile local è
una variabile creata nello stack. L'operator OFFSET non piò essere
usato in questa situazione perchè l'assembler non conosce il reale
indirizzo della variabile locale quando lo assembla.
Traduttore : -NeuRaL_NoiSE
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::........................THE.C.STANDARD.LIBRARY.IN.ASSEMBLY
The _itoa, _ltoa and _ultoa functions
by Xbios2
ATTENZIONE I:
Questo documento è basato sul Borland C++ 4.02. Quando mi è stato possibile l'ho controllato
con altre librerie / programmi contenenti le funzioni specifiche, ci potrebbero comunque
essere delle differenze tra questa e la tua versione di C. Inoltre questo è solo codice 32-bit,
Windows compiler. Niente DOS o UNIX.]
ATTENZIONE II:
I confronti di grandezza sono davvero facili da compiere. I confronti di velocità un po' meno.
Le differenze di velocità da me rilevate sono basate sui timings RDTSC, ma NON prendono in
considerazione casi estremi. E' questo il motivo per cui non fornisco il numero di cicli di
clock esatti. Naturalmente se hai bisogno dei cicli di clock esatti per il tuo Pentium II,
puoi sempre comprarmene uno :)
Il linguaggio C offre 3 funzioni per convertire un integer in ASCII:
char *itoa(int value, char *string, int radix);
char *ltoa(long value, char *string, int radix);
char *ultoa(unsigned long value, char *string, int radix);
_itoa e _ltoa fanno _esattamente_ la stessa cosa. Questo perchè un integer _è_ un long codice
32-bit. Però sono diversi: _itoa ha del codice _completamente_ inutile in sè (nel 16bit questo
codice il valore sign-extend se radix=10).
Comunque il risultato è sempre lo stesso, quindi _ltoa da qui in poi significa sia _ltoa che
_itoa. _ultoa esattamente uguale a _ltoa e _itoa, tranne quando radix=10 e il valore < 0.
In ogni modo tutte queste funzioni fanno riferimento a questa:
___longtoa(value, *string, radix, signed, char10)
I primi tre parametri sono passati 'così come sono', signed è settato ad 1 da _ltoa se
radix=10, altrimenti è settato a 0 e char10 è il carattere corrispondente a 10 se radix>10,
ed è sempre settato 'a' (___longtoa è anche utilizzato da printf, che ha un'opzione per
ottenere i caratteri maiuscoli in Hex).
___longtoa esegue le seguenti (e lo fa con codice scritto male):
1. Controlla che 2<=radix<=36, se non lo è , restituisce '0'
2. Se signed=1 e value<0 aggiunge '-' alla stringa e fa 'neg' sul valore
3. Loop1: crea una pseudo-string nello stack, invertita
4. Loop2: converte e copia la pseudo-string nella string
Il controllo su radix è necessario perchè:
radix=0 genererebbe un INT0 (divisione per zero)
radix=1 metterebbe l'applicazione in un loop infinito, distruggendo lo stack
radix=37 per valore=36 restituirebbe '}', il carattere dopo 'z'
I due loops sono necessari in ragione della maniera in cui la conversione è svolta.
(vedi il codice dopo). Per implementare una conversione a loop-unico, il numero di caratteri
dovrebbe essere calcolato in anticipo, con il risultato di un codice meno efficiente
(il numero dei caratteri nel valore è n=(int)(log(value)/log(radix))+1, ma usare un loop in
più è molto più veloce).
Includendo il listato disassembly delle funzioni di C allungherebbe di molto l'articolo, e in
ogni caso sono quelli solo esempi di codice davvero brutto. Quindi, dritti al risultato:
ltoa proc
cmp dword ptr [esp+0Ch], 10
sete ch
mov cl, 'a'-'0'-10
jmp short longtoa
ultoa:
mov cx, 'a'-'0'-10
longtoa:
push ebx
push edi
push esi
sub esp, 24h
mov ebx, [esp+3Ch]  ; radix
mov eax, [esp+34h]  ; valore
mov edi, [esp+38h]  ; stringa
cmp ebx, 2
jl short _ret
cmp ebx, 36
jg short _ret
or eax, eax
jge short skip
cmp byte ptr ch, 0  ; _ltoa ?
jz short skip
mov byte ptr [edi], '-'
inc edi
neg eax
skip: mov esi, esp
loop1: xor edx, edx
div ebx
mov [esi], dl
inc esi
or eax, eax
jnz loop1
loop2: dec esi
mov al, [esi]
cmp al, 10
jl short nochar
add al, cl
nochar: add al, '0'
stosb
cmp esi, esp
jg short loop2
_ret: mov byte ptr [edi], 0
mov eax, [esp+38h]
add esp, 24h
pop esi
pop edi
pop ebx
ret
ltoa endp
C'è un 3 in 1 procedura. ltoa e ultoa prendono gli stessi parametri come le funzioni standard
di C. longtoa era stato cambiato per prendere dallo stack gli stessi parametri di ltoa e ultoa,
mentre signed e char10 sono passati attraverso CH e CL rispettivamente. In questo modo ltoa e
ultoa 'vedono' longtoa come 'proprio' codice, e non come una procedura diversa (ciò per
evitare un problema comune in C, le procedure che 'inoltrano' i loro parametri ad un'altra
funzione).
Questo codice si compila in 102 bytes (e potrebbe essere ottimizzato per 'grattare' altri
byte), quando invece il codice standard di C impiega 270 bytes. Precisamente:
function   C size     Asm size
------------------------------
itoa          60           0
ltoa          40          12
ultoa         27           4
longtoa      143          86
------      ------
total   270         102
Va pure 2 volte più veloce di ltoa. E inoltre, questa è una versione completamente
C-compatibile di ltoa e ultoa. Naturalmente potrebbe essere adattata da C-compatibile in altro
per venire incontro a necessità specifiche (p.e renderla stdcall invece di cdecl, oppure se la
velocità e la dimensione sono cruciali si può rimuovere il controllo per radix, e così via...)
Ad ogni modo, è abbastanza anomalo che non userai mai valori di radix che differiscano
da 2, 8, 10 o 16. Quindi se velocità e dimensione sono l'essenza, può essere scritta una
routine migliore e più specifica. Per esempio, considera questa routine che deposita il valore
di EAX come un numero binario all'indirizzo specificato da EDI:
ultob proc
mov ecx, 32
more1: shl eax, 1
dec ecx
jc more2
jnl more1
more2: setc dl
add dl, '0'
shl eax, 1
mov   [edi], dl
inc   edi
dec   ecx
jnl more2
mov [edi], al
ret
ultob endp
Questa è 14 volte più veloce di ltoa in C, e 7 volte più veloce di ltoa in Asm, ed è di soli
29 bytes. Ma questo articolo è già abbastanza lungo, quindi aspetta per un altro articolo su
funzioni 'ltoa' specifiche (chi lo sa, forse potrei decidere di scrivere una funzione 'printf'
in Asm, che potrebbe usarle...).
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::............................................THE.UNIX.WORLD
x86 ASM Programming for Linux
by mammon_
Essenzialmente questo articolo è una scusa per conciliare i miei due interessi favoriti di
coding: il sistema operativo Linux e la programmazione in linguaggio assembly. Tutti e due
gli argomenti non necessitano (meglio, non dovrebbero) di una introduzione; come l'assembly
Win32, assembly per Linux è eseguito in protected mode 32-bit... comunque ha il netto
vantaggio di permetterti di chiamare le funzioni delle librerie standard C come ogni altra
funzione delle normali librerie Linux "condivise". Ho cominciato con una breve introduzione
sulla compilazione dei programmi in assembly language per Linux; per una migliore leggibilità
potresti bypassarla e andare direttamente alla sezione su "Le Basi".
Compiling e Linking
---------------------
I due assemblers principali per Linux sono Nasm, l'Assembler (gratis) di Netwide, e GAS,
l'Assembler (pure gratis) di Gnu, integrato in GCC. Mi concentrerò su Nasm in questo articolo,
lasciando GAS per un altro giorno dal momento che usa la sintassi AT&T e ciò richiederebbe una
introduzione più prolissa.
Nasm dovrebbe essere azionato con l'opzione di formato ELF ("nasm -f elf hello.asm"); l'object
che ne deriva è poi linkato con GCC ("gcc hello.o") per creare il binario ELF finale. Lo script
seguente può essere usato per compilare moduli ASM; l'ho scritto in modo che sia molto
semplice, quindi tutto ciò che fa è prendere il primo filename passatogli (io consiglio di
chiamarlo con una estensione ".asm"), lo compila con nasm, e lo linka con gcc.
#!/bin/sh
# assemble.sh =========================================================
outfile=${1%%.*}
tempfile=asmtemp.o
nasm -o $tempfile -f elf $1
gcc $tempfile -o $outfile
rm $tempfile -f
#EOF ==================================================================
Le Basi
----------
La cosa migliore per partire, naturalmente, è un esempio, prima di immergerci nei dettagli
dell'OS. Ecco qui un programma "hello-world" davvero semplice:
global main
extern printf
section .data
msg db "Helloooooo, nurse!",0Dh,0Ah,0
section .text
main:
push dword msg
call printf
pop eax
ret
Una spiegazione veloce: il "global main" deve essere dichiarato global—-e dal momento che
stiamo usando il linker GCC, l'entrypoint deve essere chamato "main"--per il loader dell'OS.
L'"extern printf" è semplicemente una dichiarazione per la call successiva nel programma;
nota che questo è tutto il necessario; non è necessario dichiarare le dimensioni dei parametri.
Ho diviso questo esempio nelle sezioni standard .data e .text, sebbene ciò non sia
strettamente necessario –-chiunque potrebbe svignarsela con il solo segmento .text, proprio
come in DOS.
Nel corpo del codice, nota che devi pushare i parametri alla call, e in Nasm devi dichiarare
la dimensione di tutti i dati ambigui (p.e. non-register): di qui il qualificatore "dword".
Nota che come in altri assemblatori, Nasm assume che ogni reference memory/label è volta a
significare l'indirizzo della locazione di memoria o della label, non il loro contenuto.
Perciò, per specificare l'indirizzo della stringa 'msg' scriveresti 'push dword msg', mentre
per specificare il contenuto della stringa 'msg' scriveresti 'push dword [msg]' (nota che
questo conterrà solo i primi 4 bytes di 'msg'). Dal momento che printf richiede un pointer
alla string, specificheremo l'indirizzo di 'msg'.
La call a printf è abbastanza lineare. Considera che pulire lo stack dopo ogni call che esegui
(vedi sotto); quindi, avendo PUSHato una dword, POPpiamo una dword dallo stack in un registro
"da cestinare". I programmi Linux si chiudono semplicemente con una RET all'OS, dato che ogni
processo è aperto dalla shell (o PID 1 ;) e finisce restituendogli il controllo.
Nota che in Linux fai uso delle librerie standard condivise fornite con l'OS in luogo di una
"API" di degli Interrupt Services. Tutte le reference esterne saranno risolte dal linker GCC,
in modo da alleggerire buona parte del carico di lavoro del programmatore asm. Una volta che
ti sei abituato alle stranezze di base, il coding in assembler in Linux è davvero più semplice
di quello su una macchina DOS-based!
La sintassi di chiamata C
--------------------
Linux usa la convenzione di chiamata C -– ciò significa che gli argomenti sono pushati nello
stack in ordine inverso (l'ultimo arg per primo), e che il caller deve pulire lo stack.
Puoi far ciò o poppando i valori dallo stack:
push dword szText
call puts
pop ecx
o modificando direttamente ESP:
push dword szText
call puts
add esp, 4
I valori restituiti dalla call si trovano in eax o in edx:eax se il valore è più grande di
32-bit. EBP, ESI, EDI, e EBX sono tutti salvati e ripristinati dal caller.
Nota che devi conservare tutti i registri che usi come illustra il codice seguente:
global main
extern printf
section .text
msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
main:
mov ecx, 0Ah
push dword msg
looper:
call printf
loop looper
pop eax
ret
A primo acchito questo sembra molto semplice: dal momento che stai per usare la stringa nelle
call 10 printf(), non hai bisogno di ripulire lo stack. Tuttavia quando lo compili, il loop
non si ferma mai. Perché? Perchè da qualche parte nella call printf()ECX è usato e non salvato.
Quindi per far funzionare il tuo loop a dovere, devi salvare il valore del contatore in ECX
prima della call e ripristinarlo dopo, così:
global main
extern printf
section .text
msg db "HoodooVoodoo WeedooVoodoo",0Dh,0Ah,0
main:
mov ecx, 0Ah
looper:
push ecx           ;salva Count
push dword msg
call printf
pop eax            ;pulisce lo stack
pop ecx            ;ripristina Count
loop looper
ret
Programmazione della Porta I/O
--------------------------------
E per avere un accesso diretto all'hardware? In Linux hai bisogno di un driver kernel-mode per
fare ogni cosa che sia davvero ingegnosa... ciò significa che il tuo programma finirà per
essere di due parti, una kernel-mode che fornisce le funzionalità direct-hardware, l'altra
user-mode per una interface. La buona notizia è che puoi ancora accedere alle porta usando i
comandi IN/OUT da un programma user-mode.
L'accesso alle porte I/O al tuo programma deve essere concesso da un permesso dell'OS; per far
ciò, devi compiere una call ioperm(). Questa funzione può essere chiamata solo da un utente
root, quindi devi o setuid() il programma come root oppure eseguire il programma da root.
La ioperm() ha la sintassi seguente:
ioperm( long StartingPort#, long #Ports, BOOL ToggleOn-Off)
dove 'StartingPort#' specifica il numero della prima porta da accedere (0 is port 0h, 40h is
port 40h, etc), '#Ports' specifica quante porte accedere (i.e., 'StartingPort# = 30h' e
'#Ports = 10' concederebbero l'accesso alle porte 30h-39h), e 'ToggleOn-Off' consente l'accesso
se TRUE (1) o lo disabilita se FALSE (0).
Una volta che la call a ioperm() è compiuta, si può accedere alle porte richieste come normal.
Il programma può chiamare ioperm() un qualsivoglia numero di volte e non ha bisogno di fare un
successiva call ioperm() (anche se l'esempio sotto lo fa) [siccome l'OS si curerà di ciò].
BITS 32
GLOBAL szHello
GLOBAL main
EXTERN printf
EXTERN ioperm
SECTION .data
szText1 db 'Enabling I/O Port Access',0Ah,0Dh,0
szText2 db 'Disabling I/O Port Acess',0Ah,0Dh,0
szDone  db 'Done!',0Ah,0Dh,0
szError db 'Error in ioperm() call!',0Ah,0Dh,0
szEqual db 'Output/Input bytes are equal.',0Ah,0Dh,0
szChange db 'Output/Input bytes changed.',0Ah,0Dh,0
SECTION .text
main:
push dword szText1
call printf
pop ecx
enable_IO:
push word 1    ; enable mode
push dword 04h ; 4 porte
push dword 40h ; inizia dalla porta 40
call ioperm    ; Deve essere SUID "root" per questa call!
add ESP, 10    ; pulisci lo stack (metodo 1)
cmp eax, 0     ; controlla i risultati di ioperm()
jne Error
SetControl:
mov al, 96     ; R/W low byte di Counter2, mode 3
out 43h, al    ; porta 43h = control register
WritePort:
mov bl, 0EEh   ; valore da inviare allo speaker timer
mov al, bl
out 42h, al    ; porta 42h = speaker timer
ReadPort:
in al, 42h
cmp al, bl     ; il byte dovrebbe essere cambiato--questo E' un timer :)
jne ByteChanged
BytesEqual:
push dword szEqual
call printf
pop ecx
jmp disable_IO
ByteChanged:
push dword szChange
call printf
pop ecx
disable_IO:
push dword szText2
call printf
pop ecx
push word 0    ; disable mode
push dword 04h ; 4 porte
push dword 40h ; parte dalla porta 40h
call ioperm
pop ecx        ;pulisci lo stack (metodo 2)
pop ecx
pop cx
cmp eax, 0     ; controlla i risultati di ioperm()
jne Error
jmp Exit
Error:
push dword szError
call printf
pop ecx
Exit:
ret
Usare gli Interrupts In Linux
-------------------------
Linux è un ambiente shared-library in protected mode, il che significa che non ci sono i
servizi interrupt. Giusto?
Sbagliato. Ho notato una call a INT 80 sul codice di alcuni esempi GAS con il commento
"sys_write(ebx, ecx, edx)". Questa funzione è parte della syscall dell'interfaccia di Linux,
e cioè l'interrupt 80 deve essere un gate ai servizi di syscall. Girovagando nel codice
sorgente di Linux (e ignorando gli avvisi di NON USARE MAI l'interface INT 80 siccome i numeri
della funzione potrebbere essere cambiati all'improvviso), ho trovato i
"system call numbers" –-che indicano la funzione da passare a INT 80 per ogni routine di
syscall–- nel file UNISTD.H. Ce ne sono 189, quindi non li elencherò qui... ma se ti accingi a
programmare in Linux assembly, fa' un favore a te stesso e stampa questo file.
Quando chiami INT 80h, eax deve contenere il numero della funzione desirata. Tutti i parametri
alla routine syscall devono trovarsi nei seguenti registri in questo ordine:
ebx, ecx, edx, esi, edi
quindi il parametro uno si trova in ebx, il parametro 2 in ecx, ecc. Nota non si usa lo stack
per passare i valori alla routine syscall. Il risultato della call sarà restituito in eax.
Inoltre, l'interfaccia INT 80 è uguale ad una normale call (solo un po' più divertente ;).
Il programma seguente dimostra una semplice call a INT 80h in cui il programma controlla e
visualizza la sua PID. Nota l'uso del formato della stringa di printf() –-è meglio
psuedocodarlo come una call C prima, poi rendere il formato della stringa DB e pushare ogni
variabile passata (%s, %d, ecc). La struttura C per questa call sarebbe
printf( "%d\n", curr_PID);
Nota anche che le sequenze di escape ("\n") non sono tutte davvero attendibili in assembly;
ho dovuto usare i valori hex (0Ah,0Dh) per il CR\LF.
BITS 32
GLOBAL main
EXTERN printf
SECTION .data
szText1 db 'Getting Current Process ID...',0Ah,0Dh,0
szDone  db 'Done!',0Ah,0Dh,0
szError db 'Error in int 80!',0Ah,0Dh,0
szOutput db '%d',0Ah,0Dh,0           ;la strana formattazione è per printf()
SECTION .text
main:
push dword szText1    ;messaggio di apertura
call printf
pop ecx
GetPID:
mov eax, dword 20       ; getpid() syscall
int 80h               ; syscall INT
cmp eax, 0            ; non sarà mai PID 0 ! :)
jb Error
push eax              ; passa il valore restituito a  printf
push dword szOutput   ; passa il formato della stringa a printf
call printf
pop ecx               ; pulisci lo stack
pop ecx
push dword szDone     ; messaggio di chiusura
call printf
pop ecx
jmp Exit
Error:
push dword szError
call printf
pop ecx
Exit:
ret
Ultime considerazioni
-----------------------
Il più dei problemi deriverà dall'abituarsi a Nasm stesso. Mentre nasm non è fornito di una
man page, non la installa per default, quindi devi spostarla (cp or mv) da
/usr/local/bin/nasm-0.97/nasm.man
in
/usr/local/man/man1/nasm.man
La formattazione è un po' incasinata, ma è facilmente risistemata usando le direttive nroff.
Non ti dà ancora tutta la documentazione di Nasm, comunque; per questo, copia nasmdoc.txt da
/usr/local/bin/nasm-0.97/doc/nasmdoc.txt
in
/usr/local/man/man1/nasmdoc.man
Ora puoi chiamare man page di nasm con 'man nasm' e la documentazione di nasm con 'man nasmdoc'.
Per ulteriori informazioni, controlla i seguenti:
Linux Assembly Language HOWTO
Linux I/O Port Programming Mini-HOWTO
Jan's Linux & Assembler HomePage (bewoner.dma.be/JanW/eng.html)
Devo anche dei ringraziamenti a Jeff Weeks alla code^x software (gameprog.com/codex)
per avermi inviato un paio di hello-world di GAS nelle giornate nere, prima che trovassi la
pagina di Jan.
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::...........................................ISSUE.CHALLENGE
11-byte Program Displays Its Command-Line
by Xbios2
La Sfida
-----------
Scrivi un programma di 11 byte che visualizzi la sua command line.
La Soluzione
--------------
Prima di dire che questi programmi non funzionano, provali. Alcuni di loro funzionano solo
dopo averli avviati due volte. In ogni caso, sono stati testati sia sotto Windows che in DOS
puro, e funzionano. Che tu ci creda o no, questi sono i primi programmi che ho scritto in DOS,
quindi ho solo provato alcune idee finchè alcune hanno funzionato, anche se ho pensato che non
potessero... :)
La command line in DOS si trova nel PSP (Program Segment Prefix, Prefisso di Segmento del
Programma, NdT) che nei file .COM occupa i primi 100h bytes nel segmento. All'offset 80h, una
stringa  (il primo byte è la lunghezza della stringa, e n bytes seguono) contiene
tutto ciò che è stato digitato dopo il nome del file. L'ultimo carattere nella stringa è CR
(carriage return, invio NdT).
I programmi richiesti dovrebbero essere composti di tre parti:
1. settaggio dei pointers ai dati
2. visualizzazione dei dati
3. uscita
In effetti tutti i programmi seguenti NON includono la parte 3, ma continua a leggere. I dati
(command line) possono essere scritti o come una singola stringa, o carattere per carattere.
APPROCCIO 1: Scrivi una singola stringa
------------------------------------------
Per il primo approccio ci sono 2 interrupts:
1. INT 21, 9 ; scrivi una stringa string '$ terminated'
2. INT 21, 40 ; scrivi sul file usando un handle
Nel primo caso, la parte 2 sarebbe:
mov ah, 9
mov dx, 81h
int 21h
che sono 7 bytes, lasciando solo 4 bytes per sostituire l'ultimo CR con un '$',
che sono troppo pochi. (Effettivamente, se l'utente digitasse un $ come ultimo carattere nella
comand line, questo sarebbe il programma più piccolo possibile.) Il programma più piccolo che
son riuscito a scrivere è:
shr si,1   ; D1 EE
lodsb    ; AC
push si   ; 56
add si,ax   ; 03 F0
mov byte ptr [si],'$' ; C6 04 24
xcgh bp,ax   ; 95
pop dx   ; 5A
int 21   ; CD 21
Per il secondo caso, il più piccolo programma sarebbe questo:
mov dx, 81h   ; BA 81 00
mov cl, ds:[80h]  ; 8A 0E 80 00
mov ah, 40h   ; B4 40
int 21h        ; CD 21
Le prime due righe sono la parte 1 (settaggio dei pointers) e le altre due sono la parte 2
(visualizzazione della stringa). Se pensi che manca qualcosa, hai ragione: non settiamo BX
(l'handle).
APPROCCIO 2: Scrivi char per char
------------------------------
Per il secondo approccio ci sono interrupts:
1. INT 21, 2 ; scrivi il char in dl
2. INT 29       ; scrivi il char in al
Naturalmente il secondo interrupt è meglio, dal momento che non c'è bisogno di caricare ah con
il valore di una funzione. In più, INT 29 legge il char da AL, quindi può essere usato con
LODSB.
Il primo modo per implementare questo approccio è ridurre la parte 2 (display loop).
Un programma che fa ciò è il seguente:
mov si, 80h ; BE 80 00
lodsb   ; AC
mov cl, al ; 8A C8
more: lodsb   ; AC
int 29h  ; CD 29
loop more  ; E2 FB
Questo programma ha scritto CX caratteri. Il secondo modo per scrivere la stringa è scrivere
fino al CR. Ecco come:
mov si, 81h ; BE 81 00
more: lodsb   ; AC
int 29h  ; CD 29
cmp al, 13 ; 3C 0D
jne more  ; 75 F9
nop   ; 90
Si, l'ultima istruzione E' un NOP. Quindi abbiamo un programma di 11-byte che funziona, e ha
anche un NOP in sè. Rimuovendo il NOP si crea un programma ancora più pazzo di 10 bytes, che
visualizza la sua command line E aspetta la pressione di un tasto prima di terminare...
In realtà la soluzione II, sostituendo MOV SI,80h con SHR SI,1, fa la stessa cosa (10 bytes
che visualizza la command line e aspetta che l'utente prema un tasto).
BTW: Davvero non so perchè questi programmi funzionano, sebbene abbia una o due teorie...
La sfida per il prossimo numero
---------------------------------
Scrivi un programma PE (win32) il più piccolo possibile che visualizzi la sua command line.
::/ \::::::.
:/___\:::::::.
/|    \::::::::.
:|   _/\:::::::::.
:| _|\  \::::::::::.
:::\_____\:::::::::::........................................................FIN
#############################################
Per questa traduzione (e per le altre) dell'APJ mi è stata lasciata l'autorizzazione personale
di +mammon
Colgo l'occasione per salutare tutto il crew di RingZ3r0 e di #crack-it ,
particolari ringraziamenti a Neural_Noise per avermi concesso le sue traduzioni dei tutorials
di Iczelion (disponibili nelle pagine di ringzer0)
Little-John
#############################################

Top: Sicurezza su Internet - Back
Previous: Tutorial per Banco v2.5 - Up: Sicurezza su Internet - Next: The Italian CRaCKiNG Encyclopedia vol. 6

This page last updated: Friday 08 November 2002 at 3:57pm

Please send any comments on this page to Astalalista.

Labelled with ICRA

This page is powered by Copyright Button(TM).
Click here to read how this page is protected by copyright laws.

Use this button to
NOT browser button


begin banner Exchange zone

Member of BannerPower Rotation System

Home Main Portal Translate Tell A Friend Create Page Create Site Search Site Search WWW E-mail Login Insert A Link Free for All Free Dating Forum disabled-come soon Guestbook Links Vote For Us disabled-come soon Health News | CTX Projectors | Small Business Voip | Online Insurance Quotes | Search Promotion Quote Seo Quote Search Engine Marketing Services Increase Search Listings