作者:FloatingGuy 转载请注明出处:https://floatingguy.github.io/


《C++ 安全编程》 – 第 三 章:指针诡计

基本概念

本章介绍内存漏洞,如何达到 劫持控制流 和 任意内存地址写。
两个典型的例子:

  • 覆盖函数指针:通过溢出操作,覆盖内存中的函数指针指向攻击者的 shellcode,达到劫持控制流的目的。如果是内核中劫持控制流应为有 PXN 的限制不能直接从内核跳转到用户态执行,所以需要 ROP 或者其他手段配合使用。(绕过 PXN 不属于本文介绍内容)
  • 修改指针对象:如果一个指针对象作为后继赋值操作的目的地址,那么攻击者就可以通过控制指针对象达到任意地址写。

学习这部分内容,前置的知识是要 知道程序中的数据,指令在内存中的位置,所处的环境(内核、用户态)。

数据包括:局部变量、参数、返回值、函数指针、全局变量、静态变量、类对象、类的成员、类的虚表 等等。
指令包括:用户代码、动态库、静态库、中断代码、系统调用、驱动+内核代码

列表:

  1. 函数指针
  2. 对象指针
  3. 修改指令指针
  4. 全局偏移表 GOT
  5. .dtors 区
  6. 虚指针
  7. longjmp 函数

函数指针安全 – 缓冲区溢出/控制流

3.2 函数指针

Case

BSS段中的缓冲区溢出

1
2
3
4
5
6
7
8
9
void good_function(const char* str) {...}
int main(int argc, char** argv) {
static char buff[BUFFERSIZE];
static void (*funcPtr)(const char *str);
funcPtr = &good_function;
srncpy(buff, argv[1], strlen(argv[1])); // 有可能覆盖 funcPtr
(void)(*funcPtr)(argv[2]);
}

对象指针 – 任意地址写/控制流

3.3

任意地址写任意数据

1
2
3
4
5
6
7
8
9
void foo(void * arg, size_t len) {
char buff[100];
long val = ..;
long * ptr = ...;
memcpy(buff, arg, len);
*ptr = val;
..
}

注意:类型长度, x86-32位系统中 void* ,int, long都是 4字节

修改指令指针

x86-32 架构,指令指针寄存器 eip,不可以直接修改。必须通过控制转移指令(jmp, jcc, call 和 ret等),中断,异常 间接修改。
(貌似 arm 上可以直接修改??)

分析 call 指令:

  1. 将返回值存储到栈中
  2. 将控制权转到目标操作数 (立即数,通用寄存器,内存地址)

Case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void good_function(const char* str) {
printf("%s", str);
}
int main(int argc, char** argv) {
static void (*funcPtr)(const char* str);
funcPtr = &good_function;
(void)(*funcPtr)("hi \n"); 【1
good_function("there!\n"); 【2
return 0;
}
x86 汇编
_main PROC
push ebp
mov ebp, esp
mov DWORD PTR good_function OFFSET ?good_function@@YAXPBD@Z
//[1]
push OFFSET $SG5338
call DWORD PTR ?funcPtr@?1??main@@9@4P6AXPBD@ZA
add esp, 4
//[2]
push OFFSET $SG5339
call good_function
add esp, 4
xor eax, eax
pop ebp
ret 0
_main ENDP
good_function PROC
push ebp
mov ebp, esp
mov eax, DWORD PTR _str$[ebp]
push eax
push OFFSET $SG5329
call _printf
add esp, 8
pop ebp
ret 0
good_function ENDP

全局偏移表 GOT – 任意地址写/控制流

任何 ELF 的二进制文件的进程空间中,都包含一个GOT 表。GOT 存放绝对地址,地址是有效的并且不影响 PIC/PIE。改变的内容和形式取决于处理器型号。

程序首次使用一个外部模块的函数之前,先要跳入 plt中调用 _dl_runtime_resolve 函数完成符号解析和重定位,将函数的绝对地址写入对应的 GOT 表项。再次执行次函数时,就从 GOT 表中执行绝对地址。

在函数中调用重定位函数

1
2
3
4
5
6
7
8
9
10
11
12
...
blx func ; 套转到 func@plt 中
...
PTL0:
push *(GOT+4) //保存的是当前模块的ID
jump *(GOT+8) //跳转到 _dl_runtime_resolve()完成符号解析和重定位
......
func@plt:
jmp *(func@GOT) // 第一次 GOT 表项会跳转到下移行执行
push n //对应 GOT 表中 func 函数的索引
jmp PLT0

攻击者可以利用,任意地址写漏洞覆盖 GOT 表中的函数地址为 shellcode 地址。
这一类攻击主要在用户态被使用。
一般C 程序最后都会调用 exit()函数,所以我们经常覆盖 exit 的 GOT 入口项。

Case

.dtors 区 – 任意地址写/控制流

任意内存写覆盖 GCC 生成的可执行文件中.dtors 区中函数指针。

GNU C 允许程序员利用attribute关键字给函数添加属性。属性包括constructor和 destructor。
constructor 在 main之前执行,在.ctors 区中。
destructor 在 exit之后执行,在 dtors 区中。
因为 constructor中的函数在 main 前执行完一次就不再执行,所以漏洞利用只考虑覆盖 destructor 中的函数指针。

.dtors 区是可写的(不可写可以用 mprotect函数修改 prot)。其内容的格式:
0xffff ffff {函数地址1,函数地址2.、、} 0x0000 0x0000

如果没有执行析构函数,.dtors 区中中包含头、尾标签而中间没有函数地址,一样可以将尾(0x0000 0000)覆盖为 shellcode 的地址。

通过覆盖.dtors进行缓冲区溢出攻击

Case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void create(void) __attribute__ ((constructor));
static void destroy(void) __attribute__ ((destructor));
int main(int argc, char const *argv[])
{
printf("create fptr: %p. \n", create);
printf("destroy fptr: %p. \n", destroy);
return 0;
}
static void create(void) {
puts("create called.");
}
static void destroy(void) {
puts("destroy called.");
}

虚指针

虚函数:用 virtual 定义的类成员函数。该函数可以被子类同名函数重写。 子类对象的指针可以被赋值给基类指针,使用该基类指针可以调用函数。

  1. 调用非虚函数, 则调用的是基类的函数,因为和指针的静态类型相关联。
  2. 调用虚函数,则是子类的函数,和动态类型相关联

Case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class a {
public:
void f(void) {
cout << "base f" << '\n';
}
virtual void g(void) {
cout << "base g" << "\n";
}
} // end a
class b {
public:
void f(void) {
cout << "subclass f" << '\n';
}
virtual void g(void) {
cout << "subclass g" << "\n";
}
}
int main() {
a *my_b = new b();
my_b->f();
mt_b->g();
return 0;
}

任意地址写覆盖 虚表中的 g 函数地址,劫持控制流。虚表在 bss 段。

atexit() 和 on_exit() 函数

atexit() 是C 标准定义的一个通用工具函数。atexit 可以注册无参函数,在程序正常接受后调用该函数。

Case

1
2
3
4
5
6
7
8
9
10
char *glob;
void test(void) {
printf("%s", glob);
}
int main(void) {
atexit(test);
glob = "Exiting. \n";
}

通过调试 可以知道其调用流程,然后分析源码:
Linux:
_start -> __libc_start_main -> __GI_exit -> __run_exit_handlers

OSX :

1
2
3
4
5
thread #1: tid = 0x264c8, 0x0000000100000f10 at`test, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000100000f10 at`test
frame #1: 0x00007fffbcbaf17f libsystem_c.dylib`__cxa_finalize_ranges + 339
frame #2: 0x00007fffbcbaf4b2 libsystem_c.dylib`exit + 55
frame #3: 0x00007fffbcb1a25c libdyld.dylib`start + 8

现在 还没有找到 文章中说的 __exit_funcs 函数, linux 中有,但是 mac 上没有。

https://code.woboq.org/userspace/glibc/stdlib/exit.h.html
https://code.woboq.org/userspace/glibc/stdlib/exit.c.html#__run_exit_handlers
https://code.woboq.org/userspace/glibc/stdlib/exit.h.html#exit_function_list

使用 gdb 调试:
p initial 打印出 全局变量 initial 的内存结构 ( struct exit_function_list )
注意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
xa_atexit.c
static struct exit_function_list initial;
struct exit_function_list *__exit_funcs = &initial;
--------------
exit.c
void
103 exit (int status)
104 {
105 __run_exit_handlers (status, &__exit_funcs, true, true);
106 }
--------------
void
32 attribute_hidden
33 __run_exit_handlers (int status, struct exit_function_list **listp,
34 bool run_list_atexit, bool run_dtors)
35 {
。。。。
47 while (*listp != NULL)
48 {
49 struct exit_function_list *cur = *listp;
50
51 while (cur->idx > 0)
52 {
53 const struct exit_function *const f =
54 &cur->fns[--cur->idx];
55 switch (f->flavor)
56 {
57 void (*atfct) (void);
58 void (*onfct) (int status, void *arg);
59 void (*cxafct) (void *arg, int status);
60
61 case ef_free:
62 case ef_us:
63 break;
64 case ef_on:
65 onfct = f->func.on.fn;
66 #ifdef PTR_DEMANGLE
67 PTR_DEMANGLE (onfct);
68 #endif
69 onfct (status, f->func.on.arg);
70 break;
71 case ef_at: (3
72 atfct = f->func.at;
73 #ifdef PTR_DEMANGLE
74 PTR_DEMANGLE (atfct);
75 #endif
76 atfct ();
77 break;
78 case ef_cxa: (4
79 cxafct = f->func.cxa.fn;
80 #ifdef PTR_DEMANGLE
81 PTR_DEMANGLE (cxafct);
82 #endif
83 cxafct (f->func.cxa.arg, status);
84 break;
85 }
86 }
87
88 *listp = cur->next;
89 if (*listp != NULL)
92 free (cur);
93 }
94
95 if (run_list_atexit)
96 RUN_HOOK (__libc_atexit, ());
97
98 _exit (status);
99 }

longjmp 函数

Case

Dictionary

关联章节

Change Log

Time Change
2017-02-28 增加 3节
2017-03-1 89
2017-03-1 90