Assembly Language - x86 Overview
¿Qué es Assembly?
- Low-level language: versión legible por humanos del instruction set de la arquitectura
- Diferente para cada arquitectura de computadora
- Basado en mnemonics
- Complejo de manejar
¿Por qué Assembly?
- Assembly es el lenguaje de bajo nivel más popular
- Es en realidad una clase de lenguajes
- Los más comunes actualmente: 8086, ARM, MIPS, x86, x64
- x86:** la mayoría de computadoras de escritorio, laptops y consolas de juegos
- ARM:** Apple M1, dispositivos móviles
Compilación y Desassembly
El proceso es:
- El autor del programa escribe código en un lenguaje de alto nivel ( ej. C)
- El código C se compila a machine code ( series de bytes que la CPU entiende)
- El investigador generalmente no tiene acceso al código fuente C, solo a los bytes
- Un disassembler traduce esos bytes a una representación textual más legible
Arquitectura x86
Cuando un programa se ejecuta, ocurre el siguiente loop:
- Una instrucción de CPU se lee de la memoria principal por la Control Unit
- La instrucción se procesa y ejecuta por la Arithmetic-Logic Unit, junto con input del usuario o de los registros
- El output de la operación se almacena en los registros de la CPU o se envía a un dispositivo de salida
x86-32
- Arquitectura CISC de 32-bit, Little-Endian
- Opera en dos modos:
- Modo Real: según arranca, soporta instrucciones de 16 bits
- Modo Protegido: modo que soporta paginación y memoria virtual
Little-Endian vs Big-Endian
Endianness es el orden o secuencia de bytes de una palabra de datos digital en la memoria de computadora.
x86 - Rings ( Anillos de Privilegio)
x86 soporta el concepto de separación de privilegios mediante 4 anillos:
- Ring 0** ( nivel de privilegio máximo)
- Ring 1 y 2** ( no se usan normalmente)
- Ring 3** ( nivel de privilegio mínimo) Los sistemas operativos típicamente implementan el nivel de usuario en Ring 3 y el kernel en Ring 0.
Sintaxis de Assembly Language x86
Sintaxis INTEL ( más común):
mov eax, offset var
mov ebx, 0f02h
Sintaxis AT&T:
movl $var, %eax
movl $0xf02, %ebx
Instrucciones
Una instrucción de assembly = mnemonic + optional operand ( s)
Ejemplo: mov eax, 0xFF equivale al opcode B8 FF 00 00 00
Un operand puede ser:
- Immediate:** un valor constante ( ej. 0x3)
- Registro:** ej. eax
- Dirección de memoria:** ej. [0x400100 + 4] El opcode son los bytes que corresponden a la instrucción y sus operands.
Instrucciones Populares
Datos
- mov
- lea
Lógica
- or
- and
- xor
- shr
- shl
Aritmética
- add
- sub
- inc
- dec
- mul
- div
Control de Flujo
- test
- cmp
- jmp
- jcc
Stack
- push
- pop
- call
- ret
Instrucciones Básicas
MOV ( Mover)
mov destination source — mueve el valor de source a destination
Ejemplos:
mov es, ax— es = axmov bl, 255— bl = 255mov ebx, [eax]— memory access por registromov ebx, [eax + 8]— acceso por desplazamientomov ebx, [eax + esi * 4]— acceso por index y desplazamiento
LEA ( Load Effective Address)
lea operand1, mem — lee una dirección de memoria y la almacena en operand1 ( sin acceder al contenido)
Ejemplos:
lea eax, [ebx+8]— pone en eax el valor ebx+8lea edx, [eax * 5]— pone en edx el valor eax*5
ADD, SUB, INC, DEC
add operand1, operand2— suma los dos operands y almacena el resultado en el primerosub operand1, operand2— resta operand2 de operand1inc operand— incrementa el registro en 1dec operand— decrementa el registro en 1
SHR/SHL ( Shift Right/Left)
Corre bits a izquierda o derecha
shr eax, 0x10shl eax, 0x2
MUL ( Multiplicación sin signo)
Multiplica el registro AX por el operand:
- Byte ⇒ resultado se almacena en AX
- Word ⇒ resultado se almacena en DX y AX
DIV ( División)
Divide entre el operand:
- Byte ⇒ divide AX, almacena resultado en AL
- Word ⇒ divide DX y AX, almacena en AX y DX ( remainder)
Registros
Un registro es la unidad básica de almacenamiento de datos de la CPU, con tiempo de acceso más rápido que RAM.
Registros Generales ( 32-bit con variantes 16 y 8-bit)
- EAX** ( AX, AH, AL) — usado frecuentemente en operaciones
- EBX** ( BX, BH, BL)
- ECX** ( CX, CH, CL) — contador en loops
- EDX** ( DX, DH, DL)
- EBP** ( BP) — base pointer
- ESP** ( SP) — stack pointer
- ESI** ( SI) — source index
- EDI** ( DI) — destination index
Registros de Segmento
- CS** — code section
- SS** — stack section
- DS** — data section
- ES, FS, GS** — general purpose
Registros de Punteros
- SP** — stack pointer
- BP** — base pointer
- SI** — source index
- DI** — destination index
- IP** — instruction pointer
Registros Especiales
- EFLAGS:** registro de 32 bits con flags que proporcionan información sobre operaciones previas
- EIP:** siempre contiene la dirección de la siguiente instrucción a ejecutar
Register Breakdown
EAX, EBX, ECX y EDX pueden desglosarse así:
- EAX** — todos los 32 bits
- AX** — 16 bits menos significativos de EAX
- AH** — 8 bits más significativos de AX
- AL** — 8 bits menos significativos de AX
Los 16 bits más significativos de EAX pueden accederse usando
SHR EAX, 0x10y luegoAX.
Convenciones de Registros
- EAX** — valor de retorno de una función
- EAX:EDX** — cociente y remainder en multiplicación y división
- ECX** — contadores ( variables de loop)
- ESI** — source en operaciones de chain/memory ( strcpy, memcpy)
- EDI** — destination en operaciones de chain/memory
- EBP** — base del stack frame
- ESP** — stack pointer
EFLAGS Register
Registro de 32 bits con flags que dan información sobre el resultado de cálculos previos. Los flags más comunes son:
- Zero Flag:** se activa si el resultado de una operación es 0; de lo contrario se limpia
- Carry Flag:** se activa si el resultado es demasiado grande o pequeño para el operand destino; de lo contrario se limpia
- Sign Flag:** se activa si el MSB se establece ( el resultado es negativo); de lo contrario se limpia
Ejemplo EFLAGS
Dado:
mov eax, 0x1
mov ebx, 0x0
sub ebx, eax ; ebx - eax = 0x0 - 0x1 = -1
Estados de flags:
- Zero flag:** 0 ( el resultado no es cero)
- Carry flag:** 1 ( hay underflow)
- Sign flag:** 1 ( el resultado es negativo)
Branching ( Control de Flujo)
Hay dos tipos de jumps en x86:
- Unconditional:** solo salta donde se le dice
jmp 0x401072- Conditional:** chequea el resultado de algún cálculo, salta según corresponda
cmp eax, 0x10— compara el valor de eax con 0x10jge 0x401072— salta a 0x401072 si eax es mayor/igual que 0x10
Jumps Condicionales
Hay muchos tipos de jumps condicionales, frecuentemente referidos como jcc ( donde j = jump y cc = condition). Cada instrucción de jump realiza diferentes chequeos en el registro EFLAGS para determinar si el jump debe realizarse o no.
Comparaciones
CMP: compara dos operands
cmp operand1, operand2- La operación cambia el estado de los flags dependiendo del resultado
- Ejemplo:
cmp ax, cxTEST: realiza un AND lógico - Comúnmente usado para chequear si un registro es cero
- Ejemplo:
test eax, eax; jz 0x400100— salta si eax == 0
Cómo decir en x86…
if ( a == b) goto 0x1000; cmp a, b; jz 0x1000;
if ( a < b) goto 0x1000; cmp a, b; jl 0x1000;
if ( a) goto 0x1000; test a, a; jnz 0x1000;
Loops
Los loops en assembly se establecen con la instrucción LOOP:
mov ECX, 10
l1:
dec CL
loop l1
O alternativamente:
mov ECX, 10
L1:
DEC CL
JNZ L1
Cuando se usa LOOP, el contador del loop se establece en ECX y cada vez que se ejecuta se resta 1.
Main Memory ( Memoria Principal)
Cuando un programa se ejecuta, se cargan varias secciones en RAM:
- Data:** variables estáticas/globales, puestas en su lugar cuando el programa se carga
- Code:** instrucciones de CPU del programa
- Heap:** memoria dinámica, asignada y liberada durante la ejecución
- Stack:** variables y argumentos locales a las funciones del programa
The Stack
El stack es una estructura de datos LIFO ( Last In First Out) con operaciones push & pop.
Stack-relevant registers:
- ESP** ( Stack Pointer) — siempre apunta al tope del stack ( dinámico)
- EBP** ( Base Pointer) — se mantiene consistente dentro de una función y se usa para referenciar variables locales y parámetros
Stack-relevant instructions:
- pop, push, call, ret ( y también enter, leave)
PUSH y POP
PUSH
push origin — decrementa stack pointer ( ESP) e inserta el valor source en el stack
Equivalente a:
sub esp, 4
mov [esp], origin
POP
pop destination — envía el primer valor del stack al operand destino e incrementa el stack pointer
Equivalente a:
mov destination, [esp]
add esp, 4
Function Calls
Una llamada a función en x86 ocurre así:
- Los parámetros se pushean en el stack en orden inverso
- La instrucción
callpushea la dirección de retorno en el stack - En la función:
- Se pushea el EBP anterior
- Se establece el nuevo EBP para el stack frame actual
- Se asigna espacio para variables locales decrementando ESP
- Al retornar,
retextrae la dirección de retorno del stack y salta a ella
C to x86
Un programa C con argumentos en main:
int main ( int argc, char** argv)
program.exe -o output.txtTendrá:
argc = 3( programa, “-o”, “output.txt”)argv = [dirección de "program.exe", dirección de "-o", dirección de "output.txt"]
Remember #1
mov [ebp+myvar], somevalue — myvar es un offset (-4, -8, -C…) desde EBP a la dirección del stack donde se almacena myvar. Esto asigna el valor somevalue a myvar.
Remember #2
function ( a,b,c) : push c; push b; push a; — Los parámetros de función se pushean en orden inverso.
Remember #3
mov eax, [ebp+argv] — argv es un offset desde EBP donde se almacena la dirección de argv
mov ecx, [eax+4*i] — esto significa “establece ecx igual a argv[i]“
Interrupt Handler
En lenguaje assembly a menudo veremos estructuras de código así:
mov eax, 4 ;system call number ( syswrite)
int 0x80 ;interrupt handler
O:
mov eax, 1 ;system call number ( sysexit)
int 0x80 ;interrupt handler
Estas llamadas hacen que el programa ceda el control de ejecución al kernel.
Bibliografía
- The Art of Assembly Language — Randall Hyde
- Fundamentos de ensamblador — Tecnología de computadores UAH
- Practical Malware Analysis — Michael Sikorski and Andrew Honig
- https://www.begin.re/x86-overview — Ophir Harpaz
- Eduardo Blázquez — Reversing, técnicas de ingeniería inversa [RootedCON 2018 - ESP]