monitor memory read operation with VM_PROT_EXECUTE_ONLY
permission
#include "executeonly_monitor.h"
#include <mach/vm_param.h>
#include <mach/mach.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/mman.h>
#include <pthread.h>
#include <vector>
#include <unordered_map>
#include "logging/logging.h"
#define ALIGN_FLOOR(address, range) ((addr_t)address & ~((addr_t)range - 1))
typedef int32_t arm64_insn_t;
#define LOG_TAG "executeonly monitor"
#define submask(x) ((1L << ((x) + 1)) - 1)
#define bits(obj, st, fn) (((obj) >> (st)) & submask((fn) - (st)))
#define bit(obj, st) (((obj) >> (st)) & 1)
#define sbits(obj, st, fn) ((long)(bits(obj, st, fn) | ((long)bit(obj, fn) * ~submask(fn - st))))
std::unordered_map<addr_t, addr_t> *backup_pages = nullptr;
void handle_fault_with_execute_only_supported(arm_thread_state64_t *ts, addr_t ts_pc, addr_t fault_addr,
addr_t fault_page_addr) {
arm64_insn_t insn;
insn = *(arm64_insn_t *)ts_pc;
#if 0
// read fault insn
addr_t ts_pc_page = ALIGN_FLOOR(ts_pc, PAGE_SIZE);
auto iter = g_crc_page_map->find(ts_pc_page);
if (iter == g_crc_page_map->end()) {
insn = *(arm64_insn_t *)ts_pc;
} else {
insn = *(arm64_insn_t *)(iter->second.bakcup_page_addr + (ts_pc - ts_pc_page));
}
#endif
/* C4.1 A64 instruction set encoding */
/* C4.1.4 Loads and Stores */
int rn_ndx = -1;
int rt_ndx = -1;
if (insn & 0x0a000000) {
uint32_t op0 = (insn & 0xf0000000) >> 28;
uint32_t op1 = (insn & 0x08000000) >> 27;
uint32_t op2 = (insn & 0x01800000) >> 23;
uint32_t op4 = (insn & 0x00000c00) >> 10;
if (((op0 & 0b0011) == 0b01) && ((op2 & 0b10) == 0b00)) {
rn_ndx = -1;
} else {
rn_ndx = ((insn & 0x1e0) >> 5);
}
if (((op0 & 0b0011) == 0b11) && ((op2 & 0b10) == 0b00) && (op4 == 0b10)) {
uint32_t size = (insn & 0xc0000000) >> 30;
uint32_t V = (insn & 0x04000000) >> 26;
uint32_t opc = (insn & 0x00c00000) >> 22;
// ldrsw(register)
if (size == 0b10 && V == 0 && opc == 0b10) {
rt_ndx = insn & 0x1f;
if (backup_pages->count(fault_page_addr) == 0) {
return;
}
auto backup_page = (*backup_pages)[fault_page_addr];
auto rn = ts->__x[rn_ndx];
auto fault_backup_addr = backup_page + (fault_addr - fault_page_addr);
ts->__x[rt_ndx] = (int64_t) * (int32_t *)fault_backup_addr;
LOG(1, "set rt register: %p", fault_backup_addr);
}
}
}
rt_ndx = bits(insn, 0, 4);
int size_flag = bits(insn, 30, 31);
int opc = bits(insn, 22, 23);
int post_pre_flag = bits(insn, 10, 11);
LOG(1, "fault: post_pre: %d, size:%d, opc: %d, rn: %d, rt: %d", post_pre_flag, size_flag, opc, rn_ndx, rt_ndx);
if (rn_ndx >= 0) {
if (backup_pages->count(fault_page_addr) == 0) {
return;
}
auto backup_page = (*backup_pages)[fault_page_addr];
auto rn = ts->__x[rn_ndx];
auto new_rn = backup_page + (rn - fault_page_addr);
if ((size_flag & 0b00) == 0b00) {
*(uint8_t *)&ts->__x[rt_ndx] = *(uint8_t *)new_rn;
ts->__x[rn_ndx] += 1;
} else if ((size_flag & 0b01) == 0b01) {
*(uint16_t *)&ts->__x[rt_ndx] = *(uint16_t *)new_rn;
ts->__x[rn_ndx] += 2;
} else if ((size_flag & 0b10) == 0b10) {
*(uint32_t *)&ts->__x[rt_ndx] = *(uint32_t *)new_rn;
ts->__x[rn_ndx] += 4;
} else {
*(uint64_t *)&ts->__x[rt_ndx] = *(uint64_t *)new_rn;
ts->__x[rn_ndx] += 8;
}
}
if (rt_ndx >= 0) {
arm_thread_state64_set_pc_fptr(*ts, ts_pc + 4);
}
}
void set_page_execute_only(void *addr) {
size_t page_size = sysconf(_SC_PAGESIZE);
addr_t page = ALIGN_FLOOR(addr, page_size);
#if 0
kern_return_t kr;
kr = vm_protect(mach_task_self(), (mach_vm_address_t) page, (mach_vm_size_t) page_size, false, VM_PROT_EXECUTE_ONLY);
if (kr != KERN_SUCCESS) {
ERROR_LOG("failed: %s", mach_error_string(kr));
}
#else
int ret = mprotect((void *)page, page_size, VM_PROT_EXECUTE_ONLY);
if (ret) {
LOG(1, "mprotect failed: %s");
}
#endif
}
static mach_port_t exception_port = MACH_PORT_NULL;
static bool check_if_fall_loop(arm_thread_state64_t *ts, arm_exception_state64_t *es, addr_t fault_addr) {
// stack backtrace
addr_t ts_fp = __darwin_arm_thread_state64_get_fp(*ts);
uint64_t fp_frame[2] = {0};
// fault at same address multi-times
bool is_multi_same_fault = false;
static int fault_stack_count = 0;
static addr_t fault_stack[16] = {0};
fault_stack[fault_stack_count++ % 16] = fault_addr;
int count = 0;
for (int i = 0; i < 16; i++) {
if (fault_stack[i] == fault_addr)
count += 1;
}
if (count >= 13) {
is_multi_same_fault = true;
}
return is_multi_same_fault;
}
static bool check_if_invalid_access(addr_t fault_addr) {
bool is_invalid_access = false;
if (fault_addr < 0x100000000 || fault_addr > 0x800000000) {
is_invalid_access = true;
}
return is_invalid_access;
}
static void *exception_handler(void *ctx) {
Request In0P;
mach_msg_header_t *InHeadP = &In0P.Head;
for (;;) {
kern_return_t kr;
kr = mach_msg(&In0P.Head, MACH_RCV_MSG | MACH_MSG_TIMEOUT_NONE, 0, sizeof(Request), exception_port,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return NULL;
}
mach_port_t thread_port = In0P.thread.name;
mach_port_t task_port = In0P.task.name;
arm_thread_state64_t ts = {0};
mach_msg_type_number_t ts_cnt = ARM_THREAD_STATE64_COUNT;
kr = thread_get_state(thread_port, ARM_THREAD_STATE64, (thread_state_t)&ts, &ts_cnt);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return NULL;
}
arm_exception_state64_t es = {0};
mach_msg_type_number_t es_cnt = ARM_EXCEPTION_STATE64_COUNT;
kr = thread_get_state(thread_port, ARM_EXCEPTION_STATE64, (thread_state_t)&es, &es_cnt);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return NULL;
}
addr_t ts_pc = __darwin_arm_thread_state64_get_pc(ts);
addr_t fault_addr = es.__far;
addr_t fault_page_addr = ALIGN_FLOOR(fault_addr, PAGE_SIZE);
LOG(1, "fault: at %p, pc %p", fault_addr, ts_pc);
if (!check_if_invalid_access(fault_addr)) {
handle_fault_with_execute_only_supported(&ts, ts_pc, fault_addr, fault_page_addr);
}
kr = thread_set_state(thread_port, ARM_THREAD_STATE64, (thread_state_t)&ts, ARM_THREAD_STATE64_COUNT);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return NULL;
}
// reply
typedef __Reply__mach_exception_raise_t Reply __attribute__((unused));
Reply OutP;
if (check_if_invalid_access(fault_addr)) {
OutP.RetCode = KERN_FAILURE;
}
// reply
kr = mach_msg(&OutP.Head, MACH_SEND_MSG | MACH_MSG_TIMEOUT_NONE, sizeof(Reply), 0, MACH_PORT_NULL,
MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL);
if (kr != KERN_SUCCESS) {
LOG(1, " failed: %s", mach_error_string(kr));
return NULL;
}
}
return NULL;
}
void install_memory_read_exception_callback() {
static bool initialized = false;
if (initialized)
return;
initialized = true;
kern_return_t kr = KERN_SUCCESS;
kr = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_RECEIVE, &exception_port);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return;
}
kr = mach_port_insert_right(mach_task_self(), exception_port, exception_port, MACH_MSG_TYPE_MAKE_SEND);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return;
}
// set exception handler
kr = task_set_exception_ports(mach_task_self(), EXC_MASK_ALL, exception_port,
EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, ARM_THREAD_STATE64);
if (kr != KERN_SUCCESS) {
LOG(1, "failed: %s", mach_error_string(kr));
return;
}
// setup a new thread where to handle the exceptions
pthread_t exception_handler_thread;
pthread_create(&exception_handler_thread, NULL, exception_handler, NULL);
LOG(1, "install memory read exception(port is %p) callback done", exception_port);
}
void executeonly_monitor_init() {
install_memory_read_exception_callback();
}
addr_t allocate_page() {
auto page = (addr_t)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, VM_MAKE_TAG(255), 0);
if ((void *)page == MAP_FAILED) {
LOG(1, "mmap failed");
return 0;
}
return page;
}
void allocate_backup_page(void *addr, char *buffer, int buffer_size) {
addr_t page_addr = ALIGN_FLOOR(addr, PAGE_SIZE);
auto backup_page_addr = allocate_page();
memcpy((void *)backup_page_addr, (void *)page_addr, PAGE_SIZE);
uint32_t page_offset = (addr_t)addr - page_addr;
memcpy((void *)(backup_page_addr + page_offset), buffer, buffer_size);
backup_pages->insert(std::make_pair(page_addr, backup_page_addr));
LOG(1, "allocate backup page: %p --> %p, %p, %p", page_addr, backup_page_addr, *(uint64_t *)page_addr,
*(uint64_t *)backup_page_addr);
}
int executeonly_monitor(void *addr, char *buffer, int buffer_size) {
if (backup_pages == nullptr) {
backup_pages = new std::unordered_map<addr_t, addr_t>();
}
addr_t page_addr = ALIGN_FLOOR(addr, PAGE_SIZE);
LOG(1, "start monitor %p page", page_addr);
allocate_backup_page(addr, buffer, buffer_size);
set_page_execute_only(addr);
return 0;
}