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.
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 GitHubPrincipe
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.
- Préparation. L'adresse IP est lue avec
scanf, et un tableauportscontient les numéros à tester. - Une socket par port.
socket()crée une socket TCP, et une structuresockaddr_inreçoit l'adresse (inet_addr) et le port (htons). - Connexion avec timeout.
connect_with_timeoutpasse la socket en mode non bloquant et lanceconnect(), qui rend la main aussitôt (EINPROGRESS).select()attend ensuite que la socket soit prête, ou que les 2 secondes soient écoulées. - Verdict.
getsockoptavecSO_ERRORdonne 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
#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;
}