编写网络模拟器笔记-全-

编写网络模拟器笔记(全)

001:C语言编写网络安全网络映射器

在本节课中,我们将要学习如何使用C语言编写一个基础的网络映射器。网络映射器是一种安全工具,用于扫描网络,发现活跃的主机及其开放的端口。我们将从最简单的版本开始,逐步构建一个功能完整的工具。

概述

网络映射器,有时也称为端口扫描器,是系统管理员和安全人员用来检查网络安全的工具。它可以扫描指定IP地址范围内的主机,尝试连接到特定的TCP端口(如SSH的22端口),并记录哪些端口是开放的。本节课我们将创建一个名为“Nema”的网络映射器。

项目结构与准备

在开始编码之前,我们需要建立项目结构并准备一些工具库。我们将创建一个名为 birchutils 的通用工具库,用于存放项目中反复使用的函数。

创建工具库

首先,我们为工具库创建一个 Makefile 文件,用于编译和构建库。

# birchutils/Makefile
CFLAGS = -O3 -Wall -std=c2x
LDFLAGS = -shared -fPIC

birchutils.o: birchutils.c
	$(CC) $(CFLAGS) -c birchutils.c -o birchutils.o

birchutils.so: birchutils.o
	$(CC) $(CFLAGS) $(LDFLAGS) birchutils.o -o birchutils.so

all: clean birchutils.so

clean:
	rm -f birchutils.o birchutils.so

创建网络映射器项目

接下来,我们为网络映射器项目创建目录和 Makefile。我们将开发多个版本,因此需要组织好代码结构。

# netmap/Makefile
CFLAGS = -O3 -Wall -std=c2x
LDFLAGS =

netmap_naive.o: netmap_naive.c
	$(CC) $(CFLAGS) -c netmap_naive.c -o netmap_naive.o

netmap_naive: netmap_naive.o
	$(CC) $(CFLAGS) netmap_naive.o -o netmap_naive $(LDFLAGS)

all: clean netmap_naive

clean:
	rm -f netmap_naive.o netmap_naive

编写基础代码框架

现在,让我们开始编写网络映射器的基础代码。我们将首先处理命令行参数,并定义一些必要的数据类型和全局变量。

包含头文件与类型定义

netmap_naive.c 文件中,我们首先包含必要的头文件,并定义一些常用的数据类型。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "netmap.h"

typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

处理命令行参数

我们的工具需要接受命令行参数,包括端口号、起始IP地址和结束IP地址。以下是处理这些参数的代码。

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <port> [start_ip] [end_ip]\n", argv[0]);
        fprintf(stderr, "If only port is provided, IPs will be read from stdin.\n");
        fprintf(stderr, "If start_ip is provided without end_ip, scan to 255.255.255.255.\n");
        return -1;
    }

    u16 port = (u16)atoi(argv[1]);
    struct in_addr current_ip, end_ip;

    // 解析起始IP地址
    if (argc >= 3) {
        if (inet_aton(argv[2], &current_ip) == 0) {
            fprintf(stderr, "Invalid start IP address.\n");
            return -1;
        }
    } else {
        // 如果没有提供起始IP,则从标准输入读取
        // 此处留待后续实现
    }

    // 解析结束IP地址
    if (argc >= 4) {
        if (inet_aton(argv[3], &end_ip) == 0) {
            fprintf(stderr, "Invalid end IP address.\n");
            return -1;
        }
    } else {
        // 如果没有提供结束IP,则默认扫描到255.255.255.255
        end_ip.s_addr = 0xFFFFFFFF;
    }

    // 简单的范围检查
    if (current_ip.s_addr > end_ip.s_addr) {
        fprintf(stderr, "Start IP must be less than or equal to end IP.\n");
        return -1;
    }

    printf("Scanning port %d from %s to %s\n", 
           port, 
           inet_ntoa(current_ip), 
           inet_ntoa(end_ip));

    // 后续扫描逻辑将在这里实现
    return 0;
}

IP地址生成器

为了遍历IP地址范围,我们需要一个生成器函数。我们将使用全局变量来跟踪当前IP地址。

// 全局变量,存储当前和结束IP地址
struct in_addr current_ip;
struct in_addr end_ip;

// IP地址生成器函数
struct in_addr generate_ip() {
    struct in_addr next_ip = current_ip;

    // 如果当前IP已经达到或超过结束IP,则返回0表示结束
    if (current_ip.s_addr >= end_ip.s_addr) {
        next_ip.s_addr = 0;
        return next_ip;
    }

    // 将当前IP地址加1
    current_ip.s_addr = htonl(ntohl(current_ip.s_addr) + 1);
    return next_ip;
}

实现简单的扫描逻辑

上一节我们介绍了如何解析参数和生成IP地址,本节中我们来看看如何实现最基础的扫描逻辑。我们将使用阻塞式套接字,逐个尝试连接目标IP和端口。

扫描单个IP地址

首先,我们编写一个函数,用于尝试连接指定的IP地址和端口。

int scan_single_ip(struct in_addr ip, u16 port) {
    int sockfd;
    struct sockaddr_in target_addr;

    // 创建TCP套接字
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        return -1;
    }

    // 设置目标地址结构
    target_addr.sin_family = AF_INET;
    target_addr.sin_port = htons(port);
    target_addr.sin_addr = ip;

    // 设置连接超时(可选,但建议)
    struct timeval timeout;
    timeout.tv_sec = 2; // 2秒超时
    timeout.tv_usec = 0;
    setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout));

    // 尝试连接
    if (connect(sockfd, (struct sockaddr *)&target_addr, sizeof(target_addr)) < 0) {
        close(sockfd);
        return 0; // 连接失败,端口可能关闭
    }

    // 连接成功,端口开放
    printf("Port %d is OPEN on %s\n", port, inet_ntoa(ip));
    close(sockfd);
    return 1; // 端口开放
}

主扫描循环

接下来,我们在 main 函数中集成IP生成器和扫描函数,实现主扫描循环。

int main(int argc, char *argv[]) {
    // ... [之前的参数解析和初始化代码] ...

    printf("Starting naive network mapper scan...\n");

    int open_ports = 0;
    struct in_addr target_ip;

    // 初始化当前IP
    current_ip = start_ip; // 假设start_ip已从参数解析

    while (1) {
        target_ip = generate_ip();
        if (target_ip.s_addr == 0) {
            break; // 生成器返回0,表示扫描结束
        }

        if (scan_single_ip(target_ip, port) > 0) {
            open_ports++;
        }
    }

    printf("Scan completed. Found %d open ports.\n", open_ports);
    return 0;
}

总结

本节课中我们一起学习了如何使用C语言编写一个基础版的网络映射器。我们完成了以下工作:

  1. 建立项目结构:创建了工具库和主项目的 Makefile
  2. 处理命令行参数:使工具能够接受端口号、起始和结束IP地址作为输入。
  3. 实现IP地址生成器:编写了函数来遍历指定的IP地址范围。
  4. 实现基础扫描逻辑:使用阻塞式套接字逐个尝试连接目标主机的指定端口,并报告开放端口。

这个初始版本非常简单,并且由于使用阻塞式调用和顺序扫描,速度会很慢。然而,它为我们理解网络扫描的基本原理奠定了坚实的基础。在接下来的课程中,我们将逐步改进这个工具,引入非阻塞套接字、多线程等技术,使其变得更高效、更实用。


002:用C编写简单的网络映射器

在本节课中,我们将继续构建一个简易的网络映射器。这个工具的主要功能是发现网络上的服务器和节点,并检查其服务的版本等信息。上一节我们完成了基础框架,本节我们将实现IP地址生成和TCP连接功能。

概述与回顾

上一节我们创建了主函数,它接收一个端口号、一个起始IP地址和一个结束IP地址作为参数。程序将扫描这个IP地址范围。如果省略结束IP地址,则只扫描起始地址所在的子网。如果两个IP地址都省略,程序将从标准输入读取IP地址。

实现IP地址生成器

首先,我们需要完善generate函数,使其能够处理从标准输入读取IP地址的情况。

以下是generate函数的核心逻辑,当起始和结束地址均为0时,从标准输入读取:

in_addr_t generate(...) {
    if (current == 0 && ending == 0) {
        // 从标准输入读取IP地址
        in8 buff[16];
        zero(buff, 16);
        fgets((char*)buff, 15, stdin);
        // ... 处理buff,转换为in_addr_t并返回
    }
    // ... 原有的范围生成逻辑
}

为了辅助内存清零,我们创建了一个工具函数zero,并将其放入自定义的工具库birch_utils中。

// birch_utils.h 或类似头文件中
#ifndef BIRCH_UTILS_H
#define BIRCH_UTILS_H

#include <stdio.h>
#include <string.h>

typedef unsigned char in8;
typedef unsigned short in16;
typedef unsigned int in32;

void zero(in8* str, in16 size);

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/e47078a6dc7c2d22a36e3072a2a75fd0_27.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/e47078a6dc7c2d22a36e3072a2a75fd0_29.png)

#endif

// zero函数实现
void zero(in8* str, in16 size) {
    in16 n = 0;
    in8* p = str;
    while (n < size) {
        *p = 0;
        p++;
        n++;
    }
}

实现TCP连接功能

有了IP地址生成器,下一步是实现一个函数来尝试连接到给定的IP地址和端口。

我们创建一个名为tcp_connect的函数,它接收一个in_addr_t类型的IP地址和一个in16类型的端口号。

以下是该函数的基本步骤:

  1. 创建套接字。
  2. 填充sockaddr_in结构体。
  3. 尝试连接。
  4. 如果连接成功,则尝试读取服务横幅(banner);如果失败,则关闭套接字并返回。
bool tcp_connect(in_addr_t ip, in16 port) {
    int s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
        // 处理套接字创建失败
        return false;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = ip;

    int ret = connect(s, (struct sockaddr*)&addr, sizeof(addr));
    if (ret != 0) {
        close(s);
        return false; // 连接失败
    }

    // 连接成功,读取横幅
    read_header(s, ip);
    close(s);
    return true;
}

读取服务横幅

连接建立后,我们尝试读取服务可能发送的初始数据(例如SSH或HTTP服务的欢迎信息),这有助于识别服务类型和版本。

我们创建一个read_header函数来执行此操作:

void read_header(int s, in_addr_t ip) {
    in8 buff[256];
    zero(buff, 256);
    int i = read(s, buff, 255);

    if (i < 1) {
        // 没有读到数据,只打印IP地址
        printf("IP: %x\n", ip);
    } else {
        // 处理读取到的数据,例如去除换行符
        in8* p = buff + i - 1;
        if (*p == '\n' || *p == '\r') {
            *p = 0;
        }
        printf("IP: %x | Header: %s\n", ip, buff);
    }
}

整合主循环

最后,我们在主函数中整合所有部分,形成一个完整的扫描循环。

int main(int argc, char* argv[]) {
    // ... 参数解析,获取端口、起始IP、结束IP
    in_addr_t ip;
    while ((ip = generate(...)) != 0) {
        tcp_connect(ip, port);
    }
    return 0;
}

测试与展望

现在,我们的简易网络映射器已经可以工作了。我们可以通过管道将IP地址列表传递给它进行测试,例如:

echo "127.0.0.1" | ./naive_scanner 80

程序会尝试连接本机的80端口并打印结果。

当前的实现使用的是阻塞式套接字,在扫描多个地址时速度会很慢,因为每次连接尝试都会等待超时或响应。在下一节课中,我们将着手改进这一点。

总结

本节课中我们一起学习了如何完善一个简易网络映射器。我们实现了从标准输入和IP范围生成地址的逻辑,创建了进行TCP连接并尝试读取服务横幅的核心功能,并将所有模块整合到一个可以运行的程序中。虽然当前版本是“朴素”且低效的,但它为我们后续实现更快速、更强大的版本(例如使用非阻塞I/O或设置连接超时)奠定了坚实的基础。

003:编写一个网络模拟器

概述

在本节课中,我们将学习如何编写一个网络模拟器。这个模拟器能够分配大量IP地址,并在不同的TCP端口上创建虚拟服务。当连接到这些服务时,模拟器会返回一些数据。我们将使用Linux Shell脚本(Bash)来实现这个项目,因为它对于此类任务来说简单高效。


回顾与准备

上一节我们完成了网络扫描器的初始版本。为了继续完善扫描器,我们需要一个模拟的网络环境来进行端口扫描。因此,本节我们将专注于创建这个网络模拟器。

但在开始之前,我们需要先为网络扫描器添加一个小功能。目前,扫描器在记录IP地址时,显示的是十六进制和网络字节序格式。这是因为我们有一个库函数 inet_addr 可以将点分十进制IP地址转换为数字,但没有一个函数能执行反向操作——将数字转换回点分十进制字符串。接下来,我们将创建这个函数。

创建 to_dotted 函数

这个函数将接收一个网络字节序格式的IP地址(数字),并返回其点分十进制字符串表示。

我们将把这个函数添加到我的可重用函数库 bird_lib 中。

函数定义如下:

char *to_dotted(in_addr_t ip) {
    unsigned char a, b, c, d;
    static char buffer[16];

    // 使用位操作提取IP地址的四个部分
    a = (ip >> 24) & 0xFF;
    b = (ip >> 16) & 0xFF;
    c = (ip >> 8) & 0xFF;
    d = ip & 0xFF;

    // 将四个部分格式化为点分十进制字符串
    snprintf(buffer, sizeof(buffer), "%d.%d.%d.%d", a, b, c, d);
    return buffer;
}

代码解释:

  1. a = (ip >> 24) & 0xFF;:将IP地址右移24位,然后与 0xFF 进行按位与操作,提取最高8位(对应点分十进制中的第一部分)。
  2. 类似地,bcd 分别提取接下来的三个8位部分。
  3. 使用 snprintf 函数将这四个整数格式化为点分十进制字符串,并存储在静态缓冲区 buffer 中。
  4. 函数返回这个缓冲区的指针。

现在,我们可以在网络扫描器中用 to_dotted 函数替换原来的日志输出,这样就能以熟悉的点分十进制格式显示IP地址了。


构建网络模拟器

现在,让我们开始构建网络模拟器。我们将使用Bash脚本编写,因为它能方便地调用系统命令来管理网络接口。

项目结构

脚本将支持三个主要命令:

  1. alloc:分配(创建)一系列虚拟IP地址。
  2. free:释放(删除)所有已创建的虚拟IP地址。
  3. simulate:根据配置文件,在随机IP地址上启动模拟的网络服务。

1. 基础框架与辅助函数

首先,我们创建脚本的基础框架,包括错误处理和用法说明。

以下是脚本开头部分:

#!/bin/bash

# 失败处理函数
fail() {
    echo "$1" >&2
    killall netcat 2>/dev/null
    exit 1
}

# 用法说明函数
usage() {
    cat <<EOF >&2
Usage: $0 <command> [arguments]

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/0aa4836ec0290e8bfae03ac6578d7ed7_57.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/0aa4836ec0290e8bfae03ac6578d7ed7_59.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/0aa4836ec0290e8bfae03ac6578d7ed7_60.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/wrt-net-simu/img/0aa4836ec0290e8bfae03ac6578d7ed7_62.png)

Commands:
    alloc <network/cidr> [first] [amount]  分配IP地址
        Example: $0 alloc 192.168.10.0/24 10 15

    free <network/cidr>                    释放所有分配的IP地址
        Example: $0 free 192.168.10.0/24

    simulate <network/cidr> <config_file>  启动网络服务模拟
        Example: $0 simulate 192.168.10.0/24 config.txt

    stop                                   停止所有模拟服务
EOF
    exit 1
}

# 检查是否以root权限运行
if [[ $EUID -ne 0 ]]; then
    fail "This script must be run as root."
fi

2. 分配IP地址(alloc命令)

alloc 命令的核心是使用 ifconfig 命令为物理网络接口创建多个虚拟子接口,并为每个子接口分配一个指定的IP地址。

实现步骤:

  1. 从命令行参数中获取网络地址、起始IP和分配数量。
  2. 使用 ip route show 命令获取指定网络对应的物理接口名称。
  3. 计算要分配的所有IP地址(需跳过本机IP和默认网关IP,避免冲突)。
  4. 循环调用 ifconfig <interface>:<index> <ip> up 命令创建子接口并分配IP。

关键代码片段:

alloc_ips() {
    local network=$1
    local first=${2:-1}
    local amount=${3:-10}
    local last=$((first + amount - 1))

    # 获取网络接口
    local iface=$(grab_interface "$network")
    [[ -z "$iface" ]] && fail "Could not determine interface for network $network"

    # 获取本机IP和网关IP,以便跳过
    local own_ip=$(grab_own_ip "$iface")
    local gateway_ip=$(grab_gateway_ip)

    local count=0
    local sub_if_index=2 # 虚拟子接口从 :2 开始编号

    for i in $(seq $first $last); do
        local ip="${network%.*}.$i" # 构造完整IP
        # 跳过本机IP和网关IP
        if [[ "$ip" == "$own_ip" || "$ip" == "$gateway_ip" ]]; then
            continue
        fi
        local sub_if="${iface}:${sub_if_index}"
        # 创建虚拟接口并分配IP
        ifconfig $sub_if $ip up 2>/dev/null || fail "Failed to create interface $sub_if"
        echo "Allocated IP: $ip on $sub_if"
        ((count++))
        ((sub_if_index++))
    done
    echo "Successfully created $count IP addresses."
}

3. 释放IP地址(free命令)

free 命令相对简单,它需要找到之前创建的所有虚拟子接口,并将其关闭。

实现步骤:

  1. 获取网络对应的物理接口。
  2. 使用 ifconfig | grep 命令找出所有该接口的虚拟子接口(格式为 eth0:2eth0:3 等)。
  3. 循环调用 ifconfig <sub_if> down 命令关闭每个子接口。

关键代码片段:

free_ips() {
    local network=$1
    local iface=$(grab_interface "$network")
    [[ -z "$iface" ]] && fail "Could not determine interface for network $network"

    # 查找所有虚拟子接口
    local sub_ifs=$(ifconfig | grep -o "^${iface}:[0-9]*" | uniq)
    for sub_if in $sub_ifs; do
        ifconfig $sub_if down 2>/dev/null && echo "Freed interface: $sub_if"
    done
    # 也尝试关闭 :0 接口(如果存在)
    ifconfig ${iface}:0 down 2>/dev/null
    echo "All IP addresses freed for network $network."
}

4. 模拟网络服务(simulate命令)

这是最有趣的部分。simulate 命令会读取一个配置文件,配置文件每一行定义了一个服务(端口号和返回的标语)。脚本会为每个已分配的虚拟IP地址随机选择一个服务,并使用 netcat 在该IP和端口上启动一个监听服务。

配置文件格式示例(config.txt):

22 SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.3
80 HTTP/1.1 200 OK

实现步骤:

  1. 读取配置文件,忽略空行和注释(以#开头的行)。
  2. 获取当前网络中已分配的所有虚拟IP地址。
  3. 对于每个IP地址,从配置文件中随机选择一行。
  4. 解析出端口号和标语。
  5. 在后台运行 netcat 命令,绑定到该IP和端口,并在客户端连接时发送对应的标语。

关键代码片段:

simulate_services() {
    local network=$1
    local config_file=$2
    [[ -f "$config_file" ]] || fail "Config file not found: $config_file"

    local iface=$(grab_interface "$network")
    # 获取所有已分配的虚拟IP地址(需要从接口信息中提取)
    local ips=$(get_allocated_ips "$iface")

    for ip in $ips; do
        # 从配置文件中随机选取一行
        local line=$(grab_random_line "$config_file")
        local port=$(echo $line | awk '{print $1}')
        local banner=$(echo $line | cut -d' ' -f2-)
        # 启动 netcat 服务
        run_netcat "$ip" "$port" "$banner" &
        echo "Launched service at $ip:$port -> $banner"
    done
    echo "Simulation started. Run '$0 stop' to terminate all services."
}

# 随机获取配置文件一行
grab_random_line() {
    local file=$1
    # 过滤注释和空行
    local lines=$(grep -v -E '^\s*(#|$)' "$file")
    local line_count=$(echo "$lines" | wc -l)
    [[ $line_count -eq 0 ]] && return
    local random_num=$((RANDOM % line_count + 1))
    echo "$lines" | sed -n "${random_num}p"
}

# 运行 netcat 的后台服务
run_netcat() {
    local ip=$1 port=$2 banner=$3
    # 循环运行,确保服务在连接断开后能重启
    while true; do
        echo "$banner" | nc -l -s "$ip" -p "$port" -q 1 2>/dev/null
        sleep 1
    done &
}

5. 停止服务(stop命令)

stop 命令用于终止所有由 simulate 命令启动的后台 netcat 进程。

实现很简单:

stop_services() {
    killall netcat 2>/dev/null
    echo "All simulation services stopped."
}

6. 主命令调度

最后,我们需要一个主逻辑来解析用户输入的命令并调用对应的函数。

case "${1:-}" in
    alloc)
        alloc_ips "$2" "$3" "$4"
        ;;
    free)
        free_ips "$2"
        ;;
    simulate)
        simulate_services "$2" "$3"
        ;;
    stop)
        stop_services
        ;;
    *)
        usage
        ;;
esac

使用示例

  1. 分配IP地址

    sudo ./net_sim.sh alloc 192.168.10.0/24 10 5
    

    这将在 192.168.10.0/24 网络中,从 .10 开始创建5个IP地址(.10.14)。

  2. 启动服务模拟

    sudo ./net_sim.sh simulate 192.168.10.0/24 services.config
    
  3. 使用扫描器测试
    现在,你可以使用之前编写的网络扫描器来扫描这个网段(例如 192.168.10.0/24),扫描器会发现并报告在指定端口上开放的模拟服务。

  1. 停止模拟并清理
    sudo ./net_sim.sh stop
    sudo ./net_sim.sh free 192.168.10.0/24
    

总结

在本节课中,我们一起完成了一个网络模拟器的构建。我们首先为扫描器添加了 to_dotted 函数,以改善IP地址的显示格式。然后,我们使用Bash脚本创建了一个功能完整的网络模拟器,它能够:

  • 批量分配和释放虚拟IP地址。
  • 根据配置文件,在随机IP和端口上启动模拟服务(如SSH、HTTP)。
  • 为测试网络扫描工具提供了一个可控、安全的模拟环境。

这个项目展示了如何利用Shell脚本快速组合系统命令来实现有用的系统管理功能。在下一节课中,我们将利用这个模拟环境来测试和优化我们的网络端口扫描器。

posted @ 2026-03-29 09:35  布客飞龙II  阅读(6)  评论(0)    收藏  举报