准备自己写一些安全工具,在此之前,先来赏析一下自 96 年维护至今的 Nmap 的源码
下载地址:
https://nmap.org/dist/nmap-7.95.tar.bz2
项目的编程语言构成:
简单过一下重点目录结构:
├─libdnet-stripped # 网络接口库,用于操作网络底层功能
├─liblinear # 解决大规模线性分类和回归问题
├─liblua # Lua 脚本支持库
├─libnetutil # Nmap 实现的网络实用函数
├─libpcap # 网络数据包捕获
├─libpcre # Perl 兼容的正则表达式函数
├─libssh2 # 实现 SSH 协议高级功能
├─libz # 压缩与解压缩
├─macosx # 兼容苹果 Mac OS X 系统
├─mswin32 # 兼容 Windows 系统
├─nbase # Nmap 的基础使用程序库,比如字符串操作、路径处理、随机数生成
├─ncat # Nmap 实现的新版 Netcat
├─ndiff # 比较 Nmap 扫描结果差异
├─nping # Nmap 实现的新版的 Hping,用于网络探测与构建数据包
├─nselib # Nmap 用 Lua 编写的常用的脚本库
├─nsock # Nmap 实现的并行的 SocketEvent 处理库
├─scripts # 常用扫描检查的 Lua 脚本
nmap_main()
项目核心功能入口在 nmap.cc,其中核心是 nmap_main() 函数
main.cc 基本只负责调用 nmap_main(),故从此函数开始分析
初始变量声明
int nmap_main(int argc, char *argv[]) {
/*argc 是参数数量,argv 是参数值 */
int i;
std::vector<Target *> Targets;
/*Target 指针向量,用于存储目标地址*/
time_t now;
time_t timep;
char mytime[128];
/*两个 time_t 类型变量用于储存时间,一个字符数组用于格式化时间*/
struct addrset *exclude_group;
/*指向 addset 结构的指针,用于储存要排除的地址*/
#ifndef NOLUA
/* Pre-Scan and Post-Scan script results datastructure */
ScriptResults *script_scan_results = NULL;
#endif
/*检查是否定义了 NOLUA 宏(用于在编译时排除 Lua 脚本支持),
如果没有定义,声明一个 ScriptResults 类型的指针用于存储脚本扫描结果*/
unsigned int ideal_scan_group_sz = 0;
/*无符号整型变量存储理想的扫描组大小*/
Target *currenths;
/*Target 指针,指向当前扫描目标*/
char myname[FQDN_LEN + 1];
/*字符数组用于存储域名,FQDN 是全限定域名(Fully Qualified Domain Name)。
FQDN_LEN 被定义为 254,加上空终止符达到 FQDN 最大值 255,
确保能够处理最长的 DNS 名称*/
int sourceaddrwarning = 0;
/* Have we warned them yet about unguessable source addresses? */
/*标记是否已经警告用户关于源地址不可预测(例如随机源地址或源地址欺骗)*/
unsigned int targetno;
/*无符号整型变量用于存储目标编号*/
char hostname[FQDN_LEN + 1] = "";
/*字符数组用于存储主机名,并初始化为空字符串,
hostname 和 FQDN 区别在于前者多用于本地*/
struct sockaddr_storage ss;
size_t sslen;
/*一个 sockaddr_storage 结构 ss 用于存储套接字地址
以及一个 size_t 类型的变量 sslen 用于存储 ss 的长度*/
int err;
/*错误记录*/
检测 WSL 环境
#ifdef LINUX
/*预处理指令,用于检查是否定义了 LINUX 宏,
如果有定义,说明在 Linux 环境,接下来的代码块将被包含在编译结果中*/
/* Check for WSL and warn that things may not go well. */
/*检查 WSL 环境,并在检测到 WSL 时发出警告*/
struct utsname uts;
/*一个 utsname 结构体变量 uts 用于存储操作系统的信息*/
if (!uname(&uts)) {
/*调用 uname 函数用于填充 utsname 结构体,调用成功返回 0*/
if (strstr(uts.release, "Microsoft") != NULL) {
/*用 strstr 函数检查 uts.release 字段中是否包含字符串 Microsoft*/
error("Warning: %s may not work correctly on Windows Subsystem for Linux.\n"
"For best performance and accuracy, use the native Windows build from %s/download.html#windows.",
NMAP_NAME, NMAP_URL);
/*以上条件判断为是 WSL 环境,警告可能无法正常工作,
并建议用户使用原生 Windows 版本的程序,提供了下载链接*/
}
}
#endif
时间处理-1
tzset();
/*调用 tzse t函数,用于根据环境变量设置时区信息*/
now = time(NULL);
/*调用 time 函数获取当前时间戳,并将结果存储在now变量中*/
err = n_localtime(&now, &local_time);
/*调用 n_localtime 函数,将当前时间 now 转换为本地时间,
并存储在local_time结构体中,并返回值 err 用于检查转换是否成功*/
if (err) {
fatal("n_localtime failed: %s", strerror(err));
}
/*如果有错误发生,调用 fatal 函数输出错误信息,并终止程序,
strerror(err) 用于获取错误描述*/
参数数量
if (argc < 2){
printusage();
exit(-1);
}
/*检查程序的参数数量,如果小于 2(即没有提供任何命令行参数,默认值是 1(nmap 自身)),
则调用 printusage 函数打印使用方法,然后退出程序并返回错误码-1*/
Targets.reserve(100);
/*为 Targets 向量预留 100 个元素的空间,
可以减少在向量中动态添加元素时可能发生的内存重新分配。*/
检测 Windows 环境
#ifdef WIN32
win_pre_init();
#endif
/*预处理指令,用于检查是否定义了 WIN32 宏,
如果有定义,说明在 Windows 环境,
将调用 win_pre_init 函数进行 Windows 特定的预初始化操作
(启动 Winsock 库,并检查启动过程中是否出现错误)*/
parse_options(argc, argv);
/*调用 parse_options 函数,解析命令行参数
根据传入的 argc(参数的数量)和 argv(参数的值)来设置程序的配置*/
if (o.debugging)
nbase_set_log(fatal, error);
else
nbase_set_log(fatal, NULL);
/*检查 o.debugging 变量,nbase_set_log 是 日志函数,
如果为真,则使其在发生致命错误时调用 fatal 和 error 函数;
如果为假,则只调用 fatal 函数*/
tty_init(); // Put the keyboard in raw mode
/*调用 tty_init 函数,将键盘设置为原始模式,
在原始模式下,键盘输入不会被行缓冲,每个按键都会立即发送给程序*/
#ifdef WIN32
// Must come after parse_options because of --unprivileged
// Must come before apply_delayed_options because it sets o.isr00t
win_init();
#endif
/*再次检查是否定义了 WIN32 宏,
如果是,则调用 win_init 函数进行 Windows 特定的初始化操作。
(会调用 WSAStartup 启动 Winsock DLL)
这个调用必须在 parse_options 之后,因为需要处理 --unprivileged 选项;
同时必须在 apply_delayed_options 之前,因为它会设置 o.isr00t 变量*/
apply_delayed_options();
/*用来处理那些需要在某些初始化步骤之后才能应用的选项的函数*/
网络路由
for (unsigned int i = 0; i < route_dst_hosts.size(); i++) {
/*遍历 route_dst_hosts 向量,包含了一系列的目标主机地址*/
const char *dst;
struct sockaddr_storage ss;
struct route_nfo rnfo;
size_t sslen;
int rc;
/*dst 用于存储目标地址的字符串表示,
ss 用于存储目标地址的套接字表示,
rnfo 用于存储路由信息,
sslen 用于存储 ss 的长度,
rc 用于存储函数返回值*/
dst = route_dst_hosts[i].c_str();
/*获取向量中第 i 个元素的 C 风格字符串表示*/
rc = resolve(dst, 0, &ss, &sslen, o.af());
/*调用 resolve 函数,尝试将 dst 字符串解析为套接字地址 ss,
0 可能是一个标志,o.af() 指定了地址族(如 IPv4 或 IPv6)*/
if (rc != 0)
fatal("Can't resolve %s: %s.", dst, gai_strerror(rc));
/*如果 resolve 函数返回非零值(表示解析失败),
则调用 fatal 函数输出错误信息并终止程序。
gai_strerror 函数用于获取 rc 对应的错误描述*/
printf("%s\n", inet_ntop_ez(&ss, sslen));
/*解析成功就使用 inet_ntop_ez 函数将套接字地址 ss 转换为可读的字符串并输出*/
if (!route_dst(&ss, &rnfo, o.device, o.SourceSockAddr())) {
/*调用 route_dst 函数,查找到目标地址 ss 的路由信息,存储在 rnfo 中。
o.device 和 o.SourceSockAddr() 用于指定网络设备和源地址*/
printf("Can't route %s (%s).", dst, inet_ntop_ez(&ss, sslen));
/*如果 route_dst 函数返回假(表示路由查找失败),输出错误信息*/
} else {
printf("%s %s", rnfo.ii.devname, rnfo.ii.devfullname);
/*如果 route_dst 函数返回真,输出网络接口的名称和完整名称*/
printf(" srcaddr %s", inet_ntop_ez(&rnfo.srcaddr, sizeof(rnfo.srcaddr)));
/*输出源地址*/
if (rnfo.direct_connect)
printf(" direct");
else
printf(" nexthop %s", inet_ntop_ez(&rnfo.nexthop, sizeof(rnfo.nexthop)));
/*根据 rnfo.direct_connect 的值,输出是直接连接还是通过下一个跳转地址*/
}
printf("\n");
}
route_dst_hosts.clear();
/*清空向量*/
if (delayed_options.iflist) {
print_iflist();
exit(0);
}
/*如果 delayed_options.iflist 为真,输出接口列表并退出程序*/
FTP 弹跳弹扫描
FTP 弹跳扫描(FTP Bounce Scan)是一种利用 FTP 协议中代理连接功能的端口扫描技术,它允许攻击者通过一台中间的 FTP 服务器来对另一台目标主机进行端口扫描
这种技术最初是基于 RFC 959 标准中的一个特性设计的,该特性允许用户连接到一台 FTP 服务器,并请求将文件发送到第三方服务器
FTP 协议在设计之初就包含了 PORT 命令(EPRT 是支持 IPv6 的扩展指令),用于指定客户端希望接收数据的 IP 地址和端口号。当客户端向 FTP 服务器发出 PORT 命令后,服务器会尝试直接与指定的 IP 地址和端口建立连接,以便传输数据。这种机制被称为主动模式。如果 FTP 服务器能够成功地与第三方服务器建立连接,则意味着目标端口是开放的;如果连接失败,则可能表明端口是关闭或被过滤的
为了实现 FTP 弹跳扫描,Nmap 提供了 -b 参数,允许用户指定一个 FTP 服务器作为中介来进行扫描
nmap -b [username:password@]server[:port] [target]
/* If he wants to bounce off of an FTP site, that site better damn well be reachable! */
/*确保 FTP 站点可达*/
if (o.bouncescan) {
int rc = resolve(ftp.server_name, 0, &ss, &sslen, AF_INET);
/*调用了 resolve 函数来将 FTP 服务器的主机名或 IP 地址转换为二进制形式的网络地址结构。
ftp.server_name 是用户提供的 FTP 服务器的名称或 IP 地址;
AF_INET 指定了使用 IPv4 协议族。如果解析成功,rc 将被置 0,否则解析失败*/
if (rc != 0)
fatal("Failed to resolve FTP bounce proxy hostname/IP: %s",
ftp.server_name);
/*确保只有正确解析 FTP 服务器地址的情况下才会继续进行后续操作*/
memcpy(&ftp.server, &((sockaddr_in *)&ss)->sin_addr, 4);
/*将解析得到的 IP 地址复制到 ftp.server 变量中,4 是 IPv4 地址长度(以字节为单位)*/
if (o.verbose) {
log_write(LOG_STDOUT, "Resolved FTP bounce attack proxy to %s (%s).\n",
ftp.server_name, inet_ntoa(ftp.server));
}
/*如果启用了详细模式,则会调用 log_write 函数告知成功解析了 FTP 服务器地址。
inet_ntoa 函数用于将二进制形式的 IP 地址转换为点分十进制字符串格式*/
}
fflush(stdout);
fflush(stderr);
/*为了确保所有输出都被立即发送到终端而不是滞留在缓冲区中,
调用了 fflush 函数对标准输出(stdout)和标准错误(stderr)进行了刷新操作*/
时间处理-2
timep = time(NULL);
err = n_ctime(mytime, sizeof(mytime), &timep);
if (err) {
fatal("n_ctime failed: %s", strerror(err));
}
/*获取当前时间、将其格式化为字符串*/
chomp(mytime);
/*去除字符串末尾的换行符 \n*/
XML 报告
XSL(可扩展样式表语言)使得生成的 XML 文件可以通过浏览器或其他支持 XSLT 的工具以更友好的方式呈现
if (!o.resuming) {
/* Brief info in case they forget what was scanned */
/*检查是否为新扫描*/
char *xslfname = o.XSLStyleSheet();
/*获取 XSL 样式表文件名*/
xml_start_document("nmaprun");
/*开始一个新的 XML 文档,根元素是 <nmaprun> 标签*/
if (xslfname) {
/*如果 XSL 文件名不为空,则添加 XML 样式表处理指令*/
xml_open_pi("xml-stylesheet");
/* 开始一个处理指令(Processing Instruction, PI)*/
xml_attribute("href", "%s", xslfname);
/*设置处理指令的 href 属性,指向 XSL 文件*/
xml_attribute("type", "text/xsl");
/*置处理指令的 type 属性,指定类型为 XSL*/
xml_close_pi();
/*结束处理指令*/
xml_newline();
}
xml_start_comment();
/*开始一个 XML 注释*/
xml_write_escaped(" %s %s scan initiated %s as: %s ", NMAP_NAME, NMAP_VERSION, mytime, join_quoted(argv, argc).c_str());
/*写入一个转义的字符串到注释中,
包含 Nmap 的名称、版本、扫描开始时间和命令行参数*/
xml_end_comment();
/*结束注释*/
xml_newline();
xml_open_start_tag("nmaprun");
/*开始 nmaprun 标签*/
xml_attribute("scanner", "nmap");
/*设置 scanner 属性为 nmap*/
xml_attribute("args", "%s", join_quoted(argv, argc).c_str());
/*设置 args 属性为命令行参数*/
xml_attribute("start", "%lu", (unsigned long) timep);
/*设置 start 属性为扫描开始的时间戳*/
xml_attribute("startstr", "%s", mytime);
/*设置 startstr 属性为扫描开始的日期字符串*/
xml_attribute("version", "%s", NMAP_VERSION);
/*设置 version 属性为 Nmap 的版本*/
xml_attribute("xmloutputversion", NMAP_XMLOUTPUTVERSION);
/*设置 xmloutputversion 属性为 XML 输出的版本*/
xml_close_start_tag();
/*结束 nmaprun 标签的开始部分*/
xml_newline();
output_xml_scaninfo_records(&ports);
/*输出扫描信息记录*/
xml_open_start_tag("verbose");
/*开始 verbose 标签*/
xml_attribute("level", "%d", o.verbose);
/*设置 level 属性为详细级别*/
xml_close_empty_tag();
/*自闭合 verbose 标签(没有子元素)*/
xml_newline();
xml_open_start_tag("debugging");
/*开始 debugging 标签*/
xml_attribute("level", "%d", o.debugging);
/*设置 level 属性为调试级别*/
xml_close_empty_tag();
/*自闭合debugging 标签*/
xml_newline();
} else {
xml_start_tag("nmaprun", false);
/*如果是恢复之前的扫描,则只输出 nmaprun 标签*/
}
日志记录
/*调用 log_write 函数,使用 LOG_NORMAL 和 LOG_MACHINE 标志来记录日志*/
log_write(LOG_NORMAL | LOG_MACHINE, "# ");
log_write(LOG_NORMAL | LOG_MACHINE, "%s %s scan initiated %s as: %s", NMAP_NAME, NMAP_VERSION, mytime, join_quoted(argv, argc).c_str());
/*输出格式化的字符串,包含 Nmap 的名称、版本、扫描开始的时间以及扫描命令的参数*/
log_write(LOG_NORMAL | LOG_MACHINE, "\n");
/* Before we randomize the ports scanned, lets output them to machine
parseable output */
/*在对要扫描的端口进行随机化之前,先输出端口信息,以便机器可以解析*/
if (o.verbose)
/*检查是否要详细输出*/
output_ports_to_machine_parseable_output(&ports);
/*将端口信息输出到一个机器可解析的格式*/
SIGPIPE 信号处理
在 Unix 和类 Unix 系统中管道是一种进程间通信机制,允许一个进程的输出直接成为另一个进程的输入,这种通信是通过管道的两端进行的:读端(reader)和写端(writer)
SIGPIPE 是一个在该操作系统中使用的信号,当一个进程尝试向一个没有读端的管道或已经关闭的套接字发送数据时,操作系统会向该进程发送此信号,通知进程发生了管道破裂(broken pipe)的情况,终止进程
#if defined(HAVE_SIGNAL) && defined(SIGPIPE)
/*预处理器条件编译指令,
HAVE_SIGNAL 用来检查系统是否支持信号处理;
SIGPIPE 用来表示是否存在 SIGPIPE 信号(管道破裂信号)*/
signal(SIGPIPE, SIG_IGN); /* ignore SIGPIPE so our program doesn't crash because
of it, but we really shouldn't get an unexpected
SIGPIPE */
/*SIG_IGN 表示忽略该信号
忽略 SIGPIPE 信号的目的是防止程序因为接收到 SIGPIPE 信号而崩溃,
理论上是不应该接收到意外的 SIGPIPE 信号*/
#endif
评论