积分1636 / 贡献20

提问18答案被采纳66文章40

[经验分享] CFI 原理和应用浅析 原创 精华

深开鸿_王石 显示全部楼层 发表于 2024-9-18 09:46:32
控制攻击简介

控制流劫持是一种危害性极大的攻击方式,攻击者能够通过它来获取目标机器的控制权,甚至进行提权操作,对目标机器进行全面控制。当攻击者掌握了被攻击程序的内存错误漏洞后,一般会考虑发起控制流劫持攻击。

  • shellcode 早期的攻击通常采用代码注入(shellcode)的方式,通过上载一段代码,将控制转向这段代码执行。任务包括:在溢出数据内包含一段攻击指令,用攻击指令的起始地址覆盖掉返回地址。攻击指令一般都是用来打开 shell,从而可以获得当前进程的控制权,所以这类指令片段也被成为“shellcode”。shellcode 可以用汇编语言来写再转成对应的机器码,也可以上网搜索直接复制粘贴,这里就不再赘述。下面我们先写出溢出数据的组成,再确定对应的各部分填充进去。

    shellcode.png

    “\x90”这个机器码对应的指令是 NOP (No Operation),也就是告诉 CPU 什么也不做,然后跳到下一条指令。有了这一段 NOP 的填充,只要返回地址能够命中这一段中的任意位置,都可以无副作用地跳转到 shellcode 的起始处,所以这种方法被称为 NOP Sled(中文含义是“滑雪橇”)。这样我们就可以通过增加 NOP 填充来配合试验 shellcode 起始地址。这种方法生效的一个前提是在函数调用栈上的数据(shellcode)要有可执行的权限(另一个前提是关闭内存布局随机化)。很多时候操作系统会关闭函数调用栈的可执行权限,这样 shellcode 的方法就失效了为了阻止这类攻击,因此后来的计算机系统中都基本上都部署了NX/DEP(Data Execution Prevention)机制,通过限定内存页不能同时具备写权限和执行权限,来阻止攻击者所上载的代码的执行。

  • Return2libc/ROP 为了突破DEP的防御,攻击者又探索出了代码重用攻击方式,他们利用被攻击程序中的代码片段,进行拼接以形成攻击逻辑。代码重用攻击包括Return-to-libc、ROP(Return Oriented Programming)、JOP(Jump Oriented Programming)等。研究表明,当被攻击程序的代码量达到一定规模后,一般能够从被攻击程序中找到图灵完备的代码片段。 Return2libc(修改返回地址,让其指向内存中已有的某个函数)。步骤包括:在内存中确定某个函数的地址,并用其覆盖掉返回地址。由于 libc 动态链接库中的函数被广泛使用,所以有很大概率可以在内存中找到该动态库。同时由于该库包含了一些系统级的函数(例如 system() 等),所以通常使用这些系统级函数来获得当前进程的控制权。鉴于要执行的函数可能需要参数,比如调用 system() 函数打开 shell 的完整形式为 system(“/bin/sh”) ,所以溢出数据也要包括必要的参数。下面就以执行 system(“/bin/sh”) 为例,先写出溢出数据的组成,再确定对应的各部分填充进去。

    return2libc.png

    padding1 处的数据可以随意填充(注意不要包含 “\x00” ,否则向程序传入溢出数据时会造成截断),长度应该刚好覆盖函数的基地址。address of system() 是 system() 在内存中的地址,用来覆盖返回地址。padding2 处的数据长度为4字节,对应调用 system() 时的返回地址。因为实验代码只需要打开 shell,并不关心 shell 退出后的行为,所以 padding2 的内容可以随意填充。address of “/bin/sh” 是字符串 “/bin/sh” 在内存中的地址,作为传给 system() 的参数。 ROP(修改返回地址,让其指向内存中已有的指令)。步骤包括:在内存中确定某段指令的地址,因为有时目标函数在内存内无法找到或没有特定函数完美适配,所以这时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的简称gadget。如果想连续执行若干段指令,就需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget 的最后一步应该是 RET 指令,这样程序的控制权(eip)才能得到切换,所以这种技术被称为返回导向编程( Return Oriented Programming )。要执行多个 gadget,溢出数据应该以下面的方式构造:

    rop.png

    在这样的构造下,被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(也就是 gadget 2 的地址)弹出至 eip,程序继续跳转执行 gadget 2,以此类推。Return2libc/ROP利用return间接访问,绕过了NX/DEP访问。因为代码并不会直接在堆栈上执行,而只是根据堆栈中的地址,间接跳转到对应正常代码段执行。

  • DOP DOP(Data Oriented Programming)攻击。随着防护技术的发展,针对控制流的攻击变得愈发困难。而不通过劫持控制流,而是针对数据流来进行攻击的方式,如Non-control data(非控制数据)攻击虽然显示出了其潜在的危害性,但目前对针对数据流的攻击还知之甚少,长久以来该攻击手段可实现的攻击目标一直被认为是有限的。实际上,非控制数据攻击可以是图灵完备的,这就是DOP攻击。 类似于ROP,DOP攻击的实现也依赖于gadgets。但二者有以下两点不同: 1、DOP的gadgets只能使用内存来传递操作的结果,而ROP的gadgets可以使用寄存器。 2、DOP的gadgets必须符合控制流图(CFG),不能发生非法的控制流转移,而且无需一个接一个的执行。而ROP的gadgets必须成链,顺序执行。

  • CFI 为了应对这些新型的控制流劫持攻击,加州大学和微软公司于2005年提出了控制流完整性(Control Flow Integrity, CFI)的防御机制。其核心思想是限制程序运行中的控制转移,使之始终处于原有的控制流图所限定的范围内。具体做法是通过分析程序的控制流图,获取间接转移指令(包括间接跳转、间接调用、和函数返回指令)目标的白名单,并在运行过程中,核对间接转移指令的目标是否在白名单中。控制流劫持攻击往往会违背原有的控制流图,CFI使得这种攻击行为难以实现,从而保障软件系统的安全。 CFI从实现角度上,被分为细粒度和粗粒度两种。细粒度CFI严格控制每一个间接转移指令的转移目标,这种精细的检查,在现有的系统环境中,通常会引入很大的开销。而粗粒度CFI则是将一组类似或相近类型的目标归到一起进行检查,以降低开销,但这种方法会导致安全性的下降。

CFI 基本概念

了解CFI(Control-Flow Integrity),需要从CFG(Control-Flow Graph)讲起。这里的CFG是基于静态分析的用图的方式表达程序的执行路径(函数级别,非指令级别?)。CFI并不会检测CFG中所有的边,为了降低开销受检测的边应该越少越好。因此在CFG中只考虑将可能受到攻击的间接call、间接jmp和ret指令作为边。

cfi.png

如上图,绿色的静态控制流路径并不易受到攻击,红色的动态控制流路径容易受到攻击。静态路径就是直接跳转,动态路径就是间接跳转的路径。

  • 直接跳转和间接跳转 直接跳转指令的示例如下所示:

    1| CALL 0x1060000F

    在程序执行到这条语句时,就会将指令寄存器的值替换为0x1060000F。这种在指令中直接给出跳转地址的寻址方式就叫做直接转移。在高级语言中, 像if-else,静态函数调用这种跳转目标往往可以确定的语句就会被转换为直接跳转指令。

    间接跳转指令则是使用数据寻址方式间接的指出转移地址,比如:

    1| JMP EBX

    执行完这条指令之后, 指令寄存器的值就被替换为EBX寄存器的值。它的转换对象为作为回调参数的函数指针等动态决定目标地址的语句。

  • 前向转移(forward)和后向转移(backward)

    image.png

    将控制权定向到程序中一个新位置的转移方式, 就叫做前向转移, 比如jmp和call指令。 而将控制权返回到先前位置的就叫做后向转移, 最常见的就是ret指令。 将以上两种分类方式结合起来: 前向转移指令call和jmp根据寻址方式不同, 又可以分为直接jmp, 间接jmp,直接call,间接call四种。 后向转移指令ret没有操作数,它的目标地址计算是通过从栈中弹出的数来决定的。正因为ret指令的特性,引发了一系列针对返回地址的攻击。 CFI(Control-Flow Integrity)关注的就是间接jmp、间接call、ret这几种指令控制流的完整性。

原理简析

参考:https://users.soe.ucsc.edu/~abadi/Papers/cfi-tissec-revised.pdf

CFI技术来源于上述文章。技术思想在于**间接jmp间接callret**这几种指令的控制流中插桩,在间接跳转之前判断跳转地址是否合法。

cfiins.png

用上图来解释,利用左侧的代码生成了右侧的CFG控制流图。其中的直接call路径是不用关注的,针对间接call和ret指令的控制流路径,插入代码进行判断:

1、在间接call和ret的目标地址插入一个独有的label id。

2、在间接call和ret指令之前插入一段桩代码,来检查目的地址的id是否合法。合法才能间接跳转,不合法则出错返回。

3、如果指向两个目标地址的边拥有相同的源集合,这两个目标地址为等价地址,等价目标用相同label表示。所以我们看到两个相同的label 55和两个相同的label 17。这就是一种粗粒度的CFI,它将多个不同的目标地址合在一起减少需要检测目标地址的数量。为了降低性能开销,是以牺牲安全性为前提的。

下图是上述理论在x86上的一个具体实现:

x86cfi.png

● 原始状态:ecx保存了目的地址,jmp ecx间接跳转到目的地址执行

● 插桩方式(a):首先在目的地址插入一个4字节ID 12345678h,然后在jmp跳转前插入一段桩函数判断,判断目的地址的值是否为12345678h。不合法则出错处理,合法则间接跳转到[ecx + 4]地址执行原来的目的指令。

● 插桩方式(b):在方式(a)的基础上做了优化,首先在目的地址插入一个4字节的lable指令prefetchnta + 4字节ID 12345678h,然后在jmp跳转前插入一段桩函数判断,判断[ecx + 4]地址的值是否为12345678h。不合法则出错处理,合法则间接跳转到[ecx]地址执行label ID指令。注意这里的技巧是判断合法后,还是跳转到ecx原地址,但是这时这个地址上存储的是label ID指令,这条命令没啥副作用,紧接着才会继续执行原有的命令。

下图是间接jmp、ret指令路径,都被cfi插桩的情况:

x86ret.png

CFI确保运行时执行沿着给定的CFG进行,例如,保证典型功能的执行始终从头开始,并从头到尾进行。因此,CFI可以提高任何基于CFG的技术的可靠性(例如,增强现有技术以防止缓冲区溢出和入侵检测[32,58])。下面介绍基于CFI的其他应用,内联参考监视器IRM(Inlined Reference Monitors)、SFI(Software Fault Isolation)、软件内存访问控制SMAC(Software Memory Access Control),我们在此介绍它们。它还显示了如何依靠SMAC或标准x86硬件支持来加强CFI实施。

实现方法

参考:https://clang.llvm.org/docs/ControlFlowIntegrity.html

  1. 在OpenHarmony里,我们通过Clang的接口来完成CFI的配置,比如:
Available schemes are:
  -fsanitize=cfi-cast-strict: Enables strict cast checks.
  -fsanitize=cfi-derived-cast: Base-to-derived cast to the wrong dynamic type.
  -fsanitize=cfi-unrelated-cast: Cast from void* or another unrelated type to the wrong dynamic type.
  -fsanitize=cfi-nvcall: Non-virtual call via an object whose vptr is of the wrong dynamic type.
  -fsanitize=cfi-vcall: Virtual call via an object whose vptr is of the wrong dynamic type.
  -fsanitize=cfi-icall: Indirect call of a function with wrong dynamic type.
  -fsanitize=cfi-mfcall: Indirect call via a member function pointer with wrong dynamic type.
  1. 对应的OpenHarmony的代码就在://build/config/sanitizers/sanitizers.gni,和 //build/config/sanitizers/BUILD.gn 路径下。最后被 //build/ohos.gni 引用
//ohos.gni
import("//build/config/sanitizers/sanitizers.gni")
import("//build/ohos/ndk/ndk.gni")
import("//build/ohos/notice/notice.gni")
import("//build/ohos/sa_profile/sa_profile.gni")
import("//build/ohos_var.gni")
import("//build/toolchain/toolchain.gni")
  1. 最后在对应的模块里声明配置
如//foundation/communication/wifi/wifi/frameworks/js/napi/BUILD.gn

import("//build/ohos.gni")
import("//foundation/communication/wifi/wifi/wifi.gni")

ohos_shared_library("wifi") {
  branch_protector_ret = "pac_ret"

  sanitize = {
    cfi = true  # Enable/disable control flow integrity detection
    boundary_sanitize = true  # Enable boundary san detection
    cfi_cross_dso = true  # Cross-SO CFI Checks
    integer_overflow = true  # Enable integer overflow detection
    ubsan = true  # Enable some Ubsan options
    debug = false
  }
调试

默认OpenHarmony里的模块是要求开CFI检查的,但是有些函数是不需要检查的,所以下面介绍几种方法,规避CFI检查:

  1. 自定义宏,加到函数声明的地方,如下:

    // foundation/communication/wifi/wifi/frameworks/js/napi/inc/wifi_napi_utils.h
    #ifndef NO_SANITIZE
    #ifdef __has_attribute
    #if __has_attribute(no_sanitize)
    #define NO_SANITIZE(type) __attribute__((no_sanitize(type)))
    #endif
    #endif
    #endif
    
    // foundation/communication/wifi/wifi/frameworks/js/napi/src/wifi_napi_device.cpp
    NO_SANITIZE("cfi") napi_value EnableWifi(napi_env env, napi_callback_info info)
    {
        TRACE_FUNC_CALL;
        WIFI_NAPI_ASSERT(env, wifiDevicePtr != nullptr, WIFI_OPT_FAILED, SYSCAP_WIFI_STA);
        ErrCode ret = wifiDevicePtr->EnableWifi();
        WIFI_NAPI_RETURN(env, ret == WIFI_OPT_SUCCESS, ret, SYSCAP_WIFI_STA);
    }
  2. 在定义里增加no_trap,方便调试,不至于程序直接奔溃

    By default, CFI will abort the program immediately upon detecting a control flow integrity violation. You can use the -fno-sanitize-trap= flag to cause CFI to print a diagnostic similar to the one below before the program aborts.
    
    If diagnostics are enabled, you can also configure CFI to continue program execution instead of aborting by using the -fsanitize-recover= flag.
    
    对应的OpenHarmony代码就是:cfi_config_debug
  3. 增加blocklist,规避不想检查的文件

    如 // foundation/communication/netmanager_ext/services/vpnmanager/BUILD.gn
    
    ohos_static_library("net_vpn_manager_static") {
      sanitize = {
        cfi = true
        cfi_cross_dso = true
        blocklist = "./cfi_blocklist.txt"
        debug = false
      }

总结

  1. CFI 保证程序调用链完整不被修改
  2. CFI 通过在编译时加标签,保证运行时代码跳转不被修改
  3. 在OpenHarmony 5.0编译配置里,CFI默认打开。如果自己增加子系统或者部件,需要注意CFI的配置

©著作权归作者所有,转载或内容合作请联系作者

您尚未登录,无法参与评论,登录后可以:
参与开源共建问题交流
认同或收藏高质量问答
获取积分成为开源共建先驱

Copyright   ©2023  OpenHarmony开发者论坛  京ICP备2020036654号-3 |技术支持 Discuz!

返回顶部