Vault7: CIA Hacking Tools Revealed
Navigation: » Directory
Owner: User #71491
User #71491
Pages | Date | User |
---|---|---|
Attachments:
Blog posts:
-
[User #71491]: Working on the Mac Kernel from EFI
This blog post will cover the work that I've been doing for QuarkMatter to attempt to hook the OSOperating System X kernel from an EFIExtensible Firmware Interface driver.
The following sections will detail each piece needed to do things to the OSOperating System X kernel, at least as I did them. I will attempt to identify where I found information for each section so you can go look at all of this too.
General Notes
This post assumes that you already understand EFIExtensible Firmware Interface basics and ExitBootServices Hooking , as this relies on those topics.
This post also assumes that you are working on OSOperating System X 10.8 or newer, and thus are using an X86_64 architecture. No PowerPC, please.
As a side note: some of the sources l list are header files, which in Yosemite, are located in /usr/include. On my User #? machine, these headers are not in /usr/include anymore, so you will need to find them in one of the Apple SDKs or the xnu source.
Finding the Mac Kernel
Sources: DarkMatter code (primarily src/EFI/Loader/*)
QuarkMatter code testing (eficore)
/usr/include/mach-o/loader.h (the header file on OSOperating System X 10.10, or xnu-base/EXTERNAL_HEADERS/mach-o/loader.h, if you are browsing the xnu source)
reverse.put.as (@osxreverser's blog)
The Mac kernel is a Mach-O executable, located either at /mach_kernel (pre-10.10) or /System/Library/Kernels/kernel (10.10 and after.) As a mach-o, it starts with a mach_header_64 struct. This structure can be found in loader.h, but the important thing to know is that this structure starts with a magic number!
In the case of the 64-bit mach kernel, this magic number is 0xFEEDFACF. Thus, in order to find a mach-o in memory, you simply need to find that magic number. However, at ExitBootServices there may be more than one mach-o in memory, and 0xFEEDFACF is the magic number for all 64-bit mach-o's.
Fortunately, Apple places the Mac kernel in a somewhat predictable location. Using the Darkmatter source and osxreverser's blog, I found that the kernel is located at address (0x00200000 + (n * 0x00100000)), where 0<= n < 3FF. This means the kernel location can be at most 0x40000000.
From my tests in trying to find the kernel in QuarkMatter, I found that at least for OSOperating System X 10.8-11, the mach kernel is the only mach-o located at any of those offsets.
If you would like to see my code that does this, you can find it (once it has been merged) in eficore/src/MachUtil/KernelLib.c in the FindKernel() function.
Parsing the Kernel to Find the Symbol and String Tables
Sources: DarkMatter code
/usr/include/mach-o/loader.h
developer.apple.com "OS X ABI Mach-O File Format Reference"
Now that I located the kernel, I could attempt to parse it to find various structures that I'd need later. The first ones I needed to find are the symbol table and the string table, because with those, I could identify which version of the OSOperating System X kernel is about to run.
Below is the diagram from Apple's Mach-O file format reference page which shows the basic structure of a mach-o. Although understanding this wasn't necessary to find the kernel, it will be from here on out.
As you can see above, the section immediately following the mach header contains load commands for each segment of the binary. There are multiple types of load commands, each representing its own segment type with its own unique structure.
The first two fields of a load command are always a struct load_command (loader.h), and contain the load command type and the command size.
The two specific load command types that we care about in this section are the LC_SYMTAB load command and a specific instance of the LC_SEGMENT_64 load command.
The LC_SYMTAB command structure is defined below.
struct symtab_command { uint32_t cmd; /* LC_SYMTAB */ uint32_t cmdsize; /* sizeof(struct symtab_command) */ uint32_t symoff; /* symbol table offset */ uint32_t nsyms; /* number of symbol table entries */ uint32_t stroff; /* string table offset */ uint32_t strsize; /* string table size in bytes */ };As you can see, this command contains information about the offsets of the symbol and string tables, as well as size information about each table.
In a normal mach-o, this offset is the offset from the beginning of the binary to the symbol table. However, for reasons I have yet to determine, this is not the case for the Mac kernel. Instead, this is an offset from a different spot of the binary, from which all offsets appear to be derived.
In order to calculate this offset, you will need to find the __LINKEDIT segment. To do this, you need to look for the LC_SEGMENT_64 load command with the segname of "__LINKEDIT". I have included the 64-bit segment command structure below. This __LINKEDIT segment is the segment in which the symbol table and string table reside.
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 VMVirtual Machine protection */ vm_prot_t initprot; /* initial VMVirtual Machine protection */ uint32_t nsects; /* number of sections in segment */ uint32_t flags; /* flags */ };The two fields that will help calculate the offset of the User #73998 are the vmaddr and fileoff fields. Applying the virtual to physical mask 0x0000007FFFFFFFFF to vmaddr, we can get the physical address of the segment. Because the offsets are offset from the same base address, if you subtract the offset of the LinkEdit segment (fileoff) from the offset of the symbol and string tables (symoff and stroff form the LC_SYMTAB command), you can calculate the offsets within the __LINKEDIT segment of those User #73998, and use the physical address of the segment to find the User #73998.
In summary, if linkedit_cmd is a pointer to the LC_SEGMENT_64 load command referring to the __LINKEDIT segment, and symtab_cmd is a pointer to the LC_SYMTAB load command, and the virtual to physical mask is called V_TO_P_MASK, then:
Symtab_addr = (linkedit_cmd->vmaddr & V_TO_P_MASK) + (symtab_cmd->symoff - linkedit_cmd->fileoff) Strtab_addr = (linkedit_cmd->vmaddr & V_TO_P_MASK) + (symtab_cmd->stroff - linkedit_cmd->fileoff)Looking up Symbols and Identifying the OS
Sources: DarkMatter code
/usr/include/mach-o/nlist.h
At this point, you now have everything you need to look up kernel symbols.
An entry in the symbol table is defined as a struct nlist_64, which is included below.
struct nlist_64 { union { uint32_t n_strx; /* index into the string table */ } n_un; uint8_t n_type; /* type flag, see below */ uint8_t n_sect; /* section number or NO_SECT */ uint16_t n_desc; /* see <mach-o/stab.h> */ uint64_t n_value; /* value of this symbol (or stab offset) */ };The fields that are most important for looking up kernel symbols are n_un.n_strx and n_value. n_un is a union for legacy purposes, as the 32-bit nlist struct also allowed for that value to be a pointer. However, in the 64-bit kernel, this value is always an index into the string table where the name of the symbol is located.
Thus, to get the name of the "entry_num" symbol in the symbol table, you simply read the string from (strtable_addr) + Symtab[entry_num].n_un.n_strx
Likewise, n_value will be the memory address of the symbol's value. This means that once you find the correct nlist_64 for the symbol you want to find, getting its value is trivial.
Because the symbol table is sorted alphabetically by symbol name (n_un.n_strx), you can use binary search (or your other favorite search algorithm) to find the symbol you are looking for fairly quickly, versus scanning the whole symbol table.
With the ability to lookup a symbol, identifying the OSOperating System version that is about to run is fairly straightforward. One of the symbols, _osrelease, has a specific value that signifies which OSOperating System version is running. Those values for OSOperating System X 10.8-11 are below.
MountainLion (10.8.x) -- "12.x" #where x is the subversion (10.8.x) that I don't care about Mavericks (10.9.x) -- "13.x" Yosemite (10.10.x) -- "14.x" User #? (10.11.x) -- "15.x"And that's it! Find the kernel, parse the kernel, and look up the "_osrelease" symbol, and you know, from EFI, which version of OSOperating System X is about to run.
Loading, Linking, and Relocating a Mach-O
Sources: DarkMatter code
/usr/include/mach-o/loader.h
/usr/include/mach-o/nlist.h
/usr/include/mach-o/reloc.h
/usr/include/mach-o/x86_64/reloc.h
This section takes a bit of a turn from the previous sections. Previously, you were dealing directly with the kernel mach-o. Now, you will move forward to how you would do dyld's job and load, link, and relocate a Mach-o into memory.
Note: You are still doing this from an EFIExtensible Firmware Interface driver, so you will need to make sure that all of the memory that you use is runtime services memory so it persists after the DXEDriver Execution Environment phase of the EFIExtensible Firmware Interface boot ends.
Loading
Similar to parsing the kernel mach-o, the first thing you need to do is find the LC_SYMTAB and LC_SEGMENT_64 (for __LINKEDIT) commands like before. In addition, you will need to find the LC_DYSYMTAB command. You can do this the same way as finding those commands earlier, by parsing the load commands.
Once those are located, the next step is to actually load the LC_SEGMENT_64 segments into memory. As shown in the Mach-o diagram near the top, all of the data for the mach-o is located in a segment. From looking into the DarkMatter code, it appears that you will need to allocate EfiRuntimeServicesCode, as 10.8 introduced an NX stack, and because you need it to be runtime data so it persists past the end of ExitBootServices. Also, to clarify – although you have already loaded the entire Mach-o into memory somehow (otherwise you couldn't parse it, manipulate it, etc as easily), you are now loading parts of it specifically so it will run when the OSOperating System has booted. As a result, from here on, if I mention manipulating values, you are manipulating the values that are in your recently-allocated RuntimeServicesCode memory block.
Linking
That is all there is to loading the mach-o. The next step is to link the executable by resolving any undefined symbols. This is where the LC_DYSYMTAB load command will be helpful. The dysymtab_command structure is provided below for reference. I included all of the comments from the header file, though we won't need all of them at all.
struct dysymtab_command { uint32_t cmd; /* LC_DYSYMTAB */ uint32_t cmdsize; /* sizeof(struct dysymtab_command) */ /* * The symbols indicated by symoff and nsyms of the LC_SYMTAB load command * are grouped into the following three groups: * local symbols (further grouped by the module they are from) * defined external symbols (further grouped by the module they are from) * undefined symbols * * The local symbols are used only for debugging. The dynamic binding * process may have to use them to indicate to the debugger the local * symbols for a module that is being bound. * * The last two groups are used by the dynamic binding process to do the * binding (indirectly through the module table and the reference symbol * table when this is a dynamically linked shared library file). */ uint32_t ilocalsym; /* index to local symbols */ uint32_t nlocalsym; /* number of local symbols */ uint32_t iextdefsym;/* index to externally defined symbols */ uint32_t nextdefsym;/* number of externally defined symbols */ uint32_t iundefsym; /* index to undefined symbols */ uint32_t nundefsym; /* number of undefined symbols */ /* * For the for the dynamic binding process to find which module a symbol * is defined in the table of contents is used (analogous to the ranlib * structure in an archive) which maps defined external symbols to modules * they are defined in. This exists only in a dynamically linked shared * library file. For executable and object modules the defined external * symbols are sorted by name and is use as the table of contents. */ uint32_t tocoff; /* file offset to table of contents */ uint32_t ntoc; /* number of entries in table of contents */ /* * To support dynamic binding of "modules" (whole object files) the symbol * table must reflect the modules that the file was created from. This is * done by having a module table that has indexes and counts into the merged * User #73998 for each module. The module structure that these two entries * refer to is described below. This exists only in a dynamically linked * shared library file. For executable and object modules the file only * contains one module so everything in the file belongs to the module. */ uint32_t modtaboff; /* file offset to module table */ uint32_t nmodtab; /* number of module table entries */ /* * To support dynamic module binding the module structure for each module * indicates the external references (defined and undefined) each module * makes. For each module there is an offset and a count into the * reference symbol table for the symbols that the module references. * This exists only in a dynamically linked shared library file. For * executable and object modules the defined external symbols and the * undefined external symbols indicates the external references. */ uint32_t extrefsymoff; /* offset to referenced symbol table */ uint32_t nextrefsyms; /* number of referenced symbol table entries */ /* * The sections that contain "symbol pointers" and "routine stubs" have * indexes and (implied counts based on the size of the section and fixed * size of the entry) into the "indirect symbol" table for each pointer * and stub. For every section of these two types the index into the * indirect symbol table is stored in the section header in the field * reserved1. An indirect symbol table entry is simply a 32bit index into * the symbol table to the symbol that the pointer or stub is referring to. * The indirect symbol table is ordered to match the entries in the section. */ uint32_t indirectsymoff; /* file offset to the indirect symbol table */ uint32_t nindirectsyms; /* number of indirect symbol table entries */ * To support relocating an individual module in a library file quickly the * external relocation entries for each module in the library need to be * accessed efficiently. Since the relocation entries can't be accessed * through the section headers for a library file they are separated into * groups of local and external entries further grouped by module. In this * case the presents of this load command who's extreloff, nextrel, * locreloff and nlocrel fields are non-zero indicates that the relocation * entries of non-merged sections are not referenced through the section * structures (and the reloff and nreloc fields in the section headers are * set to zero). * * Since the relocation entries are not accessed through the section headers * this requires the r_address field to be something other than a section * offset to identify the item to be relocated. In this case r_address is * set to the offset from the vmaddr of the first LC_SEGMENT command. * For MH_SPLIT_SEGS images r_address is set to the the offset from the * vmaddr of the first read-write LC_SEGMENT command. * * The relocation entries are grouped by module and the module table * entries have indexes and counts into them for the group of external * relocation entries for that the module. * * For sections that are merged across modules there must not be any * remaining external relocation entries for them (for merged sections * remaining relocation entries must be local). */ uint32_t extreloff; /* offset to external relocation entries */ uint32_t nextrel; /* number of external relocation entries */ /* * All the local relocation entries are grouped together (they are not * grouped by their module since they are only used if the object is moved * from it staticly link edited address). */ uint32_t locreloff; /* offset to local relocation entries */ uint32_t nlocrel; /* number of local relocation entries */ };The first values of note are iundefsym and nundefsym. As commented in the struct, iundefsym is the index into the symbol table of the first undefined symbol, and nundefsym is the number of defined symbols.
As it turns out, I partially lied earlier about how the symbol table is sorted. The symbol table is sorted alphabetically, but only within each section. The undefined symbols are their own section of the symbol table. This didn't matter in the kernel, because from what I can tell, there aren't undefined symbols inside of the kernel.
Using iundefsym and nundefsym, you can iterate through symbols in the symbol table to list each of the undefined symbols. In addition, each undefined symbol's n_type value will have the N_EXT bit set to specify that it is an external symbol, and its N_TYPE bits will be equal to N_UNDF, signifying that the symbol is undefined.
If you'd like more clarification about those fields, please refer back to the n_list_64 struct and the constants defined in nlist.h. I've left them out of this post, because it's already becoming fairly lengthy.
Now that you have identified each of the undefined symbols, you can look up those symbols in the kernel's symbol table. Because you can look up the kernel symbol by name and find its value, you can simply do that lookup and copy the memory address of the value into the undefined symbol's n_list_64.n_value field, and you will have resolved that undefined symbol.
If the symbol is not in the kernel, that may be ok. Some symbols have the N_WEAK_REF bit of the n_desc field set. If this is the case, this means that the symbol is allowed to be missing, and if it is, that the n_value is to be set to 0.
Relocating (Not completed yet)
At this point, you have loaded the binary into memory and done linking to resolve external symbols. The only remaining thing to do is to relocate the binary so its pointers that need to be relocated actually point in the correct places once virtual memory has been turned on and the kernel is running.
At least for me, this was the most complicated part of dealing with a mach-o. Fortunately, because we are only dealing with x86_64 binaries, some of the more complicated relocation types will not be encountered, so we can ignore them.
Before we get into specific types of relocations that we may encounter, I want to describe the two general types of relocations: local and external relocations. Local relocations are not common in x86_64 binaries, and typically just need to be turned into virtual addresses.
External relocations, on the other hand, rely on information in the symbol table to recalculate the address that they are to use. We will be dealing primarily with external relocations.
There are only two types of external relocations that we could potentially see in a mach-o that we're trying to load from EFI. Those types are:
1) X86_64_RELOC_UNSIGNED: a relocation used for absolute addresses
2) X86_64_RELOC_BRANCH: a relocation used for a call/jmp instruction with a 32-bit displacement