rizin/librz/bin/format/mach0/mach0_relocs.c

701 lines
19 KiB
C

// SPDX-FileCopyrightText: 2021-2023 Florian Märkl <info@florianmaerkl.de>
// SPDX-FileCopyrightText: 2020 Francesco Tamagni <mrmacete@protonmail.ch>
// SPDX-FileCopyrightText: 2010-2020 nibble <nibble.ds@gmail.com>
// SPDX-FileCopyrightText: 2010-2020 pancake <pancake@nopcode.org>
// SPDX-License-Identifier: LGPL-3.0-only
#include <rz_util.h>
#include "mach0.h"
#include <ht_uu.h>
#include "mach0_utils.inc"
#define RELOCATION_INFO_SIZE 8 // sizeof(struct relocation_info)
/**
* \param buf buffer of at least RELOCATION_INFO_SIZE bytes
*/
static void read_relocation_info(struct relocation_info *dst, ut8 *src, bool big_endian) {
dst->r_address = (st32)rz_read_at_ble32(src, 0, big_endian);
uint32_t field = rz_read_at_ble32(src, 4, big_endian);
if (big_endian) {
dst->r_type = field & rz_num_bitmask(4);
field >>= 4;
dst->r_extern = field & rz_num_bitmask(1);
field >>= 1;
dst->r_length = field & rz_num_bitmask(2);
field >>= 2;
dst->r_pcrel = field & rz_num_bitmask(1);
field >>= 1;
dst->r_symbolnum = field & rz_num_bitmask(24);
} else {
dst->r_symbolnum = field & rz_num_bitmask(24);
field >>= 24;
dst->r_pcrel = field & rz_num_bitmask(1);
field >>= 1;
dst->r_length = field & rz_num_bitmask(2);
field >>= 2;
dst->r_extern = field & rz_num_bitmask(1);
field >>= 1;
dst->r_type = field & rz_num_bitmask(4);
}
}
static int reloc_comparator(struct reloc_t *a, struct reloc_t *b) {
return a->addr - b->addr;
}
static void parse_relocation_info(struct MACH0_(obj_t) * bin, RzSkipList *relocs, ut32 offset, ut32 num) {
if (!num || !offset || (st32)num < 0) {
return;
}
ut64 total_size = (ut64)num * RELOCATION_INFO_SIZE;
ut8 *infos = malloc(total_size);
if (!infos) {
return;
}
if (rz_buf_read_at(bin->b, offset, infos, total_size) < total_size) {
free(infos);
return;
}
size_t i;
for (i = 0; i < num; i++) {
struct relocation_info a_info;
read_relocation_info(&a_info, infos + i * RELOCATION_INFO_SIZE, bin->big_endian);
ut32 sym_num = a_info.r_symbolnum;
if (sym_num >= bin->nsymtab) {
continue;
}
ut32 stridx = bin->symtab[sym_num].n_strx;
char *sym_name = MACH0_(get_name)(bin, stridx, false);
if (!sym_name) {
continue;
}
struct reloc_t *reloc = RZ_NEW0(struct reloc_t);
if (!reloc) {
free(infos);
free(sym_name);
return;
}
reloc->addr = MACH0_(paddr_to_vaddr)(bin, a_info.r_address);
reloc->offset = a_info.r_address;
reloc->ord = sym_num;
reloc->type = a_info.r_type; // enum RelocationInfoType
reloc->external = a_info.r_extern;
reloc->pc_relative = a_info.r_pcrel;
reloc->chained = false;
reloc->size = 1 << a_info.r_length; // macho/reloc.h says: 0=byte, 1=word, 2=long, 3=quad
rz_str_ncpy(reloc->name, sym_name, sizeof(reloc->name) - 1);
rz_skiplist_insert(relocs, reloc);
free(sym_name);
}
free(infos);
}
static bool is_valid_ordinal_table_size(ut64 size) {
return size > 0 && size <= UT16_MAX;
}
static int parse_import_ptr(struct MACH0_(obj_t) * bin, struct reloc_t *reloc, int idx) {
int i, j, sym;
size_t wordsize;
ut32 stype;
wordsize = get_word_size(bin);
if (idx < 0 || idx >= bin->nsymtab) {
return 0;
}
if ((bin->symtab[idx].n_desc & REFERENCE_TYPE) == REFERENCE_FLAG_UNDEFINED_LAZY) {
stype = S_LAZY_SYMBOL_POINTERS;
} else {
stype = S_NON_LAZY_SYMBOL_POINTERS;
}
reloc->offset = 0;
reloc->addr = 0;
reloc->addend = 0;
#define CASE(T) \
case ((T) / 8): reloc->type = RZ_BIN_RELOC_##T; break
switch (wordsize) {
CASE(8);
CASE(16);
CASE(32);
CASE(64);
default: return false;
}
#undef CASE
for (i = 0; i < bin->nsects; i++) {
if ((bin->sects[i].flags & SECTION_TYPE) == stype) {
for (j = 0, sym = -1; bin->sects[i].reserved1 + j < bin->nindirectsyms; j++) {
int indidx = bin->sects[i].reserved1 + j;
if (indidx < 0 || indidx >= bin->nindirectsyms) {
break;
}
if (idx == bin->indirectsyms[indidx]) {
sym = j;
break;
}
}
reloc->offset = sym == -1 ? 0 : bin->sects[i].offset + sym * wordsize;
reloc->addr = sym == -1 ? 0 : bin->sects[i].addr + sym * wordsize;
return true;
}
}
return false;
}
typedef struct {
struct MACH0_(obj_t) * obj;
RzSkipList *dst;
} FixupsAsRelocsCtx;
static void fixups_as_relocs_cb(struct mach0_chained_fixup_t *fixup, void *user) {
FixupsAsRelocsCtx *ctx = user;
struct reloc_t *reloc = RZ_NEW0(struct reloc_t);
if (!reloc) {
return;
}
reloc->offset = fixup->paddr;
reloc->addr = ctx->obj->baddr + fixup->paddr;
reloc->addend = fixup->addend;
reloc->type = fixup->size == 4 ? RZ_BIN_RELOC_32 : RZ_BIN_RELOC_64;
reloc->ord = fixup->is_bind ? fixup->bind_ordinal : -1;
reloc->external = fixup->is_bind;
reloc->chained = true;
reloc->size = fixup->size;
if (fixup->is_bind) {
struct MACH0_(chained_import_t) import;
if (MACH0_(get_chained_import)(ctx->obj, fixup->bind_ordinal, &import)) {
char *name = MACH0_(chained_import_read_symbol_name)(ctx->obj, &import);
if (name) {
strncpy(reloc->name, name, sizeof(reloc->name) - 1);
free(name);
}
}
}
rz_skiplist_insert(ctx->dst, reloc);
}
static void parse_relocs_from_chained_fixups(RzSkipList *dst, struct MACH0_(obj_t) * bin) {
FixupsAsRelocsCtx ctx = {
.obj = bin,
.dst = dst
};
// clang-format off
MACH0_(chained_fixups_foreach)(bin, fixups_as_relocs_cb, &ctx);
// clang-format on
}
RZ_API void MACH0_(bind_opcodes_foreach)(struct MACH0_(obj_t) * bin,
RZ_NONNULL BindOpcodesThreadedTableSizeCb threaded_table_size_cb,
RZ_NULLABLE BindOpcodesBindCb do_bind_cb,
RZ_NONNULL BindOpcodesThreadedApplyCb threaded_apply_cb,
void *user) {
if (!bin->dyld_info) {
return;
}
ut8 *opcodes, rel_type = 0;
size_t bind_size, lazy_size, weak_size;
size_t wordsize = get_word_size(bin);
#define CASE(T) \
case ((T) / 8): rel_type = RZ_BIN_RELOC_##T; break
switch (wordsize) {
CASE(8);
CASE(16);
CASE(32);
CASE(64);
default: return;
}
#undef CASE
bind_size = bin->dyld_info->bind_size;
lazy_size = bin->dyld_info->lazy_bind_size;
weak_size = bin->dyld_info->weak_bind_size;
if (!bind_size && !lazy_size) {
return;
}
if ((bind_size + lazy_size) < 1) {
return;
}
if (bin->dyld_info->bind_off > bin->size || bin->dyld_info->bind_off + bind_size > bin->size) {
return;
}
if (bin->dyld_info->lazy_bind_off > bin->size ||
bin->dyld_info->lazy_bind_off + lazy_size > bin->size) {
return;
}
if (bin->dyld_info->bind_off + bind_size + lazy_size > bin->size) {
return;
}
if (bin->dyld_info->weak_bind_off + weak_size > bin->size) {
return;
}
ut64 amount = bind_size + lazy_size + weak_size;
if (amount == 0 || amount > UT32_MAX) {
return;
}
if (!bin->segs) {
return;
}
opcodes = calloc(1, amount + 1);
if (!opcodes) {
return;
}
int len = rz_buf_read_at(bin->b, bin->dyld_info->bind_off, opcodes, bind_size);
len += rz_buf_read_at(bin->b, bin->dyld_info->lazy_bind_off, opcodes + bind_size, lazy_size);
len += rz_buf_read_at(bin->b, bin->dyld_info->weak_bind_off, opcodes + bind_size + lazy_size, weak_size);
if (len < amount) {
RZ_LOG_ERROR("Error: read (dyld_info bind) at 0x%08" PFMT64x "\n", (ut64)(size_t)bin->dyld_info->bind_off);
RZ_FREE(opcodes);
return;
}
bool is_threaded = false;
size_t partition_sizes[] = { bind_size, lazy_size, weak_size };
size_t pidx;
int opcodes_offset = 0;
for (pidx = 0; pidx < RZ_ARRAY_SIZE(partition_sizes); pidx++) {
size_t partition_size = partition_sizes[pidx];
ut8 type = 0;
int lib_ord = 0, seg_idx = -1, sym_ord = -1;
char *sym_name = NULL;
size_t j, count, skip;
st64 addend = 0;
ut64 seg_off = 0;
ut64 seg_size = bin->segs[0].vmsize;
ut8 *p = opcodes + opcodes_offset;
ut8 *end = p + partition_size;
bool done = false;
while (!done && p < end) {
ut8 imm = *p & BIND_IMMEDIATE_MASK;
ut8 op = *p & BIND_OPCODE_MASK;
p++;
switch (op) {
case BIND_OPCODE_DONE: {
bool in_lazy_binds = pidx == 1;
if (!in_lazy_binds) {
done = true;
}
break;
}
case BIND_OPCODE_THREADED: {
switch (imm) {
case BIND_SUBOPCODE_THREADED_SET_BIND_ORDINAL_TABLE_SIZE_ULEB: {
is_threaded = true;
if (!threaded_table_size_cb) {
continue;
}
ut64 table_size = read_uleb128(&p, end);
if (!is_valid_ordinal_table_size(table_size)) {
RZ_LOG_ERROR("Error: BIND_SUBOPCODE_THREADED_SET_BIND_ORDINAL_TABLE_SIZE_ULEB size is wrong\n");
break;
}
threaded_table_size_cb(table_size, user);
sym_ord = 0;
break;
}
case BIND_SUBOPCODE_THREADED_APPLY:
if (threaded_apply_cb) {
threaded_apply_cb(seg_idx, seg_off, user);
}
break;
default:
RZ_LOG_ERROR("Error: Unexpected BIND_OPCODE_THREADED sub-opcode: 0x%x\n", imm);
}
break;
}
case BIND_OPCODE_SET_DYLIB_ORDINAL_IMM:
lib_ord = imm;
break;
case BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB:
lib_ord = read_uleb128(&p, end);
break;
case BIND_OPCODE_SET_DYLIB_SPECIAL_IMM:
lib_ord = imm ? (st8)(BIND_OPCODE_MASK | imm) : 0;
break;
case BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM: {
sym_name = (char *)p;
while (*p++ && p < end) {
/* empty loop */
}
if (p == end) {
// string not zero-terminated
sym_name = NULL;
break;
}
if (is_threaded) {
break;
}
sym_ord = -1;
if (bin->symtab && bin->dysymtab.nundefsym < UT16_MAX) {
for (j = 0; j < bin->dysymtab.nundefsym; j++) {
size_t stridx = 0;
bool found = false;
int iundefsym = bin->dysymtab.iundefsym;
if (iundefsym >= 0 && iundefsym < bin->nsymtab) {
int sidx = iundefsym + j;
if (sidx < 0 || sidx >= bin->nsymtab) {
continue;
}
stridx = bin->symtab[sidx].n_strx;
if (stridx >= bin->symstrlen) {
continue;
}
found = true;
}
if (found && !strcmp((const char *)bin->symstr + stridx, sym_name)) {
sym_ord = j;
break;
}
}
}
break;
}
case BIND_OPCODE_SET_TYPE_IMM:
type = imm;
break;
case BIND_OPCODE_SET_ADDEND_SLEB:
addend = rz_sleb128((const ut8 **)&p, end);
break;
case BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB:
seg_idx = imm;
if (seg_idx >= bin->nsegs) {
RZ_LOG_ERROR("Error: BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB"
" has unexistent segment %d\n",
seg_idx);
goto beach;
}
seg_off = read_uleb128(&p, end);
seg_size = bin->segs[seg_idx].vmsize;
break;
case BIND_OPCODE_ADD_ADDR_ULEB:
seg_off += read_uleb128(&p, end);
break;
#define DO_BIND() \
do { \
ut64 final_addend = addend; \
ut64 vaddr = 0; \
ut64 paddr = 0; \
if (!is_threaded && seg_idx < 0) { \
break; \
} \
if (seg_idx >= 0) { \
vaddr = bin->segs[seg_idx].vmaddr + seg_off; \
paddr = bin->segs[seg_idx].fileoff + seg_off; \
if (type == BIND_TYPE_TEXT_PCREL32) { \
final_addend = addend - (bin->baddr + vaddr); \
} \
} \
do_bind_cb(paddr, vaddr, final_addend, rel_type, lib_ord, sym_ord, sym_name, user); \
} while (0)
case BIND_OPCODE_DO_BIND:
if (!is_threaded && seg_off >= seg_size) {
RZ_LOG_ERROR("Error: Malformed DO bind opcode 0x%" PFMT64x "\n", seg_off);
goto beach;
}
DO_BIND();
if (is_threaded) {
sym_ord++;
} else {
seg_off += wordsize;
}
break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB:
if (seg_off >= seg_size) {
RZ_LOG_ERROR("Error: Malformed ADDR ULEB bind opcode\n");
goto beach;
}
DO_BIND();
seg_off += read_uleb128(&p, end) + wordsize;
break;
case BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED:
if (seg_off >= seg_size) {
RZ_LOG_ERROR("Error: Malformed IMM SCALED bind opcode\n");
goto beach;
}
DO_BIND();
seg_off += (ut64)imm * (ut64)wordsize + wordsize;
break;
case BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB:
count = read_uleb128(&p, end);
skip = read_uleb128(&p, end);
for (j = 0; j < count; j++) {
if (seg_off >= seg_size) {
RZ_LOG_ERROR("Error: Malformed ULEB TIMES bind opcode\n");
goto beach;
}
DO_BIND();
seg_off += skip + wordsize;
}
break;
#undef DO_BIND
default:
RZ_LOG_ERROR("Error: unknown bind opcode 0x%02x in dyld_info\n", *p);
goto beach;
}
}
opcodes_offset += partition_size;
}
beach:
RZ_FREE(opcodes);
}
typedef struct {
struct MACH0_(obj_t) * bin;
RzSkipList *dst;
} BindCommandsRelocsCtx;
static void threaded_table_size(ut64 table_size, void *user) {}
static void do_bind(ut64 paddr, ut64 vaddr, st64 addend, ut8 rel_type, int lib_ord, int sym_ord, const char *sym_name, void *user) {
if (sym_ord < 0 && !sym_name) {
return;
}
BindCommandsRelocsCtx *ctx = user;
struct reloc_t *reloc = RZ_NEW0(struct reloc_t);
reloc->addr = vaddr;
reloc->offset = paddr;
(void)lib_ord; // available, but currently unused info
reloc->ord = sym_ord;
reloc->type = rel_type;
if (sym_name)
rz_str_ncpy(reloc->name, sym_name, 256);
rz_skiplist_insert(ctx->dst, reloc);
}
static void threaded_apply(int seg_idx, ut64 seg_off, void *user) {
// This is handled in mach0_chained_fixups.c
// We only consider classic non-threaded binds here
}
static void parse_relocs_from_bind_opcodes(RzSkipList *dst, struct MACH0_(obj_t) * bin) {
BindCommandsRelocsCtx ctx = {
.bin = bin,
.dst = dst
};
// clang-format off
MACH0_(bind_opcodes_foreach)(bin, threaded_table_size, do_bind, threaded_apply, &ctx);
// clang-format on
}
static void parse_relocs_from_indirect_syms(RzSkipList *dst, struct MACH0_(obj_t) * bin) {
if (!bin->symtab || !bin->symstr || !bin->sects || !bin->indirectsyms) {
return;
}
int amount = bin->dysymtab.nundefsym;
if (amount < 0) {
amount = 0;
}
for (int j = 0; j < amount; j++) {
struct reloc_t *reloc = RZ_NEW0(struct reloc_t);
if (!reloc || !parse_import_ptr(bin, reloc, bin->dysymtab.iundefsym + j)) {
free(reloc);
break;
}
reloc->ord = j;
RzSkipListNode *node = rz_skiplist_insert(dst, reloc);
if (node->data != reloc) {
free(reloc);
}
}
}
static void parse_relocs_from_extrels(RzSkipList *dst, struct MACH0_(obj_t) * bin) {
if (!bin->symtab || !bin->dysymtab.extreloff || !bin->dysymtab.nextrel) {
return;
}
parse_relocation_info(bin, dst, bin->dysymtab.extreloff, bin->dysymtab.nextrel);
}
RZ_BORROW RzSkipList /* struct reloc_t * */ *MACH0_(get_relocs)(struct MACH0_(obj_t) * bin) {
rz_return_val_if_fail(bin, NULL);
if (bin->relocs_parsed) {
return bin->relocs;
}
bin->relocs_parsed = true;
bin->relocs = rz_skiplist_new((RzListFree)&free, (RzListComparator)&reloc_comparator);
if (!bin->relocs) {
return NULL;
}
if (MACH0_(has_chained_fixups)(bin)) {
parse_relocs_from_chained_fixups(bin->relocs, bin);
} else {
parse_relocs_from_bind_opcodes(bin->relocs, bin);
parse_relocs_from_indirect_syms(bin->relocs, bin);
parse_relocs_from_extrels(bin->relocs, bin);
}
return bin->relocs;
}
static RzPVector /*<struct reloc_t *>*/ *get_patchable_relocs(struct MACH0_(obj_t) * obj) {
if (!obj->options.patch_relocs) {
return NULL;
}
if (obj->patchable_relocs) {
return obj->patchable_relocs;
}
RzSkipList *relocs = MACH0_(get_relocs)(obj);
if (!relocs) {
return NULL;
}
obj->patchable_relocs = rz_pvector_new(NULL);
if (!obj->patchable_relocs) {
return NULL;
}
RzSkipListNode *it;
struct reloc_t *reloc;
rz_skiplist_foreach (relocs, it, reloc) {
if (!reloc->external) {
// right now, we only care about patching external relocs
// others might be interesting too in the future though, for example in object files.
continue;
}
rz_pvector_push(obj->patchable_relocs, reloc);
}
return obj->patchable_relocs;
}
RZ_API bool MACH0_(needs_reloc_patching)(struct MACH0_(obj_t) * obj) {
rz_return_val_if_fail(obj, false);
RzPVector *patchable_relocs = get_patchable_relocs(obj);
return patchable_relocs && rz_pvector_len(patchable_relocs);
}
RZ_API ut64 MACH0_(reloc_target_size)(struct MACH0_(obj_t) * obj) {
int bits = MACH0_(get_bits_from_hdr)(&obj->hdr);
if (bits) {
return 8;
}
return bits / 8;
}
/// size of the artificial reloc target vfile
RZ_API ut64 MACH0_(reloc_targets_vfile_size)(struct MACH0_(obj_t) * obj) {
RzPVector *patchable_relocs = get_patchable_relocs(obj);
if (!patchable_relocs) {
return 0;
}
return rz_pvector_len(patchable_relocs) * MACH0_(reloc_target_size)(obj);
}
/// base vaddr where to map the artificial reloc target vfile
RZ_API ut64 MACH0_(reloc_targets_map_base)(RzBinFile *bf, struct MACH0_(obj_t) * obj) {
if (obj->reloc_targets_map_base_calculated) {
return obj->reloc_targets_map_base;
}
RzList *maps = MACH0_(get_maps_unpatched)(bf);
obj->reloc_targets_map_base = rz_bin_relocs_patch_find_targets_map_base(maps, MACH0_(reloc_target_size)(obj));
rz_list_free(maps);
obj->reloc_targets_map_base_calculated = true;
return obj->reloc_targets_map_base;
}
static bool _patch_reloc(struct MACH0_(obj_t) * bin, struct reloc_t *reloc, ut64 symbol_at) {
ut64 pc = reloc->addr;
ut64 ins_len = 0;
if (!reloc->chained) {
switch (bin->hdr.cputype) {
case CPU_TYPE_X86_64: {
switch (reloc->type) {
case X86_64_RELOC_UNSIGNED:
break;
case X86_64_RELOC_BRANCH:
pc -= 1;
ins_len = 5;
break;
default:
RZ_LOG_ERROR("Warning: unsupported reloc type for X86_64 (%d), please file a bug.\n", reloc->type);
return false;
}
break;
}
case CPU_TYPE_ARM64:
case CPU_TYPE_ARM64_32:
pc = reloc->addr & ~3;
ins_len = 4;
break;
case CPU_TYPE_ARM:
break;
default:
RZ_LOG_ERROR("Warning: unsupported architecture for patching relocs, please file a bug. %s\n", MACH0_(get_cputype_from_hdr)(&bin->hdr));
return false;
}
}
ut64 val = symbol_at;
if (reloc->pc_relative) {
val = symbol_at - pc - ins_len;
}
ut8 buf[8];
rz_write_ble(buf, val, false, reloc->size * 8);
rz_buf_write_at(bin->buf_patched, reloc->offset, buf, RZ_MIN(sizeof(buf), reloc->size));
return true;
}
/**
* \brief Patching of external relocs in a sparse overlay buffer
*
* This patches both classic Mach-O relocs and modern dyld chained pointers
*/
RZ_API void MACH0_(patch_relocs)(RzBinFile *bf, struct MACH0_(obj_t) * obj) {
rz_return_if_fail(obj);
if (obj->relocs_patched) {
return;
}
bool needs_reloc_patch = MACH0_(needs_reloc_patching)(obj);
bool needs_chained_patch = MACH0_(has_chained_fixups)(obj);
if (obj->relocs_patched || (!needs_reloc_patch && !needs_chained_patch)) {
return;
}
obj->relocs_patched = true; // run this function just once (lazy relocs patching)
obj->buf_patched = rz_buf_new_sparse_overlay(obj->b, RZ_BUF_SPARSE_WRITE_MODE_SPARSE);
if (!obj->buf_patched) {
return;
}
if (needs_reloc_patch) {
ut64 cdsz = MACH0_(reloc_target_size)(obj);
ut64 size = MACH0_(reloc_targets_vfile_size)(obj);
if (!size) {
return;
}
RzBinRelocTargetBuilder *targets = rz_bin_reloc_target_builder_new(cdsz, MACH0_(reloc_targets_map_base)(bf, obj));
if (!targets) {
return;
}
RzPVector *patchable_relocs = get_patchable_relocs(obj);
void **it;
rz_pvector_foreach (patchable_relocs, it) {
struct reloc_t *reloc = *it;
ut64 sym_addr = rz_bin_reloc_target_builder_get_target(targets, reloc->ord);
reloc->target = sym_addr;
_patch_reloc(obj, reloc, sym_addr);
}
rz_bin_reloc_target_builder_free(targets);
}
if (needs_chained_patch) {
// clang-format off
MACH0_(patch_chained_fixups)(obj, obj->buf_patched);
// clang-format on
}
// from now on, all writes should propagate through to the actual file
rz_buf_sparse_set_write_mode(obj->buf_patched, RZ_BUF_SPARSE_WRITE_MODE_THROUGH);
}