admin 管理员组

文章数量: 887019


也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大

少走了弯路,也就错过了风景,无论如何,感谢经历


转移发布平台通知:将不再在CSDN博客发布新文章,敬请移步知识星球

感谢大家一直以来对我CSDN博客的关注和支持,但是我决定不再在这里发布新文章了。为了给大家提供更好的服务和更深入的交流,我开设了一个知识星球,内部将会提供更深入、更实用的技术文章,这些文章将更有价值,并且能够帮助你更好地解决实际问题。期待你加入我的知识星球,让我们一起成长和进步


ATTACK付费专栏长期更新,本篇最新内容请前往:
[车联网安全自学篇] ATTACK安全之Android车机证书攻击场景检测「检测系统代理」

0x01 前言

拿到一个车机的Shell后,首先是要尽可能的收集相关敏感信息和内网横向的信息,以及查看密钥是否进行了不安全存储。比如密钥在系统的文件夹中存储,但是没有配置安全的权限,这里举个例子:假如有个文件的配置权限为777的开自启动.sh文件,此时低权限用户即可通过该配置错误的文件提升权限到root权限

在车企中许多时候,开发人员为了方便或量产前的疏忽,导致同款系列的每一辆车,有可能使用相同的初始CA证书,并用该CA证书生成设备与后端之间OTA通信加密用的永久证书,但往往初始CA证书的密码都为了方便设置的是弱密码(比如:123456或admin123等)或不设置密码,就跟我们在传统Web安全里发现一个后台登录界面一样,弱口令是永远的0day,嘻嘻(#.#)。看到这里大家也发现此处就存在了安全隐患,只要获得了其中一款证书就能获得同款系列所有车的CA证书,来进行攻击,比如攻击者利用泄露的初始CA证书来攻击制造商的后端服务,使攻击者获得永久证书,这会导致非常严重的危害,因为TCU和制造商之间的所有加密通信,都可被攻击者解密,所以进行渗透测试时需要重点关注已获得权限的系统上是否对密钥进行了不安全存储

密钥加密有两种:对称加密和非对称加密,后者也称为公钥加密。在对称加密中,对话的双方都使用相同的密钥将明文转换为密文,反之亦然

什么是对称/非对称加密?

  • 对称加密:指的就是加、解密使用的同是一串密钥,所以被称做对称加密。对称加密只有一个密钥作为私钥。常见的对称加密算法:DES,AES等
    • 提供了8种对称加密算法,其中7种是分组加密算法,仅有的一种流加密算法是RC4
    • 7种分组加密算法分别是AES、DES、Blowfish、CAST、IDEA、RC2、RC5,都支持电子密码本模式(ECB)、加密分组链接模式(CBC)、加密反馈模式(CFB)、计算器模式(CTR)和输出反馈模式(OFB)五种常用的分组密码加密模式。其中,AES使用的加密反馈模式(CFB)和输出反馈模式(OFB)分组长度是128位,其它算法使用的则是64位。事实上,DES算法里面不仅仅是常用的DES算法,还支持三个密钥和两个密钥3DES算法;计算器模式(CTR)不常见,在CTR模式中, 有一个自增的算子,这个算子用密钥加密之后的输出和明文异或的结果得到密文,相当于一次一密。这种加密方式简单快速,安全可靠,而且可以并行加密,但是在计算器不能维持很长的情况下,密钥只能使用一次
  • 非对称加密(也称为公钥加密):指的是加、解密使用不同的密钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。反之,私钥加密的信息,只有公钥才能解密。最常用的非对称加密算法:RSA
    • 提供了4种非对称加密算法,包括DH算法、RSA算法、DSA算法和椭圆曲线算法(EC)
    • DH算法一般用户密钥交换
    • RSA算法既可以用于密钥交换,也可以用于数字签名,但速度堪忧,你不介意的话也可以用于数据加密
    • DSA算法则一般只用于数字签名

对称加密优缺点:

  • 优点:对称加密相比非对称加密算法来说,加解密的效率要高得多、加密速度快
  • 缺点:对于密钥的管理和分发上比较困难,不是非常安全,密钥管理负担很重

非对称加密优缺点:

  • 优点:安全性更高,公钥是公开的,密钥是自己保存的,不需要将私钥给别人
  • 缺点:加密和解密花费时间长、速度慢,只适合对少量数据进行加密

对称加密与非对称加密的区别:

  • 两者之间的区别在于如何分发证书以及分发什么样的证书到设备端(客户端)

在非对称或公共密钥加密中,对话的双方各自使用不同的密钥。一个密钥称为公钥,一个密钥称为私钥,之所以如此命名,是因为其中一方将其保密,并且永远不会与任何人共享。当使用公钥加密明文时,只有私钥可以解密它,而公钥不能。反过来也成立:用私钥加密的明文,只有公钥才能将其解密

从上,可以看出要说安全性的话,肯定是非对称加密安全,但是效率比较慢,对称加密效率高,但是不安全。较好的解决方法是将对称加密的密钥使用非对称加密的公钥进行加密(对称加密+非对称加密,这里利用了对称加密性能好,利用非对称加密安全的特性),然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。但在实际工作中开发人员会直接使用非对称加、解密,因为觉得平时一般请求的报文不会很大,加解密起来速度在可接受范围内,或者可以对敏感字段,比如密码、手机号、身份证号等进行分段加密,效率还可以

:对称加密+非对称加密,虽然更安全了,但我们无法保证第一步服务器返回的公钥不会被黑客篡改。假如黑客把服务端返回的公钥变为自己的公钥,那么攻击者就可对客户端的所有消息使用自己的私钥解密

在车企中使用对称密钥加密来进行数据传输也是较为普遍,这里说车的吧,上面也说了对称加密只有一个密钥,该密钥必须在TCU(联网通信模块)和制造商的后端之间共享,同一个密钥用于加密和解密通信,这就导致制造商必须保存该密钥的副本,并将同一密钥放在TCU上,如果密钥遭到泄露,你懂得… …;再来说说非对称加密(也称为公钥加密),它是一个公钥和一个私钥来做加解密端点之间的信息,这就导致TCU(联网通信模块)将拥有制造商的公钥,而制造商将拥有该车系中每个TCU(联网通信模块)的公钥,当TCU(联网通信模块)通过OTA向后端发送数据时,将使用制造商的公钥来对信息做加密,且智能使用制造商的私钥才能对信息解密。反之亦然,当制造商将数据发送回TCU(联网通信模块)时,使用非对称加密中的TCU(联网通信模块)公钥对数据进行加密,私钥通过TCU(联网通信模块)和后端之间的GSM【全球移动通讯系统 (Global System for Mobile Communications)】连接进行交换

:后端安全通信(TLS):现在大部分汽车都具有联网通信模块(常说的TCU),该模块允许用于远程控制车辆,或者获取车辆信息。这些通常都是基于蜂窝网络的,目前在网络层的加密和验证通常使用TLS协议套件来实现,它是目前全世界最常用的安全协议

:此处的TCU,并不是指传统汽车领域里的「变速箱控制单元(transmission control unit)」,而是智能网联领域的「远程信息控制单元TCU(Telematic control unit)」,又称T-Box

依据车联网通信证书的用途可将证书分为CA证书、注册证书、假名证书和应用证书4类

  • CA证书(CA Certificate)是颁发注册证书、假名证书或应用证书的证书颁发机构(Certificate Authority,CA)的证书。其中根CA证书(Root Certificate)是一个自签名证书,其为一个PKI系统所有证书链的根节点,又称为一个PKI系统的信任锚点(trust anchor)。根CA可以根据需要向下级CA颁发子CA证书,例如注册CA证书、假名CA证书和应用CA证书等

    • CA将为后端生成用来跟OTA通信的证书,然后将证书防止在后端服务器上,用于TCU(联网通信模块)的公钥或私钥对
  • 注册证书(Enrollment Certificate)由注册CA颁发给车联网设备。车联网设备被注册机构认证后,由注册CA为其颁发注册证书。注册证书与设备唯一对应。设备需要使用注册证书从其他授权机构(Authorization Authority,AA)申请适用于某一应用领域的通信证书

    • CR 是使用公钥生成证书
  • 假名证书(Pseudonym Certificate)由假名CA颁发给OBU。OBU使用假名证书签发其播发的主动安全消息(Basic Safety Message,BSM)。为保护用户隐私,需要使用密码技术对用户的身份信息进行加密。OBU可拥有多个假名证书,用于定时切换使用,从而避免泄露车辆行驶轨迹。

  • 应用证书(Application Certificate)由应用CA颁发给RSU或车联网应用服务提供方(Service Provider,SP),用于特定的车联网应用领域。RSU使用应用证书签发其播发的某种应用消息。例如RSU使用应用证书签发其播发的交通信号灯状态、交通信息、商业服务消息等。针对某个特定的车联网应用,RSU只能拥有一个应用证书。

  • 身份证书(Identity Certificate)由应用CA颁发给OBU,用于特定的车联网应用领域。车联网设备使用应用证书签发其播发的某种应用消息。例如OBU使用应用证书签发其与其他OBU或RSU交互的消息。针对某个特定的车联网应用,OBU只能拥有一个应用证书。

互联网通信PKI系统的证书文件较大,但存储有限且需要避免DSRC通道拥堵,车辆PKI系统需较短的密钥,为了满足该需求,车辆PKI系统使用椭圆曲线加密算法((ECDSA-256)密铜,可生成大小约为Internet证书八分之一的证书)

需要注意,后端上的证书只是设备公钥或私钥对中的公钥,该公钥由CA的私钥签名,还有后端对发送到设备的通信数据进行加密,只有设备的私钥才能对传输的信息进行解密,因为后端使用与设备私钥配对的公钥对数据进行加密。

比如TCU(联网通信模块)PKI 会遇到的一些安全问题:

  • 汽车端:T-box
    • 如何实时辨别T-box端的真实身份,非仿冒车载终端
    • 如何确认从T-box传到TSP通讯数据的加密性、完整性、不可篡改性
    • 如何保障T-box软件代码不被恶意篡改
    • 如何让终端设备、终端APP、网页访问人员确认TSP平台是可信网站,而非钓鱼网站
    • 如何确认终端关键操作时间的真实性
    • 如何确保蓝牙钥匙与车载终端的安全认证
  • 手机端:终端APP
  • TSP后台怎么识别APP的真实性,怎么确认APP身份
  • 如何确认终端APP与TSP后台之间数据传输安全性、加密性
  • 如何确认用户关键操作时间的真实性
  • 如何确保蓝牙钥匙与车载终端的安全认证
  • TSP后台所在的云端服务器、云端管理人员
    • 如何让终端设备、终端APP、网页访问人员确认TSP平台是可信网站,而非钓鱼网站
    • 云端管理人员登录TSP后台时,如何确认管理人员身份
    • 云端管理人员在TSP进行重要业务操作时,如何保障信息安全性、抗抵赖性
  • 设备厂商访问TSP后台操作
    • 设备厂商登录TSP后台时,如何进行身份认证
    • 设备厂商登录TSP进行发布新消息、新产品等时,如何保障信息的完整性、防抵赖性
    • 如何保障设备厂商上传的软件代码不被恶意篡改
    • 如何确认用户关键操作时间的真实性
    • 一旦出现纠纷,如何保障车联网的权益

CA服务平台:

  • 签名验签系统
  • 数字证书认证系统
  • 加密机
  • 证书综合管理系统

TSP服务平台:

  • 车载系统服务端或OTA
  • 电子签名应用服务器(API)
  • SSL服务器证书
  • 数字证书安全存储

移动端:

  • 车联网APP
  • 移动端SDK
  • 证书存储沙盒
  • 证书应用接口

车载系统:

  • 证书定制库
  • 证书应用接口
  • 证书存储模块/芯片
  • 车载操作系统

VSD、充电桩等设备:

  • 车联网服务
  • 证书应用接口
  • 证书存储模块

车载终端安全方案:主要关注车载终端T-BOX和TSP平台系统之间的数据传输加密和数据不可篡改、车载终端T-BOX的身份识别、车载终端T-BOX软件代码不被篡改等安全,使用LDAP、OCSP、CRL服务保证TSP平台系统中数字证书的实时真实有效性

TSP云端身份安全方案:主要针对TSP云端服务和后台人员身份识别,保障TSP服务端身份真实性,TSP后台人员网页操作与TSP平台系统数据签名防抵赖和数据传输加密安全

终端APP:主要针对APP终端用户、终端设备签发身份标识,保障终端APP与车载终端、及TSP服务端身份认证安全

设备厂商安全方案:主要针对设备厂商登录TSP平台系统的身份识别、重要节点数据安全等安全保障和法律服务

TCU上的证书一般分为两种:初始证书和常规证书

  • 初始证书:在制造阶段安装在设备上的证书
    • 不排除初始证书存在有规律可循的现象或直接是一模一样的
  • 常规证书:在量产时,制造商后端初次启动的时候会使用初始证书,并为未来TCU和后端之间的所有OTA通信生成一个证书,新生成的证书将是未来所有通信的永久会话密钥

上面了解到对称加密都支持电子密码本模式(ECB),简单的说下ECB模式的原理:首先需要将明文分组,每个分组长度与密钥Key的长度相同,然后每个分组使用相同的密钥进行AES加密。再来说下ECB模式的安全缺点,因为每个分组的加密方式和Key完全相同,在明文相同的情况下,密文将会完全一样,导致存在安全风险隐患,但但但尽管ECB模式有多么多么的不行,还是有一些厂商会使用

除了ECB模式外,加密分组链接模式(CBC)、加密反馈模式(CFB)和输出反馈模式(OFB)等模式都使用一个名为初始化向量IV(一个随机的且长度为一个分组长度的比特序列)的消息随机数来确保每次加密的结果都是不同的,比如TCU(联网通信模块)和后端服务器之间的加密通信中向密文添加随机性或不可预测性来确保每次加密的结果都是不同的。IV可能存在的攻击面,车机自带的OEM系统不排除能重复使用之前发送过的初始化向量IV,该OME系统使用的IV是基于后端通过GSM【全球移动通讯系统 (Global System for Mobile Communications)】发送到TCU(联网通信模块)的证书序列号生成的,存在中间人攻击风险。这里大家可以想到路由器中使用的WEP 有线等效加密为什么不再被使用,其中一个原因就是因为V 长度为 24 位,可供使用的 IV 值为 1600 万,因此在网络上很快会出现重复,一旦出现重复的密钥流,流密码很容易被攻击者识破。因此当TCU(联网通信模块)使用固定IV与相同密钥时,总用相同的密文加密数据,一旦出现重复的密钥流,者就很容易被查看流量的攻击者们关注。下面列一下WEP 存在的一些安全问题:

  • 手动管理密钥存在重大隐患:配相同的密钥给所有成员过程繁琐,只要有任意成员离开,就需要重新分配密钥,且一旦密钥泄漏,将无任何私密性可言
  • 密钥问题:EP 通过简单级联初始化向量(Initialization Vector,IV)和密钥形成种子,并以明文方式发送 IV,这种方式在 RC4 算法下容易产生弱密钥,给入侵者打开了方便之门
  • 空间问题:V 长度为 24 位,可供使用的 IV 值为 1600 万,因此在网络上很快会出现重复,一旦出现重复的密钥流,流密码很容易被识破
  • 防篡改:EP 采用的 CRC32 算法不能阻止攻击者篡改数据,由于 CRC32 是线性运算,攻击者很容易在篡改密文的同时,更改与明文对应的 ICV,这样接收方 ICV 校验无法检测出数据是否经过篡改
  • 防重放攻击:WEP 不能防重放攻击。在重放攻击中,攻击者会发送一系列以前捕获的帧,尝试以此方式获得访问权或修改数据

可以了解一下XOR异或算法,通过指定的密钥对每个字符执行按位异或运算来加密文本字符串,想要解密的话就需要对应的密钥来做异或运算即可,某些厂商习惯使用相同的密钥和相同的固定IV加密TCU与后端之间的消息,此时攻击者只需要将两个密文异或在一起,就能得到对应明文的异或运算解密方法。这些总总信息表面,如果车企在使用TCU(联网通信模块)的时候,应用对称加密的CBC模式和固定IV的话,就容易导致加密的信息被恢复明文,而且仅仅只需要攻击者拥有有效的IV即可。所以,在对车机进行渗透时,我们可以重点关注IV是否被加密,被加密了的话我们要排查OEM系统用于加密IV的密钥是否与用于加密信息的密钥不同,如果两者使用的密钥相同,就算OEM系统使用了CTR模式加密,只要OEM仍然使用ECB模式加密IV,导致任意是谁都可用加密的IV对密文的第一个块进行异或运算来获得该块的明文信息

初始密钥:在较多数OEM系统中的TCU(联网通信模块)都会有一个初始密钥,该密钥一般情况下是OEM创建,为了避免密钥被窃取,每个控制器的初始密钥应该是都不相同的,但许多时候大家可能还是为了图方便,都使用相同的初始密钥。比如,为TCU配置第一个密钥,该密钥用于其首次通电后通过OTA与后端进行初始连接,这个过程中初始密钥用来请求其永久会话密钥,然后将该证书存储在TCU(联网通信模块)中,那么此处的攻击面就来了,只要OEM在每个设备中使用了相同的初始密钥,那么攻击者只需要满足后端在初始连接器件的所有校验,即可欺骗后端说自己就是XXX设备端

  • 同学们,是不是以为密钥没有有效期?

答:其实,也是有有效期的,但有效期时间的长短不一,理想状态的半年,但可能会遇到长达几年甚至几十年的证书,跟特闷永久的没啥区别了,可能车都开报废或人都挂彩了,证书还没过期。举个例子,比如之前我们讲过的TCU(联网通信模块)在首次通电后会通过OTA与后端进行初始连接,并使用初始密钥来新生成车辆的永久密钥(通常这个永久密钥的有效期贯穿车辆的使用寿命周期)

1.1 密钥存储不安全

密钥的安全问题,不只是在Android 移动安全中才有,在车联网安全中也是存在的。我们这里说的密钥不是指用来开门或启动汽车的车钥匙,而是指用于对后端发送TCU(联网通信模块)的数据进行解密的加密密钥(此处说的密钥是私钥)

可信平台模块(TPM)和硬件安全模块(HSM):

  • TPM:TPM就像门卫一样,特别保护车辆的外部接口,例如在车载信息娱乐系统或远程信息处理单元中的接口。它检查数字数据发送方和接收方的身份,比如制造商的后端服务器。它对数据进行加密和解密,并帮助确保只有驾驶员或制造商真正想要的数据才能进入汽车。交互中安全功能所需的加密密钥存储在TPM中,就像存储在保险箱中一样,即使有人从车上吹下芯片,这些密钥也不会被读取
  • HSM:HSM是一种自主的硬件,可以用于车辆安全信息(比如密钥)的生成,存贮以及处理,且隔离外部恶意软件的攻击;HSM可以用于构建,验证可靠的软件,以保护在软件加载并初次访问之前的安全启动。HSM包含有加密/解密硬件加速功能,和软件解决方案相比(Cry),能够有效降低CPU负载

可信平台模块(TPM)和硬件安全模块(HSM)都是用来加密存储密钥的硬件模块,是存储私钥的替代方法,只要使用了TPM或HSM后,私钥将存储在硬件模块内部

可信平台模块(TPM)和硬件安全模块(HSM)的区别:

  • 1)可信平台模块 (TPM) 是计算机主板上的硬件芯片,用于存储用于加密的加密密钥
    • 现在许多计算机都有TPM,比如,当我们打开 Microsoft Windows BitLocker 进行整个磁盘加密的时候,它实际上会在计算机的TPM中查找加密/解密文件的密钥,这样做的原因是为了防止别人将加密后的硬盘从计算机中取出,放到另外一台计算机中启动来获取原来计算机中的数据,如果新系统没有包含密钥的TPM,新系统将无法启动
    • 通常TPM 包含一个烧入其中的唯一 RSA 密钥,用于非对称加密。此外,它还可以生成、存储和保护加密和解密过程中(TCU(联网通信模块)和后端之间的数据)使用的其他密钥
  • 2)硬件安全模块 (HSM) 是一种安全设备,您可以添加到系统中以管理、生成和安全存储加密密钥
    • HSM 是可移动或使用 TCP/IP 连接到网络的外部设备(一个独立的系统图),较小的 HSM 也可作为安装在服务器中的扩展卡,或作为插入计算机端口的设备提供,但这种情况较少见
    • HSM支持密钥注入,能使用随机数生成器将单个密钥注入半导体芯片中
      • 注:凭借汽车零部件的唯一密钥,智能网联汽车拥有了数字身份证,在整个声明周期中验证车辆和内部部件以及软件。比如对车机中的APP软件进行数字签名,来验证APP软件的真实性、完整性、可用性
    • HSM 可用于车载、车对基础设施和车对车通信,被用来验证车内的每个零部件,包含每个ECU和通过OTA发送到每个车辆的更新信息
  • 两者区别:
    • HSM 是可移动或使用 TCP/IP 连接到网络的外部设备(一个独立的系统图)
    • TPM 通常是按照在TCU(联网通信模块)上的芯片

可信平台模块(TPM)和硬件安全模块(HSM)的共同点:

  • 两者都是通过存储和使用 RSA 密钥来提供安全加密功能

从上面的了解中,我们知道了用于代码签名、PKI,以及密钥注入的密钥和证书,都是在数据中心的根信任HSM 中生成和保存着的,它存放的位置可能是在云服务器或汽车制造商甚至一级供应商内部。部分制造商也将汽车中搭载的车载网络HSM在市场上售卖

:如果TCU(联网通信模块)在接收到后端的永久密钥后就会开始进行解密密钥的操作,然后将预处理和未加密的密钥存储在TCU(联网通信模块)文件系统目录的明文文件中,且该目录是全局可读的权限的话,你懂得,直接读啊,不就是我们想要的大宝贝吗?

再最后说一下OEM 通常喜欢用弱口令密码来保护私钥,攻击者只需要将私钥导出到本地物理主机,使用暴力破解工具花费一些时间来破解它,就可以获得该私钥密码并将该私钥导入到keychain(密码管理应用)中,最后我们就可以通过curl买了向后端发送HTTP请求来伪造成是该私钥所属的车辆发送的HTTP请求

1.2 冒充攻击

此处的冒充估计指的是,攻击者成功地伪装为智能网联汽车与后端服务器之间的两个端点的其中一个端点,切记这里容易跟中间人攻击混淆,两者很相似,但有一点区别就是中间人攻击是两个之前互相传送数据,攻击者在中间件监听;冒充攻击,是直接伪造某一个端点设备的信息来发送;再说的简单点就是中间人攻击包含了冒充攻击的手段

在车机中查找密钥的方法,使用如下命令:

# find /  -name  *.证书后缀名
find / -name *.p12

根据不同的服务器以及服务器的版本,需要用到不同的证书格式,下面列一些格式:

  • .DER和.CER:是二进制格式,只保存证书,不保存私钥
  • .PEM:一般是文本格式,可保存证书,可保存私钥
  • .key:是一个pem格式只包含私玥的文件,.key 作为文件名只是作为一个明显的别名
  • .cert、.cer、.crt:pem或者der编码格式的证书文件,这些文件后缀名都会被windows 资源管理器认为是证书文件
  • .CRT:可是二进制格式,也可是文本格式,与 .DER 格式相同,不保存私钥
  • .PFX 和 .P12:是二进制格式,同时包含证书和私钥,一般有密码保护
  • JKS:是二进制格式,JKS证书通常将根证书、中间证书、用户证书和私钥合并存放并设置密码,主要用于Java Web Server,同时包含证书和私钥
  • BKS
    • 在Android中是无法使用jks证书的,Android 系统中使用的证书要求是bks格式
  • .csr:是证书请求文件,是由 RFC 2986定义的PKCS10格式,包含部分/全部的请求证书的信息,比如,主题, 机构,国家等,并且包含了请求证书的公玥,这些被CA中心签名后返回一张证书。返回的证书是公钥证书(只包含公玥不含私钥)
  • PKCS#7:基于Base64编码的证书格式,扩展名包括p7b和p7c。PKCS#7证书通常将根证书和中间证书合并存放,将用户证书单独存放。PKCS#7证书不包含私钥,主要用于Tomcat和Windows Server
  • .p8.p8是 PKCS #8的文件格式后缀,PKCS #8格式的名字为私钥信息语法标准(Private-Key Information Syntax Standard)。存储的私钥可以被加密,支持多密码加密,存储的内容是PEM 编码的格式
  • PKCS#12:基于二进制编码的证书格式,该格式通常将私钥与其X508证书捆绑在一起,扩展名包括pfx和p12。PKCS#12证书通常将根证书、中间证书、用户证书和私钥合并存放并设置密码,主要用于Windows Server

:证书颁发机构CA签发的证书通常是PEM格式或PKCS#7格式,而PKCS#12格式和JKS格式的证书需要进行证书格式转换才能得到,可以通过OpenSSL、Keytool或在线证书转换工具等方式将PEM格式或PKCS#7格式的证书转换为我们需要的其他格式

证书文件常见格式:

文件后缀文件类型说明
*.DER或*.CER二进制格式只含有证书信息,不包含私钥
*.CRT二进制格式或文本格式只含有证书信息,不包含私钥
*.PEM文本格式一般存放证书或私钥,或同时包含证书和私钥。*.PEM文件如果只包含私钥,一般用*.KEY文件代替
*.PFX或*.P12二进制格式同时包含证书和私钥,且一般有密码保护

:常见SSL证书主要的文件类型和协议有: PEM、DER、PFX、JKS、KDB、CER、KEY、CSR、CRT、CRL、OCSP、SCEP

说的简单点,就是使用find 命令全局查找,先查找常见的证书后缀名格式,都没找到再换其他不常见的格式,优先查找如下格式:

  • pem
    • .crt
    • .cer
  • p12
  • pfx
  • .p7b
  • .p7c

如果找到密钥文件后,发现是加密的话,使用GPU暴力破解工具破解即可,破解成功后将其导入自己操作系统的keychain中就好了。导入证书后我们需要该证书的指纹,此时就需要在Personal–>Certificates存储区的证书列表中找到具有VIN号的证书,点击该证书查看该证书的指纹信息,该指纹是证书的唯一ID,为什么要获得该指纹,这里以SLL Pining为例说一下服务端一个简单的校验过程,如下:

  • 首先对服务器端证书/证书中的公钥进行哈希,得到ssl指纹内置到app中
  • 通信连接时,服务器发来证书
  • app对该证书进行哈希操作,将哈希值与app内置的ssl指纹进行对比,若对比成功,则建立连接,否则,断开连接

额,看明白了吗?就是本地利用curl构造一个跟后端交互的请求,并开启Wireshark抓捕进出网的TCU数据包,如果该IV的固定部分不变,只是在固定部分上家了随机字节生成的数字,那么就存在被破解的风险,攻击者可能只需要观察足够多的TCU跟后端三次握手的包,就能计算出IV

:也可使用openssl命令提取私钥,这里以从PKCS 12文件中提取私钥。下面列出OpenSSL 提取 pfx 证书公钥与私钥,从pfx证书中提取密钥信息,并转换为key格式(pfx使用pkcs12模式补足)

  • 提取密钥对(如果pfx证书已加密,会提示输入密码)
openssl pkcs12 -in idsrv4.pfx -nocerts -nodes -out idsrv4.key
  • 从密钥对提取公钥
openssl rsa -in idsrv4.key -pubout -out idsrv4_pub.key
  • 从密钥对提取私钥
openssl rsa -in  idsrv4.key -out idsrv4_pri.key
  • 因为RSA算法使用的是 pkcs8 模式补足,需要对提取的私钥进一步处理得到最终私钥
openssl pkcs8 -topk8 -inform PEM -in idsrv4_pri.key -outform PEM -nocrypt

:额外多说一个小技巧,检测开启启动脚本比如init.rc,以及系统根目录下的*.rc文件,比如:

  • 1)可能从中会发现我们意想不到的内容,比如adbd 守护进程明明被关闭了,但系统重启后会再次被开启
  • 2)可能开发人员对系统使用+RW(读+写)挂载根文件系统,一般情况下只是可读,能可写的话,那对攻击者来说就有操作的空间了,修改启动项,以root权限执行某个脚本等等恶意行为
  • 4)查看*.rc 脚本文件,是否有安装内核调试pseudo-filesystem且允许进程崩溃时转存内核
    • 如果允许进程崩溃时转存内核,攻击者可以利用内核调试信息和崩溃转储文件(核心文件)来获取进程以 UID/GID为root权限运行的当前该进程更多的信息
  • 5)检查系统所有文件中是否存在出发系统超级用户命令,比如设置root命令
  • 6)系统处于DevMode (开发者模式) 下可能会有一些意想不到的操作,在翻看文件的时候也需要重要关注

Android 的根证书存放位置( /system/etc/security/): 在 AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates目录下,而在 Android 系统中,则存放在 /system/etc/security/目录下

  • cacerts_google 目录:该目录下的根证书,主要用于 system/update_engine、external/libbrillosystem/core/crash_reporter等模块
  • cacerts 目录:该目录下的根证书则用于所有的应用,且该目录下的根证书,即 Android 系统的根证书库
  • /data/data/APK应用程序名/cache:该目录是应用程序自身需要用到的证书,比如在 /data/data/com.guoshi.httpcanary/cache/目录下我们可以找到 HttpCanary.pem
  • /dev/: 设备文件, Linux系统常规文件夹,里面的文件很多都是设备模拟的文件系统。这里面也可能存放CA证书,比如/dev/abcixxxe0.0
  • 其他存放方式,使用find / -name "*.0"命令全局查找

:Android中的CA证书以其哈希名称存储,扩展名为“ 0”,比如12312312.0是CA证书

移动端常见的几种不安全的密钥存储方式:

  • 直接硬编码在代码中,很容易被逆向分析
  • 存储在私有目录的文件中,有最高权限的终端可以导出查看
  • 将密钥分段,分别存储在代码和文件中,最终在内存中拼接起来,由于内存中还是出现了完整的原始密钥,所以攻击者只需要花点时间,也可以逆向分析出来;
  • 将密钥存储在动态库中,同时加解密也都在动态库中进行,这在一定程度上增大了分析难度,但是有经验的攻击者并不难将其逆向出来
  • 用另外一个密钥B加密这个密钥A,将密钥A的密文存储在文件或代码中,在实际使用时进行解密,虽然增加了静态分析难度,但可以使用动态调试的方法,对加解密方法进行插桩,即 Hook,也可以分析出密钥 A

1.3 证书攻击面

端上:

  • 本地存储(Android 密钥库和公钥的攻击面):分为服务端和客户端检测,服务端平台不可控所以做不了
    • 硬编码在APP代码中或本地存储在系统的某个文件中(比如某个txt文件、加密数据库、资源文件、应用程序日志文件、证书等等里面,还有可能有傻逼放到外部存储的SD卡上)
    • 被动攻击一种是指直接获取消息的内容,还有一种是对消息的某些特征进行分析
    • 主动攻击是指对明文数据的篡改来产生对攻击有价值的密文数据,防止主动攻击一般都非常困难,需要提前预防
  • 系统设置的系统/普通证书(system用户证书和user用户证书)
    • 注:Android 7以上需root权限,而且还需要包含反Hook的检测,要不然绕过风险很高
    • 校验证书完整性检查,如有效性、完整性和可信度
    • 证书是否由可信 CA 签发
    • 证书是否过期
    • 证书是否自签名
  • 应用程序SSL 证书固定
    • 某些应用程序将实现 SSL 固定,这会阻止应用程序将拦截证书作为有效证书接受。这代表者我们无法监视应用程序和服务器之间的通信量
  • 检测加密操作时监视文件系统访问,以评估向何处写入或从何处读取 key material
    • 有些证书密钥,可能会在应用程序首次跟后端服务器建立连接时获取,不会硬编码在代码中
    • 检测方法:
    • 1、D/NetworkSecurityConfig: Using Network Security Config from resource network_security_config
    • 2、I/X509Util: Failed to validate the certificate chain, error: Pin verification failed
  • 证书绕过
    • Objection 工具:使用 android sslpinning disable 命令
    • Xposed:安装 TrustMeAlready 或 SSLUnpinning 模块
    • Cydia Substrate:安装 Android-SSL-TrustKiller
    • 静态绕过自定义证书锁定
    • 执行find 命令查找证书的行为
    • 检测hook 工具绕过证书时的固定系统函数

0x02 Android Wi-Fi 是否配置系统代理的检测

wpa_supplicant.conf 配置文件是连接 WiFi 设备的一些配置信息,比如如下:

country=CN
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
network={
    ssid="12345678"
    psk="88888888"
    key_mgmt=WPA-PSK
    priority=1
}

上面配置文件的含义是:

  • ssid:网络的ssid
  • psk:密码
  • priority: 连接优先级,数字越大优先级越高(不可以是负数)
  • scan_ssid:连接隐藏WiFi时需要指定该值为1

如下为补充知识

Android Wi-Fi 主要的代码模块都放在WifiConfigStore.java中,该类主要负责网络配置信息的管理工作,包括保存、读取配置信息等。当我们在Settings中触发一个保存网络、连接网络或者auto_connect自动重连动作时,都会调用到WifiConfigStore中的方法:

public class WifiConfigStore extends IpConfigStore

WifiConfigStore继承自IpConfigStore,它提供了一套API去管理用户配置过的网络。下面介绍一些framework中经常调用到的API接口

1)saveNetwork()、selectNetwork()

WifiStateMachine中,WifiConfigStore对象的创建发生在其构造函数中:

mWifiConfigStore = new WifiConfigStore(context,this,  mWifiNative);

此处传入了Context、当前的WifiStateMachine对象和一个WifiNative对象。通过mWifiNative对象可以向wpa_s下发一系列连接、选择的命令。我们在连接一个网络的时候,会先保存该网络的配置信息,调用:

/**
     * Add/update the specified configuration and save config
     *
     * @param config WifiConfiguration to be saved
     * @return network update result
     */
    NetworkUpdateResult saveNetwork(WifiConfiguration config, int uid) {
        WifiConfiguration conf;
 
        // A new network cannot have null SSID
        if (config == null || (config.networkId == INVALID_NETWORK_ID &&
                config.SSID == null)) {
            return new NetworkUpdateResult(INVALID_NETWORK_ID);
        }
        if (VDBG) localLog("WifiConfigStore: saveNetwork netId", config.networkId);
        if (VDBG) {
            loge("WifiConfigStore saveNetwork, size=" + mConfiguredNetworks.size()
                    + " SSID=" + config.SSID
                    + " Uid=" + Integer.toString(config.creatorUid)
                    + "/" + Integer.toString(config.lastUpdateUid));
        }
 
        if (mDeletedEphemeralSSIDs.remove(config.SSID)) {
            if (VDBG) {
                loge("WifiConfigStore: removed from ephemeral blacklist: " + config.SSID);
            }
            // NOTE: This will be flushed to disk as part of the addOrUpdateNetworkNative call
            // below, since we're creating/modifying a config.
        }
 
        boolean newNetwork = (config.networkId == INVALID_NETWORK_ID);
        NetworkUpdateResult result = addOrUpdateNetworkNative(config, uid);
        int netId = result.getNetworkId();
 
        if (VDBG) localLog("WifiConfigStore: saveNetwork got it back netId=", netId);
 
        /* enable a new network */
        if (newNetwork && netId != INVALID_NETWORK_ID) {
            if (VDBG) localLog("WifiConfigStore: will enable netId=", netId);
 
            mWifiNative.enableNetwork(netId, false);
            conf = mConfiguredNetworks.get(netId);
            if (conf != null)
                conf.status = Status.ENABLED;
        }
 
        conf = mConfiguredNetworks.get(netId);
        if (conf != null) {
            if (conf.autoJoinStatus != WifiConfiguration.AUTO_JOIN_ENABLED) {
                if (VDBG) localLog("WifiConfigStore: re-enabling: " + conf.SSID);
 
                // reenable autojoin, since new information has been provided
                conf.setAutoJoinStatus(WifiConfiguration.AUTO_JOIN_ENABLED);
                enableNetworkWithoutBroadcast(conf.networkId, false);
            }
            if (VDBG) {
                loge("WifiConfigStore: saveNetwork got config back netId="
                        + Integer.toString(netId)
                        + " uid=" + Integer.toString(config.creatorUid));
            }
        }
 
        mWifiNative.saveConfig();
        sendConfiguredNetworksChangedBroadcast(conf, result.isNewNetwork() ?
                WifiManager.CHANGE_REASON_ADDED : WifiManager.CHANGE_REASON_CONFIG_CHANGE);
        return result;
    }

saveNetwork()主要负责根据WifiConfiguration对象更新、保存网络的各配置信息;WifiConfiguration代表一个配置过的网络,主要包括该网络的加密方式、SSID、密钥等等信息。重要的一个操作是调用addOrUpdateNetworkNative()来更新配置信息、并保存到本地;该函数的函数实现虽然较多,看起来复杂,但实际处理却还是较为简单的:

  • 首先从mConfiguredNetworks中根据传入的config对象获取到先前保存过的同netId的savedConfig对象;mConfiguredNetworks是一个HasMap结构,它以某个网络的netId为key,以对应的WifiConfiguration对象作为value,由此可知它以键值对的形式保存了当前所有配置过的网络信息。后续的操作都是比对config和savedConfig直接的差异,保存到wpa_s配置文件中并进行更新,最后再将更新过的WifiConfiguration对象保存到mConfiguredNetworks中
  • 调用writeIpAndProxyConfigurationsOnChange()将新的配置信息保存到本地文件/data/misc/wifi/ipconfig.txt
    /* Compare current and new configuration and write to file on change */
    private NetworkUpdateResult writeIpAndProxyConfigurationsOnChange(
            WifiConfiguration currentConfig,
            WifiConfiguration newConfig) {
        boolean ipChanged = false;
        boolean proxyChanged = false;
 
        if (VDBG) {
            loge("writeIpAndProxyConfigurationsOnChange: " + currentConfig.SSID + " -> " +
                    newConfig.SSID + " path: " + ipConfigFile);
        }
 
 
        switch (newConfig.getIpAssignment()) {
            case STATIC:
                if (currentConfig.getIpAssignment() != newConfig.getIpAssignment()) {
                    ipChanged = true;
                } else {
                    ipChanged = !Objects.equals(
                            currentConfig.getStaticIpConfiguration(),
                            newConfig.getStaticIpConfiguration());
                }
                break;
            case DHCP:
                if (currentConfig.getIpAssignment() != newConfig.getIpAssignment()) {
                    ipChanged = true;
                }
                break;
            case UNASSIGNED:
                /* Ignore */
                break;
            default:
                loge("Ignore invalid ip assignment during write");
                break;
        }
 
        switch (newConfig.getProxySettings()) {
            case STATIC:
            case PAC:
                ProxyInfo newHttpProxy = newConfig.getHttpProxy();
                ProxyInfo currentHttpProxy = currentConfig.getHttpProxy();
 
                if (newHttpProxy != null) {
                    proxyChanged = !newHttpProxy.equals(currentHttpProxy);
                } else {
                    proxyChanged = (currentHttpProxy != null);
                }
                break;
            case NONE:
                if (currentConfig.getProxySettings() != newConfig.getProxySettings()) {
                    proxyChanged = true;
                }
                break;
            case UNASSIGNED:
                /* Ignore */
                break;
            default:
                loge("Ignore invalid proxy configuration during write");
                break;
        }
 
        if (ipChanged) {
            currentConfig.setIpAssignment(newConfig.getIpAssignment());
            currentConfig.setStaticIpConfiguration(newConfig.getStaticIpConfiguration());
            log("IP config changed SSID = " + currentConfig.SSID);
            if (currentConfig.getStaticIpConfiguration() != null) {
                log(" static configuration: " +
                    currentConfig.getStaticIpConfiguration().toString());
            }
        }
 
        if (proxyChanged) {
            currentConfig.setProxySettings(newConfig.getProxySettings());
            currentConfig.setHttpProxy(newConfig.getHttpProxy());
            log("proxy changed SSID = " + currentConfig.SSID);
            if (currentConfig.getHttpProxy() != null) {
                log(" proxyProperties: " + currentConfig.getHttpProxy().toString());
            }
        }
 
        if (ipChanged || proxyChanged) {
            writeIpAndProxyConfigurations();
            sendConfiguredNetworksChangedBroadcast(currentConfig,
                    WifiManager.CHANGE_REASON_CONFIG_CHANGE);
        }
        return new NetworkUpdateResult(ipChanged, proxyChanged);
    }

函数中涉及到IpAssignment和ProxySettings两个枚举类型:

   public enum IpAssignment {
        /* Use statically configured IP settings. Configuration can be accessed
         * with staticIpConfiguration */
        STATIC,
        /* Use dynamically configured IP settigns */
        DHCP,
        /* no IP details are assigned, this is used to indicate
         * that any existing IP settings should be retained */
        UNASSIGNED
    }
 
    public enum ProxySettings {
        /* No proxy is to be used. Any existing proxy settings
         * should be cleared. */
        NONE,
        /* Use statically configured proxy. Configuration can be accessed
         * with httpProxy. */
        STATIC,
        /* no proxy details are assigned, this is used to indicate
         * that any existing proxy settings should be retained */
        UNASSIGNED,
        /* Use a Pac based proxy.
         */
        PAC
    }

IpAssignment代表当前获取IP使用的方式,我们可以根据自己的需求在里面添加自定义的方式,比如PPPoE;同理,ProxySettings表示当前网络使用的代理方式

IpAssignment类型的值一般由设置根据用户选择的IP模式来赋值,并传递给framework,以让底层可以知道该使用什么方式去获取IP地址。例如,如果用户选择Static IP,则在WifiStateMachine::ObtainingIpState中会有:

            if (!mWifiConfigStore.isUsingStaticIp(mLastNetworkId)) {
                if (isRoaming()) {
                    renewDhcp();
                } else {
                    // Remove any IP address on the interface in case we're switching from static
                    // IP configuration to DHCP. This is safe because if we get here when not
                    // roaming, we don't have a usable address.
                    clearIPv4Address(mInterfaceName);
                    startDhcp();
                }
                obtainingIpWatchdogCount++;
                logd("Start Dhcp Watchdog " + obtainingIpWatchdogCount);
                // Get Link layer stats so as we get fresh tx packet counters
                getWifiLinkLayerStats(true);
                sendMessageDelayed(obtainMessage(CMD_OBTAINING_IP_ADDRESS_WATCHDOG_TIMER,
                        obtainingIpWatchdogCount, 0), OBTAINING_IP_ADDRESS_GUARD_TIMER_MSEC);
            } else {
                // stop any running dhcp before assigning static IP
                stopDhcp();
                StaticIpConfiguration config = mWifiConfigStore.getStaticIpConfiguration(
                        mLastNetworkId);
                if (config.ipAddress == null) {
                    logd("Static IP lacks address");
                    sendMessage(CMD_STATIC_IP_FAILURE);
                } else {
                    InterfaceConfiguration ifcg = new InterfaceConfiguration();
                    ifcg.setLinkAddress(config.ipAddress);
                    ifcg.setInterfaceUp();
                    try {
                        mNwService.setInterfaceConfig(mInterfaceName, ifcg);
                        if (DBG) log("Static IP configuration succeeded");
                        DhcpResults dhcpResults = new DhcpResults(config);
                        sendMessage(CMD_STATIC_IP_SUCCESS, dhcpResults);
                    } catch (RemoteException re) {
                        loge("Static IP configuration failed: " + re);
                        sendMessage(CMD_STATIC_IP_FAILURE);
                    } catch (IllegalStateException e) {
                        loge("Static IP configuration failed: " + e);
                        sendMessage(CMD_STATIC_IP_FAILURE);
                    }
                }
            }

通过WifiConfigStore.isUsingStaticIp(mLastNetworkId)方法获知当前用户使用的获取IP地址类型,具体方法定义:

    /**
     * Return if the specified network is using static IP
     * @param netId id
     * @return {@code true} if using static ip for netId
     */
    boolean isUsingStaticIp(int netId) {
        WifiConfiguration config = mConfiguredNetworks.get(netId);
        if (config != null && config.getIpAssignment() == IpAssignment.STATIC) {
            return true;
        }
        return false;
    }

根据传入的netId,从mConfiguredNetworks集合中获取对应网络的WifiConfiguration对象,再获取该对象配置的IpAssignment值,来区分不用的网络方式,进而控制流程走不同的分支。如果我们有加入别的方式,可以仿照这个原生例子,写出自己的程序。

riteIpAndProxyConfigurationsOnChange()中会根据IpAssignment、ProxySettings的类型是否改变,去更新currentConfig对象,并writeIpAndProxyConfigurations()方法写入到本地磁盘文件:

    private void writeIpAndProxyConfigurations() {
        final SparseArray<IpConfiguration> networks = new SparseArray<IpConfiguration>();
        for(WifiConfiguration config : mConfiguredNetworks.values()) {
            if (!config.ephemeral && config.autoJoinStatus != WifiConfiguration.AUTO_JOIN_DELETED) {
                networks.put(configKey(config), config.getIpConfiguration());
            }
        }
 
        super.writeIpAndProxyConfigurations(ipConfigFile, networks);//in IpConfigStore
    }
    
   public void IpConfigStore::writeIpAndProxyConfigurations(String filePath,
                                              final SparseArray<IpConfiguration> networks) {
        mWriter.write(filePath, new DelayedDiskWrite.Writer() {
            public void onWriteCalled(DataOutputStream out) throws IOException{
                out.writeInt(IPCONFIG_FILE_VERSION);
                for(int i = 0; i < networks.size(); i++) {
                    writeConfig(out, networks.keyAt(i), networks.valueAt(i));
                }
            }
        });
    }
    
   private boolean writeConfig(DataOutputStream out, int configKey,
                                IpConfiguration config) throws IOException {
        boolean written = false;
 
        try {
            switch (config.ipAssignment) {
                case STATIC:
                    out.writeUTF(IP_ASSIGNMENT_KEY);
                    out.writeUTF(config.ipAssignment.toString());
                    StaticIpConfiguration staticIpConfiguration = config.staticIpConfiguration;
                    if (staticIpConfiguration != null) {
                        if (staticIpConfiguration.ipAddress != null) {
                            LinkAddress ipAddress = staticIpConfiguration.ipAddress;
                            out.writeUTF(LINK_ADDRESS_KEY);
                            out.writeUTF(ipAddress.getAddress().getHostAddress());
                            out.writeInt(ipAddress.getPrefixLength());
                        }
                        if (staticIpConfiguration.gateway != null) {
                            out.writeUTF(GATEWAY_KEY);
                            out.writeInt(0);  // Default route.
                            out.writeInt(1);  // Have a gateway.
                            out.writeUTF(staticIpConfiguration.gateway.getHostAddress());
                        }
                        for (InetAddress inetAddr : staticIpConfiguration.dnsServers) {
                            out.writeUTF(DNS_KEY);
                            out.writeUTF(inetAddr.getHostAddress());
                        }
                    }
                    written = true;
                    break;
                case DHCP:
                    out.writeUTF(IP_ASSIGNMENT_KEY);
                    out.writeUTF(config.ipAssignment.toString());
                    written = true;
                    break;
                case UNASSIGNED:
                /* Ignore */
                    break;
                default:
                    loge("Ignore invalid ip assignment while writing");
                    break;
            }
 
            switch (config.proxySettings) {
                case STATIC:
                    ProxyInfo proxyProperties = config.httpProxy;
                    String exclusionList = proxyProperties.getExclusionListAsString();
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.proxySettings.toString());
                    out.writeUTF(PROXY_HOST_KEY);
                    out.writeUTF(proxyProperties.getHost());
                    out.writeUTF(PROXY_PORT_KEY);
                    out.writeInt(proxyProperties.getPort());
                    if (exclusionList != null) {
                        out.writeUTF(EXCLUSION_LIST_KEY);
                        out.writeUTF(exclusionList);
                    }
                    written = true;
                    break;
                case PAC:
                    ProxyInfo proxyPacProperties = config.httpProxy;
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.proxySettings.toString());
                    out.writeUTF(PROXY_PAC_FILE);
                    out.writeUTF(proxyPacProperties.getPacFileUrl().toString());
                    written = true;
                    break;
                case NONE:
                    out.writeUTF(PROXY_SETTINGS_KEY);
                    out.writeUTF(config.proxySettings.toString());
                    written = true;
                    break;
                case UNASSIGNED:
                    /* Ignore */
                        break;
                    default:
                        loge("Ignore invalid proxy settings while writing");
                        break;
            }
 
            if (written) {
                out.writeUTF(ID_KEY);
                out.writeInt(configKey);
            }
        } catch (NullPointerException e) {
            loge("Failure in writing " + config + e);
        }
        out.writeUTF(EOS);
 
        return written;
    }

最终写入文件的操作是父类IpConfigStore::writeConfig()方法处理的。在WifiConfigStore::writeIpAndProxyConfigurations()中,我们会将所有保存的网络配置信息从mConfiguredNetworks集合中取出,重新按照<一个唯一int值,IpConfiguration>的形式保存到一个SparseArray<IpConfiguration> networks对象中(可以看做是一个集合);ipConfigFile的值就是路径:“/data/misc/wifi/ipconfig.txt”。

IpConfigStore::writeIpAndProxyConfigurations和IpConfigStore::writeConfig()中,我们会遍历networks集合,并按照

switch (config.ipAssignment) 
switch (config.proxySettings)  

的分类,将信息写入ipconfig.txt文件中;这里的写入也是有一定的规则的,每一个标签后面跟一个该标签对应的值。这样做方法后面的数据读取。定义的标签值有:

    /* IP and proxy configuration keys */
    protected static final String ID_KEY = "id";
    protected static final String IP_ASSIGNMENT_KEY = "ipAssignment";
    protected static final String LINK_ADDRESS_KEY = "linkAddress";
    protected static final String GATEWAY_KEY = "gateway";
    protected static final String DNS_KEY = "dns";
    protected static final String PROXY_SETTINGS_KEY = "proxySettings";
    protected static final String PROXY_HOST_KEY = "proxyHost";
    protected static final String PROXY_PORT_KEY = "proxyPort";
    protected static final String PROXY_PAC_FILE = "proxyPac";
    protected static final String EXCLUSION_LIST_KEY = "exclusionList";
    protected static final String EOS = "eos";
 
    protected static final int IPCONFIG_FILE_VERSION = 2;

从这里我们可以看到一些可以定制的地方。现在,有一部分Android手机上的Wifi功能是支持无线PPPoE的;要使用PPPoE,就要用到账户信息;此时,我们是否可以在WifiConfiguration或IpConfiguration中添加对应的账户属性字段,在保存网络时,加入对账户密码字段的写入保存动作;同时,在从ipconfig.txt读取信息时,将该信息重新封装到WifiConfiguration或IpConfiguration对象中,供无线PPPoE获取IP时使用

最后还会涉及到writeKnownNetworkHistory()的调用,它会向/data/misc/wifi/networkHistory.txt中写入每个WifiConfiguration对象中的一些字段值,包括优先级、SSID等等;写入方式跟前面相同。这里,saveNetwork()的处理就结束了。selectNetwork()的作用是选择一个特定的网络去准备连接,这里会涉及到网络优先级更新和enable网络的部分

    /**
     * Selects the specified network for connection. This involves
     * updating the priority of all the networks and enabling the given
     * network while disabling others.
     *
     * Selecting a network will leave the other networks disabled and
     * a call to enableAllNetworks() needs to be issued upon a connection
     * or a failure event from supplicant
     *
     * @param config network to select for connection
     * @param updatePriorities makes config highest priority network
     * @return false if the network id is invalid
     */
    boolean selectNetwork(WifiConfiguration config, boolean updatePriorities, int uid) {
        if (VDBG) localLog("selectNetwork", config.networkId);
        if (config.networkId == INVALID_NETWORK_ID) return false;
 
        // Reset the priority of each network at start or if it goes too high.
        if (mLastPriority == -1 || mLastPriority > 1000000) {
            for(WifiConfiguration config2 : mConfiguredNetworks.values()) {
                if (updatePriorities) {
                    if (config2.networkId != INVALID_NETWORK_ID) {
                        config2.priority = 0;
                        setNetworkPriorityNative(config2.networkId, config.priority);
                    }
                }
            }
            mLastPriority = 0;
        }
 
        // Set to the highest priority and save the configuration.
        if (updatePriorities) {
            config.priority = ++mLastPriority;
            setNetworkPriorityNative(config.networkId, config.priority);
            buildPnoList();
        }
 
        if (config.isPasspoint()) {
            /* need to slap on the SSID of selected bssid to work */
            if (getScanDetailCache(config).size() != 0) {
                ScanDetail result = getScanDetailCache(config).getFirst();
                if (result == null) {
                    loge("Could not find scan result for " + config.BSSID);
                } else {
                    log("Setting SSID for " + config.networkId + " to" + result.getSSID());
                    setSSIDNative(config.networkId, result.getSSID());
                    config.SSID = result.getSSID();
                }
 
            } else {
                loge("Could not find bssid for " + config);
            }
        }
 
        if (updatePriorities)
            mWifiNative.saveConfig();
        else
            mWifiNative.selectNetwork(config.networkId);
 
        updateLastConnectUid(config, uid);
        writeKnownNetworkHistory(false);
 
        /* Enable the given network while disabling all other networks */
        enableNetworkWithoutBroadcast(config.networkId, true);
 
       /* Avoid saving the config & sending a broadcast to prevent settings
        * from displaying a disabled list of networks */
        return true;
    }

mLastPriority是一个int类型的整数值,它代表当前网络中的优先级的最大值。越是最近连接过的网络,它的priority优先级值就越大。updatePriorities代表是否需要更新优先级。当当前的最大优先级值为-1或1000000时,都会重新设置mLastPriority值;如果updatePriorities为true,也会将更改更新到wpa_supplicant.conf文件中

        // Set to the highest priority and save the configuration.
        if (updatePriorities) {
            config.priority = ++mLastPriority;
            setNetworkPriorityNative(config.networkId, config.priority);
            buildPnoList();
        }

从这可以看出,每个当前正在连接的网络,都具有最高的优先级。最后enableNetworkWithoutBroadcast()中,会在mConfiguredNetworks将选中网络的status属性设为Status.ENABLED,其他的设置为Status.DISABLED

    /* Mark all networks except specified netId as disabled */
    private void markAllNetworksDisabledExcept(int netId) {
        for(WifiConfiguration config : mConfiguredNetworks.values()) {
            if(config != null && config.networkId != netId) {
                if (config.status != Status.DISABLED) {
                    config.status = Status.DISABLED;
                    config.disableReason = WifiConfiguration.DISABLED_UNKNOWN_REASON;
                }
            }
        }

2)重新打开Wifi时,ipconfig.txt文件的读取

当我们重新打开Wifi时,Wifi正常情况下都会有网络自动重连的动作。此时,WifiStateMachine中:

mWifiConfigStore.loadAndEnableAllNetworks();
    /**
     * Fetch the list of configured networks
     * and enable all stored networks in supplicant.
     */
    void loadAndEnableAllNetworks() {
        if (DBG) log("Loading config and enabling all networks ");
        loadConfiguredNetworks();
        enableAllNetworks();
    }

loadConfiguredNetworks()

    void loadConfiguredNetworks() {
 
        mLastPriority = 0;
 
        mConfiguredNetworks.clear();
 
        int last_id = -1;
        boolean done = false;
        while (!done) {
 
            String listStr = mWifiNative.listNetworks(last_id);
            if (listStr == null)
                return;
 
            String[] lines = listStr.split("\n");
 
            if (showNetworks) {
                localLog("WifiConfigStore: loadConfiguredNetworks:  ");
                for (String net : lines) {
                    localLog(net);
                }
            }
 
            // Skip the first line, which is a header
            for (int i = 1; i < lines.length; i++) {
                String[] result = lines[i].split("\t");
                // network-id | ssid | bssid | flags
                WifiConfiguration config = new WifiConfiguration();
                try {
                    config.networkId = Integer.parseInt(result[0]);
                    last_id = config.networkId;
                } catch(NumberFormatException e) {
                    loge("Failed to read network-id '" + result[0] + "'");
                    continue;
                }
                if (result.length > 3) {
                    if (result[3].indexOf("[CURRENT]") != -1)
                        config.status = WifiConfiguration.Status.CURRENT;
                    else if (result[3].indexOf("[DISABLED]") != -1)
                        config.status = WifiConfiguration.Status.DISABLED;
                    else
                        config.status = WifiConfiguration.Status.ENABLED;
                } else {
                    config.status = WifiConfiguration.Status.ENABLED;
                }
 
                readNetworkVariables(config);
 
                Checksum csum = new CRC32();
                if (config.SSID != null) {
                    csum.update(config.SSID.getBytes(), 0, config.SSID.getBytes().length);
                    long d = csum.getValue();
                    if (mDeletedSSIDs.contains(d)) {
                        loge(" got CRC for SSID " + config.SSID + " -> " + d + ", was deleted");
                    }
                }
 
                if (config.priority > mLastPriority) {
                    mLastPriority = config.priority;
                }
 
                config.setIpAssignment(IpAssignment.DHCP);//默认设置DHCP
                config.setProxySettings(ProxySettings.NONE);//默认设置NONE
 
                if (mConfiguredNetworks.getByConfigKey(config.configKey()) != null) {
                    // That SSID is already known, just ignore this duplicate entry
                    if (showNetworks) localLog("discarded duplicate network ", config.networkId);
                } else if(WifiServiceImpl.isValid(config)){
                    mConfiguredNetworks.put(config.networkId, config);
                    if (showNetworks) localLog("loaded configured network", config.networkId);
                } else {
                    if (showNetworks) log("Ignoring loaded configured for network " + config.networkId
                        + " because config are not valid");
                }
            }
 
            done = (lines.length == 1);
        }
 
        readPasspointConfig();
        readIpAndProxyConfigurations();//读取ipconfig.txt
        readNetworkHistory();//读取networkHistory.txt
        readAutoJoinConfig();
 
        buildPnoList();
 
        sendConfiguredNetworksChangedBroadcast();
 
        if (showNetworks) localLog("loadConfiguredNetworks loaded " + mConfiguredNetworks.size() + " networks");
 
        if (mConfiguredNetworks.isEmpty()) {
            // no networks? Lets log if the file contents
            logKernelTime();
            logContents(SUPPLICANT_CONFIG_FILE);
            logContents(SUPPLICANT_CONFIG_FILE_BACKUP);
            logContents(networkHistoryConfigFile);
        }
    }

函数开始就会清空mConfiguredNetworks集合:

  • 从wp_s读取保存的网络配置列表,并保存到mConfiguredNetworks中
  • 调用readIpAndProxyConfigurations()方法,从ipconfig.txt中读取保存的IpConfiguration对象,更新到mConfiguredNetworks保存的各WifiConfiguration对象中
  • 调用mConfiguredNetworks()方法,从/data/misc/wifi/networkHistory.txt文件中读取保存的信息,更新到mConfiguredNetworks保存的各WifiConfiguration对象中

读取的方式跟前面介绍的写入的方式基本相似。经过上所述的两次读取操作,我们持有的WifiConfiguration对象的信息就是比较完整的了。

如果有我们前面说过的无线PPPoE的场景,readIpAndProxyConfigurations()方法中就会把我们事先写入的账号密码信息也读取出来,存到mConfiguredNetworks中。走auto_connect流程时,获取到最近一次连接的网络netId,从mConfiguredNetworks中取出的对应的WifiConfiguration对象中就保存有PPPoE的账号密码,这样我们在PPPoE获取IP时,就有可用的账户信息了

其它更深入Android WiFi源码学习地址:

  • 如上补充知识的原文链接:https://blog.csdn/csdn_of_coder/article/details/52389603
  • 另外一个觉得挺细的Wifi启动流程的分析文章,看雪师傅的,地址如下:https://bbs.pediy/thread-252161-1.htm
  • 深入理解Android:Wi-Fi、NFC和GPS卷:http://static.kancloud/alex_wsc/android-wifi-nfc-gps/414086

源码中,我们已知,wifi配置时有两种状态,一种是直连(NONE),一种是配置了代理(STATIC)后有的状态:

接着获取这个代理的配置是否为空,不为空的话就获取获取代理设置的信息:

… … 最后发送一个广播Proxy.PROXY_CHANGE_ACTION,后面省略,主要是不想分析了(其实就是不会了,O(∩_∩)O哈哈~)… …

0x02 检测Android 系统代理是否开启的方法

这里当然用APP代码直接搞是最容易的,但我们还是想插深一丢丢,明白是怎么检测的。前面我们已经对相关的源码做了了解,接下来我们要使用dumpsys 命令来帮助我们进行检测,先来介绍一下dumpsys

dumpsys 是一种在 Android 设备上运行的工具,可提供有关系统服务的信息。您可以使用 Android 调试桥 (ADB) 从命令行调用 dumpsys,获取在连接的设备上运行的所有系统服务的诊断输出。此输出通常比您想要的更详细,因此您可以使用下文所述的命令行选项仅获取您感兴趣的系统服务的输出。本页还介绍了如何使用 dumpsys 完成常见的任务,如检查输入、RAM、电池或网络诊断

  • 第一种方式:检查/data/misc/wifi/ipconfig.txt里面有没有STATIC+proxyHost+proxyPort三个关键字,至于为什么看过之前WiFi 补充知识部分的同学应该就明白了
    • 注:有些Android设备,会把WiFi可设置系统代理等高级选项的功能阉割掉,此处的判断可能就没有效果了。可以使用adb shell settings put global http_proxy 127.0.0.1:8888命令来设置代理,但有时候也会不管用,当然我们还可以使用代理工具的APP来实现代理,此处不是我们要讨论和研究的,还有单向认证、双向认证、SSL证书固定等,但此处不设计,所以不做检测

    • 缺点:不管是有没有配置了No Proxy参数,只要做了全局代理都会被绕过。如下第一张图表示的No Proxy参数发起网络请求,即使系统设置了代理也会被绕过的对比图:

    • No Proxy参数发起网络请求的代码:

    public void run() {
          Looper.prepare();
          OkHttpClient okHttpClient = new OkHttpClient.Builder().
                  proxy(Proxy.NO_PROXY).      // 使用此参数,可绕过系统代理直接发包
                  build();
          Request request = new Request.Builder()
                  .url("http://www.baidu")
                  .build();
          Response response = null;
          try {
              response = okHttpClient.newCall(request).execute();
              Toast.makeText(this, Objects.requireNonNull(response.body()).string(), Toast.LENGTH_SHORT).show();
          } catch (IOException e) {
              e.printStackTrace();
          }
          Looper.loop();
      }
    

  • 第二种检测方式:判断WiFi信息里面的Proxy settings参数值是否不为STATIC,否则就是存在系统代理行为

利用dumpsys 获取wifi的服务信息,判断有没有Proxy settings: STATIC

adb shell dumpsys wifi | grep "Proxy settings: STATIC" -A 1

如果有代理返回代理信息,否则返回为空。需结合下面的命令判断是否是当前连接的Wi-Fi(通过networkId判断)

adb shell dumpsys netstats | grep "Active interfaces:" -A 1

:上面grep -A参数后的1 ,是表示除了显示符合范本样式的那一行之外,并显示该行之后的内容

接下来,配置Android 设备端在设置代理时和没有设置代理时的Proxy settings状态,如下:

adb shell "dumpsys wifi | grep 'Proxy settings: STATIC' -A 1"


adb shell "dumpsys wifi | grep 'Proxy settings: NONE' -A 1"

下面,是一段常见的APP检测系统是否有代理的实例代码:

 /**
     * 判断设备 是否使用代理上网
     * @param context 上下文对象
     * return  当前网络是否开启了代理
     */
public static boolean isWifiProxy(Context context) {
    final boolean IS_ICS_OR_LATER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
    String proxyAddress;
    int proxyPort;
    if (IS_ICS_OR_LATER) {
        proxyAddress = System.getProperty("http.proxyHost");    // 获取代理主机
        String portStr = System.getProperty("http.proxyPort");  // 获取代理端口
        proxyPort = Integer.parseInt((portStr != null ? portStr : "-1"));
    } else {
        proxyAddress = android.net.Proxy.getHost(context);
        proxyPort = android.net.Proxy.getPort(context);
    }
   Log.i("代理信息","proxyAddress :"+proxyAddress + "prot : " proxyPort")
   return (!TextUtils.isEmpty(proxyAddress)) && (proxyPort != -1);
}
  • VPN:tap、tun、ipsec、ppp

未完,待后续更新下一篇

参考链接

https://www.cloudflare/zh-cn/learning/ssl/what-is-a-cryptographic-key/

https://juejin/post/6844903584073515016

https://blog.51cto/groot/1877034

https://www.wangan/wenda/4013


你以为你有很多路可以选择,其实你只有一条路可以走


本文标签: 检测系统 场景 证书 专栏 Android