计算机系统工程之:计算机系统安全

Intro to System Security

  • Security is a negative goal. 安全是完成“不能让某件事情被做”的任务,想做一件事情有很多种方法,所以安全要考虑的东西很多,本身也很难
  • Security may be conflict with other goals. 安全的诉求和其他的诉求可能是冲突的

安全的目标:

  • Confidentiality 限制数据的读取
  • Integrity 限制数据的改写
  • Availability 确保系统运行符合正常情况,而非执行异常

Thread Model: Assumption

  • 攻击者可能会控制你的部分电脑、或控制电脑内部的部分软件、获得一些隐私信息、了解你程序的bug
  • 考虑物理攻击、社会工程学攻击、维持安全的成本

ROP and CFI

Password

使用如下的检查密码算法有何问题?由于是逐位检查,因此可以在密码存放的方式做手脚,比如可以将密码的第一位和剩下的位数放在不同的页中,且让剩下的位数被换页到磁盘,那么就可以根据时间来判断第一位是不是对的,从而如法炮制试出每一位的正确字符

如何降低密码验证方式被攻击的概率?

  • 存放哈希后的密码 SHA-0 MD-5 等,但是哈希后的密码仍然可以通过对猜测值也进行哈希,并与获取到的哈希进行比对,从而获得比对成功的账号的密码,而且随着时间的推移一些哈希算法已经被破解
  • 加盐(Salting):对每个用户存一个随机的加盐值,以及存哈希后的(加盐值+密码),这样即使被获取,攻击者也只能针对一个特定用户来进行密码的破解,因为两个用户即使密码一样,他们的加盐值也不一样
  • Cookies: Strawman 登录凭证,凭此凭证可以在一定时间内登录成功,会在登录有效期内更新凭证,而登出后/超时后取消凭证的有效期;但是会造成二义性,比如用户名和时间的拼接,M22-May-2022可以解释为M 22-May-2022和M2 2-May-2022
  • 网络钓鱼(Phishing):用假冒的域名来欺骗用户输入密码,比如apple.com与αpple.com
    如何不让密码在网络中裸奔?
  1. challenge-response:服务端给客户端发送一个随机数,客户端将密码和随机数一起放入哈希函数中,再将哈希后的值传给服务端,服务端也算一遍(问题:服务端也需要存放密码明文,因为哈希的对象包含随机值,存密码明文才能算,存明文本身就有问题)
  2. turn offline into online attack:用户输入账户名之后就会返回一个图像,根据图像的正确性可以检查是否为钓鱼网站;同时可以在服务端检查某一个客户的账号会不会出现多次“用户名”的请求出现在不同的IP上,并对异常情况做响应
  3. specific password:为不同的账号创建不同的密码
  4. one-time passwords:一次性密码,比如某个密码可以使用100次,那么第一次使用可以先在本地$hash^{99}$次,然后再在服务端进行hash后进行检验;随后用于检验的hash调用层数-1,这样即使攻击者获取到了$hash^{97}$的密码,也是无用的,因为这个密码已经失效了,而且很难$hash^{-1}$
  5. 可以为不同的设备的同一个应用的账号设置不同的密码,同时可以取消单个设备的密码
  6. bind auth and request:将用户验证和用户请求绑定,每次存放请求和密码的哈希用于检验请求的可靠性(避免APP尚未息屏的时候被他人操作,即每次转账的时候还需要重新输入密码)
  7. replace the password:指纹验证实际上是将指纹和私钥绑定,私钥和服务器里的公钥绑定,验证指纹获取私钥,私钥和公钥在客户端进行一系列操作后发回给服务端,再验证合法性

Principle of Security

  • 最小权限原则
  • 最小信任原则:“只有自己写的代码才是可信任的”,写编译器的人会给自己留强破解密码,而且任何的组件都需要编译器进行编译,因此只要有了第一个编译器那么就搞不掉这个强破解密码(图灵奖演讲)
    题外话:第一个编译器是怎么来的?deepseek查阅得到是通过人写出简单机器码编译器,然后编译出简单代码之后,在通过该简单编译器进行“自举”开发出许多更加强大的编译器,后续自举出来的编译器就是用相对高级的语言编写的了

Return-Oriented Programming

stack buffer overflow

  • 改写return addr以让其跳转到在栈上其他位置编写的攻击代码以期望其ret后执行攻击代码;可以通过限制可执行区域阻止,比如不允许执行属于栈的地址位置的指令
  • 改写return addr以让其跳转到其他代码区域的部分指令,通过碎片化的跳转达到完成一整套攻击逻辑的流程(ICS lab3 final 5’);可以通过随机化栈偏移量(同时设置栈的一系列跳转为相对跳转)、在return addr处设置金丝雀值检测篡改return addr来阻止;不过fork出来的子进程是和父进程有一样的地址空间规划的,就没有达到随机化栈偏移量的作用

CFI(Control-Flow Integrity)

编译成汇编指令的时候可以得到函数之间互相调用的关系,在逻辑上可以生成控制流图;通常的指令跳转都是直接的,即跳转到0x555555555234,但是也有间接的,比如跳转到rax所存的地址;控制流图主要防止攻击者通过篡改间接跳转的寄存器从而更改跳转位置,具体实现上就采用了CFI给callee/caller之间人为地添加了约束(跳转到rax可能是调用函数指针形参,实参不固定的情况)
通过Patch提前添加比对信息,跳转方比对被跳转处的预设的数据是否符合预期,从而验证跳转的正确性;当然这存在一定问题:比如A在一个跳转语句中可能跳转到C/D,那么C/D处的验证数据就必须是一样的,因为这个A检查的是同一个数据,那么如果B能跳转到C,那么即使B在控制流图中没有与D的跳转关系,他被攻击后跳转到D也依然被认为是合法的;解决方法是增强CFI,增加条件使得不同的rax能够到达不同的jmp指令,从而使关联的粒度更细

这里还存在的问题是,假如被跳转方是有Patch的,而跳转方由于自身版本比较旧没有Patch相关指令,对应于上图的Original jmpPatched mov,那么跳转到0x12345678就会因为执行了非法指令而无法继续执行,因此需要有一个兼容的指令prefetchnta,他会忽略不是合法指令的接下来的4字节指令
指令可执行区域的发展,RISC/NX表示指令对齐,从而只有32对齐的位置才被视为合法跳转地址,而X86/CFI的跳转控制流图依赖又大大加强了这一点

Blind ROP

背景:由于Nginx使用fork来生成用于服务客户端的子进程,因此这些子进程的地址空间都是固定的,即“栈随机化”这一条失效;攻击者可以通过不断地尝试去获取到使用Nginx挂载的服务的代码,以此来攻击服务端

攻击者首先通过发送GET请求,期望让请求的字符串过长从而触发buffer overflow

一旦溢出了,那么一般来说高位的地址被更改都会直接触发SegFault导致TCP connection closed,所以可以通过不断地尝试来获取server处理HTTP请求的返回地址,甚至canaries

接下来可以通过更改低位的返回地址,不断尝试出一些能够导致服务端crash(检测到连接关闭)、hang(无任何反映)、正常返回(useful gadget)的地址,这些地址可以用来检测其他的功能

如何鉴别哪一个地址是useful gadget?可以在要检测的返回地址的更高层栈的返回地址插入已知的crash/hang返回地址,如果最终响应依赖于更高层插入的crash/hang,则可以说明当前测试的这个地址属于useful gadget,可以正常返回
现在我们要达成将服务器的机器码返回给客户端,就需要借用write系统调用,sock参数传的是要返回的客户端的socket number,buf是机器码的大致起始位置,len是要传的字符串长度大小

所以这里的任务变成了如何将参数传给rdi rsi rdx,如何拿到write的地址,如何拿实参;已知通过前面的测试可以得知机器码地址起点大概是在0x401000附近,已知sock

由于攻击者现在能做的是通过改写栈进行跳转,因此可以想出一个策略,即改写栈将参数传到栈rsp附近位置,同时操控返回地址,返回到能够进行pop rdi的指令,如此就可以神不知鬼不觉地将参数传给传参寄存器了,比如这里就需要操控栈,使得返回到一个BROP gadget:这样就可以通过pop r14: 41 5e来进行pop rsi: 5e的操作

如何找到BROP gadget?通过尝试,返回地址填入后再填入6个crash gadget,以及一个hang/crash gadget,如果最后出现的结果取决于最后一个gadget,那么就可以判定前6个已经被pop吸收了,就意味着找到了,然后就可以通过偏移量计算获取能够将栈中存的参数传给rsi rdi的返回地址

但是rdx无法通过这种方式被传递!可以求助于strcmp这个syscall,因为他会根据传入的两个字符串参数的长度来自动地设置rdx的值;因此现在问题就剩下如何找到writestrcmp了,两者都是libc的函数,他们在PTL表内的位置是相近的;那么如何找到PTL呢?
大部分的PLT项都不会因为传进来的参数的原因crash,因为它们很多都是系统调用,都会对参数进行检查,如果有错误会返回EFAULT而已,并不会造成进程crash。所以攻击者可以通过下面这个方法找到PLT:如果攻击者发现好多条连续的16个字节对齐的地址都不会造成进程crash,而且这些地址加6得到的地址也不会造成进程crash,那么很有可能这就是某个PLT对应的项了。

找到了PLT后,可以通过尝试将arg1, arg2设置为空指针并且观测函数的响应来判断这个函数是不是strcmp,因为其对于不同的参数的响应关系如下
write函数也是进行尝试,只要客户端的socket返回了一大堆二进制字符流,那么就说明碰对了write函数;获得了服务端的源码之后就可以找到execve函数,通过一系列操作让他执行execve("/bin/sh", 0, 0),并且通过重定向输入输出来实现通过客户端操控服务端的shell的效果

Data Flow Protection

攻击者可以通过钓鱼网站、安装键盘Logger来检测用户的输入,从而获取隐私数据;甚至还可以通过获取内存、屏幕监控、冷启动、手机罗盘针对于用户按键的抖动规律来拿到数据

Taint Tracking

数据可能会在被删掉之前在公共区域暴露一段时间,这段时间被称为data lifetime,理论上可跟踪,但是实际上一段内存可能会被OS随机换页到内存、或者程序出现错误之后触发core dump将内存写入SSD,从而让跟踪这段数据的生命周期变得困难;因此需要一种动态的Taint Analysis
程序员设置特定的函数get_xx能够使得接收返回值的变量被打上True Taint,在程序运行中遵循or类型的Taint传递规律,最后在特定的指令或者函数检查(send_xxx)并且拒绝执行含有True Taint变量的相关代码

Taint Analysis会在很多地方被应用,具体来说就是在数据传递的每个阶段按照规则来选择是否为变量施加True Taint标签

Defending Malicious Input

黑客如何通过可执行文件的bug操控内存,进行自定义的读写?

  • 找一个不常用,没人维护的库
  • 比如ffmpeg中的4xm格式的视频媒体文件
  • 观察源文件并且找到可以进行非常规操作的修改,比如有的文件写的不好就会出现比较不合适的强制类型转换,以及一些比较低级的判定index < 0

    header是用户传的文件头,完全可以利用规则让current_track变成负数

后面还提到TaintCheck模块,主要讲述了加上这个模块会降低传输效率,但是在较大文件情况下影响的倍数相对较少,且更安全

Secure Channel

两种基本加密模式

  • encrypt(key, message)->ciphertext decrypt(key, ciphertext)->message通过key来进行数据传输的加密和解密
  • MAC(key, message)=token 计算token的目的主要是为了验证数据是否被修改,在接收端验证

常见问题:

  • 中间截获者会通过截获并重发的方式试图假冒发送者,从而欺骗接收方做出不应该做出的响应
  • 加上了序列号可以防止类proxy的重复转发,但是问题是截获者可以将信息转发给发送者,因为目前两者发送的信息是对称的,而且响应方发送相同的seq number与截获方发送发送方的seq number也符合信息检验的要求
  • 通信双方均持有密钥,同时自己仅使用自身密钥进行解密,如下图,攻击者很难通过得知真正的密钥ab,这在数论中是被认为极难算出的,于是就可以通过这种方式识别对面是不是合法的发送方了(吗?)问题在于传递密钥的时候怎么知道对面真的是目标方,而不是别有用心者?如下图,该机制也可以正常运行,在交换密钥的时候双方并不知道中间隔了一个人!
  • 私钥和公钥,公钥是公开在网上的,私钥是内部保存的,传输数据之前先用私钥签名再发送出去,解析数据的时候通过对面的公钥解析是否是期望的发送方发来的消息,那么攻击者由于拿不到私钥,因此就无法让自己发送的东西被公钥解析出来
  • 问题:如何拿到公钥?
    • 自己存每个机构的公钥:不确保时效性
    • 让特定非盈利机构保存所有机构的公钥,用户在初始安装的浏览器中有这些权威机构的公钥,不过这样子会让这些权威机构的服务器承担很大的负载,因为似乎信息来源只有他们
    • 访问网站的时候,网站服务提供方先给用户发一个公钥,以及权威机构的签名,用户可以去向权威机构验证公钥是否合法

Privacy

OT(Oblivious Transfer)

建立在一个前提:需求方不想让发送方知道自己想要什么,发送方也不希望让需求方得知额外的信息;发送方公开自己持有的所有信息的公钥,需求方根据自己的需要选择想要哪个公钥对应的数据来进行对随机数r的加密,而后发送方对于c进行解密,生成异或码;只有想要的数据才能被需求方翻译成真正的信息

Secret Sharing

给每个part分别分配一部分的密钥,使得当剩余0.6的part还存活的时候仍能从剩余的part中获取密钥;原本只打算给每个人分配精准的$\frac{1}{n}$的密钥,但是这会带来“一旦有一个人缺席,那么这个密钥就无法被拼凑出”的问题

Homomorphic Encryption

用户希望提交信息交给云服务商进行处理,但是不想发明文;因此它采用了发送解码函数以及CT明文的方式,而服务端使用类似的方式

TEE

设备厂商能够完成在内存中存明文,只在CPU cache中存放密文的功能