Environment
- OS: Ubuntu 22.04.1 LTS (Linux 5.15.0)
- Unicorn: 2.0.0 (6c1cbef6)
- CMake: 3.22.1
- GCC: 11.3.0
Issue
uc_emu_start()
doesn't stop emulation at an address specified by until
. Its PoC is the following C program:
#include <stdio.h>
#include <stdlib.h>
#include <unicorn/unicorn.h>
#include <capstone/capstone.h>
// x86_64 machine code.
const char target_code[] = \
"\xf3\x0f\x1e\xfa\x55\x48\x89\xe5\xc7\x45\xf8\x00\x00\x00\x00\xc7" \
"\x45\xfc\x00\x00\x00\x00\xeb\x1a\x83\x7d\xf8\x00\x75\x09\xc7\x45" \
"\xf8\x01\x00\x00\x00\xeb\x07\xc7\x45\xf8\x00\x00\x00\x00\x83\x45" \
"\xfc\x01\x83\x7d\xfc\x04\x7e\xe0\x90\x90\x5d\xc3";
// base address.
const uint64_t base_addr = 0x08000000;
// .text section offset.
const uint64_t text_offset = 0x40;
// .text section address.
const uint64_t text_addr = base_addr + text_offset;
// stack top offset.
const uint64_t stack_top_offset = 0x1fff;
// stack top address.
const uint64_t stack_top_addr = base_addr + stack_top_offset;
//
// a callback for tracing translation blocks.
//
static void
hook_block(uc_engine *uc, uint64_t address, uint32_t size, void *user_data)
{
uc_err uerr;
fprintf(stdout, ">>> Tracing basic block at 0x%" PRIx64 ", block size = 0x%x\n", address, size);
// disassemble with capstone.
csh handle;
cs_err cerr;
if ((cerr = cs_open(CS_ARCH_X86, CS_MODE_64, &handle))) {
fprintf(stderr, "Failed to cs_open() with error returned %u: %s\n", cerr, cs_strerror(cerr));
return;
}
uint8_t *code = (uint8_t *)malloc(sizeof(uint8_t) * size);
if ((uerr = uc_mem_read(uc, address, (void *)code, size))) {
fprintf(stderr, "Failed to uc_mem_read() with error returned %u: %s\n", uerr, uc_strerror(uerr));
return;
}
size_t count = 0;
cs_insn *insn;
if ((count = cs_disasm(handle, code, size, address, 0, &insn)) > 0) {
for (size_t i = 0; i < count; i++) {
fprintf(stdout, "\t0x%"PRIx64":\t%s\t\t%s\n", insn[i].address, insn[i].mnemonic, insn[i].op_str);
}
cs_free(insn, count);
} else {
fprintf(stderr, "\tFailed to disassemble.\n");
}
free(code);
if ((cerr = cs_close(&handle))) {
fprintf(stderr, "Failed to cs_close() with error returned %u: %s\n", cerr, cs_strerror(cerr));
return;
}
}
int
main(int argc, char *argv[])
{
uc_engine *uc;
uc_err err;
uc_hook trace;
// initialize a uc_engine instance.
if ((err = uc_open(UC_ARCH_X86, UC_MODE_64, &uc))) {
fprintf(stderr, "Failed to uc_open() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
// allocate 8KiB memory for emulation.
if ((err = uc_mem_map(uc, base_addr, 8 * 1024, UC_PROT_ALL))) {
fprintf(stderr, "Failed to uc_mem_map() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
// write the target code to the memory.
if ((err = uc_mem_write(uc, text_addr, (void *)target_code, sizeof(target_code) - 1))) {
fprintf(stderr, "Failed to uc_mem_write() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
// add a block-tracing hook.
if ((err = uc_hook_add(uc, &trace, UC_HOOK_BLOCK, hook_block, NULL, 1, 0))) {
fprintf(stderr, "Failed to uc_hook_add() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
// initialize stack-related registers.
int rbp = stack_top_addr;
int rsp = stack_top_addr;
if ((err = uc_reg_write(uc, UC_X86_REG_RBP, &rbp))) { // rbp
fprintf(stderr, "Failed to uc_reg_write() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
if ((err = uc_reg_write(uc, UC_X86_REG_RSP, &rsp))) { // rsp
fprintf(stderr, "Failed to uc_reg_write() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, "==================== 1. Run emulation until it hits 0x08000058 or runs 0x7fff instructions. ====================\n");
uint64_t eip = text_addr;
if ((err = uc_emu_start(uc, eip, 0x08000058, 0, 0x7fff))) {
fprintf(stderr, "Failed to uc_emu_start() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> Execution stoped.\n");
if ((err = uc_reg_read(uc, UC_X86_REG_EIP, &eip))) {
fprintf(stderr, "Failed to uc_reg_read() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> eip: 0x%"PRIx64"\n", eip);
fprintf(stdout, "\n");
fprintf(stdout, "==================== 2. Run just one instruction. =====================\n");
if ((err = uc_emu_start(uc, eip, UINT64_MAX, 0, 1))) {
fprintf(stderr, "Failed to uc_emu_start() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> Execution stoped.\n");
if ((err = uc_reg_read(uc, UC_X86_REG_EIP, &eip))) {
fprintf(stderr, "Failed to uc_reg_read() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> eip: 0x%"PRIx64"\n", eip);
fprintf(stdout, "\n");
fprintf(stdout, "==================== 3. Again, run emulation until it hits 0x08000058 or runs 0x7fff instructions. ====================\n");
if ((err = uc_emu_start(uc, eip, 0x08000058, 0, 0x7fff))) {
fprintf(stderr, "Failed to uc_emu_start() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> Execution stoped.\n");
if ((err = uc_reg_read(uc, UC_X86_REG_EIP, &eip))) {
fprintf(stderr, "Failed to uc_reg_read() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
fprintf(stdout, ">>> eip: 0x%"PRIx64"\n", eip);
fprintf(stdout, "\n");
if ((err = uc_close(uc))) {
fprintf(stderr, "Failed to uc_close() with error returned %u: %s\n", err, uc_strerror(err));
return -1;
}
return 0;
}
This program calls uc_emu_start()
three times:
- Run emulation until it hits 0x08000058 or runs 0x7fff instructions.
- Run just one instruction.
- Again, run emulation until it hits 0x08000058 or runs 0x7fff instructions.
The output of this program is as follows:
==================== 1. Run emulation until it hits 0x08000058 or runs 0x7fff instructions. ====================
[uc] translate tb 0x8000040: 360.094000us
>>> Tracing basic block at 0x8000040, block size = 0x18
0x8000040: endbr64
0x8000044: push rbp
0x8000045: mov rbp, rsp
0x8000048: mov dword ptr [rbp - 8], 0
0x800004f: mov dword ptr [rbp - 4], 0
0x8000056: jmp 0x8000072
[uc] exec tb 0x8000040: 2093.186000us
[uc] translate tb 0x8000072: 17.673000us
>>> Tracing basic block at 0x8000072, block size = 0x6
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
[uc] exec tb 0x8000072: 38.650000us
[uc] translate tb 0x8000058: 2.440000us
>>> Execution stoped.
>>> eip: 0x8000058
==================== 2. Run just one instruction. =====================
[uc] translate tb 0x8000058: 12.431000us
>>> Tracing basic block at 0x8000058, block size = 0x6
0x8000058: cmp dword ptr [rbp - 8], 0
0x800005c: jne 0x8000067
[uc] exec tb 0x8000058: 40.663000us
>>> Execution stoped.
>>> eip: 0x800005c
==================== 3. Again, run emulation until it hits 0x08000058 or runs 0x7fff instructions. ====================
[uc] translate tb 0x800005c: 19.594000us
>>> Tracing basic block at 0x800005c, block size = 0x2
0x800005c: jne 0x8000067
[uc] exec tb 0x800005c: 51.028000us
[uc] translate tb 0x800005e: 20.807000us
>>> Tracing basic block at 0x800005e, block size = 0x9
0x800005e: mov dword ptr [rbp - 8], 1
0x8000065: jmp 0x800006e
[uc] exec tb 0x800005e: 63.323000us
[uc] translate tb 0x800006e: 32.649000us
>>> Tracing basic block at 0x800006e, block size = 0xa
0x800006e: add dword ptr [rbp - 4], 1
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
[uc] exec tb 0x800006e: 69.026000us
>>> Tracing basic block at 0x8000058, block size = 0x6
0x8000058: cmp dword ptr [rbp - 8], 0
0x800005c: jne 0x8000067
[uc] exec tb 0x8000058: 56.497000us
[uc] translate tb 0x8000067: 41.810000us
>>> Tracing basic block at 0x8000067, block size = 0x11
0x8000067: mov dword ptr [rbp - 8], 0
0x800006e: add dword ptr [rbp - 4], 1
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
[uc] exec tb 0x8000067: 92.436000us
>>> Tracing basic block at 0x8000058, block size = 0x6
0x8000058: cmp dword ptr [rbp - 8], 0
0x800005c: jne 0x8000067
[uc] exec tb 0x8000058: 55.975000us
>>> Tracing basic block at 0x800005e, block size = 0x9
0x800005e: mov dword ptr [rbp - 8], 1
0x8000065: jmp 0x800006e
>>> Tracing basic block at 0x800006e, block size = 0xa
0x800006e: add dword ptr [rbp - 4], 1
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
>>> Tracing basic block at 0x8000058, block size = 0x6
0x8000058: cmp dword ptr [rbp - 8], 0
0x800005c: jne 0x8000067
>>> Tracing basic block at 0x8000067, block size = 0x11
0x8000067: mov dword ptr [rbp - 8], 0
0x800006e: add dword ptr [rbp - 4], 1
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
>>> Tracing basic block at 0x8000058, block size = 0x6
0x8000058: cmp dword ptr [rbp - 8], 0
0x800005c: jne 0x8000067
>>> Tracing basic block at 0x800005e, block size = 0x9
0x800005e: mov dword ptr [rbp - 8], 1
0x8000065: jmp 0x800006e
>>> Tracing basic block at 0x800006e, block size = 0xa
0x800006e: add dword ptr [rbp - 4], 1
0x8000072: cmp dword ptr [rbp - 4], 4
0x8000076: jle 0x8000058
[uc] exec tb 0x800005e: 437.196000us
[uc] translate tb 0x8000078: 34.233000us
>>> Tracing basic block at 0x8000078, block size = 0x4
0x8000078: nop
0x8000079: nop
0x800007a: pop rbp
0x800007b: ret
[uc] exec tb 0x8000078: 82.262000us
Failed to uc_emu_start() with error returned 6: Invalid memory read (UC_ERR_READ_UNMAPPED)
Unfortunately, the third uc_emu_start()
doesn't stop even though it has hit 0x08000058.
Cause
AFAIK, the cause of this issue is QEMU's TB cache. Unicorn inserts TCG-IRs, which stop emulation when it hits until
, when translating guest codes. Since QEMU doesn't know about it, it can re-execute TBs on the TB cache (with no code inserted for stopping emulation).
Solution/Workaround
I'm looking for a solution/workaround to this issue. One of the most reasonable workarounds is removing TB cache entries, including the address until
before calling uc_emu_start()
. For example:
uc_ctl_remove_cache(uc, until, until + 1);
uc_emu_start(uc, begin, until, timeout, count);
Are there any other solutions/workarounds?
question