@@ -231,3 +231,194 @@ foo:
231231```
232232
233233恢复了往日的雄风。
234+
235+ ## 元数据
236+
237+ 函数的属性可以在前后端之间传递函数的信息,例如,前端发现某个函数需要后端的特殊处理,就给这个函数加一个自定义的属性。而在LLVM的整个管线中的任意一个位置,我们往往都能读到这个属性,从而可以依据是否有这个属性来做特殊的处理/优化。正因如此,之所以函数要有属性,是因为函数是LLVM的优化过程中一个非常重要的基础单元,因此需要保留各种信息。
238+
239+ 除此之外,我们有时也会希望每一条指令,或者每一个翻译单元,都可以有类似属性一样的信息,可以在管线中传递/过滤,从而能获得一些信息。这在LLVM IR中被称为「[ 元数据] ( https://llvm.org/docs/LangRef.html#metadata ) 」(Metadata)。
240+
241+ ### 调试信息
242+
243+ 说了这么多,元数据具体有什么用处呢?元数据的语法又是怎样的呢?我们来看一个具体的例子。
244+
245+ 我们知道,在Clang中,传入` -g ` 选项可以生成调试信息。那么,调试信息是怎么在LLVM IR中体现的呢?
246+
247+ 我们这样一个` debug.c ` 文件:
248+
249+ ``` c
250+ int sum (int a, int b) {
251+ return a + b;
252+ }
253+ ```
254+
255+ 我们使用
256+
257+ ```shell
258+ clang debug.c -g -S -emit-llvm
259+ ```
260+
261+ 生成LLVM IR文件,其一部分如下:
262+
263+ ``` llvm
264+ ; ...
265+ ; Function Attrs: noinline nounwind optnone uwtable
266+ define dso_local i32 @sum(i32 noundef %0, i32 noundef %1) #0 !dbg !10 {
267+ %3 = alloca i32, align 4
268+ %4 = alloca i32, align 4
269+ store i32 %0, ptr %3, align 4
270+ call void @llvm.dbg.declare(metadata ptr %3, metadata !15, metadata !DIExpression()), !dbg !16
271+ store i32 %1, ptr %4, align 4
272+ call void @llvm.dbg.declare(metadata ptr %4, metadata !17, metadata !DIExpression()), !dbg !18
273+ %5 = load i32, ptr %3, align 4, !dbg !19
274+ %6 = load i32, ptr %4, align 4, !dbg !20
275+ %7 = add nsw i32 %5, %6, !dbg !21
276+ ret i32 %7, !dbg !22
277+ }
278+
279+ ; ...
280+
281+ !llvm.dbg.cu = !{!0}
282+ !llvm.module.flags = !{!2, !3, !4, !5, !6, !7, !8}
283+ !llvm.ident = !{!9}
284+
285+ !0 = distinct !DICompileUnit(language: DW_LANG_C11, file: !1, producer: "Homebrew clang version 16.0.6", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug, splitDebugInlining: false, nameTableKind: None)
286+ !1 = !DIFile(filename: "debug.c", directory: "...", checksumkind: CSK_MD5, checksum: "...")
287+ ; ...
288+ !10 = distinct !DISubprogram(name: "sum", scope: !1, file: !1, line: 1, type: !11, scopeLine: 1, flags: DIFlagPrototyped, spFlags: DISPFlagDefinition, unit: !0, retainedNodes: !14)
289+ !11 = !DISubroutineType(types: !12)
290+ !12 = !{!13, !13, !13}
291+ !13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
292+ !14 = !{}
293+ !15 = !DILocalVariable(name: "a", arg: 1, scope: !10, file: !1, line: 1, type: !13)
294+ !16 = !DILocation(line: 1, column: 13, scope: !10)
295+ !17 = !DILocalVariable(name: "b", arg: 2, scope: !10, file: !1, line: 1, type: !13)
296+ !18 = !DILocation(line: 1, column: 20, scope: !10)
297+ !19 = !DILocation(line: 2, column: 12, scope: !10)
298+ !20 = !DILocation(line: 2, column: 16, scope: !10)
299+ !21 = !DILocation(line: 2, column: 14, scope: !10)
300+ !22 = !DILocation(line: 2, column: 5, scope: !10)
301+ ```
302+
303+ 我们可以看到,在生成的LLVM IR中,出现了大量以` ! ` 开头的符号,这就是元数据的语法。
304+
305+ 具体而言,我们看到其中的
306+
307+ ``` llvm
308+ !12 = !{!13, !13, !13}
309+ !13 = !DIBasicType(name: "int", size: 32, encoding: DW_ATE_signed)
310+ ```
311+
312+ 这里,` !13 = ... ` 生成了一个元数据,其内容为一个给定的结构体` DIBasicType ` ,而` !12 ` 这个元数据的内容,则并不是一个给定的结构体,而是由三个` !13 ` 这个元数据组成的结构。也就是说,元数据的组织相对比较灵活。
313+
314+ 在` sum ` 函数体中,我们可以看到,几乎每条指令后都附加了一个元数据,在代码下半部分找到对应的元数据,其实就是这行指令对应C语言中源代码里的位置,也就是调试信息中的location。
315+
316+ 此外,我们还可以看到` llvm.dbg.declare ` 内置函数的调用。这个函数的作用是标记源代码中变量的地址。例如:
317+
318+ ``` llvm
319+ store i32 %0, ptr %3, align 4
320+ call void @llvm.dbg.declare(metadata ptr %3, metadata !15, metadata !DIExpression()), !dbg !16
321+ ```
322+
323+ 这里就是指,源代码中位于` !15 ` 元数据处的变量,也就是` a ` ,其在生成的二进制程序中,位于` %3 ` 变量。
324+
325+ LLVM中的调试信息非常全面且复杂,具体可以看官方文档[ Source Level Debugging with LLVM] ( https://llvm.org/docs/SourceLevelDebugging.html ) 。
326+
327+ ### 控制流完整性
328+
329+ 元数据的另一个用途,就在于控制流完整性保护。当一个攻击者攻击一个二进制程序的时候,最低级的攻击者只是让它崩溃,造成DoS攻击。高级的攻击者,往往想让这个程序执行自己想让它执行的命令。而这一途径,在现代攻击环境下,往往是通过函数指针覆盖来实现的。
330+
331+ 举一个例子来说,在前几年,有一个非常著名的漏洞[ checkm8] ( https://twitter.com/axi0mX/status/1177542201670168576?s=20 ) 。这个漏洞可以攻击苹果的大部分iPhone设备,并且由于代码处于ROM中,所以被认为无法修复。其具体的分析可以看[ Technical analysis of the checkm8 exploit] ( https://habr.com/en/companies/dsec/articles/472762/ ) 和[ iPhone史诗级漏洞checkm8攻击原理浅析 - Gh0u1L5的文章 - 知乎] ( https://zhuanlan.zhihu.com/p/87456653 ) 。我们这里只需要了解一点,它的核心是,Apple代码中有一个结构体
332+
333+ ``` c
334+ struct usb_device_io_request {
335+ void *callback;
336+ // ...
337+ };
338+ ```
339+
340+ 这里` callback ` 是一个函数指针,在程序执行中会被调用。攻击者通过某种方法,强行覆盖了这个函数指针的值,从而让程序执行自己想要执行的函数。
341+
342+ 为了抵御这种攻击,我们往往会采用控制流完整性(Control Flow Integrity, CFI)策略。最简单的思路是,我们在写程序时,函数指针所指向的函数,肯定是有限个确定的函数。那么,我们可以在执行函数指针所对应的间接调用时,检查调用目标是否是那有限个确定的函数,就可以保证不会出现之前的这种问题了。
343+
344+ 但是,如何确定这个函数指针究竟能指向哪些函数呢?这个问题非常复杂,编译器往往是做不到这件事的。因此,现在一般会使用比较弱化的控制流完整性策略。在LLVM中,我们可以通过传递` -fsanitize=cfi-icall ` 来启用LLVM-CFI所提供的控制流完整性策略(需要同时通过` -flto ` 开启LTO),例如,我们有以下程序:
345+
346+ ``` c
347+ typedef void (* f)(void);
348+
349+ void foo1(void) {}
350+ void foo2(void) {}
351+ void bar(int a) {}
352+
353+ void baz(f func) {
354+ func();
355+ }
356+ ```
357+
358+ 将其保存为`cfi.c`,然后在命令行中使用
359+
360+ ```shell
361+ clang cfi.c -flto -fsanitize=cfi-icall -S -emit-llvm
362+ ```
363+
364+ 可以生成一个开启了LLVM-CFI策略的LLVM IR代码。
365+
366+ 那么,LLVM-CFI策略是什么呢?由于其相对比较复杂,具体可以参考[ Control Flow Integrity Design Documentation] ( https://clang.llvm.org/docs/ControlFlowIntegrityDesign.html ) ,我们这里只是非常粗略地讲。
367+
368+ 在上述代码中,` baz ` 函数接收一个函数指针,然后调用了这个函数指针。这个函数指针的类型是,不接收参数,也没有返回值。而LLVM-CFI采用的策略则是,只要满足这个类型的函数,都被认为是可以被函数指针所指向的。反之,如果不满足,则被拒绝。也就是说,在这个代码中,` foo1 ` 、` foo2 ` 都是满足的,而` bar ` 函数,因为它接收一个` int ` 类型的参数,所以不满足。
369+
370+ 那么,具体是怎么实现的呢?我们来看看它的LLVM IR代码,其一部分为:
371+
372+ ``` llvm
373+ ; Function Attrs: noinline nounwind optnone uwtable
374+ define dso_local void @foo1() #0 !type !9 !type !10 {
375+ ret void
376+ }
377+
378+ ; Function Attrs: noinline nounwind optnone uwtable
379+ define dso_local void @foo2() #0 !type !9 !type !10 {
380+ ret void
381+ }
382+
383+ ; Function Attrs: noinline nounwind optnone uwtable
384+ define dso_local void @bar(i32 noundef %0) #0 !type !11 !type !12 {
385+ %2 = alloca i32, align 4
386+ store i32 %0, ptr %2, align 4
387+ ret void
388+ }
389+
390+ ; Function Attrs: noinline nounwind optnone uwtable
391+ define dso_local void @baz(ptr noundef %0) #0 !type !13 !type !14 {
392+ %2 = alloca ptr, align 8
393+ store ptr %0, ptr %2, align 8
394+ %3 = load ptr, ptr %2, align 8
395+ %4 = call i1 @llvm.type.test(ptr %3, metadata !"_ZTSFvvE"), !nosanitize !15
396+ br i1 %4, label %6, label %5, !nosanitize !15
397+
398+ 5: ; preds = %1
399+ call void @llvm.ubsantrap(i8 2) #3, !nosanitize !15
400+ unreachable, !nosanitize !15
401+
402+ 6: ; preds = %1
403+ call void %3()
404+ ret void
405+ }
406+
407+ !9 = !{i64 0, !"_ZTSFvvE"}
408+ !10 = !{i64 0, !"_ZTSFvvE.generalized"}
409+ !11 = !{i64 0, !"_ZTSFviE"}
410+ !12 = !{i64 0, !"_ZTSFviE.generalized"}
411+ ```
412+
413+ 可以看到,在` baz ` 函数中,在调用这个函数指针,也就是` call void %3() ` 之前,被插入了一部分代码:
414+
415+ ``` llvm
416+ %3 = load ptr, ptr %2, align 8
417+ %4 = call i1 @llvm.type.test(ptr %3, metadata !"_ZTSFvvE"), !nosanitize !15
418+ br i1 %4, label %6, label %5, !nosanitize !15
419+ 5: ; preds = %1
420+ call void @llvm.ubsantrap(i8 2) #3, !nosanitize !15
421+ unreachable, !nosanitize !15
422+ ```
423+
424+ 在这里,首先调用了` llvm.type.test ` 这个内置函数。这个内置函数的作用是查看` ptr %3 ` 这个函数的类型,是否是` !"_ZTSFvvE" ` 这个元数据所代表的类型,如果不是的话,就跳转,调用` llvm.ubsantrap ` 报告错误。而我们可以看到,` foo1 ` 、` foo2 ` 、` bar ` 都被附加了一些元数据,查看代码的下半部分,可以看到,` foo1 ` 、` foo2 ` 的元数据是` !"_ZTSFvvE" ` ,而` bar ` 的元数据是` !"_ZTSFviE" ` 。因此,如果攻击者想让这个间接调用前往` bar ` 函数,就会被拒绝,从而保护了控制流的完整性。
0 commit comments