也许每个人出生的时候都以为这世界都是为他一个人而存在的,当他发现自己错的时候,他便开始长大
少走了弯路,也就错过了风景,无论如何,感谢经历
转移发布平台通知:将不再在CSDN博客发布新文章,敬请移步知识星球
感谢大家一直以来对我CSDN博客的关注和支持,但是我决定不再在这里发布新文章了。为了给大家提供更好的服务和更深入的交流,我开设了一个知识星球,内部将会提供更深入、更实用的技术文章,这些文章将更有价值,并且能够帮助你更好地解决实际问题。期待你加入我的知识星球,让我们一起成长和进步
ATTACK付费专栏长期更新,本篇最新内容请前往:
[车联网安全自学篇] ATTACK安全之Android车机证书攻击场景检测「检测系统代理」
拿到一个车机的Shell后,首先是要尽可能的收集相关敏感信息和内网横向的信息,以及查看密钥是否进行了不安全存储。比如密钥在系统的文件夹中存储,但是没有配置安全的权限,这里举个例子:假如有个文件的配置权限为777的开自启动.sh文件,此时低权限用户即可通过该配置错误的文件提升权限到root权限
在车企中许多时候,开发人员为了方便或量产前的疏忽,导致同款系列的每一辆车,有可能使用相同的初始CA证书,并用该CA证书生成设备与后端之间OTA通信加密用的永久证书,但往往初始CA证书的密码都为了方便设置的是弱密码(比如:123456或admin123等)或不设置密码,就跟我们在传统Web安全里发现一个后台登录界面一样,弱口令是永远的0day,嘻嘻(#.#)。看到这里大家也发现此处就存在了安全隐患,只要获得了其中一款证书就能获得同款系列所有车的CA证书,来进行攻击,比如攻击者利用泄露的初始CA证书来攻击制造商的后端服务,使攻击者获得永久证书,这会导致非常严重的危害,因为TCU和制造商之间的所有加密通信,都可被攻击者解密,所以进行渗透测试时需要重点关注已获得权限的系统上是否对密钥进行了不安全存储
密钥加密有两种:对称加密和非对称加密,后者也称为公钥加密。在对称加密中,对话的双方都使用相同的密钥将明文转换为密文,反之亦然
什么是对称/非对称加密?
对称加密优缺点:
非对称加密优缺点:
对称加密与非对称加密的区别:
在非对称或公共密钥加密中,对话的双方各自使用不同的密钥。一个密钥称为公钥,一个密钥称为私钥,之所以如此命名,是因为其中一方将其保密,并且永远不会与任何人共享。当使用公钥加密明文时,只有私钥可以解密它,而公钥不能。反过来也成立:用私钥加密的明文,只有公钥才能将其解密

从上,可以看出要说安全性的话,肯定是非对称加密安全,但是效率比较慢,对称加密效率高,但是不安全。较好的解决方法是将对称加密的密钥使用非对称加密的公钥进行加密(对称加密+非对称加密,这里利用了对称加密性能好,利用非对称加密安全的特性),然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。但在实际工作中开发人员会直接使用非对称加、解密,因为觉得平时一般请求的报文不会很大,加解密起来速度在可接受范围内,或者可以对敏感字段,比如密码、手机号、身份证号等进行分段加密,效率还可以
注:对称加密+非对称加密,虽然更安全了,但我们无法保证第一步服务器返回的公钥不会被黑客篡改。假如黑客把服务端返回的公钥变为自己的公钥,那么攻击者就可对客户端的所有消息使用自己的私钥解密
在车企中使用对称密钥加密来进行数据传输也是较为普遍,这里说车的吧,上面也说了对称加密只有一个密钥,该密钥必须在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证书等
注册证书(Enrollment Certificate)由注册CA颁发给车联网设备。车联网设备被注册机构认证后,由注册CA为其颁发注册证书。注册证书与设备唯一对应。设备需要使用注册证书从其他授权机构(Authorization Authority,AA)申请适用于某一应用领域的通信证书
假名证书(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 会遇到的一些安全问题:
CA服务平台:
TSP服务平台:
移动端:
车载系统:
VSD、充电桩等设备:
车载终端安全方案:主要关注车载终端T-BOX和TSP平台系统之间的数据传输加密和数据不可篡改、车载终端T-BOX的身份识别、车载终端T-BOX软件代码不被篡改等安全,使用LDAP、OCSP、CRL服务保证TSP平台系统中数字证书的实时真实有效性
TSP云端身份安全方案:主要针对TSP云端服务和后台人员身份识别,保障TSP服务端身份真实性,TSP后台人员网页操作与TSP平台系统数据签名防抵赖和数据传输加密安全
终端APP:主要针对APP终端用户、终端设备签发身份标识,保障终端APP与车载终端、及TSP服务端身份认证安全
设备厂商安全方案:主要针对设备厂商登录TSP平台系统的身份识别、重要节点数据安全等安全保障和法律服务
TCU上的证书一般分为两种:初始证书和常规证书
上面了解到对称加密都支持电子密码本模式(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 存在的一些安全问题:
可以了解一下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与后端进行初始连接,并使用初始密钥来新生成车辆的永久密钥(通常这个永久密钥的有效期贯穿车辆的使用寿命周期)
密钥的安全问题,不只是在Android 移动安全中才有,在车联网安全中也是存在的。我们这里说的密钥不是指用来开门或启动汽车的车钥匙,而是指用于对后端发送TCU(联网通信模块)的数据进行解密的加密密钥(此处说的密钥是私钥)
可信平台模块(TPM)和硬件安全模块(HSM):
可信平台模块(TPM)和硬件安全模块(HSM)都是用来加密存储密钥的硬件模块,是存储私钥的替代方法,只要使用了TPM或HSM后,私钥将存储在硬件模块内部
可信平台模块(TPM)和硬件安全模块(HSM)的区别:
可信平台模块(TPM)和硬件安全模块(HSM)的共同点:
从上面的了解中,我们知道了用于代码签名、PKI,以及密钥注入的密钥和证书,都是在数据中心的根信任HSM 中生成和保存着的,它存放的位置可能是在云服务器或汽车制造商甚至一级供应商内部。部分制造商也将汽车中搭载的车载网络HSM在市场上售卖
注:如果TCU(联网通信模块)在接收到后端的永久密钥后就会开始进行解密密钥的操作,然后将预处理和未加密的密钥存储在TCU(联网通信模块)文件系统目录的明文文件中,且该目录是全局可读的权限的话,你懂得,直接读啊,不就是我们想要的大宝贝吗?
再最后说一下OEM 通常喜欢用弱口令密码来保护私钥,攻击者只需要将私钥导出到本地物理主机,使用暴力破解工具花费一些时间来破解它,就可以获得该私钥密码并将该私钥导入到keychain(密码管理应用)中,最后我们就可以通过curl买了向后端发送HTTP请求来伪造成是该私钥所属的车辆发送的HTTP请求
此处的冒充估计指的是,攻击者成功地伪装为智能网联汽车与后端服务器之间的两个端点的其中一个端点,切记这里容易跟中间人攻击混淆,两者很相似,但有一点区别就是中间人攻击是两个之前互相传送数据,攻击者在中间件监听;冒充攻击,是直接伪造某一个端点设备的信息来发送;再说的简单点就是中间人攻击包含了冒充攻击的手段
在车机中查找密钥的方法,使用如下命令:
# 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:
.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 命令全局查找,先查找常见的证书后缀名格式,都没找到再换其他不常见的格式,优先查找如下格式:
如果找到密钥文件后,发现是加密的话,使用GPU暴力破解工具破解即可,破解成功后将其导入自己操作系统的keychain中就好了。导入证书后我们需要该证书的指纹,此时就需要在Personal–>Certificates存储区的证书列表中找到具有VIN号的证书,点击该证书查看该证书的指纹信息,该指纹是证书的唯一ID,为什么要获得该指纹,这里以SLL Pining为例说一下服务端一个简单的校验过程,如下:

额,看明白了吗?就是本地利用curl构造一个跟后端交互的请求,并开启Wireshark抓捕进出网的TCU数据包,如果该IV的固定部分不变,只是在固定部分上家了随机字节生成的数字,那么就存在被破解的风险,攻击者可能只需要观察足够多的TCU跟后端三次握手的包,就能计算出IV
注:也可使用openssl命令提取私钥,这里以从PKCS 12文件中提取私钥。下面列出OpenSSL 提取 pfx 证书公钥与私钥,从pfx证书中提取密钥信息,并转换为key格式(pfx使用pkcs12模式补足)
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
openssl pkcs8 -topk8 -inform PEM -in idsrv4_pri.key -outform PEM -nocrypt
注:额外多说一个小技巧,检测开启启动脚本比如init.rc,以及系统根目录下的*.rc文件,比如:
*.rc 脚本文件,是否有安装内核调试pseudo-filesystem且允许进程崩溃时转存内核
Android 的根证书存放位置( /system/etc/security/): 在 AOSP 源码库中,CA 根证书主要存放在 system/ca-certificates目录下,而在 Android 系统中,则存放在 /system/etc/security/目录下
system/update_engine、external/libbrillo和 system/core/crash_reporter等模块/data/data/APK应用程序名/cache:该目录是应用程序自身需要用到的证书,比如在 /data/data/com.guoshi.httpcanary/cache/目录下我们可以找到 HttpCanary.pem/dev/abcixxxe0.0find / -name "*.0"命令全局查找注:Android中的CA证书以其哈希名称存储,扩展名为“ 0”,比如12312312.0是CA证书
移动端常见的几种不安全的密钥存储方式:
端上:
D/NetworkSecurityConfig: Using Network Security Config from resource network_security_configI/X509Util: Failed to validate the certificate chain, error: Pin verification failedwpa_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
}
上面配置文件的含义是:
如下为补充知识:
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()来更新配置信息、并保存到本地;该函数的函数实现虽然较多,看起来复杂,但实际处理却还是较为简单的:
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对象中(可以看做是一个集合);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集合:
readIpAndProxyConfigurations()方法,从ipconfig.txt中读取保存的IpConfiguration对象,更新到mConfiguredNetworks保存的各WifiConfiguration对象中/data/misc/wifi/networkHistory.txt文件中读取保存的信息,更新到mConfiguredNetworks保存的各WifiConfiguration对象中读取的方式跟前面介绍的写入的方式基本相似。经过上所述的两次读取操作,我们持有的WifiConfiguration对象的信息就是比较完整的了。
如果有我们前面说过的无线PPPoE的场景,readIpAndProxyConfigurations()方法中就会把我们事先写入的账号密码信息也读取出来,存到mConfiguredNetworks中。走auto_connect流程时,获取到最近一次连接的网络netId,从mConfiguredNetworks中取出的对应的WifiConfiguration对象中就保存有PPPoE的账号密码,这样我们在PPPoE获取IP时,就有可用的账户信息了
其它更深入Android WiFi源码学习地址:
https://blog.csdn.net/csdn_of_coder/article/details/52389603https://bbs.pediy.com/thread-252161-1.htmhttp://static.kancloud.cn/alex_wsc/android-wifi-nfc-gps/414086源码中,我们已知,wifi配置时有两种状态,一种是直连(NONE),一种是配置了代理(STATIC)后有的状态:

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

… … 最后发送一个广播Proxy.PROXY_CHANGE_ACTION,后面省略,主要是不想分析了(其实就是不会了,O(∩_∩)O哈哈~)… …
这里当然用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.com")
.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();
}


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);
}
未完,待后续更新下一篇
参考链接:
https://www.cloudflare.com/zh-cn/learning/ssl/what-is-a-cryptographic-key/
https://juejin.cn/post/6844903584073515016
https://blog.51cto.com/groot/1877034
https://www.wangan.com/wenda/4013
你以为你有很多路可以选择,其实你只有一条路可以走