macho/macho-go/doc/binding.html.pm
2023-05-31 16:17:03 +07:00

148 lines
5.3 KiB
Perl

#lang pollen
(define (sidenote label . xs)
`(splice-me
(label ((for ,label) (class "margin-toggle sidenote-number")))
(input ((id ,label) (class "margin-toggle")(type "checkbox")))
(span ((class "sidenote")) ,@xs)))
h1{Bind and protect Mach-O Binary}
section{
p{
Our product protects the iOS application. To do that, we modifies the original application such that without our library, the binary fails to start. We also detect jailbroken device, frida hooking device and crash the app if detected.
}
p{
We write ourself the wrapper, which modifies the original binary. The wrapper also create a code{LC_LOAD_DYLIB} command to load our library on runtime.
}
}
h2{Building the library}
section{
p{
We build the library as a dynamic library and distribute as a framework, code{bcell.framework/bcell}.
}
}
h3{Run code before the binary}
section{
p{
We ultilize the load order of code{dyld} to load our library and run our library code without requiring the user to call our library. We put our code{main} function inside the code{S_MOD_INIT_FUNC_POINTERS} section for code{dyld} to call when our library is loaded. We annotate the function with code{__attribute__((constructor))} to achive the effect.
}
pre{
void __attribute__((constructor)) main()
}
p{
Then the application must tell code{dyld} to load our library. We add a load command code{LC_LOAD_DYLIB} with the path code{@rpath/bcell.framework/bcell}. The framework folder is copied into Frameworks folder of the application.
}
}
h3{Remove unecessary information}
section{
h4{LC_FUNCTION_STARTS and LC_DATA_IN_CODE}
p{
}
h4{Classic linker symbols data}
p{
}
}
h3{Erasing initialization pointers}
section{
p{
Initialization pointers are pointers to functions, these functions are called by code{dyld} before code{__start}. These pointers are located inside section marked code{S_MOD_INIT_FUNC_POINTERS}. Conveniently this section can be modified as writable at runtime. Because ASLR sidenote["aslr"]{images loaded in memory with a random offset} adds a small offset from the prefered image address on load, so hardcoded pointers are misplaced. code{dyld} takes the concrete value at this place and call the function. To make the value points correctly into the function, code{dyld} add the pointers with ASLR offset before dispatching the function sidenote["aslr dyld"]{One may ask "why don't ◊code{dyld} get the value and add the ASLR offset later". We do not know, but the current design of code{dyld} is as presented}.
}
p{
Because this section is writable at runtime, we can modify the value at runtime. That is, we erase this section data, or replace with random bytes. When our library run, if every is fine, we restore the section with the correct pointers' value (with ASLR offset). Erasing this section is easy, just replace the bytes in the Mach-O binary at the section marked code{S_MOD_INIT_FUNC_POINTERS}.
}
}
h3{Remove lazy bind information}
section{
p{
}
}
h3{Restore on runtime}
section{
h4{Finding the mapped image on memory and ASLR offset}
p{
code{dyld} exposes some functions for traversing the list of loaded image. We use this to find the mapped executable image.
}
pre{
#if __ARM_ARCH_7A__
typedef struct mach_header macho_header;
typedef struct nlist symbol_t;
#elif __ARM_ARCH_7S__
typedef struct mach_header macho_header;
typedef struct nlist symbol_t;
#elif __ARM_ARCH_ISA_A64
typedef struct mach_header_64 macho_header;
typedef struct nlist_64 symbol_t;
#endif
for (unsigned int i = 0; i < _dyld_image_count(); i++) {
macho_header *mach_hdr = NULL;
mach_hdr = (macho_header *)_dyld_get_image_header(i);
if (mach_hdr == NULL) {
continue;
}
if (mach_hdr->filetype != MH_EXECUTE) {
continue;
}
return mach_hdr;
}
}
p{
Finding the ASLR offset is easy, we need only the prefered image base. The prefered image base is the vmaddr of a code{LC_SEGMENT} where fileoff == 0 but filesize != 0.
}
pre{
for _, segment := range segments {
if segment.Fileoff() == 0 && segment.Filesize() != 0 {
return segment.Vmaddr()
}
}
}
p{
We can save this prefered image base when we modify the binary or we can traverse the load commands from the mapped image header found. The ASLR offset is the subtraction of the mapped image header address and the prefered image base.
}
pre{
const uint64_t aslr_offset = (uint64_t)mach_hdr - (uint64_t)prefered_image_base;
}
h4{Restoring Initialization pointers}
p{
Restoring these pointers are easy, we need to locate the section on memory, set the section as writable, find the ASLR offset and fill the section with appropriate function pointers. Pointers should be written in the correct order as they originally appear due to functions using data initialized by previous functions.
}
p{
After finding the mapped image header on memory, we traverse the load commands looking for section marked code{S_MOD_INIT_FUNC_POINTERS}. Next we set the section as writable, with size page aligned.
}
pre{
vm_protect(mach_task_self(),
(vm_address_t)addr, size,
false, VM_PROT_WRITE | VM_PROT_READ)
}
p{
After that we write the pointers value with ASLR offset added to the section. And the Initialization pointers are restored.
}
h4{Restoring lazy bind}
p{
}
}