Skip to content

Commit 522bd3d

Browse files
committed
Add metadata section
1 parent bf2092c commit 522bd3d

File tree

5 files changed

+212
-26
lines changed

5 files changed

+212
-26
lines changed

code/article_06/cfi.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
typedef void (*f)(void);
2+
3+
void foo1(void) {}
4+
void foo2(void) {}
5+
void bar(int a) {}
6+
7+
void baz(f func) {
8+
func();
9+
}

code/article_06/debug.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
int sum(int a, int b) {
2+
return a + b;
3+
}

code/article_06/inline.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
inline int foo(int a) __attribute__((always_inline));
2+
3+
int foo(int a) {
4+
if (a > 0) {
5+
return a;
6+
} else {
7+
return 0;
8+
}
9+
}

code/article_07/try_catch_test.cpp

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/06-内置函数、属性和元数据.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)