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

Más contenido relacionado

PDF
Sistema de representación de la informacion
DOCX
Sistemas numericos
DOCX
Codigo Binario
PPTX
Sistemas numericos
PDF
9. electronica digital
PDF
REPRESENTACIÓN DE LA INFORMACIÓN EN LA COMPUTADORA
DOCX
El código gray
DOCX
Codigos digitales
Sistema de representación de la informacion
Sistemas numericos
Codigo Binario
Sistemas numericos
9. electronica digital
REPRESENTACIÓN DE LA INFORMACIÓN EN LA COMPUTADORA
El código gray
Codigos digitales

La actualidad más candente (16)

PPTX
Asignacion #3
PPT
Tema2 arquitectura del ordenador hardware
DOCX
Aritmética de Computadores
DOC
Guía de informática binarios
PPTX
Presentacion sistema binario
PPTX
Sistemas de números binarios
PPTX
Sistemas de numeracion
PPTX
Asignacion 3
PPTX
Metodos de convercion
PPTX
Sistemas y codigos numericos.
PDF
Material de ayuda
PPTX
Sistema Binario
PPTX
Aritmetica del computador
PPTX
Sistemas Numericos
PDF
Sistema binario
DOCX
Conversión Entre Sistemas de Numeración
Asignacion #3
Tema2 arquitectura del ordenador hardware
Aritmética de Computadores
Guía de informática binarios
Presentacion sistema binario
Sistemas de números binarios
Sistemas de numeracion
Asignacion 3
Metodos de convercion
Sistemas y codigos numericos.
Material de ayuda
Sistema Binario
Aritmetica del computador
Sistemas Numericos
Sistema binario
Conversión Entre Sistemas de Numeración
Publicidad

Similar a Mini curso assembly (20)

PDF
sistema numérico binario y todos sus componentes.
PDF
Clase de sistema de numeración
DOCX
Eventos digitales y analógicos (tema 2)
DOCX
Eventos digitales y analógicos (tema 2)
PPT
Sistema Numeración
PPT
Gill-Sistema de numeraciòn
PPTX
Asignacion #3
PPTX
Sistemas Numéricos
PPTX
Sistemas de Numeración y conversiones
PDF
Sistemas numeracion
PPTX
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
PDF
compresor de archivos
PPTX
Conversión de binario a decimal
PDF
Logica computacional
PPTX
Sistemas de numeracion
PPTX
Tarea1 daniel garcía_delicado
PDF
Sistemabinario
PDF
Sistemabinario
PPTX
Conversion de binarios a decimales y decimales a
PPT
Exposición.ppt
sistema numérico binario y todos sus componentes.
Clase de sistema de numeración
Eventos digitales y analógicos (tema 2)
Eventos digitales y analógicos (tema 2)
Sistema Numeración
Gill-Sistema de numeraciòn
Asignacion #3
Sistemas Numéricos
Sistemas de Numeración y conversiones
Sistemas numeracion
Sistemas Numericos y conversiones(Powerpoint aplicaciones m. 1)
compresor de archivos
Conversión de binario a decimal
Logica computacional
Sistemas de numeracion
Tarea1 daniel garcía_delicado
Sistemabinario
Sistemabinario
Conversion de binarios a decimales y decimales a
Exposición.ppt
Publicidad

Más de Franciny Salles (8)

PDF
Aprendendo python
PDF
C++ for hackers
PPTX
Clase 02 - Curso Hacker Ético
PDF
Basico TCP/IP
PDF
Uso de-telnet-708-mddd5d
DOCX
Operadores google
PDF
PDF
Clase01 - Curso Hacker Ético
Aprendendo python
C++ for hackers
Clase 02 - Curso Hacker Ético
Basico TCP/IP
Uso de-telnet-708-mddd5d
Operadores google
Clase01 - Curso Hacker Ético

Último (14)

PPTX
Qué es Google Classroom Insertar SlideShare U 6.pptx
PPTX
Plantilla-Hardware-Informático-oficce.pptx
PPTX
presentacion_energias_renovables_renovable_.pptx
PPTX
FUNCIONES DE CLASSROOM EN EL FUNCIONAMIENTO ESCOLAR
PPTX
Guia de power bi de cero a avanzado detallado
PPTX
PRESENTACION NIA 220 idhsahdjhJKSDHJKSHDJSHDJKHDJHSAJDHJKSAHDJkhjskdhasjdhasj...
PPT
laser seguridad a la salud humana de piel y vision en laser clase 4
PDF
CAPACITACIÓN MIPIG - MODELO INTEGRADO DE PLANEACIÓN Y GESTIÓN
PDF
Frases de Fidel Castro. Compilación Norelys Morales Aguilera
PPTX
Evolución de la computadora ACTUALMENTE.pptx
PDF
[Ebook gratuito] Introducción a la IA Generativa, Instalación y Configuración...
PDF
Herramientaa de google google keep, maps.pdf
PPTX
Presentación de un estudio de empresa pp
PDF
LA INTELIGENCIA ARTIFICAL SU HISTORIA Y EL FUTURO
Qué es Google Classroom Insertar SlideShare U 6.pptx
Plantilla-Hardware-Informático-oficce.pptx
presentacion_energias_renovables_renovable_.pptx
FUNCIONES DE CLASSROOM EN EL FUNCIONAMIENTO ESCOLAR
Guia de power bi de cero a avanzado detallado
PRESENTACION NIA 220 idhsahdjhJKSDHJKSHDJSHDJKHDJHSAJDHJKSAHDJkhjskdhasjdhasj...
laser seguridad a la salud humana de piel y vision en laser clase 4
CAPACITACIÓN MIPIG - MODELO INTEGRADO DE PLANEACIÓN Y GESTIÓN
Frases de Fidel Castro. Compilación Norelys Morales Aguilera
Evolución de la computadora ACTUALMENTE.pptx
[Ebook gratuito] Introducción a la IA Generativa, Instalación y Configuración...
Herramientaa de google google keep, maps.pdf
Presentación de un estudio de empresa pp
LA INTELIGENCIA ARTIFICAL SU HISTORIA Y EL FUTURO

Mini curso assembly

  • 1. MINICURSO ASSEMBLY Autor: Greythorne the Technomancer Traducción: Eidan Yoson Adaptación: Franciny Salles (#Bl4kd3m0n)
  • 2. 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
  • 3. 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
  • 4. 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
  • 5. 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).
  • 6. 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
  • 7. 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
  • 8. 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)
  • 9. 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
  • 10. 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)
  • 11.  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.
  • 12. 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)
  • 13. 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
  • 14. 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
  • 16.  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)
  • 17. 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)
  • 18. 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.
  • 19. 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
  • 20. ? 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:
  • 21. -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
  • 22. 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:
  • 23. 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.
  • 24. 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
  • 25. 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
  • 26. 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
  • 27. 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
  • 28. 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
  • 29. 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:
  • 30. 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.
  • 31. 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)
  • 32. 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 100a 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
  • 33. 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
  • 34. ); 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(
  • 35. 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).
  • 36. 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 );
  • 37. 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
  • 38. 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
  • 39. 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
  • 40. 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
  • 41. 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
  • 42. 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
  • 43. 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.
  • 44. 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
  • 45. 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 :
  • 46. 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
  • 47. 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
  • 48. 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
  • 49. 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
  • 50. 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
  • 51. 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).
  • 52. 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.
  • 53. 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
  • 54. 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).
  • 55. 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
  • 56. 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
  • 57. 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:
  • 58. 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
  • 59. 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.
  • 60. 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:
  • 61. 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
  • 62. 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
  • 63. 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
  • 64. 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
  • 65. 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
  • 66. 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.
  • 67. 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$;
  • 68. 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
  • 69. 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.
  • 70. 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
  • 71. 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 windowscommand (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 ;-)
  • 72. 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)
  • 73. 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
  • 74. 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
  • 75. 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 ":")
  • 76. ; 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'
  • 77. 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'?
  • 78. 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.
  • 79. 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:
  • 80. 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
  • 81. 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
  • 82. 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
  • 83. 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 ;
  • 84. 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
  • 85. ;---------------------- 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.