对映像进行操作的API都在
获取当前进程中加载的映像的数量
uint32_t _dyld_image_count(void)
获取某个映像的mach-o头部信息结构体指针
extern const struct mach_header* _dyld_get_image_header(uint32_t image_index)
获取某个映像的mach-o头部信息结构体指针
函数的入参为映像在进程当中的索引号,函数返回的值是一个映像的mach-o头部信息struct mach_header结构体指针,如果是64位系统则返回的是struct mach_header_64结构体指针。你可以通过这个函数返回的映像的头部结构体来遍历和访问映像中的所有信息和数据。
extern const struct mach_header* _dyld_get_image_header(uint32_t image_index)
- 一般情况下索引为0的映像是dyld库的映像,而索引为1的映像就是当前进程的可执行程序映像。
- 一个映像的头部信息结构体指针其实就是映像在内存中加载的基地址。
还可以使用一个未在头文件生命的方法_NSGetMachExecuteHeader
// 使用前先声明
extern const struct mach_header* _NSGetMachExecuteHeader();
获取进程中某个映像加载的Slide值
const char* _dyld_get_image_name(uint32_t image_index)
注册映像加载和卸载的回调通知函数
在objc中的方法 objc_addLoadImageFunc 可以监听镜像加载完成的操作
void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
- _dyld_register_func_for_add_image 注册了一个映像被加载时的回调函数时,那么每当后续一个新的映像被加载但未初始化前就会调用注册的回调函数,回调函数的两个入参分别表示加载的映像的头结构和对应的Slide值。之前已经加载的也会执行一遍。
- _dyld_register_func_for_remove_image注册了一个映像被卸载时的回调函数时,那么每当一个映像被卸载前都会调用注册的回调函数。
对于Segment 和Section的操作
对于Segment 和Section的操作 可以通过导入
- 获取进程中映像的某段中某个节的非Slide的数据指针和尺寸
获取进程中可执行程序映像的某个段中某个节的数据指针和尺寸。
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
获取进程加载的库的segname段和sectname节的数据指针和尺寸。
char *getsectdatafromFramework(const char *FrameworkName, const char *segname, const char *sectname, unsigned long *size);
这两个函数返回进程中可执行程序映像或者某个加载的动态库中的某个段中某个节的数据指针和尺寸。
这两个函数其实就是返回对应的节描述信息结构struct section中的addr和size两个数据成员的值。需要注意的是返回的地址值是没有加上Slide值的指针,因此当要在进程中访问真实的地址时需要加上对应的Slide值,下面就是一个实例代码:
//一般索引为1的都是可执行文件映像
intptr_t slide = _dyld_get_image_vmaddr_slide(1);
unsigned long size = 0;
char *paddr = getsectdata("__TEXT", "__text", &size);
char *prealaddr = paddr + slide; //这才是真实要访问的地址。
getsectdata函数的代码实现如下:
//假设是64位的系统
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
//这个函数会在下面介绍到。
return getsectdatafromheader_64(mhp, segname, sectname, size);
}
2.获取段和节的边界信息
//获取当前进程可执行程序映像的最后一个段的数据后面的开始地址
unsigned long get_end(void);
// 获取当前进程可执行程序映像的第一个__TEXT段的__text节的数据后面的开始地址。
unsigned long get_etext(void);
// 获取获取当前进程可执行程序映像的第一个_DATA段的__data节的数据后面的开始地址
unsigned long get_edata(void);
这几个函数主要用来获取指定段和节的结束位置,以及用来确定某个地址是否在指定的边界内。需要注意的是这几个函数返回的边界值是并未加Slide值的边界值。下面是这几个函数的内部实现:
unsigned long get_end()
{
unsigned long end = 0;
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
struct segment_command_64 *psegcmd = mhp + 1;
for (int i = 0; i < mhp->ncmds; i++)
{
if (psegcmd->cmd != LC_SEGMENT_64)
break;
end = psegcmd->vmaddr + psegcmd->vmsize;
psegcmd += 1;
}
return end;
}
unsigned long get_etext()
{
const struct section_64 *sec = getsectbyname("__TEXT","__text");
return sec->addr + sec->size;
}
unsigned long get_edata()
{
const struct section_64 *sec = getsectbyname("__DATA","__data");
return sec->addr + sec->size;
}
3.获取进程中可执行程序映像的段描述信息
const struct segment_command *getsegbyname(const char *segname)
//上面函数的64位版本
const struct segment_command_64 *getsegbyname(const char *segname)
这两个函数返回进程中可执行程序映像的某个段的段描述信息。段描述信息是一个struct segment_command或者struct segment_command_64结构体。
比如下面代码返回进程中可执行程序映像代码段__TEXT的段信息。
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
4.获取进程中可执行程序映像的某个段中某个节的描述信息
//获取进程中可执行程序映像的某个段中某个节的描述信息。
const struct section *getsectbyname(const char *segname, const char *sectname)
//上面对应函数的64位系统版本
const struct section_64 *getsectbyname(const char *segname, const char *sectname)
这两个函数分别返回32位系统和64位系统中的进程中可执行程序映像的segname段中的sectname节的描述信息。节的描述信息是一个struct section或者struct section_64的结构体。比如下面的代码返回代码段__TEXT中的代码节__text的描述信息:
struct section_64 *psection = getsectbyname("__TEXT","__text");
5.获取进程中映像的段的数据
//获取指定映像的指定段的数据。
uint8_t *getsegmentdata(const struct mach_header *mhp, const char *segname, unsigned long *size)
//上面函数的64位版本
uint8_t *getsegmentdata(const struct mach_header_64 *mhp, const char *segname, unsigned long *size)
函数返回进程内指定映像mhp中的段segname中内容的地址指针,而整个段的尺寸则返回到size所指的指针当中。这个函数的内部实现就是返回段描述信息结构struct segment_command中的vmaddr数据成员的值加上映像mhp的slide值。而size中返回的就是段描述信息结构中的vmsize数据成员。
因为在前面讲过因为映像加载时的slide值的缘故,所以映像中的各种mach-o结构体中涉及到地址的数据成员的值都需要加上slide值才能得到映像在内存中的真实加载地址。
进程中每个映像中的第一个__TEXT段的数据的地址其实就是这个映像的mach_header头结构的地址。这是一个比较特殊的情况。
下面的代码演示的是获取进程中第0个索引位置映像的__DATA段的数据。
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsegmentdata(mhp, "__DATA", &size);
6.获取进程映像的某段中某节的数据
//获取进程映像中的某段中某节的数据地址和尺寸。
uint8_t *getsectiondata(const struct mach_header *mhp, const char *segname, const char *sectname, unsigned long *size)
//上面函数的64位版本
uint8_t *getsectiondata(const struct mach_header_64 *mhp, const char *segname, const char *sectname, unsigned long *size)
函数返回进程内指定映像mhp中的段segname中sectname节中内容的地址指针,而整个节的尺寸则返回到size所指的指针当中。这个函数的内部实现就是返回节描述信息结构struct section中的addr数据成员的值加上映像mhp的slide值。而size中返回的就是段描述信息结构中的size数据成员的值。
因为在前面讲过因为映像加载时的slide值的缘故,所以映像中的各种mach-o结构体中涉及到地址的数据成员的值都需要加上slide值才能得到映像在内存中的真实加载地址。
下面的例子获取进程中第0个映像的"__TEXT"段中的"__text"节的数据地址指针和尺寸:
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsectiondata(mhp, "__TEXT", "__text", &size);
7.获取mach-O文件中的某个段中某个节的描述信息
//获取指定mach-o文件中的某个段中某个节中的描述信息
const struct section *getsectbynamefromheader(const struct mach_header *mhp, const char *segname, const char *sectname)
//获取指定mach-o文件中的某个段中某个节中的描述信息。fSwap传NXByteOrder枚举值。
const struct section *getsectbynamefromheaderwithswap(struct mach_header *mhp, const char *segname, const char *sectname, int fSwap)
//上面对应函数的64位系统版本
const struct section_64 *getsectbynamefromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname)
//上面对应函数的64位系统版本
const struct section *getsectbynamefromheaderwithswap_64(struct mach_header_64 *mhp, const char *segname, const char *sectname, int fSwap)
这一系列函数分别返回32位系统和64位系统的mach-o文件的节的描述信息。每个函数都有segname和sectname分别指明要获取的段名和节名。参数mhp则表明mach-o文件的头部结构指针。对于有一些系统或者mach-o文件中的数值采用big-endian来编码,因此对于这些采用big-endian编码的结构来说就需要传递fSwap来确定是否交换这些编码。
这一系列函数中的mhp结构不局限于进程中的映像的头部结构,针对mach-o文件的头部结构也适用,如果你不了解映像和文件的区别则请看文章中的开头的介绍。
因为不管是进程中的映像的Section的排列以及mach-o文件中的Section的排列都是一致的,因此其实上述的getsectbyname的实现就是借助本节提供的函数实现的,其实现的代码如下:
const struct section_64 *getsectbyname(
const char *segname,
const char *sectname)
{
const struct mach_header_64 *mhp = _NSGetMachExecuteHeader();
return getsectbynamefromheader_64(mhp, segname, sectname);
}
8.获取mach-o文件中的某段中的某个节的数据指针和尺寸
//获取指定mach-o文件中的某个段中的某个节的数据指针和尺寸
char *getsectdatafromheader(const struct mach_header *mhp, const char *segname, const char *sectname, uint32_t *size)
//64位系统函数
char *getsectdatafromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname, uint64_t *size)
这两个函数返回32位系统或者64位系统中的某个mach-o文件中的某个段中某个节的数据指针和尺寸。这两个函数其实就是返回对应的节描述信息结构struct section中的addr值和size值。因为这两个函数是针对mach-o文件的,但是也可以用在对应的库映像中,当应用在库映像中时就要记得对返回的结果加上对应的slide值才是真实的节数据所对应的地址!
地址和符号查询API
在一些场景中需要根据某个地址查询其所归属的函数名称,以及某个变量的符号名称这就要借助地址和符号查询的函数,这个函数定义在头文件dlfcn.h中。
1.获取地址归属的库以及最近的符号信息。
int dladdr(const void *, Dl_info *);
函数的入参是一个地址值,而输出的Dl_info信息则是地址所归属的库和符号信息,如果某个地址能够找到对应的符号信息则返回非0,否则返回0。Dl_info这个结构体定义如下:
typedef struct dl_info {
const char *dli_fname; /* 地址归属的映像库文件名称 */
void *dli_fbase; /* 地址归属的库在内存中的基地址 */
const char *dli_sname; /* 离地址最近的符号名称 */
void *dli_saddr; /* 离地址最近的符号名称的开始地址 */
} Dl_info;
这里值得关注的就是dli_fbase是指的库在内存中的映像的首地址,我们知道mach-o文件的格式的头部是一个mach_header结构,因此这里的dli_fbase就是指向对应库头部的mach_header结构体指针。比如下面的代码我想获取objc_msgSend这个函数所在的库以及对应的函数名时就可以如下获取:
#import
#import
#import
Dl_info info;
memset(&info, 0, sizeof(info));
if (dladdr(objc_msgSend, &info) != 0)
{
printf("dli_fname=%s\ndli_fbase=%p\ndli_sname=%s\ndl_saddr=%p\n", info.dli_fname, info.dli_fbase, info.dli_sname, info.dli_saddr);
struct mach_header_64 *pheader = (struct mach_header_64*)info.dli_fbase;
//...
}
通过这个函数可以获取某个函数指针所对应的字符串名称,以及通过地址能够获取对应的映像头结构信息。
Demo
测这个类的实例方法的实现地址是否在可执行程序的映像的地址区间范围内来判断这个方法是否被恶意HOOK了
#import
#import
BOOL checkMethodBeHooked(Class class, SEL selector)
{
//你也可以借助runtime中的C函数来获取方法的实现地址
IMP imp = [class instanceMethodForSelector:selector];
if (imp == NULL)
return NO;
//计算出可执行程序的slide值。
intptr_t pmh = (intptr_t)_NSGetMachExecuteHeader();
intptr_t slide = 0;
#ifdef __LP64__
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
#else
const struct segment_command *psegment = getsegbyname("__TEXT");
#endif
intptr_t slide = pmh - psegment->vmaddr
unsigned long startpos = (unsigned long) pmh;
unsigned long endpos = get_end() + slide;
unsigned long imppos = (unsigned long)imp;
return (imppos < startpos) || (imppos > endpos);
}
自定义Mach-O 文件的 Section 数据以及读取
#import
#import
#include
#include
#ifndef __LP64__
#define mach_header mach_header
#else
#define mach_header mach_header_64
#endif
const struct mach_header *machHeader = NULL;
static NSString *configuration = @"";
//设置"__DATA,__customSection"的数据为kyson
char *kString __attribute__((section("__DATA,__customSection"))) = (char *)"kyson-sa-dd-as-d-asd";
int main(int argc, const char * argv[]) {
@autoreleasepool {
//设置machheader信息
if (machHeader == NULL)
{
Dl_info info;
dladdr((__bridge const void *)(configuration), &info);
machHeader = (struct mach_header_64*)info.dli_fbase;
}
unsigned long byteCount = 0;
uintptr_t* data = (uintptr_t *) getsectiondata(machHeader, "__DATA", "__customSection", &byteCount);
NSUInteger counter = byteCount/sizeof(void*);
for(NSUInteger idx = 0; idx < counter; ++idx)
{
char *string = (char*)data[idx];
NSString *str = [NSString stringWithUTF8String:string];
NSLog(@"%@",str);
}
}
return 0;
}
获取代码段的起始、结束位置
可以用来做代码段校验
typedef void (*Initializer);
extern const Initializer inits_start __asm("section$start$__TEXT$__text");
extern const Initializer inits_end __asm("section$end$__TEXT$__text");
NSLog(@"inits_start:%p", &inits_start);
NSLog(@"inits_end:%p", &inits_end);
获取函数地址并调用
attribute((constructor)) 修饰的方法会存入到 Mach-O 的__mod_init_func section 中。从中读取数据并遍历执行。
但是执行了 &inits_start 取地址之后,系统的自动执行初始化方法就不会再执行.
__attribute__((constructor)) void init_funcs()
{
printf("--------init funcs.--------\n");
printf("--------init done--------\n");
}
__attribute__((constructor)) void init_funcs2()
{
printf("--------init funcs2.--------\n");
printf("--------init done2--------\n");
}
typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[]);
extern const Initializer inits_start __asm("section$start$__DATA$__mod_init_func");
extern const Initializer inits_end __asm("section$end$__DATA$__mod_init_func");
NSLog(@"inits_start:%p", &inits_start);
NSLog(@"inits_end:%p", &inits_end);
for (const Initializer* p = &inits_start; p < &inits_end; ++p) {
(*p)(argc, argv, NULL, NULL);
}
获取函数地址并调用
直接使用 __asm 去获取函数符号地址执行
int Func(int a, int b) {
return a + b;
}
int myFunc(int a, int b) __asm("_Func");
int myprintf(const char * __restrict, ...) __asm("_printf");
myprintf("%s", "www.dlllhook.com");
NSLog(@"ret=%d", myFunc(1, 2));
参考:
- 访问自身Mach-O、调用函数等
- __asm
- iOS系统底层之程序映像