Nmap源码分析(主机发现)

来源:互联网 发布:.moe域名注册 编辑:程序博客网 时间:2024/06/05 05:14

Nmap源码分析(主机发现)

2012年8月9日

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()的具体实现代码:

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++];}