【从零开始写漏扫】服务识别—自己动手写一个指纹识别器—网络特征指纹

前言

上篇文章中,我们讨论了端口扫描器的实现,编码实现了一个简单的多线程端口扫描器,从子域名挖掘到端口扫描,主机发现部分暂时结束了,今后遇到更好用的主机发现技术再作补充,接下来开始服务的识别工作。

通过主机发现技术,我们知道某个域名、某些IP下有哪些目标主机,指导了目标在哪,接下来就需要了解目标是什么,这就是服务识别技术。

服务识别的核心,是指纹识别,指纹的构建方式千奇百怪,识别速度和准确度也各有千秋,但万变不离其宗,目的都是通过服务的唯一特征,来确定该服务,本文重点从网络特征入手,基于前文实现的主机发现函数,重点分析和实现以网络特征为基础构建的指纹识别器。

阅读本篇文章,你需要:

  1. 一定的计算机网络基础,了解熟知端口CIDR划分ASN划分
  2. 熟悉json文件的格式,以及dict和json的互转
  3. 熟悉NmapGeoIP的使用
  4. 一定的源码阅读能力

正文

本篇文章中,我们主要讨论基于网络特征的指纹构建,所谓网络特征,主要指端口特征、数据包特征、以及主机的Internet特征,这些方式常用在操作系统识别、CDN判断以及常用服务初筛上。

端口特征

这里提到的端口特征,主要指TCP熟知端口,在基于C/S模式的服务中,为了方便连接,服务端的端口配置常常是默认端口,这就给了我们通过开放端口粗略识别服务的机会,这里我收集了一份常用端口对应的服务列表:

{
  "21": "ftp",
  "22": "ssh",
  "23": "telnet",
  "25": "smtp",
  "53": "dns",
  "68": "dhcp",
  "80": "http",
  "69": "tftp",
  "995": "pop3",
  "135": "rpc",
  "139": "netbios",
  "143": "imap",
  "443": "https",
  "161": "snmp",
  "489": "ldap",
  "445": "smb",
  "465": "smtps",
  "512": "linuxrrpe",
  "513": "linuxrrlt",
  "514": "linuxrcmd",
  "873": "rsync",
  "993": "imaps",
  "1080": "proxy",
  "1099": "javarmi",
  "1352": "lotus",
  "1433": "mssql",
  "1521": "oracle",
  "1723": "pptp",
  "2082": "cpanel",
  "2083": "cpanel",
  "2181": "zookeeper",
  "2222": "dircetadmin",
  "2375": "docker",
  "2604": "zebra",
  "3306": "mysql",
  "3312": "kangle",
  "3389": "rdp",
  "3690": "svn",
  "4440": "rundeck",
  "4848": "glassfish",
  "5432": "postgresql",
  "5632": "pcanywhere",
  "5900": "vnc",
  "5984": "couchdb",
  "6082": "varnish",
  "6379": "redis",
  "9001": "weblogic",
  "7778": "kloxo",
  "10050": "zabbix",
  "8291": "routeros",
  "9200": "elasticsearch",
  "11211": "memcached",
  "27017": "mongodb",
  "27018": "mongodb",
  "50070": "hadoop",
  "888": "宝塔",
  "8080": "http"
}

这里使用Json来保存,我们先来读取这份列表:

def initFingerprint(fingermode = 1):
    try:
        # 模式1:打开端口指纹文件
        if fingermode == 1:
            return open(portfingerfile,"r",encoding="utf8")
    except Exception as e:
        print(e)
        return None

通过这份列表,我们就可以实现一个粗略的服务识别函数,其中使用了前文中提到的端口扫描函数:

# 通过端口识别服务:
import json
def PortAnalyze(ip):
    results = []
    try:
        portjson = initFingerprint(1)
        portdict = json.load(portjson)
        ports = Scan(ip,0,65535)
        for result in ports:
            if str(result) in portdict:
               dict = {
                   "port":result,
                   "service":portdict[str(result)]
               }
               results.append(dict)
    except Exception as e:
        print(e)
    return results

接下来,我们仍然找东南某省的设备来测试:

东南某省设备

匹配一下端口试试:

识别结果

限于常见端口列表,只能粗略估计所存在的服务。

Internet特征

Internet特征主要通过的主机所在的网络位置识别服务,多用于识别网络基础设施。比如CDN或路由器,我们收集到了一份网络常见CDN的网络位置,文件放在了项目源代码中。接下来我们基于这个文件来实现CDN的识别。

首先,加载指纹文件:

def initFingerprint(fingermode = 1):
    try:
        # 模式1:打开端口指纹文件
        if fingermode == 1:
            return open(portfingerfile,"r",encoding="utf8")
        # 模式2:打开CDN指纹文件
        if fingermode == 2:
            return open(cdnfingerfile,"r",encoding="utf8")
    except Exception as e:
        print(e)
        return None

我们通过CIDR子网来判断,这里使用内置的ipaddress模块实现:

# 通过ip所在子网判断:
import ipaddress
def IpIsCDN(ip,cdn_list):
    try:
        for network in cdn_list:
            if ipaddress.ip_address(ip) in ipaddress.ip_network(network):
                return True
    except Exception as e:
        print(e)
    return False

也可以通过主机CNAME解析来判断,这里使用dnspython模块完成:

# 通过CNAME判断:
from dns import resolver
def CnameIsCDN(domain,cdn_list):
    try:
        results = resolver.resolve(domain,"CNAME")
        for result in results.response.answer:
            for cdn in cdn_list:
                if cdn in result.to_text():
                    return True
    except Exception as e:
        print(e)
    return False

通过自治系统号(Autonomous System Number)判断,这里需要使用GeoIP加载,数据库地址,注册账号并登陆后选择GeoLite2-ASN下的mmdb格式,使用pip引入geoip2后,先来编写代码加载GeoIP数据库:

# geoip引入
import geoip2.database
def initGeoip():
    try:
        return geoip2.database.Reader(geodb)
    except Exception as e:
        print(e)
        return None

接下来编写ASN判断逻辑:

# 通过ASN判断:
def ASNIsCDN(ip,cdn_list):
    geo = initGeoip()
    try:
        result = geo.asn(ip)
        if str(result.autonomous_system_number) in cdn_list:
            return True
    except Exception as e:
        print(e)
    return False

识别逻辑完成,接下来编写CDN识别主函数,使用了前文中提到的域名解析函数:

# CDN识别主函数
def CheckCDN(domain):
    cdnjson = initFingerprint(2)
    cdnfinger = json.load(cdnjson)
    if CnameIsCDN(domain,cdnfinger["all_CNAME"]):
        return True
    iplist = domainToip(domain)
    for ip in iplist:
        if IpIsCDN(ip,cdnfinger["cdns_ip"]):
            return True
        elif ASNIsCDN(ip,cdnfinger["ASNS"]):
            return True
    return False

我们拿一个使用CDN优化过的单人博客来测试:

识别结果

由于CDN厂商很少变换自己的网络位置,可以粗略使用这种方式进行CDN的识别判断。

数据包特征识别

由于不同操作系统对IP协议栈的实现不同,可以通过数据包特征识别对应的操作系统信息,其中最简单的特征是TTL,Windows的默认是128,Linux/Unix的常见TTL值是64,有些特殊的Unix会将TTL设置为255。

我们先来写一个简单的练练手,仍然使用Scapy对目标主机数据包进行探测:

# 通过数据包特征简单识别OS:
from scapy.layers.inet import TCP, IP
from scapy.sendrecv import sr1, send
def checkOS(ip,port):
    result = {
        "OS":None
    }
    try:
        # 发送一个握手包
        ans = sr1(IP(dst=ip) /
                  TCP(dport=port, flags="S"),
                  timeout=1, verbose=False)
        # 回显判断
        if ans.haslayer(IP):
            ttl = ans.getlayer(IP).ttl
            if ttl <= 64:
                result["OS"] = "Linux/Unix"
            elif ttl <= 128:
                result["OS"] = "Windows"
            elif ttl <= 255:
                result["OS"] = "Unix"
            # 发送挥手包
            send(IP(dst=ip) /
                TCP(dport=port, flags='R'),
                verbose=False)
    except Exception as e:
        print(e)
    return result

实际上,一些成熟的服务商都会伪装自己的TTL,此时需要更精确的方式来识别网络特征。这里,我们来看看Nmap是怎么处理的,源代码可以在Nmap官网找到,在nmap-os-db中包含了2600多已知系统的指纹特征,我们先挑选一条来看看:

#Windows 7 Professional Version 6.1 Build 7600
Fingerprint MicrosoftWindows 7 Professional
ClassMicrosoft | Windows | 7 | general purpose
CPEcpe:/o:microsoft:windows_7::professional
SEQ(SP=FC-106%GCD=1-6%ISR=108-112%TI=I%II=I%SS=S%TS=7)
OPS(O1=M5B4NW8ST11%O2=M5B4NW8ST11%O3=M5B4NW8NNT11%O4=M5B4NW8ST11%O5=M5B4NW8ST11%O6=M5B4ST11)
WIN(W1=2000%W2=2000%W3=2000%W4=2000%W5=2000%W6=2000)
ECN(R=Y%DF=Y%T=7B-85%TG=80%W=2000%O=M5B4NW8NNS%CC=N%Q=)
T1(R=Y%DF=Y%T=7B-85%TG=80%S=O%A=S+%F=AS%RD=0%Q=)
T2(R=N)
T3(R=N)
T4(R=N)
T5(R=Y%DF=Y%T=7B-85%TG=80%W=2000%S=Z%A=S+%F=AR%O=%RD=0%Q=)
T6(R=N)
T7(R=N)
U1(DF=N%T=7B-85%TG=80%IPL=164%UN=0%RIPL=G%RID=G%RIPCK=G%RUCK=G%RUD=G)
IE(DFI=N%T=7B-85%TG=80%CD=Z)

来分析一下各行的含义:

  1. 第一行为注释行,说明此指纹对应的操作系统与版本。
  2. Fingerprint关键字定义一个新的指纹,紧随其后的是指纹名字Microsoft Windows 7Professional。
  3. Class行用于指定该指纹所属的类别,依次指定该系统的vendor(生产厂家), OS family(系统类别), OS generation(第几代操作系统), 以及device type(设备类型)。此处Vendor为Microsoft, OS family为Windows,OS generation为7,设备类型为通用设备(普通PC或服务器)。
  4. CPE行,以标准的CPE(Common Platform Enumeration,通用平台枚举)格式来描述操作系统类型,便于Nmap与外界信息的交换,可以很快从网上开源数据库查找到CPE描述的操作系统具体信息。Nmap使用的标准格式为:cpe:/<part>:<vendor>:<product>:<version>:<update>:<edition>:<language>
  5. SEQ描述顺序产生方式
  6. OPS描述TCP包中可选字段的值
  7. WIN描述TCP包的初始窗口大小
  8. ECN(Explicit Congestion Notification)描述TCP明确指定拥塞通知时的特征
  9. T1-T7描述TCP回复包的字段特征
  10. U1描述向关闭的UDP发包产生的回复的特征
  11. IE描述向目标机发送ICMP包产生的特征

Nmap正是通过定义复杂的数据包特征来达到高精度识别操作系统的结果,我们来看一下主要的识别函数:

/* Performs the OS detection for IPv4 hosts. This method should not be called
 * directly. os_scan() should be used instead, as it handles chunking so
 * you don't do too many targets in parallel */
 
 ///IPv4的操作系统探测的实现函数,由os_scan()来调用。
int OSScan::os_scan_ipv4(vector<Target *> &Targets) {
  int itry = 0;
  /* Hosts which haven't matched and have been removed from incompleteHosts because
   * they have exceeded the number of retransmissions the host is allowed. */
  list<HostOsScanInfo *> unMatchedHosts; ///记录超时或超过最大重传而未匹配的主机扫描信息
 
  /* Check we have at least one target*/
  if (Targets.size() == 0) {
    return OP_FAILURE;
  }
 
  perf.init();///初始化扫描性能变量
 
  ///操作系统扫描的管理对象,维护未完成扫描列表std::list<HostOsScanInfo *> incompleteHosts;
  OsScanInfo OSI(Targets);
  if (OSI.numIncompleteHosts() == 0) {
    /* no one will be scanned */
    return OP_FAILURE;
  }
  ///设置起始时间与超时
  OSI.starttime = o.TimeSinceStart();
  startTimeOutClocks(&OSI);
 
  ///创建HOS对象,负责管理单个主机的具体扫描过程
  HostOsScan HOS(Targets[0]);
 
  /* Initialize the pcap session handler in HOS */
  ///打开libpcap,设置对应的BPF filter,以便接收目标的回复包
  begin_sniffer(&HOS, Targets);
  while (OSI.numIncompleteHosts() != 0) {
    if (itry > 0)
      sleep(1);
    if (itry == 3)
      usleep(1500000); /* Try waiting a little longer just in case it matters */
    if (o.verbose) {
      char targetstr[128];
      bool plural = (OSI.numIncompleteHosts() != 1);
      if (!plural) {
	(*(OSI.incompleteHosts.begin()))->target->NameIP(targetstr, sizeof(targetstr));
      } else Snprintf(targetstr, sizeof(targetstr), "%d hosts", (int) OSI.numIncompleteHosts());
      log_write(LOG_STDOUT, "%s OS detection (try #%d) against %s\n", (itry == 0)? "Initiating" : "Retrying", itry + 1, targetstr);
      log_flush_all();
    }
    ///准备第itry轮的OS探测:删除陈旧信息、初始化必要变量
    startRound(&OSI, &HOS, itry);
    ///执行顺序产生测试(发送6个TCP探测包,每隔100ms一个)
    doSeqTests(&OSI, &HOS);
    ///执行TCP/UDP/ICMP探测包测试
    doTUITests(&OSI, &HOS);
    ///对该轮探测的结果做指纹对比,获取OS扫描信息
    endRound(&OSI, &HOS, itry);
    ///将超时未匹配的主机移动到unMatchedHosts列表中
    expireUnmatchedHosts(&OSI, &unMatchedHosts);
    itry++;
  }
 
  /* Now move the unMatchedHosts array back to IncompleteHosts */
  ///对没有找到匹配的主机,将之移动的未完成列表,并查找出最接近的指纹(以概率形式展现给用户)
  if (!unMatchedHosts.empty())
    OSI.incompleteHosts.splice(OSI.incompleteHosts.begin(), unMatchedHosts);
 
  if (OSI.numIncompleteHosts()) {
    /* For hosts that don't have a perfect match, find the closest fingerprint
     * in the DB and, if we are in debugging mode, print them. */
    findBestFPs(&OSI);
    if (o.debugging > 1)
      printFP(&OSI);
  }
 
  return OP_SUCCESS;
}

总结一下,主要识别步骤如下:

  1. 根据进行的扫描次数,适当休眠
  2. 准备该轮扫描的所需的环境,清理垃圾数据并初始化必要的变量
  3. 提取指纹的SEQ/OPS/WIN/T1几行数据,并进行顺序产生测试(Sequence generationtests)。
  4. TCP/UDP/ICMP综合探测,拟合指纹数据。
  5. 处理此轮探测的结果,匹配相应的系统指纹,移除已完成的扫描任务。
  6. 移除超时不匹配的主机到unMatchedHosts列表中。

接下来,我们利用Nmap来实现指纹的探测,Nmap中文网提供了各种版本Nmap安装包,安装完成后,在Python环境中添加python-nmap模块,接下来编写OS指纹探测函数:

import nmap
# 利用Nmap识别OS:
def checkOS_nmap(ip):
    # 创建扫描对象
    scanner = nmap.PortScanner()
    result = {
        "OS":None
    }
    try:
        # 开始扫描
        response = scanner.scan(hosts=ip,arguments="-O")
        if response["scan"][ip]["osmatch"]:
            result["OS"] = response["scan"][ip]["osmatch"][0]
    except Exception as e:
        print(e)
    return result

仍然找一个某省主机做测试:

东南某省主机

测试一下两种探测结果:

识别结果

可以看到,两种方式都可以识别出操作系统类型,但Nmap的指纹库回显信息显然更加详细,关于Nmap指纹库的详细细节,可以查阅官网说明

结语

在本篇文章中我们列举了几种常见的网络特征指纹设计和识别方式,并分析了Nmap的OS指纹库和用来识别设备指纹的主要函数,需要读者有一定的文档和源码阅读能力,这里列出文中编写的完整代码地址

网络特征只是指纹构建的一种分支,这是一种基于规则库的专家系统思路。以这种方式匹配到的指纹一旦成功,精度较高,但对未收录在指纹库中的系统无能为力。

接下来的文章中,我们将总结并实现通过文本特征构建的指纹识别方式,并以SqlMap等主流工具的指纹库为例编码实现服务识别

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

halftion

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值