Mini curso assembly
-
Upload
franciny-salles -
Category
Internet
-
view
323 -
download
7
Transcript of Mini curso assembly
MINICURSO
ASSEMBLY
Autor: Greythorne the Technomancer
Traducción: Eidan Yoson
Adaptación: Franciny Salles (#Bl4kd3m0n)
Modulo 1
Cuando termine de leer esta página deberá conocer:
Sistemas de numeración
Operaciones binarias
Sistema de numeración
Estamos habituados al sistema de numeración decimal y nos parece lógico usarlo en todo momento.
Pero hay ocasiones en donde no es el más apropiado. Uno de esos mundos en los que existen
sistemas más descriptivos de los fenómenos que el decimal es el de los procesadores. Por su
naturaleza digital, los procesadores son máquinas esencialmente binarias. Utilizan el sistema de
numeración llamado binario, en el que sólo se disponen dos signos: 0 y 1. Contando
correlativamente de manera binaria, diríamos: 0, 1, 10, 11, 100, 101, 110, 111, ... ¿complicado?
Pero es muy fácil!. Tanto el sistema binario, como el decimal y el hexadecimal, son sistemas en los
que la posición de cada dígito representa información de mucha importancia. Veamos un ejemplo
de cómo se descompone posicionalmente un numero decimal:
El número 7935 = 1000 * 7 + 100 * 9 + 10 * 3 + 1 * 5
Elemental ¿no?. Sin embargo, la numeración romana no goza de tan buenas propiedades y por eso
hace ya tiempo se lo reemplazó por el sistema decimal (a excepción de la numeración de las
páginas del prefacio en los libros y del numero de serie de las películas de Rocky :=)
Como hay diez símbolos (del 0 al 9), una decena representa 10 unidades, una centena representa 10
decenas, etc. Diez unidades de una posición, valen una unidad en la posición contigua a la
izquierda. En el sistema binario, con dos símbolos solamente, cada posición a la izquierda vale el
doble de la que le sigue a la derecha. O lo que es lo mismo decir, la relación entre las sucesivas
posiciones se da según la sucesión
1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536 .....
la que a su vez puede expresarse como potencias crecientes de 2:
20 , 21 , 22 , 23 , 24 , 25 , 26 , 27 , 28 , 29 , 210 , 211 , 212 , 213 , 214 , 215 , 216 .....
Para el sistema de numeración binaria, valen las dos reglas prácticas siguientes:
Un número de n bits puede representar a un decimal de un valor de hasta 2n - 1
El multiplicador del bit de posición n vale 2n
Ejemplos: un número de 8 bits cuenta desde 0 hasta 255. El multiplicador del bit 7 es 128. Notar
que siempre se comienza a contar desde cero. En un número binario, al igual que en un decimal, el
bit menos significativo (correspondiente al multiplicador 20, o sea 1) es el que se escribe más a la
derecha:
bit# 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
mult 32768 16384 8192 4096 2078 1024 512 256 128 64 32 16 8 4 2 1
Veamos como ejemplo práctico un número de 7 bits cualquiera como 1001101 (notar que los bits
se ordenan 6...0)
1001101 = 64 * 1 + 32 * 0 + 16 * 0 + 8 * 1 + 4 * 1 + 2 * 0 + 1 * 1
Esto nos proporciona una forma de traducir (cambiar de base) un número binario a decimal. Basta
sumar aquellos multiplicadores cuyos bits estén en 1 e ignorar aquellos cuyo bit es 0. En nuestro
anterior ejemplo es:
1001101 = 64 + 8 + 4 + 1 = 77 decimal
Para el traspaso de decimal a binario, hay que dividir siempre por 2 y contar sólo los restos, de atrás
hacia adelante. Observese que el resto no es otra cosa que el multiplicador de las potencias de dos
en las anteriores igualdades, las que pueden ser definidas como la sumatoria de los productos de los
restos por sus potencias de dos respectivas Por ejemplo, para el 77 decimal obtenemos los restos:
opreración resto pot.de 2
77 / 2 = 38 r=1 1
38 / 2 = 19 r=0 2
19 / 2 = 9 r=1 4
9 / 2 = 4 r=1 8
4 / 2 = 2 r=0 16
2 / 2 = 1 r=0 32
1 / 2 = 0 r=1 64
Ordenando los restos según las potencias decrecientes de 2, obtenemos nuevamente 1001101.
Los números binarios son los que efectivamente fluyen dentro del procesador en una PC, se
guardan en memoria o disco, o se transmiten (modulados) por modem. Pero un humano no puede
manipular con facilidad números como:
1101 0011 0101 0110 1010 0101 1100 0011
que es de 32 bits (hay 32 símbolos en el número, desde el bit 31 a la izquierda hasta el bit 0, a la
derecha) y se ha ordenado ex-profeso en grupos de a cuatro por cuestiones de comodidad que serán
evidentes algo más adelante. El procesador 80386 hace ya más de una década manipulaba sin
problemas números de 32 bits. Un humano necesita manejarlo de otra manera y por eso se inventó
el sistema hexadecimal, con 16 símbolos, ya que si uno agrupa cuatro bits obtiene 16
combinaciones posibles (24 = 16). Esto tiene una razón. Nuestro sistema decimal no se corresponde
en la cantidad de dígitos con el binario en cambio, el hexadecimal si, porque cada cuatro bits
representan un dígito hexadecimal exacto.
De tal manera, el anterior número de 32 bits se traduce al hexadecimal como uno de 8 dígitos (32
bits agrupados de a 4). Para la conversión podemos usar la tabla binario-decima-hexa qe está algo
más adelante. En un sistema hexadecimal, necesitamos 16 símbolos. Ya que somos muy buenos
manejando números decimales, adoptamos esos diez símbolos (0, 1, 2, 3, 4, 5, 6, 7, 8 y 9) para
empezar, pero hay que agregar otros seis. Mmh ! por qué no A, B, C, D, E y F ? De esta forma, si
me toca contar jugando a las escondidas y quiero hacerlo en hexadecimal (de puro tonto, porque
voy a contar un 60% más:=), tengo que decir: 0, 1,.......8, 9, A, B, C, D, E, F, 10, 11.........18, 19,
1A, 1B, 1C, 1D, 1E, 1F, 20, 21........29, 2A,.........2E, 2F, 30, 31 ...
El anterior e impronunciable numero binario de 32 bits pasa a ser:
0xD356A5C3 hexa, es igual a 3.545.671.107 en decimal
Por cierto que no hice la conversión de binario a decimal a mano con la fórmula anterior, sino que
usé la calculadora de Windows en modo científico, que permite operar o convertir números entre
bases binaria, octal, decimal y hexadecimal. Otra base de numeración posible con traducción de
dígitos exacta al binario es la octal que tiene sólo 8 símbolos (del 0 al 7), con lo cual cada dígito
representa a 3 dígitos binarios, pero está casi en desuso.
Note el lector el "0x" del comienzo, para significar que lo que sigue es un número hexadecimal.
Otro estilo es poner una "h" final, con la precaución de colocar un cero adelante si el número
comienza con A, B, C, D, E o F. Para aquél número de 32 bit utilizado como ejemplo, adoptamos
como notación :
0D356A5C3h
Cada trozo de información recibe un nombre propio según la cantidad de bits que posea:
un bit es la unidad de información binaria y con él se puede contar desde 0 hasta 1
un nibble son cuatro bits y se puede contar desde 0 hasta 15 (0xF en hexa)
con un byte (8 bits) puedo contar desde 0 hasta 255 ó 0xFF hexa
una word tiene 16 bits y permite contar desde 0 hasta 65535 ó 0xFFFF
una double-word (32 bits) permite contar desde 0 hasta 4.294.967.295 ó 0xFFFFFFFF
Cuando usted escuche hablar de direcciones de 32 bits, sepa que hay un espacio de almacenamiento
de 4.294 ... millones de bytes o 4 Gigabytes (o de colores, si estamos hablando de color de 32 bits).
Para finalizar con este tema, aqui hay una tabla que convierte el primer nibble (los primeros 4 bits)
a decimal y a hexa. Usted con ella debe poder convertir cualquier numero binario en hexa y
viceversa:
binario decimal hexa binario decimal hexa
0000 0 0 1000 8 8
0001 1 1 1001 9 9
0010 2 2 1010 10 A
0011 3 3 1011 11 B
0100 4 4 1100 12 C
0101 5 5 1101 13 D
0110 6 6 1110 14 E
0111 7 7 1111 15 F
Operaciones Binarias
En lo que sigue se adopta como convención la lógica positiva, lo que implica:
verdadero = 1 = activo, ------, falso = 0 = inactivo
Hay cinco operaciones binarias básicas: AND, OR, NOT, XOR y ADD. La resta, multiplicación y
división se derivan de estas cinco anteriores. Cualquiera sea la longitud de la palabra o palabras
objeto de la operación, siempre se hace de a un bit por vez de derecha a izquierda (tal como si fuera
una suma o resta con números decimales). Esto permite una definición de cada operación que es
independiente de la longitud del o de los operando(s). La operación NOT es la única que se realiza
sobre un sólo operando (es unaria), y las otras cuatro sobre dos operandos.
o La operación AND (Y) tiene resultado 1 si sus dos operandos son ambos 1
o La operación OR (O) tiene resultado 1 si cualquiera de sus operandos es 1
o La operación XOR tiene resultado 1 si los operandos son distintos (uno en 0 y el otro
en 1)
o La operación NOT (NO) tiene resultado 1 si el operando es 0 y viceversa
o La operación ADD (SUMA) se define igual que con los números decimales
AND OR XOR NOT SUMA
0 * 0 = 0 0 + 0 = 0 0 X 0 = 0 NOT 1 = 0 0 + 0 = 0
0 * 1 = 0 0 + 1 = 1 0 X 1 = 1 NOT 0 = 1 0 + 1 = 1
1 * 0 = 0 1 + 0 = 1 1 X 0 = 1 --- 1 + 0 = 1
1 * 1 = 1 1 + 1 = 1 1 X 1 = 0 --- 1 + 1 = 10
Le extrañó el resultado de la suma? Sin embargo es lo que hacemos en la suma decimal 5+5=10
(nos llevamos "1" para la operación del dígito siguiente). Este llevarse "1" es vastamente usado
entre los procesadores digitales y tiene un nombre especial: carry (lo verá abreviado como CY, C o
CF-por carry flag), lo que en castellano se traduce como "acarreo" (que suena muy mal, asi que le
seguiremos llamando carry). Estas operaciones también se llaman "booleanas" ya que se basan en
el álgebra de Boole (invito al lector a rememorar cuando en la escuela secundaria se preguntaba,
igual que yo, si el álgebra de Boole le serviría alguna vez para algo).
MODULO 2
Cuando termine de leer esta página deberá conocer:
Modelo de procesador X86
Modos de direccionamiento
Modelo de memoria de una PC
Segmentos
Modelo de procesador X86
Los ancestros del bienamado Pentium III no fueron tan poderoso como él (por las dudas alguien lea
esto allá por el 2005 y le arranque una sonrisa el poder del Pentium III, debo decir que hoy,
mediados de 1999 es el procesador más potente disponible para PCs y acaba de salir a la venta).
Todo comenzó hace dos décadas con un oscuro (aunque revolucionario para la época) 8086, con
registros de 16 bits, que para colmo debió por cuestiones monetarias sufrir un "downsizing" hasta el
ridículo 8088 -motor de las renombradas IBM PC, con las mismas instrucciones pero con un bus de
8 bits.
Cuando hablamos de registros de 16 bits queremos decir que el procesador tiene posiciones de
almacenamiento especiales llamadas registros cuyo ancho de palabra es de 16 bits. Y cuando nos
referimos a bus, término de amplia aplicación queremos decir bus de procesador (no el de la placa
madre, ni el de I/O, ni el de los canales IDE). El procesador tiene dos buses pro uno saca
direcciones y por el otro entra instrucciones o entra y saca datos. En el 8088 el bus de datos era de 8
bits, aunque internamente sus registros manipulaban palabras de 16 bits.
Unos años después apareció el legendario 80386 DX, con arquitectura y bus de 32 bits y su
hermano menor, ese engendro con bus de 16 bits que fue el 386SX tan promocionado por las
revistas de vulgarización tipo PCmierdazine, quién sabe con qué oscuro y comercial designio.
Varios años más adelante quisieron darle auge a otro castrado, el no menos nombrado "celeron", un
Pentium II sin caché L2, que es precisamente aquello que hace muy veloz al original.
Todos estos procesadores (y algunos más como el 486) comparten el mismo juego de instrucciones
básico del 8086, al que cada generación le introdujo mejoras, alguna instrucción más, más registros,
multi-thread, predicción de saltos y hasta un fabuloso número de serie único en el Pentium III con
el que Intel no quiere perdernos pisada y al que puede accederse por instrucciones comunes que
permitirían a cualquier servidor Internet saber qué número de procesador tiene el hacker que se
acaba de conectar y con lo cual se acabaría toda diversión en la red (y toda privacidad!!!!!!!).
Pero tal vez el salto tecnológico más revolucionario lo inició el 80386 al permitir un modo de
funcionamiento con cualidades especiales al que se lo llamó "modo protegido". Debido a las
características de este modo, se podían generar "máquinas virtuales", cada una con su propio
espacio de memoria virtual, al que se acceden a través de vectores de 32 bits ubicados en dos tablas
conocidas como GDT y LDT. Este mecanismo permite que a partir del 386 los procesadores Intel
direccionen una memoria virtual de 64 Terabytes (o sea 16.384 espacios de direccionamiento reales
de 4 GB).
Programas
Todo programa fuente assembly, tienen la forma de una lista de instrucciones, rótulos (labels) y
decisiones parecida al siguiente pseudocódigo:
ORG 100h ;Directiva de Ensamblador
label1:
instrucción 1 ;comentario
label2:
instrucción 2
si (comparación) verdadera ir a label 2
instrucción 3
end ;esta es otra instrucción más
En lenguaje assembly, cada instrucción se compone de un nombre mnemónico que determina el
tipo de operación (por ejemplo MOV, PUSH, etc) y un campo de datos que especifica los
operandos sobre los que dicha operación se debe llevar a cabo.
Una línea de programa assembly tiene a su vez tres campos: el de rótulos (labels), el de la
instrucción y un campo de comentarios que siempre comienza con ";" (punto y coma).
El compilador assembly -llamado Assembler o Ensamblador- traduce las instrucciones en códigos
de operación del procesador según comandos especiales que se llaman Directivas de Ensamblador,
para producir un módulo de programa ejecutable. En los programas ejecutables estos códigos de
operación son valores binarios de uno o más bytes por cada mnemónico. De haber un dato, también
será compilado como binario, que es en definitiva la única base de numeración que pueden
interpretar los procesadores. Sin embargo, cuando un programador escribe un programa mediante
un editor, puede indicarle al compilador si un número es binario, decimal o hexa.
Al correr el programa, el procesador va ejecutando las instrucciones almacenadas en memoria e
incrementando el registro IP secuencialmente. Tanto CS (Code Segment) como IP (Instruction
Pointer) son los registros del procesador que direccionan el código ejecutable . La dirección de la
próxima instrucción a ejecutarse está dada por el vector CS:IP
Cuando se encuentra una instrucción de bifurcación, si se verifica la condición expresada por el
tipo de instrucción, el puntero de instrucciones (IP) cambia con un salto en lugar de incrementarse.
En el ejemplo de pseudo-programa anterior, en la instrucción de comparación el IP tomará el valor
de la dirección "label 2" toda vez que la comparación haya resultado verdadera o incrementará su
valor a la instrucción siguiente (instrucción 3) si la comparación resulta falsa. Hay que tener
presente que cada una de las instrucciones anteriores está almacenada en uno o varios bytes en la
memoria. El valor de label2 es en el caso anterior la dirección en donde esta almacenado el primer
byte de la instrucción 2.
Abramos las ventanas
Inicie usted una sesión DOS, teclee la orden DEBUG y cuando le aparezca el anodino prompt "-",
pulse "r" y enter. Los datos que usted ve desplegarse son los registros básicos y el contenido de los
mismos, del procesador de su PC:
(Notar que Debug supone que la notación es hexadecimal y que los registros son de 16 bits aunque
su PC sea Pentium)
AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC 1332:0100 C3 RET .
Esto significa que su Pentium con corazón de 8086 tiene al menos:
o cuatro registros generales: AX, BX, CX y DX
o cuatro registros índices: SP, BP, SI y DI
o cuatro registros de segmento: DS, ES, SS y CS
o un registro que apunta a la próxima instrucción a ejecutar: IP
o un registro de banderas de uso general: F (banderas V, D, I, P, Z, A, S y C)
Todos los registros mencionados son de 16 bits y usted se preguntará ¿no es que a partir del 80386
los registros son de 32 bits?. Y está en lo cierto, los nuevos registros se llaman EAX, EBX, etc...
pero todo a su tiempo. Recuerde que estamos viendo por el momento sólo lo más básico y esto nos
remite al modelo del 8086. En este procesador, a su vez los registros AX, BX, CX y DX pueden
dividirse en dos registros de 8 bits (por ejemplo el AX en AH (bits 8 a 15) y AL (bits 0 a 7).
Cada registro tiene sus funciones específicas (aunque hay muchas que son compartidas):
AX: Acumulador, principalmente usado para operaciones aritméticas
BX: Base. Se usa para indicar un desplazamiento (offset) sobre una posición de memoria
CX: Contador. Se usa para lazos y operaciones repetitivas
DX: Dato. De uso general
CS: Segmento de código. Indica el segmento donde residen las instrucciones
SS: Segmento de Stack. Indica el segmento que utiliza el Stack
DS y ES: Segmentos Data y Extra, segmentos donde residen los datos
SP: Puntero de Stack. Indica el offset actual del Stack
BP: Puntero de base, para operaciones de indexación
SI: Indice de origen. Offset en segmento de datos de origen
DI: Indice de destino: Offset en segmento de datos de destino
F: Flags (hay nueve banderas importantes entre las 16)
Las flags (banderas) a tener en cuenta son:
C: carry - indica si la operación anterior generó un carry
Z: zero - indica si en la operación anterior se generó una igualdad
S: sign - indica si en la operación anterior el resultado fue negativo
AC: auxiliar carry - indica si hay que hacer un ajuste decimal en AX
P: parity - indica si la paridad del último resultado fue par
V: overflow (también simbolizada O)- indica desbordamiento aritmético en AX
D: direction - indica si los indices SI o DI se incrementan (D=0) o decrementan (D=1)
I: interrupt enable - indica si se permiten las interrupciones (I=1) o no (I=0)
T: trap - controla la operación paso a paso del procesador
Asi como la dirección de la próxima instrucción a ejecutarse está apuntada por la pareja CS:IP, hay
un lugar de memoria especial apuntado por SS:SP llamado STACK y utilizado para guardar datos
transitorios, parámetros que se pasan a las funciones y direcciones de retorno de subrutinas o
interrupciones. Se llama stack porque opera como una pila de objetos, en donde el último en
ponerse es el primero en sacarse, mediante instrucciones especialmente diseñadas para eso que se
llaman PUSH y POP. Hay métodos para consultar o escribir otros valores que no son los apuntados
pos SS:SP (lo que equivale a sacar un objeto de la pila sin que se desmorone). El puntero SP está
dando el offset de la última posición de memoria escrita en la zona de stack y como se va llenando
desde posiciones altas hacia las más bajas, la próxima posición libre es la SS:SP-1.
Por ejemplo, supongamos que SP contiene el valor 0FFE4h (más adelante se verá qué papel juegan
los registros de segmento como el SS, en la determinación de la dirección de memoria real) y que
AX contiene el valor 2233h. La instrucción PUSH AX pondrá el valor 22 h (contenido en AH) en
la posición 0FFE3 h (0FFE4 - 1) y el valor 33 h almacenado en AL en la posición de memoria
0FFE2 h y deja SP apuntando al último byte ocupado, vale decir, que SP contendrá el valor
0FFE2h.. La instrucción POP AX realiza la operación inversa.
No es mi intención tratar de suplir un buen manual Intel (que puede bajarse gratis de internet del
sitio www.intel.com) en el que se describe qué es cada registro y cuáles son las instucciones en las
que está involucrado. Un buen sitio en castellano para consultar las instrucciones
es http://udgftp.cencar.udg.mx/tutoriales/TutorialEnsamblador/ensam.html de la Universidad de
Guadalajara, México, en donde además hay un tutorial de Assembly elemental con la fallida
denominación de Assembler.
Las operaciones del procesador se van ejecutando de manera secuencial tal como están
almacenadas las instrucciones en la memoria. Existen instrucciones (saltos) que permiten cambiar
la secuencia de ejecución en forma absoluta o condicionada al resultado de alguna operación
anterior, tal como se dijo de la instrucción de comparación en el ejemplo de pseudo-código antes
visto La instrucción más elemental es MOV, que permite copiar un dato de un origen a un destino.
OPERANDOS
Las instrucciones pueden tener ninguno, uno, dos o tres operandos. A su vez, los operandos pueden
ser inmediatos, registros, memoria o puertos. Un operando inmediato es un dato que viene en el
código del programa, por ejemplo, para cargar el registro AX con el número 20C5h se usa la
instrucción:
MOV AX,20C5h ;20C5h es un operando "inmediato" ;Otras instrucciones MOV pueden ser:
MOV BX,[0400h] ;0400h es una posicion de memoria
MOV DX,[BX] ;[BX] también es una posición de memoria
En el listado anterior, todo lo que hay después del punto y coma ";" son comentarios
extremadamente necesarios en programación Assembly. No es el compilador quien los debe
interpretar sino el propio programador o quien en el futuro deba modificar el programa. El número
entre corchetes indica que el 0400h debe interpretarse como una DIRECCION de memoria (los
corchetes deben leerse como "el contenido de", o sea : en esa operación cargamos el registro BX
con el contenido de la posición de memoria 0400hexa del actual segmento de datos) Este tipo de
referencia a memoria se llama Directo. En cambio, si expresamos [BX], nos estamos refiriendo a la
posición de memoria cuyo offset en el segmento actual de datos es el número contenido en el
registro BX; este tipo de referencia a las posiciones de memoria se denomina Indirecto. Por
ejemplo, supongamos para el código anterior que en la dirección [0400] hay una word cuyo valor
es 1234 h, y en la dirección de memoria [1234] hay una word cuyo valor es 56CCh, luego de
ejecutarse esas instrucciones el registro BX contiene el valor 1234h y el registro DX contiene el
valor 56CCh. En cambio el registro AX es cargado con el número 20C5h y a este direccionamiento
se lo llama Inmediato.
Los operandos pueden ser de 8, 16 o 32 bits, según se desprenda del contexto de la operación o del
otro operando (por ejemplo, en el anterior MOV BX,[0400h], dado que BX es de 16 bits, lo que se
va a mover es un word. Se deben incluír prefijos para especificar la longitud del dato cuando se de
lugar a ambigüedad como por ejemplo en la instrucción INC, en donde si el destino es una posición
de memoria, hay que especificar si es byte o word de la siguiente manera:
INC BYTE PTR [0406] ;incrementar el byte de offset 406h del segmento de datos
Modos de Direccionamiento
El modo de direccionamiento indica la forma en que el procesador calcula da dirección donde irá a
buscar el dato origen o grabará el resultado en el destino, tal como se dejó entrever en el punto
anterior. Existen ocho modos de direccionamiento en los procesadores X86
Implicito: la misma operación lo indica (p.ej. PUSHA, siempre indica como destino el
Stack)
Registro: la instrucción menciona el registro (p. ej MOV AL,CH)
Inmediato: la instrucción proporciona el dato (p. ej. MOV DL,5Fh)
Directo: la instrucción da la dirección de memoria (p. ej. MOV BX,[0400h])
Registro-Indirecto: la dirección es el contenido de un registro (p.ej. MOV AX,[BX])
Relativo a base: dirección = base + constante (p.ej. MOV CX,[BX+6])
Directo Indexado: dirección = directo + índice (p.ej. MOV DH,[0400h+SI])
Indexado a base: dirección = directo + base + índice (p.ej. MOV AL,[0400h+BX+SI])
Cada registro de uso general o índice tiene su propio registro de segmento asociado, según la tabla
siguiente:
AX, BX, CX, SI, DI DS
BP, SP SS
DI (instrucciones de strings) ES
IP CS
En instrucciones de strings se opera entre un operador fuente (DS:SI) y otro destino (ES:DI).
Aunque en estas instrucciones se lo vincula al segmento contenido en ES, en toda otra instrucción,
el registro DI está asociado con el registro de segmento DS. A pesar de esto, puede cambiarse esta
asociación default con prefijos de segmento. Por ejemplo, si queremos que el AX se cargue con el
contenido de la dirección de memoria 3C8, pero del segmento apuntado por ES, tenemos que usar:
MOV AX,ES:[3C8] ;cargar AX con el contenido de la dirección ES*10h+3C8h
A continuación veremos como calcular una dirección segmentada del tipo SEG:OFF, en donde
SEG es uno de los cuatro registros de segmento (DS, ES, SS o CS) y OFF es un registro de uso
general o puntero.
Modelo de Memoria de una PC
La capacidad de direccionamiento de un procesador está dada por la cantidad de líneas del bus de
direcciones (o sea el ancho en bits, de la palabra que el procesador es capaz de poner en el bus de
direcciones de la computadora). En un procesador típico de PC, tenemos 32 bits o sea 4 gigabytes
(2 elevado a la potencia 32) de posiciones de memoria distinguibles. Esto constituye el espacio de
direccionamiento real, pero no significa que nuestra PC tiene instalada esa cantidad de RAM, sino
que en caso de estar físicamente instalada, el procesador es capaz de direccionarla. Todo segmento
de programa que se está ejecutando debe residir en memoria real (no sólo el segmento de código
sino también el de datos).
Como Windows es un sistema multitarea, si alguna aplicación pasa a segundo plano, es posible que
en caso de escasez de memoria real, el sistema operativo decida guardar en memoria virtual parte o
toda la memoria real que la aplicación ocupa, y la almacena en el archivo de intercambio (por lo
general este archivo tiene varias decenas de megabytes y es de tipo oculto). Cuando la aplicación
vuelve a primer plano, el procesador al ver que no están en memoria real las recupera del archivo
de intercambio. Incluso si la aplicación es tan grande que excede la memoria real instalada, habrá
partes de ella en memoria física y otras partes en memoria virtual.
El modelo de memoria utilizado en Win32 se basa en dos tablas de vectores, GDT y LDT
apuntadas por registros específicos del procesador. Se llama "modelo de memoria plana" en
oposición con el más antiguo llamado "segmentado" (propio del DOS y Win16). La memoria en
lugar de dividirse en segmentos estancos, se divide en páginas contiguas. El procesador tiene la
posibilidad de detectar si una página no está presente en memoria real y a partir de ahí hay una
serie de procedimientos para recuperarla desde la memoria virtual. Los mecanismos de gestión de
memoria están integrados en el kernel de Windows32 y su explicación cae fuera de los alcances
previstos para este escrito.
Segmentación
Si observamos con atención la pantalla del DEBUG, notaremos los cuatro registros de segmento
denominados DS, ES, SS y CS. Tal como se ha dicho cada registro de segmento tiene la misión
específica de direccionar segmentos de datos, stack y código. Como cuando Intel dio a luz este
esquema de direccionamiento los registros de los procesadores eran de 16 bits y un MB de memoria
era una cantidad fabulosa reservada sólo para los computadores de laboratorio, se decidió que la
forma en que se direccionaría la memoria sería combinando dos segmentos como sigue:
DIRECCION EFECTIVA = 10h * SEGMENTO + OFFSET
tanto "segmento" como "offset" son registros que contienen un vector de 16 bits, y por lo tanto
pueden elegir entre 64 k direcciones distintas. En pocas palabras, elegido el segmento, el
procesador podía direccionar dentro del segmento 64 k posiciones de memoria distintas. Ejemplo
CS= 3701h IP= 0100h
10h*CS = 37010 h
+ IP = 0100 h
D.Eff. = 37110 h
La notación usada para expresar una dirección efectiva (o dirección absoluta) es SEG:OFFS; por
ejemplo la próxima instrucción a ejecutar está en la dirección CS:IP. De lo anterior obtenemos las
siguientes conclusiones:
Existen 64 k segmentos posibles (los registros de segmento son de 16 bits)
Con esta notación se pueden expresar direcciones entre 00000 y 10FFEFh, en decimal
1.114.095 (no hasta FFFFFh o 1.048.575 = 1 MB como parecería lógico)
La alineación es cada 10h bits (la dirección efectiva de comienzo de segmento termina en
0h). 10h bytes se llaman parágrafos. Es común decir que las direcciones efectivas de
comienzo de segmento se alinean en parágrafos (lo cual es obvio, desde que en el comienzo
de segmento el offset es 0)
Una misma dirección efectiva puede expresarse de muchas maneras usando combinaciones
entre segmento y offset (37110 h = 3701:0100 = 3600:0111, etc)
Tanto segmento como offset son dos cantidades sin signo (no puede haber un offset
negativo)
MODULO 3
Cuando termine de leer esta página deberá conocer:
Instrucciones básicas del X86
Instrucciones básicas 8086
Este listado no pretende ser un substituto del manual Intel de instrucciones del 8086 -del que
fervientemente recomiendo una minuciosa lectura una vez que haya comprendido bien esto- sino la
más breve descripción posible para poder avanzar un poco más en los aspectos más básicos que se
precisan para comprender el tutorial de lenguaje Assembly de +gthorne. Esta es una lista completa
de instrucciones 8086 a las que sólo le faltan las instrucciones ESC, LOCK y WAIT, que no son
útiles a nuestros fines inmediatos.
En la siguiente tabla se muestran encolumnados los Mnemónicos (como MOV), los operandos
(como fuente, destino) y la descripción de la operación. Los operandos son combinaciones entre
tipos (registro, memoria e inmediato) con los direccionamientos admitidos en cada instrucción. Las
instrucciones IN y OUT admiten un cuarto tipo de operando: puertos de I/O, con direccionamiento
registro o inmediato.
Instrucciones de movimientos de datos
MOV destino,fuente ;la única instrucción que utiliza todos los tipos de direccionamiento
XCHG destino,fuente ;Intercambia los contenidos de destino y fuente
XLAT tabla_fuente ;carga el registro AL con el byte direccionado por (BX+AL)
LAHF ;carga las flags S, Z, A, P y C en AH
SAHF ;guarda AH en el registro de flags
LDS destino,fuente ;transfiere un puntero de 32 bits al registro DS y al registro destino
LES destino,fuente ;transfiere un puntero de 32 bits al registro ES y al registro destino
LEA destino,fuente ;transfiere el offset de fuente (una dirección) a destino (un registro)
PUSH fuente ;guarda fuente en el stack (en la dirección SS:SP)
POP destino ;recupera del stack (dirección SS:SP-1) y guarda en registro destino
PUSHF ;almacena el registro de flags en/desde el stack
POPF ;recupera el registro de flags en/desde el stack
PUSHA ; almacena los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack
POPA ;recupera los reg DI,SI,BP,SP,BX,DX,CX,AX en/desde el stack
IN origen ;carga desde un puerto origen un byte o word en AL o AX
OUT destino ;escribe Al o AX en el puerto destino (direccionam. inmediato o DX)
Las operaciones aritméticas
ADD destino,fuente ;suma fuente + destino y guarda el resultado en destino
ADC destino,fuente ;suma fuente + destino + Carry y guarda el resultado en destino
SUB destino,fuente ;resta destino - fuente y guarda el resultado en destino
SUB destino,fuente ;resta destino - fuente - Carry y guarda el resultado en destino
MUL fuente ;multiplica AL o AX * fuente y guarda el resultado en DX:AX
IMUL fuente ;igual que la anterior pero con numeros enteros con signo
DIV fuente ;divide DX:AX / fuente y guarda cociente en AX y resto en DX
IDIV fuente ;igual que la anterior pero con numeros enteros con signo
AND destino,fuente ;opera destino AND fuente y guarda resultado en destino
OR destino,fuente ;opera destino OR fuente y guarda el resultado en destino
XOR destino,fuente ;opera destino XOR fuente y guarda el resultado en destino
NOT destino ;el NOT cambia todos los 1 en 0 y los 0 en 1 de destino.
NEG destino ;NEG realiza el complemento a 2 de destino
INC destino ;Incremente an 1 el contenido de destino
DEC destino ;Decrementa en 1 el contenido de destino
DAA / DAS ;Efectúa el ajuste decimal en suma / resta del registro AL
AAA/AAD/
AAM/AAS ;ajustan el registro AL a valor decimal desempaquetado (para aplicar en operaciones suma,
resta, multiplicación y división)
Instrucciones de rotación
RCL destino,contador ;rota destino a traves de carry a la izquierda contador veces
RCR destino,contador ;rota destino a traves de carry a la derecha contador veces
ROL destino,contador ;rota destino a la izquierda contador veces
ROR destino,contador ;rota destino a la derecha contador veces
SAL destino,contador ;desplaza destino a la izquierda contador veces y rellena con ceros
SAR destino,contador ;desplaza destino a la derecha contador veces y rellena con bit SF
SHR destino,contador ;desplaza destino a la derecha contador veces y rellena con ceros
NOTAS SOBRE INSTRUCCIONES DE ROTACIÓN
El bit SF (signo) es el que está más a la izquierda : si destino es operando es de 8 bits
"SF" es el bit número 7 y si destino es un operando de 16 bits, es el bit número 15 En el procesador 8086 se permite un dato inmediato en lugar de especificar un registro
como contador solo si ese dato inmediato es 1. Por lo tanto, para uno de esos
procesadores la instrucción RCL AX,1 es válida mientras que la RCL AX,5 no lo es.
A partir de 80286 se puede especificar cualquier numero de rotaciones como dato
inmediato. Como DEBUG presupone 8086, cualquier valor inmediato distinto de 1 da
error. Si en un programa para 8086 se desean rotar más de un bit a la vez, el valor contador se
carga en CL Para rotar un nibble (lo que es muy común en la conversión de binario a BCD) es más
rápido y ocupa menos memoria si se utilizan 4 rotaciones de contador igual a 1 que si
se utiliza el registro CL Las instrucciones SAL y SHL son equivalentes La flag de Overflow cambia con una logica precisa si la rotación es de una posición. En
caso de rotaciones mayores, OVF queda indefinida. En los procesadores 80286 en adelante la rotación se hace MODULO 32, es decir que se
rotará la cantidad de veces igual al resto de la división contador/32, o sea que ROL
AX,33 equivale a ROL AX,1 o ROL AX,65. Una rotación con CL=0 equivale a un NOP de dos bytes
Instrucciones de comparación
CMP destino,fuente ;compara fuente y destino. Modifica las flags V, Z, S, C, P y AC
TEST destino,fuente ;AND entre fuente y destino . Ninguno de los operandos cambia.
TEST modifica las mismas flags que CMP pero siempre deja a V = 0 y C = 0.
Instrucciones de strings
CMPS string_destino,string_fuente ;compara las dos cadenas de a bytes o words
CMPSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (bytes)
CMPSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (words)
LODS string_fuente ;mueve un byte o una word desde fuente a AL o AX
LODSB string_fuente ;origen indicado por DS:SI (mueve un byte a AL)
LODSW string_fuente ;origen indicado por DS:SI (mueve una word a AX)
STOS string_destino ;mueve un byte o una word al destino desde AL o AX
STOSB string_destino ;destino indicado por ES:DI (mueve AL a un byte)
STOSW string_destino ;destino indicado por ES:DI (mueve AX a una word)
MOVS string_destino,string_fuente ;mueve un byte o word de fuente a destino
MOVSB string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (un byte)
MOVSW string_destino,string_fuente ;origen y destino indicados por DS:SI y ES:DI (una word)
SCAS string_destino ;compara la cadena de destino con AL o AX
SCASB string_destino ;destino indicado por ES:DI (compara AL con un byte)
SCASW string_destino ;destino indicado por ES:DI (compara AX con una word)
En todos los casos, si se utiliza el prefijo REP, la cantidad de elementos de la cadena a operar está dada por el contenido del registro
CX, si no es un solo elemento de la cadena. A cada operación, CX es decrementado y SI y DI son incrementados o decrementados de
acuerdo con el estado de la flag de dirección (Si D=0, se incrementan). El incremento o decremento de estos registros se hace de a uno
si son operaciones de bytes o de a dos si son de a words. Para los casos en que se especifica el largo del operando con la B o W final,
la string_destino está apuntada por ES:DI, la string_fuente está apuntada por DS:SI .
Instrucciones de repetición
LOOP offset ;decrementa CX. Si CX no es cero, salta a offset (IP = IP + offset)
LOOPZ offset ;decrementa CX, Si CX <> 0 y Z = 1 , salta a offset (IP = IP + offset)
LOOPNZ offset ;decrementa CX, Si CX <> 0 y Z = 0 , salta a offset (IP = IP + offset)
En todos los casos, si no se produce el salto, se ejecuta la próxima instrucción
REP instrucción ;decrementa CX y repite la siguiente instrucción MOVS o STOS hasta que CX=0
REPZ instrucción ;igual que REP, pero para CMPS y SCAS. Repite si la flag Z queda en 1 (igualdad)
REPNZ instrucción ;igual queREPZ, pero repite si la flag Z queda en 0 (las cadenas son distintas)
Instrucciones de salto
CALL destino ;llama a procedimiento. IP <-- offset de destino y CS <-- segmento de destino
RET valor ;retorna desde un procedimiento (el inverso de CALL), valor es opcional
INT número ;llamado a interrupción. CS:IP <-- vector de INT.Las flags se guardan en el stack
INTO ;llama a la INT 4 si la flag de overflow (V) está en 1 cuando se ejecuta la instrucción
IRET ;retorna de interrupción al programa restaurando flags
JMP dirección ;Salta incondicionalmente al lugar indicado por dirección
JA offset ;salta a IP + offset si las flags C=0 Y Z=0 (salta si primer operando es mayor)
JAE offset ;salta a IP + offset si la flag C=0 (salta si primer operando es mayor o igual)
JB offset ;salta a IP + offset si las flags C=1 (salta si primer operando es menor)(igual a JC)
JBE offset ;salta a IP + offset si las flags C=1 o Z=1 (salta si primer operando es menor o igual)
JZ offset ;salta a IP + offset si las flags Z=1 (salta si primer operando es igual al segundo)(=JE)
JG offset ;salta a IP + offset si las flags S=V Y Z=0 (salta si primer operando es mayor)
JGE offset ;salta a IP + offset si las flags S=V (salta si primer operando es mayor o igual)
JL offset ;salta a IP + offset si las flags S<>V (salta si primer operando es menor)
JLE offset ;salta a IP + offset si las flags S<>V o Z=1(salta si primer operando es menor o igual)
JNC offset ;salta a IP + offset si la flag C=0 (salta si no hay carry)
JNZ offset ;salta a IP + offset si la flag Z=0 (salta si no son iguales o no es cero)
JNO offset ;salta a IP + offset si la flag V=0 (salta si no hay overflow)
JNP offset ;salta a IP + offset si la flag P=0 (salta si no hay paridad -o la paridad es impar =JPO)
JNS offset ;salta a IP + offset si la flag S=0 (salta si no hay hay bit de signo)
JO offset ;salta a IP + offset si la flag V=1 (salta si hay desbordamiento -overflow)
JP offset ;salta a IP + offset si la flag P=1 (salta si la paridad es par ) (=JPE)
JS offset ;salta a IP + offset si la flag S=1 (salta si el signo es negativo)
JCXZ offset ;salta a IP + offset si la flag CX=0 (salta si el registro CX es cero)
Las instrucciones de saltos por Above o Below se refieren entre dos valores sin signo (JA, JAE, JB y JBE), mientras que las Greater y
Less se refieren a la relación entre dos valores con signo (JG, JGE, JL y JLE). .
Instrucciones que afectan flags
CLC/CMC/STC ;pone a cero / complementa / pone en 1 la flag C (carry)
CLD/STD ;pone a cero / uno la flag de dirección (D=0 hace que SI y DI se incrementen)
CLI/STI ;deshabilita / habilita las interrupciones por hardware enmascarables
Instrucciones misceláneas
NOP ;no-operación: el procesador pasa a la instrucción siguiente sin hacer nada
CBW ;convierte el byte de AL en palabra (AX), copiando el bit 7 a todo el registro AH
CWD ;convierte word en double-word, copiando bit 15 de AX a todo el registro DX
HLT ;el procesador se detiene hasta que llegue un Reset o una interrupción por hard.
Alguien puede preguntarse para qué puede servir una instrucción que no hace absolutamente nada
como la NOP. Simplemente para llenar espacio, y es realmente una de las instrucciones más útiles
para un cracker, a punto tal que algunos mecanismos anticracking sofisticados buscan durante la
ejecución de un programa si alguien lo arregló sustituyendo algunas instrucciones por NOPs, y en
caso de detectarlo, abortan la ejecución. Pero esto es tema para más adelante.
MODULO 4
Cuando termine de leer esta página deberá conocer:
Uso del DEBUG
Posiblemente sea el debug el depurador más rudimentario que existe; pero el hecho que desde el
principio haya sido provisto con el sistema operativo, nos permite encontrarlo hoy en cualquier
máquina DOS o Windows. Muchas tareas elementales pueden realizarse sin otra ayuda que el
Debug y por eso vamos a ver algunos comandos básicos. Incluso es posible correr programas
cargados en memoria utilizando breakpoints elementales, ejecutar paso a paso, saltar sobre
procedimientos, editar programas en hexa y muchas más cosas. Ya hemos dicho cómo podemos
arrancarlo desde una ventana DOS, y usando el comando R (mostrar registros) nos mostrará algo
similar a esto:
AX=0000 BX=0000 CX=0000 DX=0000 SP=0000 BP=0000 SI=0000 DI=0000 DS=1332 ES=1332 SS=1332 CS=1332 IP=0100 NV UP EI PL NZ NA PO NC 1332:0100 C3 RET .
Esto muestra el contenido de los registros del procesador incluyendo varias banderas: en el
ejemplo, y en el mismo orden tenemos: V=0, D=0, I=1, S=0, Z=0, AC=0, P=0 y C=0
Si ponemos después de la R el nombre de un registro, es posible modificar su contenido. Por
ejemplo, para editar el contenido de CX, hay que poner el comando RCX. Debug nos presenta el
contenido actual del registro y la posibilidad de ingresar un nuevo valor para sustituirlo.
Los comandos L y W se utilizan para leer y escribir en archivos de disco. La cantidad de bytes
transferida en cada operación es el contenido de BX:CX. Previamente es necesario darle un nombre
al archivo con el comando N. Se puede especificar la dirección a partir de la que se desea transferir
datos o bien usar el vector por defecto DS:DX.
Los comandos más útiles y más usados en Debug son:
A dirección Ensamblar (ingresar código assembly)
D dirección cantidad Mostrar en pantalla direcciones de memoria en presentación hexa
E dirección Editar memoria desde dirección
F direc1 direc2 valor Llenar memoria desde direc1 hasta direc2 con el dato valor
G dirección Ir (durante la ejecución) a la dirección dirección
H valor1 valor2 Muestra el resultado de la suma y resta hexadecimal entre valor1 valor2
I puerto Obtiene una entrada desde el puerto puerto
M direc1 direc2 direc3 Mueve el bloque de memoria direc1- direc2 a partir de direc3
P cant Salta sobre procedimientos cant de veces o hasta dirección direc
Q Sale de Debug
S direc1 direc2 valores Busca en bloque de memoria desde direc1 hasta direc2 los bytes valores
T cant Igual que P pero son instrucciones simples
U direc cant Desensambla cant bytes a partir de la dirección direc
XS Muestra estado de memoria expandida
? Presenta pantalla de ayuda
Nuestro primer programa
Usaremos el Debug para ensamblar un programa que realice algo tan útil (?) como dejar en alguna
parte de la memoria el nombre de nuestra escuela ECCE. Para sacar algo a pantalla, debemos leer
el tutorial de +gthorne, que será nuestro paso siguiente. Por ahora sólo queremos practicar de
manera que abramos una ventana DOS y escribamos DEBUG (enter). Nos proponemos hacer que
ECCE sea escrito en memoria, en el offset 200h de nuestro segmento de datos DS. Sabemos que los
códigos ASCII son E=45h y C=43h, de manera que nuestro programa puede lucir así:
a 100
1322:0100 mov ax,4543 ;cargamos el registro AX con el dato 4543 (EC en ASCII)
1322:0103 mov bx,4345 ;cargamos BX con "CE" en ASCII
1322:0106 mov [200],ax ;ponemos AX en la dirección de memoria 200
1322:0109 mov [202],bx ;idem para BX, pero en la 202 (AX ocupó la 200 y 201)
1322:010D int 20 ;finalizar y salir a Debug
1322:010F
Al apretar "enter" una vez más, Debug nos devuelve su prompt "-" y ya estamos listos para nuestro
próximo comando. Podemos ver algunas curiosidades del listado anterior: 1) Debug asume que los
números que le damos, sean direcciones o datos, son hexadecimales. 2) A medida que vamos
ingresando el programa, nos va devolviendo la dirección de almacenamiento de la próxima
instrucción que escribiremos. 3) Las tres primeras instrucciones MOV ocuparon de memoria de
programa 3 bytes cada una, pero la cuarta ocupó 4 bytes y la INT 20 sólo ocupó 2 bytes. 4) Aunque
nada se ha hablado de la INT 20, es lo que por el momento usaremos para terminar el programa . 5)
Cuando hacemos referencia al contenido de una posición de memoria, encerramos la dirección
entre corchetes []. Es muy importante saber distinguir entre la dirección y el valor almacenado en
esa dirección de memoria.
Nuestra lógica es muy simple: cargamos el ASCII "EC" en AX y lo dejamos en la dirección 200.
Luego cargamos "CE" en BX y lo dejamos en la 202. Tanto AX como BX han sido meros
vehículos para cargar la memoria con datos y sólo a los efectos didácticos porque también está
permitido :
MOV word ptr [200],4543 ; cargar la word de memoria 200 directamente con el dato 4543
Esta instrucción ocupa 6 bytes, de modo que no ganamos espacio poniéndola en lugar del más
elíptico procedimiento de cargar AX y con éste escribir en 200. El prefijo "word ptr" es para que el
procesador sepa que lo que moveremos a 200 es una word y no un byte o double-word.
Veamos cómo se ve nuestro programa usando el comando desensamblar:
-u 100 (desensamble a partir de la CS:100)
(Nótese que Debug listará usando sólo mayúsculas, sin importar cómo escribimos nuestro código)
1322:0100 B84345 MOV AX,4543
1322:0103 BB4543 MOV BX,4345
1322:0106 A30002 MOV [200],AX
1322:0109 891E0202 MOV [202],BX
1322:010D CD20 INT 20
NOTA: el valor de 1322 (el contenido del registro CS) es válido para la PC donde se escribió este
ensayo. Por lo general los valores no coinciden de una a otra PC, salvo que las instalaciones de
software sean idénticas y en ambas estén corriendo previamente al DEBUG los mismos programas.
El listado es más largo, pero las líneas que siguen hacia abajo son alguna cosa que estaba en
memoria, ya que Debug desensambla por defecto los 20h primeros bytes desde la dirección
indicada (o desde la que esté apuntando), y en nuestro programa sólo hemos usado 0Fh bytes (15 en
decimal). Echémosle un vistazo:
Ajá!!, Debug no deja de sorprendernos, en una columna entre la dirección y el listado en lenguaje
assembly puso unos números hexadecimales. Son los códigos de operación (opcodes) que es lo que
en definitiva se almacena en memoria y lo que nuestro Pentium debe interpretar y ejecutar. Debug
compiló nuestro programa ingresado en assembly y produjo ese código binario con representación
hexadecimal para que el Pentium lo interprete.
Antes de correr el fabuloso programa que hemos escrito, tenemos que ver qué hay en la posición de
memoria 200. Para ello usamos el comando D 200, que nos muestra la basura que hay en nuestra
RAM desde DS:0200 hasta DS:027F. Como deseamos leer claramente nuestro nombre ECCE,
vamos a llenar este espacio con ceros usando el comando
- F 200 23F 00
con lo que le indicamos a Debug que debe llenar el bloque de memoria que comienza en 200 y
termina en 23F con "00". Para estar seguros, escribamos nuevamente el comando D 200. Debemos
ver las cuatro primeras filas del listado con los datos en 00. Estamos listos para correr nuestra
maravilla.
Con el comando R nos aseguramos que CS:IP esté apuntando al inicio de nuestro programa (o sea a
CS:0100). Para nuestro caso CS vale 1322, pero como ya se ha dicho, puede que en otra PC tenga
otro valor. Corramos el programa con el comando G. Debug nos debe informar:
El programa ha finalizado con normalidad.
Bien! todo fue de maravillas. Veamos si nuestras siglas brillan en las posiciones 200 a 203 con el
comando D 200
Esperábamos los hexa 45,43,43,45 a partir de la 200 (miremos además en la columna ASCII del
Debug, en donde claramente nos dice CEEC) y están al revés. Qué habrá pasado? Será que hemos
escrito BX en 200 y AX en 202?. Usemos al Debug para depurar , que para eso Bill Gates lo ha
puesto donde está. Repitamos el comando F 200 23F 00 para dejar nuevamente en cero la memoria
y ejecutemos nuestro programa paso a paso.
Primero el comando R. Nos debe decir que IP apunta a 0100:
1322:0100 B84345 MOV AX,4543
-T (comando para ejecutar una sola instrucción). Lo relevante es:
AX=4543 e IP=0103
1322:0103 BB4543 MOV BX,4345 es la próxima instrucción. Ejecutemos con T:
1322:0106 A30002 MOV [200],AX
Ejecutemos el comando D 200 para ver qué hay en la memoria: hasta ahora 00 de la dirección 200
a la 203. Todo ok, porque hasta aquí sólo hemos cargado los registros AX y BX. Hagamos otro T.
1322:0109 891E0202 MOV [0202],BX es la próxima instrucción
Hemos guardado AX en la dirección 200 y por lo tanto debería haber un 4543 ("EC" en ASCII) en
las direcciones 200 y 201. Verifiquemos con el comando D 200:
1322:0200 43 45 00 00 ........ CE................
QUE PASO???? Está al revés. Tengo "CE" en lugar de "EC". Mmmmm!! Mr Intel tiene algo que
ver con esto: Resulta que lo que leemos en AX como "EC", en la realidad lo debemos asumir como
: En AL tengo un 43 ("C") y en AH un 45 ("E"). Y el procesador hace algo sumamente lógico, a la
porción más baja del registro (AL) la almacena en la dirección de memoria más baja (200) y a la
porción más alta del registro (AH) la almacena en la dirección de memoria más alta (201). Todo
parece bien pero no funciona?
Pero está bien tal como lo hizo Intel. Si leemos la memoria en sentido de direcciones ascendentes,
debemos acostumbrarnos a leer los registro (y a cargarlos, ahí fue donde nos equivocamos!) desde
la porción más baja hacia la más alta. Por lo tanto, debemos rescribir nuestro programa para que en
AL se almacene la primera letra ("E") y en AH la segunda ("C"), y lo mismo para BX:
a 100
1322:0100 MOV AX,4345
1322:0103 MOV BX,4543
(enter) nuevamente para salir del comando A.
Ahora debemos modificar el registro IP, que nos quedó apuntando a la mitad del programa:
RIP (enter) nuestro comando
IP 0109 respuesta de Debug
:100 (enter) este valor lo ingresamos nosotros para decirle que queremos a IP=0100
Ejecutamos el programa nuevamente con G y examinamos la memoria con D 200 para ver nuestro
hermosa sigla ECCE ya en su lugar y en el orden debido.
Acepte este buen consejo: No siga adelante si algo no quedó claro. Reléalo, busque otra fuente,
alguien que le pueda explicar más claro que yo, pero no lea +gthorne sin haber entendido aunque
sea la mecánica con que operan los procesadores. Con el tiempo podrá memorizar los mnemónicos
de las instrucciones, con muy poca práctica puede dominar Debug y sus comando heredados de una
era sombría de las PCs.
INTERRUPCIONES – Conceptos Basicos
1. Una historia vieja como la PC
Hace muchos años, en un país muy lejano, un gigante azul se sintió solo en sus alturas y dijo: "No es bueno que
el programador solo trabaje en su oficina. Hagamos una computadora personal para que también pueda llevarse
el trabajo a su casa". Y así lo hizo. Esa decisión nos puso, amigo deseoso de convertirse en cracker que estas
leyendo esto, en contacto unos 20 años después.
IBM tomó una decisión respecto a la arquitectura de sus computadoras personales destinada a marcar un cambio
notable en la historia de la tecnología. Adoptó una arquitectura abierta, esto es, utilizó componentes que estaban
en el mercado en lugar de fabricar chips propietarios. Al tomar esta resolución, Intel pasó a ser la opción más
clara como proveedor de procesadores y periféricos: por aquél entonces acababa de salir al mercado la línea de
16 bits 8086 y existían muchos periféricos de 8 bits de su predecesor, el 8085, tales como el controlador de
interrupciones 8259, el PPI 8255, DMA 8237, la UART 8251, el timer 8253.
En los procesadores Intel de la línea X86, hay dos tipos de interrupciones: por hardware y por software. En las
primeras, una señal llega a uno de los terminales de un controlador de interrupciones 8259 y éste se lo comunica
al procesador mediante una señal LOW en su pin INT. El procesador interroga al 8259 cuál es la fuente de la
interrupción (hay 8 posibles en un 8259) y este le muestra en el bus de datos un vector que la identifica. Por
instrucciones de programa, se puede instruir al 8086 para que ignore la señal en el pin INT, por lo que estas
interrupciones se denominan "enmascarables". Hay un pin adicional llamado NMI, que se comporta como una
interrupción, pero imposible de bloquear (Non-Maskable-Interrupt).
2. Tipos de interrupciones
Las interrupciones por software se comportan de igual manera que las de hardware pero en lugar de ser
ejecutadas como consecuencia de una señal física, lo hacen con una instrucción.
Hay en total 256 interrupciones, de la 0 a la 7 (excepto la 5) son generadas directamente por el procesador. Las
8 a 0Fh son interrupciones por hardware primitivas de las PC. Desde la AT en adelante, se incorporó un
segundo controlador de interrupciones que funciona en cascada con el primero a través de la interrupción 2 (de
ahí que en la tabla siguiente se la denomine múltiplex). Las 8 interrupciones por hardware adicionales de las AT
se ubican a partir del vector 70h.
Decimal Hexa Generada Descripción
0 0 CPU División por cero
1 1 CPU Single-step
2 2 CPU NMI
3 3 CPU Breakpoint
4 4 CPU Desbordamiento Aritmético
5 5 BIOS Imprimir Pantalla
6 6 CPU Código de operación inválido
7 7 CPU Coprocesador no disponible
8 8 HARD Temporizador del sistema (18,2 ticks por seg)
9 9 HARD Teclado
10 0A HARD Múltiplex
11 0B HARD IRQ3 (normalmente COM2)
12 0C HARD IRQ4 (normalmente COM1)
13 0D HARD IRQ5
14 0E HARD IRQ6
15 0F HARD IRQ7 (normalmente LPT1)
112 70 HARD IRQ8 (reloj de tiempo real)
113 71 HARD IRQ9
114 72 HARD IRQ10
115 73 HARD IRQ11
116 74 HARD IRQ12
117 75 HARD IRQ13 (normalmente coprocesador matemático)
118 76 HARD IRQ14 (normalmente Disco Duro)
119 77 HARD IRQ15
En cuanto a las interrupciones por software, están divididas entre las llamadas por el BIOS (desde la 10h a la
1Fh) y las llamadas por el DOS (desde la 20h hasta la 3Fh). Esto es sólo la versión oficial, ya que en realidad
las interrupciones entre BIOS y DOS se extienden hasta la 7Fh.
3. Cómo funciona una interrupción
A partir del offset 0 del segmento 0 hay una tabla de 256 vectores de interrupción, cada uno de 4 bytes de largo
(lo que significa que la tabla tiene una longitud de 1KB). Cada vector está compuesto por dos partes: offset
(almacenado en la dirección más baja) y segmento (almacenado en la dirección más alta). Cuando se llama a
una interrupción (no importa si es por hardware o por software), el procesador ejecuta las siguientes
operaciones:
1. PUSHF (guarda las banderas en el stack) 2. CTF/DI (borra la bandera de Trap y deshabilita interrupciones) 3. CALL FAR [4 * INT#] (salta a nueva CS:IP, almacenando dirección de retorno en stack)
La expresión 4 * INT# es la forma de calcular la dirección de inicio del vector de interrupción a utilizar en el
salto. Por ejemplo, el vector de la INT21h estará en la dirección 84h Al efectuarse el salto, la palabra
almacenada en la dirección más baja del vector sustituye al contenido del registro IP (que previamente fue
salvado en el stack) y la palabra almacenada en la dirección más alta sustituye al contenido del registro CS
(también salvado en el stack). Por ejemplo:
La instrucción INT 21h es la usada para efectuar llamadas a las funciones del DOS. Supongamos que en la
posición de memoria 0000:0084 está almacenada la palabra 1A40h y en la dirección 0000:0086 está
almacenada la palabra 208Ch. La próxima instrucción que se ejecute es la que está en la posición 20C8:1A40
(nuevo CS:IP).
El final de una rutina de interrupción debe terminarse con la instrucción IRET, que recupera del stack los
valores de CS, IP y Flags.
Notemos que un llamado a interrupción implica el cambio de estado automático de la bandera de
habilitación de interrupciones. En pocas palabras, esto significa que al producirse una interrupción,
esta bandera inhabilita futuras interrupciones. Como la instrucción IRET restablece el registro de
flags al estado anterior que tenia antes de producirse la interrupción, las próximas interrupciones se
habilitan en el mismo momento en que se produce el retorno desde la rutina de servicio.
4. Paso de parámetros desde el programa a la ISR
Cuando las interrupciones son llamadas por software mediante la instrucción INT xx, por lo general
se le deben pasar parámetros a la rutina de servicio de interrupción (ISR). Estos parámetros definen
la tarea que debe cumplir la ISR y son pasados en los registros del procesador, lo que es una opción
muy veloz.
Un ejemplo casi extremo, en donde muchos de los registros del 8086 son utilizados son algunos
servicios cumplidos por la INT 13h (disco). Para tomar sólo un caso, en una operación de escritura
de un sector, los parámetros se pasan de la siguiente manera:
Registro Asignación
AH 03 (servicio de escritura de sectores)
AL cantidad de sectores a escribir
CH 8 bits más bajos del número de cilindro
CL(bits 0-5) número de sector
CL(bits 6 y 7) 2 bits más altos del número de cilindro
DH número de cabeza
DL número de unidad de disco (hard: mayor a 80h)
BX offset del buffer de datos
ES segmento del buffer de datos
Si bien no está escrito en ningún lado, las interrupciones utilizan el registro AH para identificar el
tipo de operación que deben ejecutar. Cuando una interrupción devuelve códigos de error siempre
vienen en el registro AL, AX y/o en la bandera de Carry.
5. La interrupción más famosa
Sin lugar a dudas se trata de la INT 21h (funciones del DOS). El número de función se pasa en el
registro AH
Función Descripción
00h Terminar un programa
01h Entrada de caracteres con salida
02h Salida de un caracter
03h Recepción de un caracter por el puerto serial
04h Envío de un caracter por el puerto serial
05h Salida por puerto paralelo
06h Entrada/salida de caracteres directa
07h Entrada/salida de caracteres directa
08h Entrada de caracteres sin salida
09h Salida de un string de caracteres
0Ah Entrada de un string de caracteres
0Bh Leer estado de una entrada
0Ch Borra buffer de entrada y llama a entrada de caracteres
0Dh Reset de los drivers de bloques
0Eh Selección de unidad actual
0Fh Abrir archivo usando FCBs (File Control Blocks)
10h Cerrar archivo (FCBs)
11h Busca primera entrada de directorio (FCBs)
12h Busca siguiente entrada de directorio (FCBs)
13h Borrar archivo(s) (FCBs)
14h Lectura secuencial (FCBs)
15h Escritura secuencial (FCBs)
16h Crear o vaciar un archivo (FCBs)
17h Renombrar archivos (FCBs)
18h Obsoleta
19h Obtener denominación de dispositivo, unidad actual
1Ah Fijar dirección para DTA (Disk Transfer Area)
1Bh Obtener información sobre unidad actual
1Ch Obtener información sobre una unidad cualquiera
1Dh/1Eh Obsoletos
1Fh Fijar puntero a DPB (Drive Parameter Block) a la unidad actual
20h Obsoleta
21h Lectura random (FCB)
22h Escritura random (FCB)
23h Leer tamaño de archivo (FCB)
24h Establecer número de registro (FCB)
25h Establecer vector de interrupción
26h Crear nuevo PSP (Program Segment Prefix)
27h Lectura random de varios registros (FCB)
28h Escritura random de varios registros (FCB)
29h Transferir nombre de archivo al FCB
2Ah Obtener fecha
2Bh Establecer fecha
2Ch Obtener hora
2Dh Establecer hora
2Eh Fijar bandera de Verify
2Fh Obtener DTA
30h Obtener número de versión del DOS
31h Terminar programa pero dejarlo residente en memoria
32h Obtener puntero a DPB de una unidad específica
33h Leer/escribir bandera de break
34h Obtener dirección de bandera INDOS
35h Leer vector de interrupción
36h Obtener espacio de disco disponible
37h Obtener/fijar signo p/separador de línea de comandos
38h Obtener/fijar formatos específicos de un país
39h Crear subdirectorio
3Ah Borrar subdirectorio
3Bh Fijar directorio actual
3Ch Crear o vaciar archivo (handle)
3Dh Abrir archivo (handle)
3Eh Cerrar archivo (handle)
3Fh Leer desde archivo (handle)
40h Escribir en archivo (handle)
41h Borrar archivo (handle)
42h Mover puntero de archivo (handle)
43h Obtener/fijar atributo de archivo
44h Funciones IOCTL (control de I/O)
45h Duplicar handle
46h Duplicación forzosa de handles
47h Obtener directorio actual
48h Reservar memoria RAM
49h Liberar memoria RAM
4Ah Modificar tamaño de memoria reservada
4Bh EXEC: ejecutar o cargar programas
4Ch Terminar programa con valor de salida
4Dh Obtener valor de salida de un programa
4Eh Buscar primera entrada en el directorio (handle)
4Fh Buscar siguiente entrada en el directorio (handle)
50h Establecer PSP activo
51h Obtener PSP activo
52h Obtener puntero al DOS-info-block
53h Traducir Bios Parameter Block a Drive Parameter Block
54h Leer bandera Verify
55h Crear PSP nuevo
56h Renombrar o mover archivo
57h Obtener/fijar fecha y hora de modificación de archivo
58h Leer/fijar estrategia de alocación de memoria
59h Obtener informaciones de error ampliadas
5Ah Crear archivo temporal (handles)
5Bh Crear archivo nuevo (handles)
5Ch Proteger parte de un archivo contra accesos
5Dh Funciones de Share.exe
5Eh Obtener nombres de dispositivos de red
5Fh Obtener/fijar/borrar entrada de la lista de red
60h Ampliar nombre de archivo
61h No usada
62h Obtener dirección del PSP
63h/64h No usadas
65h Obtener información ampliada de pais específico
66h Obtener/fijar página de códigos actual
67h Determinar el número de handles disponibles
68h Vaciar buffer de archivos
69/6A/6B No usadas
6Ch Función Open ampliada
6. Intercepción de interrupciones (hooks)
Un programa puede necesitar "enganchar" una interrupción. Supongamos que hemos creado un
virus que debe autodestruir su copia en memoria cuando el comando a ejecutar es "scan.exe".
Evidentemente debemos interceptar la interrupción 21h, función 4Bh/00 (cargar un programa y
ejecutarlo), de tal manera que "nuestra" función verifique si el programa a cargar se llama scan.exe
y en tal caso, borre lo que haya que borrar.
Esta tarea, se logra en haciendo un programa residente (que puede ser parte del mismo código del
virus) para que
1. Cuando se produzca una llamada a la INT21h-4Bh, no se ejecute el código normal del DOS
sino nuestro código
2. En él chequearemos si la función es una 4Bh-00, y en caso afirmativo verificamos si el
programa a corres se llama scan.exe. Si todo esto es verdadero, sobrescribiremos las partes
sensibles a la detección del virus y lo descargaremos de la memoria.
3. Finalmente saltamos a la verdadera INT21h función 4Bh
Para lograr esto, es necesario contar con un loader que cargue en memoria nuestro programa. Este
loader debe:
1. Reservar un espacio de memoria adecuado al tamaño del código que quedará residente.
2. Averiguar (mediante INT21h-35h) cual es el vector de interrupción de la INT21h.
Supongamos que sea 0102:2C40h
3. Poner este vector como dirección de retorno del código residente (por lo general cargándolo
en una dirección conocida en donde tiene que estar este valor)
4. Cambiar el vector 4Bh origina por la dirección de inicio de nuestro código residente
(digamos 7E00:0000)
Lo que sucederá cuando la PC infectada con nuestro virus intente ejecutar un scan.exe es lo
siguiente:
1. Dentro del Command.com, se generará un llamado a la INT21-4Bh-00 con scan.exe como
parámetro.
2. El procesador buscará el vector para el servicio a la interrupción 21h en la dirección
0000:0084h
3. En ese lugar estará la dirección de inicio de nuestro residente, o sea 7E00:0000, y en ese
lugar se inicia el procesamiento de la interrupción.
4. Al ver que la llamada es para ejecutar un programa scan.exe nuestro residente vuelve a poner
el vector de INT21h en el valor que le dio el DOS y luego se autodestruye (primero traslada
a la parte más baja de la memoria la función de borrado). Como último acto, hace un salto
JMP FAR 0102:2C40
5. Esto último hará que se ejecute scan.exe como si nada hubiese sucedido.
Frecuentemente los virus utilizan interrupciones en desuso para sus fines (por ejemplo para saber si
están activos en memoria).
El tema de las interrupciones es tan inmenso que lo que acabamos de ver no es sino un pequeño
pantallazo. Quedan cuestiones muy delicadas como la bandera INDOS y las formas de evitar la
reentrada. Una descripción muy completa de cada interrupción, que incluye los registros usados
para el paso de parámetros, está en el archivo intdos.zip, por Ralph Brown (en inglés) que pueden
bajarse de sudden dischargeo asmcoder , dos sitios que les recomiendo si se buscan tutoriales o
archivos.
MANEJO DE STRINGS EN BIOS, DOS, y WINDOWS
1.- Función BIOS para manejo de strings
El BIOS interactúa principalmente de a un caracter por vez con el teclado, pantalla y puerto serial,
por lo que a estos se los conoce como dispositivos de caracteres, en contraposición con el drive de
diskettes o el disco duro, que son dispositivos de bloques. Aunque menos frecuente que las
funciones de manejo de strings del DOS, la función 13h de la INT10h tiene la ventaja que no
depende del sistema operativo. Su función es visualizar en la pantalla una cadena de caracteres que
deben estar almacenados en un buffer (en memoria).
El paso de parámetros se realiza mediante los siguientes registros:
Registro Parámetro
AH 13 h - define la operación
AL
Modo de salida:
0 Atributo en BL, mantiene la posición del cursor
1 Atributo en BL, actualiza la posición del cursor
2 Atributo en buffer, mantiene posición del cursor
3 Atributo en buffer, actualiza posición del cursor
BL Atributo de caracteres (solo modos 0 y 1)
CX Cantidad de caracteres a visualizar
DH Línea de la pantalla
DL Columna de la pantalla
BH Página de pantalla
ES:BX Puntero al buffer de memoria
Los modos que actualizan la posición del cursor se usan cuando se quieren escribir varios strings
uno a continuación del otro. En cambio los modos que la mantienen, se utilizan para escribir
mensajes siempre en el mismo lugar de la pantalla.
En los modos 0 y 1 todos los caracteres tienen el atributo especificado en BL, mientras que en los
modos 2 y 3 en el buffer, seguido a cada caracter esta su byte de atributo, lo que permite que cada
caracter tenga un atributo distinto. La cadena en memoria tiene una longitud igual al doble de los
caracteres a visualizar. El valor de CX debe ser no obstante igual a la cantidad de caracteres (la
mitad del tamaño del buffer). El byte de atributos tiene la siguiente estructura:
Bit # Función
7 Intermitencia (1=intermitente, 0=fijo)
6,5,4 Color de fondo (0=negro, 7=blanco)
3,2,1,0 Color del caracter (0=negro, 0Fh=blanco)
2. Funciones DOS de manejo de strings
2.1 Entrada de strings de caracteres: INT21h - función 0Ah
Se leen caracteres desde la entrada standard (normalmente teclado) y se transfieren a un búffer en
memoria. La operación termina cuando se lee el caracter ASCII 0Dh (CR o retorno de carro), que
corresponde a la tecla RETURN (o ENTER).
Registro Parámetro
AH 0Ah - código de la función
DS:DX Puntero al buffer de memoria
Estructura del Buffer:
Posición Significado
DS:DX Cantidad máxima de caracteres admitida en el buffer (debe ser
inicializada por el programador)
DS:DX + 1 Cantidad de caracteres leída (la escribe el DOS)
DS:DX + 2 y
subsig.
Buffer donde se almacenan los caracteres leídos. La dirección del último
es DS:DX + byte ptr (DS:DX)
En los dos primeros bytes, la cantidad de caracteres incluye al CR final. Suponiendo que el
programador inicialice la posición de memoria DS:DX en 10h, el buffer tendrá un largo total de 16
caracteres, comenzando en DS:DX y finalizando en DS:DX + 0Fh, y podrá aceptar 13 caracteres
más el de retorno.
DOS no se preocupa por borrar la parte del buffer que no escribe. Veamos en la tabla siguiente,
para un buffer de 16 de largo qué caracteres encontramos luego de dos entradas sucesivas, la
segunda más corta que la primera:
Dirección DS:DX + ... 0 1 2 3 4 5 6 7 8 9 A B C D E F
primera entrada 10 0f B u e n o s A i r e s 0d ?
segunda entrada 10 0a C o r d o b a 0d i r e s 0d ?
Hay que notar que si bien esta función es muy cómoda, se queda esperando el caracter de retorno y
hasta que este no llegue el programa no puede hacer otra cosa que... esperar!. En cambio, si se
busca de a un caracter por vez, es posible hacer que el programa consulte el teclado como una de
las tantas actividades posibles dentro de un mismo lazo.
2.2 Salida de string de caracteres, INT 21h - función 9h
Con esta función se envía un string de caracteres al dispositivo designado como salida standard
(normalmente la pantalla). DOS permite redireccionar la salida a un archivo o a un puerto serial o
LPT desde la misma línea de comandos, por lo que al usar esta función no hay garantías de que el
string aparezca en pantalla. En realidad, esto también es válido para la entrada de caracteres vista
en el punto anterior, aunque es mucho más frecuente redireccionar la salida que la entrada. Por
ejemplo, el comando interno type archivo hará que el contenido del archivo sea visualizado en la
pantalla, pero si agregamos un redirector con un dispositivo de salida "> LPT1", los caracteres del
archivo serán direccionados al puerto de la impresora.
El string debe finalizar obligatoriamente con el caracter "$" (código ASCII 36). Los caracteres
especiales como Bell, Backspace, CR, etc serán tratados como tales. Bell (ASCII 07) hace sonar
una campana en el altavoz de la PC, CR vuelve al principio de la línea, Nueva_línea (ASCII 0Ah)
pasa a la línea de abajo, etc
Al igual que en la lectura de strings, los parámetros son:
Registro Parámetro
AH 09h - código de la función
DS:DX Puntero al buffer de memoria donde reside el string
En lenguaje Assembly, un string para usarse con esta función puede ser declarado como sigue:
mensaje1 DB "Todos los hombres de buena voluntad",0Dh,0Ah,"$"
y para utilizar la función 9h, el código a emplear sería:
display: MOV DX, offset mensaje1
MOV AH,9
INT 21H
RET
3. Funciones Windows de manejo de strings
3.1 CompareStrings
Esta función compara dos strings de caracteres usando como base el juego de caracteres del idioma
especificado por el identificador. La sintaxis del llamado es:
int CompareString(
LCID Locale, identificador de lenguaje del sistema
DWORD dwCmpFlags, opciones de comparación
LPCTSTR lpString1, puntero al primer string
int cchCount1, tamaño (bytes) del primer string
LPCTSTR lpString2, puntero al segundo string
int cchCount2 tamaño (bytes) del segundo string
);
3.2 GetDlgItemText
La función GetDlgItemText captura el titulo o texto asociando con un control en una caja de
diálogo. La sintaxis es:
UINT GetDlgItemText(
HWND hDlg, handle de la caja de diálogo
int nIDDlgItem, identificador del control
LPTSTR lpString, dirección del buffer para el texto
int nMaxCount máxima longitud del string
);
3.3 GetWindowText
La función GetWindowText copia el texto de una barra de título de una ventana especificada en un
buffer. Si la ventana especificada es un control, lo que se copia es el texto del control. Sintaxis:
int GetWindowText(
HWND hWnd, handle de la ventana o control
LPTSTR lpString, dirección del buffer de texto
int nMaxCount máximo número de caracteres a copiar
);
3.4 GetWindowTextLength
La función GetWindowTextLength obtiene la cantidad de caracteres que tiene el texto de la barra
de título de una ventana o (si la ventana especificada es un control), la cantidad de caracteres dentro
del control. La sintaxis es:
int GetWindowTextLength(
HWND hWnd handle de la ventana o control
);
3.5 lstrcat
La función lstrcat adiciona un strin a continuación de otro. Sintaxis:
LPTSTR lstrcat(
LPTSTR lpString1, dirección del buffer de strings concatenados
LPCTSTR lpString2 dirección del string a concatenar con string1
);
3.6 lstrcmp y lstrcmpi
La función lstrcmp compara dos strings de caracteres. La comparación discrimina entre mayúsculas
y minúsculas. La función lstrcmpi es idéntica pero no discrimina mayúsculas y minúsculas.-
Sintaxis:
int lstrcmp( // int lstrcmpi(
LPCTSTR lpString1, dirección del primer string
LPCTSTR lpString2 dirección del segundo string
);
3.7 lstrcpy
La función lstrcpy copia un string en un buffer. Sintaxis:
LPTSTR lstrcpy(
LPTSTR lpString1, dirección del buffer
LPCTSTR lpString2 dirección del string a copiar
);
3.8 lstrcpyn
La función lstrcpyn copia un número especificado de caracteres de un string dentro de un buffer.
LPTSTR lstrcpyn(
LPTSTR lpString1, dirección del buffer
LPCTSTR lpString2, dirección del string a copiar
int iMaxLength cantidad de caracteres o bytes a copiar
);
3.9 lstrlen
La función lstrlen devuelve la longitud en bytes (versión ANSI) o caracteres (versión Unicode) del
string especificado (no incluye el caracter NULL de terminación).
int lstrlen(
LPCTSTR lpString dirección del string
);
3.10 MultiByteToWideChar
La función MultiByteToWideChar despliega un string de caracteres en un string Unicode.
int MultiByteToWideChar(
UINT CodePage, código de página
DWORD dwFlags, opciones tipo de caracteres
LPCSTR lpMultiByteStr, dirección del string a mapear
int cchMultiByte, número de caracteres en el string
LPWSTR lpWideCharStr, dirección del buffer Unicode
int cchWideChar tamaño del buffer
);
3.11 SetDlgItemText
La función SetDlgItemText determina el texto de un control en un box de diálogo. Sintaxis:
BOOL SetDlgItemText(
HWND hDlg, handle del box de diálogo
int nIDDlgItem, identificador del control
LPCTSTR lpString puntero al texto
);
3.12 SetWindowText
La función SetWindowText cambia el texto en la barra de título de una ventana. Si la ventana es un
control, se cambia el texto del control. Sintaxis:
BOOL SetWindowText(
HWND hWnd, handle de la ventana o del control
LPCTSTR lpString dirección del string
);
PASO DE PARAMETROS EN LOS PROGRAMAS
Parte I: COMO PASAN LOS PARAMETROS A LAS INTERRUPCIONES BIOS Y DOS
PASO DE PARAMETROS
Es posible que una de las partes más tardíamente comprendidas por el principiante de ingeniería
inversa es la manera en que pasan los parámetros desde el programa a una función. Este concepto
es de fundamental importancia en el estudio de las protecciones y podemos decir sin lugar a dudas
que la comprensión de este mecanismo es crucial para el análisis del funcionamiento de un
programa DOS o Windows.
LA ANTIGUA HISTORIA DEL DOS
El viejo DOS en lugar de funciones API utilizaba interrupciones de software (INT 21h y
subsiguientes), y un poco más próximo al hardware, el mismo BIOS cuenta con su propio juego de
interrupciones. Estas interrupciones de software funcionan igual que cualquier llamada a función,
aunque el mecanismo de llamada es distinto, ya que se usa la instrucción INT en lugar de CALL.
Por lo general, tanto el DOS como el BIOS pasaban los argumentos en los registros del mismo
procesador. Si bien es una estrategia que optimiza la velocidad de procesamiento, tiene sus
limitaciones en cuanto a la cantidad de parámetros que se pueden pasar. Otro de los problemas que
tiene es que las funciones no pueden ser reentrantes a menos que se tomen previsiones
excepcionales, aunque esto no era de mucha importancia ya que el DOS no es multitarea, sería sólo
problema para programas residentes.
Por lo general se pasaban los parámetros por valor. Por ejemplo, en una interrupción de BIOS de
lectura de un sector de disco a memoria (INT 13, subfunción 02) tenemos:
reg var significado
AH 2 subfunción 2: lectura de un sector
AL n cantidad de sectores a leer
CH c0 8 bits más bajos del número de cilindro (track) a leer
CL s numero de sector a leer (bits 0 a 5)
CL c1 2 bits más altos del número de cilindro (bits 6 y 7)
DH h número de cabeza lectora
DL d número de disco lógico (bit 7 en 1 para discos duros)
ES:BX ba dirección de inicio del buffer de lectura en memoria
A menos que se trate de aplicaciones muy especiales en que estos valores pueden ser fijos, lo usual
es que cada uno de esos parámetros sea una variable que a su vez está almacenada en algún lugar
de la memoria. En el siguiente listado que sigue estos parámetros son referidos con nombres
simbólicos supuestos y el lector debe tener presente que en el listado de lenguaje de máquina lo que
se verán son las direcciones de almacenamiento de estos parámetros. Veremos cómo sería una
llamada a la interrupción que lea 4 sectores consecutivos del disco C, ubicados en la pista 801
(0321h), cabeza 3, a partir del sector 12 (0Ch), y que almacene lo leído en la dirección DS:0700.
El registro CX en binario debe ser: 0010 0001 11 001100 = 21CC h
Los bits 15 a 8 deben ser 21h (ocho bits menos significativos del número de track), los bits 7 y 6
ambos en uno (el 3 del número de track) y los bits 3 y 2 también en uno por en número de sector.
En algún lugar del programa se produce la carga de los valores iniciales:
PUSH DS ;haremos que los datos se escriban
POP AX ;en el segmento de datos DS
MOV segme,AX ;almacenamos en la variable segme
MOV AX,0700 ;en el offset 0700h
MOV offse,AX ;almacenamos en la variable offse
MOV AX,0380 ;disco C (80h), cabeza 3
Y luego se cargan los registros desde la memoria antes de llamar a la int 13h
MOV curdisk,AL ;almacena 80 en variable curdisk
MOV curhead,AH ;almacena 03 en variable curhead
MOV AX,21CC ;número de track y sector
MOV track0,AH
MOV sekt,AL
MOV AL,4 ;número de sectores a leer
CALL _leedisk ;leer
JC _error ;si CY vuelve en 1, hubo error de lectura
... ...
_leedisk: ;lectura de disco
... ...
MOV DH,AL ;salvar cantidad de sectores a leer
MOV AX,segme ;cargar segmento
MOV ES,AX
MOV BX,offse ;cargar offset de buffer
MOV CL,sekt s;ector y 2 bits más altos de track
MOV CH,track ;cargar track
MOV DL,curdisk ;unidad de disco a utilizar
MOV AL,curhead ;numero de cabeza
XCHG DH,AL cambiar número de sectores y cabeza
MOV AH,2 ;subfunción de lectura
INT 13 ;interrupción 13h BIOS disco
RET
Uno puede preguntarse cuál es el objeto de poner los parámetros en memoria en lugar de cargarlos
directamente en los registros apropiados para la llamada a la INT 13h. Es una cuestión de
practicidad y buen estilo de programación. Si las variables están en memoria, el programa puede
consultarlas en cualquier momento o modificarlas por ejemplo para hacer un lazo. Si se cargan
como constantes, tal como sucede en la primera parte de la rutina, en donde se inicializan las
variables, servirán solamente para efectuar esa llamada. Por ejemplo, si después de esa primera
lectura quisieramos leer los sectores 1 a 7 del mismo track, sólo habría que poner:
MOV AL,sekt ;nuevo sector inicial
AND AL,C0h ;dejamos solo los dos bits del track (6 y 7)
OR AL,1 ;ponemos en 1 el numero de sector
MOV sekt,AL ;guardamos nuevamente
MOV AL,7 ;numero de sectores a leer
CALL _leedisk
NOTA IMPORTANTE
Un lector de nivel intermedio podría objetar que es posible tratar parte del código como si fuesen variables y
de tal modo ahorrarnos un paso, dejando sólo la carga inmediata de registros. El programa se vería así
(incluímos ahora una columna para las direcciones del código por razones obvias)
CS:1000 MOV DL,80 ;código del disco duro, unidad C
CS:1002 MOV AL,4 ;leer cuatro sectores
CS:1006 etc etc
Si por ejemplo quisiesemos leer 2 sectores y cambiar la unidad C por la A, habría que poner:
XOR AL,AL ;poner a cero AL (unidad A)
MOV [CS:1001],AL ;cambia la carga de DL
MOV AL,2 ;numero de sectores
MOV [CS:1003],AL ;cambia carga de AL
CALL _leedisk
En algunas oportunidades se hace, es una técnica conocida como automodificación, pero no lo recomiendo
para principiantes. Por cierto que en lugar de poner la dirección absoluta como se hizo ahora en beneficio de la
claridad, es posible utilizar variables del compilador (que se traducen en constantes iguales a CS:1001 y
CS:1003 para el programa)
El lector puede encontrar en Internet la completa y muy extensa lista de llamadas a interrupción de
Ralph Brown (por ejemplo en el sitio sudden discharge ), unas 250 páginas tamaño oficio en letra
condensada a dos columnas en donde se incluyen hasta interrupciones propias de virus. Mi mejor
consejo si tiene que trabajar con programas DOS es que la consiga y la imprima (y a menos que su
vista sea excelente, no la imprima en condensada aunque le lleve el dobe de papel). Hay una
versión mucho más condensada y menos exhaustiva que viene para instalar residente, atribuida a
Peter Norton y que puede ser suficiente si los programas acceden a las interrupciones más comunes
(por ejemplo, no están ni las que se utilizan para redes ni las de los DOS-extenders). Trate de
bajarla de nuestro sitio usando este vínculo.
EN BUSCA DE ALTERNATIVAS
Un poco agotado en las complejidades crecientes, el modo de paso de parámetros mediante
registros iba a quedar acotado a rutinas del núcleo de sistemas operativos en donde la velocidad es
un factor de gran importancia. Había que buscar alternativas para mejorar la manera en que los
parámetros son pasados a las funciones. Consideraremos ahora tres temas íntimamente relacionados
con el paso de parámetros.
* Paso de valor versus uso de punteros
* Cómo opera el stack
* Estructuras de datos
1) Paso de argumentos por punteros.
En el punto anterior se vio con profundidad el paso de argumentos por valor, es decir, se le entrega
a la función convocada el VALOR con el que tendrá que operar. Dentro de las llamadas a
interrupcion más comunes, esto es algo inevitable porque los valores pasan en los mismos registros
del procesador. Sin embargo cuando se estructuró el ejemplo sobre la lectura de sectores de disco,
se hizo un pequeño avance: se colocaban los valores en direcciones de memoria y luego la rutina
los recuperaba antes de convocar a la interrupción 13h.
El paso de argumentos mediante punteros consiste en una técnica similar, en donde a la función
convocada se le dice en qué dirección están los valores con los que tiene que operar. Esta es la
manera en que trabajan los compiladores C y Pascal por ejemplo. Supongamos que queremos
sumar 7 y 11. En pseudo lenguaje C no sería correcto poner:
A = Suma (7,11)
que sería más propio de Basic, sino:
int A,B=7,C=11;
A= Suma(B,C);
printf A;
Se declaran tres enteros, definiendose el valor de dos de ellos, se llama a una función Suma(x,y)
que debe estar definida en otra parte del programa, que usa dos argumentos de entrada y devuelve
un entero. Finalmente se imprime el entero resultante. Esto corresponde más o menos con el
siguiente listado en lenguaje Assembly:
varA DW
varB DW 7
varC DW 11
.... ....
LEA AX, varA
PUSH AX
LEA AX, varB
PUSH AX
LEA AX, varC
PUSH AX
CALL _add
CALL _printAx
Lo que en realidad se le está entregando a la función _add son tres valores en el stack que no son
los que tiene que sumar, sino las direcciones en donde estan almacenados los datos de entrada y la
dirección donde debe almacenar el resultado. Consulte en el punto siguiente cómo opera el stack.
El presente ejemplo será resuelto con valores numéricos para que se aprecie bien la diferencia entre
puntero y valor.
Los valores de las direcciones de los operandos se denominan punteros (porque su valor está
"apuntando" al lugar donde está almacenado el dato). Entre otras cosas, esto implica que mientras
se está procesando una función tal como _add(x,y), otra tarea puede estar modificando los valores
contenidos en las direcciones apuntadas por x e y, lo cual no siempre es deseable.
2) Cómo opera el stack
El stack es un espacio particular de la memoria del sistema. Al stack se lo llama pila LIFO (Last In-
First Out, el último en entrar, el primero en salir) y es igual a tener una pila de diskettes: si quiero
sacar alguno, lo más sencillo es quitar primero todos los de arriba. El funcionamiento del stack se
rije por el par de registros SS:ESP (Stack Segment : Extended Stack Pointer), que apunta a la
última dirección ocupada por el stack.
El puntero al stack se decrementa a medida que el stack se va llenando (porque a medida que crece
el stack va ocupando posiciones de memoria cada vez más bajas) e inversamente el puntero crece a
medida que el stack se vacía. La instrucción para cargar al stack con parámetros es PUSH, mientras
que su inversa es POP. Al producirse una interrupción o un llamado a subrutina, se coloca
automáticamente la dirección de retorno (y en ocasiones las flags) en el stack, las que se restauran
con la instrucción POP.
Supongamos que en el momento antes de una operación PUSH AX, que pone el contenido de AX
en el stack, el par SS:SP apunta a 1800:FFEE, y que el contenido de AX es 1234h. Luego del
PUSH, la dirección de memoria 1800:FFED contendrá el valor de AH, o sea 12h, la dirección
inmediata inferior 1800:FFEC contendrá el valor de AL, o sea 34h y el puntero SS:SP tendrá igual
valor (1800:FFEC).
varA DW ;direccion de almacenamiento: DS:2000
varB DW 7 ;direccion de almacenamiento: DS:2002
varC DW 11 ;direccion de almacenamiento: DS:2004
A partir de la DS:2000 encontramos (se lista hasta la DS:2007):
DS:2000 00 00 07 00 11 00 xx xx
Supongamos que SS:SP vale SS:FF2E,
LEA AX, varA ;carga en AX la direccion 2000
PUSH AX ;carga en SS:FF2C el valor 20 00
LEA AX, varB ;carga en AX la direccion 2002
PUSH AX ;carga en SS:FF2A el valor 20 02
LEA AX, varC ;canga en AX la direccion 2004
PUSH AX ;carga en SS:FF28 el valor 20 04
El stack pointer ahora esta en FF28. Listemos desde SS:FF28 hasta FF2F:
SS:FF28 04 20 02 20 00 20 xx xx
Notemos que la carga en el stack sigue la convención Intel, poniendo el byte menos significativo en
la dirección más baja y el más significativo en la dirección más alta.
3) Estructuras de datos.
Con todo, hay veces en las que conviene no hacer referencia a variables aisladas sino manejarlas en
grupo, lo que se denomina estructura. Una estructura de datos se compone de miembros los que
pueden ser de distinta longitud o naturaleza. Cuando el programa se refiera a la estructura lo hará
usando un puntero a la estructura que no es nada más que la dirección de memoria donde comienza.
Veamos un ejemplo simple.
En un programa encontramos que hacemos constante referencia a la lectura de disco, y por lo tanto
decidimos crear nuestra propia estructura para facilitar la escritura del programa. Notemos que los
sistemas operativos tienen definidas estructuras para usos específicos. Utilizando lenguaje
Assembly, una estructura ejemplo se puede definir como:
lectura STRUCT
disco db 0 numero de disco
sector db 1 numero de sector
head db 0 numero de cabeza
reser db 0 reservado
track dw 0000 numero de track
cant dw 0001 cantidad de sectores a leer
bufseg dw 2000 segmento del buffer de lectura
bufoff dw 0000 offset del buffer de lectura
lectura ENDS
En las estructuras de datos propias, podemos usar nuestra inmaginación con total libertad, pero las
estructuras que necesita el sistema operativo debemos ajustarnos completamente a las posiciones y
longitud de los parámetros y disponerlos de la misma manera en que el sistema operativo espera
encontrarlos. Hemos reservado un byte para futuros usos y para que los valores de dos bytes se
alinien con direcciones pares de memoria. Si por ejemplo el valor del puntero "lectura" fuese 2800,
encontraríamos que:
la dirección DS:2800 almacena el número de disco
la dirección DS:2801 almacena el número de sector
la dirección DS:2802 almacena el número de cabeza
la dirección DS:2803 es un byte reservado para uso futuro
la dirección DS:2804 almacena en dos bytes el numero de track
la dirección DS:2806 almacena en dos bytes la cantidad de sectores leer
la dirección DS:2808 almacena el segmento del buffer de lectura
la dirección DS:280A almacena el offset del buffer de lectura.
Si dentro del programa queremos hacer referencia por ejempo a la cabeza lectora, podemos poner:
MOV lectura.head,5 seleccionar la cabeza lectora 5
El compilador buscará la dirección de la estructura "lectura", en nuestro ejemplo DS:2800. Luego
buscará el elemento "head", que por la definición de la estructura sabe que ocupa un byte y que es
el tercero. Por lo tanto el compilador generará una instrucción apropiada para que se almacene el
valor 5 en la dirección de memoria DS:2802.
PASO DE PARAMETROS EN LOS PROGRAMAS
Parte II: COMO PASAN LOS PARAMETROS A LAS FUNCIONES API DE WINDOWS
PARAMETROS PARA FUNCIONES API
Windows sigue las nuevas reglas sobre paso de parámetros tal como se ha visto en el módulo
anterior: pasa punteros en el stack y también hace uso de estructuras cuando esto resulte adecuado.
Para cualquier función API, los parámetros se almacenan en el stack en el orden inverso al que
figuran en la declaración. Igualmente, cualquier valor de retorno vendrá en el registro EAX si se
trata de un entero (si es mayor, será un puntero a una cadena o a una estructura). Tomemos un
ejemplo del API Help de Microsoft o del muy ágil y condensado similar elaborado por Sync+, por
ejemplo la función _lwrite:
definición de función API _lwrite extraída de las API Help
The _lwrite function writes data to the specified file. This function is provided for compatibility with 16-bit
versions of Windows. Win32-based applications should use the WriteFile function.
UINT _lwrite(
HFILE hFile, // handle to file
LPCSTR lpBuffer, // pointer to buffer for data to be written
UINT uBytes // number of bytes to write
);
OK, qué hay que hacer para llamar esta función? Supongamos que tengo abierto previamente un
archivo cuyo handle es 3CCh, en el cual quiero escribir 1000h bytes desde el buffer que está en la
dirección 40023300h
MOV EAX,1000 ;cantidad de bytes a escribir
PUSH EAX
LEA EAX,lpBuffer ;DIRECCION del buffer de escritura
PUSH EAX
MOV EAX,EBX ;normalmente el handle se guarda en EBX
PUSH EAX ;si se acaba de abrir el archivo
CALL _lwrite ;debe estar en memoria el Kernel32.dll
CMP EAX,1000 ver si se transfirierorn todos los bytes
JNZ _error
Nótese que si se ponen los parámetros en el stack en el orden inverso a lo que se declaran, significa
(por ser el stack un elemento LIFO) que serán extraídos por la función en el orden en que están
declarados.
Aqui hay dos detalles que considerar: primero el hecho de que el orden de declaración de
parámetros en Pascal es inverso al de C. Esto no presenta ningún problema, porque el compilador
llama siempre a los parámetros en el mismo orden (porque en realidad pasa a lenguaje de máquina
y en ese nivel no puede haber diferencias en el orden), de manera que de esto se encarga el
compilador y basta con recordar que es inverso al que aparecen en las API Help.
El segundo detalle es más interesante. Mientras Pascal vuelve de la función API con el stack ya
equilibrado, en el lenguaje C es el programador el que tiene que encargarse de esa tarea. Desde el
punto de vista de la ingeniería inversa, si nosotros seguimos una función API y vemos que termina
en RET (4*n) donde n es el número de parámetros, es seguro que el compilador es estilo Pascal,
mientras que si vemos que luego de retornar de la API, el programa acomoda el stack haciendo
POPs o ADD ESP,(4*n), se trata de un compilador C. Pongamos como ejemplo la función vista
_lwrite. Tiene tres parámetros y por lo tanto 4*n=0Ch, por lo tanto, si vemos algo asi:
CALL _lwrite
ADD ESP,0C
se tratará muy probablemente de un ejecutable generado por un compilador C. En cambio, en
Pascal la misma función _lwrite finaliza con un RET 0Ch y por lo tanto no es necesario el ADD
posterior.
NOTA PARA EL PRINCIPIANTE
Es muy, pero MUY importante que el stack pointer quede siempre equilibrado entre el valor que tenía antes de
ingresar los parámetros al stack y luego de ejecutada la función API. Y es fácil deducir por qué: si asi no fuera,
la instrucción RET siguiente a producirse el desequilibrio del stack retornaría a un lugar que en realidad lo más
probable es que sea un parámetro en lugar de código. Esto es igualmente válido si una función es llamada con
una cantidad de parámetros distinta a la que exige su definición.
Junto con las definiciones de función y los parámetros con los que hay que llamarlas hay en la API
Help menciones a flags que controlan la operación de la función. Quizás un función emblemática
en este sentido sea CreateWindow, que tiene una gran cantidad de banderas (por ejemplo,
WS_BORDER, que cuando está activada hace que la función cree una ventana que tiene una linea
fina como borde). Durante la construcción del programa, el compilador se encargará de activar el
bit correspondiente a WS_BORDER, dentro del parámetro dwStyle. Sin embargo, cuando
decompilamos un programa por ejemplo con el W32DASM, nos encontramos con instrucciones
como PUSH 10830041. Esto corresponda posiblemente a parámetros como el dwStyle, que
controlan mediante bits individuales el comportamiento de la función. Supongamos que
determinamos que el anterior push corresponde efectivamente al parámetro dwStyle, por ser el
antepenúltimo en ser cargado en el stack. Cómo saber cuáles son las banderas que el programador
quiso activar?. Hay un sólo camino, que es seguir los pasos que dio el compilador al generar el
ejecutable. En esto nos ayuda el archivo windows.inc, que viene con el compilador (también
disponible en el ensamblador MASM32).
Abrimos ese archivo (676 kB de definiciones!). Comenzamos a buscar WS_BORDER
y encontramos :
WS_BORDER equ 00800000h
esto significa que tiene activado el bit 23, Bingo! el push que estamos considerando lo activa y por
lo tanto vemos que la ventana tendrá borde con linea fina. De la misma manera tenemos que
proceder con todos los bits de todos los parámetros que modifican la función de acuerdo con el
estado de las banderas. Arduo? Si, nadie dijo que esto sea tarea fácil, sólo podemos afirmar que no
es muy complicada, sólo extensiva.
Por lo general no es necesario comprobar el 100% de las flags (lo que nos llevaría a perder un par
de horas en una función como CreateWindow). Tenemos que concentrar nuestra atención en el
problema que queremos resolver, por ejemplo, si la ventana de entrada de claves está inicialmente
maximizada, hay que ver aquellas llamadas a CreateWindow con la flag WS_MAXIMIZE
activada.
NOTA PARA EL PRINCIPIANTE
Es importante reconocer algunas características en las notaciones empleadas para nombrar funciones API. Las
que incluyen una A o W final son funciones de 32 bits con un equivalente de 16 bits que no lleva esa letra. Por
ejemplo
CreateWindow es de 16 bits, mientras que
CreateWindowA es de 32 bits, strings de un byte
CreateWindowW es de 32 bits, string de 2 bytes
Cuando una funcion termina en Ex tiene capacidades extendidas sobre la de igual nombre pero sin el Ex (y
también algún parámetro adicional para controlar esa capacidad). Por ejemplo:
CreateWindow: tiene 11 parámetros, en cambio
CreateWindowEx :12 parámetros: se agrega dwExStyle que controla el estilo extendido
GLOSARIO DEL CRACKING
AND
Operación binaria cuyo resultado es 1 sólo si ambos operadores son 1. También es el mnemónico de una
instrucción de procesador que consiste en realizar la operación binaria bit por bit entre los operandos
declarados en la instrucción. Por ejemplo, AND EAX,EBX instruye al procesador a realizar una operación
binaria AND bit por bit entre los registros EAX y EBX y almacenar el resultado en EAX.
ASSEMBLY Lenguaje de programación que permite el más absoluto control sobre el procesador. Es fundamental un
aceptable manejo de este lenguaje a la hora de hacer ingeniería inversa sobre un target, ya que por lo
general no se dispone del programa fuente y debe utilizarse una dead list.
BACKDOOR Literalmente "puerta trasera", es un mecanismo que se instala en los sistemas a los que un hacker accede,
con el objeto de sistematizar y ocultar futuros accesos.
BANNER El horrible aviso comercial que encabeza toda página de un sitio de hosting gratuito
BIOS
Se deriva de "Basic Input - Output System" (sistema básico de entrada-salida), por referirse de algún modo
a la interface necesaria entre el sistema operativo y el hardware. Aunque los dispositivos y el procedimiento
utilizado para controlarlos puede diferir en cada PC, los sistemas operativos tienen reglas fijas para utilizar
los recursos. Estas reglas son las funciones BIOS y la sintaxis empleada para convocarlas.
Cuando por ejemplo, corremos (es un decir) el Notepad de Windows, hay tres capas de software una dentro
de la otra: La exterior es la aplicación Notepad, la intermedia es el sistema operativo (Windows en este
caso) y la más interna el BIOS. Asi, éste avisa a Windows que el usuario apretó una tecla, Windows le avisa
a Notepad, y éste toma alguna acción al respecto, que puede ser algo tan simple como poner el caracter en
el buffer de edición, y avisar a Windows que tiene que sacar el caracter por pantalla, para lo cual este
avisará al BIOS de qué forma debe representarlo. Esta estructura en capas puede parecer compleja pero es
la única manera de permitir que distintos fabricantes puedan hacer PCs para un mismo sistema operativo o
que una misma PC pueda correr distintos sistemas operativos.
En nuesta página sobre interrupciones hay un breve listado de las interrupciones utilizadas por el BIOS y
por el DOS
BIT
La palabra se deriva de "Binary unIT" (unidad binaria). El ancho de las palabras binarias se especifica en
bits: p.e. decimos que los registros de los procesadores actuales es de 32 bits y que el del (hoy) futuro
Merced es de 64 bits. Esto da una idea de potencia de cálculo, ya que para obtener el mismo resultado para
una operación suma simple como ADD EAX,ECX el procesador 8088 de las primeras PCs tenía que hacer
dos sumas sucesivas porque el ancho de palabra era de 16 bits.
BOOLEANO
Que sigue las reglas del álgebra de Boole o que opera con valores binarios de un bit. Función Booleana:
Aquella cuyo resultado puede ser cierto o falso (1 o 0), por ejemplo comprobar si durante un acceso a disco
se produjo un error o no.
BUFFER
Area de memoria que se utiliza para realizar operaciones en las que un dispositivo deja datos a los que el
programa consulta asincrónicamente. El búffer más fácil de entender es el de teclado: entre el BIOS y el
sistema operativo leen el teclado y dejan en el búffer el código de las teclas apretadas. El programa luego va
a esa área de memoria, normalmente mediante funciones del Sistema Operativo, para leer los códigos y
descargar el búffer (le saca los caracteres que va leyendo). Esto permite, por ejemplo, seguir escribiendo
mientras se produce un acceso a disco sin que se pierdan caracteres, ya que si bien el programa debe esperar
a que termine la operación de disco, la función BIOS de lectura de teclado es llamada por la interrupción de
hard y es atendida con mayor prioridad.
El búffer para accesos a disco es el área de memoria destinada a recibir los datos que se leen o en donde el
programa escribe los datos que se deben transferir al disco.
BYTE Palabra de 8 bits. Con 8 bits se pueden representar 256 (=28) números decimales distintos desde 0 (todos los
bits en cero) hasta 255 (todos los bits en 1)
CARRY Flag de los procesadores que indica un desborde aritmético que debe ser tenido en cuenta cuando se opere
el dígito siguiente. Es el "me llevo uno" que decimos cuando hacemos una suma decimal y una columna nos
da 10 o más: lo que estamos haciendo es un "carry" (llevarse) a la siguiente posición decimal. En un
hipotético procesador de un bit de ancho de palabra, si sumamos 1+1, el resultado es 0 y se enciende la
bandera de carry porque en realidad en binario la suma de 1+1 nos da 10 (que es igual a 2 decimal), el 0 es
el resultado de la posición binaria y el 1 debe sumarse a los operandos de la siguiente posición.
Además, por convención se emplea la flag de carry para indicar el resultado de una función booleana, como
por ejemplo averiguar si existió error en un acceso a disco. Aunque esto es sólo una convención que puede
ser cambiada por el programador, por lo general si el carry vuelve en 0, no hubo error.
CRACKING De "to crack": abrir, hacer crujir. Desactivar una protección de software, sea anticopia o de limitación de
uso. En el ambiente de hacking se usa "crackear" como sinónimo de entrar en una computadora ajena para
reventar el contenido.
DEAD LIST
Literalmente "Listado Muerto", con lo que el lector puede figurarse por qué a pesar de estar orgullosos de
nuestro idioma castellano, en este caso preferimos expresarnos en inglés. Es el listado que obtenemos
procesando un archivo ejecutable con un desensamblador: una serie de instrucciones en lenguaje Assembly
que es una imagen "estática" del ejecutable en un momento en que no está corriendo (de ahí lo de listado
muerto). Es muy útil para la ingeniería inversa de programas y un poco menos útil desde el punto de vista
del cracker. Quizás por esto, los grandes gurús como ORC+ y fravia+ aconsejan este método (que
consideran mucho más sutil) antes que el SoftICE.
DEBUG
Significa "Depurar", aunque traducido literalmente es "desenbichar". Créase o no, la historia cuenta que allá
por los finales de la pasada década del 40 un prototipo de computadora que funcionaba con relés
electromecánicos tuvo un fallo y se descubrió que había sido provocado por una polilla (bug) caída entre los
contactos de un relé.
El depurador más elemental existente viene incluido en el Sistema Operativo y se llama precisamente
Debug.exe (está en la carpeta Command del directorio Windows o, para los más viejos, en el directorio
DOS). Si bien no es gran cosa, nuestra recomendación es aprender el funcionamiento de Debug porque
permite probar en forma inmediata el funcionamiento de partes pequeñas de código y porque permite ver de
cerca la operación del procesador y visualizar sus registros más importantes. En esta página, hay un
aceptable tutorial sobre el uso del Debug.
DEFAULT
Valor que se toma por defecto (es decir, en caso que no se suministre un valor para una determinada
entrada, esta asume el valor por defecto). Por ejemplo, si no indicamos otra cosa, la entrada estándar de un
procesador de textos es el teclado, pero eso cambia si le decimos al procesador que abra un archivo. En
DOS se utilizan los redireccionadores para cambiar la entrada y salida por defecto: El símbolo "<" reasigna
la entrada y el ">" la salida. Por ejemplo, la orden:
TYPE DATA.TXT > LPT1
indicaba al DOS que envíe los caracteres de salida de la orden TYPE no a la pantalla (salida estándar) sino
al puerto de la impresora.
DESENSAMBLADOR Programa que permite a partir del ejecutable de una aplicación obtener un listado en lenguaje Assembly de
esa aplicación.
DONGLE
Dispositivo de hardware que se conecta en un puerto de la PC (normalmente el de la impresora, pero
también los hay para puerto serie y teclado) y que contiene claves o algoritmos para indicarle a un programa
que está autorizado para correr. Se lo llama también hardkey (llave de hard) y pronto será denominado "ese
pedazo de plástico inútil que habría que tirar", ya que como se puede leer en la sección Protección por
hardkey de nuestro sitio, es una de las protecciones con mayor cantidad de puntos débiles. Un cracker
decente no tiene dificultades para vencerla. Con todo, aún hay programas muy importantes y caros
(AutoCAD por ejemplo) que la utilizan.
DWORD Símbolo que identifica un operador de 32 bits (doble-word) Con una DWORD pueden representarse
números desde el 0 hasta 4.294.967.295 (=231)
ENSAMBLADOR
(Assembler) Es el compilador de programas escritos en lenguaje Assembly, vale decir que toma como
entrada un archivo de texto (programa fuente Assembly) y entrega como salida un ejecutable.
Complementando el lenguaje Assembly, existen órdenes llamadas "Directivas de ensamblador" que son
procesadas (aunque no generan código ejecutable). Ejemplos de directivas son SEGMENT (define la
utilización de segmentos que hará el programa) y MACRO, útil a la hora de evitar reescribir partes de
código que se repiten varias veces a lo largo del programa fuente. Revistas baratas confunden los términos
Assembly y Assembler, usándolos en forma indistinta o equívoca.
FETCH Operación del procesador transparente para el programador que consiste en ir a buscar (fetch) la próxima
instrucción a ejecutar. El proceso en los procesadores es algo más complicado debido al cache L1 de
instrucciones, al doble thread y a la predicción de las bifurcaciones.
FLAG
La traducción literal es "bandera", aunque sería más preciso decirle "señalador". Es un operador booleano
que indica alguna situación, que puede ser carry, overflow, error, igualdad, etc. Aunque el largo útil es un
bit, algunos programadores suelen utilizar enteros para almacenar variables booleanas que señalan la
ausencia o presencia de algo, en cuyo caso normalmente sólo cuenta el bit menos significativo de la word o
dword (se utiliza la comparación con cero luego de un TEST o AND). Las flags de los procesadores X86
están descritas en esta página
FUENTE (Programa) Listado original de un programa, tal como fue escrito por el programador. Es un archivo de
texto.
HACKING Actividad consistente en acceder sin autorización a partes vitales de computadoras ajenas.
HANDLE
Literalmente significa "manija" y es muy descriptivo de la función que realiza. Los objetos que maneja un
programa tienen atributos muy variados y sería muy pesado por ejemplo en el caso de una ventana dar
instrucciones del tipo: "muestre un botón con la leyenda OK en la ventana Primera_ventana, que está en la
posición (106,190) cuyas dimensiones son (90,75) con color de fondo 225, título ....etc, etc". En lugar de
eso, al definirse la ventana se crea una estructura que contiene todos esos datos y se le da un handle (que
viene a ser como un nick de la ventana). Ese handle es como un puntero lógico al que el programa debe
hacer referencia cuando quiere operar con el objeto. El concepto de handle se había implementado en el
DOS sólo para el manejo de archivos. Cuando se crea un objeto (por ejemplo al abrir un archivo), la función
de creación devuelve un handle:
INT handle_archivo
handle_archivo = open (nombre_archivo, modo)
(en la realidad el proceso es un poco más complejo porque antes de asignar el valor a la variable
handle_archivo hay que verificar si la función OPEN no tuvo errores)
HOSTING Cuando no se posee un servidor web propio se debe recurrir a algun otro para que hospede (host) nuestras
páginas. Hay servidores pagos y otros gratis que ofrecen hosting a cambio de que cada página tenga un
aviso publicitario cuyo rédito queda para el host.
INSTRUCCION
Sentencia básica en lenguaje Assembly. En el archivo fuente debe ir una sentencia por línea.
Consta de tres partes: el mnemónico que identifica el tipo de operación (AND, ADD, MOVSB, etc), los
operandos y el comentario. Hay instrucciones que no requieren operandos, otras que necesitan uno sólo y
otras que precisan dos (fuente y destino). El comentario es siempre opcional y debe obligatoriamente
comenzar con punto y coma. La sintaxis es:
MNEMONICO destino,fuente ;comentario
Notar que tanto destino como fuente son utilizados como fuente de datos durante la ejecución de la
instrucción, pero destino además es el receptor del resultado. La posición de los operadores es fija,
siempre destino es el primero después del mnemónico.
JMP
Mnemónico de la instrucción de salto incondicional. Hay dos tipos, según el salto se realice dentro del
mismo segmento o a otro segmento. Consulte en esta página en qué consiste la segmentación. Para
identificar esta situación, se antepone NEAR o FAR a la dirección de salto.
NEAR es la opción default. En este caso, la instrucción completa ocupa 3 bytes: uno para el opcode (0E9h)
y dos para el offset, que es un entero de 16 bits, lo que permite saltar 32767 posiciones hacia adelante o
32768 posiciones hacia atrás. El offset se cuenta a partir del contenido del registro IP al momento de
finalizar la ejecución de la instrucción. Un offset 0 es igual a tres NOPs consecutivos, ya que el
procesador ejecutará la instrucción siguiente. Un offset -3 es un lazo infinito, ya que siempre se salta al
inicio del mismo JMP. Los saltos condicionados son similares a los NEAR.
En el caso de JMP FAR, si el opeando es inmediato la instrucción ocupa 5 bytes: uno del opcode (0EAh),
dos de offset y dos finales para el segmento. En este caso, el offset se cuenta no desde el contenido del
registro IP, sino desde el inicio del segmento al que se salta. Si el operando es una posición de memoria, el
opcode ocupa dos bytes (0FF2Eh) y dos bytes más el offset de la posición de memoria donde está
almacenado el vector con la dirección de destino del salto.
KERNEL Literalmente "pepita". Núcleo de un sistema operativo, por extensión de la denominación usada para el
Unix. En el Kernel están las funciones básicas del sistema operativo como el manejo del sistema de
archivos, en contraposición con el Shell que es la parte encargada de interpretar los comandos.
MNEMONICO
Símbolo con que se representa a las instrucciones en lenguaje Assembly. En esta página hay una tabla
conteniendo los mnemónicos y una breve descripción de la operación que realiza cada instrucción del
procesador 8086. Ejemplos de mnemónicos son JMP, NOP, JZ, AND, MOVSB, etc.
NEG
Mnemónico de la instrucción que realiza el complemento a dos del operando. Un complemento a dos
equivale a un cambio de signo de un número entero. El operando puede ser un registro o una posición de
memoria, tanto de 8, 16 o 32 bits.
NOT Mnemónico de la instrucción que realiza el complemento a uno de un operando. Equivale a cambiar todos
los ceros en unos y viceversa.
OFFSET
Literalmente significa "desplazamiento". Consulte cómo operan los offset en una dirección
segmentada o cómo se calcula la dirección de destino de un salto en base al offset indicado en la
instrucción.
OPCODE Número binario de uno o dos bytes que es interpretado como una instrucción por el procesador. Opcodes y
los mnemónicos son dos codificaciones distintas de una misma instrucción, una leíble por un procesador y
otra por un humano.
OPERANDOS Valores con los que se efectuará la operación definida por el mnemónico de una instrucción. Los operandos
de la mayoría de las instrucciones son fuente y destino. Las operaciones unarias (NOT por ejemplo) tienen
un solo operando.
OR Mnemónico de la instrucción que realiza una combinación lógica O de los operandos, que consiste en dar
un 1 por resultado si alguno de los operandos es 1.
OVERFLOW
Flag de procesadores X86 que se prende cuando el resultado de una operación aritmética no cabe en el
operando designado como destino (por ejemplo en un MUL cuando la mitad superior del resultado tiene
algún bit encendido). Supongamos sumar 07FFF h + 2 = 8001h. El resultado parece correcto y lo es si los
números no tienen signo, pero si los números son enteros con signo, la suma de 32767+2 nos da como
resultado -32767, cuando la realidad es que el resultado es 0001 y un carry a la palabra de orden superior
(carry que en decimal vale 32768 unidades). Por lo tanto, en este caso también se activa la flag OVF para
prevenir al programador de esta situación.
PGP
Programa de encriptación que utiliza el método de dos claves (una pública y otra privada) para mantener la
confidencialidad de los documentos intercambiados a traves de un medio tan promiscuo como internet. Es
el encriptador por default en el ambiente underground por ser gratis y tener un algoritmo muy fuerte. Desde
la versión 5 en adelante todo esto ha cambiado, cuenta con el auspicio de una empresa comercial y con una
clave universal que permite abrir cualquier mensaje.
POP, PUSH Mnemónicos para sacar y poner objetos en el stack. El objeto puede ser el contenido de un registro o una
posición de memoria.
POINTER Literalmente "puntero". Es un valor que está señalando una posición en memoria. Los procesadores X86
tienen dos registros SI y DI (ESI y EDI para 32 bits) que tienen funciones especiales como punteros, lo que
es muy útil para el manejo de strings (más sobre esto en nuestra página sobre strings). En lenguajes de alto
nivel (como C) un parámetro a función no se pasa como un valor sino como un puntero que direcciona la
posición de memoria en donde está almacenado el parámetro.
Vea además PTR en ésta misma página.
PSP
"Program Segment Prefix" o Prefijo de segmento del programa, propio del DOS. Es un área de memoria de
100h (256) bytes que se crea en el momento en que el intérprete de comandos lanza un programa (para
decirlo de otro modo, cuando el command.com leyó nuestro comando "edlin data.txt" y carga en memoria
al fabuloso edlin.com para procesar al archivo data.txt).
El PSP precederá en memoria a edlin.com, ya que se cargará a partir de la dirección 0 del segmento CS,
mientras que edlin lo hará a partir de CS:100. En esos 100h bytes se guardan varios valores, entre los cuales
está una parte del buffer de teclado que contiene el argumento (data.txt en este caso), y un procedimiento
para cerrar el programa cuando edlin nos haya hartado (a los 22 segundos, si el lector es de la época de los
ancestrales DOS 3.1, estoy seguro que me comprenderá)
PTR
Directiva que sirve al compilador Assembly para saber que no es un dato directo sino un puntero. Pueden
anteponerse BYTE, WORD o DWORD para establecer el largo de la palabra apuntada:
CMP WORD PTR [SI],2Fh ; compara la word en memoria apuntada por SI y SI+1 con 002Fh
REGISTRO
Lugar de almacenamiento extraordinariamente veloz que tiene el procesador: se lee o escribe en un ciclo
(esto no necesariamente significa que la instrucción que lo hace se procese en un ciclo). Hay registros
accesibles al programador y otros que no (el lector encontrará aquí una buena descripción del modelo 8086
). Estos últimos aparecieron a partir del 80486 y se emplearon entre otras cosas como contadores y para la
predicción de saltos.
El direccionamiento "registro" significa que uno (o dos) de los operandos es un registro interno del
procesador. Lamentablemente en castellano se usa la misma palabra para designar un registro de procesador
que para uno de una base de datos (que en inglés se dice RECORD).
REVERSING Procedimiento que consiste en aplicar ingeniería inversa a una pieza de software. A diferencia del cracking,
que sólo busca permitir la utilización del programa sin pagar por él, el reversing busca comprender el
programa a tal punto que pueda ser posible su mejoramiento o potenciación.
SEGMENT
La memoria segmentada es un resabio del DOS, que quedó "pegado" con eso por haber nacido para
procesadores de 16 bits (y que por lo tanto manipulaban hasta 65536 direcciones). Para salvar el problema
se ideó la segmentación que consiste en dividir la memoria en parágrafos de 16 bytes cada uno. Con esto
estamos pasando de una dirección de 16 bits a una de 20 bits, que ya puede direccionar 1MB. Para hacer
esto se utiliza un registro de segmento y otro de offset. Los X86 tienen cuatro registros de segmento: DS
(data), ES (extra), CS (code) y SS (stack). Más sobre segmentación en esta página
SEGMENT es también una directiva para el ensamblador que informa sobre la forma en que se deben
asumir los segmentos durante la compilación.
SERIALZ Números de serie utilizados para activar programas sin necesidad de pagar por ello. El sitio más famoso al
momento es el de Oscar
SHELL
Así como conocemos lo que es el Kernel , exterior a él esta el Shell, que es el intérprete de comandos de
Unix. Hay varios tipos, cada uno con mayores o menores restricciones (restricted Shell, Bourne Shell, etc),
que el administrador del sistema otorga a los usuarios según su categoría. Por extensión, intérprete de
comandos de cualquier sistema operativo).
STACK
El stack es una zona de memoria para ser usada por el procesador en las llamadas a procedimiento (ahí se
guarda la dirección de retorno) y para almacenamiento de variables temporales (como el valor de un
registro que se quiere conservar). Como el stack se va ocupando desde posiciones altas hacia las más bajas,
el registro SP (stack pointer; que en los procesadores de 32 bits -80386 en adelante- se llama ESP) se va
decrementando por dos en cada almacenamiento de word y en 4 por cada almacenamiento de dword. El
stack pointer señala la última (la más baja) posición ocupada por el stack. El segmento ocupado por el stack
está apuntado por el registro SS (stack segment).
STRING
Literalmente "ristra", traducido normalmente como "cadena", es una sucesión de valores, normalmente,
aunque no necesariamente, caracteres de un byte cada uno. Lea también el tutorial sobre funciones para
manejo de strings
TARGET Objetivo, programa sobre el que se realizará un crack o una operación de ingeniería inversa.
TEST
Mnemónico de una instrucción de procesador que consiste en realizar un AND lógico entre los dos
operandos de la instrucción, modificando las flags de cero, paridad y signo. Ninguno de los operandos
cambia. Es muy común usarla para verificar si el valor booleano retornado por una subrutina es cero o no.
TRAP
Flag del procesador que si está encendida provoca que el procesador ejecute una INT3 después de cada
instrucción. La utilizan los depuradores para posibilitar la ejecución del comando "step" (ejecución paso a
paso).
WAREZ Programas o sitios crackeados, disponibles en Internet.
XOR Mnemónico de la instrucción Exclusive-OR, que consiste en complementar en el operando destino los bits
cuya posición coincida con los "unos" del operando fuente.
ZEN
Modalidad de cracking que consiste en "presentir" cuál fue la intención del programador cuando ideó una
protección y que debemos ORC+. Los budistas Zen sostienen que si uno "siente" a la presa, no es necesario
verla, la flecha se disparará del arco en el momento preciso. Aunque es más rápida, sólo será efectiva si el
cracker tiene una buena experiencia en su oficio.
Assembly y Cracking Elemental 1
Introducción a numeración binaria y hexadecimal
En general, la matemática diaria es base 10 (decimal), aunque la
tendencia de los constructores de PCs fue usar base 2 (binario).
El binario fue la elección simplemente porque OFF y ON son términos
fáciles en electrónica y este modelo encaja bien con 1's y 0's.
En algún momento, alguien decidió que continuar con numeración
binaria era algo tedioso para los humanos y se propuso que los
números se vean parecidos a los de la aritmética decimal, pero
conservando la progresión con potencias de 2 para que la conversión a
binario sea fácil.
De esta forma se popularizó el hexadecimal (base 16).
Qué tiene esto que ver con crackers o programadores assembly?
TODO. Si no se comprende cómo operar con hexadecimales y cómo
convertir entre binario y hexa, es imposible depurar (reversar)
cualquier programa.
En cualquier sistema de numeración, siempre se sigue esta simple
regla: en una base B, los dígitos se numeran desde cero hasta B-1
Que significa lo anterior?, por ejemplo que en base 10 tenemos diez
dígitos, del 0 al 9. En binario tenemos sólo dos: 0 y 1, y para base 16
(hexa) tenemos 16 dígitos. Por simplicidad, se usan los números del 0 a
9 y las seis primeras letras del alfabeto.
0 1 2 3 4 5 6 7 8 9 A B C D E F
(estos dígitos valen 0-15 al convertirlos al sistema decimal)
la cuenta es similar a la del sistema decimal:
... E F 10 11 12 ... 18 19 1A 1B 1C 1D 1E 1F 20 21 22 ...
10 en hexa es 16 decimal, 20 hexa es 32, 30h es 48, 40h es 64 etc.
Una cuenta en sistema binario sería como sigue:
0 1 10 11 100 101 110 111 1000...
Asi 10 (en cualquier base) siempre es igual a B, la base misma -
Equivale a decir que 10 (binario) is 2 decimal, 10 (octal) es 8 decimal,
etc
Bien, si yo tuviese 16 dedos podría sacar la cuenta como lo hacen los
niños, pero como no los tengo, cómo calcular cuánto es A9h en base
10?
Ya sabemos que base 16 se amolda a potencias de 2, lo que no es difícil
de manejar. Una vez que uno aprende a convertir un número a
binario, es fácil cambiar de base a cualquier número, haciéndolo en
dos etapas: primero se convierte de una base a binario y luego de
binario a la otra.
Conversión de Decimal a Binario
A esto me gusta llamarlo 'matemática del resto'.
Básicamente, en lugar de una suma repetida, contando hasta un dígito
hexa, usaremos división repetida para acelerar el proceso.
Nuestros amigos DIV y MOD
En computación, los datos se almacenan como números enteros, ya sea
como una larga serie de dígitos en cualquier base más la ubicación del
punto decimal (o para hablar generalmente para cualquier base, el
punto raíz), o como partes de una fracción (numerador, denominador
y cantidad a sumar en tres partes separadas). Hay por cierto otros
métodos con números imaginarios, pero esto cae fuera de los límites de
esta lección.
Usando números enteros, la división se hace tal como lo hemos
aprendido en base 10, de a un dígito por vez y recordando el resto de
cada etapa. Para cada operación de división, tenemos 2 respuestas: el
cociente (DIV) y el resto (MOD). Aunque estamos más familiarizados
con DIV, MOD tiene interesantes propiedades usadas en programas
de computación, específicamente en randomización y scroll de menús.
Usaremos como notación 47 MOD 4, o 47%4 cuando queramos decir
"dividir 47 por 4 y obtener el resto", ya que el signo "%" se usa como
símbolo para MOD en lenguajes de alto nivel como el C.
para nuestro caso: 47/4 = 11, resto = 3
DIV = 11, MOD = 3
y también: 47%4 = 3 (47 MOD 4 es igual a 3)
Con estos conocimientos podemos comenzar con la conversión de
bases entre decimal y binario (no es tan feo, no se preocupe).
Quisiera que primero conozca que 47 en binario es 101111. Ahora voy
a mostrarle como deducirlo matemáticamente.
Básicamente, dividimos en forma repetitiva nuestro número por la
base binaria (2) y tomamos cada MOD (resto) como el próximo dígito
binario.
47 / 2 nos dá DIV=23 MOD=1, string binario = 1
23 / 2 nos dá DIV=11 MOD=1, string binario = 11
11/2 nos dá DIV=5 MOD=1, string binario = 111
5/2 nos dá DIV=2 MOD=1, string binario = 1111
2/2 nos dá DIV=1 MOD=0, string binario = 01111
1/2 nos dá DIV=0 MOD=1, string binario = 101111
Note que el string se construye de derecha a izquierda, al revés de
cómo uno lee. Esta es una característica del sistema de numeración
arábigo que incrementa el valor de los dígitos de derecha a izquierda
Esto puede parecer tonto en un principio, pero las máquinas requieren
tal nivel de instrucción para hacer aquello que nosotros sabemos hacer
desde hace tanto que olvidamos los basamentos de lo que es un
número: supongamos el 2041; lo tomamos en su totalidad, pero
sabemos muy bien que el 2 tiene mucho más valor que el 4 o el 1. En
cambio las computadoras requieren hacer esto de a pasos.
Escribiremos un programa en pseudocódigo (una mezcla de lenguaje
cotidiano con lenguaje de computación). Es de gran ayuda escribir en
pseudocódigo antes de pasar el programa a un lenguaje concreto.
Viendo nuestro anterior ejemplo, podemos determinar cuándo se
cumplió la operación consultando si DIV = 0. Nuestro programa sería:
1. Obtener el valor DIV (del usuario o del mismo programa)
2. Dividir DIV por 2, dejar el cociente en DIV y el resto en MOD
3. Almacenar MOD como próximo dígito de un string RES
4. Repetir las acciones 2 y 3 hasta que DIV = 0 (inclusive)
5. Informar el resultado RES
Conversión de un número binario a hexadecimal
Como 24 = 16 cada 4 dígitos del string binario, tendremos un dígito
hexa. Notar que también aquí hay que ir de derecha a izquierda como
en las operaciones con números decimales. Usando nuestro ejemplo
del 47:
101111
Lo separamos en grupos de a 4:
10 | 1111
Y ahora consultamos la siguiente tabla de conversión:
0000 = 0 ........ 1000 = 8
0001 = 1 ........ 1001 = 9
0010 = 2 ........ 1010 = A
0011 = 3 ........ 1011 = B
0100 = 4 ........ 1100 = C
0101 = 5 ........ 1101 = D
0110 = 6 ........ 1110 = E
0111 = 7 ........ 1111 = F
Por lo que nuestro número (10 | 1111) , se convierte en:
0010 = 2h y 1111 = Fh
por demás simple, 101111 es 2F en hexadecimal o, más rigurosamente:
47 (dec) = 101111 (bin) = 2F (hex)
Si por ejemplo quisiésemos convertir a base octal, debemos separar de
a tres bits, o sea que para el 47 decimal hacemos:
101 | 111
111 = 7
101 = 5
47 (dec) = 101111 (bin) = 2F (hex) = 57 (oct)
Ahora que usted conoce las relaciones entre las bases, le será mucho
más fácil leer código assembly, y posiblemente en un futuro próximo,
comenzar a entender qué está leyendo
Assembly y Cracking Elemental 2
Frecuentemente, los buenos ejemplos son breves y van justo al punto. Puede ser muy difícil
para un programador assembly novato obtener alguna conclusión valedera de un programa
largo e indocumentado (o malamente comentado) que parece más una sopa de letras que
código.
Estos ejemplos pueden cortarse y pegarse en sus propios programas.
Se me ha preguntado recientemente cuál es el mejor lugar para encontrar información sobre
Assembly. Un lugar (mala respuesta) es Internet. Sin embargo, mi información favorita
proviene de un medio más tradicional: los libros. Y los mejores libros sobre Assembly son sin
duda los más viejos, los que muestran cómo optimizar código de 8086. Yo prefiero comprar
manuales de segunda mano por un dólar que contienen un tesoro en código, siempre
necesario, como por ejemplo el legendario manual de Peter Norton.
En las siguientes páginas veremos el código necesario para realizar las siguientes funciones:
Operaciones con strings
Mostrando Numeros en Assembly
Operaciones con Archivos
Search Funciones de Búsqueda
Otros Códigos útiles
Lo básico con Strings
(HELLO WORLD)
La primer cosa que un instructor de programación debe mostrar es cómo sacar mensajes por
pantalla (esto se denomina "representación"). El mensaje más popularmente usado es "Hello,
World".
Ahora mismo vamos a ver un par de formas de cómo hacerlo. No hay necesidad de entender
ahora cómo funciona, sino cuáles son las reglas básicas para su utilización en futuros
programas.
También se mostrará como parte de un programa para que se vea lo fácil que es incorporar
esta pieza de código a otras de mayor tamaño.
En DOS
Llamado a la Rutina:
message db 'hello world','$'
mov dx,offset message
call DisplayString
La Rutina: (Pequeña porque DOS se encarga casi de todo)
; muestra strigs apuntados por dx usando: int 21h, ah=9
DisplayString:
mov ax,cs
mov ds,ax
mov ah,9 ; Función DOS: mostrar display
int 21h ; Llama la interrupción del DOS
ret
En BIOS
Otra manera es usar la interrupción 10h del BIOS en lugar de la función 9 de la interrupción
21h (DOS). El motivo para hacer esto es doble: por un lado, muchos de los programas a
crackear no se apoyan en los simples métodos del DOS y por otro, si no conocemos el BIOS,
no podremos escribir código para sistemas no-DOS como el UNIX.
Llamado a la Rutina:
message db'hello world','$'
mov dx,offset message
call BiosDisplayString
La Rutina:
; muestra string apuntados por dx usando: int 10h, ah=14
BiosDisplayString:
mov si,dx ; el bios necesita si en lugar de dx
mov ax,cs ; usar segmento actual (de código)
mov ds,ax ; para los datos a ser mostrados
bnxtchar:
lodsb ; buscar el próximo carácter a mostrar
push ax ; preservar ax de cualquier cambio
cmp al,'$' ; marca de final de string?
Jz endbprtstr
Pop ax ; restaura ax
Call BiosDisplayChar
Jmp bnxtchar
endbprtstr:
pop ax ; limpiar
ret
; Observe que usando el BIOS debemos mostrar de a un
carácter por vez.
BiosDisplayChar: ; muestra el caracter que hay en al
Mov ah,0Eh ;Código de la función disp-char de BIOS
Xor bx, bx
Xor dx, dx
Int 10h ; Llamado a la funcion BIOS
Ret
Aunque hay otras maneras de mostrar caracteres (por ejemplo la INT 21h, ah=02 imprime
igualmente un carácter en la pantalla), por lo general todo el mundo usa la INT 10h función
0Eh aquí mostrada. El conocimiento de las interrupciones es muy útil para el cracking .
Y LOS NUMEROS?
(Código Assembly para mostrar números en cualquier base)
Llamado a la Rutina:
mov ax, 0402h
call DisplayWord
La Rutina: ; muestra la Word que hay en AX
DigitBase dw 010h ; usando base 16 dígitos
;cambiar lo anterior por 10h por 0Ah para ver números decimales
DisplayWord proc near
mov si,offset DigitBase
mov di,offset TempNum
NextDgt:
xor dx,dx
div si
add dx,30h ; convertir a dígito ascii
mov [di],dl
dec di
cmp ax,0 ; falta algún dígito?
ja NextDgt
inc di
mov dx,di
mov ah,9
int 21h ; mostrar string apuntado por DX (DOS)
retn
DisplayWord endp
db 4 dup (20h) ; número máximo de dígitos
TempNum db 20h
db 24h,90h
Con esto último, reservamos espacio en memoria.
Es para almacenamiento temporario del string a mostrar.
Nótese que en el ejemplo anterior podríamos haber llamado a la int 10h del BIOS en lugar de
haberlo resuelto con la función 9 de la Int 21h.
Assembly y Cracking Elementales 3
Los siguientes modelos contienen las directivas de ensamblador necesarias para poder
compilar exitosamente programas simples en lenguaje assembly. Pueden ser copiados y
pegados como comienzo de edición de un programa. Para que el principiante tenga una
idea de la importancia de contar con estos modelos en lo que a ahorro de tiempo se
refiere, le sugiero que trate de hacer uno que compile sin errores.
MODELO DE ARCHIVO .COM
;**********************************************
;
; MODELO DE ARCHIVO EJECUTABLE .COM (COM.ASM)
;
; Compilar con:
;
; TASM COM.ASM
; TLINK /t COM.OBJ
;
; +gthorne'97
;
;**********************************************
.model small
.code
.386
org 100h
start: jmp MAIN_PROGRAM
;----------------------
; Zona para datos
;----------------------
;----------------------
MAIN_PROGRAM:
;---------------
; Código de programa
;---------------
;---------------
mov al, 0h ; código de retorno 0 (0 = no error)
exit_program:
mov ah,4ch ; salir al DOS
int 21h
end start
MODELO DE ARCHIVO .COM #2 (ALTERNATIVO)
;**********************************************
;
; MODELO DE ARCHIVO EJECUTABLE.COM #2(COM_B.ASM)
;
; Lo incluimos para poderlo comparar con
; el modelo .EXE mostrado más abajo
;
; Compilar con:
;
; TASM COM_B.ASM
; TLINK /t COM_B.OBJ
;
; +gthorne'97
;
;**********************************************
COM_PROG segment byte public
assume cs:COM_PROG
org 100h
start: jmp MAIN_PROGRAM
;----------------------
; Zona de datos
;----------------------
;----------------------
MAIN_PROGRAM:
;---------------
; Código de programa
;---------------
;---------------
mov al, 0h ; código de retorno (0 = no error)
exit_program:
mov ah,4ch ; salir al DOS
int 21h
COM_PROG ends
end start
MODELO DE ARCHIVO .EXE
;**********************************************
;
; MODELO DE ARCHIVO EJECUTABLE .EXE (EXE.ASM)
;
; Compilar con:
;
; TASM EXE.ASM
; TLINK EXE.OBJ
;
; +gthorne'97
;
;**********************************************
.model small ; normalmente small, medium o large
.stack 200h
.code
EXE_PROG segment byte public
assume cs:EXE_PROG,ds:EXE_PROG,es:EXE_PROG,ss:EXE_PROG
start: jmp MAIN_PROGRAM
;----------------------
; Zona de datos
;----------------------
;----------------------
MAIN_PROGRAM:
;---------------
; Código de programa
;---------------
;---------------
mov al, 0h ; código de retorno (0 = no error)
exit_program:
mov ah,4ch ; salir al DOS
int 21h
EXE_PROG ends
end start
MODELO DE ARCHIVO .EXE #2 (ALTERNATIVO)
;**********************************************
;
; MODELO DE ARCHIVO EJECUTABLE .EXE 2 (EXE2.ASM)
; (Comparar con el primer modelo .COM)
; Probado con TASM 4.1
; Donado por Eyes22, Modificado para semejarse
; los otros modelos
; Compilar con :
;
; TASM EXE2.ASM
; TLINK EXE2.OBJ
;
; +gthorne'97
;
;**********************************************
;dosseg ; directiva que es ignorada en tasm 4,
; descomentar en caso de errores
.model small
.stack 200h
.data
.code
start: jmp MAIN_PROGRAM
;----------------------
; Zona de datos
;----------------------
;---------------
; Código de programa
;---------------
;---------------
mov al, 0h ; código de retorno (0 = no error)
exit_program:
mov ah,4ch ; salir al DOS
int 21h
end start
Cita textual del libro Assembly Language for the IBM-PC
Programas COM:
Hay dos tipos de programas transitorios, dependiendo de la extensión
usada: COM y EXE. Recuerde que usamos DEBUG para crear y salvar un
pequeño programa COM. Un programa COM es una imagen binario de
un programa en lenguaje de máquina. El DOS lo carga en memoria en la
dirección de segmento más baja disponible, creando un PSP en offset 0. El
código, datos y stack se almacenan todos en el mismo segmento físico (y
lógico). El programa no puede superar los 64 kB menos el largo del PSP y
dos bytes reservados en el tope del stack. Todos los registros de segmento
se cargan con la dirección base del programa, el código comienza en el
offset 100h y el área de datos sigue al código. El stack está al final del
segmento ya que el DOS inicializa al registro SP en 0FFFEh.
Hello.asm
Programa ejemplo "hello world" escrito en formato .COM
Note que las directivas DOSSEG, DATA y STACK son innecesarias, y la
directiva ORG (que inicializa al contador de direcciones en 100h) se
antepone a toda instrucción assembly para dejar espacio para el PSP, que
ocupa desde la dirección 0 hasta la 0FFh.
.model tiny
.code
org 100h
maine proc
mov ah,9
mov dx, offset helo_msg
int 21h
mov ax, 4c00h
int 21h
maine endp
helo_msg db 'Hello, world!' '$'
end maine
Assembly y Cracking Elemental 4
Desarrollo de programas (Se aplica para cualquier lenguaje)
Hace mucho tiempo En una Galaxia no tan lejana
Alguien inventó el diagrama de flujo. Este dispositivo fue una sucia
herramienta que requería nivel universitario para ser escrita, la
entendía sólo uno mismo y era como un enredo de espaghettis a la
hora de usarse para dirigir la escritura de un programa.
Hace también mucho tiempo
En la tierra de la ficción interactiva
(Que puede muy bien estar en otra galaxia lejana...)
Alguien más se dio cuenta que diagramas de cajas simples mostrando
la ubicación en un fantasioso texto de aventuras permitía un mapeado
fácil y rápido que permitía retomar el trabajo al tiempo, sin perder la
ilación debido a la simplicidad de las cajas y sus descripciones.
Más Recientemente
En la tierra del sentido común
(El cual tiende a no ser tan común...)
Algunos astutos individuos se dieron cuenta de que las computadoras
no tenían por qué ser tan confusas que no eran necesarios unos tontos
que agregaran frustración con sus diagramas.
De aquí en adelante comienza IPO
Con sus diagramas de caja simples
Para facilitar el planeamiento y desarrollo de programas
Qué es IPO?
Del Inglés: INPUT - PROCESS - OUTPUT
En castellano: ENTRADA - PROCESO - SALIDA
Es por lejos la más simple y usable forma para planear la
programación jamás desarrollada. Tarda un gran tiempo la gente en
aprender la idea que complejidad no significa
necesariamentesuperioridad. (Lea algún texto que compare las
arquitecturas CISC y RISC y entenderá la idea).
La manera en que funciona es realmente clara, todo lo que hay que
hacer es comenzar con un plan básico... Todos los programas tienen
algún grado de entrada (desde un usuario, dispositivo o programa),
algún grado de procesamiento de aquella entrada y algún tipo
desalida, la cual puede ser una pantalla, una impresora, otro
programa, etc
Sabiendo esto, podemos desarrollar todos un programa o parte de
ellos con alguna mutación de este proceso
Aquí hay una más amplia descripción de un programa típico.
La ENTRADA incluye estas etapas:
Leer la línea de comando para ver si hay algún argumento
especial tal como un nombre de archivo o una directiva a la
manera de los comandos del DOS (por ejemplo dir /w *.txt)
donde los argumentos /w y *.txt deben ser leídos e
interpretados
Leer datos desde un archivo de configuración (.CFG)
Pedirle al usuario el ingreso de algún dato (p. ej. su nombre)
Leer datos que ingresan desde un scanner, cámara o cualquier
otro dispositivo de entrada presente en el sistema.
El PROCESAMIENTO Involucra todo aquello que se hace
para manipular o alterar los datos de entrada recibidos (por
ejemplo ordenarlos, operarlos matemáticamente, etc). Esto es
por lo general la mayor parte del programa.
La SALIDA Es como la fase inversa de la entrada. Podemos
grabar la configuración actual, mostrar al usuario algún
mensaje, imprimir algo, o enviar los datos a disco o a la entrada
de otro programa.
Assembly y Cracking Elementales 5
Comienzos en Assembly
Habiendo leído en el capítulo anterior cómo se desarrollan los
programas, es probable que se pregunte si ahora vamos a comenzar a
escribir código. Nuestra primera lección será sobre cómo construir la
caparazón de un programa para que maneje varios tipos de entradas y
se inscriba dentro del modelo IPO.
Qué tan bueno puede ser un programa si no es interactivo? Incluso
programas de baja interactividad como los patches requieren la
lectura de datos desde archivos. Si ya está familiarizado en la técnica
de cómo se hacen las llamadas al DOS, saltee esta parte. El texto que
sigue es para asegurarse que nadie quede a oscuras aún si ha
comenzado desde cero. Después de todo, hemos dirigido este tutorial a
los principiantes.
La primera cosa que uno desea de un programa es que sea capaz de
mostrar un mensaje tonto como "hello, world". Eso haremos.
Primero necesito que usted comprenda qué son los registros (las hiper-
rápidas variables construidas dentro del procesador x86 de su PC).
En los viejos lenguajes de programación en alto nivel, el BASIC fue el
más fácil de todos los que se hayan conocido (no confundir con Visual
Basic, esa monstruosidad de nuestros amigos de Microsquash,
tampoco Qbasic, ni BASICA, sino el viejo y llano BASIC que cada
máquina emuló a través de los años para que si una persona que desea
aprender un lenguaje de programación se atasque con éste, para nada
útil).
En BASIC, había una manera de ingresar información y mostrarla al
usuario, usando comandos semejantes a los que siguen:
DATA "Hello Planet Hollywood";
READ D$; (from data)
PRINT D$;
o más simplemente:
LET D$ = "Hello Planet Hollywood";
PRINT D$;
y la computadora debería haber mostrado nuestro mensaje en la
pantalla, en el supuesto que lo hayamos escrito bien.
En Assembly las cosas no son diferentes. Nos quedamos con el primero
de los dos modelos BASIC, y escribimos algo como lo que sigue:
MYSTRING DB 'Hello Planet Hollywood";
MOV DX, OFFSET MYSTRING
y cuando lo tengamos que imprimir, usaremos una llamada al DOS
con las siguientes sentencias:
MOV AH, 09h
INT 21h
Poniéndolo todo junto, tenemos:
MYSTRING DB 'Hello Planet Hollywood','$'
MOV DX, OFFSET MYSTRING
MOV AH, 09h
INT 21h
No está del todo mal, verdad?
Note el signo '$' al final de la cadena. El DOS necesita que de alguna
manera se le señale el final del string, o sea cuándo debe dejar de sacar
caracteres a la pantalla. Sin él, DOS seguiría tirando a la pantalla los
caracteres que encuentre en memoria después del string, usualmente
el mismo programa o datos sin valor que quedaron en la memoria
luego de encender la PC o que fueron dejados ahí por un programa
anterior. El signo $ es algo para no olvidar.
Hay otra manera de manejar strings en assembly, llamada string-cero,
consistente en que en lugar de terminar con "$" terminan con 00h. No
es mejor una que la otra, solo son diferentes ( en realidad, la string-
cero es manejada por el BIOS en lugar del DOS).
Uno podría hacer una rutina para impresión de cadenas de caracteres
terminadas en cualquier valor no imprimible, aunque no sería muy
útil teniendo gratis las dos ya mencionadas. Quizás más adelante usted
quiera desarrollarla para esconder alguna encriptación en la que se
incluya el caracter de terminación. También puede usar rutinas que
impriman un determinado número de caracteres, que no necesitan
contar con un caracter de terminación.
Volvamos a nuestro ejemplo:
No he mencionado aún que los datos deben separarse del código para
evitar ser ejecutados. Si pone atención en los modelos de programas
.COM y .EXE vistos un par de capítulos antes, verá que en ellos hay
una zona para datos y otra para código ejecutable. La primera
sentencia del programa hace un salto por sobre la zona de datos para
que el nunca se confundan con instrucciones de máquina.
Esto tampoco es muy diferente que en BASIC, en donde la gente
tiende a poner las sentencias DATA al final del programa. Considere
además que cualquier lenguaje de programación decente tiene que
haber sido escrito alguna vez en assembly, y por tanto no se sorprenda
en tener que usar sentencias de string similares.
COMO TRABAJAN LOS REGISTROS
En el ejemplo anterior, hemos visto que se puede utilizar el registro
DX para almacenar una variable de string (que se denominó D$ en
BASIC para hacer más fácil la comparación). En BASIC uno tiene la
cantidad de variables que quiera, pero en assembly sólo hay pocas
variables de registro para escoger, lo cual sigue estando bien porque
no se necesitan más, ya que las variables pueden almacenarse en
cualquier lugar de la memoria que uno elija, y no sólo en la zona de
variables como en BASIC. A continuación se resumen los registros de
propósitos generales de un procesador x86:
AX - Acumulador (donde usualmente quedan los resultados)
BX - Registro Base (usualmente indica el comienzo de una estructura
que reside en memoria)
CX - Contador (lara contar lo que sea, incluso la longitud de strings)
DX - Registro de datos - Usualmente apunta a strings o áreas de datos
en memoria.
Los anteriores registros de propósito general son exactamente eso: de
propósito general. En el programa uno puede en ocasiones
intercambiar las funciones de uno con otro, pero cuando nuestro
programa se tiene que comunicar con el DOS no, porque DOS espera
datos específicos en cada registro. El programa "hello" visto es un
buen ejemplo de esto.
El Acumulador (AX) tiene el mayor perfil que uno puede imaginar.
Tiende a "acumular" lo que sea. Cuando uno sale de un programa o
de una subrutina de cualquier clase, el resultado o los códigos de error
por lo general vuelven en AX. Y cuando se llama un procedimiento,
contiene el código de comando como el en ejemplo "hello" en donde
AH se carga con un 9h.
Cada uno de estos registros tiene 16 bits (dos bytes) aunque desde el
80386 en adelante estos registros pasan a ser de 32 bits y a llamarse
EAX, EBX, etc (aunque sigue siendo válido referirse a la parte baja
del registro como AX, o a los más pequeños AH y AL -por high y low).
AX está compuesto por AH (bits 15...7) y AL (bits 7...0)
BX está compuesto por BH y BL
etc.
Veamos ahora registros de uso mucho más especializado. Los dos
siguientes usualmente se unan en operaciones de copia o comparación
de cadenas de caracteres.
DI - Indice Destino (El lugar a dónde se mueven los datos)
SI - Indice Fuente (El lugar de origen de los datos)
y ahora mencionemos a otro:
BP - Puntero Base
Muy frecuentemente SI, DI y BP se usan para tener presente en qué
lugar del código uno se encuentra -realmente no importa cuál sea el
uso que se le da a cada registro hasta que uno tiene que comunicarse
con algún otro código que espera los datos ubicados en lugares
específicos. Esto sucede bastante a menudo. Examine por ejemplo los
virus y verá qué poco frecuentes son las referencias a SI y BP.
Hay un registro especial que parece como poner la mano de dios en el
programa. Es el puntero de instrucciones IP, usado por el procesador
para saber cuál es la próxima instrucción que ha de ejecutarse.
Por qué esto es importante?
Ahora mostraremos un truco de uso frecuente: digamos que por
ejemplo estamos en un depurador como SoftICE viendo un lazo del
programa que estamos examinando y queremos salir de ese lazo.
Cambiando el valor de IP podemos quedar en la parte exterior del
lazo. Tenga cuidado al hacer esto porque pasar de un lugar a otro del
programa puede tener consecuencias imprevisibles.
Los virus (otra vez usando estas bestias como ejemplo) tienden a usar
bastante instrucciones que cambian al IP de manera no convencional.
Los encabezados de archivos .EXE informan al DOS cuál es el
segmento de arranque de código (que debe cargarse en CS) y cuál es la
dirección de la primera instrucción a ejecutar (que debe cargarse en
IP).
Por lo común los virus de archivos EXE ponen su código al final del
programa y alteran el encabezado de tal forma que los registros CS e
IP apunten a sus instrucciones de inicio, con lo cual logran ejecutarse
antes que cualquier otra instrucción del programa. Luego al final de
su código hacen un salto al inicio del programa (cuya dirección saben
porque la leyeron del encabezamiento antes de cambiarla). Más que
creativo, podría decirse.
Sólo por diversión héchele un vistazo a mi programa SYMBIOTE.
Hace exactamente la misma cosa y es el modo que hay que usar para
agregar código a los programas. Los archivos .COM son un poco
diferentes, tal vez incluso más simples. Symbiote puede manejar
archivos EXE o COM y aunque le tome un rato, por favor no deje de
revisarlo porque puede aprender bastante de él, ya que está
comentado de forma que se pueda comprender lo que está haciendo en
cada momento.
SI USTED NO HA HECHO ESTO ANTES:
Vaya y modifique tanto los modelos .COM o .EXE agregando las
líneas de código para nuestro anterior ejemplo "hello". Considerando
que la mayoría de las llamadas DOS usan básicamente el mismo
método, no tendrá dificultades con otras llamadas.
En la próxima lección, entraremos en el tema de interactividad
leyendo entradas de usuario como parámetros en la línea de comando.
Sería muy bueno que antes usted practique algo con el DEBUG. Abra
una ventana DOS e ingrese los siguientes comandos:
cd \windows\command (o cualquiera sea el directorio de comandos)
debug mode.com master greythorne
-d 80
Se obtendrá esta imagen de la dirección DS:0080 y subsiguientes:
1788:0080 12 20 6D 61 73 74 65 72 - 20 67 72 65 79 74 68 6F
1788:0090 72 6E 65 0D ...............
Lo que estamos viendo es la parte del PSP que DOS crea para correr
el programa MODE.COM, en donde se almacenan los parámetros que
el usuario ingresa en la línea de comandos. Los valores son todos hexa
y el primer 12 indica que el largo de la línea de comandos es 18
caracteres (=12h), la que comienza con 20h (código ASCII del espacio
que separa el nombre del programa cargado MODE.COM del primer
parámetro). Notar además que finaliza con 0Dh, que es el ASCII para
el retorno de línea, pero que ese caracter no se cuenta entre los 12h de
largo. También sobre la derecha de la ventana DOS verá el texto que
ha escrito como parámetro. Los caracteres más allá del 0Dh no tienen
ninguna importancia. No olvide que para salir de debug se utiliza el
comando "q".
Todo esto será explicado próximamente, pero un poco de investigación
previa no puede herir a nadie ;-)
Assembly y Cracking Elemental 6
Un poquito de Interactividad con el usuario
Nuestro primer programa real
En esta sección vamos a realizar un pequeño programa con una dosis
de interactividad con el usuario.
Ante todo, insistamos sobre los comentarios, todo aquello que sigue al
punto y coma en cada línea, que son muy útiles y que el compilador los
ignora. Note además que la convención del punto y coma iniciando un
comentario es para los assemblers, pero no intente comentar así una
porción en lenguaje assembly de un programa C: el compilador C/C++
interpreta el símbolo ";" de manera distinta al assembler.
El siguiente trozo de programa muestra texto en pantalla:
;**********************************************
;
; .COM Modelo de archivo de programa (COM.ASM)
;
; Compilar con:
;
; TASM COM.ASM
; TLINK /t COM.OBJ
;
; +gthorne'97
;
;**********************************************
.model small
.code
.386
org 100h
start: jmp MAIN_PROGRAM
CopyMsg db 'Copyright (c)1997 By Me!',0Dh,0Ah,'$'
MAIN_PROGRAM:
mov dx,offset CopyMsg ;apunta al string en zona de datos
mov ah, 09h ;función DOS 9 = print string
int 21h ;ejecutar función
mov al, 0h ;codigo de retorno (0 = sin error)
EXIT_PROGRAM:
mov ah,4ch ;salir al DOS
int 21h
end start
Nótese que el mensaje tiene un "rótulo" (CopyMsg). El programa hace
referencia al rótulo cuando solicita que se imprima el string porque en
realidad todo string es referido como la dirección de la memoria en
donde está almacenada su primer caracter, lo que se llama brevemente
"offset" (desplazamiento en castellano, aunque le seguiremos diciendo
offset para que coincida con el nombre de la directiva de compilador
que se usa en los programas), pero que en realidad significa "offset
desde el comienzo del segmento".
Notemos además que MAIN_PROGRAM es también un rótulo, pero
en el segmento de código, por lo que no sería adecuado utilizar la
directiva offset para referirse a él. En su lugar, se puede hacer que este
rótulo sea la dirección de destino de una instrucción de salto.
Aunque nuestro string a imprimir es "'Copyright (c)1997 By Me!", hay
otros tres caracteres "de cola": 0Dh (retorno de carro), 0Ah (nueva
línea) y $, que indica el final del string. La combinación 0Dh,0Ah es el
equivalente a apretar la tecla ENTER y hace que el cursor se ubique
en el comienzo de la línea siguiente. Otro caracter de interés es Bell,
código 07h, que en lugar de mostrarse en pantalla, hace que la PC
haga un "beep" en el parlante, el mismo que se escucha durante el
proceso de booteo o al producirse algún tonto error de Windows (lo
que sucede bastante frecuentemente :-)
Ahora veamos cómo hacer para imprimir un sólo caracter. La versión
DOS se muestra en las líneas que siguen y aunque para esta clase no se
necesita, tenga en cuenta que las personas imprimen caracteres usando
métodos de lo más variados y la ingeniería inversa requiere conocer
todas estas posibilidades.
Primero veamos dos líneas que son muy útiles y que muestran cómo
obtener un caracter del teclado. En esta versión, el se examina el
teclado hasta que el usuario apriete una tecla.
mov ah,08h ; DOS función 08h, esperar que el usuario apriete una
int 21h ; tecla.
Al volver, la función DOS tiene el código de la tecla apretada en el
registro AL. A continuación veremos cómo hacer que ese caracter sea
enviado a la pantalla. La función DOS que lo hace espera que el
caracter a mostrar esté en el registro DL, de modo que la próxima
instrucción copiará el contenido de AL en DL (instrucción MOV) y a
continuación se llama a la función DOS para mostrar el caracter en la
pantalla.
mov dl, al ; copiar el caracter que vino del teclado en AL al reg. DL
mov ah,06h ; función DOS 06h, imprimir un caracter en pantalla.
int 21h
Emplearemos lo aprendido en un programa que ya hemos usado como
ejemplo:
;**********************************************
;
; KEYPRESS.ASM
; Nuestro primer programa interactivo
;
; Compilar con:
;
; TASM KEYPRESS.ASM
; TLINK /t KEYPRESS.OBJ
;
; +gthorne'97
;
;**********************************************
.model small
.code
.386
org 100h
start: jmp MAIN_PROGRAM
;----------------------datos-------------------
CopyMsg db 'Copyright (c)1997 By Me!',0Dh,0Ah,'$'
PressEnter db 0Dh,0Ah,'$'
;----------------------código------------------
MAIN_PROGRAM:
; DISPLAY OUR COPYRIGHT MESSAGE
mov dx,offset CopyMsg ;dar a conocer el offset del string
mov ah, 09h ;función 9 = print string
int 21h ;llamada a función DOS
;adicionamos un nuevo ENTER
mov dx, offset PressEnter ;offset de string a DX
mov ah, 09h ;función 9 = print string
int 21h ;
; tomar una tecla apretada por el usuario (sin eco)
; El resultado queda en el registro AL
mov ah,08h ;función 8: leer una tecla
int 21h
; sacar a pantalla el eco (imprimir el caracter)
mov dl, al ; copiar el código del caracter al DL
mov ah,06h ;función 6: mostrar el contenido de DL
int 21h ;en pantalla
; Sólo por diversión, emitiremos un beep
mov dl, 07h ;poner en DL el código del beep
mov ah,06h ;igual que antes
int 21h
mov al, 0h ;código de retorno (0 = sin error)
mov ah,4ch ;salir al DOS
int 21h
end start
Tenemos ahora casi todo lo necesario para hacer un programa que
realice una tarea útil.
Toda vez que en un programa hay un cursor parpadeando esperando
que se apriete una tecla, no está inactivo, sino que se encuentra en un
loop que verifica constantemente si se apretó una tecla. No es la PC en
si misma la que lo hace sino el programa que esta corriendo.
El procesador está haciendo sus propias tareas dentro de la PC. Y de
repente, una persona o programa hace algo que interrumpe el flujo
normal de las cosas. No es necesario que el procesador gaste su tiempo
en lo que le interesa un programa en particular (escaneando
constantemente al teclado para ver si se apretó alguna tecla). El que
nuestro programa deba atender permanentemente al teclado en un
momento dado, no significa problema y es en realidad la forma en que
cualquier juego o programa de entrada de datos lo debe hacer, aún
cuando no sea evidente.
Es usual que se establezca un lazo infinito del que sólo se sale en un
caso especial o cuando se ingresa determinado código. En nuestro caso,
aceptaremos como teclas válidas una "Y" o una "N" tanto en
mayúscula como en minúscula.
Es importante aclarar este aspecto: el programa no será optimizado.
Dejaremos esto para más tarde y nos preocuparemos de hacer que
funcione. El pseudocódigo de un lazo infinito es:
START_OF_LOOP: ; el rótulo (observe los dos puntos ":")
; el código va aqui
JMP START_OF_LOOP ; saltar hacia el inicio del lazo
GO_ON_WITH_PROGRAM: ; un rótulo fuera del lazo
Lo que necesitamos ahora es saber cómo se sale del lazo (la lógica que
nos lleva al rótulo GO_ON_WITH_PROGRAM). Debemos poder
decirle que si se obtuvo una tecla válida que salga del lazo. Para esto,
disponemos de la función CMP (comparar), que evalúa dos variables y
prende o apaga flags según sean iguales o una mayor que la otra (pero
no modifica a ninguna de las variables).
Por si queremos usarlo para otra cosa, pondremos en BL el código de
la tecla que la Int 08 nos deja en AL. Por qué BL? sólo porque es un
registro que no hemos usado aún. No hay otra razón. La sintaxis es:
CMP var_x, var_y
Para nuestro caso, que queremos comparar BL con el caracter "Y":
CMP BL,'Y'
Notar las comillas en la "Y", que indican que la comparación se hace
entre BL y el código ASCII correspondiente a la letra "Y". Para los
números (decimales y hexa) se utilizan las notaciones:
CMP BL, 89
CMP BL, 059h
Las tres formas son equivalentes ya que el código ASCII para la Y es
89 decimal o 59h hexa.
Ahora veamos la instrucción de salto JZ (jump if zero), también
llamada JE (jump if equal) que salta a la dirección indicada si el
resultado de la comparación es cero, es decir var_x es igual a var_y.
En caso de ser distintos, continúa la ejecución de la instrucción que
sigue. La instrucción opuesta es JNZ, también llamada JNE.
El siguiente trozo de código hace lo que hemos descrito hasta ahora:
START_OF_LOOP:
mov ah,8 ;funccion 8, leer una tecla apretada
int 21h
mov bl, al ;guardamos la tecla en BL
cmp bl, 'Y' ;ver si la tecla es una 'Y'
je GO_ON_WITH_PROGRAM
cmp bl, 'N' ;ver si la tecla es una 'N'
je GO_ON_WITH_PROGRAM
JMP START_OF_LOOP ;volver a buscar una tecla
GO_ON_WITH_PROGRAM: ;lugar de salida, ya fuera del lazo
Ahora está en condiciones de seguir este programa con facilidad:
;**********************************************
;
; KEYPRESS.ASM
; Nuestro primer programa interactivo
;
; Compilar con:
;
; TASM KEYPRESS.ASM
; TLINK /t KEYPRESS.OBJ
;
; +gthorne'97
;
;**********************************************
.model small
.code
.386
org 100h
start: jmp MAIN_PROGRAM
;----------------------
CopyMsg db 'Copyright (c)1997 By Me!',0Dh,0Ah,'$'
PressEnter db 0Dh,0Ah,'$'
;----------------------
MAIN_PROGRAM:
; Mostramos el mensaje de Copyright
mov dx, offset CopyMsg ; offset del string en DX
mov ah, 09h ; función 9 = print string
int 21h
;ahora enviemos un retorno y nueva línea adicionales
mov dx, offset PressEnter ;offset en DX
mov ah, 09h ; función 9 = print string
int 21h
START_OF_ENDLESS_LOOP:
mov ah,8 ;funcion 8, buscar una tecla apretada
int 21h
mov bl, al ;guardamos el código de la tecla
cmp bl, 'Y' ; es la tecla una 'Y'?
je GO_ON_WITH_PROGRAM ;salir del lazo si es cierto
cmp bl, 'N' ; es la tecla una 'N'?
je GO_ON_WITH_PROGRAM ;salir del lazo si es cierto
cmp bl, 'y' ; es la tecla una 'y'?
je GO_ON_WITH_PROGRAM ;salir del lazo si es cierto
cmp bl, 'n' ; es la tecla una 'n'?
je GO_ON_WITH_PROGRAM ;salir del lazo si es cierto
; si no es lo que esperábamos, emitir un beep
mov dl, 07h ; poner código de beep en DL
mov ah,6 ; funcion 6, imprimir un character
int 21h
JMP START_OF_ENDLESS_LOOP
GO_ON_WITH_PROGRAM:
; mostrar la tecla apretada (eco)
mov dl, bl ;poner el código de la tecla en DL
mov ah,6 ;función 6, imprimir un character
int 21h
mov al, bl ; poner de nuevo el código de la tecla
; en AL para que sea el valor de retorno
mov ah,4ch ; salir al DOS
int 21h
end start
Como práctica, modifique el programa anterior para que si la tecla
apretada fue una "n" o "N", saque a pantalla el texto "ha dicho que
no!" y si la tecla fue una "y" o "Y", que imprima "Afirmativo!".
También sería de utilidad que en el inicio del programa oriente al
futuro usuario que las teclas que se esperan son Y/N. No hay nada más
frustrante que no conocer qué espera la PC como respuesta.
NOTA: el valor de salida de un programa se almacena en la variable
ERRORLEVEL del DOS, de manera que es posible usar el programa
dentro de un archivo batch que haga diferentes cosas basado en la
tecla que el programa le informa que fue apretada.
Assembly y Cracking Elemental 7
Modularidad y Procedimientos
Desarrollo de Aplicaciones
Recientemente me preguntador cómo crear grandes programas. Hay
algunos trucos para hacerlo. En la segunda parte haremos una
revisión para hacer que nuestros programas sean modulares.
IMPORTANTE MAS ALLA DE TODA RAZON: COMENTE
TANTO COMO PUEDA LOS PROGRAMAS, es decir ponga un
comentario en la mayoría de las líneas sobre la acción que se está
tomando en esa parte del código.
Decir para un MOV CX,8 que se carga un 8 al CX no es comentario
brillante, pero en cambio si : "cargar CX con el número de loops " y
es invaluable a la hora de depurar el código.
Si nunca ha escrito programas grandes, NO CUESTIONE, HAGALO.
Los comentarios son importante cuando usted necesita que alguien le
ayude a depurar el código. Nadie ayudará si no hay comentarios que
den información, porque es verdaderamente difícil, cuando no
imposible. Y si esta pensando que usted lo puede lograr sin
comentarios ni ayuda, adelante, seguramente es mejor que yo. O un
tonto, usted decide.
También escriba comentarios sobre lo que hacen las distintas
secciones del programa, por ejemplo:
;***********
;esta sección toma la línea de comandos y la copia en un buffer
;luego la interpreta y almacena switches y flags en memoria
;espera que se le pase en CX la longitud de la línea de comando
;***********
- - - - - - - - - - - - - - -
Modularidad:
Se puede escribir un programa linealmente desde el principio al fin
sin separarlo en módulos. Pero hay problemas: no es posible escribir
código más allá de los 64 kB. El código puede quedar tan rígido que
un ligero cambio en una de sus partes, posiblemente implique la re-
escritura del programa completo. Y además si nuestro programa de
una sección es más extenso que el buffer de memoria del compilador,
no lo podremos compilar.
Escribiendo el programa en módulos, si es necesario cambiar algo,
sólo se debe modificar el módulo en cuestión. Como estos módulos
pueden llamarse desde varios puntos del programa, se reduce el
tipeado y por lo tanto la posibilidad de error. Cuando los programas
se escriben en módulos, los compiladores toman cada parte por
separado, no produce desborde de memoria y la depuración se hace
más fácil.
La forma de modularizar un programa es utilizando PROCs. Todos
los lenguajes decentes permiten la construcción de subrutinas que
pueden ser llamadas desde cualquier parte del programa. En
assembly se las llama "procs" (abreviatura de procedures, igual que
en Pascal). En C se las denomina "funciones", aunque cada lenguaje
las maneja de manera ligeramente diferente.
Para el TASM, un proc puede verse como sigue:
PrintLine proc near
mov ah, 9 ;función ah=9 (imprime en pantalla)
int 21h ;
ret ;código de retorno desde el proc
endp PrintLine
y para llamarla usamos el siguiente código:
mov dx, offset MyMessage
call PrintLine
Es verdaderamente práctico!!! Hay también algunas
desventajas, que se muestran cuando uno escribe muchos
procs.
CompareByte proc near
Loop:
inc ah
cmp ah, 092h
jne Loop
ret
endp CompareByte
;----------------------
CompareWord proc near
Loop:
inc ax
cmp ax, 02942h
jne Loop
ret
endp CompareWord
;----------------------
Cuando esto se compila, si bien CompareWord y CompareByte son
dos rutinas distintas, el rótulo "Loop" está duplicado y el compilador
nos da error porque no sabe a cual de los dos nos referimos en los
saltos JNE. La solución evidente es tratar de diferenciarlos:
CompareByte proc near
CmpByteLoop1:
inc ah
cmp ah, 092h
jne CmpByteLoop1
ret
endp CompareByte
;----------------------
CompareWord proc near
CompareWordLoop1:
inc ax
cmp ax, 02942h
jne CompareWordLoop1
ret
endp CompareWord
;----------------------
Si nuestro programa es suficientemente largo uno puede volverse
tonto tratando de encontrar maneras de diferenciar las cosas.
Hay una solución fácil: Usar el IDEAL MODE
Al principio del modelo EXE o COM, agregamos la palabra IDEAL
para que TASM sepa que debe utilizar el modo ideal. Veamos cómo
alterar el encabezado del modelo COM :
;----------------------
COM_PROG segment byte public
ideal
assume cs:COM_PROG
org 100h
start:
;----------------------
Esto simplifica la manera de escribir procedimientos
también:
proc PrintLine
mov ah, 9 ;función ah=9 (imprimir en pantalla)
int 21h ;
ret ;volver del proc
endp PrintLine
La ventaja real viene a la hora de diferenciar rótulos comunes. Esto se
hace con u par de símbolos "at" (@@) antepuestos al rótulo:
proc CompareByte
@@Loop:
inc ah
cmp ah, 092h
jne @@Loop
ret
endp CompareByte
;----------------------
proc CompareWord
@@Loop:
inc ax
cmp ax, 02942h
jne @@Loop
ret
endp CompareWord
;----------------------
Los símbolos @@ indican que se trata de un símbolo local a la rutina
y que no debe ser visible para el resto del código. Cuando algo no es
local, se lo denomina GLOBAL, y está disponible para cualquier parte
del programa. Las partes globales son útiles, pero para algunas pocas
cosas. Por ejemplo en el programa de un juego es adecuado tener el
score en variable global para que sea visible en todas las secciones del
juego. Las variables y símbolos locales permiten hacer programas
largos sin el riesgo de tener efectos peligrosos en otras secciones del
código. Veamos como ejemplo nuestro programa para obtener una
tecla Y o N. Lo escribiremos como ejemplo de procedimientización
(qué palabrita!) de código. Lo exageraremos un poco a propósito.
Nuestro objetivo es hacer una sección "main" con la menor líneas de
código posible, sólo hacer unos pocos llamados y salir.
Simplísticamente, una sección main sería:
; entrada
call input
call process
call output
; salida
Si bien este no es un requerimiento excluyente, tenga en cuenta que
cuanto más se acostumbre a programar en módulos, más fácil le será
hacerlo.
;**********************************************
;
; KEYPRESS.ASM
; Nuestro primer Programa Interactivo
; (con un poco de procedimientización)
;
; Compilar con:
;
; TASM KEYPRESS.ASM
; TLINK /t KEYPRESS.OBJ
;
; +gthorne'97
;
;**********************************************
.model small
.code
.386
ideal
org 100h
start: jmp MAIN_PROGRAM
;---------------------- datos --------------
CopyMsg db 'Copyright (c)1997 By Me!',0Dh,0Ah,'$'
PressEnter db 0Dh,0Ah,'$'
;---------------------- código --------------
proc PrintString ;imprime el string apuntado por DX
mov ah, 09h ;comando 9 = imprimir string
int 21h ;
ret ;
endp PrintString
;----------------------
proc PrintChar ; imprime caracter contenido en DL
mov ah,6 ;función 6 = imprimir un caracter
int 21h ;
ret
endp PrintChar
;----------------------
proc CopyRight ;muestra anuncio de Copyright
mov dx,offset CopyMsg ;poner en DX el puntero al string
call PrintString
mov dx,offset PressEnter ;agregar un ENTER final
call PrintString
ret
endp CopyRight
;----------------------
proc GetInput ;verifica teclas válidas
@@Loop:
; primero buscar una tecla leyendo el teclado
mov ah,8 ;función 8, leer tecla apretada
int 21h
mov bl, al ;guardamos el código de la tecla
;porque nos fascina hacerlo
cmp bl, 'Y' ;ver si la tecla es una 'Y'
je @@Done
cmp bl, 'N' ;ver si la tecla es una 'N'
je @@Done
cmp bl, 'y' ;ver si la tecla es una 'y'
je @@Done
cmp bl, 'n' ;ver si la tecla es una 'n'
je @@Done
; hacer un "beep" si la tecla es distinta a la esperada
mov dl,07h ;poner código para BEEP en DL
call PrintChar
jmp @@Loop ;y pedir tecla nuevamente
@@Done:
;ECO (mostrar en pantalla la tecla que el usuario apretó)
mov dl,bl ;copiar el código de tecla a DL
call PrintChar
ret
endp GetInput
;----------------------
MAIN_PROGRAM:
call CopyRight ;mostrar mensaje de Copyright
call GetInput ;requerir de entrada de usuario
;-----------------------
mov al, bl ;valor de salida = código de tecla
mov ah,4ch ;salir al DOS
int 21h
end start
------------------------------------------------------
Aqui damos por terminado este pequeño tutorial de lenguaje
assembly. No olvide que así como usted obtuvo estos conocimientos
gratuitamente, debe brindarlos a los demás y aportar los propios para
que la comunidad de entusiastas del assembly siga creciendo día tras
día.