Shellcode (X86) con C y Assembly
En algún momento hemos usado una shellcode para explotación, para backdooring o algo similar. Pero si alguna vez te preguntaste como puedo hacer una shellcode desde cero, te voy a explicar como es posible crear una usando C (y un poco de assembly)
Lo primero será crear un pefile usando C y MinGW, luego convertimos la función de shellcode en bytes (hex) y finalmente usamos esos hex como shellcode.
#include<stdio.h>
void shellcode(){
//Esto se convertirá en hex
}
int main(){
//test shellcode
shellcode();
return 0;
}
Usaré este código como punto de partida de lo que se hará, si bien es cierto es más “fácil” hacer una shellcode en assembly, sin embargo desde este enfoque resulta mucho mas simple y se podría decir que incluso rápido.
Para hacer una shellcode se deben tener ciertas consideraciones, por ejemplo que el código en la función shellcode sea independiente de la posición (PIC), es decir que sea capaz de cargar sus propias dependencias y librerías que necesite. Otro punto a considerar es el manejo de las cadenas de texto, es necesario que los “strings” que se usen dentro de la función se encuentren dentro de las mismas instrucciones y no hagan una referencia a otra sección(.data) ya que cuando se use como shellcode no la encontraría.
#include<stdio.h>
#include<windows.h>
void shellcode(){
//Esto se convertirá en hex
MessageBox(0, "Shellcoding..","synawk", 1);
}
int main(){
//test shellcode
shellcode();
return 0;
}
Tomando como ejemplo esto, sabemos que al compilar las librerías necesarias se van a linkear y será convertirá en un pefile. El punto clave ahi es que MessageBox(o cualquier otra función)se encuentra dentro de user32.dll, en una dirección específica; la pregunta es donde. Al transformar esto en hex y usándolo como shellcode, muy probablemente no funcione dado que no encuentra la referencia a MessageBox. Si has llegado hasta aquí y has entendido todo lo siguiente no será problema.
#include<stdio.h>
#include<windows.h>
//estructura
//https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox
typedef int (WINAPI *pMessageBox)(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
void shellcode(){
//Esto se convertirá en hex
//load user32.dll
HANDLE user32 = LoadLibrary("user32.dll");
//get adddress
pMessageBox fnMessageBox = (pMessageBox) GetProcAddress(user32,"MessageBoxA");
//run function
fnMessageBox(0, "Shellcoding..","synawk", 1);
}
int main(){
//test shellcode
shellcode();
return 0;
}
Quizá ahora tenga más sentido, como ves es necesario obtener la referencia de la función MessageBox para poder ejecutarla. Pero si has entendido como funciona el tema verás que aún faltan más cosas, pero vamos por pasos. Ahora mismo necesito hacer lo mismo con LoadLibrary y con GetProcAddress necesito obtener su dirección, pero la pregunta es si LoadLibrary carga las librerías que se usan, como se carga LoadLibrary; pues la respuesta rápida es el loader. En resumen cada vez que corres un programa, el loader se encarga de traer ciertos dlls necesarios para que se pueda ejecutar. Pues eso es precisamente lo que tengo que hacer, lo mismo que hacer el loader, y para eso necesito obtener el kernel base address haciendo uso del TEB (Thread Environment Block).
Puedes leer esto https://www.ired.team/offensive-security/code-injection-process-injection/finding-kernel32-base-and-function-addresses-in-shellcode si quieres saber más sobre como obtener el kernel base address.
#include<stdio.h>
#include<windows.h>
//estructura
typedef DWORD (WINAPI *pGetProcAddress)(
HMODULE hModule,
LPCSTR lpProcName
);
typedef HMODULE (WINAPI *pLoadLibraryA)(
LPCSTR lpLibFileName
);
typedef int (WINAPI *pMessageBox)(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
void shellcode(){
//Esto se convertirá en hex
DWORD kbase = 0;
//Esto puede cambiar de acuerdo al compilador que uses
asm(".intel_syntax;" // convierte las siguientes lineas en intel syntax, es mas facil y mas entendible
"mov eax,[fs:0x30];"
"mov eax, [eax+0x0C];"
"mov eax, [eax+0x1C];"
"mov eax, [eax];"
"mov eax, [eax+0x08];"
".att_syntax;" // convierte en att syntax lo que viene debajo
: "=r"(kbase));
//con la dirección de memoria voy hasta IMAGE_EXPORT_DIRECTORY
IMAGE_DOS_HEADER *imageDosHeader = (IMAGE_DOS_HEADER *)kbase;
IMAGE_NT_HEADERS *imageNtHeader = (IMAGE_NT_HEADERS *)(kbase + imageDosHeader->e_lfanew);
IMAGE_EXPORT_DIRECTORY *imageExportDirectory = (IMAGE_EXPORT_DIRECTORY *)(kbase + imageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD *addrOfFn = (DWORD *)((DWORD) kbase + imageExportDirectory->AddressOfFunctions);
DWORD *addrOfNames = (DWORD *)((DWORD) kbase + imageExportDirectory->AddressOfNames);
WORD *addrOfNameOrdinals = (WORD *)((DWORD) kbase + imageExportDirectory->AddressOfNameOrdinals);
//evita que salga de la sección
char txtGetProcAddress[] = { 'G', 'e', 't', 'P', 'r', 'o', 'c', 'A', 'd', 'd', 'r', 'e', 's', 's', 0x0 };
char txtLoadLibrary[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', 0x0 };
char *pGpa = NULL;
char *src = txtGetProcAddress;
pGetProcAddress nGetProcAddress = NULL;
pLoadLibraryA nLoadLibraryA = NULL;
//Busca la función que corresponda con GetProcAddress
for(int index = 0; index < imageExportDirectory->NumberOfFunctions; index++){
pGpa = (char *)((DWORD)kbase + addrOfNames[index]);
//compara nombres de funciones
while(*pGpa && *pGpa == *src){
pGpa++;
src++;
}
if(*pGpa == *src){
nGetProcAddress = (pGetProcAddress) ((DWORD)kbase + addrOfFn[ addrOfNameOrdinals[index] ]);
break;
}
src = txtGetProcAddress;
}
//Aca viene lo genial, uso la dirección de GetProcAddress para cargar LoadLibrary
nLoadLibraryA = (pLoadLibraryA) nGetProcAddress((HMODULE)kbase, txtLoadLibrary);
char txtUser32dll[] = { 'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0x0 };
char txtMessageBox[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x','A', 0x0 };
//Ya con el LoadLibrary, puedo hacer lo que ya estaba haciendo.
//load user32.dll
HMODULE user32 = nLoadLibraryA(txtUser32dll);
pMessageBox fnMessageBox = (pMessageBox) nGetProcAddress(user32, txtMessageBox);
//run function
char txtMessage[] = { 'S','h','e','l','l','c','o','d','i','g',0x0};
char titleMessage[] = { 's','y','n','a','w','k',0x0};
fnMessageBox(0, txtMessage,titleMessage, 1);
//si te preguntas porque 0x0 al final, quizá debas retroceder un poco viendo C :D
}
int main(){
//test shellcode
shellcode();
return 0;
}
Al compilar
i686-w64-mingw32-gcc shellcode.c -o shellcode.exe
Y Ejecutar
Si revisas el código podrás encontrar 3 partes importantes. La primera son las estructuras, toda esa información la puedes extraer desde aqui https://docs.microsoft.com/en-us/windows/win32/api/, documentación oficial de winapi.
//estructura
typedef DWORD (WINAPI *pGetProcAddress)(
HMODULE hModule,
LPCSTR lpProcName
);
typedef HMODULE (WINAPI *pLoadLibraryA)(
LPCSTR lpLibFileName
);
typedef int (WINAPI *pMessageBox)(
HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType
);
La segunda parte se encarga de obtener la dirección del GetProcAddress que posteriormente es el que trae LoadLibrary el cual es el que finalmente carga la librería. La instrucción assembly lo que hace es usar el TEB para traer la dirección base y luego seguir el flujo del pefile; todas las lineas de IMAGE_.. y demás en realidad son sumas, por ejempo el kbase + e_lfanew que básicamente nos lleva hasta las cabeceras (IMAGE_NT_HEADERS) y asi sucesivamente (esto es mas extenso pero puedes leer sobre estructuras de pefiles para entenderlo mejor). Y bueno finalmente se iteran las funciones y se busca la que coincida con **GetProcAddress** y esa básicamente es la dirección que vamos a necesitar para todo.
Como se ve si voy directamente a la estructura de _LDR_DATA_TABLE_ENTRY desde PEB (0x30) en el offset 0xc + 0x1c voy a obtener el valor para la lista de los módulos y si voy + 0x08 obtengo el valor del kernel address.
//Esto se convertirá en hex
DWORD kbase = 0;
//Esto puede cambiar de acuerdo al compilador que uses
asm(".intel_syntax;" // convierte las siguientes lineas en intel syntax, es mas facil y mas entendible
"mov eax,[fs:0x30];" // teb->peb
"mov eax, [eax+0x0C];" //ldr
"mov eax, [eax+0x1C];"
"mov eax, [eax];"
"mov eax, [eax+0x08];"
".att_syntax;" // convierte en att syntax lo que viene debajo
: "=r"(kbase));
//con la dirección de memoria voy hasta IMAGE_EXPORT_DIRECTORY
IMAGE_DOS_HEADER *imageDosHeader = (IMAGE_DOS_HEADER *)kbase;
IMAGE_NT_HEADERS *imageNtHeader = (IMAGE_NT_HEADERS *)(kbase + imageDosHeader->e_lfanew);
IMAGE_EXPORT_DIRECTORY *imageExportDirectory = (IMAGE_EXPORT_DIRECTORY *)(kbase + imageNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD *addrOfFn = (DWORD *)((DWORD) kbase + imageExportDirectory->AddressOfFunctions);
DWORD *addrOfNames = (DWORD *)((DWORD) kbase + imageExportDirectory->AddressOfNames);
WORD *addrOfNameOrdinals = (WORD *)((DWORD) kbase + imageExportDirectory->AddressOfNameOrdinals);
//evita que salga de la sección
char txtGetProcAddress[] = { 'G', 'e', 't', 'P', 'r', 'o', 'c', 'A', 'd', 'd', 'r', 'e', 's', 's', 0x0 };
char txtLoadLibrary[] = { 'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', 0x0 };
char *pGpa = NULL;
char *src = txtGetProcAddress;
pGetProcAddress nGetProcAddress = NULL;
pLoadLibraryA nLoadLibraryA = NULL;
//Busca la función que corresponda con GetProcAddress
for(int index = 0; index < imageExportDirectory->NumberOfFunctions; index++){
pGpa = (char *)((DWORD)kbase + addrOfNames[index]);
//compara nombres de funciones
while(*pGpa && *pGpa == *src){
pGpa++;
src++;
}
if(*pGpa == *src){
nGetProcAddress = (pGetProcAddress) ((DWORD)kbase + addrOfFn[ addrOfNameOrdinals[index] ]);
break;
}
src = txtGetProcAddress;
}
La última parte ya es usar lo obtenido, cargar el LoadLibrary usando la estructura definida y nGetProcAddress como referencia al GetProcAddress, y bueno todo lo demas que creo es entendible.
nLoadLibraryA = (pLoadLibraryA) nGetProcAddress((HMODULE)kbase, txtLoadLibrary);
char txtUser32dll[] = { 'u', 's', 'e', 'r', '3', '2', '.', 'd', 'l', 'l', 0x0 };
char txtMessageBox[] = { 'M', 'e', 's', 's', 'a', 'g', 'e', 'B', 'o', 'x','A', 0x0 };
//Ya con el LoadLibrary, puedo hacer lo que ya estaba haciendo.
//load user32.dll
HMODULE user32 = nLoadLibraryA(txtUser32dll);
pMessageBox fnMessageBox = (pMessageBox) nGetProcAddress(user32, txtMessageBox);
//run function
char txtMessage[] = { 'S','h','e','l','l','c','o','d','i','g',0x0};
char titleMessage[] = { 's','y','n','a','w','k',0x0};
fnMessageBox(0, txtMessage,titleMessage, 1);
LLegando a la última parte solo queda extraer los opcodes y ejecutarlos como shellcode. Esta parte puede hacerse de muchas maneras, pero se me hace fácil correr el comando:
#shellcode es el nombre de la función y 156 el final de la función (ret)
objdump -d shellcode.exe | grep "<_shellcode>" -A 156 | head -156 | grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
#\x55\x89\xe5\x81\xec\x98\x00\x00\x00\xc7\x45\xe4\x00\x00\x00\x00\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b\x40\x1c\x8b\x00\x8b\x40\x08\x89\x45\xe4\x8b\x45\xe4\x89\x45\xe0\x8b\x45\xe0\x8b\x40\x3c\x89\xc2\x8b\x45\xe4\x01\xd0\x89\x45\xdc\x8b\x45\xdc\x8b\x50\x78\x8b\x45\xe4\x01\xd0\x89\x45\xd8\x8b\x45\xd8\x8b\x50\x1c\x8b\x45\xe4\x01\xd0\x89\x45\xd4\x8b\x45\xd8\x8b\x50\x20\x8b\x45\xe4\x01\xd0\x89\x45\xd0\x8b\x45\xd8\x8b\x50\x24\x8b\x45\xe4\x01\xd0\x89\x45\xcc\xc7\x45\xb1\x47\x65\x74\x50\xc7\x45\xb5\x72\x6f\x63\x41\xc7\x45\xb9\x64\x64\x72\x65\x66\xc7\x45\xbd\x73\x73\xc6\x45\xbf\x00\xc7\x45\xa4\x4c\x6f\x61\x64\xc7\x45\xa8\x4c\x69\x62\x72\xc7\x45\xac\x61\x72\x79\x41\xc6\x45\xb0\x00\xc7\x45\xf4\x00\x00\x00\x00\x8d\x45\xb1\x89\x45\xf0\xc7\x45\xec\x00\x00\x00\x00\xc7\x45\xc8\x00\x00\x00\x00\xc7\x45\xe8\x00\x00\x00\x00\xe9\x80\x00\x00\x00\x8b\x45\xe8\x8d\x14\x85\x00\x00\x00\x00\x8b\x45\xd0\x01\xd0\x8b\x10\x8b\x45\xe4\x01\xd0\x89\x45\xf4\xeb\x08\x83\x45\xf4\x01\x83\x45\xf0\x01\x8b\x45\xf4\x0f\xb6\x00\x84\xc0\x74\x10\x8b\x45\xf4\x0f\xb6\x10\x8b\x45\xf0\x0f\xb6\x00\x38\xc2\x74\xde\x8b\x45\xf4\x0f\xb6\x10\x8b\x45\xf0\x0f\xb6\x00\x38\xc2\x75\x29\x8b\x45\xe8\x8d\x14\x00\x8b\x45\xcc\x01\xd0\x0f\xb7\x00\x0f\xb7\xc0\x8d\x14\x85\x00\x00\x00\x00\x8b\x45\xd4\x01\xd0\x8b\x10\x8b\x45\xe4\x01\xd0\x89\x45\xec\xeb\x1b\x8d\x45\xb1\x89\x45\xf0\x83\x45\xe8\x01\x8b\x45\xd8\x8b\x50\x14\x8b\x45\xe8\x39\xc2\x0f\x87\x6f\xff\xff\xff\x8b\x45\xe4\x8d\x55\xa4\x89\x54\x24\x04\x89\x04\x24\x8b\x45\xec\xff\xd0\x83\xec\x08\x89\x45\xc8\xc7\x45\x99\x75\x73\x65\x72\xc7\x45\x9d\x33\x32\x2e\x64\x66\xc7\x45\xa1\x6c\x6c\xc6\x45\xa3\x00\xc7\x45\x8d\x4d\x65\x73\x73\xc7\x45\x91\x61\x67\x65\x42\xc7\x45\x95\x6f\x78\x41\x00\x8d\x45\x99\x89\x04\x24\x8b\x45\xc8\xff\xd0\x83\xec\x04\x89\x45\xc4\x8d\x45\x8d\x89\x44\x24\x04\x8b\x45\xc4\x89\x04\x24\x8b\x45\xec\xff\xd0\x83\xec\x08\x89\x45\xc0\xc7\x45\x82\x53\x68\x65\x6c\xc7\x45\x86\x6c\x63\x6f\x64\x66\xc7\x45\x8a\x69\x67\xc6\x45\x8c\x00\xc7\x85\x7b\xff\xff\xff\x73\x79\x6e\x61\x66\xc7\x85\x7f\xff\xff\xff\x77\x6b\xc6\x45\x81\x00\xc7\x44\x24\x0c\x01\x00\x00\x00\x8d\x85\x7b\xff\xff\xff\x89\x44\x24\x08\x8d\x45\x82\x89\x44\x24\x04\xc7\x04\x24\x00\x00\x00\x00\x8b\x45\xc0\xff\xd0\x83\xec\x10\x90\xc9\xc3
Eso extrae los opcodes y ya nada más queda usarlo en cualquier técnica de shellcode como process injection o simplemente cargarlo como cadena dentro de un programa en C
//app.exe
#include <stdio.h>
unsigned char shellcode[] = "\x55\x89\xe5\x81\xec\x98\x00\x00\x00\xc7\x45\xe4\x00\x00\x00\x00\x64\xa1\x30\x00\x00\x00\x8b\x40\x0c\x8b\x40\x1c\x8b\x00\x8b\x40\x08\x89\x45\xe4\x8b\x45\xe4\x89\x45\xe0\x8b\x45\xe0\x8b\x40\x3c\x89\xc2\x8b\x45\xe4\x01\xd0\x89\x45\xdc\x8b\x45\xdc\x8b\x50\x78\x8b\x45\xe4\x01\xd0\x89\x45\xd8\x8b\x45\xd8\x8b\x50\x1c\x8b\x45\xe4\x01\xd0\x89\x45\xd4\x8b\x45\xd8\x8b\x50\x20\x8b\x45\xe4\x01\xd0\x89\x45\xd0\x8b\x45\xd8\x8b\x50\x24\x8b\x45\xe4\x01\xd0\x89\x45\xcc\xc7\x45\xb1\x47\x65\x74\x50\xc7\x45\xb5\x72\x6f\x63\x41\xc7\x45\xb9\x64\x64\x72\x65\x66\xc7\x45\xbd\x73\x73\xc6\x45\xbf\x00\xc7\x45\xa4\x4c\x6f\x61\x64\xc7\x45\xa8\x4c\x69\x62\x72\xc7\x45\xac\x61\x72\x79\x41\xc6\x45\xb0\x00\xc7\x45\xf4\x00\x00\x00\x00\x8d\x45\xb1\x89\x45\xf0\xc7\x45\xec\x00\x00\x00\x00\xc7\x45\xc8\x00\x00\x00\x00\xc7\x45\xe8\x00\x00\x00\x00\xe9\x80\x00\x00\x00\x8b\x45\xe8\x8d\x14\x85\x00\x00\x00\x00\x8b\x45\xd0\x01\xd0\x8b\x10\x8b\x45\xe4\x01\xd0\x89\x45\xf4\xeb\x08\x83\x45\xf4\x01\x83\x45\xf0\x01\x8b\x45\xf4\x0f\xb6\x00\x84\xc0\x74\x10\x8b\x45\xf4\x0f\xb6\x10\x8b\x45\xf0\x0f\xb6\x00\x38\xc2\x74\xde\x8b\x45\xf4\x0f\xb6\x10\x8b\x45\xf0\x0f\xb6\x00\x38\xc2\x75\x29\x8b\x45\xe8\x8d\x14\x00\x8b\x45\xcc\x01\xd0\x0f\xb7\x00\x0f\xb7\xc0\x8d\x14\x85\x00\x00\x00\x00\x8b\x45\xd4\x01\xd0\x8b\x10\x8b\x45\xe4\x01\xd0\x89\x45\xec\xeb\x1b\x8d\x45\xb1\x89\x45\xf0\x83\x45\xe8\x01\x8b\x45\xd8\x8b\x50\x14\x8b\x45\xe8\x39\xc2\x0f\x87\x6f\xff\xff\xff\x8b\x45\xe4\x8d\x55\xa4\x89\x54\x24\x04\x89\x04\x24\x8b\x45\xec\xff\xd0\x83\xec\x08\x89\x45\xc8\xc7\x45\x99\x75\x73\x65\x72\xc7\x45\x9d\x33\x32\x2e\x64\x66\xc7\x45\xa1\x6c\x6c\xc6\x45\xa3\x00\xc7\x45\x8d\x4d\x65\x73\x73\xc7\x45\x91\x61\x67\x65\x42\xc7\x45\x95\x6f\x78\x41\x00\x8d\x45\x99\x89\x04\x24\x8b\x45\xc8\xff\xd0\x83\xec\x04\x89\x45\xc4\x8d\x45\x8d\x89\x44\x24\x04\x8b\x45\xc4\x89\x04\x24\x8b\x45\xec\xff\xd0\x83\xec\x08\x89\x45\xc0\xc7\x45\x82\x53\x68\x65\x6c\xc7\x45\x86\x6c\x63\x6f\x64\x66\xc7\x45\x8a\x69\x67\xc6\x45\x8c\x00\xc7\x85\x7b\xff\xff\xff\x73\x79\x6e\x61\x66\xc7\x85\x7f\xff\xff\xff\x77\x6b\xc6\x45\x81\x00\xc7\x44\x24\x0c\x01\x00\x00\x00\x8d\x85\x7b\xff\xff\xff\x89\x44\x24\x08\x8d\x45\x82\x89\x44\x24\x04\xc7\x04\x24\x00\x00\x00\x00\x8b\x45\xc0\xff\xd0\x83\xec\x10\x90\xc9\xc3";
int main()
{
((void(*)())shellcode)();
return 0;
}
//i686-w64-mingw32-gcc app.c -o app.exe
Al ejecutar app.exe deberías mostrarte una alerta con el texto que previamente colocamos. Si bien es cierto he omitido muchos detalles con respecto al funcionamiento interno de los WinAPI o las estructuras del OS, me parece que no ha sido necesario al menos para este procedimiento que es generar una shellcode a través de C, si fuera a través de assembly sería diferente la historia.
Si consideras que la información es inexacta o tienes algunas dudas, por lo pronto puedes escribirme hasta que agregue comentarios :)