1.   DDoS攻击基础

DDoS(Distributed Denial of Service,分布式拒绝服务)攻击的主要目的是让指定目标无法提供正常服务,甚至从互联网上消失,是目前最强大、最难防御的攻击之一。

按照发起的方式,DDoS可以简单分为三类。

第一类以力取胜,海量数据包从互联网的各个角落蜂拥而来,堵塞IDC入口,让各种强大的硬件防御系统、快速高效的应急流程无用武之地。这种类型的攻击典型代表是ICMP Flood和UDP Flood,现在已不常见。

第二类以巧取胜,灵动而难以察觉,每隔几分钟发一个包甚至只需要一个包,就可以让豪华配置的服务器不再响应。这类攻击主要是利用协议或者软件的漏洞发起,例如Slowloris攻击、Hash冲突攻击等,需要特定环境机缘巧合下才能出现。

第三类是上述两种的混合,轻灵浑厚兼而有之,既利用了协议、系统的缺陷,又具备了海量的流量,例如SYN Flood攻击、DNS Query Flood攻击,是当前的主流攻击方式。

本文将一一描述这些最常见、最具代表性攻击方式,并介绍它们的防御方案。

1.1. SYN Flood

SYN Flood是互联网上最经典的DDoS攻击方式之一,最早出现于1999年左右,雅虎是当时最著名的受害者。SYN Flood攻击利用了TCP三次握手的缺陷,能够以较小代价使目标服务器无法响应,且难以追查。

标准的TCP三次握手过程如下:

l  客户端发送一个包含SYN标志的TCP报文,SYN即同步(Synchronize),同步报文会指明客户端使用的端口以及TCP连接的初始序号;

l  服务器在收到客户端的SYN报文后,将返回一个SYN+ACK(即确认Acknowledgement)的报文,表示客户端的请求被接受,同时TCP初始序号自动加1;

l  客户端也返回一个确认报文ACK给服务器端,同样TCP序列号被加1。

经过这三步,TCP连接就建立完成。TCP协议为了实现可靠传输,在三次握手的过程中设置了一些异常处理机制。第三步中如果服务器没有收到客户端的最终ACK确认报文,会一直处于SYN_RECV状态,将客户端IP加入等待列表,并重发第二步的SYN+ACK报文。重发一般进行3-5次,大约间隔30秒左右轮询一次等待列表重试所有客户端。另一方面,服务器在自己发出了SYN+ACK报文后,会预分配资源为即将建立的TCP连接储存信息做准备,这个资源在等待重试期间一直保留。更为重要的是,服务器资源有限,可以维护的SYN_RECV状态超过极限后就不再接受新的SYN报文,也就是拒绝新的TCP连接建立。

SYN Flood正是利用了上文中TCP协议的设定,达到攻击的目的。攻击者伪装大量的IP地址给服务器发送SYN报文,由于伪造的IP地址几乎不可能存在,也就几乎没有设备会给服务器返回任何应答了。因此,服务器将会维持一个庞大的等待列表,不停地重试发送SYN+ACK报文,同时占用着大量的资源无法释放。更为关键的是,被攻击服务器的SYN_RECV队列被恶意的数据包占满,不再接受新的SYN请求,合法用户无法完成三次握手建立起TCP连接。也就是说,这个服务器被SYN Flood拒绝服务了。

对SYN Flood有兴趣的可以看看http://www.icylife.net/yunshu/show.php?id=367,这是我2006年写的代码,后来做过几次修改,修改了Bug,并降低了攻击性,纯做测试使用。

1.2. DNS Query Flood

作为互联网最基础、最核心的服务,DNS自然也是DDoS攻击的重要目标之一。打垮DNS服务能够间接打垮一家公司的全部业务,或者打垮一个地区的网络服务。前些时候风头正盛的黑客组织anonymous也曾经宣布要攻击全球互联网的13台根DNS服务器,不过最终没有得手。

UDP攻击是最容易发起海量流量的攻击手段,而且源IP随机伪造难以追查。但过滤比较容易,因为大多数IP并不提供UDP服务,直接丢弃UDP流量即可。所以现在纯粹的UDP流量攻击比较少见了,取而代之的是UDP协议承载的DNS Query Flood攻击。简单地说,越上层协议上发动的DDoS攻击越难以防御,因为协议越上层,与业务关联越大,防御系统面临的情况越复杂。

DNS Query Flood就是攻击者操纵大量傀儡机器,对目标发起海量的域名查询请求。为了防止基于ACL的过滤,必须提高数据包的随机性。常用的做法是UDP层随机伪造源IP地址、随机伪造源端口等参数。在DNS协议层,随机伪造查询ID以及待解析域名。随机伪造待解析域名除了防止过滤外,还可以降低命中DNS缓存的可能性,尽可能多地消耗DNS服务器的CPU资源。

关于DNS Query Flood的代码,我在2011年7月为了测试服务器性能曾经写过一份代码,链接是http://www.icylife.net/yunshu/show.php?id=832。同样的,这份代码人为降低了攻击性,只做测试用途。

1.3. HTTP Flood

上文描述的SYN Flood、DNS Query Flood在现阶段已经能做到有效防御了,真正令各大厂商以及互联网企业头疼的是HTTP Flood攻击。HTTP Flood是针对Web服务在第七层协议发起的攻击。它的巨大危害性主要表现在三个方面:发起方便、过滤困难、影响深远。

SYN Flood和DNS Query Flood都需要攻击者以root权限控制大批量的傀儡机。收集大量root权限的傀儡机很花费时间和精力,而且在攻击过程中傀儡机会由于流量异常被管理员发现,攻击者的资源快速损耗而补充缓慢,导致攻击强度明显降低而且不可长期持续。HTTP Flood攻击则不同,攻击者并不需要控制大批的傀儡机,取而代之的是通过端口扫描程序在互联网上寻找匿名的HTTP代理或者SOCKS代理,攻击者通过匿名代理对攻击目标发起HTTP请求。匿名代理是一种比较丰富的资源,花几天时间获取代理并不是难事,因此攻击容易发起而且可以长期高强度的持续。

另一方面,HTTP Flood攻击在HTTP层发起,极力模仿正常用户的网页请求行为,与网站业务紧密相关,安全厂商很难提供一套通用的且不影响用户体验的方案。在一个地方工作得很好的规则,换一个场景可能带来大量的误杀。

最后,HTTP Flood攻击会引起严重的连锁反应,不仅仅是直接导致被攻击的Web前端响应缓慢,还间接攻击到后端的Java等业务层逻辑以及更后端的数据库服务,增大它们的压力,甚至对日志存储服务器都带来影响。

有意思的是,HTTP Flood还有个颇有历史渊源的昵称叫做CC攻击。CC是Challenge Collapsar的缩写,而Collapsar是国内一家著名安全公司的DDoS防御设备。从目前的情况来看,不仅仅是Collapsar,所有的硬件防御设备都还在被挑战着,风险并未解除。

1.4. 慢速连接攻击

提起攻击,第一反应就是海量的流量、海量的报文。但有一种攻击却反其道而行之,以慢著称,以至于有些攻击目标被打死了都不知道是怎么死的,这就是慢速连接攻击,最具代表性的是rsnake发明的Slowloris。

HTTP协议规定,HTTP Request以\r\n\r\n结尾表示客户端发送结束,服务端开始处理。那么,如果永远不发送\r\n\r\n会如何?Slowloris就是利用这一点来做DDoS攻击的。攻击者在HTTP请求头中将Connection设置为Keep-Alive,要求Web Server保持TCP连接不要断开,随后缓慢地每隔几分钟发送一个key-value格式的数据到服务端,如a:b\r\n,导致服务端认为HTTP头部没有接收完成而一直等待。如果攻击者使用多线程或者傀儡机来做同样的操作,服务器的Web容器很快就被攻击者占满了TCP连接而不再接受新的请求。

很快的,Slowloris开始出现各种变种。比如POST方法向Web Server提交数据、填充一大大Content-Length但缓慢的一个字节一个字节的POST真正数据内容等等。关于Slowloris攻击,rsnake也给出了一个测试代码,参见http://ha.ckers.org/slowloris/slowloris.pl。

2.   DDoS攻击进阶

2.1. 混合攻击

以上介绍了几种基础的攻击手段,其中任意一种都可以用来攻击网络,甚至击垮阿里、百度、腾讯这种巨型网站。但这些并不是全部,不同层次的攻击者能够发起完全不同的DDoS攻击,运用之妙,存乎一心。

高级攻击者从来不会使用单一的手段进行攻击,而是根据目标环境灵活组合。普通的SYN Flood容易被流量清洗设备通过反向探测、SYN Cookie等技术手段过滤掉,但如果在SYN Flood中混入SYN+ACK数据包,使每一个伪造的SYN数据包都有一个与之对应的伪造的客户端确认报文,这里的对应是指源IP地址、源端口、目的IP、目的端口、TCP窗口大小、TTL等都符合同一个主机同一个TCP Flow的特征,流量清洗设备的反向探测和SYN Cookie性能压力将会显著增大。其实SYN数据报文配合其他各种标志位,都有特殊的攻击效果,这里不一一介绍。对DNS Query Flood而言,也有独特的技巧。

首先,DNS可以分为普通DNS和授权域DNS,攻击普通DNS,IP地址需要随机伪造,并且指明服务器要求做递归解析;但攻击授权域DNS,伪造的源IP地址则不应该是纯随机的,而应该是事先收集的全球各地ISP的DNS地址,这样才能达到最大攻击效果,使流量清洗设备处于添加IP黑名单还是不添加IP黑名单的尴尬处境。添加会导致大量误杀,不添加黑名单则每个报文都需要反向探测从而加大性能压力。

另一方面,前面提到,为了加大清洗设备的压力不命中缓存而需要随机化请求的域名,但需要注意的是,待解析域名必须在伪造中带有一定的规律性,比如说只伪造域名的某一部分而固化一部分,用来突破清洗设备设置的白名单。道理很简单,腾讯的服务器可以只解析腾讯的域名,完全随机的域名可能会直接被丢弃,需要固化。但如果完全固定,也很容易直接被丢弃,因此又需要伪造一部分。

其次,对DNS的攻击不应该只着重于UDP端口,根据DNS协议,TCP端口也是标准服务。在攻击时,可以UDP和TCP攻击同时进行。

HTTP Flood的着重点,在于突破前端的cache,通过HTTP头中的字段设置直接到达Web Server本身。另外,HTTP Flood对目标的选取也非常关键,一般的攻击者会选择搜索之类需要做大量数据查询的页面作为攻击目标,这是非常正确的,可以消耗服务器尽可能多的资源。但这种攻击容易被清洗设备通过人机识别的方式识别出来,那么如何解决这个问题?很简单,尽量选择正常用户也通过APP访问的页面,一般来说就是各种Web API。正常用户和恶意流量都是来源于APP,人机差别很小,基本融为一体难以区分。

之类的慢速攻击,是通过巧妙的手段占住连接不释放达到攻击的目的,但这也是双刃剑,每一个TCP连接既存在于服务端也存在于自身,自身也需要消耗资源维持TCP状态,因此连接不能保持太多。如果可以解决这一点,攻击性会得到极大增强,也就是说Slowloris可以通过stateless的方式发动攻击,在客户端通过嗅探捕获TCP的序列号和确认维护TCP连接,系统内核无需关注TCP的各种状态变迁,一台笔记本即可产生多达65535个TCP连接。

前面描述的,都是技术层面的攻击增强。在人的方面,还可以有一些别的手段。如果SYN Flood发出大量数据包正面强攻,再辅之以Slowloris慢速连接,多少人能够发现其中的秘密?即使服务器宕机了也许还只发现了SYN攻击想去加强TCP层清洗而忽视了应用层的行为。种种攻击都可以互相配合,达到最大的效果。攻击时间的选择,也是一大关键,比如说选择维护人员吃午饭时、维护人员下班堵在路上或者在地铁里无线上网卡都没有信号时、目标企业在举行大规模活动流量飙升时等。

这里描述的只是纯粹的攻击行为,因此不提供代码,也不做深入介绍。

2.2. 来自P2P网络的攻击

前面的攻击方式,多多少少都需要一些傀儡机,即使是HTTP Flood也需要搜索大量的匿名代理。如果有一种攻击,只需要发出一些指令,就有机器自动上来执行,才是完美的方案。这种攻击已经出现了,那就是来自P2P网络的攻击。

大家都知道,互联网上的P2P用户和流量都是一个极为庞大的数字。如果他们都去一个指定的地方下载数据,使成千上万的真实IP地址连接过来,没有哪个设备能够支撑住。拿BT下载来说,伪造一些热门视频的种子,发布到搜索引擎,就足以骗到许多用户和流量了,但这只是基础攻击。

高级P2P攻击,是直接欺骗资源管理服务器。如迅雷客户端会把自己发现的资源上传到资源管理服务器,然后推送给其他需要下载相同资源的用户,这样,一个链接就发布出去。通过协议逆向,攻击者伪造出大批量的热门资源信息通过资源管理中心分发出去,瞬间就可以传遍整个P2P网络。更为恐怖的是,这种攻击是无法停止的,即使是攻击者自身也无法停止,攻击一直持续到P2P官方发现问题更新服务器且下载用户重启下载软件时为止。

3.   总结

限于篇幅,DDoS攻击的介绍就写这么多,而且我也不愿意对这个做更进一步的阐述了——理解防御这么多已经够用了。

总的来说,DDoS攻击可以很灵巧,可以很优美。运用之妙,存乎一心。

原文来自阿里云产品博客

http://blog.aliyun.com/243

1. 防御基础

1.1. 攻击流量到底多大
谈到DDoS防御,首先就是要知道到底遭受了多大的攻击。这个问题看似简单,实际上却有很多不为人知的细节在里面。

以SYN Flood为例,为了提高发送效率在服务端产生更多的SYN等待队列,攻击程序在填充包头时,IP首部和TCP首部都不填充可选的字段,因此IP首部长度恰好是20字节,TCP首部也是20字节,共40字节。

对于以太网来说,最小的包长度数据段必须达到46字节,而攻击报文只有40字节,因此,网卡在发送时,会做一些处理,在TCP首部的末尾,填充6个0来满足最小包的长度要求。这个时候,整个数据包的长度为14字节的以太网头,20字节的IP头,20字节的TCP头,再加上因为最小包长度要求而填充的6个字节的0,一共是60字节。

但这还没有结束。以太网在传输数据时,还有CRC检验的要求。网卡会在发送数据之前对数据包进行CRC检验,将4字节的CRC值附加到包头的最后面。这个时候,数据包长度已不再是40字节,而是变成64字节了,这就是常说的SYN小包攻击,数据包结构如下:

|14字节以太网头部|20字节IP头部|20字节TCP|6字节填充|4字节检验||目的MAC|源MAC|协议类型| IP头 |TCP头|以太网填充 | CRC检验 |

到64字节时,SYN数据包已经填充完成,准备开始传输了。攻击数据包很小,远远不够最大传输单元(MTU)的1500字节,因此不会被分片。那么这些数据包就像生产流水线上的罐头一样,一个包连着一个包紧密地挤在一起传输吗?事实上不是这样的。

以太网在传输时,还有前导码(preamble)和帧间距(inter-frame gap)。其中前导码占8字节(byte),即64比特位。前导码前面的7字节都是10101010,1和0间隔而成。但第八个字节就变成了10101011,当主机监测到连续的两个1时,就知道后面开始是数据了。在网络传输时,数据的结构如下:

|8字节前导码|6字节目的MAC地址|6字节源MAC地址|2字节上层协议类型|20字节IP头|20字节TCP头|6字节以太网填充|4字节CRC检验|12字节帧间距|

有了上面的基础,现在可以开始计算攻击流量和网络设备的线速问题了。当只填充IP头和TCP头的最小SYN包跑在以太网络上时,100Mbit的网络,能支持的最大PPS(Packet Per Second)是100×106 / (8 * (64+8+12)) = 148809,1000Mbit的网络,能支持的最大PPS是1488090。

1.2. SYN Flood防御
前文描述过,SYN Flood攻击大量消耗服务器的CPU、内存资源,并占满SYN等待队列。相应的,我们修改内核参数即可有效缓解。主要参数如下:

net.ipv4.tcp_syncookies = 1net.ipv4.tcp_max_syn_backlog = 8192 

net.ipv4.tcp_synack_retries = 2

分别为启用SYN Cookie、设置SYN最大队列长度以及设置SYN+ACK最大重试次数。

SYN Cookie的作用是缓解服务器资源压力。启用之前,服务器在接到SYN数据包后,立即分配存储空间,并随机化一个数字作为SYN号发送SYN+ACK数据包。然后保存连接的状态信息等待客户端确认。启用SYN Cookie之后,服务器不再分配存储空间,而且通过基于时间种子的随机数算法设置一个SYN号,替代完全随机的SYN号。发送完SYN+ACK确认报文之后,清空资源不保存任何状态信息。直到服务器接到客户端的最终ACK包,通过Cookie检验算法鉴定是否与发出去的SYN+ACK报文序列号匹配,匹配则通过完成握手,失败则丢弃。当然,前文的高级攻击中有SYN混合ACK的攻击方法,则是对此种防御方法的反击,其中优劣由双方的硬件配置决定

tcp_max_syn_backlog则是使用服务器的内存资源,换取更大的等待队列长度,让攻击数据包不至于占满所有连接而导致正常用户无法完成握手。net.ipv4.tcp_synack_retries是降低服务器SYN+ACK报文重试次数,尽快释放等待资源。这三种措施与攻击的三种危害一一对应,完完全全地对症下药。但这些措施也是双刃剑,可能消耗服务器更多的内存资源,甚至影响正常用户建立TCP连接,需要评估服务器硬件资源和攻击大小谨慎设置。

除了定制TCP/IP协议栈之外,还有一种常见做法是TCP首包丢弃方案,利用TCP协议的重传机制识别正常用户和攻击报文。当防御设备接到一个IP地址的SYN报文后,简单比对该IP是否存在于白名单中,存在则转发到后端。如不存在于白名单中,检查是否是该IP在一定时间段内的首次SYN报文,不是则检查是否重传报文,是重传则转发并加入白名单,不是则丢弃并加入黑名单。是首次SYN报文则丢弃并等待一段时间以试图接受该IP的SYN重传报文,等待超时则判定为攻击报文加入黑名单。

首包丢弃方案对用户体验会略有影响,因为丢弃首包重传会增大业务的响应时间,有鉴于此发展出了一种更优的TCP Proxy方案。所有的SYN数据报文由清洗设备接受,按照SYN Cookie方案处理。和设备成功建立了TCP三次握手的IP地址被判定为合法用户加入白名单,由设备伪装真实客户端IP地址再与真实服务器完成三次握手,随后转发数据。而指定时间内没有和设备完成三次握手的IP地址,被判定为恶意IP地址屏蔽一定时间。除了SYN Cookie结合TCP Proxy外,清洗设备还具备多种畸形TCP标志位数据包探测的能力,通过对SYN报文返回非预期应答测试客户端反应的方式来鉴别正常访问和恶意行为。

清洗设备的硬件具有特殊的网络处理器芯片和特别优化的操作系统、TCP/IP协议栈,可以处理非常巨大的流量和SYN队列。

1.3. HTTP Flood防御
HTTP Flood攻击防御主要通过缓存的方式进行,尽量由设备的缓存直接返回结果来保护后端业务。大型的互联网企业,会有庞大的CDN节点缓存内容。

当高级攻击者穿透缓存时,清洗设备会截获HTTP请求做特殊处理。最简单的方法就是对源IP的HTTP请求频率做统计,高于一定频率的IP地址加入黑名单。这种方法过于简单,容易带来误杀,并且无法屏蔽来自代理服务器的攻击,因此逐渐废止,取而代之的是JavaScript跳转人机识别方案。

HTTP Flood是由程序模拟HTTP请求,一般来说不会解析服务端返回数据,更不会解析JS之类代码。因此当清洗设备截获到HTTP请求时,返回一段特殊JavaScript代码,正常用户的浏览器会处理并正常跳转不影响使用,而攻击程序会攻击到空处。

1.4. DNS Flood防御
DNS攻击防御也有类似HTTP的防御手段,第一方案是缓存。其次是重发,可以是直接丢弃DNS报文导致UDP层面的请求重发,可以是返回特殊响应强制要求客户端使用TCP协议重发DNS查询请求。

特殊的,对于授权域DNS的保护,设备会在业务正常时期提取收到的DNS域名列表和ISP DNS IP列表备用,在攻击时,非此列表的请求一律丢弃,大幅降低性能压力。对于域名,实行同样的域名白名单机制,非白名单中的域名解析请求,做丢弃处理。

1.5. 慢速连接攻击防御
Slowloris攻击防御比较简单,主要方案有两个。

第一个是统计每个TCP连接的时长并计算单位时间内通过的报文数量即可做精确识别。一个TCP连接中,HTTP报文太少和报文太多都是不正常的,过少可能是慢速连接攻击,过多可能是使用HTTP 1.1协议进行的HTTP Flood攻击,在一个TCP连接中发送多个HTTP请求。

第二个是限制HTTP头部传输的最大许可时间。超过指定时间HTTP Header还没有传输完成,直接判定源IP地址为慢速连接攻击,中断连接并加入黑名单。

2. 企业级防御
互联网企业防御DDoS攻击,主要还是使用上文的基础防御手段, 重点在于使用监控、组织以及流程等东西来保障及时、正确的使用这些手段,并根据攻击策略的改变而改变。

2.1. 异常监控
监控需要具备多层监控、纵深防御的概念,从骨干网络、IDC入口网络的BPS、PPS、协议分布,负载均衡层的VIP新建连接数、并发连接数、BPS、PPS到主机层的CPU状态、TCP新建连接数状态、TCP并发连接数状态,到业务层的业务处理量、业务连通性等多个点部署监控系统。即使一个监控点失效,其他监控点也能够及时给出报警信息。多个点的信息结合起来,有助于准确的判断攻击目标和攻击手法。

2.2. 流程以及预案、演习
一旦发现异常,立即启动在虚拟防御组织中的应急流程。防御组织需要囊括到足够全面的人员,至少包含监控部门、运维部门、网络部门、安全部门、客服部门、业务部门等,所有人员都需要2-3个备份。流程启动后,除了人工处理,还应该包含一定的自动处理、半自动处理能力。例如自动化的攻击分析,确定攻击类型,自动化、半自动化的防御策略,在安全人员到位之前,最先发现攻击的部门可以做一些缓解措施。

除了DDoS到来之时的流程等工作之外,更多的工作是在攻击到来之前。主要包含CDN节点部署、DNS设置、流程演习等。对于企业来说,具备多个CDN节点是DDoS防御容量的关键指标。当一个机房承担不住海量数据时,可以通过DNS轮询的方式,把流量引导到多个分布节点,使用防御设备分头处理。因此DNS的TTL值需要设置得足够小,能够快速切换,每个CDN节点的各种VIP设置也需要准备充分。

3. 总结
在虚拟化时代,海量用户的不同业务共处在相同的物理机平台,遭受DDoS攻击的可能性越来越高。而且一个用户被攻击可能牵扯到大量的其他用户,危害被显著放大,因此防御显得尤为重要。阿里云的虚拟化云计算业务,平均每天遭受约200起DDoS攻击,最大流量达到接近80Gbit/s,所有这些攻击都在1分钟内自动处理完成,让客户远离DDoS的威胁,专心发展业务。

总地来说,对DDoS防御,主要的工作是幕后积累。台上十分钟,台下十年功,没有充分的资源准备,没有足够的应急演练,没有丰富的处理经验,DDoS攻击将是所有人的噩梦。

原文来自阿里云产品博客

http://blog.aliyun.com/232

php连接mysql的驱动支持

请使用mysqlnd php官方原生驱动。编译参数中需要加如下

./configure --with-mysql=mysqlnd \
--with-mysqli=mysqlnd \
--with-pdo-mysql=mysqlnd \

三种(mysql,mysqli,pdo-mysql)php连接 mysql api接口都采用mysqlnd驱动。

php连接mysql的三种api(mysql,mysqli,pdo-mysql)

php官方推荐使用 mysqli 和 PDO_MySQL

php连接mysql的三种api示例

<?php
// mysqli
$mysqli = new mysqli("example.com", "user", "password", "database");
$result = $mysqli->query("SELECT 'Hello, dear MySQL user!' AS _message FROM DUAL");
$row = $result->fetch_assoc();
echo htmlentities($row['_message']);

// PDO
$pdo = new PDO('mysql:host=example.com;dbname=database', 'user', 'password');
$statement = $pdo->query("SELECT 'Hello, dear MySQL user!' AS _message FROM DUAL");
$row = $statement->fetch(PDO::FETCH_ASSOC);
echo htmlentities($row['_message']);

// mysql
$c = mysql_connect("example.com", "user", "password");
mysql_select_db("database");
$result = mysql_query("SELECT 'Hello, dear MySQL user!' AS _message FROM DUAL");
$row = mysql_fetch_assoc($result);
echo htmlentities($row['_message']);
?>

 

PHP中三种主要的MySQL连接方式的功能:

PHP的mysqli扩展 PDO (使用PDO MySQL驱动和MySQL Native驱动) PHP的mysql扩展
引入的PHP版本 5.0 5.0 3.0之前
PHP5.x是否包含
MySQL开发状态 活跃 在PHP5.3中活跃 仅维护
在MySQL新项目中的建议使用程度 建议 – 首选 建议 不建议
API的字符集支持
服务端prepare语句的支持情况
客户端prepare语句的支持情况
存储过程支持情况
多语句执行支持情况 大多数
是否支持所有MySQL4.1以上功能 大多数

 

ext/mysqli PDO_MySQL ext/mysql
PHP version introduced 5.0 5.1 2.0
Included with PHP 5.x Yes Yes Yes
Development status Active Active Maintenance only
Lifecycle Active Active Deprecated
Recommended for new projects Yes Yes No
OOP Interface Yes Yes No
Procedural Interface Yes No Yes
API supports non-blocking, asynchronous queries with mysqlnd Yes No No
Persistent Connections Yes Yes Yes
API supports Charsets Yes Yes Yes
API supports server-side Prepared Statements Yes Yes No
API supports client-side Prepared Statements No Yes No
API supports Stored Procedures Yes Yes No
API supports Multiple Statements Yes Most No
API supports Transactions Yes Yes No
Transactions can be controlled with SQL Yes Yes Yes
Supports all MySQL 5.1+ functionality Yes Most No

 

 

扩展阅读:

php连接mysql的驱动支持 http://www.php.net/manual/zh/mysqlnd.install.php

php连接mysql api  mysqil http://php.net/manual/zh/mysqli.overview.php

php连接mysql 三种api的选择 https://php.net/manual/zh/mysqlinfo.api.choosing.php

 

 


php后门木马常用的函数大致上可分为四种类型:

1. 执行系统命令: system, passthru, shell_exec, exec, popen, proc_open
2. 代码执行与加密: eval, assert, call_user_func,base64_decode, gzinflate, gzuncompress, gzdecode, str_rot13
3. 文件包含与生成: require, require_once, include, include_once, file_get_contents, file_put_contents, fputs, fwrite
4. .htaccess: SetHandler, auto_prepend_file, auto_append_file

 


想找一个 关键词是“hellow word” 在哪些文件中有,我们用grep命令
grep –color -i -r -n “hellow word”  /data/www/

这样就能搜索出来 文件中包含关键词的文件

–color是关键词标红

-i是不区分大小写
-r是包含子目录的搜索
-d skip忽略子目录

可以用以上命令查找网站项目里的带有挂马的文件

 


.两个查后门的实用linux命令:
find /data/web/website/ -iname *.php -mtime -35 找出/data/web/website/目录下 35分钟前新建的php
find /data/web/website/ -name “*.php” | xargs grep “eval($_POST[” 找出/data/web/website/ 里面源码包含eval($_POST[的php文件

 

例如
注入漏洞eval(base64_decode
grep –color -i -r -n “eval”  /data/www/   找出来对比以前正常的代码,看是否正常。然后用stat查看这个木马文件的修改时间,最后去寻找WEB日志,找出木马从哪里进来的

 

五:

实用查找PHP木马命令:

查找PHP木马

# find ./ -name "*.php" |xargs egrep "phpspy|c99sh|milw0rm|eval\(gunerpress|eval\(base64_decoolcode|spider_bc"> /tmp/php.txt
# grep -r --include=*.php  '[^a-z]eval($_POST' . > /tmp/eval.txt
# grep -r --include=*.php  'file_put_contents(.*$_POST\[.*\]);' . > /tmp/file_put_contents.txt
# find ./ -name "*.php" -type f -print0 | xargs -0 egrep "(phpspy|c99sh|milw0rm|eval\(gzuncompress\(base64_decoolcode|eval\(base64_decoolcode|spider_bc|gzinflate)" | awk -F: '{print $1}' | sort | uniq

 

查找最近一天被修改的PHP文件

#   find -mtime -1 -type f -name \*.php

修改网站的权限

# find -type f -name \*.php -exec chmod 444 {} \;
# find ./ -type d -exec chmod 555{} \;

假设最后更新是10天前我们可以查找10天内生成的可以php文件:

find /var/www/ -name “*.php” -mtime -10

也可以通过关键字的形式查找 常见的木马常用代码函数 eval,shell_exec,passthru,popen,system

#find /var/www/ -name “*.php” |xargs grep “eval” |more
#find /var/www/ -name “*.php” |xargs grep “shell_exec” |more
#find /var/www/ -name “*.php” |xargs grep “passthru” |more

还有查看access.log 当然前提是你网站的所有php文件不是很多的情况下

一句话查找PHP木马

# find ./ -name “*.php” |xargs egrep “phpspy|c99sh|milw0rm|eval(gunerpress|eval(base64_decode|spider_bc”> /tmp/php.txt
# grep -r –include=*.php ’[^a-z]eval($_POST’ . > /tmp/eval.txt
# grep -r –include=*.php ’file_put_contents(.*$_POST[.*]);’ . > /tmp/file_put_contents.txt
# find ./ -name “*.php” -type f -print0 | xargs -0 egrep “(phpspy|c99sh|milw0rm|eval(gzuncompress(base64_decode|eval(base64_decode|spider_bc|gzinflate)” | awk -F: ‘{print $1}’ | sort | uniq

查找最近一天被修改的PHP文件
# find -mtime -1 -type f -name *.php

 

以下其实是多余的操作了其实,但是还是有值得看的地方

检查代码。

肯定不是一个文件一个文件的检查,Linxu有强悍的命令

grep ‘eval’ * -R 全盘搜索当前目录所有文件(包含子目录)中带有eval的文件,这条可以快速查找到被挂马的文件。

关于eval,请自行google一句话php代码。

2,查看日志。

不到这个时候不知道日志的可贵啊。

还是以grep命令为主。

思路:负责的站点是Linux,只开了2个端口,一个22和80,外部的执行命令是由从80端口进来,Selinux报httpd访问/boot文件,确认被挂马。而所有的命令执行必须POST提交给执行的文件。所以,查找日志中所有的POST记录。

cat access_log_20120823.log | grep ‘POST’ | grep -v ‘反向查找’ | less,通过grep -v排除正常post,egrep也支持正则,但是太复杂了,看懂不知道怎么运用。

(这里不建议用cat,用tail可以追加一个文件来看)

这可以防患于未然,防止不知道哪天又被人黑进来了。每天看一眼日志。

3,对于网页目录,只给apache用户rx权限,不要给w权限,目录设置要加上rx,不要给w,个别文件除外。所以,配合2使用,Linux下可以快速过滤刷选出来不规则的POST请求。

综合1,2其实就可以快速查找被黑的页面,被修改的文件替换干净的代码。

 

文章来源: http://blog.csdn.net/miltonzhong/article/details/9717179

0x00 背景


最近世界真是越来越不太平了,尤其是对于大部分普通人而言。昨天又传来噩耗,根据网络监测公司BGPMon,Google的公开DNS服务器 IP 8.8.8.8被劫持到了委内瑞拉和巴西超过22分钟。

Google DNS 服务器平均每天处理超过1500亿个查询,在被劫持的22分钟里起码几百万个查询包括金融系统,政府和个大商业网站的DNS查询流量都被劫持走了。

 

g1

根据砖家们的推测,这次劫持可能是黑客利用了Border Gateway Protocol(BGP) 协议中一个众所周知的漏洞来实现的,BGP协议为ISP级的路由协议,一般用来协调大型ISP之间的路由走向。这次劫持可以让黑客把网上的部分流量劫持从而经过他们所控制的路由。

g2

这已经不是Google DNS服务器被第一次劫持了,在2010年也Google DNS的流量也曾经被劫持到了罗马尼亚和奥地利境内。

BGP劫持攻击是一种大规模的中间人攻击,并且较难发现,因为数据包的最终目的地并没有变,只是绕了下路而已。

0x01 BGP劫持详解


本部分来源于Tony Kapela 和 Alex Pilosov在2008年 Defcon会议上的演讲。

什么是BGP

首先互联网整体上来说是一个分布式的网络,并没有整个网络的中心。但是整个互联网实际上是由成百上千个不同的ISP的子网络组成的。

这些子网络互相连接,通过BGP协议告诉对方自己子网络里都包括哪些IP地址段,自己的AS编号(AS Number)以及一些其他的信息。

这里又要扯到互联网的IP地址分配方式。互联网的IP地址分配是中心化的,ICANN这个机构把IP地址大段分给Regional Internet Registries(RIR),区域互联网注册管理机构。RIR再把IP地址段细分后分给ISP们。

大部分情况下,AS Number和分给该AS什么IP段是没有任何关系的。

下面问题来了,BGP协议里虽然有一些简单的安全认证的部分,但是对于两个已经成功建立BGP连接的AS来说,基本会无条件的相信对方AS所传来的信息,包括对方声称所拥有的IP地址范围。

对于ISP分配给大公司客户的地址段,ISP往往会对BGP做一些有限的过滤。但是对于大型ISP来说,因为对方所拥有的IP地址段可能过于分散,所以一般是按最大范围设置BGP prefix 地址过滤。比如假设ISP A拥有地址段20.1.0.0/16和20.200.0.0/16,那么ISP B可能会设置过滤对方传来的20.0.0.0/8以外的路由。

当然这种情况比较极端,一般ISP分配到的IP地址段都是连续的,但是基本也都有可操作的空间,可以把数百到几万个不属于自己的IP合法加到自己的BGP信息里。

多数ISP甚至都没有把自己本身的IP段过滤掉,也就是说如果其他AS声称拥有该ISP自己的IP段,这个ISP的BGP路由也会相信。

为了解决这个问题,有人发明了一个叫Internet Routing Registry (IRR)的东西,相当于一个开放式的数据库,像DNS 根服务器一样采用分布式镜像服务器放在世界各地。

ISP可以向IRR注册自己的IP地址段和路由策略,其他ISP就可以查询IRR从而对自己的BGP路由器做过滤。这样做的确防止了一些由于无意而导致的路由劫持。

但是IRR这个东西本身也是不靠谱的。IRR里存了大约10万条记录,如果全部加载进路由器的话是个不小的负担。另外IRR基本没人管,任何人可以可以往里面注册任何路由记录。

所以在大部分ISP都无条件相信IRR的时代,IRR也带来了不少的麻烦。

最简单的方式就是通过Whois找到目标IP段的 管理员邮箱,如果该邮箱或者邮箱所在的域名已经过期,那么就自己注册一个,然后就可以随便通过邮件向IRR修改记录了。

或者直接通过BGP路由向ISP发送,反正大家都不care……

实际案例

现在我们来看一个Youtube被劫持的案例:

youtube有5个网段,其中一个是

208.65.152.0/22  

因为觉得Youtube不和谐,于是巴基斯坦政府决定封锁Youtube。

巴基斯坦电信在路由器上加了条static route把

208.65.153.0/24

弄到了null0接口(GFW之黑洞路由大法)

巴电信的工程师手抖把static route redistribute到BGP了(Cisco路由器上同步不同协议路由表的方法),也就是说把该路由器上的静态路由表添加到BGP的路由表了,静态路由同步到其他路由表里的优先值最高。

BGP把这条路由向其他AS的路由器同步了,最先中枪的是香港的电讯盈科(PCCW),然后接着被逐渐同步到了全世界。

这时互联网的大部分用户想上Youtube的时候数据包都跑到巴基斯坦了,结果当然是打不开了(因为进来就被弄到null0了)。

Youtube发现后重新用BGP声明了对该IP段和其他IP段的所有权,成功刷新了部分ISP路由器的路由表。

两小时后PCCW断开了和巴基斯坦电信路由器的BGP连接。3-5分钟后,一切恢复正常,除了苦逼的巴基斯坦用户们。

这意味着只要控制了任何一个ISP的任何一个BGP路由,都将具备影响全世界互联网的能力。

BGP劫持很难被发现,如果不是因为巴基斯坦电信把youtube的IP段转发到了null0接口,数据包就只会在巴基斯坦网络里绕一圈然后再到达Youtube。

如果攻击者的路由器具备篡改TTL的功能,那么即使通过traceroute也很难发现数据包被劫持,唯一的方法就是像前面所说的BGPmon那样检测全世界范围内的AS路由表和BGP信息。

BGP劫持理论

当我们控制了ISP的BGP路由后,像平常一样发送路由信息。通过修改AS Path等BGP信息,让其他AS认为你到目标网络的距离最短。

为了让回来的数据包也经过你的路由器,你需要记录trace route到目标网络的时候都会经过哪些AS。

使用AS-PATH prepend list包括这些AS Number

设置static route到traceroute出现的第一个ASN

详解:

目标IP段

10.10.220.0/22

在AS 200中
ASN 200向相邻的AS 20和30发送BGP通告。
此时为正常的状态。

2014031815415353677

攻击者控制了AS 100的BGP路由。

AS 100的路由表和BGP表显示到达

10.10.200.0/22

需要经过 AS 10.

于是我们把AS10,20和200加入我们的AS PATH prepend list

2014031815423285580

通过route-map把目标IP段加入BGP路由表

10.10.220.0/24 is announced with a route-map:  
route-map hijacked permit 10  
match ip address prefix-list jacked  
set as-path prepend 10 20 200  

然后在AS100的路由器中加入static route,把流向目标IP段的数据包指向AS10

ip route 10.10.220.0 255.255.255.0 4.3.2.1 

2014031815431276804

完成后可以看出,AS30 40 50 60的数据包如果想要到AS 200去,都会先经过AS 100.

到了这里我们已经可以分析出,BGP劫持的本质再次回到安全的本质既是信任这一点,因为BGP直接无条件信任对方AS发来的路由信息,并且缺乏有效的认证和过滤手段,导致BGP劫持屡次得手。

 

来源: http://drops.wooyun.org/papers/1207

扩展阅读

Pakistan hijacks YouTube

BGP AS-Path Prepending

汉化:http://doslin.com/learn-regular-expressions-in-about-55-minutes/

原文:http://qntm.org/files/re/re.html

 

翻译水平有限,如有谬误,欢迎评论斧正或者Pull Request

正则表达式(“regexes”)即增强查找/字符串替换操作。当在文本编辑器中编辑文字时,正则表达式经常用于:

  • 检查文本是否包含一个给定的模式
  • 查找任何匹配的模式
  • 从文本中拉取信息(比如截断)
  • 修改文本

和文本编辑器一样,绝大多数高级编程语言支持正则表达式。在本文中,“文本”仅仅是一个字符串变量,但是有效的操作却是一致的。某些编程语言(Perl,JavaScript)甚至为正则表达式提供专用的语法。

但是正则表达式是什么?

一个正则表达式仅仅为一个字符串。它没有长度限制,但是通常该字符串很短。下面看几个例子:

  • I had a \S+ day today
  • [A-Za-z0-9\-_]{3,16}
  • \d\d\d\d-\d\d-\d\d
  • v(\d+)(\.\d+)*
  • TotalMessages="(.*?)"
  • <[^<>]>

这个字符串实际上是一个极小的计算程序,并且正则表达式是一门语法小而简洁,领域特定的编程语言。牢记以下几点,它们不该在学习过程中让你感到惊讶:

  • 每个正则表达式都能分解成一串指令。“找到这个,再找到那个,然后找到其中一个…”
  • 一个正则表达式拥有输入(文本)和输出(模式匹配,和有些时候的自定义文本)。
  • 存在语法错误——不是每个字符串都是合法的正则表达式!
  • 语法有些怪异,也可以说是恐怖。
  • 一个正则表达式有时候可以被编译以便更快运行。

正则实现一直有着显著的改变。对于本文,我所关注的是那些几乎每个正则表达式都实现了的核心语法。

练习

获取一个支持正则的文本编辑器。我推荐Notepad++

下载一篇很长的散文故事比如Gutenberg出版社出版的H. G. Wells的《时光机器》然后打开它。

下载一部字典,比如这个,解压然后打开。

一切准备就绪,稍后开始练习。

提示: 正则表达式与文件通配符语法完全不兼容,比如*.xml

正则表达式基础语法

字面值(Literals)

正则表达式由只代表自身的字面值和代表特定含义的元字符组成。

这里也有一些例子。我会对元字符进行高亮。

  • I had a \S+ day today
  • [A-Za-z0-9-_]{3,16}
  • \d\d\d\d-\d\d-\d\d
  • v(\d+)(.\d+)*
  • TotalMessages="(.*?)"
  • <[^<>]*>

大部分字符,包括字母数字字符,会以字面值的形式出现。这意味着它们查找的是自身。比如,正则表达式cat代表“先找到c,接着找到a,最后找到t”。

目前为止感觉良好。这的确很像

  • 一个普通的查找对话框
  • Java中的String.indexOf()函数
  • PHP中的strpos()函数
  • 等等

提示:除非特别说明,正则表达式是大小写敏感的。然而,绝大多数实现都会提供一个标记来开启不区分大小写的功能。

句点(dot)

我们第一个元字符是句号(译者注:句点,英文句号),.。一个.表示匹配任何单个字符。下面这个正则表达式c.t代表“先找到c,接着找到任何单个字符,再找到t”。

在一段文本中,这个表达式将会找到catcotczt,甚至字面值为c.t的字符串(c,句点,t),但是不包括ct或者coot

任何元字符如果用一个反斜杆\进行转义就会变成字面值。所以上述的正则表达式c\.t就代表“先找到c,接着找到句号,再找到t”。

反斜杠是一个元字符,这意味着它也可以使用反斜杠转义。所以正则表达式c\\t代表“先找到c,接着找到反斜杆,再找到t”。

注意! 在一些实现中,. 会匹配任意字符除了 换行符。这意味着“换行符”在不同的实现中也会变化。 要查看你的文档。在这篇文章中, 我会确保. 会匹配任意字符。

在其它情况下, 通常会有一个标记来调整这种行为,那就是`DOTALL`或类似的标记

练习

使用你目前所学,在字典中使用正则表达式,匹配一个有两个z的单词,其中这两个z离得越远越好。

你最终的正则表达式应该是z.......z会匹配到四个单词: razzamatazzrazzamatazzeszwischenzug以及zwischenzugs

练习

《时光机器》这本书中,使用正则表达式来查找以介词收尾的句子。

你的正则表达式应该类似这样up\.

字符类(Character classes)

字符类是字符在方括号中的集合。表示“找到其中任意的字符”。

  • 正则表达式c[aeiou]t表示“找到c后跟一个元音字母,再找到t”。在一段文本中,将会匹配到catcetcitcotcut
  • 正则表达式[0123456789]表示找到一个数字
  • 正则表达式[a]a意义相同:“找到a

一些转义的例子:

  • \[a\]表示“找到一个左方括号紧跟着一个a,再跟着一个右方括号”。
  • [\[\]ab]表示“匹配一个左方括号或者右方括号或者a或者 b”。
  • [\\\[\]]表示“匹配一个反斜杆或者一个左方括号或者一个右方括号”。(呕!)

在字符类中顺序和重复字符并不重要。[dabaaabcc][abcd]一样。

重要的提示

在字符类内部的“规则”和在字符类内部的规则有所不同。一些字符在字符类内部扮演着元字符的角色,但在字符类外部则充当字面值。还有一些字符做着相反的事。一些字符在两种情形都为元字符,但在各自情形里代表不同的含义。

特别地, .表示“匹配任意字符”,但是[.]表示“匹配句点”。不能并为一谈。

练习

结合目前所学,在字典中,使用正则表达式查找有连续的元音和连续的辅音的单词。

[aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]匹配到六元音单词euouae and euouaes,而可怕的[bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]找到了有十个辅音的华丽的sulphhydryls。我们将会很快看到如何简化这些恐怖的表达式。

字符类区间(ranges)

你可以在字符类中使用连字符来表示一个字母或数字的区间

  • [b-f][bcdef]都表示“找到一个bcd或 ef”。
  • [A-Z][ABCDEFGHIJKLMNOPQRSTUVWXYZ]都表示“匹配大写字母”。
  • [1-9][123456789]都表示“匹配一个非零数字”。

连字符在字符类外部使用时并没有特别都含义。正则表达式a-z表示“找到一个a接着跟着一个连字符,然后匹配一个z”。

区间和单独都字符可能会共存于同一个字符类:

  • [0-9.,]表示“匹配一个数字或者一个句点或者一个逗号”。
  • [0-9a-fA-F]表示“匹配一个十六进制数”。
  • [a-zA-Z0-9\-]表示“匹配一个字母数字字符或连字符”。

虽然你可以尝试在区间内以非字母数字字符结束(比如abc[!-/]def),但这在其它实现中的语法不一定对。即使语法正确,但在这个区间内很难看出包含了哪个字符。请谨慎使用(我的意思是不要这么干)。

同样的,区间端点的范围应该一致。即使像[A-z]这种表达式在你选择的实现中合法,但它做的可能会与你想法用出入。(补充:可以有Za的区间范围)。

注意。 区间是字符的区间,不是数字的区间。正则表达式[1-31]表示“找到一个1或一个 2或一个3”,不是“找到一个从131的整数”。

练习

使用目前学习,编写一个查找以YYYY-MM-DD为格式的日期的正则表达式。

目前我们能写出来的是[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]。同样地,我们将能够很快简化这个式子。

字符类的否定(negation)

你可以通过在最开始的位置使用插入符号(译者注:^)来否定一个字符类。

  • [^a]表示“匹配除了a的任意字符”。
  • [^a-zA-Z0-9]表示“找到一个非字母数字字符”。
  • [\^abc]表示“找到一个插入符或者a或者b或者c”。
  • [^\^]表示“找到除了插入符外的任意字符”。(呕!)

练习

在字典中,使用正则表达式去找到这个规则的反例“i位于e 前面并且不出现在c的后面”。

字符类补充

正则表达式\d含义与[0-9]一致:“匹配一个数字”。(为了匹配一个反斜杆后跟一个d,可以使用\\d。)

\w的含义与[0-9A-Za-z_]一致:“匹配一个单词字符(译者注:字母或数字或下划线或汉字)”。

\s表示“匹配任意空白字符(空格,tab,回车或者换行)”。

此外,

  • \D[^0-9]:“匹配任意非数字的字符”。
  • \W[^0-9A-Za-z_]:“匹配任意非单词字符(译者注:匹配任意不是字母,数字,下划线,汉字的字符)”。
  • \S表示“匹配任意不是空白符的字符”。

这些字符类都很常见,你必须学会。

你可能也注意到了,句点.本质上是一个包含任意字符的字符类

许多实现提供了很多额外的字符类或标记,它们通过扩展现有的字符类来覆盖ASCII之外范围的字符。提示:Unicode包含更多的“数字字符”而不仅仅是09,这一点同样对于“单词”和“空格”也适用。注意你的文档所写。

练习

简化正则表达式[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]

\d\d\d\d-\d\d-\d\d.

乘法器(Multipliers)

你可以在一个字面值或者字符类后跟着一个大括号来使用乘法器

  • 正则表达式a{1}a,表示“匹配一个a”。
  • a{3}表示“找到一个a后再跟一个a,最后找到一个a”。
  • a{0}表示“匹配空字符”。就其本身而言,这似乎没有用处。如果你在任何一段文本中使用该表达式,你会在你刚开始搜索的端点处立即得到一个匹配。即使你的文本为空字符串结果也为真。
  • a\{2\}代表“找到一个a,跟着一个左大括号,接着跟匹配一个2,然后跟着一个右大括号”。
  • 在字符类中大括号没有特别的含义。[{}]代表“匹配一个左大括号或者一个右大括号”。

注意。 乘法器没有记忆。该正则表达式[abc]{2}表示“匹配a或者b或者c,接着匹配a或者b或者c。这跟“匹配aaabacbabbbccacbcc”相同。这跟“匹配aabbcc”含义不同

练习

简化以下正则表达式:

  • z.......z
  • \d\d\d\d-\d\d-\d\d
  • [aeiou][aeiou][aeiou][aeiou][aeiou][aeiou]
  • [bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz][bcdfghjklmnpqrstvwxyz]

  • z.{7}z
  • \d{4}-\d{2}-\d{2}
  • [aeiou]{6}
  • [bcdfghjklmnpqrstvwxyz]{10}

乘法器区间

乘法器可能会有区间

  • x{4,4}x{4}一样。
  • colou{0,1}r表示“匹配colourcolor
  • a{3,5}表示“匹配aaaaaaaaaaaa”。

值得注意的是优先选择更长的匹配,因为乘法器是贪婪的。如果你输入的文本是I had an aaaaawful day,该正则表达式就会在aaaaawful中匹配到aaaaa。不会在第三个a后就停止匹配。

乘法器是贪婪的,但它不会忽略一个更好的匹配。如果你的输入文本为I had an aaawful daaaaay,之后这个正则表达式会在第一次的匹配中于aaawful找到aaa。只有在你说“给我找到另一个匹配”的时候,它才会继续搜索然后在daaaaay中找到aaaaa

乘法器区间可能是开区间:

  • a{1,}表示“在一列中找到一个或多个a”。然而你的乘法器将会是贪婪的。在找到第一个a后,它将会尽可能匹配到更多的a
  • .{0,}表示“匹配任何情形”。不管你的输入文本是什么——甚至为空——这个正则表达式都会匹配整个字符串然后返回给你。

练习

编写一个能匹配双引号字符串的正则表达式。同时该字符串可以拥有任意数量的字符。

用你已经学到的之时,修改上面的正则表达式,来找到了双引号字符串,但它们之间没有多余的双引号。

".{0,}",然后是"[^"]{0,}"

乘法器补充

?代表的含义与{0,1}相同。比如说,colour?r表示“匹配colourcolor”。

*等于{0,}。比如说,.*表示“匹配一切”,跟上面提到的一样。

+等于{1,}。比如说,\w+表示“匹配一个单词”。这里的“单词”是1个或多个“单词字符”的序列,就像_varAccountName1

这些乘法器都很常见,你必须掌握。还有:

  • \?\*\+表示“匹配一个问号,接着找到一个星号,然后跟着一个加号”。
  • [?*+]表示“找到一个问号或者一个星号或者一个加号”。

练习

简化下面的正则表达式:

  • ".{0,}""[^"]{0,}"
  • x?x?x?
  • y*y*
  • z+z+z+z+

  • ".*""[^"]*"
  • x{0,3}
  • y*
  • z{4,}

练习

编写一个表达式来查找非单词字符分隔的两个单词。如果改为三个单词或者六个单词又该怎么写?

\w+\W+\w+\w+\W+\w+\W+\w+\w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+。当然,我们之后会学习如何简化它们。

惰性(Non-greed)

正则表达式".*"表示“找到一个双引号,接着找到尽可能多的字符,最后再找到一个双引号”。注意一下被.*匹配的内部字符,很可能包含多个双引号。这通常不是非常有用。

乘法器可通过追加问号来实现惰性。这里对优先顺序进行了反转:

  • \d{4,5}?表示“匹配\d\d\d\d\d\d\d\d\d”。其实跟\d{4}行为一致。
  • colou??r就是colou{0,1}?r,表示“找到colorcolour”。和colou?r行为一致。
  • ".*?"表示“匹配一个双引号,跟着一个尽可能少的字符,再跟着一个双引号”。这个不像上面两个例子,实际上很有用。

分支(Alternation)

你可以使用管道符号来实现匹配多种选择:

  • cat|dog表示“匹配catdog”。
  • red|blue|red||blue以及|red|blue都是同样的意思,“匹配redblue或空字符串”。
  • a|b|c[abc]一样。
  • cat|dog|\|表示“匹配catdog管道符号”。
  • [cat|dog]表示“找到acddgot或一个管道符号”。

练习

尽你所能简化下述正则表达式:

  • s|t|u|v|w
  • aa|ab|ba|bb
  • [abc]|[^abc]
  • [^ab]|[^bc]
  • [ab][ab][ab]?[ab]?

  • [s-w]
  • [ab]{2}
  • .
  • [^b]
  • [ab]{2,4}

练习

编写一个正则表达式匹配1到31(含)之间的整数。 记住,[1-31]不是正确答案。

有几种方法都可以做到这一点。我认为其中[1-9]|[12][0-9]|3[01]可读性最好。

组合(Grouping)

你可以使用圆括号来组合表达式:

  • 在一周中找到一天,使用(Mon|Tues|Wednes|Thurs|Fri|Satur|Sun)day
  • (\w*)ility等同于\w*ility。都表示“找到以ility结尾的单词”。为什么第一种形式更有用,后面会看到…
  • \(\)表示“匹配一个左圆括号后,再匹配一个右圆括号”。
  • [()]表示“匹配一个左圆括号或一个右圆括号”。

练习

《时光机器》这本书中,使用正则表达式来查找包裹在括号中的句子。接着,修改你的答案来查找没有被括号包裹的句子。

\(.*\),然后是\([^()]*\)

组合可能会包含空字符串:

  • (red|blue|)表示“匹配redblue空字符串”。
  • abc()def等同于abcdef

可能你会在组合中使用乘法器:

  • (red|blue)?等同于(red|blue|)
  • \w+(\s+\w+)*代表“找到一个或多个单词,它们以空格隔开”。

练习

简化\w+\W+\w+\W+\w+\w+\W+\w+\W+\w+\W+\w+\W+\w+\W+\w+

\w+(\W+\w+){2}\w+(\W+\w+){5}

单词边界(Word boundaries)

单词边界是一个单词字符和非单词字符之间的位置。记住,一个单词字符是\w,它是[0-9A-Za-z_],一个非单词字符是\W,也就是[^0-9A-Za-z_]

文本的开头和结尾总是当作单词边界。

输入的文本it's a cat有八个单词边界。如果我们在cat后追加一个空格,这里就会有九个单词边界。

  • 正则表达式\b表示“匹配一个单词边界”。
  • \b\w\w\w\b表示“匹配一个三个字母的单词”。
  • a\ba表示“找到a,跟着一个单词边界,接着找到b”。不管输入文本是什么,这个正则表达式永远都不会成功找到一个匹配。

单词边界不是字符。它们宽度为零.下面的正则表达式表示相同的含义:

  • (\bcat)\b
  • (\bcat\b)
  • \b(cat)\b
  • \b(cat\b)

练习

查找字典中最长的单词。

在一些试验和错误之后,这个正则表达式就是\b.{45,}\b,在字典中找到唯一一个结果:pneumonoultramicroscopicsilicovolcanoconiosis

行边界(Line boundaries)

每一块文本会分解成一个或多个行,用换行符分隔,像这样:

  • 换行
  • 换行
  • 换行

注意文本不是以换行符结束,而是以行结束。然而,任何行,包括最后一行,可以包含零个字符。

起始行位置是在一个换行符和下一行的第一个字符之间。与单词边界一样,在文本的开头也算作一个起始的行。

结束行位置是在行的最后一个字符和换行符之间。与单词边界一样,文本结束也算作行结束。

所以我们都细分为:

  • 起始行,行,结束行
  • 换行
  • 开始行,行,结束行
  • 换行
  • 换行
  • 开始行,行,结束行

在此基础上,有:

  • 正则表达式^表示“匹配开始行”。
  • 正则表达式$表示“匹配结束行”。
  • ^$表示“匹配空行”。
  • ^.*$将会匹配整个文本,因为换行符是一个字符,所以.会匹配它。为了匹配单行,要使用惰性乘法器,^.*?$
  • \^\$表示“匹配尖符号后跟着一个美元符号”。
  • [$]表示“匹配一个美元符”。然而[^]是非法单正则表达式。要记住的是尖符号在方括号中时有不同的特殊含义。把尖符号放在字符类中,这么用[\^]

像单词边界一样,行边界也不是字符。它们宽度为零。下面的正则表达式表示相同的含义:

  • (^cat)$
  • (^cat$)
  • ^(cat)$
  • ^(cat$)

练习

适用正则表达式查找《时光机器》中最长的一行。

Gutenberg出版社的这个版本一行最多有73个字符,使用该^.{73,}$表达式。许多行都是这个长度。

文本边界(Text boundaries)

很多实现提供一个标记,通过改变它来改变^$的含义。从“行开始”和“行结束”变成“文本开始”和“文本结束”。

其它的一些实现提供单独的元字符\A\z来达到这个目的。

捕获和替换

这里就是正则表达式开始变得异常强大的地方。

捕获组

你已经知道,括号是用来表示组。它们也可以用来捕获子串。如果正则表达式是一个很小的电脑程序,这个捕获组就是它的输出(的一部分)。

正则表达式(\w*)ility表示“找到一个以ility结束的单词”。捕获组1就是匹配了部分内容的\w*。举个例子,如果我们的文本包含单词accessibility,捕获组1就是accessib。如果我们的文本自身只包含ility,捕获组1就是空字符串。

你可以拥有多个捕获组,它们甚至可以嵌套使用。捕获组从左到右进行编号。只要计算左圆括号。

假设我们到正则表达式是(\w+) had a ((\w+) \w+)。如果我们的输入文本是I had a nice day,那么

  • 捕获组1是I
  • 捕获组2是nice day
  • 捕获组3是nice

在一些实现中,你可能可以访问捕获组0,即完整匹配:I had a nice day

是的,这确实意味着圆括号有些重复。一些实现就提供了一个独立语法来声明“非捕获组”,但是这个语法不符合标准,所以这里我们不涉及。

从一个成功返回的匹配中捕获组数量总是等于原来正则表达式中捕获组的数量。记住这一点,因为它可以帮助你理解一些令人困惑的情形。

正则表达式((cat)|dog)表示“匹配catdog”。这里总是存在两组捕获组。如果我们的输入文本是dog,那么捕获组1是dog,捕获组2是空字符串,因为另一个选择未被使用。

正则表达式a(\w)*表示“匹配一个以a开头的单词”。这里总是只有一个捕获组(译者注:除去捕获组0)

  • 如果输入文本是a,捕获组1是空字符串。
  • 如果输入文本是ad,捕获组1是d
  • 如果输入文本是avocado,捕获组1是v。然而,捕获组0会是整个单词,avocado

替换

一旦你用了正则表达式来查找字符串,你可以指定另一个字符串来替换它。第二个字符串时替换表达式。首先,就像:

  • 传统的替换对话框
  • Java的String.replace()函数
  • PHP的String.replace()函数
  • 等等

练习

使用r替换《时间机器》中所有的元音字母。确保使用正确的大小写!

分别使用正则表达式[aeiou][AEIOU],替换表达式rR

然而,你可以在你的替换表达式中引用捕获组。这是你可以在替换表达式唯一能的特殊的事,它是令人难以置信的强大,因为它意味着你不必完全销毁你刚刚发现的东西。

比方说,你尝试去用ISO 8691格式的日期(YYYY-MM-DD)去替换美式日期(MM/DD/YY)。

  • 通过正则表达式(\d\d)/(\d\d)/(\d\d)开始。注意这里有三个捕获组:月,日和两个数字表示的年。
  • 通过使用一个反斜杆和一个捕获组号来引用一个捕获组。所以,你的替换表达式为20\3-\1-\2
  • 如果我们的输入文本是03/04/05(表示 3月4号,2005年),那么
    • 捕获组1是03
    • 捕获组2是04
    • 捕获组3是05
    • 替换字符串为2005-03-04

你可以在替换表达式中多次引用捕获组。

  • 使用正则表达式([aeiou])和替换表达式\1\1来让元音翻倍。

在替换表达式中的反斜杆必须进行转义。举个例子,你有一些在计算机程序的字面值中使用的文本。那就意味着你需要在普通文本中的每个双引号或者反斜杆前放置一个反斜杆。

  • 正则表达式([\\"])中,捕获组1是双引号或者反斜杆。
  • 替换表达式\\\1中,一个字面值反斜杆后跟着一个匹配的双引号或者反斜杆。

后向引用(Back-references)

你可以在同样的表达式中引用同一个捕获组。这称为后向引用

举个例子,再次调用前面的表达式[abc]{2}表示“匹配aaabac or babbbccacbcc”。但是表达式([abc])\1表示“匹配aabbcc”。

练习

在字典中,找到出现两次相同字符串的最长的单词(比如papa, coco)。

\b(.{6,})\1\b匹配到chiquichiqui。如果我们不关心完整的单词,我们可以舍去单词边界断言,使用(.{7,})\1会找到countercountermeasurecountercountermeasures

结合正则表达式编程

一些具体的注意事项:

过度反斜线综合征(Excessive backslash syndrome)

在一些编程语言中,如Java,对于含有正则表达式的字符串没有提供特别的支持。字符串有自己的转义规则,这些规则与正则表达式的转义规则叠加,通常会导致反斜杆过多(overload)。比如(还是Java):

  • 为了匹配一个数字,正则表达式\d在源代码中变成String re = "\\d;"
  • 为了匹配一个双引号字符串,"[^"]*"变成String re = "\"[^\"]*\"";
  • 为了匹配一个反斜杆或者一个左方括号或者一个又方括号,正则表达式[\\\[\]]变成String re = "[\\\\\\[\\]]";
  • String re = "\\s";String re = "[ \t\r\n]";是一样的。注意不同的转义“优先级”。

在其它编程语言里,通过一个特殊标记来标识正则表达式,通常是正斜杆/。这里有一些JavaScript例子:

  • 为了匹配一个数字,\d变成var regExp = /\d/;
  • 匹配一个反斜杆或者一个左方括号或者一个右方括号,var regExp = /[\\\[\]]/;
  • var regExp = /\s/;var regExp = /[ \t\r\n]/;一样。
  • 当然,这意味着必须对正斜杠而不是双引号进行转义。匹配URL的前面部分:var regExp = /https?:\/\//;

基于这一点,我希望你明白为什么我对你反复提及反斜杆。

偏移量(Offsets)

在文本编辑器中,会在你光标所在处开始搜索。这个编辑器会向前开始搜索文字,然后停在第一个匹配的地方。下一次搜索会在第一次完成搜索的地方的右侧开始。

当编程的时候,文本的偏移量是必须的。这个偏移量会在代码中有明确的支持,或保存在包含文本的对象中(如Perl),或包含正则表达式的对象中(如JavaScirpt)。(在Java里,这是一个由正则表达式和复合对象的字符串。)在任何情况下,默认值为0,表示文本的开始。搜索后,偏移量会自动更新,或者作为输出的一部分返回。

无论什么情况,通常很容易去使用循环来解决这个问题。

注意。正则表达式匹配空字符串是完全可能的。 你可以立马实现的一个简单的例子是a{0}在这种情况下,新的偏移量等于旧偏移量,从而导致死循环。

一些实现可能保护你避免发生这些情况,但要查下对应的文档。

动态正则表达式

动态地构造一个正则表达式字符串时一定要小心。如果你使用的字符串不是固定的,那么它可能包含意想不到的元字符。这会导致语法错误。更糟糕的是,它可能产生一个语法正确,但行为不可预期的正则表达式。

有bug的Java代码:

  1. String sep = System.getProperty("file.separator");
  2. String[] directories = filePath.split(sep);

这个bug就是:String.split()认为sep是一个正则表达式。但是在Windows下,sep是由犯斜杆组成的字符串"\\".这不是一个语法正确的正则表达式。结果是:一个异常PatternSyntaxException

任何一个优秀的编程语言都提供了一种机制,用以转义在一个字符串中出现的所有元字符。在Java中,你可以这么做:

  1. String sep = System.getProperty("file.separator");
  2. String[] directories = filePath.split(Pattern.quote(sep));

循环内的正则表达式

把正则表达式字符串编译进一个正在运行的“程序”中是一个代价昂贵的操作。如果你能避免在循环内这么做的话能提高程序性能。

各类建议

输入验证

正则表达式能用于用户输入验证。但过于严格的验证会让用户感到难受。下面举几个例子:

支付卡号

我在网页上输入我的卡号如1234 5678 8765 4321。会被这个站点拒绝。因为它使用\d{16}来进行验证。

该正则表达式允许出现空格和连字符。

其实,为什么不直接去掉所有非数字字符,然后再进行验证?要做到这一点,使用正则表达式\D和空字符串来替换表达式。

练习

编写一个正则表达式,可以验证我的卡号而不用让我删去非数字字符。

\D*(\d\D*){16}是能多种实现中的一个方式。

名字

不要使用正则表达式来验证用户的名字。其实,不需要验证名字,你无能无力。

Falsehoods programmers believe about names提到了:

  • 名字不能包含空格。
  • 名字不能包含标点符号。
  • 名字只能使用ASCII字符。
  • 名字会被限制在任何特定的字符集。
  • 名字总是有像M字符那么长。
  • 人总是有且只有一个用的名字。
  • 人总是有且仅有一个中间名。
  • 人总是有且只有一个姓。

邮件地址

不要使用正则表达式来验证邮件地址。

首先,这很难保证正确无误。电子邮件地址确实符合一个正则表达式,但是这个表达式长又复杂地让人联想到世界末日。任何缩略都会可能产生遗漏(false negatives)。(你知道吗?电子邮件地址可以包含注释!)

其次,即使所提供的电子邮件地址符合正则表达式,但也并不能证明它的存在。验证电子邮件地址的唯一方法是发送电子邮件给它。

标记

在正式的应用中,不要使用正则表达式来解析HTML或XML。解析HTML/XML是

  1. 不可能使用简单的正则
  2. 一般来说很难
  3. 一个已解决了的问题。

不妨找一个已有的解析库来为你搞定这些工作。

这就是55分钟内容

总结:

  • 字面值:a b c d 1 2 3 4等等。
  • 字符类:. [abc] [a-z] \d \w \s
    • .表示“任何字符”
    • \d表示“一个数字”
    • \w表示“一个单词字符”,[0-9A-Za-z_]
    • \s表示“一个空格,tab,回车或一个换行符”
    • 否定字符类:[^abc] \D \W \S
  • 乘法器:{4} {3,16} {1,} ? * +
    • ?表示“没有或一个”
    • *表示“没有或多个”
    • +表示“一个或多个”
    • 乘法器是贪婪的除非你在之后使用?
  • 分支和组合:(Septem|Octo|Novem|Decem)ber
  • 词、行和文本边界:\b ^ $ \A \z
  • 反向捕获组:\1 \2 \3等等。(在替换表达式和匹配表达式中同时生效)
  • 元字符列表:. \ [ ] { } ? * + | ( ) ^ $
  • 字符类中使用到元字符列表:[ ] \ - ^
  • 你总是可以使用反斜杆对元字符进行转义:\

感谢阅读

正则表达式无处不在,令人难以置信的有用。那些在编辑文本和写电脑程序方面将花费大量时间的人们应该学会如何使用它们。 到目前为止,我们只接触了冰山一角。

练习

继续阅读你选择的正则表达式实现的对应文档。我保证在我们这里所讨论的部分之外还有更多的特性并未涉及。