现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

Nmap源码分析(主机发现)

2014-01-26 21:18 工业·编程 ⁄ 共 10340字 ⁄ 字号 暂无评论

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

来源:网络安全技术修炼

给我留言

留言无头像?