Contents

Simple Analysis of Software Virtualization of Memory in Unicorn Engine

Unicorn Engine is cut from QEMU, it can be said to be a branch of QEMU, but it is a completely independent project. Unicorn provides a wealth of APIs, allowing us to easily experience many features brought by QEMU virtualization technology, but it also faces some security issues. We take the analysis of CVE-2022-29694 as an example to briefly outline the principle of Unicorn memory virtualization. Only some simple analysis is made here, so that everyone can quickly grasp the whole picture of QEMU virtualization.

Important Structure

We first introduce two important structures in QEMU, namely MemoryRegion and RAMBlock. You can also look at the specific code in the following chapters, and then look back at the role of each domain member in them to achieve a better understanding.

../../../../images/2-null-pointer/MemoryManager.png
Structure of memory manager

Memory Region

MemoryRegion is an important structure in QEMU memory management, and it is a structure that manages each memory space of GVA(Guest Virtual Address).

struct MemoryRegion {
    /* private: */

    /* The following fields should fit in a cache line */
    bool ram;					// 
    bool subpage;
    bool readonly; 				//
    bool is_iommu;
    RAMBlock *ram_block;		// physical address in virtual machine

    const MemoryRegionOps *ops;
    void *opaque;
    MemoryRegion *container;	// parent MemoryRegion
    Int128 size;
    hwaddr addr;
    void (*destructor)(MemoryRegion *mr);
    uint64_t align;
    bool terminates;
    bool enabled;
    int32_t priority;
    QTAILQ_HEAD(, MemoryRegion) subregions;
    QTAILQ_ENTRY(MemoryRegion) subregions_link;

    struct uc_struct *uc;
    uint32_t perms;
    hwaddr end;
};

RAM Block

RAM Block represents a memory stick in the virtual machine, namely GPA(Guest Physical Address). All RAMBlocks will be connected to the linked list through the next field, and the linked list header is ram_list.blocks.

struct RAMBlock {
    struct MemoryRegion *mr;		// GPA->GVA
    uint8_t *host;					// GPA->HVA
    ram_addr_t offset;				// The offset address of the memory stick in the entire virtual machine
    ram_addr_t used_length;			// used length
    ram_addr_t max_length;
    uint32_t flags;
    /* RCU-enabled, writes protected by the ramlist lock */
    QLIST_ENTRY(RAMBlock) next;
    size_t page_size;				// system page size
};

Entry to Memory

uc_mem_map is an interface provided by Unicorn to users. We can use this function to apply for a memory space for the simulator. This space can not only store the instructions to be simulated, but also be used as the stack of the virtual machine.

uc_err uc_mem_map(uc_engine *uc, uint64_t address, size_t size, uint32_t perms)
{
    uc_err res;	
    
    /* init unicorn */
    UC_INIT(uc);	
	
    /* only defined in MIPS */
    if (uc->mem_redirect) {
        address = uc->mem_redirect(address);
    }
	
    /* For memory safety checks, check the incoming address range */
    res = mem_map_check(uc, address, size, perms);
    if (res) {
        return res;
    }
	
    /* mem_map mapped_blocks
     * memory_map GVA
     */
    return mem_map(uc, address, size, perms,
                   uc->memory_map(uc, address, size, perms));
}

Apply for Memory Region

Continue to analyze memory_map, this function is defined in qemu/softmmu/memory.c file, used to formally apply for Memory Region. As can be seen from the function memory_region_add_subregion, Unicorn uses uc->system_memory as a parent set to manage all Regions.

../../../../images/2-null-pointer/MemoryRegion.png
Memory region in QEMU

Code

MemoryRegion *memory_map(struct uc_struct *uc, hwaddr begin, size_t size, uint32_t perms)
{
    /* ram */
    MemoryRegion *ram = g_new(MemoryRegion, 1);
	
    /* RAM allocation */
    memory_region_init_ram(uc, ram, size, perms);
    if (ram->addr == -1) {
        // out of memory
        return NULL;
    }
	
    /* system_memory->subregion = ram
     */
    memory_region_add_subregion(uc->system_memory, begin, ram);

    if (uc->cpu) {
        tlb_flush(uc->cpu);
    }

    return ram;
}

memory_region_init_ram is an important function for the virtual machine to request RAM.

void memory_region_init_ram(struct uc_struct *uc,
                            MemoryRegion *mr,
                            uint64_t size,
                            uint32_t perms)
{
    /* init MR */
    memory_region_init(uc, mr, size);
    mr->ram = true;
    if (!(perms & UC_PROT_WRITE)) {
        mr->readonly = true;
    }
    mr->perms = perms;
    mr->terminates = true;
    mr->destructor = memory_region_destructor_ram;
    /* important!apply for GPA and HVA */
    mr->ram_block = qemu_ram_alloc(uc, size, mr);
}

The function qemu_ram_alloc is the encapsulation of qemu_ram_alloc_from_ptr, which is used to apply for RAMBlock.

Apply for RAM Block

RAMBlock initialization

RAMBlock *qemu_ram_alloc_from_ptr(struct uc_struct *uc, ram_addr_t size, void *host,
                                   MemoryRegion *mr)
{
    RAMBlock *new_block;
    ram_addr_t max_size = size;

    size = HOST_PAGE_ALIGN(uc, size);
    max_size = HOST_PAGE_ALIGN(uc, max_size);
    new_block = g_malloc0(sizeof(*new_block));
    if (new_block == NULL)
        return NULL;
    new_block->mr = mr;
    new_block->used_length = size;
    new_block->max_length = max_size;
    assert(max_size >= size);
    new_block->page_size = uc->qemu_real_host_page_size;
    new_block->host = host;
    if (host) {
        new_block->flags |= RAM_PREALLOC;
    }
    ram_block_add(mr->uc, new_block);

    return new_block;
}

The function ram_block_add is used to add a memory stick to the system. This function first applies for PVA(Physical Virutal Address), and then adds RAMBlock to the system space.

CVE-2022-29694

The QEMU virtual machine applies for memory, and finally needs to apply for the corresponding memory on the host. Then the problem arises. When a virtual machine applies for a large memory, the actual application will inevitably fail. The following function does not return the status of the application result, and the current RAMBlock has not been added to ram_list.blocks.

../../../../images/2-null-pointer/CVE-2022.png
Alloc memory in GPA->HVA

The upper layer function does not judge whether the applied HVA is successful, but the virtual machine defaults that the applied GVA has been successful. When the virtual machine is shut down, QEMU memory free is triggered. At this time, the block is not QLIST, resulting in null pointer dereference.

../../../../images/2-null-pointer/CVE-2022-1.png
Null pinter to free QEMU ram

As shown below, when the virtual machine applies for large memory, the GVA is successful, but the GPA->HVA using the mmap function fails. Unsynchronized allocated memory results in a null pointer.

../../../../images/2-null-pointer/CVE-2022-2.png
Analysis of vulnerability

Fixs

The first repair method is relatively simple, directly using QLIST_SAFE_REMOVE provided by QEMU.

../../../../images/2-null-pointer/fix-1.png
Patch 1

The second repair solution seems more reasonable, because the essence of this problem is the inconsistency of the judgment of applying for GVA and GPA, so it is necessary to add consistency judgment results of them.

../../../../images/2-null-pointer/fix-2.png
Patch 2

Conclusion

This article is only a preliminary analysis of the process of Unicorn Engine memory virtualization, and introduces the general scheme for QEMU to apply for memory for virtual machines. QEMU memory virtualization is a complex process, and more mechanisms behind it need to be further studied in the future.