Registers, Addresses, and Data Types
Ahora que entendemos la arquitectura general de computadoras y procesadores, necesitamos comprender algunos elementos de Assembly antes de comenzar a aprender Assembly: Registers
, Memory Addresses
, Address Endianness
y Data Types
. Cada uno de estos elementos es importante, y comprenderlos correctamente nos ayudará a evitar problemas y horas de solución de errores mientras escribimos y depuramos código en Assembly.
Registers
Como se mencionó anteriormente, cada núcleo de CPU tiene un conjunto de registros. Los registros son los componentes más rápidos en cualquier computadora, ya que están construidos dentro del núcleo de la CPU. Sin embargo, los registros son muy limitados en tamaño y solo pueden contener unos pocos bytes de datos a la vez. Hay muchos registros en la arquitectura x86, pero solo nos enfocaremos en los necesarios para aprender Assembly básico y los esenciales para futuras explotaciones binarias.
Hay dos tipos principales de registros en los que nos enfocaremos: Data Registers
y Pointer Registers
.
Data Registers | Pointer Registers |
---|---|
rax |
rbp |
rbx |
rsp |
rcx |
rip |
rdx |
|
r8 |
|
r9 |
|
r10 |
-
Data Registers
- se utilizan generalmente para almacenar argumentos de instrucciones/syscalls. Los registros de datos principales son:rax
,rbx
,rcx
yrdx
. Los registrosrdi
yrsi
también existen y generalmente se utilizan para los operandos dedestination
ysource
en las instrucciones. Luego, tenemos registros de datos secundarios que pueden usarse cuando todos los registros anteriores están en uso, los cuales sonr8
,r9
yr10
. -
Pointer Registers
- se utilizan para almacenar punteros de direcciones importantes. Los registros principales de punteros son el Base Stack Pointerrbp
, que apunta al inicio del Stack, el Current Stack Pointerrsp
, que apunta a la ubicación actual dentro del Stack (parte superior del Stack), y el Instruction Pointerrip
, que contiene la dirección de la siguiente instrucción.
Sub-Registers
Cada registro de 64-bit
puede dividirse en sub-registros más pequeños que contienen los bits inferiores, con un tamaño de un byte 8-bits
, 2 bytes 16-bits
y 4 bytes 32-bits
. Cada sub-registro puede utilizarse y accederse de forma independiente, por lo que no tenemos que consumir los 64-bits completos si tenemos una cantidad menor de datos.
Los sub-registros pueden accederse como:
Size in bits | Size in bytes | Name | Example |
---|---|---|---|
16-bit |
2 bytes |
el nombre base | ax |
8-bit |
1 bytes |
nombre base y/o termina en l |
al |
32-bit |
4 bytes |
nombre base + comienza con el prefijo e |
eax |
64-bit |
8 bytes |
nombre base + comienza con el prefijo r |
rax |
Por ejemplo, para el registro de datos bx
, el registro de 16-bit es bx
, por lo que el de 8-bit es bl
, el de 32-bit sería ebx
, y el de 64-bit sería rbx
. Lo mismo se aplica a los registros de punteros. Si tomamos el Base Stack Pointer bp
, su sub-registro de 16-bit es bp
, el de 8-bit es bpl
, el de 32-bit es ebp
, y el de 64-bit es rbp
.
Los siguientes son los nombres de los sub-registros para todos los registros esenciales en una arquitectura x86_64:
Description | 64-bit Register | 32-bit Register | 16-bit Register | 8-bit Register |
---|---|---|---|---|
Data/Arguments Registers | ||||
Syscall Number/Return value | rax |
eax |
ax |
al |
Callee Saved | rbx |
ebx |
bx |
bl |
1st arg - Destination operand | rdi |
edi |
di |
dil |
2nd arg - Source operand | rsi |
esi |
si |
sil |
3rd arg | rdx |
edx |
dx |
dl |
4th arg - Loop counter | rcx |
ecx |
cx |
cl |
5th arg | r8 |
r8d |
r8w |
r8b |
6th arg | r9 |
r9d |
r9w |
r9b |
Pointer Registers | ||||
Base Stack Pointer | rbp |
ebp |
bp |
bpl |
Current/Top Stack Pointer | rsp |
esp |
sp |
spl |
Instruction Pointer 'call only' | rip |
eip |
ip |
ipl |
A medida que avancemos en el módulo, discutiremos cómo usar cada uno de estos registros.
Existen otros registros varios, pero no los cubriremos en este módulo, ya que no se necesitan para el uso básico de Assembly. Por ejemplo, está el registro RFLAGS
, que se utiliza para mantener diversas flags usadas por la CPU, como el zero flag ZF
, que se utiliza para instrucciones condicionales.
Memory Addresses
Como se mencionó anteriormente, los procesadores x86 de 64 bits tienen direcciones de 64 bits de ancho que van desde 0x0
hasta 0xffffffffffffffff
, por lo que esperamos que las direcciones estén dentro de este rango. Sin embargo, la RAM está segmentada en varias regiones, como el Stack, el heap y otras regiones específicas del programa y del kernel. Cada región de memoria tiene permisos específicos de read
, write
, execute
que especifican si podemos leer de ella, escribir en ella o llamar a una dirección en ella.
Siempre que una instrucción pasa por el Instruction Cycle para ser ejecutada, el primer paso es obtener la instrucción desde la dirección en la que está ubicada, como se mencionó anteriormente. Hay varios tipos de obtención de direcciones (es decir, addressing modes) en la arquitectura x86:
Addressing Mode | Description | Example |
---|---|---|
Immediate |
El valor se proporciona dentro de la instrucción | add 2 |
Register |
El nombre del registro que contiene el valor se proporciona en la instrucción | add rax |
Direct |
La dirección completa directa se proporciona en la instrucción | call 0xffffffffaa8a25ff |
Indirect |
Se proporciona un puntero de referencia en la instrucción | call 0x44d000 o call [rax] |
Stack |
La dirección está en la parte superior del stack | add rsp |
En la tabla anterior, mientras más abajo esté, más lento será. Cuanto menos inmediato sea el valor, más lento será obtenerlo.
Aunque la velocidad no es nuestra principal preocupación al aprender Assembly básico, debemos comprender dónde y cómo se ubica cada dirección. Tener este conocimiento nos ayudará en futuras explotaciones binarias, como Buffer Overflow. Esta comprensión tendrá implicaciones aún más significativas con explotaciones binarias avanzadas, como ROP o Heap exploitation.
Address Endianness
El endianness de una dirección es el orden de sus bytes en el que se almacenan o recuperan de la memoria. Hay dos tipos de endianness: Little-Endian
y Big-Endian
. Con los procesadores Little-Endian, el byte menos significativo de la dirección se llena/recupera primero (derecha-a-izquierda
), mientras que con los procesadores Big-Endian, el byte más significativo se llena/recupera primero (izquierda-a-derecha
).
Por ejemplo, si tenemos la dirección 0x0011223344556677
para almacenar en memoria, los procesadores Little-Endian almacenarían el byte 0x00
en los bytes más a la derecha, luego el byte 0x11
después de él, quedando 0x1100
, y luego el byte 0x22
, quedando 0x221100
, y así sucesivamente. Una vez que todos los bytes están en su lugar, se verán como 0x7766554433221100
, que es la inversión del valor original. Por supuesto, al recuperar el valor, el procesador también utilizará la recuperación Little-Endian, por lo que el valor recuperado será el mismo que el original.
Otro ejemplo que muestra cómo esto puede afectar los valores almacenados es binario. Por ejemplo, si tuviéramos el entero de 2 bytes 426
, su representación binaria es 00000001 10101010
. El orden en el que se almacenan estos dos bytes cambiaría su valor. Por ejemplo, si se almacenara en orden inverso como 10101010 00000001
, su valor se convertiría en 43521
.
Los procesadores Big-Endian almacenarían estos bytes como 00000001 10101010
(izquierda-a-derecha
), mientras que los procesadores Little-Endian los almacenarían como 10101010 00000001
(derecha-a-izquierda
). Al recuperar el valor, el procesador debe usar el mismo endianness utilizado al almacenarlos, o se obtendrá un valor incorrecto. Esto indica que el orden en el que se almacenan/recuperan los bytes marca una gran diferencia.
La siguiente tabla demuestra cómo funciona el endianness:
Address | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Address Value |
---|---|---|---|---|---|---|---|---|---|
Little Endian | 77 | 66 | 55 | 44 | 33 | 22 | 11 | 00 | 0x |
Big Endian | 00 | 11 | 22 | 33 | 44 | 55 | 66 | 77 | 0x |
Address: 0x0011223344556677
Puedes hacer clic en el botón "Load Address" para visualizar cómo cada endianness carga datos/direcciones en la memoria.
Como podemos ver, esto significa que una dirección escrita en Little-Endian o en Big-Endian se referiría a diferentes ubicaciones en la memoria, ya que sería leída de manera diferente por cada tipo de procesador.
Este módulo siempre usará el orden de bytes Little-Endian, ya que se utiliza con Intel/AMD x86 en la mayoría de los sistemas operativos modernos, por lo que el shellcode siempre se representa de derecha a izquierda.
Lo importante que debemos aprender aquí es saber que nuestros bytes se almacenan en memoria de derecha a izquierda.
Por lo tanto, si quisiéramos almacenar una dirección o una cadena con Assembly, tendríamos que almacenarla en orden inverso. Por ejemplo, si queremos almacenar la palabra Hello
, empujaríamos sus bytes en orden inverso: o
, l
, l
, e
y finalmente H
.
Esto puede parecer un poco contraintuitivo, ya que la mayoría de las personas están acostumbradas a leer de izquierda a derecha. Sin embargo, esto tiene múltiples ventajas al procesar datos, como poder recuperar un subregistro sin tener que recorrer todo el registro o poder realizar operaciones aritméticas en el orden correcto de derecha a izquierda.
Data Types
Finalmente, la arquitectura x86 admite muchos tipos de tamaños de datos, que pueden utilizarse con varias instrucciones. Los siguientes son los tipos de datos más comunes que utilizaremos con las instrucciones:
Component | Length | Example |
---|---|---|
byte |
8 bits | 0xab |
word |
16 bits - 2 bytes | 0xabcd |
double word (dword) |
32 bits - 4 bytes | 0xabcdef12 |
quad word (qword) |
64 bits - 8 bytes | 0xabcdef1234567890 |
Siempre que usemos una variable con un cierto tipo de dato o usemos un tipo de dato con una instrucción, ambos operandos deben ser del mismo tamaño.
Por ejemplo, no podemos usar una variable definida como byte
con rax
, ya que rax
tiene un tamaño de 8 bytes. En este caso, tendríamos que usar al
, que tiene el mismo tamaño de 1 byte. La siguiente tabla muestra el tipo de dato apropiado para cada subregistro:
Sub-register | Data Type |
---|---|
al |
byte |
ax |
word |
eax |
dword |
rax |
qword |
Discutiremos esto más a fondo en las próximas secciones. Con todos los fundamentos de Assembly cubiertos, podemos comenzar a aprender sobre las instrucciones x86 y escribir código Assembly básico.