作者:冬瓜
原文链接:Guardia · 瓜地
继续使用上一篇文中的代码示例:
#include <stdio.h>
#include "fishhook.h"
static int (*original_strlen)(const char *_s);
int new_strlen(const char *_s) {
return 666;
}
int main(int argc, const char * argv[]) {
struct rebinding strlen_rebinding = { "strlen", new_strlen,
(void *)&original_strlen };
rebind_symbols((struct rebinding[1]){ strlen_rebinding }, 1);
char *str = "hellolazy";
printf("%d\n", strlen(str));
return 0;
}
运行之后将其生成的执行文件拖入 MachOView 中。对 Mach-O 进行解析。
从 MachOView 中 Load Commands -> LC_SEGMENT_64(__LINKEDIT) 中获取到以下值:
进行下列*式(1)和式(2)*的计算:
继续看上文的这张图片:
此时,我们已经获取到了 Base Address,根据之前的流程,需要找到 __LINKEDIT Section 中的 Symbols 表、Indirect Symbols 表和 String 字符串表,这些地址我们需要在 Load Commands 中来获取。找到 LC_SYMTAB 和 LC_DYSYMTAB 这两个 Commaneds。
我们在 MachOView 中验证了三个表的位置。之后继续跟随着 fishhook 的思路来进行验证。下面将进入二尺遍历 Load Command 流程。这一次需要拿出的 Command 是 LC_SEGMENT(__DATA),目的是为了找到 __nl_symbol_ptr
和 __la_symbol_ptr
这两个 Section 的位置。
这里的 C 其实是上方 slide 常量的运算结果,由于在这个场景下 MachOView 验证时发现 slide = 0
,但不意味着 slide
是一个恒为 0 的值。vmaddr_slide
的取值其实是**地址空间布局随机化(ASLR)**机制的结果,这是一种针对缓冲区溢出的安全保护技术,这里不再赘述。
根据 Indirect Symbols 中的描述,发现下标标识的区域的起始位置与我们的计算相吻合,此处也再次验证成功。在 Indirect Symbols 的结构中,我们可以找到其 Lazy Symbol Pointer。例如,我们在代码中出现的 strlen
方法:
在 Lazy Symbol Pointers
这里,我们最终获取到了 0x10002068 -> _strlen
这个值。它起始是对于当前 _strlen
方法在符号表中的一个映射,我们可以简单的理解为它就是在符号表中对应方法的下标。
这个结论虽然至今笔者无法跟踪代码,将其结构实例化,但是可以通过 fishhook 的源码进行分析得出结论。
...
// 这里直接将 Lazy Symbol Point 的一个内容强转为二阶指针
// 也就是说明每个 Lazy Symbol Point 也是一个指针(当然从命名中也能猜出)
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
...
// 在遍历每一个内容的时候,先拿出其对应的 index
uint32_t symtab_index = indirect_symbol_indices[i];
// 之后直接从符号表中用下标访问的方法来获取信息
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
symtab[symtab_index].n_un.n_strx
从这个获取的方式来看,symtab_index
处的位置对应的是某个符号表的基址位,因此可以将类型转成 nlist_64
。
struct nlist_64 {
union {
uint32_t n_strx; /* 符号表中的位置 */
} n_un;
uint8_t n_type; /* 经过掩码处理,表示重定义符号不同的种类 */
uint8_t n_sect; /* section 的编号 */
uint16_t n_desc; /* 类似于 n_type 经过掩码处理,用来定义一些性质 */
uint64_t n_value; /* 记录信息的特殊值 */
};
为什么在 fishhook 源码中可以将符号名称直接通过 char *symbol_name = strtab + strtab_offset;
直接取得呢?因为在 String Table 存储过程中会在每一个符号名的末尾增加不确定个 \0
,而在 C 中一次的字符数组获取会以 \0
为结束。在 MachOView 中已经将符号名进行了解析,我们可以清晰的在解析后的 nlist
结构中发现符号名:
此时我们已经找到了改符号表的位置,剩下的工作仅需将 Lazy Symbol Pointers
中对应的指针进行指向变更即可完成操作。我在 fishhook 中加入了部分输出验证代码,可以大致验证这一地址的更变,但是由于在运行时有着 ASLR 的参与,是无法精准的将地址完全与 MachOView 的解析结果精准比对,但是从地址的大致区域中可以看出这个操作的成功性,以下是加入代码和输出的 log:
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
// 在 Indirect Symbol 表中检索到对应位置
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
// 获取 _DATA.__nl_symbol_ptr(或__la_symbol_ptr) Section
// 已知其 value 是一个指针类型,整段区域用二阶指针来获取
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 用 size / 一阶指针来计算个数,遍历整个 Section
for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 通过下标来获取每一个 Indirect Address 的 Value
// 这个 Value 也是外层寻址时需要的下标
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
// 获取符号名在字符表中的偏移地址
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 获取符号名
char *symbol_name = strtab + strtab_offset;
// 过滤掉符号名小于 4 位的符号
if (strnlen(symbol_name, 2) < 2) {
continue;
}
// 取出 rebindings 结构体实例数组,开始遍历链表
struct rebindings_entry *cur = rebindings;
while (cur) {
// 对于链表中每一个 rebindings 数组的每一个 rebinding 实例
// 依次在 String Table 匹配符号名
for (uint j = 0; j < cur->rebindings_nel; j++) {
// 符号名与方法名匹配
if (strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
// 如果是第一次对跳转地址进行重写
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
// 记录原始跳转地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
// 重写跳转地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
// 完成后不再对当前 Indirect Symbol 处理
// 继续迭代到下一个 Indirect Symbol
/** 加入调试代码 **/
printf("\n\nSymbol Name: %s\n", &symbol_name[1]);
printf("Rebinding Name: %s\n", cur->rebindings[j].name);
printf("Origin Addr: 0x%X\n", cur->rebindings[j].replaced);
printf("Rebinding Addr: 0x%X\n", cur->rebindings[j].replacement);
/** 调试代码 END **/
goto symbol_loop;
}
}
// 链表遍历
cur = cur->next;
}
symbol_loop:;
}
}
Symbol Name: strlen
Rebinding Name: strlen
Origin Addr: 0x20A0
Rebinding Addr: 0x1DA0
666
Program ended with exit code: 0
0x1DA0
这个地址在原始的间接表指向的位置之前,所以我们大致断定它处在 _TEXT
段。并且__stub_helper
, __cstring
, __unwoind_info
这些 Session 是我们无法直接干预的位置,所以可以猜测 0x1DA0
落在我们的 __text
Session 中。我们掏出 Hopper 来验证一下这个猜想,发现正是如此:
fishhook 的原理探究至此就告一段落。通过这个源码探求,强化了对于 Mach-O 的学习和认识,也在之中学习到了 Facebook 对于 Hook C 方法这个很妙的技巧。最后不得不再惊叹一句,FB 真的是令技术人向往的地方。