Tengo la impresión de que algunas personas que hacen ingeniería inversa habitualmente conocen bien la CPU pero no tanto la x87 FPU, MMX, SSEs, AVX y sus posibles aplicaciones. Por eso en esta serie de artículos vamos a explicar como funciona todo esto en x86 y x86_64.
Además se explicará como jugar con todo esto desde el sistema operativo Windows y desde la perspectiva de un depurador.
x86 (current):
En caso de que todo vaya bien reservaremos la memoria para el CONTEXT y lo inicializaremos:
Además se explicará como jugar con todo esto desde el sistema operativo Windows y desde la perspectiva de un depurador.
Introducción:
Ojeando un poco de la wikipedia sobre las extensiones multimedia:x86 (current):
- MMX (1996)
- 3DNow! (1998)
- Streaming SIMD Extensions (SSE) (1999)
- SSE2 (2001)
- SSE3 (2004)
- Supplemental SSE3 (SSSE3) (2006)
- SSE4 (2006)
- SSE5 (2007)
- Advanced Encryption Standard (AES) (2008)
- Advanced Vector Extensions (AVX) (2008)
- F16C (2009 (AMD), 2011 (Intel))
- XOP (2009)
- FMA instructions (FMA4: 2011, FMA3: 2012 (AMD), 2013 (Intel))
- Bit manipulation instructions (ABM: 2007, BMI1: 2012, BMI2: 2013, TBM: 2012)
- AVX-512 (2015)
Para esta serie de artículos nos vamos a centrar de momento solo en:
x87: es un subconjunto de coma flotante del conjunto de instrucciones de la arquitectura x86. Se originó como una extensión del conjunto de instrucciones del 8086 en la forma de un coprocesador opcional de coma flotante que trabajó en paralelo con el correspondiente CPU x86. Las instrucciones x87 no son estrictamente necesarias para construir programas funcionales, pero proporcionan implementaciones de hardware y microcódigo de tareas numéricas comunes, permitiendo a estas tareas desempeñarse mucho más rápido que las rutinas correspondientes en código máquina. El conjunto de instrucciones x87 incluye instrucciones para operaciones de coma flotante básicas tales como: adición, sustracción y comparación. Así como también para operaciones numéricas más complejas como por ejemplo el cálculo de la función tangente y su inversa. Desde el Intel 80486, la mayoría de los procesadores x86 han tenido estas instrucciones x87 implementadas en el propio CPU. Los registros principales de x87 son un total de 8 registros desde el ST(0) hasta el ST(7). Se puede acceder directamente a estos registros usando un desplazamiento relativo al tope de la pila, así como también se permiten apilados y desapilados en la pila. x87 proporciona aritmética de coma flotante binaria de simple precisión, doble precisión y precisión extendida de 80 bits según el estándar IEEE 754-1985. Por defecto, todos los procesadores x87 usan internamente la precisión extendida de 80 bits.
MMX: es un Conjunto de instrucciones SIMD (Single Instruction, Multiple Data) diseñado por Intel e introducido en 1997 en sus microprocesadores Pentium con tecnología MMX. Fue desarrollado a partir de un set introducido en el Intel i860. Ha sido soportado por la mayoría de fabricantes de microprocesadores x86 desde entonces. MMX define 8 registros desde el MM0 hasta el MM7. Para evitar problemas de compatibilidad con los dispositivos de conmutación contexto en los sistemas operativos existentes, estos registros eran los alias de los registros de pila FPU x87 existentes. Por lo tanto, cualquier operación con los registros que se haga en la FPU x87 también podrá afectar a los registros MMX y viceversa. Sin embargo, a diferencia de los registros internos x87, los registros MMX son directamente accesibles. Cada registro MMX es de 64 bits.
SSE (Streaming SIMD Extensions): es una extensión al grupo de instrucciones MMX para procesadores Pentium III, introducida por Intel en febrero de 1999. Las instrucciones SSE son especialmente adecuadas para decodificación de MPEG2, que es el códec utilizado normalmente en los DVD, procesamiento de gráficos tridimensionales y software de reconocimiento de voz. Con la tecnología SSE, los microprocesadores x86 fueron dotados de setenta nuevas instrucciones y de ocho registros nuevos: del XMM0 al XMM7. Estos registros tienen una extensión de 128 bits. Las extensiones AMD64 de AMD (llamado originalmente x86-64) añaden otros ocho registros desde el XMM8 al XMM15. También hay un nuevo registro de control y estado de 32 bits llamado MXCSR. Los registros del XMM8 al XMM15 son accesibles sólo en modo de 64 bits.
AVX: (extensiones vectoriales avanzadas) es un juego de instrucciones de 256 bits desarrollado por Intel Corporation como una extensión al conjunto de instrucciones x86 utilizado en procesadores de Intel y AMD . Provee nuevas características, instrucciones y un nuevo esquema de codificación. El ancho del registro SIMD es incrementado de 128-bits a 256-bits, y renombrado de XMM0-XMM15 a YMM0-YMM15. En los procesadores que soportan AVX, el juego de instrucciones SSE (que anteriormente operaba en los registros XMM) pasa a operar en los primeros 128-bits de los registros YMM. Los registros del YMM8 al YMM15 son accesibles solo en modo de 64 bits. Además, se espera la inclusión de vectores de 512 (AVX-512) e incluso 1024 bits.
x87 FPU conceptos básicos:
En mi opinión la forma más sencilla de entender la pila de la x87 FPU es pensar en el cañón del revólver. Los registros ST0-ST7 serían los huecos donde se alojan las balas y las balas serían el contenido de los registros.
El registro interno de la FPU que se encuentre ese momento listo para ser disparado sería el registro ST0 y los demás serían relativos a ésta posición (el TOP de la pila). Hemos hecho una imagen que muestra esta idea y con un incremento y un decremento de la pila x87:
A la hora de programar el soporte básico x87 FPU para el x64dbg se me ocurrio que mostrar la representación interna de los registros de la FPU era buena idea para saber en todo momento a qué registro interno corresponde cada ST, el Ollydbg muestra los registros en referencia a su ST, vamos con un par de imágenes de lo que quiero decir:
Como se puede apreciar en el x64dbg se puede ver la relación directa entre los registros internos de la FPU x87r* y que ST tiene asignado en ese momento y se puede ver de una manera más clara (en mi opinión) cosas como que la pila se ha movido de un primer vistazo. Además tiene la ventaja que un registro MMX pertenece siempre a un x87r* correspondiente, por ejemplo la parte baja del registro interno de la FPU x87r0 siempre será MM0, x87r1 siempre será MM1, etc.
Además de los 8 registros de 80 bits en la x87 FPU también se encuentran tres registros de 16 bits: Control Word, Status Word y Tag Word. En este apartado de conceptos básicos vamos a explicar que es cada uno de estos registros y sus bits correspondientes.
Control Word: se utiliza para seleccionar entre los distintos modos de cálculo disponible en la FPU y para definir qué excepciones deben ser manejadas por la FPU o por un manejador propio de excepciones.
A continuación vamos a explicar todos los campos:
El campo X (tambien se puede encontrar como IC) corresponde al bit 12, se le denomina Infinity Control y permite dos tipos de aritmética infinita los valores que puede tener son:
- 0: -infinity y +infinity son tratados como "unsigned infinity" (estado inicializado).
- 1: -infinity y +infinity
Nota: este campo se mantiene para guardar la compatibilidad con 287 y las FPU anteriores. En las FPU "modernas" este bit se ignora.
El campo RC corresponde a los bits 10 y 11, se le denomina Round Control y sirve para indicar como debe la FPU redondear los resultados. Los valores que puede tener son:
- 00: Redondeo al más cercano (or to even if equidistant). Este es el estado inicializado.
- 01: Redondeo al infinito negativo (toward -infinity)
- 10: Redondeo al infinito positivo (toward +infinity)
- 11: Truncar (toward 0)
El campo PC corresponde a los bits 8 y 9, se le denomina Precision Control y sirve para determinar a qué precision se deben redondear los resultados después de cada instrucción aritmética. Los valores que puede tener son:
- 00: 24 bits (REAL4)
- 01: Not used
- 10: 53 bits (REAL8)
- 11: 64 bits (REAL10). Este es el estado inicializado.
El campo IEM corresponde al bit 7, se le denomina Interrupt Enable Mask y sirve para determinar si alguna de las "interrupt masks" se activarán o no. Este es otro de los bits que se guardan por compatibilidad con las FPUs antiguas y no se usa. Los valores que puede tener son:
- 1: Las "interrupt masks" están activadas. Este también es el estado inicializado.
- 0: Todas las "interrupt masks" están desactivadas.
Los bits que van del 0 al 5 son las llamadas "interrupt masks". En el estado inicializado todas valen 1, esto quiere decir que la FPU manejará todas las excepciones. Cuando alguno de estos bits tiene el valor 0 la FPU generará una interrupción cuando la excepción se detecta y el programa podrá ejecutar el código deseado antes de devolver el control a la FPU. De momento no vamos a entrar en más detalles sobre este tema. Las "interrupt masks" que hay son:
- IM (bit 0) también llamado Invalid operation Mask.
- DM (bit 1) también llamado Denormalized operand Mask.
- ZM (bit 2) también llamado Zero divide Mask.
- OM (bit 3) también llamado Overflow Mask.
- UM (bit 4) también llamado Underflow Mask.
- PM (bit 5) también llamado Precision Mask.
Status Word: almacena el estado actual de la FPU. Así que muchas de de las instrucciones van modificando los bits de éste registro al igual que ocurre con los FLAGS de la CPU.
A continuación vamos a explicar los campos de este registro:
El campo B corresponde al bit 15 y se pone a 1 cuando la FPU está ocupada o a 0 si está en espera (idle).
Los campos C3 (bit 14), C2 (bit 10), C1 (bit 9), C0 (bit 8) son flags condicionales que se usan por ejemplo para comprobar el resultado de una comparación. De momento no se explicará más sobre esto.
El campo TOP corresponde a los bits 11, 12 y 13 y almacena que registro interno de la FPU es el ST0 actual, es decir qué registro interno está en el TOP de la pila.
Nota: un ejemplo del uso y explicación de este campo se puede encontrar en el apartado "Obteniendo y entendiendo el CONTEXT en Windows: I".
El campo ES también llamado IR o Interrupt Request corresponde al bit 7 y tiene el valor 1 si una excepción está siendo manejada y tiene el valor 0 cuando se ha acabado de manejar.
Los bits del 0 al 6 son flags que son manejados por la FPU cuando detecta una excepción.
El campo SF corresponde al bit 6, tambien es llamado Stack Fault Flag y se pone a 1 cuando se intenta cargar un valor en un registro que no está libre.
El campo PE corresponde al bit 5, también es llamado Precision Exception Flag y se pone a 1 cuando la precisión se ha perdido en alguna instrucción aritmética.
El campo UE corresponde al bit 4, también es llamado Underflow Exception Flag y se pone a 1 cuando un valor es demasiado pequeño para poder ser representado.
El campo OE corresponde al bit 3, también es llamado Overflow Exception Flag y se pone a 1 cuando un valor es demasiado grande para poder se representado.
El campo ZE corresponde al bit 2, también es llamado Zero Divide Exception Flag y se pone a 1 cuando se intenta dividir por 0.
El campo DE corresponde al bit 1, también es llamado Denormalized Exception Flag y se pone a 1 cuando se intenta operar en un "denormalized number" o el resultado de una operación es un "denormalized number".
El campo IE corresponde al bit 0, también es llamado Invalid Operation Exception Flag se pone a 1 cuando se realiza una operación inválida para FPU.
Tag Word: contiene información sobre cada registro de 80 bits de la FPU. Hay 3 bits para indicar el estado de cada registro de 80 bits de la FPU. Y estos corresponden a la posición fija del registro NO al ST(n) actual. Es decir TAG(0) siempre se referirá al registro interno 0 de la FPU independientemente de que ese registro sea actualmente ST0 o ST5.
Cada TAG puede tener los siguientes valores:
- 00: También llamado valid: el registro contiene un valor que no es 0.
- 01: También llamado zero: el registro contiene un valor 0.
- 10: También llamado special: el registro contiene un valor especial (NAN, infinity...).
- 11: Tambien llamado empty: el registro está vacío.
MxCsr
Este registro de 32 bits tiene una función parecida al registro Control Word y al Status Word de la x87 FPU: mantiene toda la información de enmascaramiento y flags para su uso con operaciones de coma flotante SSE. Los bits del 16 al 31 están reservados y causarán una excepción #GP si se intentan activar.
A continuación vamos a explicar los campos del MxCsr:
El campo FZ también llamado Flush to Zero corresponde al bit 15 activa el modo en el que todas las operaciones donde ocurra un underflow valdrán 0. Esto hace que el tiempo de procesado sea más rápido pero se pierde precisión.
El campo RC corresponde a los bits 13 y 14, se le denomina Round Control y sirve para indicar como deben redondear los resultados. Los valores que puede tener son:
- 00: Redondeo al más cercano
- 01: Redondeo al infinito negativo (toward -infinity)
- 10: Redondeo al infinito positivo (toward +infinity)
- 11: Redondea a 0 (toward 0)
Los bits que van del 7 al 12 son las llamadas "interrupt masks". En el estado inicializado todas valen 1, esto quiere decir que el programa no manejará las excepciones. Cuando alguno de estos bits tiene el valor 0 se generará una interrupción cuando la excepción se detecta y el programa podrá hacer lo necesario antes de devolver el control. En esta parte del post no vamos a entrar en más detalles sobre este tema. Las "interrupt masks" son:
- IM (bit 7) también llamado Invalid Operation Mask.
- DM (bit 8) también llamado Denormalized Operand Mask.
- ZM (bit 9) también llamado Zero Divide Mask.
- OM (bit 10) también llamado Overflow Mask.
- UM (bit 11) también llamado Underflow Mask.
- PM (bit 12) también llamado Precision Mask.
El campo DAZ corresponde al bit 6, también es llamado Denormals Are Zeros, este campo no estaba disponible en la primera versión de SSE. Al igual que el modo Flush To Zero (FZ) el modo DAZ es más rápido pero también pierde precisión. Si el bit 6 está activado el modo DAZ es soportado.
El campo PE corresponde al bit 5, también es llamado Precision Exception Flag y se pone a 1 cuando la precisión se ha perdido en alguna instrucción aritmética.
El campo UE corresponde al bit 4, también es llamado Underflow Exception Flag y se pone a 1 cuando un valor es demasiado pequeño para poder ser representado.
El campo OE corresponde al bit 3, también es llamado Overflow Exception Flag y se pone a 1 cuando un valor es demasiado grande para poder ser representado.
El campo ZE corresponde al bit 2, también es llamado Zero Divide Exception Flag y se pone a 1 cuando se intenta dividir por 0.
El campo DE corresponde al bit 1, también es llamado Denormalized Exception Flag y se pone a 1 cuando se intenta operar en un "denormalized number" o el resultado de una operación es un "denormalized number".
El campo IE corresponde al bit 0, también es llamado Invalid Operation Exception Flag se pone a 1 cuando se realiza una operación inválida para FPU.
Obteniendo y entendiendo el CONTEXT en Windows: I
Para el código de esta parte del artículo voy a basarme en el soporte FPU XMM, MMX, x87 & AVX programado para el x64dbg (An open-source x64/x32 debugger for windows). Actualmente x64dbg usa por debajo como debugger el proyecto Open Source Titanengine Community Edition y es en este donde realmente se encuentra la parte de código que nos interesa.
Para obtener el contexto de un hilo en Windows se usa la API GetThreadContext:
BOOL WINAPI GetThreadContext( _In_ HANDLE hThread, _Inout_ LPCONTEXT lpContext );
Para simplificar usaremos un CONTEXT_ALL para obtener toda la información del hilo. Hay que suspender el hilo antes de obtener el contexto con la API SuspendThread, un ejemplo de código sin comprobar errores de las llamadas a las APIs podría ser:
SuspendThread(hThread); CONTEXT Context; memset(&Context, 0, sizeof(Context)); DBGContext.ContextFlags = CONTEXT_ALL; GetThreadContext(hThread, &Context); ResumeThread(hThread);
Nota: Para acceder a Intel AVX hay otras APIs.
CONTEXT_ALL tiene la siguiente pinta (recomiendo leer en la msdn más sobre el tema si se tienen dudas):
#define CONTEXT_ALL (CONTEXT_CONTROL | CONTEXT_INTEGER | CONTEXT_SEGMENTS | CONTEXT_FLOATING_POINT | CONTEXT_DEBUG_REGISTERS | CONTEXT_EXTENDED_REGISTERS)
Para saber si una característica está disponible se puede usar la API IsProcessorFeaturePresent con la característica a comprobar. Si retorna 0 la característica no está disponible:
- PF_MMX_INSTRUCTIONS_AVAILABLE: disponible MMX.
- PF_XMMI_INSTRUCTIONS_AVAILABLE: disponible XMM.
- PF_XMMI64_INSTRUCTIONS_AVAILABLE: set de instrucciones SSE2 disponible.
- PF_SSE3_INSTRUCTIONS_AVAILABLE: set de instrucciones SSE3 disponible (Característica no disponible en Windows Server 2003 and Windows XP/2000).
- PF_3DNOW_INSTRUCTIONS_AVAILABLE está disponible 3DNOW.
- PF_XSAVE_ENABLED el procesador implementa las instrucciones XSAVE y XRSTOR.
Para obtener los registros MMX de la estructura CONTEXT de Windows hay que tener en cuenta que vienen desordenados y además vienen dentro de los registros x87 (80 bits). Así que hay acceder a la parte baja del registro x87 para obtener los 64 bits de cada registro MMX.
El orden en el que vienen los registros MMX es el que hay en la pila FPU x87 en el momento de obtener el CONTEXT. Para calcular a qué MMX se está accediendo es necesario obtener el campo TOP del registro StatusWord de x87.
El campo TOP ocupa 3 bits y corresponde a las posiciones 11, 12 y 13 y almacena qué registro x87 se encuentra primero en la pila, por ejemplo si el TOP es 3, quiere decir que en el TOP de la pila está el registro 3 de x87, así que el registro MMX que obtendremos en la parte baja del primer registro x87 del stack será MM3.
De tal forma que los registros MMX vendrían en la estructura CONTEXT en el siguiente orden: MM3, MM4, MM5, MM6, MM7, MM0, MM1 & MM2. Como los registros ST vienen siempre en orden de ST0 a ST7, en este caso, la correspondencia de los registros ST con los MMX sería: ST0: MM3, ST1: MM4, ST2: MM5, ST3: MM6, ST4: MM7, ST5: MM0, ST6: MM1 & ST7: MM2.
El principal problema es que en las versiones de 32 bits de Windows se accede a través del campo FloatSave y ExtendedRegisters de la estructura CONTEXT y en 64 bits se debe usar el campo
FltSave.
Además Microsoft a partir de Windows 7 SP1 ha añadido una nueva forma de acceder al contexto para obtener los registros AVX: XState functions.
A continuación vamos a explicar como acceder a los diferentes CONTEXT:
Para obtener los registros AVX YMM de 256 bits será neceserario obtener las funciones en tiempo de ejecución con GetModuleHandle y GetProcAddress ya que no existe SDK para Windows 7 con SP1. Para ello lo primero será crear los punteros a funciones e inicializarlos al valor correspondiente. Hay que tener en cuenta que esto solo funcionará desde Windows 7 SP1:
typedef DWORD64 (WINAPI *PGETENABLEDXSTATEFEATURES)(); typedef BOOL (WINAPI *PINITIALIZECONTEXT)(PVOID Buffer, DWORD ContextFlags, PCONTEXT* Context, PDWORD ContextLength); typedef BOOL (WINAPI *PGETXSTATEFEATURESMASK)(PCONTEXT Context, PDWORD64 FeatureMask); typedef PVOID (WINAPI *LOCATEXSTATEFEATURE)(PCONTEXT Context, DWORD FeatureId, PDWORD Length); typedef BOOL (WINAPI *SETXSTATEFEATURESMASK)(PCONTEXT Context, DWORD64 FeatureMask); PGETENABLEDXSTATEFEATURES pGetEnabledXStateFeatures; PINITIALIZECONTEXT pInitializeContext; PGETXSTATEFEATURESMASK pGetXStateFeaturesMask; LOCATEXSTATEFEATURE pLocateXStateFeature; SETXSTATEFEATURESMASK pSetXStateFeaturesMask; kernel32h = GetModuleHandleA("kernel32.dll"); pGetEnabledXStateFeatures = (PGETENABLEDXSTATEFEATURES)GetProcAddress(kernel32h, "GetEnabledXStateFeatures"); pInitializeContext = (PINITIALIZECONTEXT)GetProcAddress(kernel32h, "InitializeContext"); pGetXStateFeaturesMask = (PGETXSTATEFEATURESMASK)GetProcAddress(kernel32h, "GetXStateFeaturesMask"); pLocateXStateFeature = (LOCATEXSTATEFEATURE)GetProcAddress(kernel32h, "LocateXStateFeature"); pSetXStateFeaturesMask = (SETXSTATEFEATURESMASK)GetProcAddress(kernel32h, "SetXStateFeaturesMask");
Otro de los problemas que se nos presentan es que el valor para CONTEXT_XSTATE cambió de Windows 7 a Windows 7 SP1. Así que para evitar problemas si usamos un SDK u otro añadimos el código directamente, además también añadiremos otras constantes que pueden no estar presentes en nuestro SDK y necesitaremos XSTATE_AVX y XSTATE_MASK_AVX:
#undef CONTEXT_XSTATE #if defined(_M_X64) #define CONTEXT_XSTATE (0x00100040) #else #define CONTEXT_XSTATE (0x00010040) #endif #define XSTATE_AVX (XSTATE_GSSE) #define XSTATE_MASK_AVX (XSTATE_MASK_GSSE)
Una vez obtenidos los punteros a las funciones que usaremos en tiempo de ejecución y hemos comprobado que hemos podido obtener todas, es el momento de obtener el CONTEXT. Para ello primero necesitamos saber el tamaño llamando a la API InitializeContext de la siguiente manera:
DWORD ContextSize = 0; BOOL Success = pInitializeContext(NULL, CONTEXT_ALL | CONTEXT_XSTATE, NULL, &ContextSize);
Si ha ocurrido algún error obtendremos en Success un valor TRUE o el Last Error será
ERROR_INSUFFICIENT_BUFFER.
En caso de que todo vaya bien reservaremos la memoria para el CONTEXT y lo inicializaremos:
PCONTEXT Context; void * buffer = malloc(ContextSize); if buffer != NULL) { Success = pInitializeContext(buffer, CONTEXT_ALL | CONTEXT_XSTATE, &Context, &ContextSize); }
Si todo ha ido bien Success tendrá el valor TRUE y será necesario llamar a SetXStateFeaturesMask. Antes de obtener el contexto de un hilo con la API GetThreadContext (según Microsoft) debemos usar SetXStateFeaturesMask para verificar que tipo de funcionalidad queremos obtener o cambiar.
Success = pfnSetXStateFeaturesMask(Context, XSTATE_MASK_AVX);
Si todo ha ido bien Success tendrá el valor TRUE y será necesario suspender el hilo con la API SuspendThread y obtener el contexto con la API GetThreadContext:
if ( SuspendThread(hThread) != (DWORD) -1) { Success = GetThreadContext(hThread, Context); }
Si todo ha ido bien Success tendrá el valor TRUE y será necesario llamar a la API GetXStateFeaturesMask para obtener la máscara de las características válidas en el contexto especificado.
Hay que tener en cuenta que si un bit en particular no está activado quiere decir que se está en un estado inicializado y el contenido obtenido con la API LocateXStateFeature no está definido.
DWORD64 FeatureMask; Success = pGetXStateFeaturesMask(Context, &FeatureMask);
Si todo ha ido bien Success tendrá el valor TRUE y ya solo tenemos que llamar a la API LocateXStateFeature para obtener los registros YMM. Para saber si estamos en 32 bits (podemos obtener del registro YMM0 al YMM7) o en 64 bits (podemos obtener del registro YMM0 al YMM15) usaremos el tercer parámetro de la API para obtener el tamaño:
PM128A Xmm; PM128A Ymm; DWORD FeatureLength; Xmm = (PM128A)pLocateXStateFeature(Context, XSTATE_LEGACY_SSE, &FeatureLength); Ymm = (PM128A)pLocateXStateFeature(Context, XSTATE_AVX, NULL);
Y ahora con un simple
for (i = 0; i < (FeatureLength / sizeof(*Ymm)); i++)
Podemos acceder a la parte alta y baja de los primeros 128 bits de YMM y a la parte alta y baja de los últimos 128 bits de YMM que corresponden con los registros XMM:
Ymm[Index].High Ymm[Index].Low Xmm[Index].High Xmm[Index].Low
Actualmente el ollydbg no tiene soporte para Intel AVX, así que se puede ir usando la versión básica del soporte AVX del x64dbg:
A continuación vamos a ver como obtener el CONTEXT de x87 FPU, MMX, SSE, XMM en las versiones de 32 bits y de 64 bits:
Win32: La estructura CONTEXT en Win32 tiene la siguiente pinta:
typedef struct _CONTEXT { ... // This section is specified/returned if the // ContextFlags word contians the flag CONTEXT_FLOATING_POINT. FLOATING_SAVE_AREA FloatSave; ... // This section is specified/returned if the ContextFlags word // contains the flag CONTEXT_EXTENDED_REGISTERS. // The format and contexts are processor specific BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; #define SIZE_OF_80387_REGISTERS 80 typedef struct _FLOATING_SAVE_AREA { DWORD ControlWord; DWORD StatusWord; DWORD TagWord; DWORD ErrorOffset; DWORD ErrorSelector; DWORD DataOffset; DWORD DataSelector; BYTE RegisterArea[SIZE_OF_80387_REGISTERS]; DWORD Cr0NpxState; } FLOATING_SAVE_AREA;
Acceder a los 3 registros de 16 bits de la x87 FPU:
ControlWord: Context.FloatSave.ControlWord; StatusWord: Context.FloatSave.StatusWord; TagWord: Context.FloatSave.TagWord;
Para obtener los registros ST0-ST7 (80 bytes ocupan todos los registros) se usa el campo Context.FloatSave.RegisterArea:
BYTE RegisterArea[80]; memcpy(RegisterArea, Context.FloatSave.RegisterArea, 80);
Obtener MMX desordenados:
uint64_t mmx[8]; for(i = 0; i < 8; i++) { memcpy( &(mmx[i]), & (Context.FloatSave.RegisterArea[i*10]), sizeof(mmx[i])); }
MxCsr: el registro MxCsr se encuentra en Context.ExtendedRegisters[24]:
DWORD MxCsr; memcpy(&MxCsr, & (Context.ExtendedRegisters[24]), sizeof(MxCsr));
XMM: Para acceder a los 8 registros XMM disponibles en 32 bits se usa el campo ExtendedRegisters de la siguiente forma Context.ExtendedRegisters[(10 + indice) * 16]:
M128A XmmRegisters[8]; for(i = 0; i < 8; i++) { memcpy(&(XmmRegisters[i]), & (Context.ExtendedRegisters[(10 + i) * 16]), 16); }
Win64: La estructura CONTEXT en Win64 y otras estructuras que nos interesan tienen la siguiente pinta:
typedef struct DECLSPEC_ALIGN(16) _CONTEXT { ... // Floating point state. union { XMM_SAVE_AREA32 FltSave; struct { M128A Header[2]; M128A Legacy[8]; M128A Xmm0; M128A Xmm1; M128A Xmm2; M128A Xmm3; M128A Xmm4; M128A Xmm5; M128A Xmm6; M128A Xmm7; M128A Xmm8; M128A Xmm9; M128A Xmm10; M128A Xmm11; M128A Xmm12; M128A Xmm13 M128A Xmm14; M128A Xmm15; } DUMMYSTRUCTNAME; } DUMMYUNIONNAME; // Vector registers. M128A VectorRegister[26]; DWORD64 VectorControl; ... } typedef XSAVE_FORMAT XMM_SAVE_AREA32, *PXMM_SAVE_AREA32; // Format of data for (F)XSAVE/(F)XRSTOR instruction typedef struct DECLSPEC_ALIGN(16) _XSAVE_FORMAT { WORD ControlWord; WORD StatusWord; BYTE TagWord; BYTE Reserved1; WORD ErrorOpcode; DWORD ErrorOffset; WORD ErrorSelector; WORD Reserved2; DWORD DataOffset; WORD DataSelector; WORD Reserved3; DWORD MxCsr; DWORD MxCsr_Mask; M128A FloatRegisters[8]; #if defined(_WIN64) M128A XmmRegisters[16]; BYTE Reserved4[96]; #else M128A XmmRegisters[8]; BYTE Reserved4[192]; ... }
Acceder a los 3 registros de 16 bits de la x87 FPU:
ControlWord: Context.FltSave.ControlWord; StatusWord: Context.FltSave.StatusWord; TagWord: Context.FltSave.TagWord;
Para obtener los registros ST0-ST7 (80 bytes ocupan todos los registros) se debe obtener la parte baja (10 bytes) de cada entrada de 128 bits del campo Context.FltSave.FloatRegisters:
BYTE RegisterArea[80]; for(i = 0; i < 8; i++) { memcpy(&(RegisterArea[i * 10]), & (Context.FltSave.FloatRegisters[i]), 10); }
Para obtener los MMX desordenados se debe obtener la parte baja (64 bits) de cada entrada de 128 bits (M128A FloatRegisters[8]) del campo Context.FltSave.FloatRegisters:
uint64_t mmx[8]; for(i = 0; i < 8; i++) { memcpy(&(MMX[i]), & (Context.FltSave.FloatRegisters[i]), sizeof(MMX[i])); }
MxCsr:
DWORD MxCsr = Context.FltSave.MxCsr;
XMM: acceder a los 16 registros XMM disponibles en 64 bits es mucho más fácil, se debe usar el campo Context.FltSave.XmmRegisters.
M128A XmmRegisters[16]; for(i = 0; i < 16; i++) { memcpy(& (XmmRegisters[i]), & (Context.FltSave.XmmRegisters[i]), 16); }
Petición & Agradecimientos
En primer lugar, gracias al equipo de Security By Default y Buguroo por su apoyo y en segundo lugar me gustaría que la gente que haya trabajado y/o experimentado se animara y escribiera más sobre este tema.
David Reguera Garcia aka Dreg @fr33project - Senior Malware Analyst en Buguroo
Revolver barrel images by Jorge Martinez Taboada @jmtaboada - Senior UX Consultant en Buguroo
Revolver barrel images by Jorge Martinez Taboada @jmtaboada - Senior UX Consultant en Buguroo
4 comments :
Menudo artículo de calidad! Enhorabuena! ... esperando ansioso el siguiente!!
Vaya articulazo, David es puro talento. Es un placer poder trabajar con él! Enhorabuena por el curro que te has pegado con este post! :-)
muy bueno. felicitaciones
Con ganas de mas!
Publicar un comentario