13. Hook原理介紹
13.1 Objective-C消息傳遞(Messaging)
對于C/C++這類靜態語言,調用一個方法其實就是跳到內存中的某一點并開始執行一段代碼。沒有任何動態的特性,因為這在編譯時就決定好了。
而在 Objective-C 中,[object foo] 語法并不會立即執行 foo 這個方法的代碼。它是在運行時給 object 發送一條叫 foo 的消息。這個消息,也許會由 object 來處理,也許會被轉發給另一個對象,或者不予理睬假裝沒收到這個消息。多條不同的消息也可以對應同一個方法實現。這些都是在程序運行的時候動態決定的。
事實上,在編譯時你寫的 Objective-C 函數調用的語法都會被翻譯成一個 C 的函數調用 objc_msgSend() 。比如,下面兩行代碼就是等價的:
[people setName:@"Flonger" Age:18];
objc_msgSend(people, @selector(setName:Age:), "Flonger", 18);
在 Objective-C 中,類、對象和方法都是一個C的結構體,從 objc/objc.h 和 objc/runtime.h 頭文件中,我們可以找到他們的定義:
typedef struct objc_class *Class;
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
//Class 是一個 objc_class 結構類型的指針, id是一個 objc_object 結構類型的指針.
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
#endif
} OBJC2_UNAVAILABLE;
isa
是一個 objective-c Class 類型的指針. 實例對象有個isa的屬性,指向Class, 而Class里也有個isa的屬性, 指向meteClass. 這里就有個點, 在Objective-C中任何的類定義都是對象.-
super_class
指向該類的父類, 如果該類已經是最頂層的根類(如 NSObject 或 NSProxy),那么 super_class 就為 NULL.
objective-c.png name
類的名字version
類的版本信息,默認為0info
供運行期使用的一些位標識。instance_size
該類的實例變量大小ivars
成員變量的鏈表
struct objc_ivar_list {
int ivar_count
/* variable length structure */
struct objc_ivar ivar_list[1]
}
-
methodLists
方法定義的鏈表struct objc_method_list { struct objc_method_list *obsolete; int method_count; struct objc_method method_list[1]; }; struct objc_method { SEL method_name; char *method_types; IMP method_imp;};
objc_cache
指向最近使用的方法.用于方法調用的優化
struct objc_cache {
unsigned int mask /* total = mask + 1 */;
unsigned int occupied;
Method buckets[1];
};
- protocols
協議的鏈表
struct objc_protocol_list {
struct objc_protocol_list *next;
long count;
Protocol *list[1];
};
objc_method_list 本質是一個有 objc_method 元素的可變長度的數組。一個 objc_method 結構體中:
- 函數名,也就是SEL
- 表示函數原型的字符串 (見 Type Encoding)
- 函數的實現IMP
13.2 Method Swizzling示例
以上面可知方法的名字(SEL)跟方法的實現(IMP,指向 C 函數的指針)一一對應。Swizzle 一個方法其實就是在程序運行時對 objc_method_list 里做點改動,讓這個方法的名字(SEL)對應到另個IMP。
Method Swizzling(方法調配技術),僅針對Objective-C方法有效。Method Swizzling 利用 Runtime 特性把一個方法的實現與另一個方法的實現進行替換。
//涉及到的主要方法
class_addMethod
class_replaceMethod
method_exchangeImplementations
為什么在load里面調用?
一般情況下,類別里的方法會重寫掉主類里相同命名的方法。如果有兩個類別實現了相同命名的方法,只有一個方法會被調用。
但 +load是個特例,當一個類被讀到內存的時候, runtime 會給這個類及它的每一個類別都發送一個 +load: 消息。(多個類別時需要防止多次執行)object_getClass(obj)與[obj class]的區別?
參考資料:http://www.lxweimin.com/p/ae5c32708bc6
13.3 Fishhook
fishhook蘋果系統下的一種C函數的hook方案,是facebook提供的一個動態修改鏈接Mach-O符號表的開源工具。Mach-O為Mach Object文件格式的縮寫,也是用于iOS可執行文件,目標代碼,動態庫,內核轉儲的文件格式。Mach-O有自己的dylib規范(/usr/include/mach-o/loader.h文件里面)。
官網:https://github.com/facebook/fishhook
- Hook示例
#import <Foundation/Foundation.h>
#import <dlfcn.h>
#import "fishhook.h"
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
int my_close(int fd) {
printf("Calling real close(%d)\n", fd);
return orig_close(fd);
}
int my_open(const char *path, int oflag, ...) {
va_list ap = {0};
mode_t mode = 0;
if ((oflag & O_CREAT) != 0) {
// mode only applies to O_CREAT
va_start(ap, oflag);
mode = va_arg(ap, int);
va_end(ap);
printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
return orig_open(path, oflag, mode);
} else {
printf("Calling real open('%s', %d)\n", path, oflag);
return orig_open(path, oflag, mode);
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
//NSLog(@"Hello, World!");
struct rebinding rbd[2];
rbd[0].name = "close";
rbd[0].replacement = my_close;
rbd[0].replaced = (void*)&orig_close;
rbd[1].name = "open";
rbd[1].replacement = my_open;
rbd[1].replaced = (void*)&orig_open;
rebind_symbols(rbd, 2);
// Open our own binary and print out first 4 bytes (which is the same
// for all Mach-O binaries on a given architecture)
int fd = open(argv[0], O_RDONLY);
uint32_t magic_number = 0;
read(fd, &magic_number, 4);
printf("Mach-O Magic Number: 0x%x \n", magic_number);
close(fd);
}
return 0;
}
- 使用fishHook監聽微信文件讀寫
Makefile 文件
THEOS_DEVICE_IP = 192.168.1.113
DEBUG = 1
ARCHS = armv7 arm64
TARGET = iphone:latest:8.0
include $(THEOS)/makefiles/common.mk
TWEAK_NAME = WeChatReProject
WeChatReProject_FILES = Tweak.xm fishhook.c
WeChatReProject_FRAMEWORKS = UIKit Foundation CoreLocation
WeChatReProject_CFLAGS = -fobjc-arc
include $(THEOS_MAKE_PATH)/tweak.mk
after-install::
install.exec "killall -9 WeChat"
clean::
rm -rf ./packages/*
Tweak.xm文件
#import<UIKit/UIKit.h>
#import<CoreLocation/CoreLocation.h>
#import<CoreLocation/CLLocation.h>
#import "fishhook.h"
@interface SeePeopleNearByLogicController
- (void)onRetrieveLocationOK:(id)arg1;
@end
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
int my_close(int fd) {
printf("Calling real close(%d)\n", fd);
return orig_close(fd);
}
int my_open(const char *path, int oflag, ...) {
va_list ap = {0};
mode_t mode = 0;
if ((oflag & O_CREAT) != 0) {
// mode only applies to O_CREAT
va_start(ap, oflag);
mode = va_arg(ap, int);
va_end(ap);
NSLog(@"Calling real open('%s', %d, %d)", path, oflag, mode);
return orig_open(path, oflag, mode);
} else {
NSLog(@"Calling real open('%s', %d)", path, oflag);
return orig_open(path, oflag, mode);
}
}
%hook MicroMessengerAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
struct rebinding rbd[2];
rbd[0].name = "close";
rbd[0].replacement = (void*)my_close;
rbd[0].replaced = (void**)&orig_close;
rbd[1].name = "open";
rbd[1].replacement = (void*)my_open;
rbd[1].replaced = (void**)&orig_open;
rebind_symbols(rbd, 2);
NSLog(@"begin hook");
return %orig;
}
%end
%hook SeePeopleNearByLogicController
- (void)onRetrieveLocationOK:(id)arg1
{
CLLocation *location = [[CLLocation alloc] initWithLatitude:31.154352 longitude:121.42562];
%orig(location);
UIAlertView *alertView = [[UIAlertView alloc]
initWithTitle:[@"onRetrieveLocationOK"
stringByAppendingString:[[NSString alloc]
initWithFormat:@"location is %@", location]]
message:nil
delegate:self
cancelButtonTitle:@"ok"
otherButtonTitles:nil];
[alertView show];
}
%end
13.4 Mach-o文件結構
Mach-o包含三個基本區域:
頭部(header structure)
加載命令(load command)。
段(segment)。可以擁有多個段(segment),每個段可以擁有零個或多個區域(section)。每一個段(segment)都擁有一段虛擬地址映射到進程的地址空間。
-
鏈接信息。一個完整的用戶級Mach-o文件的末端是鏈接信息。其中包含了動態加載器用來鏈接可執行文件或者依賴庫所需 使用的符號表,字符串表等等。
mach-o.png
- 使用MachOView查看
- 文件頭 mach64 Header
- 加載命令 Load Commands
- 文本段 __TEXT
- 數據段 __DATA
- 動態庫加載信息 Dynamic Loader Info
- 入口函數 Function Starts
- 符號表 Symbol Table
- 動態庫符號表 Dynamic Symbol Table
- 字符串表 String Table
13.4.1 Mach-o的header
- otool工具來查看Mach-o的頭部,看看都包含哪些信息:
? otool -hv WeChat.decrypted
WeChat.decrypted (architecture armv7):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC ARM V7 0x00 EXECUTE 76 7416 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
WeChat.decrypted (architecture arm64):
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 ARM64 ALL 0x00 EXECUTE 76 8168 NOUNDEFS DYLDLINK TWOLEVEL WEAK_DEFINES BINDS_TO_WEAK PIE
- 使用hexdump或者UE編輯器查看
? hexdump -C WeChatReProject.dylib | more
00000000 ce fa ed fe 0c 00 00 00 09 00 00 00 06 00 00 00 |................|
00000010 15 00 00 00 fc 07 00 00 85 00 10 00 01 00 00 00 |................|
00000020 d0 01 00 00 5f 5f 54 45 58 54 00 00 00 00 00 00 |....__TEXT......|
00000030 00 00 00 00 00 00 00 00 00 80 00 00 00 00 00 00 |................|
00000040 00 80 00 00 05 00 00 00 05 00 00 00 06 00 00 00 |................|
頭部的的結構如下:
struct mach_header {
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
};
struct mach_header_64 {
uint32_t magic;
cpu_type_t cputype;
cpu_subtype_t cpusubtype;
uint32_t filetype;
uint32_t ncmds;
uint32_t sizeofcmds;
uint32_t flags;
uint32_t reserved;
};
mach_header各個字段的具體意義:
-
magic
魔數,系統加載器通過改字段快速,判斷該文件是用于32位or64位。//32位魔數 #define MH_MAGIC 0xfeedface #define MH_CIGAM 0xcefaedfe //64位魔數 #define MH_MAGIC_64 0xfeedfacf #define MH_CIGAM_64 0xcffaedfe
-
cputype
CPU類型以及子類型字段,該字段確保系統可以將適合的二進制文件在當前架構下運行。#define CPU_TYPE_ARM ((cpu_type_t) 12) #define CPU_TYPE_ARM64 (CPU_TYPE_ARM | CPU_ARCH_ABI64)
-
cpusubtype
CPU指定子類型,對于inter,arm,powerpc等CPU架構,其都有各個階段和等級的CPU芯片,該字段就是詳細描述其支持CPU子類型。#define CPU_SUBTYPE_ARM_V7 ((cpu_subtype_t) 9) #define CPU_SUBTYPE_ARM64_ALL ((cpu_subtype_t) 0) #define CPU_SUBTYPE_ARM64_V8 ((cpu_subtype_t) 1)
-
filetype
說明該mach-o文件類型(可執行文件,庫文件,核心轉儲文件,內核擴展,DYSM文件,動態庫等)#define MH_OBJECT 0x1 //.o目錄文件 #define MH_EXECUTE 0x2 //a.out可主動執行文件 #define MH_DYLIB 0x6 //.dylib文件 #define MH_DSYM 0xa //.dSYM文件 #define MH_KEXT_BUNDLE 0xb //.kext驅動文件
ncmds
說明加載命令條數sizeofcmds
表示加載命令大小-
flags
標志位,該字段用位表示二進制文件支持的功能,主要是和系統加載,鏈接相關。#define MH_NOUNDEFS 0x1 // 目前沒有未定義的符號,不存在鏈接依賴 #define MH_DYLDLINK 0x4 // 該文件是dyld的輸入文件,無法被再次靜態鏈接 #define MH_PIE 0x200000 // 加載程序在隨機的地址空間,只在 MH_EXECUTE中使用 #define MH_TWOLEVEL 0x80 // 兩級名稱空間
隨機地址空間
進程每一次啟動,地址空間都會簡單地隨機化。
如果采用傳統的方式,程序的每一次啟動的虛擬內存鏡像都是一致的,黑客很容易采取重寫內存的方式來破解程序。采用ASLR-地址空間配置隨機加載(Address space layout randomization)將可執行程序隨機裝載到內存里,可以有效的避免緩沖區溢出攻擊。dyld(/usr/lib/dyld)
動態鏈接器:當內核執行LC_DYLINK時,鏈接器會啟動,查找進程所依賴的動態庫,并加載到內存中。二級名稱空間
這是dyld的一個獨有特性,符號空間中還包括所在庫的信息,這樣子就可以讓兩個不同的庫導出相同的符號。
13.4.2 Load Commands - 加載命令
Mach-O文件包含非常詳細的加載指令,這些指令非常清晰地指示加載器如何設置并且加載二進制數據。Load Commands信息緊緊跟著二進制文件頭后面。加載命令的數目以及總的大小在header中已經給出。
- load command的結構如下:
struct load_command {
uint32_t cmd; /* type of load command */
uint32_t cmdsize; /* total size of command in bytes */
};
- 使用otool命令查看加載指令信息
MachDemo otool -l a.out
a.out:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
...
-
CMD字段的解釋
LC_SEGMENT_64
將文件中(32位或64位)的段映射到進程地址空間中。LC_SYMTAB
符號表地址。LC_DYSYMTAB
動態符號表地址LC_DYLD_INFO_ONLY
動態鏈接相關信息LC_LOAD_DYLINKER
加載一個動態鏈接器(動態庫加載器),通常路徑是“/usr/lib/dyld”。LC_LOAD_DYLIB:
加載一個動態鏈接共享庫。如“/usr/lib/libSystem.B.dylib”,這是C標準庫。每個庫由動態鏈接器加載并包含一個符號表。LC_UUID
文件的唯一標識,crash解析中也會有該值,去確定dysm文件和crash文件是匹配的。LC_VERSION_MIN_MACOSX
二進制文件要求的最低操作系統版本LC_MAIN
設置程序主線程的入口地址和棧大小LC_SOURCE_VERSION
構建該二進制文件使用的源代碼版本LC_FUNCTION_STARTS
定義一個函數起始地址表,使調試器和其他程序易于看到一個地址是否在函數內LC_DATA_IN_CODE
定義在代碼段內的非指令數據
LC_SEGMENT_64和LC_SEGMENT是加載的主要命令,它負責指導內核來設置進程的內存空間。
segment數據結構:
struct segment_command { /* for 32-bit architectures */
uint32_t cmd; /* LC_SEGMENT */
uint32_t cmdsize; /* includes sizeof section structs */
char segname[16]; /* segment name */
uint32_t vmaddr; /* memory address of this segment */
uint32_t vmsize; /* memory size of this segment */
uint32_t fileoff; /* file offset of this segment */
uint32_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* LC_SEGMENT_64 */
uint32_t cmdsize; /* includes sizeof section_64 structs */
char segname[16]; /* segment name */
uint64_t vmaddr; /* memory address of this segment */
uint64_t vmsize; /* memory size of this segment */
uint64_t fileoff; /* file offset of this segment */
uint64_t filesize; /* amount to map from the file */
vm_prot_t maxprot; /* maximum VM protection */
vm_prot_t initprot; /* initial VM protection */
uint32_t nsects; /* number of sections in segment */
uint32_t flags; /* flags */
};
- cmd
就是Load commands的類型,這里LC_SEGMENT_64代表將文件中64位的段映射到進程的地址空間。LC_SEGMENT_64和LC_SEGMENT的結構差別不大。
- cmdsize
代表load command的大小
- segname
16字節的段名字
- vmaddr
段的虛擬內存起始地址
- vmsize
段的虛擬內存大小
- fileoff
段在文件中的偏移量
- filesize
段在文件中的大小
- maxprot
段頁面所需要的最高內存保護(1=r,2=w,4=x)
- initprot
段頁面初始的內存保護
- nsects
段中包含section的數量
- flags
其他雜項標志位
13.4.3 段(segment)和節(section)
“__TEXT"代表的是Segment,小寫的”__text"代表 Section
__PAGEZERO
一個全用0填充的段,用于抓取空指針引用(非法內存訪問)。這通常不會占用物理內存空間。-
__TEXT
本段只有可執行代碼和其他只讀數據。__text 主程序代碼 __stubs 用于動態庫鏈接的樁 __stub_helper 用于動態庫鏈接的樁的輔助 __cstring 常量字符串符號表描述信息,通過該區信息,可以獲得常量字符串符號表地址 __unwind_info 存儲堆棧展開信息供處理異常。 __eh_frame: 提供堆棧展開信息,用于異常處理 ? otool -tv a.out a.out: (__TEXT,__text) section _main: 0000000100000f30 pushq %rbp 0000000100000f31 movq %rsp, %rbp 0000000100000f34 subq $0x20, %rsp 0000000100000f38 leaq 0x47(%rip), %rax 0000000100000f3f movl $0x0, -0x4(%rbp) 0000000100000f46 movl %edi, -0x8(%rbp) 0000000100000f49 movq %rsi, -0x10(%rbp) 0000000100000f4d movq %rax, %rdi 0000000100000f50 movb $0x0, %al 0000000100000f52 callq 0x100000f64 0000000100000f57 xorl %ecx, %ecx 0000000100000f59 movl %eax, -0x14(%rbp) 0000000100000f5c movl %ecx, %eax 0000000100000f5e addq $0x20, %rsp 0000000100000f62 popq %rbp 0000000100000f63 retq
-
__DATA
用于讀取和寫入數據的一個段。__nl_symbol_ptr:非延遲導入符號指針表。 __la_symbol_ptr:延遲導入符號指針表。
__LINKEDIT
包含給動態鏈接器的原始數據的段,包括符號和字符串表,壓縮動態鏈接信息,以及動態符號表等。Section的數據結構
struct section { /* for 32-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint32_t addr; /* memory address of this section */
uint32_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
};
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* name of this section */
char segname[16]; /* segment this section goes in */
uint64_t addr; /* memory address of this section */
uint64_t size; /* size in bytes of this section */
uint32_t offset; /* file offset of this section */
uint32_t align; /* section alignment (power of 2) */
uint32_t reloff; /* file offset of relocation entries */
uint32_t nreloc; /* number of relocation entries */
uint32_t flags; /* flags (section type and attributes)*/
uint32_t reserved1; /* reserved (for offset or index) */
uint32_t reserved2; /* reserved (for count or sizeof) */
uint32_t reserved3; /* reserved */
};
sectname:比如__text、__stubs
segname :該section所屬的segment,比如__TEXT
addr : 該section在內存的起始位置
size: 該section的大小
offset: 該section的文件偏移
align : 字節大小對齊(以多少字節對齊,一般是2的乘冪)
reloff :重定位入口的文件偏移
nreloc: 需要重定位的入口數量
flags:包含section的type和attributes
reserved: 預留的字段
13.4.4 動態庫鏈接信息
- LC_DYLD_INFO_ONLY
根據該加載命令的字段偏移,可以得到壓縮動態數據信息區(動態庫綁定,地址重定向等信息)。struct dyld_info_command { uint32_t cmd; /* LC_DYLD_INFO or LC_DYLD_INFO_ONLY */ uint32_t cmdsize; /* sizeof(struct dyld_info_command) */ uint32_t rebase_off; /* file offset to rebase info */ uint32_t rebase_size; /* size of rebase info */ uint32_t bind_off; /* file offset to binding info */ uint32_t bind_size; /* size of binding info */ uint32_t weak_bind_off; /* file offset to weak binding info */ uint32_t weak_bind_size; /* size of weak binding info */ uint32_t lazy_bind_off; /* file offset to lazy binding info */ uint32_t lazy_bind_size; /* size of lazy binding infs */ uint32_t export_off; /* file offset to lazy binding info */ uint32_t export_size; /* size of lazy binding infs */ }; 重定向數據rebase(命令碼:高四位 低四位) 11: 高四位0x10 表示設置立即數類型 低四位0x01 表示立即數類型為指針 22: 表示REBAE_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB + 2 重定向到數據段第2個section 意思就是:重定向到數據段第二個section,該數據段信息為一個指針。 e.g: 在demo中是[0x100001010->_printf] 綁定數據 bind: 進行動態綁定依賴的dyld的函數(dyld_stub_binder) 弱綁定數據 weak bind: 用于弱綁定動態庫,就像weak_framework一樣 延時綁定數據 lazy bind: 對于需要從動態庫加載的函數符號(_printf) export數據: 用于對外開放的函數(_add、_main) ?nm a.out 0000000100000000 T __mh_execute_header 0000000100000f00 T _add 0000000100000f20 T _main U _printf U dyld_stub_binder
13.4.5 動態庫鏈接器運行方式
VA : 虛擬地址,也就是程序被加載到內存空間中的地址
RVA : 以虛擬地址前邊加上個“相對的”,也就是說它還是按虛擬地址來換算,只不過不是從0開始。
RAW :一般稱文件偏移,你把一個文件看成一個連續的字節流,OFFSET就是這個字節流中的位置。
- MachoDemo.m中main函數對printf調用的匯編代碼:
callq 0x100000f84 ## symbol stub for: _printf
匯編代碼中0x100000f84,這個地址是 __TEXT段的section __stubs區的地址。即JMP到__stubs(樁區)
-
0x100000f84地址,是一段匯編指令
0x100000f84:FF2586000000 等效于:jmpq *0x86(%rip) # 0x100001010 跳轉地址 = 當前地址+加指令長度+跳轉偏移 = 0x100000f84 + 0x6 + 0x86
-
0x100001010地址,是指向__DATA段__la_symbol_ptr區。
0x100001010:0000000100000f9c
-
0x100000f9c 這個地址是 __TEXT段的section __stub_helper區的地址
0x100000f9c: pushq $0x0 0x100000fa1: jmpq 0x100000f8c
-
0x100000f8c 這個地址是__TEXT段section __stub_helper區的起始地址。
0x100000f8c: lea ["0x100001008->ABSOLUTE"](%rip),%r11 0x100000f93: push %r11 0x100000f95: jmpq *[0x100001000->dyld_stub_binder](%rip) 0x100000f9c: pushq $0x0 0x100000fa1: jmpq 0x100000f8c
-
0x100001000地址,恰好是__DATA段section __nl_symbol_ptr區的起始地址。
該地址指向的數據值位為:0x100001000:0x0000000000000000 0x100001008:0x0000000000000000
-
說明:
__stubs區和__stub_helper區是幫助動態鏈接器找到指定數據段__nl_symbol_ptr區,二進制文件用0x0000000000000000進行占位,在運行時,系統根據dynamic loader info信息,把占位符換為調用dylib的dyld_stub_binder函數的匯編指令。
當第一次調用完動態庫中的符號后,動態鏈接器會根據dynamic loader info信息,把__la_symbol_ptr區中的數據指向正確的符號地址,而不是指向_nl_symbol_ptr區。
13.4.6 可執行文件的加載過程
參考資料:
https://opensource.apple.com/tarballs/dyld/dyld-360.18.tar.gz
https://opensource.apple.com/source/xnu/xnu-2422.1.72/bsd/kern/kern_exec.c
解析mach-o文件,確定該文件是一個有效的Mach-O文件,所以內核為程序(fork)創建一個進程并開始程序執行過程(execve)
-
加載命令。內核配合動態連接器,進入加載指令指定分配的地址空間。段的虛擬內存保護標志也按指示添加上(例如__TEXT是只讀)
- 動態庫信息
- 符號表地址信息
- 動態符號表地址信息
- 常量字符串表地址信息
- 動態庫加載信息
- 符號函數地址
- 依賴動態庫信息
- 動態鏈接器路徑信息
根據動態庫加載信息,把__DATA段section __nl_symbol_ptr區占位符換為調用dylib的dyld_stub_binder函數的匯編指令。
根據LC_MAIN的entry point調用指定entry offset偏移地址執行entry offset相關匯編指令。
第一次運行到動態庫函數時,進行一次懶加載動態綁定,并且動態鏈接器自動修改_la_symbol_ptr區的地址,指向動態庫對應符號的地址。
第二次運行到動態庫函數時,直接jmp到指定的符號地址
總結:
通過對Mach-O文件的分析,我們知道代碼段(__TEXT)都是只讀區,包含了程序邏輯處理。但是對于動態庫的函數調用借助了數據段(__DATA)的_la_symbol_ptr區和_nl_symbol_pt區。用戶可以去修改這兩個區的數據,因此我們可以利用這個特性去替換相關函數的調用(如:fishhook)。
注:通過DYLD_INSERT_LIBRARIES進行代碼注入是dyld提供的功能。
13.4.7 fishhook工作原理
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
//在鉤子鏈表頭增加新的鉤子
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
if (!_rebindings_head->next) { //首次調用
//注冊系統回調
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
//回調函數(參數1:mach_header的地址,參數2:slide 隨機偏移量)
//由于ASLR的緣故,導致程序實際虛擬內存地址與對應的Mach-o結構中的地址不一致,有一個偏移量
//slide,slide是程序裝在時隨機生成的隨機數。
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
rebind_symbols_for_image(_rebindings_head, header, slide);
}
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
if (dladdr(header, &info) == 0) {
return;
}
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
//計算load commands區域的位置(緊跟mach_header之后)
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
//遍歷加載指令區域
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
//LC_SEGMENT指令
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//__LINKEDIT段
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) { //符號表
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {//動態符號表
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
//計算mach-o header在內存空間中的位置
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
//計算Symbol Table的位置
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
//計算String Table的位置
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
//計算Dynamic Symbol Table的位置
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
//計算load commands區域的位置(緊跟mach_header之后)
cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {//遍歷
cur_seg_cmd = (segment_command_t *)cur;
//LC_SEGMENT
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//數據段(__DATA)
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
//__la_symbol_ptr區
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
//__nl_symbol_ptr區
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
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 symtab表中的位置
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
//計算(延時/非延時)加載區的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
//計算(延時/非延時)加載區的大小,并遍歷
for (uint i = 0; i < section->size / sizeof(void *); i++) {
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;
}
//獲取符號在String Table中的偏移
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
//獲取符號名字
char *symbol_name = strtab + strtab_offset;
if (strnlen(symbol_name, 2) < 2) {
continue;
}
struct rebindings_entry *cur = rebindings;
while (cur) {
//遍歷鉤子鏈表,將替換成新實現,保存老實現
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;
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
-
fishhook官方原理圖
fishhook.png