本地客户端
注意: 这些讲座笔记略有修改,来自 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, %eaxJMP *%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:8181http://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