06 - Ejecutables y herramientas para Windows

El formato PE.

El formato utilizado para los ejecutables y librerías compartidas (DLLs) en windows se llama Portable Executable. En esencia, al igual que los ELF para Linux, su objetivo es contener todo lo necesario para que el loader del sistema operativo pueda subir el ejecutable a memoria y ponerlo a correr. Eso incluye, entre otras cosas, las secciones de text y datos e información sobre las librerías externas y funciones externas de las que depende el ejecutable. Al igual que en linux lo común es que los ejecutables sean dynamically linked.

Las primeras partes de un PE sirven como headers y metadata para el resto del contenido del ejectable. Una manera de entender el contenido de los headers de los PE es saber que cada posición en el archivo corresponde a algún data member de una estructura que a su vez representa alguna pieza de información sobre ejecutable, e.g. su magic number, su cantidad de secciones, la dirección del entry point. Por ejemplo, he aquí los primeros bytes de un PE:

(BTW encontré un hex editor online muy bueno: https://hexed.it/)

Según el formato PEs los primeros bytes corresponden a los campos de la siguiente estructura (llamada DOS header).

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;        // Magic number
    WORD   e_cblp;         // Bytes on last page of file
    WORD   e_cp;           // Pages in file
    WORD   e_crlc;         // Relocations
    WORD   e_cparhdr;      // Size of header in paragraphs
    WORD   e_minalloc;     // Minimum extra paragraphs needed
    WORD   e_maxalloc;     // Maximum extra paragraphs needed
    WORD   e_ss;           // Initial (relative) SS value
    WORD   e_sp;           // Initial SP value
    WORD   e_csum;         // Checksum
    WORD   e_ip;           // Initial IP value
    WORD   e_cs;           // Initial (relative) CS value
    WORD   e_lfarlc;       // File address of relocation table
    WORD   e_ovno;         // Overlay number
    WORD   e_res[4];       // Reserved words
    WORD   e_oemid;        // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;      // OEM information; e_oemid specific
    WORD   e_res2[10];     // Reserved words
    LONG   e_lfanew;       // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

Eso quiere decir, que para el ejemplo, algunos de los valores son.

e_magic = 0x5A4D 
e_cblp = 0x0090
e_cp = 0x0003
...
...
e_lfanew = 0x00000080

No es necesario entender cada uno de esos campos, pero si necesitaras puedes consultar fuentes como https://0xrick.github.io/win-internals/pe3/.

El último campo de esa estructura (e_lfanew) indica dónde comienza la próxima estructura importante. En nuestro ejemplo, su valor de 0x00000080 indica que el primer byte de la próxima estructura (IMAGE_NT_HEADERS) está en la posición 0x80 del archivo.

struct IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} 

Así que el Signature de nuestro file es 0x00004550, etc. Nota que algunas de las estructuras (como IMAGE_NT_HEADERS) contienen data members que a su vez son estructuras! Por ejemplo, el data member FileHeader es de tipo IMAGE_FILE_HEADER que es una estructura así:

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;              // 0x014c
  WORD  NumberOfSections;     // 0x0003
  DWORD TimeDateStamp;        // 0x4d81fa8d
  DWORD PointerToSymbolTable; // 0x00000000
  DWORD NumberOfSymbols;      // 0x00000000
  WORD  SizeOfOptionalHeader; // 0x000e
  WORD  Characteristics;      // 0x0102
} 

(Le puse como comentario los valores para el ejemplo)

El próximo data member de la struct IMAGE_NT_HEADERS también es una estructura llamada IMAGE_OPTIONAL_HEADER:

struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  ...
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
}

Y al final de ese data member hay un arreglo llamado DataDirectory que contiene los headers y punteros a las secciones del ejecutable como text y data. Si te sientes con ganas de estudiar los detalles de las diversas partes de un PE puedes hacerlo en la ventana Program Trees de ghydra.

¿Has programado alguna vez usando el API de Windows?

Si eres como yo, un hater de windows declarado es posible que (1) nunca hayas programado una aplicación de consola o GUI en Windows, (2) no lo quieras admitir. Sin embargo, si te vas a convertir en un reverse engineer capaz de entender ejecutables PE, es imprescindible que conozcas algunas funciones básicas o al menos que le pierdas el miedo a buscar referencia sobre ellas en el web.

Windows provee servicios a través de funciones que expone en sus dynamic linked libraries (DLLs). Cuando una aplicación del usuario necesita usar algun servicio del sistema operativo, lo hace invocando una de esas funciones del API de Windows, la cual a su vez invoca algun system service routine (que son las que corren en el kernel space, con provilegios de acceso a al hardware)

Imagen del libro Learning Malware Analysis.

Veamos un PE en Ghidra y/o en IDA Free! Comenzaremos con un PE de un programa para la consola (terminal).

Descarga este file. Descomprime usando passwd: 4702c32.

C para Windows

Según trato de resolver los ejercicios de reverse engineering me doy cuenta que la mejor forma de prepararse (aparte de la práctica) es familiarizarse con la creación de las aplicaciones. Por ejemplo, me resulta difícil tratar de hacer reverse engineering a una aplicación de Win32 si no recuerdo cómo se estructuran esas aplicaciones.

Las aplicaciones que están creadas para el comand line, lucen iguales que las que hemos estado trabajando para Linux. Sin embargo, dependiendo del compilador que uses, la secuencia de funciones que observas antes de llegar a main puede variar.

Las que son para GUI, son otro cantar...

¿Te hace falta el main?

Ghidra no es tan eficiente como otras herramientas identificando la función main cuando su símbolo ha sido borrado. Si no lo encuentras, algunas cosas que puedes tratar:

Recomendación, una vez deduzcas a main o WinMain (o cualquier otra función interesante) cambia su nombre usando right click -> Function -> Rename Function!

Programación dirigida por eventos (event-drive programming)

eventDrivenLoop.png

Figura tomada de https://courses.cs.washington.edu/courses/cse331/13sp/lectures/lect24-events.pdf

Los siguientes ejemplos de un programa en Win32 vienen de este tutorial

Al programar para Win32, una de las primeras cosas que debe hacer un programa es establecer la función que procesará los eventos de la aplicación.

El siguiente es un trozo de la función WinMain:

WNDCLASSEX wcx; // WINDOW class information

wcx.style = CS_HREDRAW|CS_VREDRAW |CS_DBLCLKS ; // Class styles
wcx.lpfnWndProc = (WNDPROC)MainWndProc;        // Pointer to the callback procedure

Cada control (ventana, botón, etc.) es creado usando una función que establece algunas de sus propiedades (posición, nombre, padre).

// Create the window
hwndMain = CreateWindowEx(0, //Extended window style
                          "Lesson2", // Window class name
                          "Lesson 2 - A simple win32 application", // Window title
                          WS_OVERLAPPEDWINDOW, // Window style
                          CW_USEDEFAULT,CW_USEDEFAULT, // (x,y) pos of the window
                          CW_USEDEFAULT,CW_USEDEFAULT, // Width and height of the window
                          HWND_DESKTOP, // HWND of the parent window (can be null also)
                          NULL, // Handle to menu
                          hInstance, // Handle to application instance
                          NULL); // Pointer to window creation data

La función que hemos asignado para que responda a los mensajes se llama MainWndProc. Típicamente consiste de un switch con case's para cada uno de los mensajes a los que deseamos responder.

LRESULT CALLBACK MainWndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam) {
    switch (msg) {
        case WM_DESTROY: // User closed the window
            PostQuitMessage(0);
            break;

        case WM_LBUTTONDOWN:
            pt.x = LOWORD(lParam);
            pt.y = HIWORD(lParam);
            wsprintf(str, "Co-ordinates are\nX=%i and Y=%i",pt.x,pt.y);
            MessageBox(hwnd, str, "Left Button Clicked", MB_OK);
            break;

        default: // Call the default window handler
            return DefWindowProc(hwnd,msg,wParam,lParam);
    }
    return 0;
}

Algunas librerarias compartidas en programas Windows

Los strings en Windows:

Immunity debugger

Algunos hot keys del Immunity Debugger

shortcut acción equivalente en gdb
F3 abrir un ejectuable gdb ejecutable
F2 toggle a breakpoint b
F8 step over ni
F9 run or continue r, c
CTRL-F2 restart r
CTRL-F9 execute until return ??

Algunos "wokflow" que uso con frecuencia: