mini curso assembly

85
MINICURSO ASSEMBLY Autor: Greythorne the Technomancer Traducción: Eidan Yoson Adaptación: Franciny Salles (#Bl4kd3m0n)

Upload: franciny-salles

Post on 15-Apr-2017

323 views

Category:

Internet


7 download

TRANSCRIPT

Page 1: Mini curso assembly

MINICURSO

ASSEMBLY

Autor: Greythorne the Technomancer

Traducción: Eidan Yoson

Adaptación: Franciny Salles (#Bl4kd3m0n)

Page 2: Mini curso assembly

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

Page 3: Mini curso assembly

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

Page 4: Mini curso assembly

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

Page 5: Mini curso assembly

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).

Page 6: Mini curso assembly

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

Page 7: Mini curso assembly

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

Page 8: Mini curso assembly

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)

Page 9: Mini curso assembly

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

Page 10: Mini curso assembly

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)

Page 11: Mini curso assembly

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.

Page 12: Mini curso assembly

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)

Page 13: Mini curso assembly

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

Page 14: Mini curso assembly

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

Page 15: Mini curso assembly
Page 16: Mini curso assembly

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)

Page 17: Mini curso assembly

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)

Page 18: Mini curso assembly

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.

Page 19: Mini curso assembly

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

Page 20: Mini curso assembly

? 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:

Page 21: Mini curso assembly

-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

Page 22: Mini curso assembly

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:

Page 23: Mini curso assembly

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.

Page 24: Mini curso assembly

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

Page 25: Mini curso assembly

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

Page 26: Mini curso assembly

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

Page 27: Mini curso assembly

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

Page 28: Mini curso assembly

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

Page 29: Mini curso assembly

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:

Page 30: Mini curso assembly

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.

Page 31: Mini curso assembly

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)

Page 32: Mini curso assembly

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

Page 33: Mini curso assembly

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

Page 34: Mini curso assembly

);

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(

Page 35: Mini curso assembly

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).

Page 36: Mini curso assembly

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

);

Page 37: Mini curso assembly

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

Page 38: Mini curso assembly

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

Page 39: Mini curso assembly

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

Page 40: Mini curso assembly

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

Page 41: Mini curso assembly

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

Page 42: Mini curso assembly

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

Page 43: Mini curso assembly

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.

Page 44: Mini curso assembly

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

Page 45: Mini curso assembly

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 :

Page 46: Mini curso assembly

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

Page 47: Mini curso assembly

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

Page 48: Mini curso assembly

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

Page 49: Mini curso assembly

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

Page 50: Mini curso assembly

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

Page 51: Mini curso assembly

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).

Page 52: Mini curso assembly

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.

Page 53: Mini curso assembly

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

Page 54: Mini curso assembly

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).

Page 55: Mini curso assembly

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

Page 56: Mini curso assembly

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

Page 57: Mini curso assembly

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:

Page 58: Mini curso assembly

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

Page 59: Mini curso assembly

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.

Page 60: Mini curso assembly

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:

Page 61: Mini curso assembly

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

Page 62: Mini curso assembly

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

Page 63: Mini curso assembly

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

Page 64: Mini curso assembly

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

Page 65: Mini curso assembly

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

Page 66: Mini curso assembly

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.

Page 67: Mini curso assembly

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$;

Page 68: Mini curso assembly

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

Page 69: Mini curso assembly

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.

Page 70: Mini curso assembly

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

Page 71: Mini curso assembly

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 ;-)

Page 72: Mini curso assembly

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)

Page 73: Mini curso assembly

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

Page 74: Mini curso assembly

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

Page 75: Mini curso assembly

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 ":")

Page 76: Mini curso assembly

; 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'

Page 77: Mini curso assembly

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'?

Page 78: Mini curso assembly

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.

Page 79: Mini curso assembly

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:

Page 80: Mini curso assembly

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

Page 81: Mini curso assembly

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

Page 82: Mini curso assembly

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

Page 83: Mini curso assembly

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 ;

Page 84: Mini curso assembly

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

Page 85: Mini curso assembly

;----------------------

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.