普通视图

发现新文章,点击刷新页面。
昨天以前http://nianxi.net/feed.xml

iOS应用代码段瘦身办法

2016年8月4日 00:00

大型app应对苹果官方代码段大小限制的小伎俩…

背景

苹果官方文档 对二进制 __TEXT 段大小有限制:

代码实在瘦不下去怎么办?



解决方案

利用 rename_section 过审核,在Xcode中向 “Other Linker Flags” 中添加

-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype

漫谈iOS Crash收集框架

2015年6月27日 00:00

为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrashplcrashreporterCrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 CrashlyticsHockeyapp友盟Bugly 等等。

  • 是否集成越多的Crash日志收集服务就越保险?
  • 自己收集的Crash日志和系统生成的Crash日志有分歧,应该相信谁?
  • 为什么有大量Crash日志显示崩在main函数里,但函数栈中却没有一行自己的代码?
  • 野指针类的Crash难定位,有何妙招来应对?

想解释清这些问题,必须从Mach异常说起

Mach异常与Unix信号

iOS系统自带的 Apple’s Crash Reporter 记录在设备中的Crash日志,Exception Type项通常会包含两个元素: Mach异常 和 Unix信号。

Exception Type:         EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype:      KERN_INVALID_ADDRESS at 0x041a6f3

Mach异常是什么?它又是如何与Unix信号建立联系的? Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在 <mach/exception_types.h>下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。

所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API 就是通过 Mach 之上的 BSD 层实现的。

因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。既然最终以信号的方式投递到出错的线程,那么就可以通过注册signalHandler来捕获信号:

signal(SIGSEGV,signalHandler);

捕获Mach异常或者Unix信号都可以抓到crash事件,这两种方式哪个更好呢? 优选Mach异常,因为Mach异常处理会先于Unix信号处理发生,如果Mach异常的handler让程序exit了,那么Unix信号就永远不会到达这个进程了。转换Unix信号是为了兼容更为流行的POSIX标准(SUS规范),这样不必了解Mach内核也可以通过Unix信号的方式来兼容开发。

因为硬件产生的信号(通过CPU陷阱)被Mach层捕获,然后才转换为对应的Unix信号;苹果为了统一机制,于是操作系统和用户产生的信号(通过调用killpthread_kill)也首先沉下来被转换为Mach异常,再转换为Unix信号。

Crash收集的实现思路

正如上述所说,可以通过捕获Mach异常、或Unix信号两种方式来抓取crash事件,于是总结起来实现方案就一共有3种。

1)Mach异常方式

2)Unix信号方式

signal(SIGSEGV,signalHandler);

3)Mach异常+Unix信号方式

Github上多数开源项目都采用的这种方式,即使在优选捕获Mach异常的情况下,也放弃捕获EXC_CRASH异常,而选择捕获与之对应的SIGABRT信号。著名开源项目plcrashreporter在代码注释中给出了详细的解释:

We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for EXC_CRASH.

另外,需要重点说明的是:对于应用级异常NSException,还需要特殊处理。 你是否见过崩溃在main函数的crash日志,但是函数栈里面没有你的代码:

Thread 0 Crashed:
0       libsystem_kernel.dylib          0x3a61757c   __semwait_signal_nocancel + 0x18
1       libsystem_c.dylib               0x3a592a7c   nanosleep$NOCANCEL + 0xa0
2       libsystem_c.dylib               0x3a5adede   usleep$NOCANCEL + 0x2e
3       libsystem_c.dylib               0x3a5c7fe0   abort + 0x50
4       libc++abi.dylib                 0x398f6cd2   abort_message + 0x46
5       libc++abi.dylib                 0x3990f6e0   default_terminate_handler() + 0xf8
6       libobjc.A.dylib                 0x3a054f62   _objc_terminate() + 0xbe
7       libc++abi.dylib                 0x3990d1c4   std::__terminate(void (*)()) + 0x4c
8       libc++abi.dylib                 0x3990cd28   __cxa_rethrow + 0x60
9       libobjc.A.dylib                 0x3a054e12   objc_exception_rethrow + 0x26
10      CoreFoundation                  0x2f7d7f30   CFRunLoopRunSpecific + 0x27c
11      CoreFoundation                  0x2f7d7c9e   CFRunLoopRunInMode + 0x66
12      GraphicsServices                0x346dd65e   GSEventRunModal + 0x86
13      UIKit                           0x32124148   UIApplicationMain + 0x46c
14      XXXXXX                          0x0003b1f2   main + 0x1f2
15      libdyld.dylib                   0x3a561ab4   start + 0x0

可以看出是因为某个NSException导致程序Crash的,只有拿到这个NSException,获取它的reasonnamecallStackSymbols信息才能确定出问题的程序位置。

/* NSException Class Reference */
@property(readonly, copy) NSString *name;
@property(readonly, copy) NSString *reason;
@property(readonly, copy) NSArray *callStackSymbols;
@property(readonly, copy) NSArray *callStackReturnAddresses;

方法很简单,可通过注册NSUncaughtExceptionHandler捕获异常信息:

static void my_uncaught_exception_handler (NSException *exception) {
    //这里可以取到 NSException 信息
}
NSSetUncaughtExceptionHandler(&my_uncaught_exception_handler);

将拿到的NSException细节写入Crash日志,精准的定位出错程序位置:

Application Specific Information:
*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<__NSDictionaryI 0x14554d00> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key key.'

Last Exception Backtrace:
0 CoreFoundation 0x2f8a3f7e     __exceptionPreprocess + 0x7e
1 libobjc.A.dylib 0x3a054cc     objc_exception_throw + 0x22
2 CoreFoundation 0x2f8a3c94     -[NSException raise] + 0x4
3 Foundation 0x301e8f1e         -[NSObject(NSKeyValueCoding) setValue:forKey:] + 0xc6
4 DemoCrash 0x00085306          -[ViewController crashMethod] + 0x6e
5 DemoCrash 0x00084ecc          main + 0x1cc
6 DemoCrash 0x00084cf8          start + 0x24

那么,是不是收到了大量crash在main函数却没有NSException信息的日志,就代表自己集成的Crash日志收集服务没有注册NSUncaughtExceptionHandler呢?不一定,还有另外一种可能,就是被同时存在的其他Crash日志收集服务给坑了。

多个Crash日志收集服务共存的坑

是的,在自己的程序里集成多个Crash日志收集服务实在不是明智之举。通常情况下,第三方功能性SDK都会集成一个Crash收集服务,以及时发现自己SDK的问题。当各家的服务都以保证自己的Crash统计正确完整为目的时,难免出现时序手脚,强行覆盖等等的恶意竞争,总会有人默默被坑。

1)拒绝传递 UncaughtExceptionHandler

如果同时有多方通过NSSetUncaughtExceptionHandler注册异常处理程序,和平的作法是:后注册者通过NSGetUncaughtExceptionHandler将先前别人注册的handler取出并备份,在自己handler处理完后自觉把别人的handler注册回去,规规矩矩的传递。不传递强行覆盖的后果是,在其之前注册过的日志收集服务写出的Crash日志就会因为取不到NSException而丢失Last Exception Backtrace等信息。(P.S. iOS系统自带的Crash Reporter不受影响)

在开发测试阶段,可以利用 fishhook 框架去hookNSSetUncaughtExceptionHandler方法,这样就可以清晰的看到handler的传递流程断在哪里,快速定位污染环境者。不推荐利用调试器添加符号断点来检查,原因是一些Crash收集框架在调试状态下是不工作的。

检测代码示例:

static NSUncaughtExceptionHandler *g_vaildUncaughtExceptionHandler;
static void (*ori_NSSetUncaughtExceptionHandler)( NSUncaughtExceptionHandler * );

void my_NSSetUncaughtExceptionHandler( NSUncaughtExceptionHandler * handler)
{
    g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    if (g_vaildUncaughtExceptionHandler != NULL) {
        NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler);
    }

    ori_NSSetUncaughtExceptionHandler(handler);
    NSLog(@"%@",[NSThread callStackSymbols]);

    g_vaildUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    NSLog(@"UncaughtExceptionHandler=%p",g_vaildUncaughtExceptionHandler);
}

对于越狱插件注入应用进程内部,恶意覆盖NSSetUncaughtExceptionHandler的情况,应用程序本身处理起来比较弱势,因为越狱环境下操作时序的玩法比较多权利比较大。

2)Mach异常端口换出+信号处理Handler覆盖

和NSSetUncaughtExceptionHandler的情况类似,设置过的Mach异常端口和信号处理程序也有可能被干掉,导致无法捕获Crash事件。

3)影响系统崩溃日志准确性

应用层参与收集Crash日志的服务方越多,越有可能影响iOS系统自带的Crash Reporter。由于进程内线程数组的变动,可能会导致系统日志中线程的Crashed 标签标记错位,可以搜索abort()等关键字来复查系统日志的准确性。 若程序因NSException而Crash,系统日志中的Last Exception Backtrace信息是完整准确的,不会受应用层的胡来而影响,可作为排查问题的参考线索。

ObjC野指针类的Crash

收集Crash日志这个步骤没有问题的情况下,还是有很多全系统栈的日志的情况,没有自己一行代码,分析起来十分棘手,ObjC野指针类的Crash正是如此,这里推荐几篇好文章:

除此之外,在Crash日志中补充记录一些额外信息可以辅助定位,如切面标记线程出处、队列出处,记录用户操作轨迹等等……



感谢阅读


版权申明:本文在微信公众平台的发表权,已「独家代理」给 iOS 开发(iOSDevTips)微信公共帐号。

App Extension的脱壳办法

2015年2月26日 00:00

从app store下载的app和app extension是加过密的,可以通过otool查看:

$ otool -l binary_name | grep crypt

cryptoff  16384
cryptsize 294912
cryptid   1

iPhone applications的解密办法

dumpdecrypted 是个出色的app脱壳开源工具,它的原理是:将应用程序运行起来(iOS系统会先解密程序再启动),然后将内存中的解密结果dump写入文件中,得到一个新的可执行程序。

iPhone app extensions的特别之处

  • app extension虽是独立进程,但不可独立运行
  • app extension的进程中,写权限被严格控制

基于以上两点,dumpdecrypted 无法实现对iPhone app extensions的脱壳。

iPhone app extensions的解密办法

通过对dumpdecrypted稍作修改,更改其写入dump结果的path,变通启动方式就可实现对app extension的解密,详见:Carina’s dumpdecrypted

使用方法很简单,用微信的Share Extension为例

1、本地编译好 dumpdecrypted.dylib
2、指定作用的Extension Bundle

{
	Filter = {
		Bundles = ("com.tencent.xin.sharetimeline");
	};
}

3、将 dumpdecrypted.plistdumpdecrypted.dylib 拷贝至越狱机的 /Library/MobileSubstrate/DynamicLibraries/
4、利用系统相册启动微信的Share Extension

当微信的Share Extension被启动时,解密插件自动工作。 值得注意的是,如果你的越狱机是armv7架构,那么也就只dump armv7那部分; 如果越狱机是arm64架构,那么也就只dump arm64那部分。So,最后你需要:

$ lipo -thin armv7 xxx.decrypted -output xxx_armv7.decrypted
$ lipo -thin armv64 xxx.decrypted -output xxx_arm64.decrypted

来得到干净的dump结果

iOS字符串安全

2014年9月22日 00:00

一个编译成功的可执行程序,其中已初始化的字符串都是完整可见的。 针对于iOS的Mach-O二进制通常可获得以下几种字符串信息:

  • 资源文件名
  • 可见的函数符号名
  • SQL语句
  • format
  • 通知名
  • 对称加密算法的key

攻击者如何利用字符串

资源文件名通常用来快速定位逆向分析的入口点。 想要知道判断购买金币成功与否的代码位置?只要确定购买成功时播放的音频文件名字或者背景图名字就可以顺藤摸瓜了。

kLoginSuccessNotification类似这种通知名称格外炸眼,利用Cycript发个此通知试试,也许会有什么意外收获。 拿到对称加密算法的key是件很幸福的事情。

字符串异或加解密

是的,字符串需要加密处理,但只需要对高度敏感字符数据做加密,比如对称加密算法的key。 其他的,需要提高编程安全意识来弥补。

常规办法是通过异或来加解密,来写个sample code:

static unsigned char XOR_KEY = 0xBB;

void xorString(unsigned char *str, unsigned char key)
{
    unsigned char *p = str;
    while( ((*p) ^=  key) != '\0')  p++;
}

- (void)testFunction
{
    unsigned char str[] = {(XOR_KEY ^ 'h'),
                           (XOR_KEY ^ 'e'),
                           (XOR_KEY ^ 'l'),
                           (XOR_KEY ^ 'l'),
                           (XOR_KEY ^ 'o'),
                           (XOR_KEY ^ '\0')};
    xorString(str, XOR_KEY);
    static unsigned char result[6];
    memcpy(result, str, 6);
    NSLog(@"%s",result);      //output: hello
}

这样就无法从二进制中直接分析得到字符串“hello”了 扔进IDA里分析一下,发现效果不好,连续存储的’h’、’e’等字符还是暴露了可读的”hello”

改进一下,取消XOR_KEY独立变量的身份,改为宏,作为数据直接插入

#define XOR_KEY 0xBB

void xorString(unsigned char *str, unsigned char key)
{
    unsigned char *p = str;
    while( ((*p) ^=  key) != '\0')  p++;
}

- (void)testFunction
{
    unsigned char str[] = {(XOR_KEY ^ 'h'),
                           (XOR_KEY ^ 'e'),
                           (XOR_KEY ^ 'l'),
                           (XOR_KEY ^ 'l'),
                           (XOR_KEY ^ 'o'),
                           (XOR_KEY ^ '\0')};
    xorString(str, XOR_KEY);
    static unsigned char result[6];
    memcpy(result, str, 6);
    NSLog(@"%s",result);      //output: hello
}

嗯这下好多了

iOS应用安全防护框架概述

2014年9月8日 00:00

攻易防难,唯有缜密、多层的防护网络才能可靠的保护我们iOS应用程序的安全。那么,一个完善的iOS应用安全防护框架都要写哪些东西呢? 首先,先梳理一下常见的逆向及攻击工具。

iOS应用逆向常用工具

裸奔app的安全隐患

一部越狱的iOS设备,外加上述的逆向工具,给裸奔的iOS应用程序带来哪些威胁呢?

  • 任意读写文件系统数据
  • HTTP(S)实时被监测
  • 重新打包ipa
  • 暴露的函数符号
  • 未加密的静态字符
  • 篡改程序逻辑控制流
  • 拦截系统框架API
  • 逆向加密逻辑
  • 跟踪函数调用过程(objc_msgSend)
  • 可见视图的具体实现
  • 伪造设备标识
  • 可用的URL schemes
  • runtime任意方法调用
  • ……

iOS应用安全防护开源工具

  • ios-class-guard 是对抗class-dump的利器,作用是将ObjC类名方法名等重命名为难以理解的字符。

iOS应用安全防护框架概述

针对上述安全隐患,我们的iOS应用安全防护框架需实现的任务大致如下:

  • 防护
    • ObjC类名方法名等重命名为难以理解的字符
    • 加密静态字符串运行时解密
    • 混淆代码使其难于反汇编
    • 本地存储文件防篡改
  • 检测
    • 调试状态检测
    • 越狱环境检测
    • ObjC的Swizzle检测
    • 任意函数的hook检测
    • 指定区域或数据段的校验和检测
  • 自修复
    • 自修复被篡改的数据和代码段

此外,还需要多层的防护,通过高层保护低层的方式来保证整个防护机制不失效。 参考IBM移动终端安全防护框架解决方案:

IBM移动终端安全防护框架解决方案

Objective-C的“多继承”——消息转发

2013年5月24日 00:00

本文最初发表于 2013/5/24 17:10:43 ,现从老博客节选搬迁至此,当个笔记

当单继承不够用,很难为问题域建模时,我们通常都会直接想到多继承。多继承是从多余一个直接基类派生类的能力,可以更加直接地为应用程序建模。但是Objective-C不支持多继承,由于消息机制名字查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题。

不过其实 Objective-C 也无需支持多继承,我们可以找到如下几种间接实现多继承目的的方法:

  • 消息转发
  • delegate和protocol
  • 类别

消息转发

当向someObject发送某消息,但runtime system在当前类和父类中都找不到对应方法的实现时,runtime system并不会立即报错使程序崩溃,而是依次执行下列步骤:

分别简述一下流程:

1.动态方法解析 向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法。(迷茫请搜索:@dynamic)

2.快速消息转发 检查该类是否实现了 forwardingTargetForSelector: 方法,若实现了则调用这个方法。若该方法返回值对象非nil或非self,则向该返回对象重新发送消息。

3.标准消息转发 runtime发送methodSignatureForSelector:消息获取Selector对应的方法签名。返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:消息,程序崩溃退出。

顾名思义,我们可以利用上述过程中的2、3两种方式来完成消息转发。

快速消息转发

快速消息转发的实现方法很简单,只需要重写

- (id)forwardingTargetForSelector:(SEL)aSelector

举个栗子

//Teacher类需要实现将消息转发给Doctor:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    Doctor *doctor = [[Doctor alloc]init];
	if ([doctor respondsToSelector:aSelector]) {
		return doctor;
	}
	return nil;
}

标准消息转发

标准消息转发需要重写

methodSignatureForSelector:
forwardInvocation:

举个栗子

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
    if (signature==nil) {
        signature = [someObj methodSignatureForSelector:aSelector];
    }
    NSUInteger argCount = [signature numberOfArguments];
    for (NSInteger i=0 ; i<argCount ; i++) {
    }

    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL seletor = [anInvocation selector];
    if ([someObj respondsToSelector:seletor]) {
        [anInvocation invokeWithTarget:someObj];
    }

}

两种消息转发方式的比较

  • 快速消息转发:简单、快速、但仅能转发给一个对象
  • 标准消息转发:稍复杂、较慢、但转发操作实现可控,可以实现多对象转发


❌
❌