任何带特征的检测都是不安全的 & 隐而不发

之前说了下关于反调试的相关, 这次说下反注入相关.

前言

要实现 hook 的首要前提就是能够注入. 但是注入的手段比较多.

1. DYLD_INSERT_LIBRARIES

这种主要是 CydiaSubstrate 使用的注入方式, 首先将自身通过 pthread 远程线程注入到 launchd. 之后通过 hook_posix_spawn_generic 这个函数为启动的 app, 添加 DYLD_INSERT_LIBRARIES env.

2. LC_LOAD_DYLIB

修改 macho 文件结构, 写入依赖库.

__restrict, optool, yololib

3. dlopen

在 lldb 调试过程中很好用.

4. pthread 远程注入

http://bbs.pediy.com/thread-187833.htm

反注入 绕过

先对这些进行 hook, 这些都是存在 反注入的可能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/*
* The following functions allow you to iterate through all loaded images.
* This is not a thread safe operation. Another thread can add or remove
* an image during the iteration.
*
* Many uses of these routines can be replace by a call to dladdr() which
* will return the mach_header and name of an image, given an address in
* the image. dladdr() is thread safe.
*/
extern uint32_t _dyld_image_count(void) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern const struct mach_header* _dyld_get_image_header(uint32_t image_index) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern intptr_t _dyld_get_image_vmaddr_slide(uint32_t image_index) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern const char* _dyld_get_image_name(uint32_t image_index) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
/*
* The following functions allow you to install callbacks which will be called
* by dyld whenever an image is loaded or unloaded. During a call to _dyld_register_func_for_add_image()
* the callback func is called for every existing image. Later, it is called as each new image
* is loaded and bound (but initializers not yet run). The callback registered with
* _dyld_register_func_for_remove_image() is called after any terminators in an image are run
* and before the image is un-memory-mapped.
*/
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
extern void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);

这里使用 frida-script 进行检测.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function zz_monitor_dyld_export_function() {
var export_functions = [
"_dyld_image_count",
"_dyld_get_image_header",
"_dyld_get_image_vmaddr_slide",
"_dyld_get_image_name",
"_dyld_register_func_for_add_image",
"_dyld_register_func_for_remove_image"
];
for (var i = 0; i < export_functions.length; i++) {
console.log(Module.findExportByName("dyld", export_functions[i]));
Interceptor.attach(Module.findExportByName("dyld", export_functions[i]), {
onEnter: function(args) {
console.log('Context information:');
console.log('Context : ' + JSON.stringify(this.context));
console.log('Return : ' + this.returnAddress);
console.log('ThreadId : ' + this.threadId);
console.log('Depth : ' + this.depth);
console.log('Errornr : ' + this.err);
console.log("dyld:" + Object.prototype.toString.call(this));
},
onLeave: function(retval) {}
});
}
return export_functions;
}

附录

frida-core 的模块化封装太好了, 里面很多高端的姿势.

太高端, 简单介绍下.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
What now happens behind the scenes is this:
1. We inject our [launchd.js][] agent into launchd (if not done already).
2. Call the agent's RPC-exported [prepareForLaunch()][] giving it the identifier
of the app we're about to launch.
3. Call [SBSLaunchApplicationWithIdentifierAndLaunchOptions()][] so SpringBoard
launches the app.
4. Our launchd.js agent then intercept launchd's *__posix_spawn()* and adds
[POSIX_SPAWN_START_SUSPENDED][], and [signals back][] the identifier and PID.
This is the */usr/libexec/xpcproxy* helper that will perform an exec()-style
transition to become the app.
5. We then inject our [xpcproxy.js][] agent into this so it can hook
*__posix_spawn()* and add *POSIX_SPAWN_START_SUSPENDED* just like our launchd
agent did. This one will however also have *POSIX_SPAWN_SETEXEC*, so that
means it will replace itself with the app to be launched.
6. We *resume()* the xpcproxy process and [wait for the exec][] to happen and the
process to be suspended.
At this point we let the *device.spawn()* return with the PID of the app that
was just launched. The app's process has been created, and the main thread is
suspended at dyld's entrypoint. frida-trace will then want to attach to it
so it can load its agent that hooks *open*. So it goes ahead and does something
similar to this:

上面简单介绍了流程. 具体在代码中. frida-core/src/darwin/frida-helper-backend-glue.m, 大致流程就是在注入 launchd 利用 hook spawnPOSIX_SPAWN_START_SUSPENDED, 在 __dyld_start 时, 在 dyld::initializeMainExecutable() 写入 load frida-agent 的指令, 进而实现优先加载 dylib.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// frida-core/src/darwin/frida-helper-backend-glue.m
void
_frida_darwin_helper_backend_prepare_spawn_instance_for_injection (FridaDarwinHelperBackend * self, void * opaque_instance, guint task, GError ** error)
{
FridaSpawnInstance * instance = opaque_instance;
FridaHelperContext * ctx = self->context;
const gchar * failed_operation;
kern_return_t ret;
mach_port_t self_task, child_thread;
guint page_size;
thread_act_array_t threads;
guint thread_index;
mach_msg_type_number_t thread_count = 0;
GumDarwinUnifiedThreadState state;
mach_msg_type_number_t state_count = GUM_DARWIN_THREAD_STATE_COUNT;
thread_state_flavor_t state_flavor = GUM_DARWIN_THREAD_STATE_FLAVOR;
GumAddress dyld_start, dyld_granularity, dyld_chunk, dyld_header;
GumAddress probe_address, dlerror_clear_address;
GumDarwinModule * dyld;
FridaExceptionPortSet * previous_ports;
dispatch_source_t source;
/*
* We POSIX_SPAWN_START_SUSPENDED which means that the kernel will create
* the task and its main thread, with the main thread's instruction pointer
* pointed at __dyld_start. At this point neither dyld nor libc have been
* initialized, so we won't be able to inject frida-agent at this point.
*
* So here's what we'll do before we consider spawn() done:
* - Get hold of the main thread to read its instruction pointer, which will
* tell us where dyld is in memory.
* - Walk backwards to find dyld's Mach-O header.
* - Walk its symbols and find a function that's called at a point where the
* process is sufficiently initialized to load frida-agent, but early enough
* so that app's initializer still didn't run. In this case we choose
* dyld::initializeMainExecutable(). At the beginning of this function dyld is
* initialized but libSystem is still missing.
* - Set a hardware breakpoint at the beginning of this function.
* - Swap out the thread's exception ports with our own.
* - Resume the task.
* - Wait until we get a message on our exception port, meaning our breakpoint
* was hit.
* - Hijack thread's instruction pointer to call dlopen("/usr/lib/libSystem.B.dylib")
* and then return back to the beginning of initializeMainExecutable() and restore
* previous thread state.
* - Swap back the thread's orginal exception ports.
* - Clear the hardware breakpoint by restoring the thread's debug registers.
*
* It's actually more complex than that, because:
* - This doesn't work on newer versions of dyld because to call dlopen() it's
* necessary to registerThreadHelpers() first, which is normally done by libSystem
* itself during its initialization.
* - To overcome this catch-22 we alloc a fake LibSystemHelpers object and register
* it (also by hijacking thread's instruction pointer as described above).
* - On older dyld versions, registering helpers before loading libSystem led to
* crashes, so we detect this condition and unset the helpers before calling dlopen(),
* by writing a NULL directly into the global dyld::gLibSystemHelpers because in
* some dyld versions calling registerThreadHelpers(NULL) causes a NULL dereference.
* - At the end of dlopen(), we set the global "libSystemInitialized" flag present in
* the global dyld::qProcessInfo structure, because on newer dyld versions that doesn't
* happen automatically due to the presence of our fake helpers.
* - One of the functions provided by the helper should return a buffer for the errors,
* but since our fake helpers object implements its functions only using a return,
* it will not return any buffer. To avoid this to happen, we set a breakpoint also
* on dyld:dlerrorClear function and inject an immediate return,
* effectively disabling the function.
* - At the end of dlopen() we finally deallocate our fake helpers (because now they've
* been replaced by real libSystem ones) and the string we used as a parameter for dlopen.
*
* Then later when resume() is called:
* - Send a response to the message we got on our exception port, so the
* kernel considers it handled and resumes the main thread for us.
*/