Kelp: 通过Regional Pointer Information增强类型匹配的间接调用分析方法

1.Introduction

这篇文章主要通过简单数据流分析增强基于类型匹配的间接调用分析方法。图1示例中,在分析13行和14行的indirect-call的callee时。基于类型分析的方法mlta会报出2个call语句都会调用 checked_printunchecked_print 函数,造成误报。

但作者观察到函数指针相比其它变量,其数据流通常简单的多。比如17行的 b1,其定义了一个函数指针后直接被当作实参传入 process_input 随后直接在13行被调用,作者将其称为simple function pointer。与之相比 b2 的数据流复杂些,经过了24行 *p = b 和18行 b = *p 两次赋值。

请添加图片描述作者对linux kernel的emprical study发现34.5%的indirect-call属于 b1 这一类simple function pointer。这部分indirect-call唯一调用(其它非simple function pointer不会调用)了23.9%的address-taken function。

因此作者提出了Kelp工具,在对上面示例分析时,kelp首先通过simple def-use分析确定simple function pointer b1 的target只有 checked_print。随后对 b2 分析时首先用类型分析确定target包括 checked_printunchecked_print。但是 checked_print 被simple function pointer调用了因此 b2 的target只有 unchecked_print

Kelp的优势包括:

  • 相比mlta,Kelp多识别出了396个uniquely-resolved indirect calls。target size减少了54.2%。在18个程序的分析中平均callee size减少了20%。

  • 相比mlta只带来了额外8.5%的时间开销(大约6.3秒)。分析linux大约花了11分钟,分析Firefox花了4分钟。

  • 辅助hread-sharing analysis时减少了25.3%的spurious thread-shared load/store memory accesses。

  • 辅助value-flow analysis时减少了Saber 17.1%的误报。

  • 辅助grey-box fuzzing时,在复现10个CVE减少了51.9%的时间开销以及 46.9%的无关程序执行。

2.Kelp in a Nutshell

address-taken function通常指的是会被当作间接调用target的函数,通常这类函数需要将其地址赋值给函数指针。

Definition 1. 当一个函数指针没有被其他指针引用,并且不通过解引用其他指针来获取其值时,它被称为simple function pointer。比如图1中,b 被指针 p 引用了,同时 b2 又解引用了 p 的值,那么它就不是simple function pointer。

Definition 2. 当一个address-taken function只能被simple function pointer引用时,作者将其称为confined function。在图1中,Kelp首先通过类型分析计算 b2 的target set为 checked_print, unchecked_print ,但是 checked_print 为confined function,因此不能被 b2 调用。

3.A Characteristic Empirical Study

作者对Linux kernel v5.15做了一个实证研究,研究问题包括:

  • Q1: 使用simple function pointer常见吗?

  • Q2: address-taken function只通过简单的simple function pointer调用是常见的吗?

Q1:结果表明,87355个icall中34.5%属于simple function pointer,通常simple function pointer包括全局变量和局部变量,值在声明时就已经确定了。图3中 dec_inflight, inc_inflight_move_tail, inc_inflight 被赋值给simple function pointer func,随后又被作为参数传递给 scan_inflight。而随后 func 没有参与其它赋值操作。

Q2:23.9%的fuction只被simple function pointer调用。90127个address-taken function中,76.3%的只被取地址一次,也就是只有1个address-taken site。在图3中,dec_inflight, inc_inflight_move_tail, inc_inflight 分别只在4、7、11行被取地址。也就是只有20行的 func 可以调用它们。

请添加图片描述

4.Algorithm Design

4.1.Preliminaries

Assumption: 作者假定分析的程序不需要考虑第三方库调用,所有的target均在分析scope范围内。

Program Abstraction: Kelp基于LLVM实现,分析的目标语言是LLVM IR。作者还会使用mem2reg来简化目标程序的LLVM IR。程序的变量可分为top-level variable和address-taken variable。前者主要是寄存器变量,后者对应内存对象。

Points-to Relations: 指针v在语句s处的指向集合被表示为 p t ( s , v ) = { o ∣ o ∈ O } pt(s, v) = \{o|o \in O\} pt(s,v)={ooO}。例如,在图5(a)中,在s2处 p t ( s 2 , p ) = { o 1 } pt(s_2, p) = \{o_1\} pt(s2,p)={o1}。为了解析s20和s21处的间接调用,Kelp需要通过识别和追踪它们的def-use链来计算 p t ( s 20 , b 1 ) pt(s_{20}, b_1) pt(s20,b1) p t ( s 21 , b 2 ) pt(s_{21}, b_2) pt(s21,b2),如图5(b)所示。

请添加图片描述
direct value flow edge s 1 → v s 2 s_1 \xrightarrow{v} s_2 s1v s2,表示变量 v v v 在语句 s 1 s_1 s1 处被定义, s 2 s_2 s2 处被使用。 通常表示top-level variable的数据流,通常直接用LLVM就可以,不需要指针分析。

indirect value-flow edge s 1 → o s 2 s_1 \xrightarrow{o} s_2 s1o s2 s 1 s_1 s1store s 2 s_2 s2storeload) 表示address-taken variable o o o 的value flow,通常需要先进行指针分析。

图5(b)展示了从在 s 8 s_8 s8 处定义的变量 b 1 b_1 b1 s 20 s_{20} s20 处使用的 b 1 b_1 b1 的def-use链,以及从在 s 3 s_3 s3 处定义的变量 b b b s 21 s_{21} s21 处使用的 b 2 b_2 b2 的def-use链,这些链将被Kelp以反向方式跟踪。例如, s 18 → b 2 s 20 s_{18} \xrightarrow{b_2} s_{20} s18b2 s20 为direct-value flow edge,而 s 4 → o 1 s 9 s_{4} \xrightarrow{o_1} s_{9} s4o1 s9 为indirect value flow edge。灰色部分表示KELP不会反向跟踪相应的链,因为 b2 的值沿indirect value flow传播。

4.2.Resolving Simple Function Pointers through Efficient Def-Use Tracking

( s , p ) ← ( s 1 , q ) (s, p) \leftarrow (s_1, q) (s,p)(s1,q) 表示一个从 ( s 1 , q ) (s_1, q) (s1,q) ( s , p ) (s, p) (s,p) 的def-use关系。图6表示backward分析规则。FuncSite 表示函数指针初始化操作 p = &func。backward分析的起点是indirect-callsite,目标是找到从indirect-callsite到FuncSite的direct value flow。

请添加图片描述
对于图5(b)中示例,通过copy规则可以分析出data flow ( s 20 , b 1 ) ← ( s 18 , b 1 ) ← ( s 10 , b 1 ) ← ( s 8 , b 1 ) (s_{20}, b_1) \leftarrow (s_{18}, b_1) \leftarrow (s_{10}, b_1) \leftarrow (s_8, b_1) (s20,b1)(s18,b1)(s10,b1)(s8,b1)。而涉及到 storeload 的indirect value flow,Kelp直接停止分析。

除backward分析,还有一个forward分析步骤,目标是确定indirect-call对于的函数指针是否被赋值给其它变量,从而变成complex data flow。

在backward分析时,对全局变量的跟踪与局部变量有所不同。具体来说,对于可以被多次赋值(而不是通过显式参数和返回值传递)的全局变量的def-use链进行流敏感跟踪是具有挑战性的,因为控制流是未知的。因此,简单的全局变量是以流不敏感方式处理的。具体来说,在backward分析间接调用并将函数指针识别为简单全局变量时,def-use分析还全局收集了在不同区域对全局变量可能写入的所有可能值作为潜在的被调用者。作者发现大多数函数指针的全局变量只被初始化一次,可能是因为在大型系统中在不同模块中对全局变量进行多次写入可能容易出错,因此通常会避免这种情况。

4.3.Resolving Complex Function Pointer through Precise Type Analysis

作者使用 ConFunc 来表示confined address-taken function,使用 CandiFunc 来表示类型检查的候选函数。前一步Kelp可以分析出simple function pointer对应的target set,同时分析出了 ConFunc。算法1中的 IdentifyConfinedATFunc() 方法描述了捕获函数 ConFunc 的两个关键步骤。

请添加图片描述
在图5(b),checked_print 只被获取一次地址,其地址获取位置 s 8 s_8 s8 通过 b 1 b_1 b1 的def-use到达 s 20 s_{20} s20。因此,checked_print 只能在 s 20 s_{20} s20 处被调用。因此,在 s 21 s_{21} s21 处的间接调用不能通过访问另一个指针 b 2 b_2 b2 调用 checked_print

识别 ConFunc 后,剩余indirect-call的target set即 CandiFunc - ConFunc

5.Implementation

Kelp基于LLVM实现。

Promoting Memory to Register。LLVM IR可能会对非常简单和常见的操作产生过多的栈操作,这可能会降低def-use分析的性能。为了解决这个问题,作者使用mem2reg优化pass,它将内存引用转换为寄存器引用,并在必要时引入 Phi 节点。该pass会提升仅用于加载和存储的 alloca 指令。

Safe Fallback Strategy。作者默认被分析的代码是自包含的,这是一种标准做法。否则,大多数先前的类型和指针分析技术可能会忽略间接调用的目标。此外,当simple function pointer的def-use链无法完全收集时,使用Kelp可能会产生新的false negative。

  • 首先,def-use遍历可能会遇到未知的控制流或代码,例如动态链接库、间接调用和汇编代码。需要注意的是,这种情况在real-world中并不常见,因为直观地说,简单栈变量使用不会跨越复杂的控制流。

  • 第二,全局变量的处理也可能会产生问题。全局变量的def-use链以一种流不敏感和保守的方式收集。当一个初始化的全局变量被重新用某处通过解引用其他指针获得的值赋值时,Kelp很难完全收集全局变量的间接def-use链。需要注意的是,这种情况也不常见,因为作者观察到大多数全局变量都很简单,并且只被初始化一次,之后不再修改。

为了缓解这两个问题,作者在Kelp中设计了一种自动回退策略。具体来说,对于这两种特殊情况,Kelp将类型分析的结果用做正在分析的indirect-call的备用callee targets。因此,作者的方法不会给类型分析引入额外的false negative。

6.Evaluation

  • 6.1.与类型分析对比,Kelp效果如何?

  • 6.2.对下游任务提升如何?

6.1.Advancing Pure Type Analysis

baseline为mlta,benchmark如表1所示。作者使用3个指标评估precision:

  • single-callee indirect calls数量。

  • 所有indirect-call平均callee数量。

  • 最大callee size。

请添加图片描述

如表1所示,Kelp在任何指标上都比mlta更精确。与mlta相比,平均而言,Kelp可以额外增加396个single-callee间接调用,将平均callee set大小减少54.2%,并从具有最大调用目标数量的间接调用中移除94个错误的调用目标。此外,对于18个项目,Kelp可以将平均调用者大小大幅减少超过20%。最多时,Kelp可以在pjsip中剪掉82.8%的虚假间接调用目标。

6.2.Enhancing Downstream Clients

为了选择这些下游客户端的工具,作者

  • 首先在SVF的MTA模块中选择了FSAM作为线程共享分析工具,用于识别访问线程共享内存的加载和存储操作。

  • 其次,作者在SVF中选择了Saber作为检查souce-sink属性的漏洞检测工具。具体而言,作者使用了SABER中的三个内置检查器,即memory leak checker, file leak checker, double free checker。

  • 第三,作者使用Beacon作为模糊测试工具,用于漏洞复现的情景。

在作者专门对Kelp产生的调用图以及纯类型分析进行评估时,作者使用了两种不同的间接调用结果集来构建调用图。作者的检查了每个工具在利用不同精度的调用图时效果的改善程度。

6.2.1.Thread-Sharing Analysis

线程共享分析(TSA)确定语句是否可以读取或写入线程共享数据,这对理解和调试并发程序,以及并发 bug 检测至关重要。作者选择了表1中使用 Pthread API 的八个软件程序作为benchmark。为了对大型代码库执行TSA,作者选择了SVF 的WPA模块中的Steensgaard指针分析。作者通过使用Kelp和mlta分别生成的调用图来评估 FSAM 中虚假线程共享语句的减少情况。如图8所示,通过使用 Kelp 的调用图,平均可以额外删除 25.3% 的虚假线程共享语句。如表1所示,精度的提高是由于 Kelp 进一步删除了55.5%的虚假间接调用目标,而在构建调用图时额外增加了一些时间成本(9.2%)。综上所述,Kelp 在帮助线程共享分析方面非常有效。

请添加图片描述

6.2.2.Source-Sink Value-Flow Bug Detection

如图2所示,Kelp可以帮助SaberR进一步减少17.1%的假阳性。如表1所示,精度的提高是由于Kelp进一步删除了54.9%的虚假间接调用目标,而在构建调用图时额外增加了一些时间成本(6.2%)。

请添加图片描述

6.2.3.Directed Grey-Box Fuzzing

最近的灰盒Fuzz工作,Beacon,通过剪除无法到达目标代码的不相关路径,加速了漏洞重现过程。Beacon通过插桩 "asserts" 来指示不相关路径。因此,与目标代码不相关的执行在模糊测试期间会提前停止。总的来说,更精确的调用图带来更精确的CFG,因此,路径剪枝更为精准。因此,模糊器可以探索更少的不可行程序路径,从而提高了重现效率。

作者采用了Beacon论文中的benchmark,选取了受到间接调用影响的10个indirect-call。

请添加图片描述
结果如表3所示,平均而言,Kelp可以帮助Beacon进一步减少51.9%的时间成本(Tstat + Tfuzzing)和46.9%的执行次数(#Executions)。具体而言,通过使用Kelp生成的调用图来重现10个CVE,Beacon只需额外付出几秒钟(每个项目少于十秒)的Tstat,从而在整个时间成本Tall中节省了16.618小时和6738.9万次的执行次数。如表3所示,效率的提高是由于Kelp额外减少了49.3%的虚假间接调用目标。总之,Kelp的调用图在帮助有向模糊测试方面非常有效。

6.3.Discussion

6.3.1.Constant Propagation

常量传播是编译器优化和静态分析中使用的一种技术,用于确定并传播程序中的常量值。它可以通过捕获函数指针变量的常量值来处理single callee间接调用。作者进行了一个实验,比较了由常量传播支持的Kelp和mlta。作者对20个程序的结果显示,Kelp比使用常量传播的mlta更精确,提高了45.7%,而额外的开销仅约为4秒。总之,常量值传播对于处理许多简单的间接调用并不有效,因为由于函数指针的动态性,这些调用可能跨越不同的调用上下文具有多个被调用者

6.3.2.Ablation Study

接下来,作者对Kelp的每个阶段进行消融分析,以评估通过对simple function pointer和confined function带来的精度增强。精度改善是使用平均callee set大小作为度量标准来衡量的。结果显示,仅使用simple function pointer时,Kelp只能将mlta的改善幅度提高32.7%。仅使用confined function时,Kelp只能将mlta的改善幅度提高23.2%。总之,与mlta相比,Kelp同时使用两个阶段可以实现更高水平的精度。

6.3.3.False-Negative Analysis

作者用动态测试的方法收集indirect-call运行时信息分析是否有漏报,发现漏报全是由于类型分析没有考虑原始类型的隐式转换(例如,从 long intchar * 的转换)。

7.Testcase分析

因为作者并没有开源实现,所以对于Kelp具体实现我也有些好奇,先放一些简单测试用例。

首先是关于simple function pointer的定义,因为原文貌似没有给出识别simple function pointer的具体伪代码,所以我个人分析了一些project之后,发现下面几种模式的simple function pointer(可能不全):

  • Type1.直接通过函数调用链传递函数指针

  • Type2.定义局部变量,初始化并直接调用

  • Type3.定义全局变量,只初始化并直接调用

需要注意的是LLVM是SSA形式,因此没有copy指令,clang编译的时候默认会把所有的赋值操作(copy 关系)都用 storeload 替换,如果不用mem2reg pass那么所有的赋值操作都是 storeload,这个时候Kelp几乎没有作用。因此在编译的时候注意

  • 编译命令:clang -emit-llvm -fno-discard-value-names -Xclang -disable-O0-optnone -Xclang -no-opaque-pointers -S test.c,这是编译简单testcase的命令

    • -S 是为了dump出可读IR,如果是做静态分析需要原bc文件,记得把 -S 替换为 -c

    • -fno-discard-value-names 是为了保留变量名,增加可读性。

    • -Xclang -disable-O0-optnone 是禁用O0优化,参考stackoverflow,不禁用的话mem2reg就没法起作用。

    • -Xclang -no-opaque-pointers 是为了禁用模糊指针,clang15开始LLVM默认开启模糊指针,所有的指针类型编译后都不知道原类型,这对MLTA等静态类型分析方法很不利,最好禁用。

  • 优化命令:opt -S --mem2reg test.ll -o test_.ll-S 是加载可读文本模式的IR,如果是优化bc文件可以不添加或者替换为 -c

7.1.Type1

// testcase1
typedef void io_callback_t(void *context);

void callback_func(void *context) {
    int a = 1;
}

void process_with_context(io_callback_t callback, void *context) {
    callback(context);
}

int main() {
    void *data = 0;
    io_callback_t* ctx = &callback_func;
    process_with_context(ctx, data);
    return 0;
}

mem2reg优化前的IR,可以看到 callback(context); 编译后也包括了 storeload,基本无法分析。

; Function Attrs: noinline nounwind ssp uwtable
define void @callback_func(i8* noundef %context) #0 {
entry:
  %context.addr = alloca i8*, align 8
  %a = alloca i32, align 4
  store i8* %context, i8** %context.addr, align 8
  store i32 1, i32* %a, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define void @process_with_context(void (i8*)* noundef %callback, i8* noundef %context) #0 {
entry:
  %callback.addr = alloca void (i8*)*, align 8
  %context.addr = alloca i8*, align 8
  store void (i8*)* %callback, void (i8*)** %callback.addr, align 8
  store i8* %context, i8** %context.addr, align 8
  %0 = load void (i8*)*, void (i8*)** %callback.addr, align 8
  %1 = load i8*, i8** %context.addr, align 8
  call void %0(i8* noundef %1)
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %data = alloca i8*, align 8
  %ctx = alloca void (i8*)*, align 8
  store i32 0, i32* %retval, align 4
  store i8* null, i8** %data, align 8
  store void (i8*)* @callback_func, void (i8*)** %ctx, align 8
  %0 = load void (i8*)*, void (i8*)** %ctx, align 8
  %1 = load i8*, i8** %data, align 8
  call void @process_with_context(void (i8*)* noundef %0, i8* noundef %1)
  ret i32 0
}

mem2reg优化后的,可以看到多余的 storeload 都被删除了。

; Function Attrs: noinline nounwind ssp uwtable
define void @callback_func(i8* noundef %context) #0 {
entry:
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define void @process_with_context(void (i8*)* noundef %callback, i8* noundef %context) #0 {
entry:
  call void %callback(i8* noundef %context)
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  call void @process_with_context(void (i8*)* noundef @callback_func, i8* noundef null)
  ret i32 0
}

7.2.Type2

7.2.1.case1

void func(int* val) {
    *val += 1;
}

int main() {
    int var = 0;
    int* val = &var;
    void (*fp)(int*);
    fp = func;

    fp(val);
    return 0;
}

mem2reg优化前的,可以看到 fp 调用前经历了 store void (i32*)* @func, void (i32*)** %fp, align 8%0 = load void (i32*)*, void (i32*)** %fp, align 8。同样没法分析出来。

; Function Attrs: noinline nounwind ssp uwtable
define void @func(i32* noundef %val) #0 {
entry:
  %val.addr = alloca i32*, align 8
  store i32* %val, i32** %val.addr, align 8
  %0 = load i32*, i32** %val.addr, align 8
  %1 = load i32, i32* %0, align 4
  %add = add nsw i32 %1, 1
  store i32 %add, i32* %0, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  %retval = alloca i32, align 4
  %var = alloca i32, align 4
  %val = alloca i32*, align 8
  %fp = alloca void (i32*)*, align 8
  store i32 0, i32* %retval, align 4
  store i32 0, i32* %var, align 4
  store i32* %var, i32** %val, align 8
  store void (i32*)* @func, void (i32*)** %fp, align 8
  %0 = load void (i32*)*, void (i32*)** %fp, align 8
  %1 = load i32*, i32** %val, align 8
  call void %0(i32* noundef %1)
  ret i32 0
}

mem2reg优化后的。好家伙,直接替换成direct call了。看来testcase不够复杂。

; Function Attrs: noinline nounwind ssp uwtable
define void @func(i32* noundef %val) #0 {
entry:
  %0 = load i32, i32* %val, align 4
  %add = add nsw i32 %0, 1
  store i32 %add, i32* %val, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  %var = alloca i32, align 4
  store i32 0, i32* %var, align 4
  call void @func(i32* noundef %var)
  ret i32 0
}

7.2.2.case2

void func1(int* val) {
    *val += 1;
}

void func2(int* val) {
    *val += 2;
}

int main(int argc, char** argv) {
    int var = 0;
    int* val = &var;
    void (*fp)(int*);
    fp = argc == 1 ? func1: func2;

    fp(val);
    return 0;
}

mem2reg优化后的如下,这里 select 指令充当 phi

define void @func1(i32* noundef %val) #0 {
entry:
  %0 = load i32, i32* %val, align 4
  %add = add nsw i32 %0, 1
  store i32 %add, i32* %val, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define void @func2(i32* noundef %val) #0 {
entry:
  %0 = load i32, i32* %val, align 4
  %add = add nsw i32 %0, 2
  store i32 %add, i32* %val, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 noundef %argc, i8** noundef %argv) #0 {
entry:
  %var = alloca i32, align 4
  store i32 0, i32* %var, align 4
  %cmp = icmp eq i32 %argc, 1
  %0 = zext i1 %cmp to i64
  %cond = select i1 %cmp, void (i32*)* @func1, void (i32*)* @func2
  call void %cond(i32* noundef %var)
  ret i32 0
}

这里假如把 fp = argc == 1 ? func1: func2; 替换成

if (argc == 1)
   fp = func1;
else
   fp = func2;

那么mem2reg优化后的 main 代码如下,phi 取代了 select 指令。

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 noundef %argc, i8** noundef %argv) #0 {
entry:
  %var = alloca i32, align 4
  store i32 0, i32* %var, align 4
  %cmp = icmp eq i32 %argc, 1
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  br label %if.end

if.else:                                          ; preds = %entry
  br label %if.end

if.end:                                           ; preds = %if.else, %if.then
  %fp.0 = phi void (i32*)* [ @func1, %if.then ], [ @func2, %if.else ]
  call void %fp.0(i32* noundef %var)
  ret i32 0
}

7.3.Type3

7.3.1.case1

void (*fp)(int*);

void func1(int* val) {
    *val += 1;
}

void func2(int* val) {
    *val += 2;
}

void assignment(void (*f)(int*)) {
    fp = f;
}

int main(int argc, char** argv) {
    int var = 0;
    int* val = &var;
    
    if (argc == 1)
        assignment(func1);
    else
        assignment(func2);

    fp(val);
    return 0;
}

mem2reg优化如下,可以看到用到了 store 给函数指针赋值,因此不属于simple function pointer。

@fp = global void (i32*)* null, align 8

; Function Attrs: noinline nounwind ssp uwtable
define void @func1(i32* noundef %val) #0 {
entry:
  %0 = load i32, i32* %val, align 4
  %add = add nsw i32 %0, 1
  store i32 %add, i32* %val, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define void @func2(i32* noundef %val) #0 {
entry:
  %0 = load i32, i32* %val, align 4
  %add = add nsw i32 %0, 2
  store i32 %add, i32* %val, align 4
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define void @assignment(void (i32*)* noundef %f) #0 {
entry:
  store void (i32*)* %f, void (i32*)** @fp, align 8
  ret void
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main(i32 noundef %argc, i8** noundef %argv) #0 {
entry:
  %var = alloca i32, align 4
  store i32 0, i32* %var, align 4
  %cmp = icmp eq i32 %argc, 1
  br i1 %cmp, label %if.then, label %if.else

if.then:                                          ; preds = %entry
  call void @assignment(void (i32*)* noundef @func1)
  br label %if.end

if.else:                                          ; preds = %entry
  call void @assignment(void (i32*)* noundef @func2)
  br label %if.end

if.end:                                           ; preds = %if.else, %if.then
  %0 = load void (i32*)*, void (i32*)** @fp, align 8
  call void %0(i32* noundef %var)
  ret i32 0
}

7.3.2.Testcase 2

测试一个加载了数组的函数指针

int add1(int a) {
    return a + 1;
}

int add2(int a) {
    return a + 2;
}

int (*add[2]) (int a) = {add1, add2};

int main() {
    int a = 0;
    a = add[0](a);
    a = add[1](a);
    return 0;
}

mem2reg优化后的IR如下,可以看到数组处理用到了 load 因此这个data-flow也有点负责。

@add = global [2 x i32 (i32)*] [i32 (i32)* @add1, i32 (i32)* @add2], align 8

; Function Attrs: noinline nounwind ssp uwtable
define i32 @add1(i32 noundef %a) #0 {
entry:
  %add = add nsw i32 %a, 1
  ret i32 %add
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @add2(i32 noundef %a) #0 {
entry:
  %add = add nsw i32 %a, 2
  ret i32 %add
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  %0 = load i32 (i32)*, i32 (i32)** getelementptr inbounds ([2 x i32 (i32)*], [2 x i32 (i32)*]* @add, i64 0, i64 0), align 8
  %call = call i32 %0(i32 noundef 0)
  %1 = load i32 (i32)*, i32 (i32)** getelementptr inbounds ([2 x i32 (i32)*], [2 x i32 (i32)*]* @add, i64 0, i64 1), align 8
  %call1 = call i32 %1(i32 noundef %call)
  ret i32 0
}

7.3.3.case3

测试涉及结构体的全局变量

typedef struct Handler {
    int (*func1)(int a, int b);
    int (*func2)(int a, int b);
    int (*func3)(int a, int b);
} Handler_t;

int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

int mul(int a, int b) {
    return a * b;
}

Handler_t handler = { add, sub, mul };

int main() {
    int a = 0, b = 1;
    int r1 = handler.func1(a, b);
    int r2 = handler.func2(a, b);
    int r3 = handler.func3(a, b);
    return 0;
}

mem2reg优化后如下,可以看到还是用了 load 指令加载函数指针。

%struct.Handler = type { i32 (i32, i32)*, i32 (i32, i32)*, i32 (i32, i32)* }

@handler = global %struct.Handler { i32 (i32, i32)* @add, i32 (i32, i32)* @sub, i32 (i32, i32)* @mul }, align 8

; Function Attrs: noinline nounwind ssp uwtable
define i32 @add(i32 noundef %a, i32 noundef %b) #0 {
entry:
  %add = add nsw i32 %a, %b
  ret i32 %add
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @sub(i32 noundef %a, i32 noundef %b) #0 {
entry:
  %sub = sub nsw i32 %a, %b
  ret i32 %sub
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @mul(i32 noundef %a, i32 noundef %b) #0 {
entry:
  %mul = mul nsw i32 %a, %b
  ret i32 %mul
}

; Function Attrs: noinline nounwind ssp uwtable
define i32 @main() #0 {
entry:
  %0 = load i32 (i32, i32)*, i32 (i32, i32)** getelementptr inbounds (%struct.Handler, %struct.Handler* @handler, i32 0, i32 0), align 8
  %call = call i32 %0(i32 noundef 0, i32 noundef 1)
  %1 = load i32 (i32, i32)*, i32 (i32, i32)** getelementptr inbounds (%struct.Handler, %struct.Handler* @handler, i32 0, i32 1), align 8
  %call1 = call i32 %1(i32 noundef 0, i32 noundef 1)
  %2 = load i32 (i32, i32)*, i32 (i32, i32)** getelementptr inbounds (%struct.Handler, %struct.Handler* @handler, i32 0, i32 2), align 8
  %call2 = call i32 %2(i32 noundef 0, i32 noundef 1)
  ret i32 0
}

参考文献

[1]Cai Y, Jin Y, Zhang C. Unleashing the Power of Type-Based Call Graph Construction by Using Regional Pointer Information[C]//33nd USENIX Security Symposium (USENIX Security 24). 2024

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值