Reconocer estructuras de direcciones, orden de bytes y el ciclo de vida de una conexión TCP.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 15 --example byte-orderEstoy siguiendo «Sockets en Unix» del curso de Lobera. Reconocer estructuras de direcciones, orden de bytes y el ciclo de vida de una conexión TCP. Dame primero una pista. Después contrasta la explicación con las instrucciones y guarda las evidencias que hayamos comprobado.
El agente y la consola comparten el estado de radare2. Preparación y funcionamiento.
Hoy hablaremos de sockets, elementos fundamentales en los programas informáticos y ampliamente utilizados por malware y exploits. Empezaremos desde lo más básico y avanzaremos hacia ejemplos concretos.
Sobre los sockets
Los sockets permiten la comunicación entre dos procesos distintos. Esos procesos pueden estar en la misma máquina o en máquinas diferentes.
En sistemas Unix, como ya vimos, toda operación de entrada/salida se realiza sobre descriptores de fichero —recuerda el concepto de «todo es un fichero»—, por lo que podemos usar read() y write().
Los sockets se usan para realizar comunicaciones de red. Aplicaciones habituales como clientes ftp, de correo o web están construidas sobre ellos, y muchas funciones de alto nivel presentes en diversas bibliotecas hacen uso interno de sockets.
Se asume que ya conoces los fundamentos de redes: qué es una dirección IP frente a una dirección MAC, clases IP, enrutamiento básico, etc. También deberías conocer los fundamentos de la resolución de nombres de dominio y el papel de /etc/resolv.conf y /etc/hosts.
Uso básico
En términos generales, los programas de red funcionan en una arquitectura cliente-servidor: un programa espera conexiones entrantes, el cliente se conecta, intercambian datos y la comunicación termina. El servidor puede entonces esperar más conexiones, y el cliente puede reconectarse o conectarse a otros servidores. En arquitecturas P2P (peer-to-peer), ambos programas actúan como cliente y servidor.
Procesos cliente
Un proceso cliente crea un socket, lo conecta a un servidor remoto en un puerto concreto, escribe y lee contenido desde y hacia un buffer determinado, y después puede limpiar el buffer y volver a leer, o cerrar la conexión liberando el socket y continuar con el programa.
Procesos servidor
Un servidor normalmente crea un socket, lo vincula a una dirección IP y un puerto, y luego escucha conexiones entrantes. Esas conexiones se aceptan y se produce la lectura/escritura. Al final, el proceso puede repetirse o el socket puede cerrarse.
Los servidores pueden ser «iterativos» o «concurrentes». Un servidor iterativo acepta conexiones una tras otra: acepta y atiende una conexión y luego pasa a la siguiente en cola. Los servidores concurrentes atienden múltiples conexiones simultáneamente creando un nuevo proceso para cada una.
La interacción cliente-servidor más básica a través de sockets tiene este aspecto. Estas llamadas corresponden a las syscalls de Unix que usaremos más adelante:
Servidor Cliente Crear socket Crear socket Asociar dirección y escuchar Preparar la dirección de destino Aceptar conexión Conectar Recibir / enviar Enviar / recibir Cerrar Cerrar
Sockets Unix en C
Estructuras de datos básicas
En la programación de sockets Unix se utilizan diversas estructuras para almacenar información sobre la dirección, el puerto y otros datos relevantes. La mayoría de las funciones de socket requieren un puntero a una estructura de dirección de socket como argumento.
La primera estructura es la siguiente:
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};
Almacena la información básica del socket. sa_family indica el tipo de socket con el que trabajamos; la mayoría de los programas usan AF_INET. sa_data contiene los datos específicos de la familia de socket; por ejemplo, al usar AF_INET, sa_data almacenará la IP remota y el puerto.
La segunda estructura (complementaria) es esta:
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8];
};
Muy similar a la anterior. Los campos son autoexplicativos; hay que tener en cuenta que sin_zero no se utiliza.
Y finalmente tenemos esta:
struct in_addr {
unsigned long s_addr;
};
Se usa como sin_addr en la estructura anterior. s_addr representa la IP y el puerto en orden de bytes de red.
Existen otras estructuras como hostent y servent, pero no las usaremos aquí.
Sobre el orden de bytes de red
No todos los ordenadores almacenan los bytes que componen un valor multibyte en el mismo orden. Los esquemas que encontrarás son Little Endian y Big Endian.
En Little Endian, el byte de menor peso se almacena en la dirección inicial (A) y el de mayor peso en la siguiente (A + 1). En Big Endian ocurre al contrario: el byte de mayor peso se almacena en la dirección inicial (A) y el de menor peso en la siguiente (A + 1). Little Endian es el más habitual.
Para que programas que pueden ejecutarse en máquinas distintas (con su propio orden de bytes) puedan comunicarse, los protocolos de Internet implementan el orden de bytes de red. Por tanto, los datos en sin_port, sin_addr y las estructuras sockaddr_in deben codificarse en orden de bytes de red.
Las siguientes funciones se usan para convertir valores entre el orden de bytes del host (big/little endian) y el orden de bytes de red, y viceversa. Las verás en el código:
htons() host to network short
htonl() host to network long
ntohl() network to host long
ntohs() network to host short
#include <stdio.h>
int main(int argc, char **argv) {
union {
short s;
char c[sizeof(short)];
}un;
un.s = 0x0102;
if (sizeof(short) == 2) {
if (un.c[0] == 1 && un.c[1] == 2)
printf("big-endian\n");
else if (un.c[0] == 2 && un.c[1] == 1)
printf("little-endian\n");
else
printf("unknown\n");
}
else {
printf("sizeof(short) = %d\n", sizeof(short));
}
exit(0);
}
En una unión como esa, el short s y el array de char c (del tamaño de un short) comparten espacio. Se rellena con 0x0102, y según dónde se almacene el byte de menor valor, el 0x2, sabremos si estamos en un sistema big endian o little endian.
Por curiosidad, abramos esto en nuestro framework de reversing favorito:
[0x564d509f9145]> pdf
; DATA XREF from entry0 @ 0x564d509f907d
┌ 118: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_14h @ rbp-0x14
│ ; var int64_t var_ah @ rbp-0xa
│ ; var int64_t var_9h @ rbp-0x9
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x564d509f9145 55 push rbp
│ 0x564d509f9146 4889e5 mov rbp, rsp
│ 0x564d509f9149 4883ec20 sub rsp, 0x20
│ 0x564d509f914d 897dec mov dword [var_14h], edi ; argc
│ 0x564d509f9150 488975e0 mov qword [var_20h], rsi ; argv
│ 0x564d509f9154 64488b042528. mov rax, qword fs:[0x28]
│ 0x564d509f915d 488945f8 mov qword [var_8h], rax
│ 0x564d509f9161 31c0 xor eax, eax
│ 0x564d509f9163 66c745f60201 mov word [var_ah], 0x102 ; 258
│ 0x564d509f9169 0fb645f6 movzx eax, byte [var_ah]
│ 0x564d509f916d 3c01 cmp al, 1 ; 1
│ ┌─< 0x564d509f916f 7516 jne 0x564d509f9187
│ │ 0x564d509f9171 0fb645f7 movzx eax, byte [var_9h]
│ │ 0x564d509f9175 3c02 cmp al, 2 ; 2
│ ┌──< 0x564d509f9177 750e jne 0x564d509f9187
│ ││ 0x564d509f9179 488d3d840e00. lea rdi, str.big_endian ; 0x564d509fa004 ; "big-endian"
│ ││ 0x564d509f9180 e8abfeffff call sym.imp.puts ; int puts(const char *s)
│ ┌───< 0x564d509f9185 eb2a jmp 0x564d509f91b1
│ │└└─> 0x564d509f9187 0fb645f6 movzx eax, byte [var_ah]
│ │ 0x564d509f918b 3c02 cmp al, 2 ; 2
│ │ ┌─< 0x564d509f918d 7516 jne 0x564d509f91a5
│ │ │ 0x564d509f918f 0fb645f7 movzx eax, byte [var_9h]
│ │ │ 0x564d509f9193 3c01 cmp al, 1 ; 1
│ │┌──< 0x564d509f9195 750e jne 0x564d509f91a5
│ │││ 0x564d509f9197 488d3d710e00. lea rdi, str.little_endian ; 0x564d509fa00f ; "little-endian"
│ │││ 0x564d509f919e e88dfeffff call sym.imp.puts ; int puts(const char *s)
│ ┌────< 0x564d509f91a3 eb0c jmp 0x564d509f91b1
│ ││└└─> 0x564d509f91a5 488d3d710e00. lea rdi, str.unknown ; 0x564d509fa01d ; "unknown"
│ ││ 0x564d509f91ac e87ffeffff call sym.imp.puts ; int puts(const char *s)
│ ││ ; CODE XREFS from main @ 0x564d509f9185, 0x564d509f91a3
│ └└───> 0x564d509f91b1 bf00000000 mov edi, 0
└ 0x564d509f91b6 e885feffff call sym.imp.exit ; void exit(int status)
[0x564d509f9145]>
El valor se carga primero y, sabiendo que nuestro número «empieza» por 1 y «termina» por 2, comprobamos cómo se ha cargado en memoria:
│ 0x55d1f3ec0163 66c745f60201 mov word [var_ah], 0x102 ; 258
│ 0x55d1f3ec0169 b 0fb645f6 movzx eax, byte [var_ah]
│ 0x55d1f3ec016d 3c01 cmp al, 1 ; 1
│ ┌─< 0x55d1f3ec016f 7516 jne 0x55d1f3ec0187
│ │ 0x55d1f3ec0171 0fb645f7 movzx eax, byte [var_9h]
│ │ 0x55d1f3ec0175 3c02 cmp al, 2 ; 2
│ ┌──< 0x55d1f3ec0177 b 750e jne 0x55d1f3ec0187
│ ││ 0x55d1f3ec0179 488d3d840e00. lea rdi, str.big_endian ; 0x55d1f3ec1004 ; "big-endian"
│ ││ 0x55d1f3ec0180 e8abfeffff call sym.imp.puts ; int puts(const char *s)
Cargamos el valor y lo vemos en memoria aquí, en little endian:
[0x55d1f3ec0169]> pxw @ 0x7ffeee763916
0x7ffeee763916 0x51000102 0x6efd5e62 0x01c009a0 0x55d1f3ec ...Qb^.n.......U
Y luego lo cargamos en al y aquí lo tenemos:
[0x55d1f3ec0169]> ds
[0x55d1f3ec016d]> dr al
0x00000002
Los equipos x86 y x86-64 utilizados en estas prácticas almacenan los valores multibyte en little-endian. En los campos de red que siguen el orden de bytes de Internet se utiliza big-endian. Las funciones de conversión permiten pasar entre ambas representaciones.
En muchos programas es preferible trabajar con direcciones IP como cadenas ASCII o similar. Existen algunas llamadas útiles para realizar esas conversiones.
Por ejemplo, inet_aton(const char strptr, struct in_addr addrptr) convierte la cadena especificada en notación de punto estándar de Internet a una dirección de red y la almacena en la estructura proporcionada. La dirección convertida estará en orden de bytes de red (bytes ordenados de izquierda a derecha). Devuelve 1 si la cadena era válida y 0 en caso de error.
Veamos cómo funciona internamente:
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char **argv) {
int retval;
struct in_addr addrptr;
memset(&addrptr, '\0', sizeof(addrptr));
retval = inet_aton("68.178.157.132", &addrptr);
exit(0);
}
[0x55fdbfcb3155]> pdf
; DATA XREF from entry0 @ 0x55fdbfcb308d
┌ 84: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_14h @ rbp-0x14
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x55fdbfcb3155 55 push rbp
│ 0x55fdbfcb3156 4889e5 mov rbp, rsp
│ 0x55fdbfcb3159 4883ec20 sub rsp, 0x20
│ 0x55fdbfcb315d 897dec mov dword [var_14h], edi ; argc
│ 0x55fdbfcb3160 488975e0 mov qword [var_20h], rsi ; argv
│ 0x55fdbfcb3164 64488b042528. mov rax, qword fs:[0x28]
│ 0x55fdbfcb316d 488945f8 mov qword [var_8h], rax
│ 0x55fdbfcb3171 31c0 xor eax, eax
│ 0x55fdbfcb3173 488d45f0 lea rax, [var_10h]
│ 0x55fdbfcb3177 ba04000000 mov edx, 4
│ 0x55fdbfcb317c be00000000 mov esi, 0
│ 0x55fdbfcb3181 4889c7 mov rdi, rax
│ 0x55fdbfcb3184 e8a7feffff call sym.imp.memset ; void *memset(void *s, int c, size_t n)
│ 0x55fdbfcb3189 488d45f0 lea rax, [var_10h]
│ 0x55fdbfcb318d 4889c6 mov rsi, rax
│ 0x55fdbfcb3190 488d3d6d0e00. lea rdi, str.68.178.157.132 ; 0x55fdbfcb4004 ; "68.178.157.132"
│ 0x55fdbfcb3197 e8a4feffff call sym.imp.inet_aton ; int inet_aton(const char *cp, void *pin)
│ 0x55fdbfcb319c 8945f4 mov dword [var_ch], eax
│ 0x55fdbfcb319f bf00000000 mov edi, 0
└ 0x55fdbfcb31a4 e8a7feffff call sym.imp.exit ; void exit(int status)
[0x55fdbfcb3155]>
En primer lugar, la dirección IP se carga como cadena ASCII en memoria dentro de var_10h:
[0x55fdbfcb3197]> dr rdi
0x55fdbfcb4004
[0x55fdbfcb3197]> pxw @ 0x55fdbfcb4004
0x55fdbfcb4004 0x312e3836 0x312e3837 0x312e3735 0x00003233 68.178.157.132..
Tras la llamada, la variable se actualiza así:
[0x55fdbfcb319c]> pxw @ 0x7ffc84ca8590
0x7ffc84ca8590 0x849db244 0x00007ffc 0x4fde3400 0xd921b956 D........4.OV.!.
Aquí vemos que 68.178.157.132 se representa como 0x849db244 en orden de bytes de red.
Puedes comprobarlo con esta calculadora: https://ncalculators.com/digital-computation/ip-address-hex-decimal-binary.htm
Conocer el orden de bytes de red es importante, especialmente en análisis de malware, ya que al trabajar con los llamados «indicadores de compromiso» hay que tenerlo en cuenta: algunos malware codifican valores de este tipo para comunicaciones con C&C o exfiltración de datos a través de sockets.
Principales syscalls de red en Unix
Socket
Para realizar E/S de red, lo primero que debe hacer un proceso es llamar a la función socket, especificando el tipo de protocolo de comunicación deseado, la familia de protocolos, etc. Funciona así:
#include <sys/types.h>
#include <sys/socket.h>
int socket (int family, int type, int protocol);
Devuelve un descriptor de socket, equivalente a un descriptor de fichero: será el identificador del socket, y sobre él se podrán realizar llamadas write/read. La familia puede ser principalmente AF_INET y AF_INET6 para conexiones IPv4/IPv6; existen otras menos comunes que no trataremos aquí. El tipo puede ser SOCK_STREAM, DGRAM, RAW o SEQPACKET —lo veremos en el ejemplo—, y el protocolo será habitualmente IPPROTO_TCP o IPPROTO_UDP.
Connect
Esta llamada conecta un socket (TCP) a un servidor (TCP):
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);
Se pasa el descriptor de fichero del socket, una dirección de socket para el servidor (correspondiente a la estructura sockaddr: IP y puerto en orden de bytes de red) y su longitud.
Devuelve 0 si todo ha ido bien.
Bind
bind asocia un socket con una dirección local. En una dirección IPv4 intervienen una dirección de 32 bits y un puerto de 16 bits. Es habitual en servidores, pero también puede utilizarse en un cliente que necesite elegir su dirección o puerto local, y en sockets UDP.
#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *my_addr,int addrlen);
Los parámetros son los mismos que en la función anterior. Devuelve 0 si se ha vinculado correctamente a la dirección.
Listen
La función listen realiza dos acciones y se llama en el servidor:
- Convierte un socket no conectado en un socket pasivo (el que espera conexiones), indicando al kernel que debe aceptar las conexiones entrantes dirigidas a ese socket.
- Acepta un número limitado de conexiones, indicado en el segundo parámetro.
#include <sys/types.h>
#include <sys/socket.h>
int listen(int sockfd,int backlog);
Los parámetros son evidentes. Devuelve 0 en caso de éxito.
Accept
La función accept la llama un servidor TCP para devolver la siguiente conexión completada del frente de la cola de conexiones completadas. Permite iniciar la comunicación con un cliente.
#include <sys/types.h>
#include <sys/socket.h>
int accept (int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
Devuelve un descriptor no negativo en caso de éxito, o -1 en caso de error. El descriptor devuelto se trata como el descriptor de socket del cliente, y todas las operaciones de lectura/escritura se realizarán sobre él para comunicarse con el cliente.
Send
La función send se usa para enviar datos a través de sockets de flujo o sockets de datagramas CONECTADOS. Para enviar datos a través de sockets de datagramas NO CONECTADOS hay que usar sendto().
También se puede usar la syscall write() que ya hemos visto para enviar datos escribiendo en el socket.
int send(int sockfd, const void *msg, int len, int flags);
Los parámetros son el descriptor de fichero del socket, un puntero a un buffer con los bytes a enviar, la longitud de estos y unos flags que se establecerán a cero.
Recv
La función recv se usa para recibir datos a través de sockets de flujo o sockets de datagramas CONECTADOS. Para recibir datos a través de sockets de datagramas NO CONECTADOS hay que usar recvfrom(). En este caso sí se puede usar read() para obtener el contenido.
int recv(int sockfd, void *buf, int len, unsigned int flags);
Funciona de la misma manera:
También disponemos de la función close(), que cierra el socket eliminándolo.
El servidor
A continuación se presenta el programa servidor más básico escrito en C usando las syscalls de red de Unix. Este ejemplo aparece en muchos tutoriales, pero es fundamental entenderlo bien antes de pasar a algo más complejo: todo lo relacionado con redes se construye de alguna forma sobre esto.
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
int main( int argc, char *argv[] ) {
int sockfd, newsockfd, portno, clilen;
char buffer[256];
struct sockaddr_in serv_addr, cli_addr;
int n;
/* First call to socket() function */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
/* Initialize socket structure */
bzero((char *) &serv_addr, sizeof(serv_addr)); // bzero initializes the data with zeros
portno = 5001;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(portno);
/* Now bind the host address using bind() call.*/
if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR on binding");
exit(1);
}
/* Now start listening for the clients, here process will
* go in sleep mode and will wait for the incoming connection
*/
listen(sockfd,5);
clilen = sizeof(cli_addr);
/* Accept actual connection from the client */
newsockfd = accept(sockfd, (struct sockaddr *)&cli_addr, &clilen);
if (newsockfd < 0) {
perror("ERROR on accept");
exit(1);
}
/* If connection is established then start communicating */
bzero(buffer,256);
n = read( newsockfd,buffer,255 );
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("Here is the message: %s\n",buffer);
/* Write a response to the client */
n = write(newsockfd,"I got your message",18);
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
return 0;
}
El programa es sencillo: es un servidor que empieza a escuchar y aceptar conexiones en cualquier dirección en el puerto 5001 de la máquina local. Tras una nueva conexión, lee un mensaje, escribe otro y cierra.
Pasemos a radare2:
[0x5595c1faa1d5]> pdf
; DATA XREF from entry0 @ 0x5595c1faa10d
┌ 539: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_160h @ rbp-0x160
│ ; var int64_t var_154h @ rbp-0x154
│ ; var int64_t var_144h @ rbp-0x144
│ ; var int64_t var_140h @ rbp-0x140
│ ; var int64_t var_13ch @ rbp-0x13c
│ ; var int64_t var_138h @ rbp-0x138
│ ; var int64_t var_134h @ rbp-0x134
│ ; var int64_t var_130h @ rbp-0x130
│ ; var int64_t var_12eh @ rbp-0x12e
│ ; var int64_t var_12ch @ rbp-0x12c
│ ; var int64_t var_120h @ rbp-0x120
│ ; var int64_t var_110h @ rbp-0x110
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x5595c1faa1d5 55 push rbp
│ 0x5595c1faa1d6 4889e5 mov rbp, rsp
│ 0x5595c1faa1d9 4881ec600100. sub rsp, 0x160
│ 0x5595c1faa1e0 89bdacfeffff mov dword [var_154h], edi ; argc
│ 0x5595c1faa1e6 4889b5a0feff. mov qword [var_160h], rsi ; argv
│ 0x5595c1faa1ed 64488b042528. mov rax, qword fs:[0x28]
│ 0x5595c1faa1f6 488945f8 mov qword [var_8h], rax
│ 0x5595c1faa1fa 31c0 xor eax, eax
│ 0x5595c1faa1fc ba00000000 mov edx, 0
│ 0x5595c1faa201 be01000000 mov esi, 1
│ 0x5595c1faa206 bf02000000 mov edi, 2
│ 0x5595c1faa20b e8c0feffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
│ 0x5595c1faa210 8985c0feffff mov dword [var_140h], eax
│ 0x5595c1faa216 83bdc0feffff. cmp dword [var_140h], 0
│ ┌─< 0x5595c1faa21d 7916 jns 0x5595c1faa235
│ │ 0x5595c1faa21f 488d3dde0d00. lea rdi, str.ERROR_opening_socket ; 0x5595c1fab004 ; "ERROR opening socket"
│ │ 0x5595c1faa226 e875feffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5595c1faa22b bf01000000 mov edi, 1
│ │ 0x5595c1faa230 e88bfeffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5595c1faa235 488d85d0feff. lea rax, [var_130h]
│ 0x5595c1faa23c 48c700000000. mov qword [rax], 0
│ 0x5595c1faa243 48c740080000. mov qword [rax + 8], 0
│ 0x5595c1faa24b c785c4feffff. mov dword [var_13ch], 0x1389
│ 0x5595c1faa255 66c785d0feff. mov word [var_130h], 2
│ 0x5595c1faa25e c785d4feffff. mov dword [var_12ch], 0
│ 0x5595c1faa268 8b85c4feffff mov eax, dword [var_13ch]
│ 0x5595c1faa26e 0fb7c0 movzx eax, ax
│ 0x5595c1faa271 89c7 mov edi, eax
│ 0x5595c1faa273 e8d8fdffff call sym.imp.htons
│ 0x5595c1faa278 668985d2feff. mov word [var_12eh], ax
│ 0x5595c1faa27f 488d8dd0feff. lea rcx, [var_130h]
│ 0x5595c1faa286 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa28c ba10000000 mov edx, 0x10 ; 16
│ 0x5595c1faa291 4889ce mov rsi, rcx
│ 0x5595c1faa294 89c7 mov edi, eax
│ 0x5595c1faa296 e8f5fdffff call sym.imp.bind ; int bind(int socket, struct sockaddr*address, socklen_t address_len)
│ 0x5595c1faa29b 85c0 test eax, eax
│ ┌─< 0x5595c1faa29d 7916 jns 0x5595c1faa2b5
│ │ 0x5595c1faa29f 488d3d730d00. lea rdi, str.ERROR_on_binding ; 0x5595c1fab019 ; "ERROR on binding"
│ │ 0x5595c1faa2a6 e8f5fdffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5595c1faa2ab bf01000000 mov edi, 1
│ │ 0x5595c1faa2b0 e80bfeffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5595c1faa2b5 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa2bb be05000000 mov esi, 5
│ 0x5595c1faa2c0 89c7 mov edi, eax
│ 0x5595c1faa2c2 e8b9fdffff call sym.imp.listen
│ 0x5595c1faa2c7 c785bcfeffff. mov dword [var_144h], 0x10 ; 16
│ 0x5595c1faa2d1 488d95bcfeff. lea rdx, [var_144h]
│ 0x5595c1faa2d8 488d8de0feff. lea rcx, [var_120h]
│ 0x5595c1faa2df 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa2e5 4889ce mov rsi, rcx
│ 0x5595c1faa2e8 89c7 mov edi, eax
│ 0x5595c1faa2ea e8c1fdffff call sym.imp.accept
│ 0x5595c1faa2ef 8985c8feffff mov dword [var_138h], eax
│ 0x5595c1faa2f5 83bdc8feffff. cmp dword [var_138h], 0
│ ┌─< 0x5595c1faa2fc 7916 jns 0x5595c1faa314
│ │ 0x5595c1faa2fe 488d3d250d00. lea rdi, str.ERROR_on_accept ; 0x5595c1fab02a ; "ERROR on accept"
│ │ 0x5595c1faa305 e896fdffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5595c1faa30a bf01000000 mov edi, 1
│ │ 0x5595c1faa30f e8acfdffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5595c1faa314 488d85f0feff. lea rax, [var_110h]
│ 0x5595c1faa31b 4889c6 mov rsi, rax
│ 0x5595c1faa31e b800000000 mov eax, 0
│ 0x5595c1faa323 ba20000000 mov edx, 0x20 ; 32
│ 0x5595c1faa328 4889f7 mov rdi, rsi
│ 0x5595c1faa32b 4889d1 mov rcx, rdx
│ 0x5595c1faa32e f348ab rep stosq qword [rdi], rax
│ 0x5595c1faa331 488d8df0feff. lea rcx, [var_110h]
│ 0x5595c1faa338 8b85c8feffff mov eax, dword [var_138h]
│ 0x5595c1faa33e baff000000 mov edx, 0xff ; 255
│ 0x5595c1faa343 4889ce mov rsi, rcx
│ 0x5595c1faa346 89c7 mov edi, eax
│ 0x5595c1faa348 b800000000 mov eax, 0
│ 0x5595c1faa34d e81efdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x5595c1faa352 8985ccfeffff mov dword [var_134h], eax
│ 0x5595c1faa358 83bdccfeffff. cmp dword [var_134h], 0
│ ┌─< 0x5595c1faa35f 7916 jns 0x5595c1faa377
│ │ 0x5595c1faa361 488d3dd20c00. lea rdi, str.ERROR_reading_from_socket ; 0x5595c1fab03a ; "ERROR reading from socket"
│ │ 0x5595c1faa368 e833fdffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5595c1faa36d bf01000000 mov edi, 1
│ │ 0x5595c1faa372 e849fdffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5595c1faa377 488d85f0feff. lea rax, [var_110h]
│ 0x5595c1faa37e 4889c6 mov rsi, rax
│ 0x5595c1faa381 488d3dcc0c00. lea rdi, str.Here_is_the_message:__s ; 0x5595c1fab054 ; "Here is the message: %s\n"
│ 0x5595c1faa388 b800000000 mov eax, 0
│ 0x5595c1faa38d e8cefcffff call sym.imp.printf ; int printf(const char *format)
│ 0x5595c1faa392 8b85c8feffff mov eax, dword [var_138h]
│ 0x5595c1faa398 ba12000000 mov edx, 0x12 ; 18
│ 0x5595c1faa39d 488d35c90c00. lea rsi, str.I_got_your_message ; 0x5595c1fab06d ; "I got your message"
│ 0x5595c1faa3a4 89c7 mov edi, eax
│ 0x5595c1faa3a6 b800000000 mov eax, 0
│ 0x5595c1faa3ab e880fcffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ 0x5595c1faa3b0 8985ccfeffff mov dword [var_134h], eax
│ 0x5595c1faa3b6 83bdccfeffff. cmp dword [var_134h], 0
│ ┌─< 0x5595c1faa3bd 7916 jns 0x5595c1faa3d5
│ │ 0x5595c1faa3bf 488d3dba0c00. lea rdi, str.ERROR_writing_to_socket ; 0x5595c1fab080 ; "ERROR writing to socket"
│ │ 0x5595c1faa3c6 e8d5fcffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5595c1faa3cb bf01000000 mov edi, 1
│ │ 0x5595c1faa3d0 e8ebfcffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5595c1faa3d5 b800000000 mov eax, 0
│ 0x5595c1faa3da 488b4df8 mov rcx, qword [var_8h]
│ 0x5595c1faa3de 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x5595c1faa3e7 7405 je 0x5595c1faa3ee
│ │ 0x5595c1faa3e9 e852fcffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x5595c1faa3ee c9 leave
└ 0x5595c1faa3ef c3 ret
[0x5595c1faa1d5]>
En primer lugar, el programa crea un socket de flujo para IPv4:
0x5595c1faa20b e8c0feffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
│ ;-- rip:
│ 0x5595c1faa210 b 8985c0feffff mov dword [var_140h], eax
│ 0x5595c1faa216 83bdc0feffff. cmp dword [var_140h], 0
│ ┌─< 0x5595c1faa21d 7916 jns 0x5595c1faa235
El socket tendrá 0x3 como descriptor:
[0x5595c1faa210]> dr eax
0x00000003
A continuación, la estructura serv_addr (sockaddr_in) se inicializa así:
│ └─> 0x5595c1faa235 488d85d0feff. lea rax, [var_130h]
│ 0x5595c1faa23c 48c700000000. mov qword [rax], 0
│ 0x5595c1faa243 48c740080000. mov qword [rax + 8], 0
│ 0x5595c1faa24b c785c4feffff. mov dword [var_13ch], 0x1389 ; rdi
│ 0x5595c1faa255 66c785d0feff. mov word [var_130h], 2
│ 0x5595c1faa25e c785d4feffff. mov dword [var_12ch], 0
│ 0x5595c1faa268 8b85c4feffff mov eax, dword [var_13ch]
│ 0x5595c1faa26e 0fb7c0 movzx eax, ax
│ 0x5595c1faa271 89c7 mov edi, eax
│ 0x5595c1faa273 e8d8fdffff call sym.imp.htons
│ 0x5595c1faa278 668985d2feff. mov word [var_12eh], ax
│ ;-- rip:
│ 0x5595c1faa27f b 488d8dd0feff. lea rcx, [var_130h]
0x1389 corresponde al puerto 5001. INADDR_ANY se representa habitualmente con un 0, como se ve aquí.
Luego se llama a bind; el programa envía el socket y la dirección junto con el tamaño, que como puedes ver es 16:
│ 0x5595c1faa286 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa28c ba10000000 mov edx, 0x10 ; 16
│ 0x5595c1faa291 4889ce mov rsi, rcx
│ 0x5595c1faa294 89c7 mov edi, eax
│ 0x5595c1faa296 e8f5fdffff call sym.imp.bind ; int bind(int socket, struct sockaddr*address, socklen_t address_len)
│ 0x5595c1faa29b 85c0 test eax, eax
Se devuelve cero, lo que indica que todo ha ido bien:
[0x5595c1faa29b]> dr
rax = 0x00000000
A continuación, listen: escuchará 5 conexiones en el socket que ahora está vinculado a una dirección:
│ └─> 0x5595c1faa2b5 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa2bb be05000000 mov esi, 5
│ 0x5595c1faa2c0 89c7 mov edi, eax
│ 0x5595c1faa2c2 e8b9fdffff call sym.imp.listen
A partir de este punto podemos ver cómo nuestra máquina está escuchando conexiones en ese puerto:
lab@hal9000:~/rev/socket$ netstat -putona | grep "5001"
tcp 0 0 0.0.0.0:5001 0.0.0.0:* LISTEN 19351/./server off (0.00/0/0)
El siguiente paso es aceptar conexiones en ese socket, por lo que llamamos a accept() pasando el socket que está escuchando y punteros a sockaddr.
│ 0x5595c1faa2c7 c785bcfeffff. mov dword [var_144h], 0x10 ; rdx
│ 0x5595c1faa2d1 488d95bcfeff. lea rdx, [var_144h]
│ 0x5595c1faa2d8 488d8de0feff. lea rcx, [var_120h]
│ 0x5595c1faa2df 8b85c0feffff mov eax, dword [var_140h]
│ 0x5595c1faa2e5 4889ce mov rsi, rcx
│ 0x5595c1faa2e8 89c7 mov edi, eax
│ 0x5595c1faa2ea e8c1fdffff call sym.imp.accept
│ 0x5595c1faa2ef 8985c8feffff mov dword [var_138h], eax
│ 0x5595c1faa2f5 83bdc8feffff. cmp dword [var_138h], 0
Devolverá un nuevo descriptor de socket.
Como vamos a leer desde él con la syscall read(), necesitamos reservar espacio para el mensaje:
│ 0x5595c1faa31e b800000000 mov eax, 0
│ 0x5595c1faa323 ba20000000 mov edx, 0x20 ; 32
│ 0x5595c1faa328 4889f7 mov rdi, rsi
│ 0x5595c1faa32b 4889d1 mov rcx, rdx
│ 0x5595c1faa32e f348ab rep stosq qword [rdi], rax
│ 0x5595c1faa331 488d8df0feff. lea rcx, [var_110h]
│ 0x5595c1faa338 8b85c8feffff mov eax, dword [var_138h]
La instrucción rep junto con rax (=0x0) rellenará esa estructura con ceros, repitiendo el movimiento de ceros hasta completarla (https://docs.oracle.com/cd/E19455-01/806-3773/instructionset-64/index.html).
Luego read() leerá desde el socket:
│ 0x5595c1faa331 488d8df0feff. lea rcx, [var_110h]
│ 0x5595c1faa338 8b85c8feffff mov eax, dword [var_138h]
│ 0x5595c1faa33e baff000000 mov edx, 0xff ; rdx
│ 0x5595c1faa343 4889ce mov rsi, rcx
│ 0x5595c1faa346 89c7 mov edi, eax
│ 0x5595c1faa348 b800000000 mov eax, 0
│ 0x5595c1faa34d e81efdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
Como vemos, se pasa el nuevo descriptor de socket:
[0x5595c1faa352]> pxw @ 0x7ffd85c18908
0x7ffd85c18908 0x00000004 0x00007fc3 0x89130002 0x00000000 ................
Esto es relevante porque en tutoriales anteriores usábamos un descriptor referido a un fichero; ahora nos referimos a algo en la red. write() funciona igual.
Y podemos ver el contenido:
[0x5595c1faa352]> afvd
[...]
var var_110h = 0x7ffd85c18930 = (qword)0x554c424b49545241
[0x5595c1faa352]> pxw @ 0x7ffd85c18930
0x7ffd85c18930 0x49545241 0x554c424b 0x00000a45 0x00000000 ARTIKBLUE.......
El programa termina realizando el write sobre ese nuevo socket:
│ 0x5595c1faa392 8b85c8feffff mov eax, dword [var_138h]
│ 0x5595c1faa398 ba12000000 mov edx, 0x12 ; rdx
│ 0x5595c1faa39d 488d35c90c00. lea rsi, str.I_got_your_message ; 0x5595c1fab06d ; "I got your message"
│ 0x5595c1faa3a4 89c7 mov edi, eax
│ 0x5595c1faa3a6 b800000000 mov eax, 0
│ ;-- rip:
│ 0x5595c1faa3ab b e880fcffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
Se usa el mismo socket, esta vez para write. Este es también un concepto importante: el nuevo socket creado al aceptar la conexión con accept() es ahora un socket de flujo asociado a una comunicación real; es el canal de comunicación entre las dos máquinas.
El cliente
Mientras tanto, en el cliente...
Exploremos ahora el programa cliente para ese servidor:
#include <stdio.h>
#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <string.h>
int main(int argc, char *argv[]) {
int sockfd, portno, n;
struct sockaddr_in serv_addr;
struct hostent *server;
char buffer[256];
if (argc < 3) {
fprintf(stderr,"usage %s hostname port\n", argv[0]);
exit(0);
}
portno = atoi(argv[2]);
/* Create a socket point */
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
perror("ERROR opening socket");
exit(1);
}
server = gethostbyname(argv[1]);
if (server == NULL) {
fprintf(stderr,"ERROR, no such host\n");
exit(0);
}
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);
/* Now connect to the server */
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("ERROR connecting");
exit(1);
}
/* Now ask for a message from the user, this message
* will be read by server
*/
printf("Please enter the message: ");
bzero(buffer,256);
fgets(buffer,255,stdin);
/* Send message to the server */
n = write(sockfd, buffer, strlen(buffer));
if (n < 0) {
perror("ERROR writing to socket");
exit(1);
}
/* Now read server response */
bzero(buffer,256);
n = read(sockfd, buffer, 255);
if (n < 0) {
perror("ERROR reading from socket");
exit(1);
}
printf("%s\n",buffer);
return 0;
}
Es otro de esos ejemplos que se encuentran en cualquier sitio. Hace algo muy sencillo y muy similar al servidor: crea un socket, lo conecta al servidor remoto en una IP:puerto, envía un mensaje y espera una respuesta.
Aquí está el desensamblado:
[0x563973801235]> pdf
; DATA XREF from entry0 @ 0x56397380116d
┌ 715: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x563973801235 55 push rbp
│ 0x563973801236 4889e5 mov rbp, rsp
│ 0x563973801239 4881ec500100. sub rsp, 0x150
│ 0x563973801240 89bdbcfeffff mov dword [var_144h], edi ; argc
│ 0x563973801246 4889b5b0feff. mov qword [var_150h], rsi ; argv
│ 0x56397380124d 64488b042528. mov rax, qword fs:[0x28]
│ 0x563973801256 488945f8 mov qword [var_8h], rax
│ 0x56397380125a 31c0 xor eax, eax
│ 0x56397380125c 83bdbcfeffff. cmp dword [var_144h], 2
│ ┌─< 0x563973801263 7f2f jg 0x563973801294
│ │ 0x563973801265 488b85b0feff. mov rax, qword [var_150h]
│ │ 0x56397380126c 488b10 mov rdx, qword [rax]
│ │ 0x56397380126f 488b05ca2d00. mov rax, qword [reloc.stderr] ; [0x563973804040:8]=0
│ │ 0x563973801276 488d35870d00. lea rsi, str.usage__s_hostname_port ; 0x563973802004 ; "usage %s hostname port\n"
│ │ 0x56397380127d 4889c7 mov rdi, rax
│ │ 0x563973801280 b800000000 mov eax, 0
│ │ 0x563973801285 e836feffff call sym.imp.fprintf ; int fprintf(FILE *stream, const char *format, ...)
│ │ 0x56397380128a bf00000000 mov edi, 0
│ │ 0x56397380128f e86cfeffff call sym.imp.exit ; void exit(int status)
│ └─> 0x563973801294 488b85b0feff. mov rax, qword [var_150h]
│ 0x56397380129b 4883c010 add rax, 0x10 ; 16
│ 0x56397380129f 488b00 mov rax, qword [rax]
│ 0x5639738012a2 4889c7 mov rdi, rax
│ 0x5639738012a5 e846feffff call sym.imp.atoi ; int atoi(const char *str)
│ 0x5639738012aa 8985ccfeffff mov dword [var_134h], eax
│ 0x5639738012b0 ba00000000 mov edx, 0
│ 0x5639738012b5 be01000000 mov esi, 1
│ 0x5639738012ba bf02000000 mov edi, 2
│ 0x5639738012bf e86cfeffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
│ 0x5639738012c4 8985d0feffff mov dword [var_130h], eax
│ 0x5639738012ca 83bdd0feffff. cmp dword [var_130h], 0
│ ┌─< 0x5639738012d1 7916 jns 0x5639738012e9
│ │ 0x5639738012d3 488d3d420d00. lea rdi, str.ERROR_opening_socket ; 0x56397380201c ; "ERROR opening socket"
│ │ 0x5639738012da e801feffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5639738012df bf01000000 mov edi, 1
│ │ 0x5639738012e4 e817feffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5639738012e9 488b85b0feff. mov rax, qword [var_150h]
│ 0x5639738012f0 4883c008 add rax, 8
│ 0x5639738012f4 488b00 mov rax, qword [rax]
│ 0x5639738012f7 4889c7 mov rdi, rax
│ 0x5639738012fa e8b1fdffff call sym.imp.gethostbyname
│ 0x5639738012ff 488985d8feff. mov qword [var_128h], rax
│ 0x563973801306 4883bdd8feff. cmp qword [var_128h], 0
│ ┌─< 0x56397380130e 752a jne 0x56397380133a
│ │ 0x563973801310 488b05292d00. mov rax, qword [reloc.stderr] ; [0x563973804040:8]=0
│ │ 0x563973801317 4889c1 mov rcx, rax
│ │ 0x56397380131a ba14000000 mov edx, 0x14 ; 20
│ │ 0x56397380131f be01000000 mov esi, 1
│ │ 0x563973801324 488d3d060d00. lea rdi, str.ERROR__no_such_host ; 0x563973802031 ; "ERROR, no such host\n"
│ │ 0x56397380132b e8f0fdffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ │ 0x563973801330 bf00000000 mov edi, 0
│ │ 0x563973801335 e8c6fdffff call sym.imp.exit ; void exit(int status)
│ └─> 0x56397380133a 488d85e0feff. lea rax, [var_120h]
│ 0x563973801341 48c700000000. mov qword [rax], 0
│ 0x563973801348 48c740080000. mov qword [rax + 8], 0
│ 0x563973801350 66c785e0feff. mov word [var_120h], 2
│ 0x563973801359 488b85d8feff. mov rax, qword [var_128h]
│ 0x563973801360 8b4014 mov eax, dword [rax + 0x14]
│ 0x563973801363 4863d0 movsxd rdx, eax
│ 0x563973801366 488b85d8feff. mov rax, qword [var_128h]
│ 0x56397380136d 488b4018 mov rax, qword [rax + 0x18]
│ 0x563973801371 488b00 mov rax, qword [rax]
│ 0x563973801374 488d8de0feff. lea rcx, [var_120h]
│ 0x56397380137b 4883c104 add rcx, 4
│ 0x56397380137f 4889c6 mov rsi, rax
│ 0x563973801382 4889cf mov rdi, rcx
│ 0x563973801385 e846fdffff call sym.imp.memmove ; void *memmove(void *s1, const void *s2, size_t n)
│ 0x56397380138a 8b85ccfeffff mov eax, dword [var_134h]
│ 0x563973801390 0fb7c0 movzx eax, ax
│ 0x563973801393 89c7 mov edi, eax
│ 0x563973801395 e8d6fcffff call sym.imp.htons
│ 0x56397380139a 668985e2feff. mov word [var_11eh], ax
│ 0x5639738013a1 488d8de0feff. lea rcx, [var_120h]
│ 0x5639738013a8 8b85d0feffff mov eax, dword [var_130h]
│ 0x5639738013ae ba10000000 mov edx, 0x10 ; 16
│ 0x5639738013b3 4889ce mov rsi, rcx
│ 0x5639738013b6 89c7 mov edi, eax
│ 0x5639738013b8 e853fdffff call sym.imp.connect ; ssize_t connect(int socket, void *addr, size_t addrlen)
│ 0x5639738013bd 85c0 test eax, eax
│ ┌─< 0x5639738013bf 7916 jns 0x5639738013d7
│ │ 0x5639738013c1 488d3d7e0c00. lea rdi, str.ERROR_connecting ; 0x563973802046 ; "ERROR connecting"
│ │ 0x5639738013c8 e813fdffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5639738013cd bf01000000 mov edi, 1
│ │ 0x5639738013d2 e829fdffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5639738013d7 488d3d790c00. lea rdi, str.Please_enter_the_message: ; 0x563973802057 ; "Please enter the message: "
│ 0x5639738013de b800000000 mov eax, 0
│ 0x5639738013e3 e898fcffff call sym.imp.printf ; int printf(const char *format)
│ 0x5639738013e8 488d85f0feff. lea rax, [var_110h]
│ 0x5639738013ef 4889c6 mov rsi, rax
│ 0x5639738013f2 b800000000 mov eax, 0
│ 0x5639738013f7 ba20000000 mov edx, 0x20 ; 32
│ 0x5639738013fc 4889f7 mov rdi, rsi
│ 0x5639738013ff 4889d1 mov rcx, rdx
│ 0x563973801402 f348ab rep stosq qword [rdi], rax
│ 0x563973801405 488b15142c00. mov rdx, qword [reloc.stdin] ; [0x563973804020:8]=0
│ 0x56397380140c 488d85f0feff. lea rax, [var_110h]
│ 0x563973801413 beff000000 mov esi, 0xff ; 255
│ 0x563973801418 4889c7 mov rdi, rax
│ 0x56397380141b e880fcffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
│ 0x563973801420 488d85f0feff. lea rax, [var_110h]
│ 0x563973801427 4889c7 mov rdi, rax
│ 0x56397380142a e821fcffff call sym.imp.strlen ; size_t strlen(const char *s)
│ 0x56397380142f 4889c2 mov rdx, rax
│ 0x563973801432 488d8df0feff. lea rcx, [var_110h]
│ 0x563973801439 8b85d0feffff mov eax, dword [var_130h]
│ 0x56397380143f 4889ce mov rsi, rcx
│ 0x563973801442 89c7 mov edi, eax
│ 0x563973801444 b800000000 mov eax, 0
│ 0x563973801449 e8f2fbffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ 0x56397380144e 8985d4feffff mov dword [var_12ch], eax
│ 0x563973801454 83bdd4feffff. cmp dword [var_12ch], 0
│ ┌─< 0x56397380145b 7916 jns 0x563973801473
│ │ 0x56397380145d 488d3d0e0c00. lea rdi, str.ERROR_writing_to_socket ; 0x563973802072 ; "ERROR writing to socket"
│ │ 0x563973801464 e877fcffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x563973801469 bf01000000 mov edi, 1
│ │ 0x56397380146e e88dfcffff call sym.imp.exit ; void exit(int status)
│ └─> 0x563973801473 488d85f0feff. lea rax, [var_110h]
│ 0x56397380147a 4889c6 mov rsi, rax
│ 0x56397380147d b800000000 mov eax, 0
│ 0x563973801482 ba20000000 mov edx, 0x20 ; 32
│ 0x563973801487 4889f7 mov rdi, rsi
│ 0x56397380148a 4889d1 mov rcx, rdx
│ 0x56397380148d f348ab rep stosq qword [rdi], rax
│ 0x563973801490 488d8df0feff. lea rcx, [var_110h]
│ 0x563973801497 8b85d0feffff mov eax, dword [var_130h]
│ 0x56397380149d baff000000 mov edx, 0xff ; 255
│ 0x5639738014a2 4889ce mov rsi, rcx
│ 0x5639738014a5 89c7 mov edi, eax
│ 0x5639738014a7 b800000000 mov eax, 0
│ 0x5639738014ac e8dffbffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ 0x5639738014b1 8985d4feffff mov dword [var_12ch], eax
│ 0x5639738014b7 83bdd4feffff. cmp dword [var_12ch], 0
│ ┌─< 0x5639738014be 7916 jns 0x5639738014d6
│ │ 0x5639738014c0 488d3dc30b00. lea rdi, str.ERROR_reading_from_socket ; 0x56397380208a ; "ERROR reading from socket"
│ │ 0x5639738014c7 e814fcffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5639738014cc bf01000000 mov edi, 1
│ │ 0x5639738014d1 e82afcffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5639738014d6 488d85f0feff. lea rax, [var_110h]
│ 0x5639738014dd 4889c7 mov rdi, rax
│ 0x5639738014e0 e84bfbffff call sym.imp.puts ; int puts(const char *s)
│ 0x5639738014e5 b800000000 mov eax, 0
│ 0x5639738014ea 488b75f8 mov rsi, qword [var_8h]
│ 0x5639738014ee 644833342528. xor rsi, qword fs:[0x28]
│ ┌─< 0x5639738014f7 7405 je 0x5639738014fe
│ │ 0x5639738014f9 e862fbffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x5639738014fe c9 leave
└ 0x5639738014ff c3 ret
[0x563973801235]>
Inspeccionemos el programa.
Como al tratar con muchos programas grandes, un enfoque eficaz es centrarse en las syscalls relevantes analizando su contexto.
Empecemos con socket():
│ 0x5639738012b0 ba00000000 mov edx, 0
│ 0x5639738012b5 be01000000 mov esi, 1
│ 0x5639738012ba bf02000000 mov edi, 2
│ 0x5639738012bf e86cfeffff call sym.imp.socket ; int socket(int domain, int type, int protocol)
Como de costumbre, definimos un socket AF_INET, SOCK_STREAM:
[0x55bd5654d2c4]> dr rax
0x00000003
Y obtenemos el descriptor de socket como de costumbre:
A continuación tenemos esto:
│ └─> 0x55bd5654d2e9 488b85b0feff. mov rax, qword [var_150h]
│ 0x55bd5654d2f0 4883c008 add rax, 8
│ 0x55bd5654d2f4 488b00 mov rax, qword [rax]
│ 0x55bd5654d2f7 4889c7 mov rdi, rax
│ 0x55bd5654d2fa e8b1fdffff call sym.imp.gethostbyname
gethostbyname resuelve un nombre y devuelve información en una estructura hostent. El ejemplo utiliza esa API heredada para obtener la dirección que incorporará al socket. Para código nuevo, getaddrinfo permite trabajar con IPv4 e IPv6.
[0x55bd5654d2fa]> dr
rax = 0x7ffd9295a32f
[0x55bd5654d2fa]> pxw @ 0x7ffd9295a32f
0x7ffd9295a32f 0x2e373231 0x2e302e30 0x30350031 0x53003130 127.0.0.1.5001.S
Después configurará la estructura para la dirección del servidor (sockaddr_in) usando los datos devueltos por gethostbyname():
│ 0x55bd5654d341 48c700000000. mov qword [rax], 0
│ 0x55bd5654d348 48c740080000. mov qword [rax + 8], 0
│ 0x55bd5654d350 66c785e0feff. mov word [var_120h], 2
│ 0x55bd5654d359 488b85d8feff. mov rax, qword [var_128h]
│ 0x55bd5654d360 8b4014 mov eax, dword [rax + 0x14]
│ 0x55bd5654d363 4863d0 movsxd rdx, eax
│ 0x55bd5654d366 488b85d8feff. mov rax, qword [var_128h]
│ 0x55bd5654d36d 488b4018 mov rax, qword [rax + 0x18]
│ 0x55bd5654d371 488b00 mov rax, qword [rax]
│ 0x55bd5654d374 488d8de0feff. lea rcx, [var_120h]
│ 0x55bd5654d37b 4883c104 add rcx, 4
│ 0x55bd5654d37f 4889c6 mov rsi, rax
│ 0x55bd5654d382 4889cf mov rdi, rcx
│ 0x55bd5654d385 e846fdffff call sym.imp.memmove ; void *memmove(void *s1, const void *s2, size_t n)
Tras eso se llama a htons para el puerto:
│ 0x55bd5654d38a b 8b85ccfeffff mov eax, dword [var_134h]
│ 0x55bd5654d390 0fb7c0 movzx eax, ax
│ 0x55bd5654d393 89c7 mov edi, eax
│ 0x55bd5654d395 e8d6fcffff call sym.imp.htons
│ ;-- rip:
│ 0x55bd5654d39a b 668985e2feff. mov word [var_11eh], ax
[0x55bd5654d39a]> dr rax
0x00008913
0x8913 corresponde a 5001 en decimal:
Y ahora vemos connect():
│ 0x55bd5654d3b8 e853fdffff call sym.imp.connect ; ssize_t connect(int socket, void *addr, size_t addrlen)
│ 0x55bd5654d3bd 85c0 test eax, eax
El socket se conectará al servidor, que ahora aceptará la conexión con accept().
El programa, ya conectado al servidor remoto, usará write sobre el descriptor de socket para enviar el mensaje:
│ 0x55bd5654d42f 4889c2 mov rdx, rax
│ 0x55bd5654d432 488d8df0feff. lea rcx, [var_110h]
│ 0x55bd5654d439 8b85d0feffff mov eax, dword [var_130h]
│ 0x55bd5654d43f 4889ce mov rsi, rcx
│ 0x55bd5654d442 89c7 mov edi, eax
│ 0x55bd5654d444 b800000000 mov eax, 0
│ 0x55bd5654d449 e8f2fbffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
Más adelante en el código, leerá la respuesta tras reservar espacio en un buffer, como vimos en el servidor:
│ 0x55bd5654d47a 4889c6 mov rsi, rax
│ 0x55bd5654d47d b800000000 mov eax, 0
│ 0x55bd5654d482 ba20000000 mov edx, 0x20 ; 32
│ 0x55bd5654d487 4889f7 mov rdi, rsi
│ 0x55bd5654d48a 4889d1 mov rcx, rdx
│ 0x55bd5654d48d f348ab rep stosq qword [rdi], rax
│ 0x55bd5654d490 488d8df0feff. lea rcx, [var_110h]
│ 0x55bd5654d497 8b85d0feffff mov eax, dword [var_130h]
│ 0x55bd5654d49d baff000000 mov edx, 0xff ; 255
│ 0x55bd5654d4a2 4889ce mov rsi, rcx
│ 0x55bd5654d4a5 89c7 mov edi, eax
│ 0x55bd5654d4a7 b800000000 mov eax, 0
│ 0x55bd5654d4ac e8dffbffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
0x7ffd92958e50 0x89130002 0x0100007f 0x00000000 0x00000000 ................
0x7ffd92958e60 0x6f672049 0x6f792074 0x6d207275 0x61737365 I got your messa
0x7ffd92958e70 0x00006567 0x00000000 0x00000000 0x00000000 ge..............
En una segunda parte estudiaremos ejemplos más avanzados, entre ellos cómo el malware usa esto para implementar mecanismos de C&C mediante peticiones GET o DNS falsificadas. También veremos cómo tratar todo esto en Windows.
Leer el orden de los bytes.
El programa está detenido después de escribir 0x0102. Hermes consulta los dos bytes de la unión en la memoria del proceso y describe el orden observado.
Leer la explicación y los comandos
La consulta
El programa está detenido justo después de guardar 0x0102 en la unión. Lee los dos bytes del objeto, situado en RBP menos 10, usando el registro real del depurador. Enuméralos en orden de direcciones crecientes. ¿Qué orden de bytes observas? No continúes la ejecución. Máximo 80 palabras.
Qué muestra la sesión
La unión recibe el valor 0x0102. El programa consulta por separado el byte situado en la dirección inicial y el situado una posición después.
En la compilación x86-64, los bytes del valor se almacenan como 02 y 01 en direcciones crecientes. Las lecturas movzx y las comparaciones sobre AL permiten reconocer la rama que imprime little-endian.
La consola permite repetir la lectura sobre el mismo proceso detenido. El breakpoint se prepara antes de la grabación, justo después de la escritura del valor.
Comprobación desde la consola
Desde la consola del companion se revisan estas instrucciones y datos:
dr rbp
px 2 @ 0x7ffd5e6b7e56
pd 10 @ 0x401178
La dirección de pila cambia entre ejecuciones. En esta compilación, el objeto comienza diez bytes antes del valor actual de RBP. El proceso permanece detenido durante la lectura. Preparar el companion ↗
Vuelve al binario
Reconocer estructuras de direcciones, orden de bytes y el ciclo de vida de una conexión TCP. Identifica al menos una instrucción y el dato que utiliza; conserva la compilación con la que lo has observado.
Puedes utilizar la consola del companion sin conversar con el agente, o abrir el ejecutable en tu desensamblador habitual.
./coursectl start 15 --example byte-order
./coursectl consoleCierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.
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.