Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Sistemas y comunicaciones / 17

Sockets en Unix

LoberaFormación técnica17 / 33 · 11 min de lectura · Linux / Windows · x86-64
17 / 33

Reconocer estructuras de direcciones, orden de bytes y el ciclo de vida de una conexión TCP.

Consultar conceptos ↗

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:

Conexión TCP / recorrido básico
ServidorCliente
Crear socketCrear socket
Asociar dirección y escucharPreparar la dirección de destino
Aceptar conexiónConectar
Recibir / enviarEnviar / recibir
CerrarCerrar

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:

C / EJEMPLO
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:

C / EJEMPLO
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:

C / EJEMPLO
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:

CÓDIGO / SALIDA
htons() host to network short
htonl() host to network long
ntohl() network to host long
ntohs() network to host short
C / EJEMPLO
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[0x55d1f3ec0169]> pxw @ 0x7ffeee763916
0x7ffeee763916  0x51000102 0x6efd5e62 0x01c009a0 0x55d1f3ec  ...Qb^.n.......U

Y luego lo cargamos en al y aquí lo tenemos:

CÓDIGO / SALIDA
[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:

C / EJEMPLO
#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);
}
C / EJEMPLO
[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:

CÓDIGO / SALIDA
[0x55fdbfcb3197]> dr rdi
0x55fdbfcb4004
[0x55fdbfcb3197]> pxw @ 0x55fdbfcb4004
0x55fdbfcb4004  0x312e3836 0x312e3837 0x312e3735 0x00003233  68.178.157.132..

Tras la llamada, la variable se actualiza así:

CÓDIGO / SALIDA
[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í:

C / EJEMPLO
#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):

C / EJEMPLO
#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.

C / EJEMPLO
#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.
C / EJEMPLO
#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.

C / EJEMPLO
#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.

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

C / EJEMPLO
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.

C
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[0x5595c1faa210]> dr eax
0x00000003

A continuación, la estructura serv_addr (sockaddr_in) se inicializa así:

ENSAMBLADOR / REFERENCIA
       └─> 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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[0x5595c1faa29b]> dr
rax = 0x00000000

A continuación, listen: escuchará 5 conexiones en el socket que ahora está vinculado a una dirección:

ENSAMBLADOR / REFERENCIA
       └─> 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:

CÓDIGO / SALIDA
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.

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[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:

CÓDIGO / SALIDA
[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:

ENSAMBLADOR / REFERENCIA
           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:

C / EJEMPLO
#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:

C / EJEMPLO
[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():

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[0x55bd5654d2c4]> dr rax
0x00000003

Y obtenemos el descriptor de socket como de costumbre:

A continuación tenemos esto:

ENSAMBLADOR / REFERENCIA
       └─> 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.

CÓDIGO / SALIDA
[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():

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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():

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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)
CÓDIGO / SALIDA
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.

Sesión en vídeo / Companion0:52

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.

Captura de una sesión real de Hermes y radare2. Sin audio; esperas abreviadas.Descargar vídeo ↙
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:

CÓDIGO / SALIDA
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 ↗

Práctica manual

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.

TERMINAL / COMPANION
./coursectl start 15 --example byte-order
./coursectl console

Cierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.

Comprobación personal

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.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.