编写网络模拟器笔记-全-
编写网络模拟器笔记(全)
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], ¤t_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语言编写一个基础版的网络映射器。我们完成了以下工作:
- 建立项目结构:创建了工具库和主项目的
Makefile。 - 处理命令行参数:使工具能够接受端口号、起始和结束IP地址作为输入。
- 实现IP地址生成器:编写了函数来遍历指定的IP地址范围。
- 实现基础扫描逻辑:使用阻塞式套接字逐个尝试连接目标主机的指定端口,并报告开放端口。
这个初始版本非常简单,并且由于使用阻塞式调用和顺序扫描,速度会很慢。然而,它为我们理解网络扫描的基本原理奠定了坚实的基础。在接下来的课程中,我们将逐步改进这个工具,引入非阻塞套接字、多线程等技术,使其变得更高效、更实用。


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);


#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类型的端口号。


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

- 创建套接字。
- 填充
sockaddr_in结构体。 - 尝试连接。
- 如果连接成功,则尝试读取服务横幅(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;
}
代码解释:
a = (ip >> 24) & 0xFF;:将IP地址右移24位,然后与0xFF进行按位与操作,提取最高8位(对应点分十进制中的第一部分)。- 类似地,
b、c、d分别提取接下来的三个8位部分。 - 使用
snprintf函数将这四个整数格式化为点分十进制字符串,并存储在静态缓冲区buffer中。 - 函数返回这个缓冲区的指针。
现在,我们可以在网络扫描器中用 to_dotted 函数替换原来的日志输出,这样就能以熟悉的点分十进制格式显示IP地址了。
构建网络模拟器
现在,让我们开始构建网络模拟器。我们将使用Bash脚本编写,因为它能方便地调用系统命令来管理网络接口。
项目结构
脚本将支持三个主要命令:
- alloc:分配(创建)一系列虚拟IP地址。
- free:释放(删除)所有已创建的虚拟IP地址。
- simulate:根据配置文件,在随机IP地址上启动模拟的网络服务。




1. 基础框架与辅助函数
首先,我们创建脚本的基础框架,包括错误处理和用法说明。
以下是脚本开头部分:
#!/bin/bash
# 失败处理函数
fail() {
echo "$1" >&2
killall netcat 2>/dev/null
exit 1
}
# 用法说明函数
usage() {
cat <<EOF >&2
Usage: $0 <command> [arguments]




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地址。
实现步骤:
- 从命令行参数中获取网络地址、起始IP和分配数量。
- 使用
ip route show命令获取指定网络对应的物理接口名称。 - 计算要分配的所有IP地址(需跳过本机IP和默认网关IP,避免冲突)。
- 循环调用
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 命令相对简单,它需要找到之前创建的所有虚拟子接口,并将其关闭。
实现步骤:
- 获取网络对应的物理接口。
- 使用
ifconfig | grep命令找出所有该接口的虚拟子接口(格式为eth0:2、eth0: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



实现步骤:
- 读取配置文件,忽略空行和注释(以
#开头的行)。 - 获取当前网络中已分配的所有虚拟IP地址。
- 对于每个IP地址,从配置文件中随机选择一行。
- 解析出端口号和标语。
- 在后台运行
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
使用示例
-
分配IP地址:
sudo ./net_sim.sh alloc 192.168.10.0/24 10 5这将在
192.168.10.0/24网络中,从.10开始创建5个IP地址(.10到.14)。 -
启动服务模拟:
sudo ./net_sim.sh simulate 192.168.10.0/24 services.config -
使用扫描器测试:
现在,你可以使用之前编写的网络扫描器来扫描这个网段(例如192.168.10.0/24),扫描器会发现并报告在指定端口上开放的模拟服务。



- 停止模拟并清理:
sudo ./net_sim.sh stop sudo ./net_sim.sh free 192.168.10.0/24
总结
在本节课中,我们一起完成了一个网络模拟器的构建。我们首先为扫描器添加了 to_dotted 函数,以改善IP地址的显示格式。然后,我们使用Bash脚本创建了一个功能完整的网络模拟器,它能够:
- 批量分配和释放虚拟IP地址。
- 根据配置文件,在随机IP和端口上启动模拟服务(如SSH、HTTP)。
- 为测试网络扫描工具提供了一个可控、安全的模拟环境。

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

浙公网安备 33010602011771号