如何正确的hook方法objc_msgSend

前言

如果希望对 Objective-C 的方法调用进行 log, 一个很好的解决方法就是 hook 方法 objc_msgSend, 当然想到的就是利用 InlinkHook 直接 hook 完事, 然而 objc_msgSend 是一个可变参数函数, 这就有点蛋疼了.

objc4-680, 和目前的 objc4-709 没有有很大出入.

以下 在举例 arm 相关时使用 objc4-680, 说明 x64 时使用 objc4-709

整个代码使用 c++, 所以有些地方需要参考 objc 的源码去造一个轮子, 比如 object_getClass 等.

这篇文章假设读者对以下有了解.

  1. OC 类的内存布局模型
  2. OC 实例的内存布局模型
  3. OC 函数调用与消息传递机制
  4. macho 文件格式
  5. 基本 x64, arm 汇编指令
  6. 函数调用与参数传递的 x64, arm 汇编实现机制(函数调用约定)
  7. inlinehook 机制

Hook 思路

这里首先明确, objc_msgSend 的第三个参数是不定参数, 无法确定 objc_msgSend 的具体函数签名, 也就无法通过传参来调用原函数, 所以只能上暴力的方法, 通过保存/恢复栈和寄存器方法调用原函数, 之后在汇编指令中实现原函数的跳转调用.

objc_msgSend 的函数声明

1
2
// runtime/message.h
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)

objc_msgSend 的函数实现, 这里以 x64 和 arm64 举例.

ARM64 下 ojbc_msgSend 实现机制

_objc_msgSend 首先会取得基本的参数, 比如 isa, class, 之后会使用宏 CacheLookup 先进行缓存查找, 如果没有命中, 会触发 JumpMiss 宏处理, 跳转到 __objc_msgSend_uncached_impcache, 这个方法是了解如何正确 hook 的关键.

1
2
3
4
5
6
7
8
9
10
// objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s
ENTRY _objc_msgSend
MESSENGER_START
cmp x0, #0 // nil check and tagged pointer check
b.le LNilOrTagged // (MSB tagged pointer looks negative)
ldr x13, [x0] // x13 = isa
and x9, x13, #ISA_MASK // x9 = class
LGetIsaDone:
CacheLookup NORMAL // calls imp or objc_msgSend_uncached
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s
.macro JumpMiss
.if $0 == NORMAL
b __objc_msgSend_uncached_impcache
.else
b LGetImpMiss
.endif
.endmacro
.macro CacheLookup
// x1 = SEL, x9 = isa
ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask
and w12, w1, w11 // x12 = _cmd & mask
add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4)
ldp x16, x17, [x12] // {x16, x17} = *bucket
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket
b 1b // loop
3: // wrap: x12 = first bucket, w11 = mask
add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp x16, x17, [x12] // {x16, x17} = *bucket
1: cmp x16, x1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: x12 = not-hit bucket
CheckMiss $0 // miss if bucket->cls == 0
cmp x12, x10 // wrap if bucket == buckets
b.eq 3f
ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket
b 1b // loop
3: // double wrap
JumpMiss $0
.endmacro

具体解析下 __objc_msgSend_uncached_impcache 这个函数, 在函数入口保存寄存器和返回地址, 在调用 __class_lookupMethodAndLoadCache3 之后进行恢复. __class_lookupMethodAndLoadCache3 函数返回需要调用函数的地址.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s
STATIC_ENTRY __objc_msgSend_uncached_impcache
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band x9 is the class to search
MESSENGER_START
// push frame
stp fp, lr, [sp, #-16]!
mov fp, sp
MESSENGER_END_SLOW
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// receiver and selector already in x0 and x1
mov x2, x9
bl __class_lookupMethodAndLoadCache3
// imp in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
br x17
END_ENTRY __objc_msgSend_uncached_impcache

举个例子演示下, 这里可以仔细观察 lldb 的输出.

x64 下 ojbc_msgSend 实现机制

整体思路与 arm64 类似, 但几个关键部分不同, 这里主要关注 GetIsaFastMethodTableLookup, MethodTableLookup 是在缓存中没有命中时进行查找.

1
2
3
4
5
6
7
8
9
objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
MESSENGER_START
NilTest NORMAL
GetIsaFast NORMAL // r10 = self->isa
CacheLookup NORMAL, CALL // calls IMP on success
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s
.macro GetIsaFast
.if $0 != STRET
testb $$1, %a1b
PN
jnz LGetIsaSlow_f
movq $$0x00007ffffffffff8, %r10
andq (%a1), %r10
.else
testb $$1, %a2b
PN
jnz LGetIsaSlow_f
movq $$0x00007ffffffffff8, %r10
andq (%a2), %r10
.endif
LGetIsaDone:
.endmacro

这里需要关注 x64 保存和恢复参数的操作, 特别是这里的栈对齐.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s
.macro MethodTableLookup
push %rbp
mov %rsp, %rbp
sub $$0x80+8, %rsp // +8 for alignment
movdqa %xmm0, -0x80(%rbp)
push %rax // might be xmm parameter count
movdqa %xmm1, -0x70(%rbp)
push %a1
movdqa %xmm2, -0x60(%rbp)
push %a2
movdqa %xmm3, -0x50(%rbp)
push %a3
movdqa %xmm4, -0x40(%rbp)
push %a4
movdqa %xmm5, -0x30(%rbp)
push %a5
movdqa %xmm6, -0x20(%rbp)
push %a6
movdqa %xmm7, -0x10(%rbp)
// _class_lookupMethodAndLoadCache3(receiver, selector, class)
.if $0 == NORMAL
// receiver already in a1
// selector already in a2
.else
movq %a2, %a1
movq %a3, %a2
.endif
movq %r10, %a3
call __class_lookupMethodAndLoadCache3
// IMP is now in %rax
movq %rax, %r11
movdqa -0x80(%rbp), %xmm0
pop %a6
movdqa -0x70(%rbp), %xmm1
pop %a5
movdqa -0x60(%rbp), %xmm2
pop %a4
movdqa -0x50(%rbp), %xmm3
pop %a3
movdqa -0x40(%rbp), %xmm4
pop %a2
movdqa -0x30(%rbp), %xmm5
pop %a1
movdqa -0x20(%rbp), %xmm6
pop %rax
movdqa -0x10(%rbp), %xmm7
.if $0 == NORMAL
cmp %r11, %r11 // set eq for nonstret forwarding
.else
test %r11, %r11 // set ne for stret forwarding
.endif
leave
.endmacro

构建 fake_objc_msgSend

在了解了 objc_msgSend 的流程, 接下来构建 fake_objc_msgSend, 需要保存保存寄存器状态, 以正确调用原 objc_msgSend, 这里解释下为什么需要保存寄存器状态, 因为参数个数不定, 所以可能会同时用到多个寄存器和栈来同时传参, 所以这里需要将可能会用到的所有寄存器以及 sp 都保存, 以确保多参数不被修改. 其实把这里的保护寄存器实现在 hook 层可能会更好一些. 最终通过把寄存器参数保存在栈中, 并以栈指针作为参数, 传给 hookBefore, 实现函数参数的完全访问.

ok, 说完了函数的参数(寄存器)保存和恢复部分, 接下来谈一谈函数调用栈的问题.

  1. 限制函数调用栈深度
  2. 记录函数调用的返回值

问题 1, 比较好理解. 问题 2, 如果需要记录函数返回值, 并且过滤了部分的函数记录, 必须要保证 hookBeforehookAfter 是对于同一个 函数 做的处理, 所以这里需要自建函数调用栈以及标记 函数 , 也就是说必须要保证自建的调用栈的 push 和 pop 操作必须对应, 这一点 InspectiveC 不同, 它是直接跟踪所有函数入栈(依赖系统标准函数栈的准确). 这里使用 sp 寄存器做标记区分函数(关于为何使用 sp 寄存器做标记区分, 希望读者能自己仔细思考函数调用本质), 以确保 push 和 pop 的对应正确.

这里总结一下 fake_objc_msgSend_safe 的工作

  1. 保存寄存器/参数
  2. hookBefore 工作(过滤/判断 调用的函数)
  3. 恢复寄存器/参数
  4. 如果经过判断不需要追踪, 则直接调用原 ojbc_msgSend
  5. 如果需要追踪, 则修改默认栈内保存的返回地址后, 继续调用 objc_msgSend (这里涉及到 trick)
  6. 保存寄存器/参数
  7. hookAfter 工作(解析返回值, 函数调用出栈, 确保两次 sp 值相同)
  8. 恢复寄存器/参数
  9. ret

下面列出 fake_objc_msgSend_safe 的核心代码.

这里关于调用栈的操作主要放在 hookBeforehookAfter, 关于在内联汇编中调用 C 函数这里也使用了两种方法, 一种使用 nm 导出符号, 一种使用内联汇编传参.

这里关于寄存器的使用与污染问题, 请查阅相关资料或参考下方参考资料, 以了解寄存器使用约定.

这里关于如何获取到汇编的下一条指令地址的 trick, 可以参考 <程序员自我修养> 中地址无关代码中使用到的 get_pc_thunk.

使用 ‘横线’ 分割了 hookBeforehookAfter 相关处理代码, 大部分代码我都已经加了注释.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
__attribute__((__naked__)) static void fake_objc_msgSend_safe()
{
// test for direct jmp
// __asm__ volatile(
// "jmpq *%0n":
// : "r" (orig_objc_msgSend)
// :);
// backup registers
__asm__ volatile(
"subq $(16*8+8), %%rspn" // +8 for alignment
"movdqa %%xmm0, (%%rsp)n"
"movdqa %%xmm1, 0x10(%%rsp)n"
"movdqa %%xmm2, 0x20(%%rsp)n"
"movdqa %%xmm3, 0x30(%%rsp)n"
"movdqa %%xmm4, 0x40(%%rsp)n"
"movdqa %%xmm5, 0x50(%%rsp)n"
"movdqa %%xmm6, 0x60(%%rsp)n"
"movdqa %%xmm7, 0x70(%%rsp)n"
"pushq %%raxn" // stack align
"pushq %%r9n" // might be xmm parameter count
"pushq %%r8n"
"pushq %%rcxn"
"pushq %%rdxn"
"pushq %%rsin"
"pushq %%rdin"
"pushq %%raxn"
// origin rsp, contain `ret address`, how to use leaq, always wrong.
"movq %%rsp, %%raxn"
"addq $(16*8+8+8+7*8), %%raxn"
"pushq (%%rax)n"
"pushq %%raxn" ::
:);
// prepare args for func
__asm__ volatile(
"movq %%rsp, %%rdin"
"callq __Z10hookBeforeP9RegState_n" ::
:);
// get value from `ReturnPayload`
__asm__ volatile(
"movq (%%rax), %%r10n"
"movq 8(%%rax), %%r11n" ::
:);
// restore registers
__asm__ volatile(
"popq %%raxn"
"popq (%%rax)n"
"popq %%raxn"
"popq %%rdin"
"popq %%rsin"
"popq %%rdxn"
"popq %%rcxn"
"popq %%r8n"
"popq %%r9n"
"popq %%raxn" // stack align
"movdqa (%%rsp), %%xmm0n"
"movdqa 0x10(%%rsp), %%xmm1n"
"movdqa 0x20(%%rsp), %%xmm2n"
"movdqa 0x30(%%rsp), %%xmm3n"
"movdqa 0x40(%%rsp), %%xmm4n"
"movdqa 0x50(%%rsp), %%xmm5n"
"movdqa 0x60(%%rsp), %%xmm6n"
"movdqa 0x70(%%rsp), %%xmm7n"
"addq $(16*8+8), %%rspn" ::
:);
// go to the original objc_msgSend
__asm__ volatile(
// "br x9n"
"cmpq $0, %%r11n"
"jne Lthroughxn"
"jmpq *%%r10n"
"Lthroughx:n"
// trick to jmp
"jmp NextInstructionn"
"Begin:n"大专栏  如何正确的hook方法objc_msgSend · jmpews/div>
"popq %%r11n"
"movq %%r11, (%%rsp)n"
"jmpq *%%r10n"
"NextInstruction:n"
"call Begin" ::
:);
//-----------------------------------------------------------------------------
// after objc_msgSend we parse the result.
// backup registers
__asm__ volatile(
"pushq %%r10n" // stack align
"push %%rbpn"
"movq %%rsp, %%rbpn"
"subq $(16*8), %%rspn" // +8 for alignment
"movdqa %%xmm0, -0x80(%%rbp)n"
"push %%r9n" // might be xmm parameter count
"movdqa %%xmm1, -0x70(%%rbp)n"
"push %%r8n"
"movdqa %%xmm2, -0x60(%%rbp)n"
"push %%rcxn"
"movdqa %%xmm3, -0x50(%%rbp)n"
"push %%rdxn"
"movdqa %%xmm4, -0x40(%%rbp)n"
"push %%rsin"
"movdqa %%xmm5, -0x30(%%rbp)n"
"push %%rdin"
"movdqa %%xmm6, -0x20(%%rbp)n"
"push %%raxn"
"movdqa %%xmm7, -0x10(%%rbp)n"
"pushq 0x8(%%rbp)n"
"movq %%rbp, %%raxn"
"addq $8, %%raxn"
"pushq %%raxn" ::
:);
// prepare args for func
__asm__ volatile(
"movq %%rsp, %%rdin"
// "callq __Z9hookAfterP9RegState_n"
"callq *%0n"
"movq %%rax, %%r10n"
:
: "r"(func_ptr)
: "%rax");
// restore registers
__asm__ volatile(
"pop %%raxn"
"pop 8(%%rbp)n"
"movdqa -0x80(%%rbp), %%xmm0n"
"pop %%raxn"
"movdqa -0x70(%%rbp), %%xmm1n"
"pop %%rdin"
"movdqa -0x60(%%rbp), %%xmm2n"
"pop %%rsin"
"movdqa -0x50(%%rbp), %%xmm3n"
"pop %%rdxn"
"movdqa -0x40(%%rbp), %%xmm4n"
"pop %%rcxn"
"movdqa -0x30(%%rbp), %%xmm5n"
"pop %%r8n"
"movdqa -0x20(%%rbp), %%xmm6n"
"pop %%r9n"
"movdqa -0x10(%%rbp), %%xmm7n"
"leaven"
// go to the original objc_msgSend
"movq %%r10, (%%rsp)n"
"retn" ::
:);
}

虽然现在已经可以 hook 到 objc_msgSend.

利用下面的数据结构获取之前保存在栈中的参数, ps: 这个结构是参考 InspectiveC, 实现的很精巧, 在此基础上做了一些修改.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
-ARM64
http://infocenter.arm.com/help/topic/com.arm.doc.den0024a/DEN0024A_v8_architecture_PG.pdf (7.2.1 Floating-point) (4.6.1 Floating-point register organization in AArch64)
use struct and union to describe diagram in the above link, nice!
-X86
https://en.wikipedia.org/wiki/X86_calling_conventions
RDI, RSI, RDX, RCX, R8, R9, XMM0–7
*/
// x86_64 is XMM, arm64 is q
typedef union FPMReg_ {
__int128_t q;
struct {
double d1; // Holds the double (LSB).
double d2;
} d;
struct {
float f1; // Holds the float (LSB).
float f2;
float f3;
float f4;
} f;
} FPReg;
// just ref how to backup/restore registers
struct RegState_ {
uint64_t bp;
uint64_t ret;
union {
uint64_t arr[7];
struct {
uint64_t rax;
uint64_t rdi;
uint64_t rsi;
uint64_t rdx;
uint64_t rcx;
uint64_t r8;
uint64_t r9;
} regs;
} general;
uint64_t _; // for align
union {
FPReg arr[8];
struct {
FPReg xmm0;
FPReg xmm1;
FPReg xmm2;
FPReg xmm3;
FPReg xmm4;
FPReg xmm5;
FPReg xmm6;
FPReg xmm7;
} regs;
} floating;
};
typedef struct pa_list_ {
struct RegState_ *regs; // Registers saved when function is called.
unsigned char *stack; // Address of current argument.
int ngrn; // The Next General-purpose Register Number.
int nsrn; // The Next SIMD and Floating-point Register Number.
} pa_list;

构建 hookBefore

hookBefore 实现了函数调用前的 trace 工作, 过滤 函数, 将 函数 压进调用栈, 解析类实例对象, 解析不定参数.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
arg1: object-address(need to parse >> class)
arg2: method string address
arg3: method signature
*/
vm_address_t hookBefore(struct RegState_ *rs)
{
gettimeofday(&start,NULL);
// TODO: parse args
pa_list args = (pa_list){
rs,
reinterpret_cast<unsigned char *>(rs->bp),
2,
0};
ThreadCallStack *cs = getThreadCallStack(threadKey);
ReturnPayload *rp = cs->rp;
vm_address_t xaddr = (uint64_t)orig_objc_msgSend;
rp->addr = xaddr;
rp->ret = rs->ret;
rp->bp = rs->bp;
rp->isTrace = 0;
rp->key = rs->ret & rs->bp;
if (1 || pthread_main_np())
{
/*
pay attention!!! as `objc_msgSend` invoked so often, the `filter` code snippet that use `c` to write it must be fast!!!.
*/
do
{
// check method string address, can be narrow the scope.
char *methodSelector = reinterpret_cast<char *>(rs->general.regs.rsi);
if(!(methodSelector && checkAddressRange((vm_address_t)methodSelector, macho_load_addr, macho_load_end_addr)))
break;
// check object's class address
vm_address_t class_addr = macho_objc_getClass(rs->general.regs.rdi);
if (!(class_addr && checkAddressRange(class_addr, macho_load_addr, macho_load_end_addr)))
break;
// check `call count filter`
if(check_freq_filter((unsigned long)methodSelector))
break;
// // test for wechat
// if(!strncmp(methodSelector, "onRevokeMsg", 9))
// debug_break();
// check exclude
// check method_name first, no need parse object.
if (!checkLogFilters_MethodName(NULL, methodSelector, FT_EXINLUDE))
break;
objc_class_info_t *xobjc_class_info;
xobjc_class_info = mem->parse_OBJECT(rs->general.regs.rdi);
if(!xobjc_class_info)
break;
// check class name, need parse object
if (!checkLogFilters_ClassName(xobjc_class_info->class_name, NULL, FT_EXINLUDE))
break;
objc_method_info_t * objc_method_info;
objc_method_info = search_method_name(&(xobjc_class_info->methods), methodSelector);
if (!objc_method_info)
break;
if(check_thread_filter(cs->thread))
break;
// add to `method call count cache`
add_freq_filter((unsigned long)methodSelector, objc_method_info);
// may be can pass into 'objc_method_info_t' without 'class_name' and 'method_name'
CallRecord *cr = pushCallRecord(xobjc_class_info->class_name, methodSelector, reinterpret_cast<void *>(rs->bp), reinterpret_cast<void *>(rs->ret), cs);
if(!cr)
break;
printCallRcord(cr, cs);
rp->isTrace = 1;
} while(0);
}
gettimeofday(&end,NULL);
time_cost += (end.tv_sec-start.tv_sec)+(end.tv_usec-start.tv_usec)/1000000.0;
return reinterpret_cast<unsigned long>(rp);
}

hookBefore 的参数其实之前备份寄存器后的 sp 地址, 再通过 RegState_ 格式, 就可以取得所有寄存器的值. rs->general.regs.x0 存放的是实例地址, 但是按理说应该通过 object_getClass(rs->general.regs.x0) 应该取得类地址, 但是这里避免使用 Objective-C以及 parser 内代码格式统一 就直接使用了 c++ 去实现源码中对应的函数. rs->general.regs.x1 存放的是函数签名字符串.

Macho 的文件解析

这里用到了个人之前实现的 macho 的 parser 模块, 大致介绍下这个模块, 可以通过三种状态对 macho 格式进行解析: 1. 文件 2. pid 3. 自身进程, 这里使用第三种, 对自身进程进行解析. (这个模块实现也挺有意思, 需要考虑到 ‘文件偏移’, ‘虚拟地址’, ‘内存地址’ 三种地址的处理)

对于类实例的解析, 比较复杂, 这里简单介绍下, 具体可以参照 macho-ABI 文档.

构建 hookAfter

hookAfter 实现了 objc_msgSend 执行后, 函数的出栈工作, 并解析函数返回值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vm_address_t hookAfter(struct RegState_ *rs)
{
pa_list args = (pa_list){
rs,
reinterpret_cast<unsigned char *>(rs->bp),
2,
0};
ThreadCallStack *cs = getThreadCallStack(threadKey);
CallRecord *cr = popCallRecordSafe(cs, (void *)rs->bp);
if (cr)
{
// printCallRcordReturnValue(cr, cs, rs);
return reinterpret_cast<unsigned long>(cr->ret);
}
else
{
cr = popCallRecord(cs);
return reinterpret_cast<unsigned long>(cr->ret);
}
}

hookBefore 与 hookAfter 的数据传递.

x64 层面

关键在于函数调用栈中 函数返回地址 的传递, 也就是 (%%rsp) , 由于需要获取 objc_msgSend 的返回值, 所以需要修改栈内默认的函数返回值为下一条指令的地址(请读者思考为什么不直接 push 内存地址? ). 这涉及到如何恢复栈中函数返回地址, 采用线程私有变量的方式, 为每一个 CallRecord 添加 ret 成员.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct
{
vm_address_t addr;
unsigned long isTrace;
vm_address_t ret;
vm_address_t bp;
vm_address_t key;
} ReturnPayload;
// Shared structures.
typedef struct CallRecord_ {
void *objc;
void *method;
void *bp;
void *ret;
} CallRecord;
arm64 层面

其实关键在于 lr 寄存器值的传递, 由于需要获取 objc_msgSend 的返回值, 所以必须以 blr objc_msgSend 的方式调用, 此时之前 lr 寄存器的值被覆盖, 此时也不能进行 push 操作, 因为栈中可能已经保存了可变参数的值, 需要维持 sp 和 栈中内容不变, 那这里的 lr 如何保存恢复成了问题.

最后采用了线程私有变量的方法解决, 为每一个 CallRecord 添加 lr 成员, 在 hookAfter 是进行安全 pop, 也就是说同时需要校验 rs->fp 的值, 是否一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct
{
vm_address_t addr;
unsigned long isTrace;
vm_address_t lr;
vm_address_t fp;
vm_address_t key;
} ReturnPayload;
// Shared structures.
typedef struct CallRecord_ {
void *objc;
void *method;
void *fp;
void *lr;
} CallRecord;

如何正确过滤 objc_msgSend

目前我是通过过滤来避免 log 太复杂.

1. 过滤函数地址

默认会过滤解析地址在 self 内存区内, 因为 MachoParser 本身就是一个耗时操作.

2.1 设置不解析类/方法

需要使用 hash 表进行快速 cache 和 search.

2.2 设置只解析的类/方法

需要使用 hash 表进行快速 cache 和 search.

3. 限制函数调用栈深度

通过自建模拟函数栈, 限制函数栈调用深度.

4. 过滤频繁调用函数 (log, monitor 之类)

对于一定时间段内频繁调用的函数添加到 hash-map 中.

应用场景

目前主要关注逆向方面, 当然 APM 方向也是可以玩的.

Thunder 逆向

参考资料

1
2
// 函数调用约定, 寄存器使用约定
http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/