在拥有以上的知识后我们可以先构想以下的运行器需要做什么.
因为x64的Windows和Linux程序使用的cpu指令集都是一样的,我们可以直接执行汇编而不需要一个指令模拟器,
而且这次我打算在用户层实现, 所以不能像Bash On Windows一样模拟syscall, 这个运行器会像下图一样模拟libc库的函数
这样运行器需要做的事情有:
这些工作会在以下的示例程序中一一实现, 完整的源代码可以看文章顶部的链接
首先我们需要把ELF文件格式对应的代码从binutils
中复制过来, 它包含了ELF头, 程序头和相关的数据结构, 里面用unsigned char[]
是为了防止alignment
, 这样结构体可以直接从文件内容中转换过来
ELFDefine.h
:
#pragma once
namespace HelloElfLoader {
// 以下内容复制自
// https://github.com/aeste/binutils/blob/develop/elfcpp/elfcpp.h
// https://github.com/aeste/binutils/blob/develop/include/elf/external.h
// e_ident中各项的偏移值
const int EI_MAG0 = 0;
const int EI_MAG1 = 1;
const int EI_MAG2 = 2;
const int EI_MAG3 = 3;
const int EI_CLASS = 4;
const int EI_DATA = 5;
const int EI_VERSION = 6;
const int EI_OSABI = 7;
const int EI_ABIVERSION = 8;
const int EI_PAD = 9;
const int EI_NIDENT = 16;
// ELF文件类型
enum {
ELFCLASSNONE = 0,
ELFCLASS32 = 1,
ELFCLASS64 = 2
};
// ByteOrder
enum {
ELFDATANONE = 0,
ELFDATA2LSB = 1,
ELFDATA2MSB = 2
};
// 程序头类型
enum PT
{
PT_NULL = 0,
PT_LOAD = 1,
PT_DYNAMIC = 2,
PT_INTERP = 3,
PT_NOTE = 4,
PT_SHLIB = 5,
PT_PHDR = 6,
PT_TLS = 7,
PT_LOOS = 0x60000000,
PT_HIOS = 0x6fffffff,
PT_LOPROC = 0x70000000,
PT_HIPROC = 0x7fffffff,
// The remaining values are not in the standard.
// Frame unwind information.
PT_GNU_EH_FRAME = 0x6474e550,
PT_SUNW_EH_FRAME = 0x6474e550,
// Stack flags.
PT_GNU_STACK = 0x6474e551,
// Read only after relocation.
PT_GNU_RELRO = 0x6474e552,
// Platform architecture compatibility information
PT_ARM_ARCHEXT = 0x70000000,
// Exception unwind tables
PT_ARM_EXIDX = 0x70000001
};
// 动态节类型
enum DT
{
DT_NULL = 0,
DT_NEEDED = 1,
DT_PLTRELSZ = 2,
DT_PLTGOT = 3,
DT_HASH = 4,
DT_STRTAB = 5,
DT_SYMTAB = 6,
DT_RELA = 7,
DT_RELASZ = 8,
DT_RELAENT = 9,
DT_STRSZ = 10,
DT_SYMENT = 11,
DT_INIT = 12,
DT_FINI = 13,
DT_SONAME = 14,
DT_RPATH = 15,
DT_SYMBOLIC = 16,
DT_REL = 17,
DT_RELSZ = 18,
DT_RELENT = 19,
DT_PLTREL = 20,
DT_DEBUG = 21,
DT_TEXTREL = 22,
DT_JMPREL = 23,
DT_BIND_NOW = 24,
DT_INIT_ARRAY = 25,
DT_FINI_ARRAY = 26,
DT_INIT_ARRAYSZ = 27,
DT_FINI_ARRAYSZ = 28,
DT_RUNPATH = 29,
DT_FLAGS = 30,
// This is used to mark a range of dynamic tags. It is not really
// a tag value.
DT_ENCODING = 32,
DT_PREINIT_ARRAY = 32,
DT_PREINIT_ARRAYSZ = 33,
DT_LOOS = 0x6000000d,
DT_HIOS = 0x6ffff000,
DT_LOPROC = 0x70000000,
DT_HIPROC = 0x7fffffff,
// The remaining values are extensions used by GNU or Solaris.
DT_VALRNGLO = 0x6ffffd00,
DT_GNU_PRELINKED = 0x6ffffdf5,
DT_GNU_CONFLICTSZ = 0x6ffffdf6,
DT_GNU_LIBLISTSZ = 0x6ffffdf7,
DT_CHECKSUM = 0x6ffffdf8,
DT_PLTPADSZ = 0x6ffffdf9,
DT_MOVEENT = 0x6ffffdfa,
DT_MOVESZ = 0x6ffffdfb,
DT_FEATURE = 0x6ffffdfc,
DT_POSFLAG_1 = 0x6ffffdfd,
DT_SYMINSZ = 0x6ffffdfe,
DT_SYMINENT = 0x6ffffdff,
DT_VALRNGHI = 0x6ffffdff,
DT_ADDRRNGLO = 0x6ffffe00,
DT_GNU_HASH = 0x6ffffef5,
DT_TLSDESC_PLT = 0x6ffffef6,
DT_TLSDESC_GOT = 0x6ffffef7,
DT_GNU_CONFLICT = 0x6ffffef8,
DT_GNU_LIBLIST = 0x6ffffef9,
DT_CONFIG = 0x6ffffefa,
DT_DEPAUDIT = 0x6ffffefb,
DT_AUDIT = 0x6ffffefc,
DT_PLTPAD = 0x6ffffefd,
DT_MOVETAB = 0x6ffffefe,
DT_SYMINFO = 0x6ffffeff,
DT_ADDRRNGHI = 0x6ffffeff,
DT_RELACOUNT = 0x6ffffff9,
DT_RELCOUNT = 0x6ffffffa,
DT_FLAGS_1 = 0x6ffffffb,
DT_VERDEF = 0x6ffffffc,
DT_VERDEFNUM = 0x6ffffffd,
DT_VERNEED = 0x6ffffffe,
DT_VERNEEDNUM = 0x6fffffff,
DT_VERSYM = 0x6ffffff0,
// Specify the value of _GLOBAL_OFFSET_TABLE_.
DT_PPC_GOT = 0x70000000,
// Specify the start of the .glink section.
DT_PPC64_GLINK = 0x70000000,
// Specify the start and size of the .opd section.
DT_PPC64_OPD = 0x70000001,
DT_PPC64_OPDSZ = 0x70000002,
// The index of an STT_SPARC_REGISTER symbol within the DT_SYMTAB
// symbol table. One dynamic entry exists for every STT_SPARC_REGISTER
// symbol in the symbol table.
DT_SPARC_REGISTER = 0x70000001,
DT_AUXILIARY = 0x7ffffffd,
DT_USED = 0x7ffffffe,
DT_FILTER = 0x7fffffff
};;
// ELF头的定义
typedef struct {
unsigned char e_ident[16]; /* ELF "magic number" */
unsigned char e_type[2]; /* Identifies object file type */
unsigned char e_machine[2]; /* Specifies required architecture */
unsigned char e_version[4]; /* Identifies object file version */
unsigned char e_entry[8]; /* Entry point virtual address */
unsigned char e_phoff[8]; /* Program header table file offset */
unsigned char e_shoff[8]; /* Section header table file offset */
unsigned char e_flags[4]; /* Processor-specific flags */
unsigned char e_ehsize[2]; /* ELF header size in bytes */
unsigned char e_phentsize[2]; /* Program header table entry size */
unsigned char e_phnum[2]; /* Program header table entry count */
unsigned char e_shentsize[2]; /* Section header table entry size */
unsigned char e_shnum[2]; /* Section header table entry count */
unsigned char e_shstrndx[2]; /* Section header string table index */
} Elf64_External_Ehdr;
// 程序头的定义
typedef struct {
unsigned char p_type[4]; /* Identifies program segment type */
unsigned char p_flags[4]; /* Segment flags */
unsigned char p_offset[8]; /* Segment file offset */
unsigned char p_vaddr[8]; /* Segment virtual address */
unsigned char p_paddr[8]; /* Segment physical address */
unsigned char p_filesz[8]; /* Segment size in file */
unsigned char p_memsz[8]; /* Segment size in memory */
unsigned char p_align[8]; /* Segment alignment, file & memory */
} Elf64_External_Phdr;
// DYNAMIC类型的程序头的内容定义
typedef struct {
unsigned char d_tag[8]; /* entry tag value */
union {
unsigned char d_val[8];
unsigned char d_ptr[8];
} d_un;
} Elf64_External_Dyn;
// 动态链接的重定位记录,部分系统会用Elf64_External_Rel
typedef struct {
unsigned char r_offset[8]; /* Location at which to apply the action */
unsigned char r_info[8]; /* index and type of relocation */
unsigned char r_addend[8]; /* Constant addend used to compute value */
} Elf64_External_Rela;
// 动态链接的符号信息
typedef struct {
unsigned char st_name[4]; /* Symbol name, index in string tbl */
unsigned char st_info[1]; /* Type and binding attributes */
unsigned char st_other[1]; /* No defined meaning, 0 */
unsigned char st_shndx[2]; /* Associated section index */
unsigned char st_value[8]; /* Value of the symbol */
unsigned char st_size[8]; /* Associated symbol size */
} Elf64_External_Sym;
}
接下来我们定义一个读取和执行ELF文件的类, 这个类会在初始化时把文件加载到fileStream_
, execute
函数会负责执行
HelloElfLoader.h
:
#pragma once
#include <string>
#include <fstream>
namespace HelloElfLoader {
class Loader {
std::ifstream fileStream_;
public:
Loader(const std::string& path);
Loader(std::ifstream&& fileStream);
void execute();
};
}
构造函数如下, 也就是标准的c++打开文件的代码
HelloElfLoader.cpp:
Loader::Loader(const std::string& path) :
Loader(std::ifstream(path, std::ios::in | std::ios::binary)) {}
Loader::Loader(std::ifstream&& fileStream) :
fileStream_(std::move(fileStream)) {
if (!fileStream_) {
throw std::runtime_error("open file failed");
}
}
接下来将实现上面所说的步骤, 首先是解析ELF文件
void Loader::execute() {
std::cout << "====== start loading elf ======" << std::endl;
// 检查当前运行程序是否64位
if (sizeof(intptr_t) != sizeof(std::int64_t)) {
throw std::runtime_error("please use x64 compile and run this program");
}
// 读取ELF头
Elf64_External_Ehdr elfHeader = {};
fileStream_.seekg(0);
fileStream_.read(reinterpret_cast<char*>(&elfHeader), sizeof(elfHeader));
// 检查ELF头,只支持64位且byte order是little endian的程序
if (std::string(reinterpret_cast<char*>(elfHeader.e_ident), 4) != "\x7f\x45\x4c\x46") {
throw std::runtime_error("magic not match");
}
else if (elfHeader.e_ident[EI_CLASS] != ELFCLASS64) {
throw std::runtime_error("only support ELF64");
}
else if (elfHeader.e_ident[EI_DATA] != ELFDATA2LSB) {
throw std::runtime_error("only support little endian");
}
// 获取program table的信息
std::uint32_t programTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_phoff);
std::uint16_t programTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phentsize);
std::uint16_t programTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_phnum);
std::cout << "program table at: " << programTableOffset << ", "
<< programTableEntryNum << " x " << programTableEntrySize << std::endl;
// 获取section table的信息
// section table只给linker用,loader中其实不需要访问section table
std::uint32_t sectionTableOffset = *reinterpret_cast<std::uint32_t*>(elfHeader.e_shoff);
std::uint16_t sectionTableEntrySize = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
std::uint16_t sectionTableEntryNum = *reinterpret_cast<std::uint16_t*>(elfHeader.e_shentsize);
std::cout << "section table at: " << sectionTableOffset << ", "
<< sectionTableEntryNum << " x " << sectionTableEntrySize << std::endl;
ELF文件的的开始部分就是ELF头,和Elf64_External_Ehdr
结构体的结构相同, 我们可以读到Elf64_External_Ehdr
结构体中,
然后ELF头包含了程序头和节头的偏移值, 我们可以预先获取到这些参数
节头在运行时不需要使用, 运行时需要遍历程序头
// 准备动态链接的信息
std::uint64_t jmpRelAddr = 0; // 重定位记录的开始地址
std::uint64_t pltRelType = 0; // 重定位记录的类型 RELA或REL
std::uint64_t pltRelSize = 0; // 重定位记录的总大小
std::uint64_t symTabAddr = 0; // 动态符号表的开始地址
std::uint64_t strTabAddr = 0; // 动态符号名称表的开始地址
std::uint64_t strTabSize = 0; // 动态符号名称表的总大小
// 遍历program hedaer
std::vector<Elf64_External_Phdr> programHeaders;
programHeaders.resize(programTableEntryNum);
fileStream_.read(reinterpret_cast<char*>(programHeaders.data()), programTableEntryNum * programTableEntrySize);
std::vector<std::shared_ptr<void>> loadedSegments;
for (const auto& programHeader : programHeaders) {
std::uint32_t type = *reinterpret_cast<const std::uint32_t*>(programHeader.p_type);
if (type == PT_LOAD) {
// 把文件内容(包含程序代码和数据)加载到虚拟内存,这个示例不考虑地址冲突
std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
std::uint64_t fileSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_filesz);
std::uint64_t virtAddr = *reinterpret_cast<const std::uint64_t*>(programHeader.p_vaddr);
std::uint64_t memSize = *reinterpret_cast<const std::uint64_t*>(programHeader.p_memsz);
if (memSize < fileSize) {
throw std::runtime_error("invalid memsz in program header, it shouldn't less than filesz");
}
// 在指定的虚拟地址分配内存
std::cout << std::hex << "allocate address at: 0x" << virtAddr <<
" size: 0x" << memSize << std::dec << std::endl;
void* addr = ::VirtualAlloc((void*)virtAddr, memSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (addr == nullptr) {
throw std::runtime_error("allocate memory at specific address failed");
}
loadedSegments.emplace_back(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
// 复制文件内容到虚拟内存
fileStream_.seekg(fileOffset);
if (!fileStream_.read(reinterpret_cast<char*>(addr), fileSize)) {
throw std::runtime_error("read contents into memory from LOAD program header failed");
}
}
else if (type == PT_DYNAMIC) {
// 遍历动态节
std::uint64_t fileOffset = *reinterpret_cast<const std::uint64_t*>(programHeader.p_offset);
fileStream_.seekg(fileOffset);
Elf64_External_Dyn dynSection = {};
std::uint64_t dynSectionTag = 0;
std::uint64_t dynSectionVal = 0;
do {
if (!fileStream_.read(reinterpret_cast<char*>(&dynSection), sizeof(dynSection))) {
throw std::runtime_error("read dynamic section failed");
}
dynSectionTag = *reinterpret_cast<const std::uint64_t*>(dynSection.d_tag);
dynSectionVal = *reinterpret_cast<const std::uint64_t*>(dynSection.d_un.d_val);
if (dynSectionTag == DT_JMPREL) {
jmpRelAddr = dynSectionVal;
}
else if (dynSectionTag == DT_PLTREL) {
pltRelType = dynSectionVal;
}
else if (dynSectionTag == DT_PLTRELSZ) {
pltRelSize = dynSectionVal;
}
else if (dynSectionTag == DT_SYMTAB) {
symTabAddr = dynSectionVal;
}
else if (dynSectionTag == DT_STRTAB) {
strTabAddr = dynSectionVal;
}
else if (dynSectionTag == DT_STRSZ) {
strTabSize = dynSectionVal;
}
} while (dynSectionTag != 0);
}
}
还记得我们上面使用readelf
读取到的信息吗?
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001f8 0x00000000000001f8 R E 8
INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x00000000000007d4 0x00000000000007d4 R E 200000
LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x0000000000000228 0x0000000000000230 RW 200000
DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
0x00000000000001d0 0x00000000000001d0 RW 8
NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x0000000000000680 0x0000000000400680 0x0000000000400680
0x000000000000003c 0x000000000000003c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 10
GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
0x00000000000001f0 0x00000000000001f0 R 1
这里面类型是LOAD
的头代表需要加载文件的内容到内存,Offset
是文件的偏移值, VirtAddr
是虚拟内存地址, FileSiz
是需要加载的文件大小, MemSiz
是需要分配的内存大小, Flags
是内存的访问权限,
这个示例不考虑访问权限(统一使用PAGE_EXECUTE_READWRITE).
这个程序有两个LOAD头, 第一个包含了代码和只读数据(.data, .init, .rodata等节的内容), 第二个包含了可写数据(.init_array, .fini_array等节的内容).
把LOAD
头对应的内容加载到指定的内存地址后我们就完成了构想中的第2个第3个步骤, 现在代码和数据都在内存中了.
接下来我们还需要处理动态链接的函数, 处理所需的信息可以从DYNAMIC
头得到DYNAMIC
头包含的信息有
Dynamic section at offset 0xe28 contains 24 entries:
标记 类型 名称/值
0x0000000000000001 (NEEDED) 共享库:[libc.so.6]
0x000000000000000c (INIT) 0x4003c8
0x000000000000000d (FINI) 0x400624
0x0000000000000019 (INIT_ARRAY) 0x600e10
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x600e18
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x400298
0x0000000000000005 (STRTAB) 0x400318
0x0000000000000006 (SYMTAB) 0x4002b8
0x000000000000000a (STRSZ) 63 (bytes)
0x000000000000000b (SYMENT) 24 (bytes)
0x0000000000000015 (DEBUG) 0x0
0x0000000000000003 (PLTGOT) 0x601000
0x0000000000000002 (PLTRELSZ) 48 (bytes)
0x0000000000000014 (PLTREL) RELA
0x0000000000000017 (JMPREL) 0x400398
0x0000000000000007 (RELA) 0x400380
0x0000000000000008 (RELASZ) 24 (bytes)
0x0000000000000009 (RELAENT) 24 (bytes)
0x000000006ffffffe (VERNEED) 0x400360
0x000000006fffffff (VERNEEDNUM) 1
0x000000006ffffff0 (VERSYM) 0x400358
0x0000000000000000 (NULL) 0x0
一个个看上面代码中涉及到的类型
.rela.plt
节在内存中保存的地址24 * 2 = 48
重定位节 '.rela.plt' 位于偏移量 0x398 含有 2 个条目:
偏移量 信息 类型 符号值 符号名称 + 加数
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 printf@GLIBC_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
.dynsym
节在内存中保存的地址.dynstr
节在内存中保存的地址Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
在遍历完程序头以后, 我们可以知道有两个动态链接的函数需要重定位, 它们分别是__libc_start_main
和printf
, 其中__libc_start_main
负责调用main
函数
接下来让我们需要设置这些函数的地址
// 读取动态链接符号表
std::string dynamicSymbolNames(reinterpret_cast<char*>(strTabAddr), strTabSize);
Elf64_External_Sym* dynamicSymbols = reinterpret_cast<Elf64_External_Sym*>(symTabAddr);
// 设置动态链接的函数地址
std::cout << std::hex << "read dynamic entires at: 0x" << jmpRelAddr <<
" size: 0x" << pltRelSize << std::dec << std::endl;
if (jmpRelAddr == 0 || pltRelType != DT_RELA || pltRelSize % sizeof(Elf64_External_Rela) != 0) {
throw std::runtime_error("invalid dynamic entry info, rel type should be rela");
}
std::vector<std::shared_ptr<void>> libraryFuncs;
for (std::uint64_t offset = 0; offset < pltRelSize; offset += sizeof(Elf64_External_Rela)) {
Elf64_External_Rela* rela = (Elf64_External_Rela*)(jmpRelAddr + offset);
std::uint64_t relaOffset = *reinterpret_cast<const std::uint64_t*>(rela->r_offset);
std::uint64_t relaInfo = *reinterpret_cast<const std::uint64_t*>(rela->r_info);
std::uint64_t relaSym = relaInfo >> 32; // ELF64_R_SYM
std::uint64_t relaType = relaInfo & 0xffffffff; // ELF64_R_TYPE
// 获取符号
Elf64_External_Sym* symbol = dynamicSymbols + relaSym;
std::uint32_t symbolNameOffset = *reinterpret_cast<std::uint32_t*>(symbol->st_name);
std::string symbolName(dynamicSymbolNames.data() + symbolNameOffset);
std::cout << "relocate symbol: " << symbolName << std::endl;
// 替换函数地址
// 原本应该延迟解决,这里图简单就直接覆盖了
void** relaPtr = reinterpret_cast<void**>(relaOffset);
std::shared_ptr<void> func = resolveLibraryFunc(symbolName);
if (func == nullptr) {
throw std::runtime_error("unsupport symbol name");
}
libraryFuncs.emplace_back(func);
*relaPtr = func.get();
}
上面的代码遍历了DT_JMPREL
重定位记录, 并且在加载时设置了这些函数的地址,
其实应该通过延迟解决实现的, 但是这里为了简单就直接替换成最终的地址了.
上面获取函数实际地址的逻辑我写到了resolveLibraryFunc
中,这个函数的实现在另外一个文件, 如下
namespace HelloElfLoader {
namespace {
// 原始的返回地址
thread_local void* originalReturnAddress = nullptr;
void* getOriginalReturnAddress() {
return originalReturnAddress;
}
void setOriginalReturnAddress(void* address) {
originalReturnAddress = address;
}
// 模拟libc调用main的函数,目前不支持传入argc和argv
void __libc_start_main(int(*main)()) {
std::cout << "call main: " << main << std::endl;
int ret = main();
std::cout << "result: " << ret << std::endl;
std::exit(0);
}
// 模拟printf函数
int printf(const char* fmt, ...) {
int ret;
va_list myargs;
va_start(myargs, fmt);
ret = ::vprintf(fmt, myargs);
va_end(myargs);
return ret;
}
// 把System V AMD64 ABI转换为Microsoft x64 calling convention
// 因为vc++不支持inline asm,只能直接写hex
// 这个函数支持任意长度的参数,但是性能会有损耗,如果参数数量已知可以编写更快的loader代码
const char generic_func_loader[]{
// 让参数连续排列在栈上
// [第一个参数] [第二个参数] [第三个参数] ...
0x58, // pop %rax 暂存原返回地址
0x41, 0x51, // push %r9 入栈第六个参数,之后的参数都在后续的栈上
0x41, 0x50, // push %r8 入栈第五个参数
0x51, // push %rcx 入栈第四个参数
0x52, // push %rdx 入栈第三个参数
0x56, // push %rsi 入栈第二个参数
0x57, // push %rdi 入栈第一个参数
// 调用setOriginalReturnAddress保存原返回地址
0x48, 0x89, 0xc1, // mov %rax, %rcx 第一个参数是原返回地址
0x48, 0x83, 0xec, 0x20, // sub $0x20, %rsp 预留32位的影子空间
0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
0xff, 0xd0, // callq *%rax 调用setOriginalReturnAddress
0x48, 0x83, 0xc4, 0x20, // add %0x20, %rsp 释放影子空间
// 转换到Microsoft x64 calling convention
0x59, // pop %rcx 出栈第一个参数
0x5a, // pop %rdx 出栈第二个参数
0x41, 0x58, // pop %r8 // 出栈第三个参数
0x41, 0x59, // pop %r9 // 出栈第四个参数
// 调用目标函数
0x48, 0x83, 0xec, 0x20, // sub $0x20, %esp 预留32位的影子空间
0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs 0, %rax
0xff, 0xd0, // callq *%rax 调用模拟的函数
0x48, 0x83, 0xc4, 0x30, // add $0x30, %rsp 释放影子空间和参数(影子空间32 + 参数8*2)
0x50, // push %rax 保存返回值
// 调用getOriginalReturnAddress获取原返回地址
0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs $0, %rax
0xff, 0xd0, // callq *%rax 调用getOriginalReturnAddress
0x48, 0x89, 0xc1, // mov %rax, %rcx 原返回地址存到rcx
0x58, // 恢复返回值
0x51, // 原返回地址入栈顶
0xc3 // 返回
};
const int generic_func_loader_set_addr_offset = 18;
const int generic_func_loader_target_offset = 44;
const int generic_func_loader_get_addr_offset = 61;
}
// 获取动态链接函数的调用地址
std::shared_ptr<void> resolveLibraryFunc(const std::string& name) {
void* funcPtr = nullptr;
if (name == "__libc_start_main") {
funcPtr = __libc_start_main;
}
else if (name == "printf") {
funcPtr = printf;
}
else {
return nullptr;
}
void* addr = ::VirtualAlloc(nullptr,
sizeof(generic_func_loader), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (addr == nullptr) {
throw std::runtime_error("allocate memory for _libc_start_main_loader failed");
}
std::shared_ptr<void> result(addr, [](void* ptr) { ::VirtualFree(ptr, 0, MEM_RELEASE); });
std::memcpy(addr, generic_func_loader, sizeof(generic_func_loader));
char* addr_c = reinterpret_cast<char*>(addr);
*reinterpret_cast<void**>(addr_c + generic_func_loader_set_addr_offset) = setOriginalReturnAddress;
*reinterpret_cast<void**>(addr_c + generic_func_loader_target_offset) = funcPtr;
*reinterpret_cast<void**>(addr_c + generic_func_loader_get_addr_offset) = getOriginalReturnAddress;
return result;
}
}
理解这段代码需要先了解什么是x86 calling conventions, 在汇编中传递函数参数的办法由很多种, 像cdecl
是把所有参数都放在栈中从低到高排列, 而fastcall
是把第一个参数放ecx
, 第二个参数放edx
, 其余参数放栈中.
我们需要模拟的64位Linux程序,它传参使用了System V AMD64 ABI
标准, 先把参数按RDI, RSI, RDX, RCX, R8, R9
的顺序设置,如果有再多参数就放在栈中.
而64位的Windows传参使用了Microsoft x64 calling convention
标准, 先把参数按RCX, RDX, R8, R9
的顺序设置,如果有再多参数就放在栈中, 除此之外还需要预留一个32字节的影子空间.
如果我们需要让Linux程序调用Windows程序中的函数, 需要对参数的顺序进行转换, 这就是上面的汇编代码所做的事情.
转换前的栈结构如下
[原返回地址 8bytes] [第七个参数] [第八个参数] ...
转换后的栈结构如下
[返回地址 8bytes] [影子空间 32 bytes] [第五个参数] [第六个参数] [第七个参数] ...
因为需要支持不定个数的参数, 上面的代码用了一个thread local
变量来保存原返回地址, 这样的处理会影响性能, 如果函数的参数个数已知可以换成更高效的转换代码.
在设置好动态链接的函数地址后, 我们完成了构想中的第4步, 接下来就可以运行主程序了
// 获取入口点
std::uint64_t entryPointAddress = *reinterpret_cast<const std::uint64_t*>(elfHeader.e_entry);
void(*entryPointFunc)() = reinterpret_cast<void(*)()>(entryPointAddress);
std::cout << "entry point: " << entryPointFunc << std::endl;
std::cout << "====== finish loading elf ======" << std::endl;
// 执行主程序
// 会先调用__libc_start_main, 然后再调用main
// 调用__libc_start_main后的指令是hlt,所以必须在__libc_start_main中退出执行
entryPointFunc();
入口点的地址在ELF头中可以获取到,这个地址就是_start
函数的地址, 我们把它转换成一个void()
类型的函数指针再执行即可,
至此示例程序完成了构想中的所有功能.
执行效果如下图
这份示例程序还有很多不足, 例如未支持32位Linux程序, 不支持加载其他Linux动态链接库(so), 不支持命令行参数等等.
而且这份示例程序和Bash On Windows的原理有所出入, 因为在用户层是无法模拟syscall.
我希望它可以让你对如何运行其他系统的可执行文件有一个初步的了解, 如果你希望更深入的了解如何模拟syscall, 可以查找rdmsr
和wrmsr
指令相关的资料.
如果您发现该资源为电子书等存在侵权的资源或对该资源描述不正确等,可点击“私信”按钮向作者进行反馈;如作者无回复可进行平台仲裁,我们会在第一时间进行处理!
加入交流群
请使用微信扫一扫!