How to develop an iOS tweak
by xiangxiang
Method Swizzling & fishhook & theos
0x00 Env Settings
theos
bash -c "$(px curl -fsSL https://raw.githubusercontent.com/theos/theos/master/bin/install-theos)"
$THEOS/bin/install-sdk iPhoneOS15.6
$THEOS/bin/nic.pl
# for TrollFools tweak, choose iphone/library
make
cp .theos/obj/debug/tweak.dylib /mnt/e/iOS-security/
FINALPACKAGE=1 make
cp .theos/obj/tweak.dylib /mnt/e/iOS-security/
VSCode
sudo apt-get install bear
make clean; bear -- make
dump IPA header
python -m venv ~/python-venv/iOS
source ~/python-venv/iOS/bin/activate
python -m pip install setuptools
python -m pip install --upgrade k2l
ktool dump --out ./headers --headers BINARY
0x01 Method Swizzling
1.1 Core Components of the Objective-C Runtime
-
https://developer.apple.com/documentation/objectivec/objective-c-runtime
- Selectors (
SEL
): A selector is just a C string that represents the name of a method - Implementations (
IMP
): An implementation is a pointer to the actual C function that contains the code for the method. Every method has an IMP - Methods (
Method
): A Method is an opaque struct in the runtime that bundles a selector (SEL) with its corresponding implementation (IMP) and type information - Dispatch Table: Every class has a dispatch table (often called a vtable) that maps selectors to their implementations. When you send a message to an object, the runtime looks up the selector, finds the corresponding IMP, and jumps to that function pointer to execute the code
Method swizzling directly manipulates this dispatch table
Class
Dispatch Table
+---------------------+
| SEL | IMP |
|---------------------|
| method1 ptr_1 ---|---> actual method1 code
| method2 ptr_2 |
| ... |
| |
| init ptr_init |
+---------------------+
Method
+---------------------------+
| method1(SEL) | (ptr1)IMP |
| | |
+---------------------------+
1.2 Method Swizzling for tweak
No access to private class inside the target app(ld error)
NSClassFromString
&NSSelectorFromString
- 由于我们的swizzled method不在原来的class中, 需要先加入到原class
class_addMethod
NSString *originClassName;
NSString *originMethodName
Class originClass = NSClassFromString(originClassName);
SEL swizzledSel = NSSelectorFromString(newMethodName);
class_addMethod(originalClass, swizzledSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
Superclass
- If the original method is implemented in a superclass,
class_addMethod
will add it to the current class
// Attempt to add the swizzled method to the class.
// If the original method is implemented in a superclass, this will add it to the current class.
BOOL didAddMethod = class_addMethod(cls,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// If the method was added successfully, it means the original method was in a superclass.
// We now need to replace the implementation of our swizzled method with the original (superclass's) implementation.
class_replaceMethod(cls,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// If the method already existed in this class, we can just exchange the implementations.
method_exchangeImplementations(originalMethod, swizzledMethod);
}
Class method
Class originMetaClass = object_getClass(originClass);
0x02 Fishhook
/*
* A structure representing a particular intended rebinding from a symbol
* name to its replacement
*/
struct rebinding {
const char *name; // 需要hook的函数名称(C字符串)
void *replacement; // 新函数地址
void **replaced; // 原始函数地址的指针
};
/*
* For each rebinding in rebindings, rebinds references to external, indirect
* symbols with the specified name to instead point at replacement for each
* image in the calling process as well as for all future images that are loaded
* by the process. If rebind_functions is called more than once, the symbols to
* rebind are added to the existing list of rebindings, and if a given symbol
* is rebound more than once, the later rebinding will take precedence.
*/
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
/*
* Rebinds as above, but only in the specified image. The header should point
* to the mach-o header, the slide should be the slide offset. Others as above.
*/
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
static int (*orig_close)(int);
int my_close(int fd) {
return orig_close(fd);
}
struct rebinding close_rebinding;
close_rebinding.name = "close";
close_rebinding.replacement = my_close;
close_rebinding.replaced = (void *) &orig_close; // 注意为了修改我们需要传指针
rebind_symbols(struct rebinding[1]{close_rebinding}, 1);
原理
- dyld共享缓存库 macOS下
/private/var/db/dyld/
-
dyld
binds lazy and non-lazy symbols by updating pointers in particular sections of the__DATA
segment of a Mach-O binary.__fishhook__
re-binds these symbols by determining the locations to update for each of the symbol names passed torebind_symbols
and then writing out the corresponding replacements. -
For a given image, the
__DATA
segment may contain two sections that are relevant for dynamic symbol bindings:__nl_symbol_ptr
and__la_symbol_ptr
.__nl_symbol_ptr
is an array of pointers to non-lazily bound data (these are bound at the time a library is loaded) and__la_symbol_ptr
is an array of pointers to imported functions that is generally filled by a routine calleddyld_stub_binder
during the first call to that symbol (it’s also possible to telldyld
to bind these at launch). In order to find the name of the symbol that corresponds to a particular location in one of these sections, we have to jump through several layers of indirection. For the two relevant sections, the section headers (struct section
s from<mach-o/loader.h>
) provide an offset (in thereserved1
field) into what is known as the indirect symbol table. The indirect symbol table, which is located in the__LINKEDIT
segment of the binary, is just an array of indexes into the symbol table (also in__LINKEDIT
) whose order is identical to that of the pointers in the non-lazy and lazy symbol sections. So, givenstruct section nl_symbol_ptr
, the corresponding index in the symbol table of the first address in that section isindirect_symbol_table[nl_symbol_ptr->reserved1]
. The symbol table itself is an array ofstruct nlist
s (see<mach-o/nlist.h>
), and eachnlist
contains an index into the string table in__LINKEDIT
which where the actual symbol names are stored. So, for each pointer__nl_symbol_ptr
and__la_symbol_ptr
, we are able to find the corresponding symbol and then the corresponding string to compare against the requested symbol names, and if there is a match, we replace the pointer in the section with the replacement. -
The process of looking up the name of a given entry in the lazy or non-lazy pointer tables looks like this:
- Lazy Symbol Pointer Table -> Indirect Symbol Table -> Symbol Table -> String Table(查找的值)
void hookXXXX(const char *libName, const char *funcName,
void *replaceFun, void **origFun) {
void *libHandle = RTLD_DEFAULT;
if (libName) {
libHandle = dlopen(libName, RTLD_NOW);
if (!libHandle) {
libHandle = RTLD_DEFAULT;
}
}
void *pFunc = dlsym(libHandle, funcName);
if (!pFunc) {
NSLog(@"hook failed to find function %s", funcName);
return;
}
uint32_t *pIns =
(uint32_t *)ptrauth_strip(pFunc, ptrauth_key_function_pointer);
NSLog(@"hook %s resolved pFunc -> %p -> %p", funcName, pFunc, pIns);
if (origFun)
*origFun = pFunc;
if (rebind_symbols(
(struct rebinding[1]), 1) <
0) {
NSLog(@"Failed to do fishhook for %s!", funcName);
}
}