Nmap在进行真正的端口扫描之前,通常需要确定目标主机是否在线(主机发现过程),以免发送大量探测包到不在线的主机。主机发现作为Nmap的基本功能之一,用户也可以单独运用。例如,仅仅需要确定局域网内哪些IP在线,那么可用“主机发现”功能扫描所有机器,枚举出在线主机即可,而没有必要进行端口扫描、服务侦测、OS侦测等更加详细的操作。
1 简单回顾
命令行参数
Nmap提供的主机发现参数相对较少,易于掌握:
-sL: List Scan 列表扫描,仅将指定的目标的IP列举出来,不进行主机发现。
-sn: Ping Scan 只进行主机发现,不进行端口扫描。
-Pn: 将所有指定的主机视作开启的,跳过主机发现的过程。
-PS/PA/PU/PY[portlist]: 使用TCP SYN/ACK或SCTP INIT/ECHO方式进行发现。
-PE/PP/PM: 使用ICMP echo, timestamp, and netmask 请求包发现主机。-PO[protocol list]: 使用IP协议包探测对方主机是否开启。
-n/-R: -n表示不进行DNS解析;-R表示总是进行DNS解析。
--dns-servers <serv1[,serv2],...>: 指定DNS服务器。
--system-dns: 指定使用系统的DNS服务器
--traceroute: 追踪每个路由节点
2 实现框架
Nmap主机发现部分的源码比较简洁。在nmap_main()函数的主循环部分,通过nexthost()函数进行具体的主机发现过程,在nexthost()函数中主要分为两个阶段:地址解析阶段、实际探测阶段。地址解析阶段:主要负责从主机表达式中解析出目标主机地址,将之存放在hostbatch中,并配置该主机所需的路由、网口、MAC地址、源IP等信息。实际发现阶段:分别对解析出来的目标主机,进行实际的探测以及获取RDNS相关信息,例如采用ARP包发现局域网内主机是否在线。
流程图如下所示:
2.1 地址解析阶段
从主机表达式中获取目标主机地址,主要思想包括以下几个方面:
批量进行主机发现
批量处理,可以加快主机发现的速率。默认配置以4096个目标地址作为一批(batch),若配置了--randomize-hosts选项,每个batch大小为4096*4(以便能有更多的IP地址混合洗牌、乱序扫描)。
从主机表达式获取目标主机地址
主机表达式(hostexpression),是Nmap用于管理主机的方式,该数据结构对应到用户在命令行中传入的目标机地址。例如,命令行nmap192.168.1-10.1-254 scanme.nmap.org/24中,192.168.1-10.1-254为一个主机表达式,而scanme.nmap.org/24为另一个主机表达式。Nmap需扫描的目标地址,即逐个解析该表达式包含的各个IP分别是多少,如scanme.nmap.org/24,首先需要进行DNS域名查询,获取scanme.nmap.org对应的IP地址,然后将与此地址的高24位相同的C类IP地址都将被获取出来。
跳过被排除的地址
如果使用--exclude或--exclude-file指定了排除地址,主机发现时应当跳过该类型地址。
设置已转换地址
若该地址在已经被转换解析,即在解析主机表达式过程中(parse_expr()函数),已经处理了该地址,那么设置该地址对应的转换的地址或名字。例如,在上述例子中,scanme.nmap.org/24表达式在解析过程中,scanme.nmap.org的地址会被DNS查询出来,记录在主机表达式中。如果在从该表达式过程取地址时,取出的地址正好对应的scanme.nmap.org的IP地址,那么说明该地址之前已被转换解析,此时让该主机记录被转换解析的表达式名字(此处为scanme.nmap.org/24),并记录转换地址列表(同一域名可能对应到多个不同IP地址)。
获取所需源IP与网络设备
需要配置源端的IP地址与网卡信息,当且仅当:用户具有系统权限(以root运行),并至少满足以下三个条件之一:
1. PING类型为TCP/UDP/SCTP/PROTOCOL/ARP packet;
2. Nmap进行RawScan,即会对原始TCPIP协议packet进行定制,如控制TCP的flag类型等;
3. Nmap在Windows平台运行并且PING类型为ICMP ECHO/ICMP TIMESTAMP/ICMP MASK类型。
获取源端IP与网络设备,需要进行路由信息查询,调用nmap_route_dst()函数。根据目的地址与查询的路由表对表,决定将采用哪个网卡发送数据包,设置直连状态(目标机与源端是否直接相连)、设置接口类型(包括devt_ethernet,devt_loopback, devt_p2p, devt_other)、设置MAC地址、源端IP地址、设置诱骗地址、设置设备名字、设置MTU等信息。
判断是否需要重新划分批次
批量进行主机发现是为了加快发现速度,如果新发现的主机与本批次中其他主机差异较大,那么在进行主机发现时,反而可能降低性能。所以,这里需要检查该目标是否需要新的批次。需要划分新批次的情况有以下几种:
1. 目标主机的地址类型不同。例如,批次内的目标机为IPv4地址,当前主机为IPv6。
2. 目标主机需要网卡不同。例如,批次内目标机需要网卡A进行探测,当前需要网卡B。
3. 目标主机需要不同IP地址。例如,用户指定欺骗的IP地址。
4. 目标主机与源主机直接相连,而其他主机不直接相连,反之亦然。
5. 目标主机的IP地址与当前批次的其他目标机相同(此种情况下,无法判定回复包到底来自哪个目标主机)。
更换主机表达式
若当前主机表达式包含的目标主机已经被获取完毕,而且当前批次允许的最大目标主机数量还未饱和,那么会更换下一个主机表达式继续解析目标主机地址(若此时还有剩余主机表达式)。
2.2 实际发现阶段
在从主机表达式获取完毕了目标主机后,就开始批量进行实际发现的过程。这里主要包含以下几个方面的内容:
检查该批次是否为空
如果在地址解析阶段,无法找到有效目标地址,那么该批次可能是空的。此处若检查到hostbatch为空,就结束主机发现过程。
随机打乱
如果用户在命令行中使用--randomize-hosts,那么在对目标地址进行探测时需要打乱顺序执行(为防止某些防火墙或IDS检测到用户的扫描)。
方式1:ARP方式探测
如果该批次内所有的目标主机都在源主机所在的以太网内,并且用户没有指定--send-ip(表示偏好通过发送IP数据包探测目标主机)选项,那么采用ARP REQUEST的数据包探测所有的目标主机是否在线。依次在局域网内广播ARP查询包,例如:Broadcast ARP Who has192.168.1.100? Tell 192.168.1.102
该方式在arpping()函数中实现,arpping()函数最终调用ultra_scan()进行扫描(ultra_scan()是Nmap中统一的扫描函数,能完成丰富的功能,在端口扫描阶段大量运用,在主机发现阶段也有被调用)。
ETH报文设置
若用户指定偏好以使用ethernet数据包进行探测(命令行选项:--send-eth),那么需要设置该目标主机的下一跳的MAC地址,以便在之后构建ethernet包时能够找到传送的目的MAC地址。
方式2:列表扫描与无PING扫描
此方式其实并没有进行真正扫描,而是直接将目标主机的状态设置为HOST_UP。当用户指定列表扫描(选项-sL,仅仅列举出所有IP地址而不做真正的扫描,直接将IP地址输出,以用于后续的其他操作),或者当用户指定不需进行主机发现(选项-Pn,当用户确知目标主机在线,那么可用该选项跳过主机发现,以便加快扫描速度),此处将目标主机标识为在线的。
方式3:其他方式探测
上述两种发现方式没有覆盖的所有情况,都在此种方式中进行处理。Nmap默认情况下,会发送四种数据包探测目标主机是否在线:
1. ICMPecho request
2. aTCP SYN packet to port 443
3. aTCP ACK packet to port 80
4. anICMP timestamp request
只要收到任何一个探测包的回复,就说明目标主机在线。
此方式在massping()函数中实现,最终该函数会调用到ultra_scan()函数进行端口扫描。
RDNS解析
若用户没有配置-n选项(表示Never Dns Resolution),那么会对该主机进行reverse dns解析,尝试查询出该IP地址对应的域名,因为可能此IP对应到某个固定域名。这样就可以识别到目标主机更多的信息,而且便于维护信息的一致性。
3 代码分析
主机发现部分核心函数nexthost()的具体实现代码:
[cpp] view plaincopy
Target *nexthost(HostGroupState *hs, const addrset *exclude_group,
struct scan_lists *ports, int pingtype) {
int i;
struct sockaddr_storage ss;
size_t sslen;
struct route_nfo rnfo;
bool arpping_done = false;
struct timeval now;
///当已经批量地探测一组主机,并将主机缓存在hostbatch中时,直接返回该主机对象指针即可
if (hs->next_batch_no < hs->current_batch_sz) {
/* Woop! This is easy -- we just pass back the next host struct */
return hs->hostbatch[hs->next_batch_no++];
}
/* Doh, we need to refresh our array */
/* for (i=0; i < hs->max_batch_sz; i++) hs->hostbatch[i] = new Target(); */
///进行新一批的主机探测,以下do{}while(1)循环是先产生各个IP的主机对象并放入hostbatch[]中
///真正确定主机是否在线,是在batchfull:代码段内
hs->current_batch_sz = hs->next_batch_no = 0;
do {
/* Grab anything we have in our current_expression */
while (hs->current_batch_sz < hs->max_batch_sz &&
hs->current_expression.get_next_host(&ss, &sslen) == 0) {
Target *t;
///以下跳过被排除地址
if (hostInExclude((struct sockaddr *)&ss, sslen, exclude_group)) {
continue; /* Skip any hosts the user asked to exclude */
}
t = new Target();
t->setTargetSockAddr(&ss, sslen);
/* Special handling for the resolved address (for example whatever
scanme.nmap.org resolves to in scanme.nmap.org/24). */
if (hs->current_expression.is_resolved_address(&ss)) {
if (hs->current_expression.get_namedhost())
t->setTargetName(hs->current_expression.get_resolved_name());
t->resolved_addrs = hs->current_expression.get_resolved_addrs();
}
/* We figure out the source IP/device IFF
1) We are r00t AND
2) We are doing tcp or udp pingscan OR
3) We are doing a raw-mode portscan or osscan or traceroute OR
4) We are on windows and doing ICMP ping */
if (o.isr00t &&
((pingtype & (PINGTYPE_TCP|PINGTYPE_UDP|PINGTYPE_SCTP_INIT|PINGTYPE_PROTO|PINGTYPE_ARP)) || o.RawScan()
#ifdef WIN32
|| (pingtype & (PINGTYPE_ICMP_PING|PINGTYPE_ICMP_MASK|PINGTYPE_ICMP_TS))
#endif // WIN32
)) {
t->TargetSockAddr(&ss, &sslen);
if (!nmap_route_dst(&ss, &rnfo)) {
fatal("%s: failed to determine route to %s", __func__, t->NameIP());
}
if (rnfo.direct_connect) {
t->setDirectlyConnected(true);
} else {
t->setDirectlyConnected(false);
t->setNextHop(&rnfo.nexthop, sizeof(rnfo.nexthop));
}
t->setIfType(rnfo.ii.device_type);
if (rnfo.ii.device_type == devt_ethernet) {
if (o.spoofMACAddress())
t->setSrcMACAddress(o.spoofMACAddress());
else
t->setSrcMACAddress(rnfo.ii.mac);
}
t->setSourceSockAddr(&rnfo.srcaddr, sizeof(rnfo.srcaddr));
if (hs->current_batch_sz == 0) /* Because later ones can have different src addy and be cut off group */
o.decoys[o.decoyturn] = t->v4source();
t->setDeviceNames(rnfo.ii.devname, rnfo.ii.devfullname);
t->setMTU(rnfo.ii.mtu);
// printf("Target %s %s directly connected, goes through local iface %s, which %s ethernet\n", t->NameIP(), t->directlyConnected()? "IS" : "IS NOT", t->deviceName(), (t->ifType() == devt_ethernet)? "IS" : "IS NOT");
}
/* Does this target need to go in a separate host group? */
if (target_needs_new_hostgroup(hs, t)) {
/* Cancel everything! This guy must go in the next group and we are
out of here */
hs->current_expression.return_last_host();
delete t;
goto batchfull;
}
hs->hostbatch[hs->current_batch_sz++] = t;
}
///若当前batch数组还没有填满,并且还有更多主机表达式,那么尝试进行新的表达式解析
if (hs->current_batch_sz < hs->max_batch_sz &&
hs->next_expression < hs->num_expressions) {
/* We are going to have to pop in another expression. */
while (hs->next_expression < hs->num_expressions) {
const char *expr;
expr = hs->target_expressions[hs->next_expression++];
if (hs->current_expression.parse_expr(expr, o.af()) != 0)///解析表达式
log_bogus_target(expr);///若解析出错,标记此表达式
else
break;///解析成功,进行新一轮的目标地址IP解析
}
} else break;
} while(1);
batchfull:
if (hs->current_batch_sz == 0)///没有解析出有效地址,返回NULL
return NULL;
/* OK, now we have our complete batch of entries. The next step is to
randomize them (if requested) */
if (hs->randomize) { ///若命令行指定randomize-hosts选项,那么将目标地址随机打乱
hoststructfry(hs->hostbatch, hs->current_batch_sz);
}
/* First I'll do the ARP ping if all of the machines in the group are
directly connected over ethernet. I may need the MAC addresses
later anyway. */
///探测方式1:主机组内所有IP地址都直连在ethernet内,那么进行ARP PING报文探测
///向局域网广播:ARP REQUEST包,询问谁持有xx.xx.xx.xxIP地址
if (hs->hostbatch[0]->ifType() == devt_ethernet &&
hs->hostbatch[0]->af() == AF_INET &&
hs->hostbatch[0]->directlyConnected() &&
o.sendpref != PACKET_SEND_IP_STRONG) {
arpping(hs->hostbatch, hs->current_batch_sz);///局域网内主机发现的执行函数
arpping_done = true;
}
/* No other interface types are supported by ND ping except devt_ethernet
at the moment. */
if (hs->hostbatch[0]->ifType() == devt_ethernet &&
hs->hostbatch[0]->af() == AF_INET6 &&
hs->hostbatch[0]->directlyConnected() &&
o.sendpref != PACKET_SEND_IP_STRONG) {
arpping(hs->hostbatch, hs->current_batch_sz);
arpping_done = true;
}
///若命令行指定了--send-eth,并判断到当前接口类型为ethernet网卡,
///对每一个状态不是HOST_DOWN且未超时的主机,设置下一跳MAC地址
gettimeofday(&now, NULL);
if ((o.sendpref & PACKET_SEND_ETH) &&
hs->hostbatch[0]->ifType() == devt_ethernet) {
for (i=0; i < hs->current_batch_sz; i++) {
if (!(hs->hostbatch[i]->flags & HOST_DOWN) &&
!hs->hostbatch[i]->timedOut(&now)) {
if (!setTargetNextHopMAC(hs->hostbatch[i])) {
fatal("%s: Failed to determine dst MAC address for target %s",
__func__, hs->hostbatch[i]->NameIP());
}
}
}
}
/* TODO: Maybe I should allow real ping scan of directly connected
ethernet hosts? */
/* Then we do the mass ping (if required - IP-level pings) */
///探测方式2:若指定不进行PING操作(如命令行指定了-Pn或-sL都不会进行PING操作)而arpping_done为被标记
///或指定扫描自己回环网口,那么都在此处将主机标记位HOST_UP.
if ((pingtype == PINGTYPE_NONE && !arpping_done) || hs->hostbatch[0]->ifType() == devt_loopback) {
for (i=0; i < hs->current_batch_sz; i++) {
if (!hs->hostbatch[i]->timedOut(&now)) {
initialize_timeout_info(&hs->hostbatch[i]->to);
hs->hostbatch[i]->flags |= HOST_UP; /*hostbatch[i].up = 1;*/
if (pingtype == PINGTYPE_NONE && !arpping_done)///用户指定该主机为HOST_UP,例如用户已知某个目标已经开启,
hs->hostbatch[i]->reason.reason_id = ER_USER;///就可以通过-Pn选项让Nmap不进行PING过程。
else
hs->hostbatch[i]->reason.reason_id = ER_LOCALHOST;///本地主机,当然为HOST_UP
}
}
} else if (!arpping_done) {///探测方式3:其他情况,则采用massping方式探测主机是否在线
massping(hs->hostbatch, hs->current_batch_sz, ports);
}
///若命令行没有指定-n选项(含义是不做DNS/RDNS解析),那么这里对rdns进行解析
if (!o.noresolve)
nmap_mass_rdns(hs->hostbatch, hs->current_batch_sz);
///返回hostbatch中当前next_batch_no所在的主机(next_host()会批量解析主机IP,下一次进入时直接返回已解析的地址)。
return hs->hostbatch[hs->next_batch_no++];
}
来源:网络安全技术修炼