kuji@portfolio:~/projets$ cat portscan-v1.md

Scanneur de port, v1 : séquentiel

Première version : un scanner TCP en C qui teste les ports un par un, avec des connexions non bloquantes et un timeout.

C Réseau

Pourquoi ce projet

En CTF, j'ai remarqué que nmap devenait lent dès qu'on lui demande de scanner beaucoup de ports. L'idée : un scanner volontairement basique mais rapide, pour dégrossir la liste des ports ouverts et alléger le travail de nmap en amont, avant de le lancer pour une analyse plus fine sur les ports qui le méritent.

Débogage : Le port fantôme

En testant rigoureusement le programme avec un serveur d'écoute placé sur le port 10000, je me suis rendu compte que mes versions ultérieures (v2 et v3) le détectaient, mais que cette v1 passait complètement à côté. Le diagnostic a révélé un grand classique du C : une erreur d'indexation (off-by-one error).

Le tableau ports était alloué correctement, mais la boucle de remplissage allait de 1 à 9999. L'indice 0 n'était jamais initialisé, et le port 10000 n'était jamais stocké. Le correctif a tenu en une ligne lors de l'initialisation du tableau : ports[i] = i + 1; (avec i allant de 0 à 9999). Un rappel parfait de l'importance des tests unitaires sur les bornes !

Source

Lien vers le repo GitHub

Principe

Le programme demande une adresse IP, puis tente d'ouvrir une connexion TCP vers chaque port, l'un après l'autre. Si la connexion aboutit, le port est ouvert : un service écoute derrière.

  1. Préparation. L'adresse IP est lue avec scanf, et un tableau ports contient les numéros à tester.
  2. Une socket par port. socket() crée une socket TCP, et une structure sockaddr_in reçoit l'adresse (inet_addr) et le port (htons).
  3. Connexion avec timeout. connect_with_timeout passe la socket en mode non bloquant et lance connect(), qui rend la main aussitôt (EINPROGRESS). select() attend ensuite que la socket soit prête, ou que les 2 secondes soient écoulées.
  4. Verdict. getsockopt avec SO_ERROR donne le résultat de la connexion : 0 si elle a réussi, un code d'erreur sinon. Un port ouvert est affiché, puis la socket est fermée.

Le mode non bloquant est la clé. Un connect() classique sur un port filtré par un pare-feu peut rester bloqué très longtemps, car l'expiration par défaut du système se compte en minutes. Avec select(), c'est le programme qui fixe sa limite d'attente. Trois cas sont possibles : le port est ouvert (la connexion aboutit), fermé (la machine refuse la connexion, et SO_ERROR contient l'erreur), ou filtré (aucune réponse, et c'est le timeout qui tranche).

Utilisation

$ gcc mapping_network.c -o mapping_network
$ ./mapping_network
IP Address to scan :127.0.0.1
BEGINNING IP SCAN : 127.0.0.1
--------------------------
[+] PORT 8080 : OPENED
[+] PORT 9000 : OPENED
--------------------------
SCAN FINISHED.

Exemple avec deux serveurs locaux, sur les ports 8080 et 9000. Dans cette version, seuls les ports ouverts sont affichés.

Limites

Le scan est séquentiel : le programme attend la réponse d'un port avant de passer au suivant. Un port filtré coûte jusqu'à 2 secondes, soit plus de 5 heures dans le pire des cas pour 10 000 ports (10 000 × 2 s). Presque tout ce temps est passé à attendre le réseau et non à calculer, ce qui rend le problème idéal à paralléliser. C'est l'objet de la version 2.

Code source

mapping_network.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/types.h>
#include <pthread.h>

int connect_with_timeout(int s, struct sockaddr_in* addr, int timeout_sec){
    int flags = fcntl(s, F_GETFL, 0);
    fcntl(s, F_SETFL, flags | O_NONBLOCK);

    int res = connect(s, (struct sockaddr*)addr, sizeof(*addr));
    if (res < 0 && errno != EINPROGRESS){
        return -1;
    }

    if (res == 0){
        fcntl(s, F_SETFL, flags);
        return 0;
    }

    fd_set fdset;
    FD_ZERO(&fdset);
    FD_SET(s, &fdset);

    struct timeval tv;
    tv.tv_sec = timeout_sec;
    tv.tv_usec = 0;

    res = select(s + 1, NULL, &fdset, NULL, &tv);

    fcntl(s, F_SETFL, flags);

    if (res <= 0){
        return -1;
    }
    int so_error;
    socklen_t len = sizeof(so_error);
    getsockopt(s, SOL_SOCKET, SO_ERROR, &so_error, &len);
    if (so_error != 0){
        return -1;
    }

    return 0;
}

int main(){
    printf("IP Address to scan :");
    char ip_cible[20];
    scanf("%19s", ip_cible);
    int* ports = malloc(sizeof(int)*10001);
    for(int i = 0; i < 10000; i++){
        ports[i] = i+1;
    }
    int n_port = 10000;
    struct sockaddr_in configuration_serveur;

    printf("BEGINNING IP SCAN : %s\n", ip_cible);
    printf("--------------------------\n");
    for(int i = 0; i < n_port; i++){
        int current_port = ports[i];

        int s = socket(AF_INET, SOCK_STREAM, 0);
        if(s < 0){
            printf("Error while creating socket. Port %d\n", current_port);
            continue;
        }

        memset(&configuration_serveur, 0, sizeof(configuration_serveur));
        configuration_serveur.sin_addr.s_addr = inet_addr(ip_cible);
        configuration_serveur.sin_family = AF_INET;
        configuration_serveur.sin_port = htons(current_port);

        if (connect_with_timeout(s, &configuration_serveur, 2) == 0){
            printf("[+] PORT %d : OPENED\n", current_port);
        }
        else{

        }

        close(s);
    }

    printf("--------------------------\n");
    printf("SCAN FINISHED.\n");
    free(ports);
    return 0;
}