本地客户端
注意: 这些讲座笔记略有修改,来自 2014 年 6.858 课程网站。
本文的目标是什么?
- 当时,浏览器只允许任何网页运行 JS(+Flash)代码。
- 希望允许Web应用程序在用户的计算机上运行本机(例如,x86)代码。
- 不想在服务器上运行复杂代码。
- 需要大量服务器资源,为用户带来高延迟。
- 这有什么用?
- 性能。
- 除 JS 外的其他语言。
- 传统应用程序。
- 实际上正在现实世界中使用。
- 作为 Google Chrome 的一部分发布:NaCl 运行时是浏览器扩展。
- 网页可以像 Flash 程序一样运行 NaCl 程序。
- Javascript 可以通过传递消息与 NaCl 程序交互。
- NaCl 还为一些其他用例提供了强大的沙盒功能。
- 核心问题:沙盒化 x86 代码。
使用本地客户端:
- 安装浏览器插件
- 使用 Nacl 工具更改以编译 C 或 C++程序。
- 对可以使用的系统调用有限制。
- 示例应用程序:游戏(不需要太多系统支持)
- 与浏览器交流的特殊接口(在发布中称为 Pepper)
- 制作一个包含 Nacl 模块的网页:
- 模块是“受控”的 x86 代码。
快速演示:
% urxvt -fn xft:Monospace-20 % export NACL_SDK_ROOT=/home/nickolai/tmp/nacl_sdk/pepper_35 % cd ~/6.858/git/fall14/web/lec/nacl-demo ## this is from NaCl's tutorial part1 % vi hello.cc % vi index.html % make % make serve ## copy-paste and add --no-dir-check as the error message asks ## visit http://localhost:5103/ ## change hello.cc to "memset(buf, 'A', 1024);" % make % !python ## visit http://localhost:5103/ ## ctrl-shift-J, view console
有哪些安全运行 x86 代码的选项?
方法 0: 信任代码开发者。
- ActiveX,浏览器插件,Java 等。
- 开发者用私钥签署代码。
- 要求用户决定是否信任某些开发者的代码。
- 用户很难做出这样的决定(例如,使用 ActiveX 代码)。
- 适用于已知开发者(例如,由 MS 签名的 Windows 更新代码)。
- 不清楚如何回答未知的 Web 应用程序(除了“否”)。
- 本地客户端的目标是强制执行安全性,避免询问用户。
方法 1: 硬件保护/操作系统沙盒。
- 与我们已经阅读过的一些想法类似的计划:OKWS,Capsicum,VMs,…
- 将不受信任的代码作为常规用户空间程序或单独的 VM 运行。
- 需要控制不受信任的代码可以调用的系统调用。
- Linux:seccomp。
- FreeBSD:Capsicum。
- MacOSX:Seatbelt。
- Windows:不清楚存在哪些选项。
- 本地客户端使用这些技术,但仅作为备用计划。
- 为什么不直接依赖操作系统的沙盒功能?
- 每个操作系统可能会施加不同的,有时是不兼容的要求。
- 系统调用以分配内存,创建线程等。
- 虚拟内存布局(Windows 中的固定地址共享库?)。
- 操作系统内核漏洞相当常见。
- 允许不受信任的代码逃离沙盒。
- 并非每个操作系统都可能具有足够的沙盒机制。
- 例如,在 Windows 上,没有特殊的内核模块,不清楚该怎么做。
- 一些沙盒机制需要 root 权限:不想以 root 身份运行 Chrome。
- 硬件可能存在漏洞(!)。
- 作者声称某些指令恰好会使硬件挂起。
- 如果访问网站可能导致计算机挂起,那将是不幸的。
**方法 2:**软件故障隔离(本地客户端的主要沙箱计划)。
- 给定一个要在本地客户端中运行的 x86 二进制文件,请验证其安全性。
- 验证涉及检查二进制文件中的每条指令。
- 一些指令可能总是安全的:允许。
- 一些指令可能有时是安全的。
- 软件故障隔离的方法是在这些指令之前要求进行检查。
- 必须确保检查在验证时存在。
- 另一个选项:通过二进制重写插入检查。
- 在 x86 上很难做到,但在更高级别的语言中可能更容易。
- 一些指令可能不值得安全化:禁止。
- 验证后,可以安全地在与其他受信任代码相同的进程中运行它。
- 允许沙箱调用受信任的“服务运行时”代码。
- 论文中的图 2
对于本地客户端模块,安全性意味着什么?
- **目标#1:**不执行任何不允许的指令(例如,syscall,int)。
- 确保模块不执行任何系统调用。
- **目标#2:**不访问模块边界之外的内存或执行代码。
- 确保模块不会破坏服务运行时数据结构。
- 确保模块不会跳转到服务运行时代码,如返回到 libc。
- 如论文所述,模块代码+数据位于[0…256MB)虚拟地址内。
- 不需要填充整个 256MB 的虚拟地址空间。
- 其他所有内容应受到 NaCl 模块的保护。
如何检查模块是否可以执行不允许的指令?
- 草案:扫描可执行文件,查找“int”或“syscall”操作码。
- 如果检查通过,可以开始运行代码。
- 当然,还需要将所有代码标记为只读。
- 并将所有可写内存标记为不可执行。
- *复杂性:*x86 具有可变长度指令。
- “int”和“syscall”指令长度为 2 字节。
- 其他指令可能在 1 到 15 个字节之间。
- 假设程序的代码包含以下字节:
25 CD 80 00 00
- 如果从 25 开始解释为指令,它是一个 5 字节指令:
AND %eax, $0x000080cd
- 但如果从 CD 开始解释,它是一个 2 字节指令:
INT $0x80 # Linux 系统调用
- 可以尝试在每个偏移处查找不允许的指令。
- 可能会产生太多误报。
- 真实指令可能会意外包含一些“不允许”的字节。
可靠的反汇编
- **计划:**确保代码只执行验证器知道的指令。
- 我们如何保证这一点?请参考论文中的表 1 和图 3。
- 从开头开始向前扫描所有指令。
- 如果我们看到跳转指令,请确保它跳转到我们看到的地址。
- 静态跳转(常量地址)很容易确保。
- 无法静态确保计算跳转(从寄存器跳转到地址)。
计算跳转
- 思路是依赖于运行时插装:在跳转之前添加检查。
- 对于计算跳转到%eax,NaCl 需要以下代码:
`AND $0xffffffe0, %eax # 清除最后 4 位(=>只跳转到 32 字节边界)
JMP *%eax` - 这将确保跳转到 32 字节的倍数。为什么是 32 字节?
- 长度超过最大指令长度
- 2 的幂
- 需要适应跳板(稍后见下文)代码
- 不会更大,因为我们不想为单个指令的跳转目标浪费空间。
- NaCl 还要求没有指令跨越 32 字节边界。
- 编译器的工作是确保这两条规则。
- 用上述的两条指令序列替换每个计算跳转。
- 如果其他指令可能跨越 32 字节边界,添加 NOP 指令。
- 如果下一条指令是计算跳转目标,添加 NOP 以填充到 32 字节的倍数。
- 总是可能的,因为 NOP 指令只有一个字节。
- 验证器的工作是检查这些规则。
- 在反汇编过程中,确保没有指令跨越 32 字节边界。
- 对于计算跳转,确保它在上述的两条指令序列中。
- 这将保证什么?
- 验证器检查了所有从 32 字节倍数地址开始的指令。
- 计算跳转只能到达 32 字节的倍数地址。
- 是什么阻止模块跳过 AND,直接到 JMP?
- 伪指令:NaCl 跳转指令永远不会被编译,以便
AND
部分和JMP
部分被 32 字节边界分隔。因此,你永远无法直接跳转到JMP
部分。
- NaCl 如何处理
RET
指令?
- 禁止 – 实际上是一个计算跳转,地址存储在堆栈上。
- 这是由于一个固有的竞争条件:
ret
指令从堆栈中弹出返回地址,然后跳转到它。 NaCl 可以在弹出之前检查堆栈上的地址,但这里存在 TOCTOU 问题:地址可能会在检查后立即被另一个线程修改。这可能发生是因为返回地址在内存中,而不在寄存器中。
- 相反,编译器必须生成显式的 POP + 计算跳转代码。
表 1 中的规则在论文中为什么是必要的?
- C1:内存中的可执行代码不可写。
- C2:二进制在零处静态链接,代码从 64K 开始。
- C3:所有计算跳转使用上述的两条指令序列。
- C4:二进制被填充到页面边界,其中包含一个或多个 HLT 指令。
- C5:没有指令,或者我们特殊的两条指令对,可以跨越 32 字节。
- C6/C7:从起始位置通过顺序反汇编可到达的所有跳转目标。
作业问题: 如果验证器得到了一些指令长度错误,会发生什么?
答案: 取决于在 x86 指令流中的偏移位置,你可以得到意外有用的指令(在 BROP 论文中,我们从 BROP 小工具的 0x7 偏移处得到了一个"pop rsi; ret;")。
如果检查器错误地计算 x86 指令的长度,那么攻击者可以利用这一点。假设检查器将 bad_len(i)
计算为地址 a
处某个指令 i
的长度。了解 x86 的攻击者可以在地址 a + bad_len(i)
处编写汇编代码,通过所有检查并看似无害。这段汇编代码就是 NaCl 检查器会“看到”的内容,考虑到指令长度错误。然而,当代码执行时,指令 i
之后的下一条指令将位于地址 a + real_len(i)
。而且,攻击者精心设计了他的代码,使得在地址 a + real_len(i)
及之后的指令执行了一些有用的操作。比如跳出沙箱,或者进行系统调用。
如何防止 NaCl 模块在其代码之外跳转到 32 字节的倍数?
- 可以在计算跳转序列中使用额外的检查。
AND $0x0fffffe0, %eax
JMP *%eax
- 为什么他们不使用这种方法?
- 用于计算跳转的更长指令序列。
- 他们的序列是
3+2=5
字节,上述序列是5+2=7
字节。 - 另一种解决方案非常简单:分段。
分段
- x86 硬件提供“段”。
- 每次内存访问都是相对于某个“段”。
- 段指定基址 + 大小。
- 段由段选择器指定:指向段表的指针。
%cs, %ds, %ss, %es, %fs, %gs
- 每条指令都可以指定用于访问内存的段。
- 代码始终使用
%cs
段获取。
- 地址转换:(段选择器,地址)->(segbase + addr % segsize)。
- 通常,所有段都具有
base=0, size=max
,因此分段是一个无操作。 - 可以更改段:在 Linux 中,使用
modify_ldt()
系统调用。 - 可以更改段选择器:只需
MOV %ds
等。
将代码/数据限制为模块的大小:
- 添加一个新的段,
offset=0, size=256MB
。 - 将所有段选择器设置为该段。
- 修改验证器以拒绝任何更改段选择器的指令。
- 确保所有代码和数据访问都在 [0…256MB) 范围内。
- (实际上,NaCl 似乎将代码段限制为文本部分大小。)
在没有分段的系统上运行 Native Client 需要什么条件?
- 例如,AMD/Intel 决定在它们的 64 位 CPU 中取消段限制。
- 一个实际的可能性:在 32 位模式下运行。
- AMD/Intel CPU 在 32 位模式下仍支持段限制。
- 即使在 64 位操作系统上也可以在 32 位模式下运行。
- 将不得不更改计算跳转代码以将目标限制为 256MB。
- 将不得不对每个内存读/写添加运行时检测。
- 有关更多详细信息,请参阅下面的附加参考文献中的论文。
为什么 Native Client 不支持模块的异常?
- 如果模块触发硬件异常:空指针,除零等。
- 操作系统内核需要将异常(作为信号)传递给进程。
- 但 Native Client 使用不寻常的堆栈指针/段选择器
%ss
运行。
- 因此,如果操作系统尝试传递异常,它将终止程序。
- 一些操作系统内核在这种情况下拒绝传递信号。
- NaCl 的解决方案是完全禁止硬件异常。
- 语言级别的异常(例如,C++)不涉及硬件:没有问题。
如果 NaCl 模块发生缓冲区溢出会发生什么?
- 任何计算调用(函数指针,返回地址)必须使用 2 指令跳转。
- 因此,只能跳转到模块区域中经过验证的代码。
- 缓冲区溢出可能允许攻击者接管模块。
- 然而,无法逃脱 NaCl 的沙箱。
原始 NaCl 设计的局限性?
- 静态代码:无 JIT,无共享库。
- 近期版本支持动态代码(请参考结尾的附加参考资料)。
从沙箱调用受信任的代码
- 短代码序列,过渡到/从位于[4KB…64KB)的沙箱中。
- 跳板取消沙箱,进入受信任的代码。
- 从 32 字节的倍数边界开始。
- 将无限段加载到
%cs, %ds
段选择器中。 - 跳转到位于 256MB 以上的受信任代码。
- *稍微棘手:*必须确保跳板适合 32 字节。
- (否则,模块可能会跳转到跳板代码的中间…)
- 受信任的代码首先切换到不同的堆栈:为什么?
- NaCl 模块堆栈无法接收异常,并且由跳板调用的库代码可能会出现异常。
- 此外,在论文中提到这个新堆栈,它是每个线程的,将驻留在不受信任的地址空间之外,以保护它免受其他 NaCl 模块线程的攻击!
- 随后,受信任的代码必须重新加载其他段选择器。
- 弹簧板(重新)在返回或初始启动时重新进入沙箱。
- 弹簧板槽(32 字节的倍数)以
HLT
(停止)指令开始。
- 防止模块代码跳转到弹簧板。
- 重新设置段选择器,在 NaCl 模块中跳转到特定地址。
服务运行时提供了什么?(NaCl 的“系统调用”等效)
- 内存分配:sbrk/mmap。
- 线程操作:创建等。
- IPC:最初与启动此 NaCl 程序的页面上的 Javascript 代码。
- 浏览器接口通过 NPAPI:DOM 访问,打开 URL,用户输入,…
- 没有网络:可以使用 Javascript 根据 SOP 访问网络。
Native Client 有多安全?
- 攻击面列表:开始于第 2.3 节的开头。
- 内部沙箱:验证器必须正确(有一些棘手的错误!)。
- 外部沙箱:依赖于操作系统的计划。
- 在 Linux 上,可能是 seccomp。
- 在 FreeBSD 上(如果 NaCl 支持),Capsicum 会很有意义。
- 为什么外部沙箱?
- 内部沙箱可能存在漏洞。
- 如果对内部沙箱进行了妥协,对手会做什么?
- 利用 CPU 漏洞。
- 利用 OS 内核漏洞。
- 利用其他进程中的漏洞与沙箱进程通信。
- 服务运行时:初始加载程序,运行时跳板接口。
- 模块间通信(IMC)接口 + NPAPI:复杂的代码,可能(并且确实)存在错误。
它的性能如何?
- CPU 开销似乎主要受 NaCl 的代码对齐要求的影响。
- 更大的指令缓存占用。
- 但对于某些应用程序,NaCl 的对齐方式比 gcc 更好。
- 对于计算跳转的附加检查,开销最小。
- 调用服务运行时的性能似乎与 Linux 系统调用相当。
将代码移植到 NaCl 有多难?
- 对于计算性的事物,似乎很简单:H.264 只需改动 20 行代码。
- 对于与系统交互的代码(系统调用等),需要进行更改。
- 例如,Bullet 物理模拟器(第 4.4 节)。
其他参考资料
- 本地客户端适用于 64 位 x86 和 ARM。
- 本地客户端用于运行时生成的代码(JIT)。
- 无硬件依赖的本地客户端。
- 其他具有细粒度内存访问控制的软件故障隔离系统:
Web 安全
长期以来,Web 安全意味着查看服务器的操作,因为客户端非常简单。在服务器上,CGI 脚本被执行,并且它们与数据库等进行交互。
如今,浏览器非常复杂:
- JavaScript:页面执行客户端代码
- 文档对象模型(DOM)
- XMLHttpRequests:JavaScript 客户端代码从 Web 服务器异步获取内容的一种方式
- 又名 AJAX
- Web 套接字
- 多媒体支持(<video>标签)
- 地理位置(网页可以确定您的物理位置)
- 本地客户端,适用于 Google Chrome
对于 Web 安全来说,这意味着我们很糟糕:巨大的攻击面(见图 1)
likelihood of correct ness ^ |--\ | --\ --- we are here | --\ / | \ / | \ <-- | -----*---- |-----------------------> # of features
组合问题:许多层
Web 存在的一个问题是parsing contexts问题
<script>var = "UNTRUSTED CONTENT FROM USER";</script>
如果不受信任的内容中有引号,也许攻击者可以修改代码为:
<script>var = "UNTRUSTED CONTENT"</script> <script> /* bad stuff from attacker here */ </script>
Web 规范很长、繁琐、乏味、不一致,大小如欧盟宪法(CSS、HTML)=>它们是模糊的抱负性文件,从未被实施。
本讲座我们将专注于客户端 Web 安全。
桌面应用程序来自单一主体(微软、谷歌等),Web 应用程序来自多个主体。
http://foo.com/index.html
(见图 2)
- 分析代码能够访问 Facebook 框架内容吗?
- 分析代码能够与文本输入交互吗?它能声明事件处理程序吗?
- Facebook 框架(https)和 foo.com 框架(http)之间的关系是什么?
为了回答这些问题,浏览器使用了一个称为同源策略的安全模型
*目标:*两个网站不应该能够互相篡改,除非它们想要这样做。
定义tampering的含义自从 Web 开始以来变得更加复杂。
策略:每个资源都被分配一个起源。JS 代码(一个资源本身)只能访问来自自己起源的资源。
什么是起源?起源是网络协议方案+主机名+端口。例如:
https://facebook.com:8181
http://foo.com/index.html
,隐式端口 80https://foo.com/index.html
,隐式端口 443
粗略地说,你可以将一个起源视为 UNIX 中的 UID,而一个框架就是一个进程。
在实现起源的四个想法中:
- 每个起源都有客户端资源
- Cookies,用于在不同的 HTTP 请求之间实现状态
- DOM 存储,一个相当新的接口,一个键值存储
- 一个 JavaScript 命名空间,定义了对起源可用的函数和接口(如 String 类)
- DOM 树:页面中 HTML 的 JavaScript 反射
[ HTML ] / \ [ HEAD ] [ BODY ]
- 一个可视化显示区域
- 每个框架都获得其 URL 的起源
- 脚本以其框架起源的权限执行
- 被动内容(图像、CSS 文件)从浏览器中获得零权限
- 内容嗅探攻击
回到我们的例子:
- Google 分析和 jQuery 可以在 foo.com 框架上执行各种操作
- Facebook 框架的内联 JS 无法对 foo.com 框架执行任何操作
- 但它可以使用
postMessage()
API 与 foo.com 框架通信
- FB frame 中的 JS 代码无法向 foo.com web 服务器发出 AJAX 请求
MIME 类型:text/html。过去的所有 IE 版本都会查看对象的前 256 个字节并忽略Content-Type
头。结果,IE 会误解文件的类型(由于错误)。攻击者可以将 JS 代码放入.jpg 文件中。IE 将其强制转换为 text/html,然后在页面中执行 JS 代码。
Frames 和 window 对象
Frames 代表这种独立的 JS 宇宙
一个 frame,关于 JS 是一个 DOM 节点的实例。Frames 和 JS 中的 window 对象相互指向。window 对象充当一个命名空间,通过它可以访问任何变量x
。
Frames 获取 frame 的 URL 的 origin OR
原始域名的后缀。
x.y.z.com
可以说“我想将我的源设置为”y.z.com
通过将document.domain
分配给y.z.com
。这只适用于x.y.z.com
的后缀(或应该)。因此,它不能执行document.domain = a.y.z.com
。也不能设置document.domain = .com
,因为该站点将能够影响任何.com 网站中的 cookies。
浏览器区分已分配值给 document.domain 的 frame 和未分配值给 document.domain 的 frame。
两个 frame 可以相互访问如果:
- 两个 frame 都将
document.domain
设置为相同的值 - 两个 frame 都没有改变
document.domain
,并且两个值匹配
你有x.y.z.com
(有 bug 或者恶意)试图攻击y.z.com
,通过缩短其域名。浏览器不会允许这种情况发生,因为 y.z.com 并未改变其 document.domain,而 x.y.z.com 已经改变了。
DOM 节点
Cookies
Cookies 有一个domain和一个path。
*.mit.edu/6.858
如果路径是/
,那么域中的所有路径都可以访问 cookie。
在客户端有document.cookie
。
Cookies 有一个secure flag
,意味着 HTTP 内容不应该能够访问该 cookie。
当浏览器生成请求时,它将包括该请求中的所有匹配 cookie(环境权限)。
不同 frame 如何访问其他 frame 的 cookies?如果其他 frame 可以为其他 frame 写入 cookies,那么攻击者可以将受害者登录到攻击者的 gmail 帐户,并可能读取用户发送的电子邮件。
应该允许foo.co.uk
为co.uk
设置 cookie 吗?https://publicsuffix.org 包含所有顶级域的列表,因此浏览器不允许为co.uk
等域设置 cookie。
XMLHttpRequest
默认情况下,JS 只能生成一个 AJAX 请求,如果它要去自己的源。
有一种新的范式称为跨源请求 S.(CORS),其中服务器可以使用 ACL 允许其他域访问它。服务器返回一个头Access-Control-Allow-Origin: foo.com
来指示 foo.com 是被允许的。
图片,CSS
一个 frame 可以从任何它想要的源加载图片,但它实际上不能检查位。但它可以通过 DOM 中其他节点的位置推断出图片的大小。
CSS 也是如此。
JavaScript
如果您对 JS 进行跨源提取,是允许的,但框架不能查看源代码。但是 JS 架构有点让你可以,因为您可以调用任何公共函数f
的toString
方法。框架还可以要求 Web 服务器为其提取 JS 并发送。
JS 代码经常被混淆。
插件
Java,Flash。
框架可以从任何来源运行插件。HTML5 可能会使它们过时。
跨站请求伪造(CSRF)
攻击者可以设置一个页面,并在其中嵌入以下来源的框架:
http://bank.com/xfer?amount=500&to=attacker
框架被设置为大小为零(不可见),然后攻击者让用户访问该页面。因此,他可以从用户那里窃取钱。
这是因为 URL 可以被猜测,而不是随机的。
解决方案:在 URL 中添加一些随机性。
服务器可以生成一个随机令牌并将其嵌入发送给用户的“转账”页面。
<form action="/transfer.cgi" ...> <input type="hiddne" name="csrf" value="a72fedb2129985bdc">
现在攻击者必须猜测令牌。
网络地址
框架可以向与其来源匹配的主机发送 HTTP 和 HTTPS 请求。同源策略的安全性与 DNS 安全性相关联。因为来源名称是 DNS 名称,DNS 重新绑定攻击可能会对您产生影响。
目标:以受害者网站victim.com
的权限运行受攻击者控制的 JS。
方法:
- 注册一个域名
attacker.com
- 攻击者设置 DNS 服务器以响应对
*.attacker.com
的请求。 - 攻击者让用户访问
*.attacker.com
- 浏览器向
attacker.com
生成 DNS 请求。 - 攻击者响应具有较短的生存时间(TTL)
- 与此同时,攻击者配置 DNS 服务器将
attacker.com
名称绑定到victim.com
的 IP 地址 - 现在,如果用户请求对 attacker.com 的 DNS 解析,他将获得 victim.com 的地址
- 加载的 attacker.com 网站希望通过 AJAX 获取一个新对象。此请求现在将发送到 victim.com
- 不好的原因是 attacker.com 网站刚刚在其来源之外发出了一个 AJAX 请求。
如何解决这个问题?
- 修改您的 DNS 解析器以检查外部域名是否解析为内部地址。
- 强制 TTL 为 30 分钟
像素
每个框架都有自己的边界框,并可以在其中任意绘制。具体来说,父框架可以覆盖子框架(见图 3)。
解决方案:
- 使用破坏框架的代码(JS 来判断是否被别人放入框架中)
`if (self != top)
alert("我是一个子框架,所以不会加载")
- Web 服务器可以发送一个名为
X-Frame-Options
的 HTTP 响应头,告诉浏览器不允许任何人将其内容放入框架中。
命名问题
ASCII 中的c
与 Cyrillic 中的c
允许攻击者注册一个模仿真实cats.com
的cats.com
域。
插件
与浏览器的其余部分存在微妙的不兼容性。
Java 假设具有相同 IP 地址的不同主机名具有相同的来源(与 SOP 策略不符)。
如果它们共享相同的 IP 地址,x.y.com 将与 z.y.com 具有相同的来源。
HTML5 屏幕共享
如果您有一个包含多个框架的页面,一个框架可以截取整个浏览器的屏幕截图。
MIT 6.858 计算机系统安全讲义 2014 秋季(二)(2)https://developer.aliyun.com/article/1484151