总览
QEMU(quick emulator)是一款由Fabrice Bellard等人编写的免费开源的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序在另外一种架构上面运行。
qemu是一个进程启动一个虚拟机。guest关机,qemu进程就退出。为了方便可以重启guest而不用重新启动qemu,当然guest关机后再启动qemu也是可以的。
QEMU支持大端和小端的架构。 字节序转换是通过辅助函数来实现的,而不是直接访问guest的RAM。 这样就可以运行具有与host不同的字节序的目标了。
KVM
KVM(Kernel Virtual Machine)是Linux的一个内核驱动模块,让qemu之类的程序直接在host上的CPU安全地执行guest的代码。KVM现在支持x86, ARMv8, ppc, s390和MIPS的CPU。KVM内核模块使用的是Intel或者AMD的硬件虚拟化技术来执行guest的代码。它的作用主要是负责虚拟机的创建,虚拟内存的分配,虚拟CPU寄存器的读写和虚拟cpu的运行。
那么怎么让KVM执行guest上面的代码呢?
首先qemu进程打开/dev/kvm,之后调用 KVM_RUN ioctl。
假如guest需要访问hardware device register,那就挂起guest的CPU,KVM就会退出,控制权回到qemu进程。
代码如下:
1 | open("/dev/kvm") |
在另一个博客看到一个更详细的
1 | // 第一步,获取到 KVM 句柄 |
内存
guest虚拟机里面的内存,同样使用的虚拟内存,而guest所使用的物理内存,实际是对应的是启动它的那个qemu的虚拟内存的一部分。
1 | Guest' processes |
比如我们qemu启动了一个2G内存的虚拟机,我们查看内存maps,可以发现有个内存就是2G,就是guest所使用的物理内存
1 | 55dad0b86000-55dad1124000 r-xp 00000000 08:01 665015 /XXXXX/XXXXX/qemu-system-x86_64 |
而我们在guest里面申请的虚拟内存可以转化到host的qemu进程中的虚拟内存(相当于guest所认为物理内存)
漏洞利用的时候有些函数需要传递的是物理地址,所以需要将guest中的虚拟地址转化为物理地址。
这有两层转换:
1、从guest 的虚拟机地址 to guest 的物理地址
2、从 guest 的物理地址 to host的QEMU进程虚拟地址
对于第一层转换,通过pagemap页面映射文件来获取信息进行转换,具体可以参考下面的文档
https://www.kernel.org/doc/Documentation/vm/pagemap.txt
1 | * Bits 0-54 page frame number (PFN) if present |
下面的代码来源于http://phrack.org/papers/vm-escape-qemu-case-study.html
,而它参考的是https://github.com/nelhage/virtunoid/blob/master/virtunoid.c
,我加了点注释
核心的点是:
1、虚拟地址的低12位是页内偏移,而高位是物理帧在pagemap文件中的偏移,由于一个地址占用8个字节,所以获取在pagemap文件中的偏移需要乘8
2、在pagemap读取出来的是满足上面的规则,可以通过bit 63判断页面是否存在
3、最后0-54位返回就是物理帧的地址了,再或上低12位的页内偏移,那就是完整的物理地址了
1 | #include <stdio.h> |
将上面这个代码编译后,放到qemu运行(root权限)
之后我们在主机gdb attach到qemu的pid(root权限)
查看分配给qemu虚拟机对应的内存,我们分配的是2G,所以大小是0x8000000
1 | gdb-peda$ info proc mappings |
确实可以在qemu的进程的虚拟地址看到我们字符串
PCI设备
PCI是一个外部链接(Peripheral Component Interconnect)标准,PCI设备就是符合这个标准的设备,且连接到PCI总线上。而PCI总线是CPU与外部设备沟通的桥梁。
每个PCI设备对应备一个PCI配置空间(PCI Configuration Space),它记录了关于此设备的信息。PCI配置空间最大256个字节,其中前64字节都是预定义好的标准。
我们可以看下面的图,我copy过来了两张,其实都是一样的,不过第一张对于Base Address Registers更加细致。
具体的数据结构如下(复制于一个文章,暂时没在qemu源码找到,不确定是否准确,仅供参考):
1 | typedef struct { |
前面就是一些制造商ID,设备ID,等信息.
比较重要的就是那6个Base Address Registers,简称BAR。当然不是必须要有6个BAR。每个BAR记录了该设备映射的一段地址空间,映射的地址空间有Memory 空间和 I/O 空间。
Memory 空间和 I/O 空间的区别是最低位,Memory 空间最低位是0, I/O 空间的最低位是1
若是Memory 空间,1-2位表示内存的类型(type),bit 2为1表示采用64位地址,为0表示采用32位地址。bit1为1表示区间大小超过1M,为0表示不超过1M。bit3表示是否支持可预读取(Prefetchable)。
比如下面的设备,第一个是Memory 空间,第二个是 I/O 空间,看到最后一列,0x0000000000040200的最低bit是0,而0x0000000000040101的最低bit是1(前两列是空间的其实地址,第一个Memory 空间是0xfebf1000到0xfebf10ff,而第二个I/O ports是从0xc050到0xc057共8个端口)
1 | ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/resource |
其实上面说Memory 空间和I/O 空间分别对应我们常见到的MMIO,PMIO
1 | 内存映射I/O (Memory-mapped I/O —— MMIO) |
通过Memory 空间访问设备I/O的方式称为memory mapped I/O,即MMIO,这种情况下,CPU直接使用普通访存指令即可访问设备I/O。
通过I/O 空间访问设备I/O的方式称为port mapped I/O,即PMIO,这种情况下CPU需要使用专门的I/O指令如IN/OUT访问I/O端口。
MMIO,PMIO是PC机在中央处理器(CPU)和外部设备之间执行输入输出操作的两种方法,这两种方法互为补充。
查看PCI设备的信息
以BlizzardCTF 2017 Strng为例
查看当前虚拟机的pci设备
1 | ubuntu@ubuntu:~$ lspci |
-v可以查看更加详细信息,看到内存是0xfebf1000的256字节大小的,PMIO端口是0xc050开始的8个端口号
1 | ubuntu@ubuntu:~$ lspci -v |
上面设备过多可能不太友好,可用-s指定
1 | ubuntu@ubuntu:~$ lspci -v -s 00:03.0 |
查看header的一些具体的值
1 | ubuntu@ubuntu:~$ lspci -v -m -n -s 00:03.0 |
我们在文件系统中也可以看到这个设备的文件(linux一切皆文件)
1 | ubuntu@ubuntu:~$ ll /sys/devices/pci0000\:00/0000:00:03.0/ |
查看设备id是device文件
1 | ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000\:00\:03.0/device |
查看MMIO,PMIO映射可以看resource(三列分别是开始地址 结束地址 标志),第一行是MMIO,第二行是PMIO(有时候lspci -v
看不到信息的时候可以通过resource文件查看)
1 | ubuntu@ubuntu:~$ cat /sys/devices/pci0000\:00/0000:00:03.0/resource |
查看ioports(有些虚拟机查看不到)
1 | # cat /proc/ioports |
访问PCI设备配置空间中的Memory 空间和 I/O 空间
PMIO端口的编址是独立于系统的地址空间,其实就是一段地址区域,所有外设的地址都映射到这段区域中。
MMIO是直接把寄存器的地址空间直接映射到系统地址空间,系统地址空间往往会保留一段内存区用于这种MMIO的映射(当然肯定是位于系统内存区),这样系统可以直接使用普通的访存指令直接访问设备的寄存器,随着计算机内存容量的日益增大,这种方式更是显出独特的优势,在性能至上的理念下,使用MMIO可以最大限度满足日益增长的系统和外设存储的需要。所以当前其实大多数外设都是采用MMIO的方式。
MMIO
MMIO示例代码:通过映射resource0文件实现对Memory 空间的访问
1 | #include <stdio.h> |
据说还可以这样,但是这个在用户空间好像不行,应该是只能编写内核模块,即驱动才能用
1 | #include <asm/io.h> |
PMIO
需要权限才能访问端口,0x000-0x3ff可以用ioperm(from, num, turn_on)
比如ioperm(0x300,5,1); 获得 0x300 到 0x304 端口的访问权限
但是更高的端口就要用iopl(3)来获得权限,这个可以获得范围所有端口权限。当然我们需要root用户来运行程序才行。
in,out系列函数如下,分别是写入/读取一个字节(b结尾),两个字节(w结尾),四个字节(l结尾)
1 | #include <sys/io.h > |
当然调试的时候可以通过dd来触发PMIO
比如用dd命令向0xc050端口写入666吧,echo会自动加上换行,所以实际写入的是666加上换行
1 | ubuntu@ubuntu:~$ echo 666 > test |
QEMU中的对象模型
QEMU提供了一套面向对象编程的模型——QOM,即QEMU Object Module,几乎所有的设备如CPU、内存、总线等都是利用这一面向对象的模型来实现的。
而对象的初始化分为四步:
- 将 TypeInfo 注册 TypeImpl
- 实例化 ObjectClass
- 实例化 Object
- 添加 Property
QOM模型的实现代码位于qom/文件夹下的文件中,这涉及了几个结构TypeImpl, ObjectClass, Object和TypeInfo。看了下它们的定义都在https://github.com/qemu/qemu/blob/master/include/qom/object.h
可以找到,只有TypeImpl的具体结构是在https://github.com/qemu/qemu/blob/master/qom/object.c
中。
ObjectClass: 是所有类对象的基类,仅仅保存了一个整数 type 。
Object: 是所有对象的 基类Base Object , 第一个成员变量为指向 ObjectClass 的指针。
TypeInfo:是用户用来定义一个 Type 的工具型的数据结构。
TypeImpl:对数据类型的抽象数据结构,TypeInfo的属性与TypeImpl的属性对应。
将 TypeInfo 注册 TypeImpl
下面是TypeInfo
1 | struct TypeInfo |
2018年12月更新,删除了class_finalize函数
https://github.com/qemu/qemu/commit/37fdb2c56c603378b85466d1dd64fb4c95f9deb7
注释对于里面的成员写得比较详细了
1 | /** |
其实包含了下面信息
- Name
包括自己的Name,Parent的Name。 - Class(针对ObjectClass)
ObjectClass的信息包括,class_size,class_data,class相关函数:class_base_init,class_init,class_finalize。
这些函数都是为了初始化,释放结构体ObjectClass。 - Instance(针对的是Object)
对象Object信息包括:instance_size,instance相关函数:instance_post_init,instance_init,instance_finalize。
这些函数都是为了初始化,释放结构体Object。 - 其他信息
abstract是否为抽象。interface数组。
一般是定义一个TypeInfo,然后调用 type_register(TypeInfo) 或者 type_register_static(TypeInfo) 函数(我看到的基本都是type_register_static比较多),就会生成相应的TypeImpl实例,将这个TypeInfo注册到全局的TypeImpl的hash表中。
TypeInfo的属性与TypeImpl的属性对应,实际上qemu就是通过用户提供的TypeInfo创建的TypeImpl的对象。
我们看看https://github.com/qemu/qemu/blob/master/include/qom/object.h
里面的注释,看看怎么定义一个TypeInfo,
1 | * <example> |
或者我们去源码找一个实际的硬件https://github.com/qemu/qemu/blob/1c5880e785807abcc715a7ee216706e02c1af689/hw/pci/pci.c#L2801
1 | static const TypeInfo pci_device_type_info = { |
可以看到定义的时候不一定要初始化所有的成员,
type_init
接收用户写好的XXX_register_types(里面使用type_register_static生成相应的TypeImpl实例)
跟随type_register_static函数去看看
1 | static TypeImpl *type_register_internal(const TypeInfo *info) |
可以看到最终进入type_register_internal
,type_new
就是将TypeInfo的信息传递给TypeImpl
1 | static TypeImpl *type_new(const TypeInfo *info) |
之后的type_table_add
就是将TypeImpl插入到一个哈希表
1 | static GHashTable *type_table_get(void) |
上面的g_hash_table_insert
是glib库中的函数,定义如下:
1 | gboolean |
首先第一个参数通过type_table_get()
中的g_hash_table_new
创建一个GHashTable,第二、三个参数就是key和value了,这里分别是name还有TypeImpl。
有了一个TypeImpl的哈希表,下一步就是初始化每个type了,这一步可以看成是class的初始化,可以理解成每一个type对应了一个class,接下来会初始化class。
我们回到type_init
,这实际是个宏,代码在https://github.com/qemu/qemu/blob/bb9bf94b3e8926553290bc9a7cb84315af422086/include/qemu/module.h#L21
,看着跟linux的驱动有点像的感觉,当肯定不是一回事,可以看到do_qemu_init_ ## function(void)
前面有__attribute__((constructor))
关键字,这个可以让函数在main函数之前执行!!!。
1 | #ifdef BUILD_DSO |
可以看到是调用的register_module_init
,https://github.com/qemu/qemu/blob/810923480863c43ecb22ae124156298385439339/util/module.c#L62
1 | static ModuleTypeList init_type_list[MODULE_INIT_MAX]; |
可以看到将函数指针fn
给到了ModuleEntry->init
,之后通过find_type(MODULE_INIT_QOM)
找到对应的list,最后insert到MODULE_INIT_QOM对应的list——QTAILQ_INSERT_TAIL(l, e, node);
1 | #define QTAILQ_INSERT_TAIL(head, elm, field) do { \ |
那么这个东西是怎么调用的呢?(经过一顿操作,我发现qemu-system的main函数代码在vl.c文件,通过qemu-system-x86_64的main函数的特征去grep源码)
看到main函数https://github.com/qemu/qemu/blob/aceeaa69d28e6f08a24395d0aa6915b687d0a681/vl.c#L2753
1 | int main(int argc, char **argv, char **envp) |
看打上面,在main函数中调用了module_call_init(MODULE_INIT_QOM);
再去看看module_call_init
的实现,这时候就很明了了,调用的ModuleEntry中的init函数,正好和之前的register_module_init
将fn函数指针复制到init函数指针的操作连起来了
1 | // https://github.com/qemu/qemu/blob/810923480863c43ecb22ae124156298385439339/util/module.c#L89 |
总结一下:
1、首先__attribute__((constructor))
的修饰让type_init
在main之前执行,type_init
的参数是XXX_register_types
函数指针,将函数指针传递到ModuleEntry
的init函数指针,最后就是将这个ModuleEntry
插入到ModuleTypeList
2、main函数中的module_call_init(MODULE_INIT_QOM);
调用了MODULE_INIT_QOM
类型的ModuleTypeList中的所有ModuleEntry中的init()函数,也就是第一步type_init
的第一个参数XXX_register_types
函数指针
3、那就下了就是XXX_register_types
函数的操作了,就是创建TypeImpl的哈希表
ObjectClass的初始化
main函数中的module_call_init
调用了MODULE_INIT_QOM list
中的ModuleEntry
的init函数,init函数进行初始化(init函数就是上面说的XXX_register_types
函数),创建TypeImpl的哈希表。
main函数继续往下走我们看到调用了下面的https://github.com/qemu/qemu/blob/dd5b0f95490883cd8bc7d070db8de70d5c979cbc/vl.c#L3804
1 | machine_class = select_machine(); |
直接给出调用链,详细代码就不贴出来了,有兴趣可以自己找找
1 | main->select_machine->object_class_get_list->object_class_foreach |
看到object_class_foreach函数,调用参数贴上:object_class_foreach(object_class_get_list_tramp,
implements_type, include_abstract, &list);
1 | void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque), |
g_hash_table_foreach
的第一个参数是函数指针——type_table_get函数,看了下应该是之前创建的name为key,TypeImpl为value的GHashTable。
上面的g_hash_table_foreach
是对GHashTable中执行 GHFunc函数,也即执行object_class_foreach_tramp
函数,它的参数除了key,value对,还有gpointer user_data
1 | void |
到object_class_foreach_tramp
这里,这里已经出现了ObjectClass
,通过调用type_initialize
后,即可获得ObjectClass *k
。
1 | static void object_class_foreach_tramp(gpointer key, gpointer value, |
最后调用的data->fn(k, data->opaque);
,data->fn函数
其实是object_class_get_list_tramp
函数,g_slist_prepend
是glib库的函数(非glibc库),g_slist_prepend(*list, klass);
是将klass
插入到*list
的开头的地方,即将ObjectClass *k
插入到data->opaque
列表里面,data->opaque
也即在object_class_get_list
函数定义的局部变量GSList *list = NULL;
列表
1 | static void object_class_get_list_tramp(ObjectClass *klass, void *opaque) |
我们进入到type_initialize
函数看看,可以看到传入的正是TypeImpl
1 | static void type_initialize(TypeImpl *ti) |
我就将一些说明直接写在源码里头,简要概括就是,将parent->class->interfaces
的一些信息添加到ti->class->interfaces
列表上面,ti->interfaces[i].typename
对应的type的信息也添加到ti->class->interfaces列表,最后最重要的就是调用parent的class_base_init进行初始化,最后调用自己ti->class_init
进行初始化。
参考文章https://terenceli.github.io/%E6%8A%80%E6%9C%AF/2017/01/08/qom-introduction
以vmxnet3为例给出了class的层次结构
可以看到如下层次关系:VMXNET3Class->PCIDeviceClass->DeviceClass->ObjectClass
,这是Class的集成关系
1 | static const TypeInfo vmxnet3_info = { |
对象的构造——实例化 Instance(Object)
https://github.com/qemu/qemu/blob/dd5b0f95490883cd8bc7d070db8de70d5c979cbc/vl.c#L4304
接下来main函数调用了qemu_opts_foreach,循环查找参数(options)
1 | qemu_opts_foreach(qemu_find_opts("device"), |
先看qemu_opts_foreach
函数的定义,就是对于@list的每个成员——member,调用@func(@opaque, member, @errp)
1 | int qemu_opts_foreach(QemuOptsList *list, qemu_opts_loopfunc func, |
当然之前还调用上面的之前是调用了下面两个,看了下default_driver_check就是将qemu_opt_get(opts, "driver")
获取到的driver与default_list[i].driver
中的比较,相等就将*(default_list[i].flag) = 0;
,default_list
可以看这:https://github.com/qemu/qemu/blob/dd5b0f95490883cd8bc7d070db8de70d5c979cbc/vl.c#L220:3
;
而第二个device_help_func
实际里面调用了qdev_device_help(opts);
,简单看了下qdev_device_help
,首先调用driver = qemu_opt_get(opts, "driver");
,接下来就是输出那个driver的help信息,还有那些option什么的:https://github.com/qemu/qemu/blob/dd5b0f95490883cd8bc7d070db8de70d5c979cbc/qdev-monitor.c#L253:5
1 | qemu_opts_foreach(qemu_find_opts("device"), |
扯远了,还是回到device_init_func
,里面调用qdev_device_add
1 | static int device_init_func(void *opaque, QemuOpts *opts, Error **errp) |
而在qdev_device_add
里面,重要的一行是调用了dev = DEVICE(object_new(driver));
,而且上一行有个注释——/* create device */
1 | DeviceState *qdev_device_add(QemuOpts *opts, Error **errp) |
DEVICE
是一个宏,实际是OBJECT_CHECK
,而OBJECT_CHECK
是A type safe version of @object_dynamic_cast_assert.,看了下object_dynamic_cast_assert
的代码,主要是是看看obj是否是TYPE_DEVICE的一个实例(an instance of TYPE_DEVICE)
1 | #define DEVICE(obj) OBJECT_CHECK(DeviceState, (obj), TYPE_DEVICE) |
扯远了,重点是在object_new
啊,
1 | Object *object_new(const char *typename) |
向下继续看object_new_with_type
,首先type_initialize
之前说过,主要是调用parent的class_base_init
进行初始化,最后调用自己class_init
进行初始化
而object_initialize_with_type
的话,不知为何又一次调用type_initialize
,接下来就是一些判断,对obj的class和properties成员进行初始化,而object_ref
看了下是对&obj->ref进行+1,主要还是看下object_init_with_type
和object_init_with_type
函数吧
1 | static Object *object_new_with_type(Type type) |
object_init_with_type
函数首先判断ti是否有parent(即type->parent != NULL
),有parent就会递归调用object_init_with_type
,最终就是调用ti->instance_init
函数
而object_post_init_with_type
差不多,只不过先调用自身的ti->instance_post_init
,再递归调用parent的ti->instance_post_init
而这些函数都是在type_init(XXXX_register_types)
中的XXXX_register_types
设置好的。
1 | static void object_init_with_type(Object *obj, TypeImpl *ti) |
上一小节我们看到了Class的继承关系,这次是Object的继承关系,上次直接复制了参考文章作者vmxnet3
的例子,虽然这个他也是用这个例子,但这次Object的继承关系我自己换一个吧,我用e1000网卡为例,看看Object的集成关系
1 | typedef struct E1000State_st { |
整个集成关系是
1 | E1000State->PCIDevice->DeviceState->Object |
MMIO,PMIO的Memory Region是在哪设置的呢
Memory Region的设置一般是在XXX_realize函数里面。比如全志科技Allwinner的一个网卡就直接卸载XXX_inti函数里面了,源码路径/hw/net/allwinner_emac.c
1 | static void aw_emac_init(Object *obj) |
以e1000网卡为例,首先你定义的XXXState,这里是E1000State,得定义MemoryRegion类型的变量,pmio,mmio都是这个类型
1 | typedef struct E1000State_st { |
继续看看e1000_mmio_setup
函数调用完,接下来调用了两个pci_register_bar
,第一个是针对MEMORY 空间的(MMIO),第二个是IO空间(PMIO)。实际这个函数的操作是对&pci_dev->io_regions[region_num]
的相应区域进行赋值,MMIO就是&pci_dev->io_regions[0]
,PMIO是&pci_dev->io_regions[1]
,这里的0,1并不是却别MMIO与PMIO的,只是区分是resource0还是resource1。
1 | void pci_register_bar(PCIDevice *pci_dev, int region_num, |
此外MMIO与PMIO下面的不同点还有对io_regions的address_space的赋值,利用type变量进行选择,根据下面的定义,实际也是0和1,那就是0选择pci_get_bus(pci_dev)->address_space_mem
,而type为1选pci_get_bus(pci_dev)->address_space_io
1 | #define PCI_BASE_ADDRESS_SPACE_MEMORY 0x00 |
io_regions的类型是PCIIORegion
,可以看到跟上面的赋值也是相对应的
1 | typedef struct PCIIORegion { |
最后问题来了,那么pci_e1000_realize
在什么时候调用的呢,根据引用关系只是在e1000_class_init
函数中对PCIDeviceClass->realize
进行了赋值操作
1 | static void e1000_class_init(ObjectClass *klass, void *data) |
刚好有个题目有符号,而且也是在XXX_class_init设置realize函数指针,那就在下面实际题目中调试看看吧
总结
将 TypeInfo 注册 TypeImpl:
1、首先__attribute__((constructor))
的修饰让type_init
在main之前执行,type_init
的参数是XXX_register_types
函数指针,将函数指针传递到ModuleEntry
的init函数指针,最后就是将这个ModuleEntry
插入到ModuleTypeList
2、main函数中的module_call_init(MODULE_INIT_QOM);
调用了MODULE_INIT_QOM
类型的ModuleTypeList中的所有ModuleEntry中的init()函数,也就是第一步type_init
的第一个参数XXX_register_types
函数指针
3、那就下了就是XXX_register_types
函数的操作了,就是创建TypeImpl的哈希表
ObjectClass的初始化:
调用链main->select_machine->object_class_get_list->object_class_foreach->object_class_foreach_tramp->type_initialize
将parent->class->interfaces
的一些信息添加到ti->class->interfaces
列表上面,ti->interfaces[i].typename
对应的type的信息也添加到ti->class->interfaces列表,最后最重要的就是调用parent的class_base_init进行初始化,最后调用自己ti->class_init
进行初始化。
实例化 Instance(Object)
调用链qemu_opts_foreach->device_init_func->qdev_device_add->object_new->object_new_with_type
object_new_with_type
函数里面初始化了Object的一些成员,并通过object_init_with_type
函数调用ti->instance_init
函数(有parent就会先递归调用object_init_with_type
,再调用自身的ti->instance_init
函数),而最后就是通过object_post_init_with_type函数差不多,只不过先调用自身的ti->instance_post_init
,再递归调用parent的ti->instance_post_init
实际题目中的调用关系HITB-GSEC-2017-babyqemu
我们说过__attribute__((constructor))
的修饰让type_init
在main之前执行,通过读代码,发现是将函数写在.init_array
段中的__frame_dummy_init_array_entry
数组中
接下来具体看看这个是怎么实现的
以x64的qemu-system-x86_64
为例,它也是一个ELF 64,也是从_start
开始执行,之后调用__libc_start_main
函数原型:int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end));
__libc_start_main
函数主要做了下面工作:
- 如果EUID不等于RUID进行一些必要的安全检查(通过
__libc_init_secure
函数判断是否需要检查,并设置一个全局变量,需要检查则调用__libc_check_standard_fds
检查,防止启动一个SUID的程序,而标准文件描述符0,1,2没有打开——据说是防止拒绝服务攻击或者黑客将不受信任的文件放在特殊的硬编码的文件描述符上。) - 初始化线程子系统(看了下应该是
ARCH_SETUP_TLS ();
,/* The stack guard goes into the TCB, so initialize it early. */
) - 调用
_dl_setup_stack_chk_guard
函数设置canary - 注册
rtld_fini
函数(__cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);
,__cxa_atexit
函数的作用是Register a function to be called by exit or when a shared library is unloaded.),这个函数作用是在dynamic shared object退出或者unloaded的时候释放资源 - 注册
fini
函数(__cxa_atexit ((void (*) (void *)) fini, NULL, NULL);
),程序退出的时候会调用它 - 调用初始化函数
init
(调用代码:(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
) - 调用main函数(调用代码是:
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
) - 用main函数的返回值作为参数调用exit函数(调用代码:
exit (result);
)
而上面的init函数一般是__libc_csu_init
,而里面是循环调用_frame_dummy_init_array_entry[v5++])(a1, a2, v3);
1 | void __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3) |
这个题目我们关注的是hitb相关的函数,我们看看这个数组里面有哪些函数指针,可以看到do_qemu_init_pci_hitb_register_types
1 | .init_array:0000000000964CB0 __frame_dummy_init_array_entry dq offset frame_dummy |
还记得type_init
就是module_init
,也即do_qemu_init_ ## function(void)
,所以上面的函数指针为啥都是do_qemu_init_
开头很清楚了吧
1 | #define module_init(function, type) \ |
之后register_module_init
这些之前都讲过了,就将后面的pci_hitb_register_types
函数指针赋值给ModuleEntry中的init成员并插到了一个ModuleTypeList,而main函数会调用ModuleTypeList中的ModuleTypeList中的ModuleEntry的init函数,也即这里的pci_hitb_register_types
接下来pci_hitb_register_types
会调用type_register_static
,参数hitb_info_27046
就是一个TypeInfo
类型
1 | { |
里面初始化了instance_init和class_init成员,分别是hitb_instance_init和hitb_class_init(ObjectClass的初始化的时候会调用hitb_class_init,而Object初始化的时候会调用instance_init)
1 | data.rel.ro:0000000000969020 hitb_info_27046 dq offset aHitb ; name |
到这里整个流程已经清楚了,但是唯一不清楚的就是hitb_class_init中的pci_hitb_realize是什么时候调用的
1 | void __fastcall hitb_class_init(ObjectClass_0 *a1, void *data) |
pci_hitb_realize什么时候调用
接下来调试看看pci_hitb_realize什么时候调用,先看hitb_class_init
(我能说调试比看代码方便多了么。。。,整个调用关系一目了然,害我上面看代码看了这么旧,不过其实也值得)
1 | Breakpoint hitb_class_init |
接下来看pci_hitb_realize,可以看到是qdev_device_add
函数里面的object_property_set_bool(OBJECT(dev), true, "realized", &err);
,而且是在object_new
之后,也就是说Object实例化后才调用class_init函数中设置的realize函数指针
代码:https://github.com/qemu/qemu/blob/dd5b0f95490883cd8bc7d070db8de70d5c979cbc/qdev-monitor.c#L675
1 | Breakpoint pci_hitb_realize |
根据栈,我跟了下代码,还是比较复杂的,有兴趣的也可以去跟一下
总结
基础知识基本上都是搬运别人的知识,到后面源码阅读从参考别人,到自己去跟,学到很多。
其实调试会更加高效,而且没有参考文章的时候,你也可以找到一些蛛丝马迹
参考
http://blog.vmsplice.net/2011/03/qemu-internals-big-picture-overview.html
http://phrack.org/papers/vm-escape-qemu-case-study.html
https://www.giantbranch.cn/2019/12/03/CTF%20QEMU%20%E8%99%9A%E6%8B%9F%E6%9C%BA%E9%80%83%E9%80%B8%E4%B9%8BBlizzardCTF%202017%20Strng/
https://www.kernel.org/doc/Documentation/vm/pagemap.txt
https://cloud.tencent.com/developer/article/1018022
https://my.oschina.net/u/3626804/blog/1822539
http://liujunming.top/2019/07/19/%E7%A8%8B%E5%BA%8F%E5%91%98%E7%9C%BC%E4%B8%AD%E7%9A%84PCI%E8%AE%BE%E5%A4%87/
http://www.mnc.co.jp/english/INtime/faq07-2_kanren/PCIconfigurationregister.htm
https://ray-cp.github.io/archivers/qemu-pwn-basic-knowledge
https://www.w0lfzhang.com/2018/11/02/How-QEMU-Emulates-Devices/
https://blog.csdn.net/u011364612/article/details/53485856
https://www.binss.me/blog/qemu-note-of-qemu-object-model/
https://juniorprincewang.github.io/2018/07/23/qemu%E6%BA%90%E7%A0%81%E6%B7%BB%E5%8A%A0%E8%AE%BE%E5%A4%87/
https://www.cnblogs.com/etangyushan/p/6077307.html
https://developer.gnome.org/glib/stable/glib-Hash-Tables.html
https://terenceli.github.io/%E6%8A%80%E6%9C%AF/2017/01/08/qom-introduction
https://developer.gnome.org/glib/stable/glib-Singly-Linked-Lists.html
https://terenceli.github.io/%E6%8A%80%E6%9C%AF/2015/09/26/qemu-options
https://sq.163yun.com/blog/article/175668619278782464
https://www.cnblogs.com/anker/p/3462363.html
https://www.jianshu.com/p/dd425b9dc9db
https://www.anquanke.com/post/id/86412
http://www.voidcn.com/article/p-bxeqwthp-n.html
http://answerrrrrrrrr.github.io/2017/03/16/Linux%E7%A8%8B%E5%BA%8F%E5%90%AF%E5%8A%A8%E8%BF%87%E7%A8%8B/
https://refspecs.linuxbase.org/LSB_3.1.0/LSB-generic/LSB-generic/baselib---libc-start-main-.html
https://github.com/bminor/glibc/blob/653d74f12abea144219af00400ed1f1ac5dfa79f/csu/libc-start.c#L128