Recently in Security Category

安全:该做什么和不该做什么

| 3 Comments

很多事情都有Do's and Don'ts,这里试着整理一下与安全有关的。

本文版权所有 © 2010 Xin LI <delphij@FreeBSD.org> 保留所有权利;非商业转载请注明出处 http://blog.delphij.net/, 谢绝商业转载。

安全不能建立在"别人不知道"的基础上

"别人不知道"是一种非常常见的安全 假象,举例来说,一种自己设计的山寨加密算法、一个系统中一般人不知道的位置等等,都属于这一类。

将安全建立在"别人不知道"的基础上是非常危险的。首先它会给设计者和用户带来"安全"的幻像,这会直接导致与系统交互的人放松警惕;其次,这样的设计往往留有"后门",甚至是设计者不知道的后门(因为往往他们并不对这类设计进行充分的、专业的审计),容易被攻击者利用;最后,这种做法存在第三方泄密问题,即,使用这种系统的人,需要提防设计系统的人被其他人买通并泄漏一些秘密的情况。

延缓攻击的手段不能用来阻挡攻击

有许多延缓攻击的手段,例如改变服务的端口(比较常见的如将 ssh 改为 tcp/22 以外的端口),或禁止服务程序显示自己的版本等等,或仅仅简单地启用防火墙,这些手段起到的作用只是延缓攻击,而不应作为一种安全屏障。对于多层次式的安全设计来说,采取这些措施有助于提高检测到入侵的机会,但是它们本身并不会提高安全性。

与前一种情况类似,这种做法也只是让管理员放松警惕。例如以 ssh 为例,有人认为将端口改为一个非知名端口可以避免相关的攻击,但事实是,攻击者依然可以利用 ssh 实现或协议设计中存在的一些漏洞来攻破系统。拥有特定资源的攻击者甚至不需要直接对目标系统实施攻击。在较复杂的攻击手段中,包括简单的 port knocking 一类的保护手法,都可以使用类似分组重放这样的方法来逐步攻破。

采用层次式的安全设计

所谓层次式的安全设计,说的是在一套安全系统中包含不同层次的、存在层次式监控关系的安全结构。例如,将本地包含执行文件的那些文件系统通过一定的方式导出给监控网段的机器,就可以让那些机器在攻击者不知情,或至少不太容易注意到的情况下对入侵进行检测;通过将一些重要日志发到以不同的访问控制机制,甚至不同网络协议的记录设备上,则可以有效地检测入侵者的入侵行为,并为日后的分析留下更多的有用信息。

层次式安全在现实中也有应用。例如产品的质检,除了制造商自己进行的质量控制之外,有时分销商或政府也会进行一些抽样的检查。我们注意到,这些设计中的一个重要的特点是在不同的系统中使用不同的访问控制逻辑。例如,日志服务器必须从特定的客户端,甚至只能从某些隔离的内网登录。此时,延缓攻击的手段可以作为它的一项辅助设施,即其目的并不是阻止攻击,而是吸引攻击者在攻击目标上花费更多的时间,从而帮助入侵检测机制更容易地检测这些攻击。

不要轻信任何东西,包括X.509证书

安全系统的设计者必须对安全有全面的理解和认知。有一句很著名的话叫做 In God we trust, all others must submit an X.509 Certificate,需要注意的是,这里说的是 must submit,并没有说 submit 了就可以 trust 了。

和前面所说的层次式安全设计类似,我们的一个基本假定应该是,一个安全系统中的任何参与者,无论是用户还是计算机或程序,都是可能存在弱点的。安全系统,或用户,都不应轻信任何东西,例如,在特权隔离 (Privilege Separation) 这样一种设计中,特权进程除了完成那个非特权的子进程的请求之外,还有一个任务是维护一个"理性状态机"(Sanity DFA),这个状态机的作用是检测非特权进程的异常状况,如果发生这样的情况,则特权进程有拒绝提供服务,并杀掉非特权进程的责任。作为用户,对于系统给出的响应,除了验证对方的证书之外,也应有常识性的了解和适当的判断。

用户口令的验证问题

| 2 Comments

口令是许多系统中用于判断用户身份的重要手段。当然,使用口令作为身份验证方法是很不靠谱的(例如,许多用户会使用弱口令,或者在多个系统中使用同样的口令,以及它存在对称失密问题,等等),不过因为口令便于携带(相对于私钥)和实现,因此仍然被广泛使用。

验证用户口令有很多种不同的方法。最原始的方法,就是将用户输入的口令与我们保存的口令相比较,如果相同,就认为是验证通过。但是这样一来,我们就必须保存用户的口令,而保存用户的口令会导致很多问题,例如,假如系统用于保存口令的数据库被攻陷,直接泄露用户的明文口令肯定不是一件好事。

于是,我们可以对前面的方法进行改进,即,只保存用户口令的散列(hash)字符串,而不是口令的明文。散列算法是一种单向的计算,即散列函数 H 对明文信息 p 计算出的结果 h:

H(p) = h

端到端(End to End)加密与安全

| 1 Comment

说明:这篇文章希望能够以比较通俗的方式介绍一下相关的概念。

端到端加密,通常是指两个通信实体之间在会话通道基础上进行的、由两个实体之间协商的密文会话。端到端加密的好处是能够减少会话通道本身的泄密或其他问题导致两个通信实体之间的通讯遭到第三方破译,并避免通讯的对方对通讯内容抵赖(即,不承认通讯内容来自自己)。相对而言,端到端加密的实施成本要比单独架设一条物理的通讯线路要便宜许多,因此有时它也用于架设这类通讯线路。端到端加密本身不能阻止由于使用的会话通道本身的丢包导致的干扰。然而,由于端到端加密可以工作在较高的抽象层次上,它可以使用多个实际的会话通道而提高抗干扰能力。

通常我们会希望在一个安全系统中设置多个层次的安全设施,或者说类似"洋葱"的模型,例如,在系统中,将所有与安全有关的信息记录日志并放到不可擦除的介质上、对连接进行加密,和对发送的消息进行加密,这几种方法分别考虑的是不同级别的信息安全,而这些方法可以在同一个安全系统中作为其不同的保护层次来使用。在这一类模型中,我们不仅视阻止入侵为目标,同时也将检测入侵做为目标,或者换言之,每攻破系统中的一个层次,入侵者都需要付出额外的努力,并冒更大的被发现的风险才能达到这样的目标。

以邮件系统为例,传统的电子邮件系统是完全不使用任何加密或签名的手段的。拥有接入层控制权的网络管理员,可以轻易地通过在网络上监听数据包来获知用户与自己所使用的邮件服务器之间交换的邮件内容。随后,当邮件到达了邮件服务器时,邮件服务器管理员也可以很容易地知道邮件内容;在邮件服务器将邮件传送到互联网上的另一台邮件服务器时,问题就更多了,每一跳路由,以及它们所经过的所有网络都可以被监听,并将邮件内容泄漏出去;最后,在另一台服务器和收件人之间,也有和发件人这一边一样的风险。

在Internet上传输电子邮件的过程,由于其经过的环节众多,并且几乎完全不受收发邮件的双方控制,因此很容易导致一些敏感信息的泄露。针对这种问题,人们设计出了一种叫做Transport Layer Security(TLS,用于替代SSL)的密码学协议来实现服务器之间,以及客户端到服务器的端到端安全。简而言之,这套协议首先使用公钥加密算法在通讯双方之间协商一个会话密钥,并用这个密钥完成之后的通讯加密/解密。公钥加密算法是一种加密和解密时使用不同密钥的加密算法,以对方公开密钥加密的数据,只能以(不公开的)解密密钥解密,而从公开密钥或已知明文、密文推算出解密密钥的代价非常大,因此它能够确保数据只能被"正确的"那个接收方解密。

portsnap镜像的另一种实现方法

| 2 Comments

FreeBSD 5.5和6.1开始内建了 portsnap,portsnap是一种新的 ports 套件同步机制。

与传统的 cvsup 更新方式相比, portsnap 有一些优越性:

  • 对于经常进行更新的情况,portsnap很快:portsnap包含索引和具体的ports两部分,在更新的时候它首先会更新索引,而具体的更新则是下载两次索引描述的文件之间的差异,因此可以大大节省带宽。
  • portsnap有数字签名:cvsup并不包含任何加密或数字签名的机制,而portsnap的公钥则是通过光盘发给每一台机器的,相对来说安全性要好的多。
  • portsnap使用HTTP协议:通常,企业防火墙并不会拦截HTTP。

早先,架设portsnap镜像的方法比较复杂。由于portsnap的设计,架设portsnap镜像所产生的流量,在通常情况下大约会是仅做portsnap操作的3000倍左右。

之前听lwhsu提到了台湾那边的镜像所采用的方法,值得借鉴:

由于portsnap使用的是HTTP协议,而数据集相对较小,因此,实际上可以把这些内容全部作为反向代理放进内存。

客户端应该去计算什么?

| 3 Comments

这是一个很有意思的话题:随着计算机技术的发展,客户端的计算能力越来越强。想要提高在服务器端运行的系统的负载能力,最直接有效的办法就是把计算任务尽可能交给客户端去做,并减少两者之间的交互;然而,另一方面,这样做又可能会带来一些其他问题,例如,客户端完成某些计算任务的时候可能会比较慢(因为在客户端可以用到的资源比较少,想要保持兼容性最好的办法就是只使用普适的Java Script子集),或者,作为安全系统的一个最基本的原则,任何来自外界的数据都是不应被信任的,等等。

我们可以把数据根据一些规则来进行分类。一般来说数据会具有一些这样的属性:

  • 传输方向。数据是服务器发给客户端的?还是反过来?或者,只是在客户端兜个圈?或者在两台服务器之间传递,只不过客户端是一个中间的载体(当然一般来说,这样做的系统的架构师应该去看看神经科的医生)?
  • 敏感性。如果数据在传输过程中(从客户端到服务器,或反过来)被人截获,是否会产生安全威胁?
  • 尺寸。这个主要是从经济方面考虑,不过多增加计算量的前提下,通常我们会希望传输的尺寸越小越好。

这样一来,判断计算是否应在客户端进行就比较简单了:我们首先要看的第一个问题是:放在客户端进行,是否会增加服务器的计算量?例如,服务器是否需要再增加一些额外的步骤,才能够完成具体的操作?这些额外的步骤与原先的计算相比是否更少?通常来说,这个答案都是"是的",这样我们可以继续考察第二个问题。

第二个问题是,这样做是否有助于减少通讯量?例如,对于用户输入信息的合法性检查,比如说,用户输入的内容是否可能导致SQL注入,或者,他是否在一个该输入数字的地方输入了其他字符?这些检查很显然必须在服务器端做(因为数据跨越了安全边界),但是,假如客户端也进行了这些检查的话,那么那些无效输入就可以在与服务器交互之前被拦住,从而减少通讯量并减少服务器的负担。

第三个问题是,这样做是否会占用太多客户端资源?这个问题可能不太容易评估,一般来说,计算密集型的任务使用Java Script去做的效果有可能会不太好,有时,为了改善响应时间,有可能会希望这些操作由服务器去完成(由于可以采用任意的软件,因此服务器完成某些任务可能更高效)。简单的排序操作在客户端做有助于减少服务器负载,但如果很多客户端需要请求同样的内容,那么将排序结果在服务器端直接缓存起来就会更好一些,等等。

以上仅代表个人经验供参考。

C程序设计中,内存操作相关的错误可以说是最常见,同时也是非常隐蔽的一类错误。这类错误往往导致程序莫名其妙地崩溃、耗尽系统资源,或是形成严重的安全弱点。

FreeBSD,以及多数其他 BSD 派生的系统中,重复 free() 在默认情况下都会导致 C 函数库调用 abort() 终止程序。除了 malloc(3) 函数族本身的设计之外,这也是一项非常重要的安全特性。与此相反,包括 *BSD 在内的多数系统的 C 函数库并不对堆进行审计,也就是说,从 API 设计者的观点来看,内存泄漏并不被认为是非常严重的程序设计问题。

为什么会有这样的区别呢?事实上,内存泄漏同样可以导致比较严重的问题,例如响应速度变慢、进程由于占用的资源太多而被 OS 杀掉导致 DoS 等等。为了回答这个问题,我们来观察一下两种问题出现的场景。

内存泄漏 是指这样一种场景:程序分配了一块内存,但已经不再持有引用这块内存的对象(通常是指针)。从 OS 的角度,它知道进程持有的内存数量;然而,从进程的角度,它可能并不完全知道自己持有哪些内存。换言之,内存泄漏就是通过遍历进程内所有可以从栈上,或以静态变量形式存于堆上的指针及其后继,无法到达所有全部已分配内存的情形。

如果程序不存在其他问题(例如缓冲区溢出),此时程序访问内存时,任何时候都不会在无意中覆写超出范围的数据。即,将数据覆写到程序其他部分保存数据的内存单元。

重复释放 则指这样一种场景:程序分配一块内存之后,经过使用将这块内存释放,但并没有将指向这块内存的所有指针抹零或回收,并在其他部分再次将指向同一块内存单元的指针交给内存分配器去进行释放操作。这种情况下,我们可以断言:

  • 程序逻辑并不清楚这块内存已经被释放;并且,
  • 有理由相信,对这块内存进行的写操作,可能已经影响了程序其他部分的行为,因为这块内存可能已经分配作为其他用途。

因此,这应被看作立即停止程序运行的一项致命错误,因为程序行为已经出现了异常,而C函数库拥有的信息不足以纠正这种异常行为,而另一方面,程序可能已经发生了堆缓冲区溢出。

为了削弱这类问题带来的实质性安全影响,现代的内存分配器往往会将尺寸接近的内存块放在一起(这样做还能够抑制内存碎片的产生,并提高CPU的数据缓存命中率,因为通常程序会倾向于一次性地访问相近的内存结构),从而能够在一定程度上减轻由于向已经释放的内存块继续写数据导致的损害(因为这些内存很可能被分配给同样的数据结构,这类写操作的危害往往会低于向其他类型的数据结构写数据,特别是当这些数据中包含一部分用户输入的时候)。

当然,彻底消除这类问题,需要为程序设计语言增加一些新的基础设施(例如强类型、托管内存等)。现代程序设计语言如Java、Python和.net系列等,都采用了避免这类问题的措施。然而,也正因为如此,通过这些语言入门并准备撰写 C 程序的开发人员就更需要注意这类问题。

NULL指针引用和内核bug的利用

| No Comments

总算是发公告了,可以说具体的事情了。

FreeBSD昨天发布了2项安全公告和1项Errata Notice:SA-09:13.pipeSA-09:14.devfsEN-09:05.null。两个安全公告修正的是同一类问题,也就是我们常说的多线程程序中的竞态条件(Race Condition);EN-09:05.null则是增加了一个使这类问题不再那么容易被利用达到特权提升目的的功能,但默认并不启用。

在 C 程序中,NULL 指针是一项很有用的特性。NULL有很多功能,例如:

  • 表示链表结束,或指针没有引用数据对象。因为NULL是0,因此判断一个指针是否是NULL要比判断一个指针是否指向某一个特定的对象要高效。
  • 作为调试工具:引用NULL会立即触发缺页(Page Fault),通常系统会将用户进程虚拟地址空间中地址为0开始的至少一页不予映射,因此这次缺页将让OS直接向进程发出SIGSEGV,这可以让程序尽早崩溃,以帮助开发人员找到问题。(映射一整页能够让访问 NULL 和 NULL 附近的内存时都产生缺页,例如指向结构 struct mystruct 的指针 struct mystruct *p,在访问 p->m_member 的时候,实际访问的内存地址是 offsetof(struct mystruct, m_member) 而未必是 0。

等等。与用户态程序类似,内核也使用 NULL 完成相同的目的。当内核自己引用空指针内存(NULL deferencing)时,x86/amd64硬件会产生异常12。

不过,传统上 Unix 系统并不明确地禁止用户在虚拟地址0上映射自己的内存页。因为对于程序来说,它也完全可以把地址 0 看作一个再普通不过的内存地址。另一方面,有些应用程序,特别是模拟器一类的程序,很可能会需要利用这一特性来完成一些功能,因为如果靠截取 SIGSEGV 并进行特殊处理的话,效率会很差。

事实上,用户程序能够在地址 0 上映射内存页这件事本身并不是一个问题。真正的问题在于,为了改善性能,许多操作系统(如果不是全部支持 x86 的通用操作系统的话)在内存分配方面会采用一个小技巧,将内核内存和用户内存映射在同一个虚拟地址空间,从而避免在执行系统调用进入和退出内核时进行完整的地址空间切换,因为这个操作在x86硬件上的代价是比较大的,而另一方面,由于同样的原因,这个技巧也会改善内核访问用户地址空间中内存的性能。

然而,这一切能够相安无事的前提是,内核没有bug,或者用户态运行的程序是"友善"的。由于欠锁等原因,内核可能会引用 NULL 指针(例如,在没有获取对象引用的情况下调用对象中的函数指针,而此时另一线程已经将对象的内存释放并进行了抹零操作)。这种竞态条件通常会导致系统崩溃,但如果用户能够控制虚拟地址为0的那一小块内存,则这类竞态条件就有可能被利用来执行用户所指定的代码,进而导致特权提升问题。

从 FreeBSD 8.0-RELEASE 开始,FreeBSD将默认禁止用户程序映射虚拟地址为0的内存页(返回EINVAL,个人认为应返回EPERM)。修正日期之后的 FreeBSD 其他版本如 6.x 和 7.x,可以通过将 security.bsd.map_at_zero="0" 增加到 /boot/loader.conf 来启用这一特性。

ZFS实现快速部署(作弊条)

| No Comments

FreeBSD从8.0开始支持从ZFS引导系统,因此,可以使用ZFS的快照功能来实现快速部署。

新机器使用LiveFS启动,Fix it,CDROM。

kldload /dist/boot/kernel/nullfs.ko

mount_nullfs /dist/boot /boot

kldload tmpfs

kldload zfs

如果需要,此时用dd抹除硬盘内容,例如 dd if=/dev/zero of=/dev/da0 bs=1m count=1

gpart create -s gpt da0(重复此步直到所有硬盘皆包含GPT分区表)

gpart add -b 34 -s 128 -t freebsd-boot da0(实际上只有启动盘需要,不过64K空间对现代硬盘来说基本上可以忽略不计)

gpart add -b 162 -s 8388608 -t freebsd-swap da0(根据需要酌情配置)

gpart add -b 8388770 -t freebsd-zfs da0

cd /boot

gpart bootcode -b pmbr da0(建议所有盘有freebsd-boot分区的盘都做)

gpart bootcode -p gptzfsboot -i 1 da0(所有有freebsd-boot分区的盘都做)

glabel label swap0 /dev/da0p2(swap1,2,3,4,...类推)

glabel label vdsk0 /dev/da0p3(vdsk1...类推)

zpool create -m legacy 集群名 raidz2 /dev/label/vdsk0 /dev/label/vdsk1 .. spare /dev/label/vdskn

ifconfig em0 inet x.x.x.x/m

nc -l 80 | zfs receive -vF 集群名

在样本机上执行:zfs send 集群名@快照名 | nc 客户机IP 80

之后,在安装的机器上mount -t zfs 集群名 /mnt,然后把/boot/zfs内容复制到/mnt/boot/zfs,修改etc/rc.conf等,即可。如果配置时使用的是DHCP,则此过程可进一步简化。

SYNCookie反制

| No Comments

最近看到一个很有意思的攻击。记一笔。

针对 SYN Flood 攻击(特点是攻击者发出大量 SYN 请求,但并不完成 TCP 握手),目前操作系统会采用 SYNcookie 予以反制。FreeBSD上的实现是在系统资源充足的时候采用标准的握手机制,而当发现存在可能的攻击(即可用的 TCP 状态资源不足时)应用 SYNcookie;SYNcookie是将TCP状态建立推迟到三次握手 之后,具体做法是将服务器本地的一个秘密数据与来源/目的IP、端口编码来形成序号来发出SYN+ACK,并在收到对方回应ACK时验证这个序号的真实性。

这种方法实际上是用计算来换内存资源,即增加了两步计算操作来避免在握手完成之前消耗 TCP 状态资源。

SYN Flood提供了两个优势:

  1. 攻击者可以伪造来源IP
  2. 攻击者可以不维护TCP状态(四两拨千斤)

新的攻击方法在攻击者一方也使用了类似SYN cookie的方法,即,它能够实际完成TCP握手,从而击败 SYN cookie(当然,由于完成了三次握手,伪造源IP就不太容易了),方法是将本机和服务器的端口/IP编码进序号。

这次公开的攻击方法还有很多非常聪明的做法,在改TCP实现之前,防守一方能够采取的缓解方法不太多(限制IP、推迟服务启动时间等)。

DNS和MX的冗余

| 1 Comment

一般来说DNS和MX都需要做冗余。前者的冗余和容灾要求要高于后者。

DNS的冗余通常需要做到跨ISP,甚至跨大洲。由于安全和性能方面的理由,提供权威DNS解析的服务器都不应同时作为缓存DNS服务器;此外,实践上权威DNS服务器应该是受保护的管理服务器的slave,而不应直接在权威DNS服务器上操作(读、写分开),这样可以有效减少由于修改操作所导致的downtime。

MX的冗余可以是跨网段,也可以是跨ISP甚至跨大洲的。如果条件不具备,甚至可以在同一网段,但这会增加网络设备或机房故障所导致的downtime风险。不同的MX在会话阶段的过滤策略应该是一致的。

不按牌理出牌的无线网络

| 2 Comments

今天到了Seattle,在一家很便宜的酒店住下。酒店发给我一张密码卡,用来上那里的无线网。摆弄了几下发现:

加密模式是open,所以需要自己加密数据了。

密码的实现方式是劫持DNS,然后指到1.1.1.1。

最搞笑的是,实际上不登录,IP访问也是通的......

-----

话说,其实我觉得在网关上做点规则比DNS劫持要简单的多?有谁知道他们为啥会这样实现吗?

Windows 2000的时间服务

| No Comments

两年前我在一篇blog中解释了 为什么应该用 ntpd 而不是 ntpdate 来对时间。今天整理旧机器的时候观察了一下Windows 2000 Server的行为。

首先关闭时间服务(Windows Time Service),设置sntp(net time /setsntp:"time-a time-b",然后把时间人为调快2分钟,然后启动时间服务。

乍一看时间还是快2分钟,打开系统时间一看,秒的部分整整慢了将近一倍,直到时间和真正的时间逐渐接近之后时钟速率才逐渐追赶上来。

所以,Windows的时间服务在差距不太大的时候也是会主动避免跳变的。这里就不再强调这样做的重要性了。

About this Archive

This page is an archive of recent entries in the Security category.

Others is the previous category.

Shared Chaos is the next category.

Find recent content on the main index or look in the archives to find all content.

Pages

OpenID accepted here Learn more about OpenID
Powered by Movable Type 5.01