作者:FloatingGuy 转载请注明出处:https://floatingguy.github.io/
《C++ 安全编程》 – 第 三 章:指针诡计
基本概念
本章介绍内存漏洞,如何达到 劫持控制流 和 任意内存地址写。
两个典型的例子:
- 覆盖函数指针:通过溢出操作,覆盖内存中的函数指针指向攻击者的 shellcode,达到劫持控制流的目的。如果是内核中劫持控制流应为有 PXN 的限制不能直接从内核跳转到用户态执行,所以需要 ROP 或者其他手段配合使用。(绕过 PXN 不属于本文介绍内容)
- 修改指针对象:如果一个指针对象作为后继赋值操作的目的地址,那么攻击者就可以通过控制指针对象达到任意地址写。
学习这部分内容,前置的知识是要 知道程序中的数据,指令在内存中的位置,所处的环境(内核、用户态)。
数据包括:局部变量、参数、返回值、函数指针、全局变量、静态变量、类对象、类的成员、类的虚表 等等。
指令包括:用户代码、动态库、静态库、中断代码、系统调用、驱动+内核代码
列表:
- 函数指针
- 对象指针
- 修改指令指针
- 全局偏移表 GOT
- .dtors 区
- 虚指针
- 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])); (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 指令:
- 将返回值存储到栈中
- 将控制权转到目标操作数 (立即数,通用寄存器,内存地址)
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 push OFFSET $SG5338 call DWORD PTR ?funcPtr@?1??main@@9@4P6AXPBD@ZA add esp, 4 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 定义的类成员函数。该函数可以被子类同名函数重写。 子类对象的指针可以被赋值给基类指针,使用该基类指针可以调用函数。
- 调用非虚函数, 则调用的是基类的函数,因为和指针的静态类型相关联。
- 调用虚函数,则是子类的函数,和动态类型相关联
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"; } } 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 |