iOS RE 4 beginners 2 - 静态链接&&动态链接
ENV
macos11.4 + iphone6 iOS 12.2
静态链接
静态链接:输入多个目标文件,输出一个文件(一般是可执行文件)。这个过程中,把多个目标文件里相同性质的段合并到一起。
过程
- 地址和空间分配 (Address and Storage Allocation)
- 符号决议 (Symbol Resolution) / 符号绑定 (Symbol Binding)
- 重定位 (Relocation)
源码
1 | ~/study/ios_re_link/static_link cat main.c |
1 | ~/study/ios_re_link/static_link xcrun -sdk iphoneos clang -c main.c foo.c -target arm64-apple-ios12.2 |
两个模块(main.o 和 foo.o) 通过静态链接组合成了一个可执行文件(main)
模块&&产物
main.o
通过machoview可以看到重定位段有三条信息,意味着程序中有三处需要重定位处理:
这个图是hopper反汇编的main函数,可以看到对于引用到其他模块(foo.o)重的变量/函数的地方看起来“正常”,但是点击 bl _foo
就会发现跳转到了:
根据<macho/reloc.h>的定义,可以看到reloc段的结构:
1 | struct relocation_info { |
结合上面的图来看(以_foo符号为例):
- r_address : 0x28
- r_symbolnum(24bits): 指向_foo 字符串
- 剩下的8bits是标志位
对应到汇编里就是,main函数的0x28行引用了 _foo 符号,reloc段把这个信息告知linker,这样在链接的时候linker就会处理这条信息,把对应的符号做替换处理。
foo.o
其实都是对 global_var
的引用
在foo.o模块中,是 0x20处的data,这个信息也要告诉linker,在link的阶段做替换。
main
最终的可执行文件main,可以看到没有重定位信息,而且mian和foo函数中改替换的符号都已经完成了替换,可以顺利的索引到想要使用的符号(foo和global_var)。
对比两者符号表:
以foo符号为例 :
Type 从 N_UNDF → NSECT
Value 从0 → 0x100007f90
符号表结构:
1 | struct nlist_64 { |
foo 符号的话
- string table index : 指向符号的字符串
- n_sect : 改符号在第几个section
- n_value : 符号具体值(地址)
举个🌰
这里以demo中 global_var 使用的代码举例子。
源码中:
1 | int ret = foo(42 + global_var); |
如果对应到汇编里应该是:
1 |
|
可知 w0 是参数,w10是global_var的值,来自x9
w10 = [x9 + 0x68]
(未重定位修复)
最开始索引x9的时候可以发现是把0赋给了x9,因为这里还没有重定位,所以用0代替。
最终的产物中可以看到:
1 | 0000000100007f64 adrp x9, #0x100008000 ; 0x100008000@PAGE |
把0替换成了 0x100008000,这个地址恰好指向global_var。
可以看到经过linker的处理,可以正确找到global_var,符号foo同理
动态链接
debug set up
应该是签名有问题,最终解决方案:
1 | /usr/bin/security find-identity -v -p codesigning |
debug lazy binding process
可以看到,第一次调用 printf
的时候,bl跳过去并不是 printf函数
1 | Target 0: (main) stopped. |
通过 dyld_stub_binder
找 printf
的地址,把找到的地址写回到 DATA,__la_symbol_ptr
第二次调用printf的时候就可以看到,这个地方printf函数地址已经被写过来了
1 | (lldb) x/10i $pc |
所以这里就可以直接获取到地址,然后直接跳转过去就行:
1 | (lldb) s |
libdyld.dylib`dyld_stub_binder
dyld-852的代码:
因为我目标环境是iOS12.2,所以具体汇编代码有一些差别:
1 | Target 0: (main) stopped. |
但是本质上是差不多的,影响不大。
下面看看怎么一步一步调用进去,找到所需要的符号
1. call dyld_stub_binder
1 | 0000000100007f98 ldr w16, =0x6967616d0000001a |
个人猜测:0x000000000000001a
应该是 类似 linux下elf lazy binding的时候那个index参数的东西,每个符号都不一样 。
初始化好需要的参数就调用进去dyld中去做符号绑定操作了
1 | (lldb) s |
2. call dyld::fastBindLazySymbol(loadercache, lazyinfo)
保存栈帧,保存当前的寄存器信息(一大堆stp指令,后面符号绑定完成后,ldp会恢复,这些是成对的),然后设置好参数,就直接转到 dyld::fastBindLazySymbol
(函数前面的保存操作看起来和x86上函数开头的保存栈帧 抬高栈給临时变量预留空间的操作差不多)
1 | Process 1465 resuming |
调用的是 : fastBindLazySymbol(0x0000000100fb8028, 0x1a)
1 | // LINK_EDIT seg |
对应汇编中:
1 | (lldb) c |
这里用到了 我这个可执行文件的LINK_EDIT 段去做符号绑定工作:
1 | (lldb) image lookup -va $x1 |
3. ImageLoaderMachO::getLazyBindingInfo
根据不同的opcode,走不同分支:
1 | if ( lazyBindingInfoOffset > (lazyInfoEnd-lazyInfoStart) ) |
获取目标符号相关的信息 :
1 | &segIndex, &segOffset, &libraryOrdinal, &symbolName, &doneAfterBind |
然后根据这些信息,获取该符号的地址:
1 | uintptr_t address = segActualLoadAddress(segIndex) + segOffset; |
1 | // dyld版本不一致,实现的函数有些差别,但是本质是一样的 |
执行符号绑定:
1 | result = bindAt(context, this, address, BIND_TYPE_POINTER, symbolName, 0, 0, libraryOrdinal,NULL, "lazy ", patcher, NULL, true); |
1 | // 调试: |
执行之后:
1 | (lldb) n |
可以看到符号地址已经被写过去了(0x0000000100fb8020)
至此,符号绑定过程完成。
reference
《程序员的自我修养-链接、装载和库》
https://juejin.cn/post/6844903912147795982
https://juejin.cn/post/6844903922654511112#heading-10