[iOS调试进阶]让lldb在遇到EXC_BAD_ACCESS之后能继续执行进入signal_handler

[iOS调试进阶]让lldb在遇到EXC_BAD_ACCESS之后能继续执行进入signal_handler

0x0 引子

在之前的调试过程中,经常想在遇到EXC_BAD_ACCESS之后能够继续执行到自己注册的signal_handler来做一些操作,让应用程序可以从崩溃中恢复到正常状况,但始终都没有找到合适的方法。最近在做某项目的某个需求使得这个问题成为一个不得不绕过的一个巨大障碍,那还是想想办法解决吧。


0x1 示例代码

示例代码如下:

#import "ViewController.h"
#include <sys/types.h>
#include <sys/sysctl.h>

void sig_handler(int sig, siginfo_t *info, ucontext_t *ucontext)
{
    NSLog(@"signal caught 0: %d, pc 0x%llx\n", sig, ucontext->uc_mcontext->__ss.__pc);
    ucontext->uc_mcontext->__ss.__pc = ucontext->uc_mcontext->__ss.__lr;
}

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_flags = SA_SIGINFO;
    sa.sa_sigaction = sig_handler;
    
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGABRT, &sa, NULL);
    sigaction(SIGKILL, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    
    [NSThread detachNewThreadSelector:@selector(entry) toTarget:self withObject:nil];
}

// 假装在webview线程
- (void)entry {
    void *a = calloc(1, sizeof(void *));
    ((void(*)())a)();
    dispatch_async(dispatch_get_main_queue(), ^{
        UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"hehe" message:@"revive!" preferredStyle:UIAlertControllerStyleAlert];
        [self presentViewController:alert animated:YES completion:nil];
    });
}

@end

效果是在`entry`方法里的这句` ((void(*)())a)();`会挂掉,就像这样:

然后之前设定的sig_handler就不会执行进去就没法复活了。

断开调试器,直接在手机上点击应用,就会发现可以弹出`revive!`对话框,成功复活。


0x2 为什么不能复活

我们在调试手机上的应用时,并不是mac上的lldb直接加载上了手机中的应用,而是通过手机上的debugserver中转一次加载的,而debugserver可以接收到内核抛出的异常,在接到异常后直接捕获住而并没有透明地传给lldb,就导致我们mac上的lldb什么都干不了了。


那有没有办法让debugserver不捕获EXC_BAD_ACCESS异常呢?曲折之路开始了,首先是stackoverflow上的方法,提到可以修改debugserver的代码:

把tools/debugserver/source/MacOSX/MachTask.mm 文件的

err = ::task_set_exception_ports (task, m_exc_port_info.mask, m_exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

改成

err = ::task_set_exception_ports (task, m_exc_port_info.mask & ~EXC_MASK_BAD_ACCESS, m_exception_port, EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

好说,立马从github下来lldb代码,改debugserver代码,编译通过。

但是,怎么搞到手机上去?没有越狱当然是没有权限的,那有没有办法替换系统原有的debugserver?

原理是这样的,手机连接xcode后,会找到对应手机版本的DeveloperDiskImage.dmg文件,上传到手机然后mount(参见github的ios-deploy项目)。debugserver就在这个镜像里面,那我们改改镜像好了:

看到`DeveloperDiskImage.dmg.signature`就死心一半了,有签名..


那如果绕过签名改debugserver呢?我又看到这个:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>com.apple.backboardd.debugapplications</key>
	<true/>
	<key>com.apple.backboardd.launchapplications</key>
	<true/>
	<key>com.apple.diagnosticd.diagnostic</key>
	<true/>
	<key>com.apple.frontboard.debugapplications</key>
	<true/>
	<key>com.apple.frontboard.launchapplications</key>
	<true/>
	<key>com.apple.security.network.client</key>
	<true/>
	<key>com.apple.security.network.server</key>
	<true/>
	<key>com.apple.springboard.debugapplications</key>
	<true/>
	<key>run-unsigned-code</key>
	<true/>
	<key>seatbelt-profiles</key>
	<array>
		<string>debugserver</string>
	</array>
</dict>
</plist>

debugserver的entitlement里面有这么多权限,更吊的是,居然可以`run-unsigned-code`!!!

但是遗憾的是我们自己的mobileprovision的权限少的可怜:

        <key>Entitlements</key>
        <dict>
                <key>keychain-access-groups</key>
                <array>
                        <string>隐藏.*</string>
                </array>
                <key>inter-app-audio</key>
                <true/>
                <key>get-task-allow</key>
                <true/>
                <key>application-identifier</key>
                <string>隐藏.*</string>
                <key>com.apple.developer.team-identifier</key>
                <string>隐藏</string>
        </dict>

好吧,如此就算我们用自己的签名打出了debugserver也没有debug其他应用的权限。


还有更绝望的,debugserver本身的entitlement里没有`get-task-allow`也就是它不能被调试,就没法通过调试手段改其数据了!


0x3 路

既然绕不过了,那看看debugserver是怎么附加程序并且捕获异常的吧,看看能否有新的突破:

  task_t task = TaskPortForProcessID(err);

  ... 省略 ...

  err = ::task_set_exception_ports(
        task, m_exc_port_info.mask, m_exception_port,
        EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES, THREAD_STATE_NONE);

找到前面提到的修改debugserver代码的方案的地方,发现这里是在设定task的exception ports而task来自`TaskPortForProcessID`,一看就是取被调试的进程的task然后修改其task port嘛,是不是意味着应用本可以自己设定自己的exception ports把调试器的给覆盖掉?翻了下内核代码,发现确实可以被覆盖!动手在应用加代码:

#include <mach/task.h>
#include <mach/mach_init.h>
#include <mach/mach_port.h>

int ret = task_set_exception_ports(
                                   mach_task_self(),
                                   EXC_MASK_BAD_ACCESS,
                                   MACH_PORT_NULL,//m_exception_port,
                                   EXCEPTION_DEFAULT,
                                   0);

然后发现EXC_BAD_ACCESS就没有了,变成了:

SIGBUS! 信号!也就可以捕获了?发现lldb还是走不动,好说,lldb提前执行:

(lldb) pro handle SIGBUS -s false
(lldb) pro handle SIGBUS
NAME         PASS   STOP   NOTIFY
===========  =====  =====  ======
SIGBUS       true   false   true 

用`-s`把SIGBUS的STOP改成false,这样lldb就不会停了:

目标达成!绕了一大圈,结局如此简单!


0x4 参考资料

- How to make lldb ignore EXC_BAD_ACCESS exception?: How to make lldb ignore EXC_BAD_ACCESS exception?

- ios-deploy: phonegap/ios-deploy

- lldb: llvm-mirror/lldb


0x5 招人

iOS Android 算法 欢迎扔简历 zhibing.lwh@alibaba-inc.com

编辑于 2018-02-02 20:53