敏感逻辑的保护方案

24 敏感逻辑的保护方案

Objective-C 代码容易被 hook,暴露信息太赤裸裸,为了安全,改用 C 来写吧!

图片 24.1 sensitive1

当然不是全部代码都要 C 来写,我指的是敏感业务逻辑代码。

本文就介绍一种低学习成本的,简易的,Objective-C 逻辑代码重写为 C 代码的办法。

也许,程序中存在一个类似这样的类:

1
2
3
4
5
6
7
@interface XXUtil : NSObject

+ (BOOL)isVerified;
+ (BOOL)isNeedSomething;
+ (void)resetPassword:(NSString*)password;

@end

被 class-dump 出来后,利用 Cycript 很容易实现攻击,容易被 hook ,存在很大的安全隐患。

图片 24.2 sensitive2

想改,但是不想大改程序结构,肿么办呢?

把函数名隐藏在结构体里,以函数指针成员的形式存储。 这样做的好处是,编译后,只留了下地址,去掉了名字和参数表,提高了逆向成本和攻击门槛。

改写的程序如下:

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
//XXUtil.h  
 #import <Foundation/Foundation.h>

typedef struct _util {
    BOOL (*isVerified)(void);
    BOOL (*isNeedSomething)(void);
    void (*resetPassword)(NSString *password);
}XXUtil_t ;

 #define XXUtil ([_XXUtil sharedUtil])

@interface _XXUtil : NSObject

+ (XXUtil_t *)sharedUtil;
@end


//XXUtil.m  
 #import "XXUtil.h"

static BOOL _isVerified(void)
{
    //bala bala ...  
    return YES;
}

static BOOL _isNeedSomething(void)
{
    //bala bala ...  
    return YES;
}

static void _resetPassword(NSString *password)
{
    //bala bala ...  
}

static XXUtil_t * util = NULL;
@implementation _XXUtil

+(XXUtil_t *)sharedUtil
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        util = malloc(sizeof(XXUtil_t));
        util->isVerified = _isVerified;
        util->isNeedSomething = _isNeedSomething;
        util->resetPassword = _resetPassword;
    });
    return util;
}

+ (void)destroy
{
    util ? free(util): 0;
    util = NULL;
}
@end

最后,根据 Xcode 的报错指引,把以前这样的调用 [XXUtil isVerified];

对应改成: XXUtil->isVerified();

就可以了。

是的,绝不费一点脑子。

Objective-C 代码混淆

23 Objective-C 代码混淆

class-dump 可以很方便的导出程序头文件,不仅让攻击者了解了程序结构方便逆向,还让着急赶进度时写出的欠完善的程序给同行留下笑柄。 所以,我们迫切的希望混淆自己的代码。

混淆的常规思路

混淆分许多思路,比如:

1)花代码花指令,即随意往程序中加入迷惑人的代码指令

2)易读字符替换 等等

图片 23.1 confusion1

防止 class-dump 出可读信息的有效办法是易读字符替换。

Objective-C 的方法名混淆

混淆的时机

我们希望在开发时一直保留清晰可读的程序代码,方便自己。 同时,希望编译出来的二进制包含乱七八糟的混淆后的程序代码,恶心他人。

因此,我们可以在 Build Phrase 中设定在编译之前进行方法名的字符串替换。

混淆的方法

方法名混淆其实就是字符串替换,有 2 个方法可以,一个是 #define,一个是利用 tops。

利用 #define 的方法有一个好处,就是可以把混淆结果合并在一个 .h 中,在工程 Prefix.pch 的最前面 #import 这个 .h 。不导入也可以编译、导入则实现混淆。

单段的 selector ,如 func: ,可以通过 #define func 来实现字符串替换。

多段的 selector,如 a:b:c: ,可以通过分别 #define a 、b、c 来实现字符串替换。

我的混淆工具

我写了个简易的混淆脚本,主要思路是把敏感方法名集中写在一个名叫 func.list 的文件中,逐一 #define 成随机字符,追加写入 .h 。

脚本如下:

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
#!/usr/bin/env bash  

TABLENAME=symbols  
SYMBOL_DB_FILE="symbols"  
STRING_SYMBOL_FILE="func.list"  
HEAD_FILE="$PROJECT_DIR/$PROJECT_NAME/codeObfuscation.h"  
export LC_CTYPE=C  

#维护数据库方便日后作排重  
createTable()  
{  
   echo "create table $TABLENAME(src text, des text);" | sqlite3 $SYMBOL_DB_FILE  
}  

insertValue()  
{  
   echo "insert into $TABLENAME values('$1' ,'$2');" | sqlite3 $SYMBOL_DB_FILE  
}  

query()  
{  
   echo "select * from $TABLENAME where src='$1';" | sqlite3 $SYMBOL_DB_FILE  
}  

ramdomString()  
{  
   openssl rand -base64 64 | tr -cd 'a-zA-Z' |head -c 16  
}  

rm -f $SYMBOL_DB_FILE  
rm -f $HEAD_FILE  
createTable  

touch $HEAD_FILE  
echo '#ifndef Demo_codeObfuscation_h  
#define Demo_codeObfuscation_h' >> $HEAD_FILE  
echo "//confuse string at `date`" >> $HEAD_FILE  
cat "$STRING_SYMBOL_FILE" | while read -ra line; do  
   if [[ ! -z "$line" ]]; then  
       ramdom=`ramdomString`  
       echo $line $ramdom  
       insertValue $line $ramdom  
       echo "#define $line $ramdom" >> $HEAD_FILE  
   fi  
done  
echo "#endif" >> $HEAD_FILE  


sqlite3 $SYMBOL_DB_FILE .dump

操作步骤

1.将混淆脚本 confuse.sh 放到工程目录下 mv confuse.sh your_proj_path/

2.修改 Prefix.pch

打开 Xcode,修改 XXX-Prefix.ch ,添加混淆头文件:

1
2
3
4
5
6
#ifdef __OBJC__  
   #import <UIKit/UIKit.h>
   #import <Foundation/Foundation.h>
   //添加混淆作用的头文件(这个文件名是脚本confuse.sh中定义的)  
   #import "codeObfuscation.h"
#endif

3.配置 Build Phase

在工程 Build Phase 中添加执行脚本操作,执行 confuse.sh 脚本,如图:

图片 23.2 confusion3

4.创建函数名列表 func.list ,写入待混淆的函数名,如:

1
2
-(void)sample;
-(void)seg1:(NSString *)string seg2:(NSUInteger)num;

就这样写:

sample
seg1
seg2

并将文件放置于与 confuse.sh 脚本同级
mv func.list your_proj_path/

5.编译查看结果

直接 build,混淆脚本会在编译前运行,进行字符随机替换,并且每次 build 的随机字符不同,如图:

图片 23.3 confusion4

Static 和被裁的符号表

22 static 和被裁的符号表

为了不让攻击者理清自己程序的敏感业务逻辑,于是我们想方设法提高逆向门槛。

本文就介绍一个防御技巧——利用 static 关键字裁掉函数符号。

原理

如果函数属性为 static ,那么编译时该函数符号就会被解析为 local 符号。

在发布 release 程序时(用 Xcode 打包编译二进制)默认会 strip 裁掉这些函数符号,无疑给逆向者加大了工作难度。

验证

写个 demo 验证一下上述理论,以一段创建 Button 的代码为例,对应补充一个 static 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
id createBtn()
{
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectZero];
    [btn setFrame:CGRectMake(200, 100, 100, 100)];
    [btn setBackgroundColor:[UIColor redColor]];
    btn.layer.cornerRadius = 7.0f;
    btn.layer.masksToBounds = YES;
    return btn;
}

static id static_createBtn()
{
    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectZero];
    [btn setFrame:CGRectMake(50, 100, 100, 100)];
    [btn setBackgroundColor:[UIColor blueColor]];
    btn.layer.cornerRadius = 7.0f;
    btn.layer.masksToBounds = YES;
    return btn;
}

再来看一下反编的结果,对于 createBtn() 方法,我们可以得到它的伪代码:

图片 22.1 static

函数名虽然面目全非,但是基本操作还是清晰的。

对于static_createBtn() 方法呢,我们已经无法看到它任何直观的有价值信息了。

局限

当然这种方法也有局限性。正如你所知道的,static 函数,只在本文件可见。

打破局限

怎么让别的文件也能调到本文件的 static 方法呢?

在本文件建造一个结构体,结构体里包含函数指针。把 static 函数的函数指针都赋在这个结构体里,再把这个结构体抛出去。

这样做的好处是,既隐藏了函数代码也丰富了调用方式。

废除应用程序的 ASLR特性

21 废除应用程序的 ASLR 特性

ASLR (Address Space Layout Randomization),即地址空间随机布局。大部分主流的操作系统都已实现了 ASLR,以防范对已知地址进行恶意攻击。iOS 从 4.3 开始支持 ASLR,Android 从 4.0 也支持了 ASLR 机制。

ASLR 的存在,给 iOS 系统越狱造成了很大的困难,某些不完美越狱方案就是因为攻破不了或者绕不开 ASLR ,所以每次重新启动后地址再度随机偏移,需要重新进行越狱操作。与此同时,ASLR 也给应用层攻击带来了一些困难,不同进程会造成不同的地址空间偏移,而且在运行时才可确定其偏移量,不易锁定攻击地址。

Mach-O 文件的文件头会记录二进制的属性标识,有个 flag 叫做 PIE (Position Independent Enable)。开启了 PIE 的二进制文件,在执行时会产生 ASLR 。

我们可以使用 otool 工具,来查看任意应用程序二进制文件的属性,以支付宝为例:
otool -hv Portal

图片 21.1 aslr

有 PIE 标识,表示该程序在启动时会产生随机地址布局。

图片 21.2 aslr1

removePIE 是个去掉 PIE flag 的工具。

坏消息是,年久失修,它不支持 iOS7 。 好消息是,我们还有 2 个变通方法可以走。
- 利用 Theos 编译 removePIE
- 改编一个 Mac 版的 MyRemovePIE

非越狱开发者可能不熟悉 Theos ,低学习成本的做法是第二种,那么让我们来改编一个 Mac 版的 MyRemovePIE 吧。 (懒得动手的可以直接到这里下载 demo )

创建一个 Command Line Tool 工程,

图片 21.3 aslr2

然后复制 removePIE.c 代码到 main.c 中,并且修改第 43 行: if(currentHeader.magic == MH_MAGIC){ //little endian

添加 iOS7 的判断条件: if(currentHeader.magic == MH_MAGIC || currentHeader.magic == 0xbebafeca ){ //little endian

编译后生成可执行文件 MyRemovePIE .

利用我们编译生成的 MyRemovePIE 来处理应用程序:

./MyRemovePIE Portal

图片 21.4 aslr3

这样以后支付宝 Portal 再被启动执行就不会具有 ASLR 特性了

图片 21.5 aslr4

如何验证一下结果呢?

把处理过的 Portal 二进制拷贝回 iPhone ,启动支付宝钱包应用,然后 gdb 该进程,利用 info sh 命令查看偏移:

图片 21.6 aslr5

偏移量为 0 ,嗯,这下就好了。一些手动处理的过程可以升级为自动了~

图片 21.7 aslr6

越狱检测的攻与防

20 越狱检测的攻与防

在应用开发过程中,我们希望知道设备是否越狱,正以什么权限运行程序,好对应采取一些防御和安全提示措施。

iOS7 相比之前版本的系统而言,升级了沙盒机制,封锁了几乎全部应用沙盒可以共享数据的入口。即使在越狱情况下,限制也非常多,大大增加了应用层攻击难度。比如,在 iOS7 之前,我们可以尝试往沙盒外写文件判断是否越狱,但 iOS7 越狱后也无该权限,还使用老方法检测会导致误判。

那么,到底应该如何检测越狱呢?攻击者又会如果攻破检测呢?本文就着重讨论一下越狱检测的攻与防。

图片 20.1 gongfang

首先,你可以尝试使用 NSFileManager 判断设备是否安装了如下越狱常用工具:

/Applications/Cydia.app /Library/MobileSubstrate/MobileSubstrate.dylib /bin/bash /usr/sbin/sshd /etc/apt

但是不要写成 BOOL 开关方法,给攻击者直接锁定目标 hook 绕过的机会

1
2
3
4
5
6
+(BOOL)isJailbroken{
    if ([[NSFileManager defaultManager] fileExistsAtPath:@"/Applications/Cydia.app"]){
        return YES;
    }
    // ...  
}

攻击者可能会改变这些工具的安装路径,躲过你的判断。

那么,你可以尝试打开 cydia 应用注册的 URL scheme:

1
2
3
if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"cydia://package/com.example.package"]]){
     NSLog(@"Device is jailbroken");
}

但是不是所有的工具都会注册 URL scheme,而且攻击者可以修改任何应用的 URL scheme

那么,你可以尝试读取下应用列表,看看有无权限获取:

1
2
3
4
5
6
if ([[NSFileManager defaultManager] fileExistsAtPath:@"/User/Applications/"]){
        NSLog(@"Device is jailbroken");
        NSArray* applist = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:@"/User/Applications/"
                                                                               error:nil];
        NSLog(@"applist = %@",applist);
}

越了狱的设备是可以获取到的:

图片 20.2 gongfang2

攻击者可能会 hook NSFileManager 的方法,让你的想法不能如愿。

那么,你可以回避 NSFileManager,使用 stat 系列函数检测 Cydia 等工具:

1
2
3
4
5
6
7
8
9
#import <sys/stat.h>  

void checkCydia(void)
{
   struct stat stat_info;
   if (0 == stat("/Applications/Cydia.app", &stat_info)) {
       NSLog(@"Device is jailbroken");
   }
}

攻击者可能会利用 Fishhook 原理 hook 了 stat 。

那么,你可以看看 stat 是不是出自系统库,有没有被攻击者换掉:

1
2
3
4
5
6
7
8
9
10
11
#import <dlfcn.h>  

void checkInject(void)
{
    int ret ;
    Dl_info dylib_info;
    int (* func_stat)(const charchar*, struct stat* ) = stat;
    if ((ret = dladdr(func_stat, &dylib_info))) {
        NSLog(@"lib :%s", dylib_info.dli_fname);
    }
}

如果结果不是 /usr/lib/system/libsystem_kernel.dylib 的话,那就 100% 被攻击了。 如果 libsystem_kernel.dylib 都是被攻击者替换掉的…… 那也没什么可防的大哥你随便吧……

那么,你可能会想,我该检索一下自己的应用程序是否被链接了异常动态库。 列出所有已链接的动态库:

1
2
3
4
5
6
7
8
9
10
#import <mach-o/dyld.h>  

void checkDylibs(void)
{
    uint32_t count = _dyld_image_count();
    for (uint32_t i = 0 ; i < count; ++i) {
        NSString* name = [[NSString alloc]initWithUTF8String:_dyld_get_image_name(i)];
        NSLog(@"--%@", name);
    }
}

通常情况下,会包含越狱机的输出结果会包含字符串:Library/MobileSubstrate/MobileSubstrate.dylib

攻击者可能会给 MobileSubstrate 改名,但是原理都是通过 DYLD_INSERT_LIBRARIES注入动态库。

那么,你可以通过检测当前程序运行的环境变量:

1
2
3
4
5
void printEnv(void)
{
    char* env = getenv("DYLD_INSERT_LIBRARIES");
    NSLog(@"%s", env);
}

未越狱设备返回结果是 null ,越狱设备就各有各的精彩了,尤其是老一点的 iOS 版本越狱环境。

基于脚本实现动态库注入

19 基于脚本实现动态库注入

MobileSubstrate 可以帮助我们加载自己的动态库,于是开发者们谨慎的采取了对 MobileSubstrate 的检索和防御措施。

那么,除了依靠 MobileSubstrate 帮忙注入 dylib ,还有别的攻击入口吗?

图片 19.1 sript-injection1

理理思路,条件、目的很明确:

1)必须在应用程序启动之前,把 dylib 的环境变量配置好
2)dylib 的位置必须能被应用程序放问到
3)最后再启动应用程序

图片 19.2 sript-injection2

啊哈,原汁原味,走 bash

在点击应用程序图标–>程序启动这个过程中,在我们看来程序是被动执行的。为了让特定功能的脚本被执行,我们可以把脚本改成应用程序二进制的名字伪装成应用程序,让系统调用启动。在脚本中,配置好 dylib ,然后再手动启动真的应用程序,假装什么也没发生,挥一挥衣袖不带走一片云彩~ 将真的支付宝程序改名为 oriPortal :

mv Portal oriPortal

将待执行的脚本改名为支付宝:

mv Portal.sh Portal

脚本代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash  

 #得到第一个参数  
C=$0  

 #第一个参数是二进制的绝对路径 比如 :  
 #/private/var/mobile/Applications/4763A8A5-2E1D-4DC2-8376-6CB7A8B98728/Portal.app/  
 #截取最后一个 / 之前的内容  
C=${C%/*}  

 #库和二进制放在一起  
export DYLD_INSERT_LIBRARIES=${C:-.}/wq.dylib  
 #执行原来APP $@ 别忘了把原来的参数保留  
exec "${C:-.}"/oriPortal "$@"

结果不尽人意,失败了……

图片 19.3 sript-injection3

错误信息如下:

图片 19.4 sript-injection6

在打开某个加密信息时出了错误,大概猜一下应该是类似加密签名校验的步骤,但是我们无法去了解其中详细的操作到底是什么样的,没关系,那么就把原始的可执行文件环境全部给他造出来,因为检验文件属性肯定不会带着路径信息的。

备份一份 Portal.app 目录 Portal_ori.app ,修改脚本为:

1
2
3
4
5
#!/bin/bash  
C=$0  
C=${C%/*}  
export DYLD_INSERT_LIBRARIES=${C:-.}/wq.dylib  
exec "${C:-.}"/../Portal_ori.app/Portal "$@"

运行支付宝 app 验证一下, 好消息是,在 iOS6 上,成功加载了动态库 wq.dylib 坏消息是,在 iOS7 上,失败了

错误信息如下:

图片 19.5 sript-injection7

应该是因为 iOS7 的沙盒机制升了级,把我们这套小把戏拦在门外了……

图片 19.6 sript-injection8

那又怎么样,面包总会有的~

数据保护 API

18 数据保护 API

题外话

开篇先扯几句题外话,许多朋友都问我怎么不写防啊,我确实有点犹豫。 hackers 总是想象如果自己是开发者会怎么写,然后才能找到入手点。同理,开发者们也要想象自己是 hackers 会怎么做,才能采取相应的防御措施。然后,就是一场递归的博弈。

拿越狱检测这件事来说,起初大家只需判断有无安装 Cydia 就好了,hackers 们说好,那我就不安装 Cydia 也可以动手脚。开发者们又说,那你一定得用的上 MobileSubstrate bash ssh 吧,我去检测手机有没有安装这些工具。可是又有什么用呢?你判断什么我绕过去什么。

class-dump 大肆流行,函数符号都被暴露,开发者想尽办法藏起自己的敏感函数代码。hackers 们也知道 class-dump 的死穴在哪里,于是新的检索办法油然而生。也就说,当一个防御手段成为流行,它就不会再是个让 hackers 大骂“真特么费劲”的防御手段了。比如之前介绍的一个小技巧:内存数据擦除 ,hackers 知道开发者都去擦数据了,那我 hook memset 在你擦之前去读就好了。开发者说:我直接写硬盘上然后删除!hackers 说:难道你没听说过文件恢复?

图片 18.1 data-erase1

OK,贫的有点多了,本文介绍一下防御相关的话题—— iOS 的数据保护 API 。

数据保护 API

文件系统中的文件、keychain 中的项,都是加密存储的。当用户解锁设备后,系统通过 UDID 密钥和用户设定的密码生成一个用于解密的密码密钥,存放在内存中,直到设备再次被锁,开发者可以通过 Data Protection API 来设定文件系统中的文件、keychain 中的项应该何时被解密。

文件保护

1
2
3
4
5
6
7
8
9
10
11
12
/* 为filePath文件设置保护等级 */
NSDictionary *attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
                                                       forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attributes
                                 ofItemAtPath:filePath
                                        error:nil];

//文件保护等级属性列表  
NSFileProtectionNone                                    //文件未受保护,随时可以访问 (Default)  
NSFileProtectionComplete                                //文件受到保护,而且只有在设备未被锁定时才可访问  
NSFileProtectionCompleteUntilFirstUserAuthentication    //文件收到保护,直到设备启动且用户第一次输入密码  
NSFileProtectionCompleteUnlessOpen                      //文件受到保护,而且只有在设备未被锁定时才可打开,不过即便在设备被锁定时,已经打开的文件还是可以继续使用和写入

keychain 项保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 设置keychain项保护等级 */
NSDictionary *query = @{(__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
                        (__bridge id)kSecAttrGeneric:@"MyItem",
                        (__bridge id)kSecAttrAccount:@"username",
                        (__bridge id)kSecValueData:@"password",
                        (__bridge id)kSecAttrService:[NSBundle mainBundle].bundleIdentifier,
                        (__bridge id)kSecAttrLabel:@"",
                        (__bridge id)kSecAttrDescription:@"",
                        (__bridge id)kSecAttrAccessible:(__bridge id)kSecAttrAccessibleWhenUnlocked};

OSStatus result = SecItemAdd((__bridge CFDictionaryRef)(query), NULL);

//keychain项保护等级列表  
kSecAttrAccessibleWhenUnlocked                          //keychain项受到保护,只有在设备未被锁定时才可以访问  
kSecAttrAccessibleAfterFirstUnlock                      //keychain项受到保护,直到设备启动并且用户第一次输入密码  
kSecAttrAccessibleAlways                                //keychain未受保护,任何时候都可以访问 (Default)  
kSecAttrAccessibleWhenUnlockedThisDeviceOnly            //keychain项受到保护,只有在设备未被锁定时才可以访问,而且不可以转移到其他设备  
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly        //keychain项受到保护,直到设备启动并且用户第一次输入密码,而且不可以转移到其他设备  
kSecAttrAccessibleAlwaysThisDeviceOnly                  //keychain未受保护,任何时候都可以访问,但是不能转移到其他设备

应用实例

把一段信息 infoStrng 字符串写进文件,然后通过 Data Protection API 设置保护。

1
2
3
4
5
6
7
8
9
10
11
NSString *documentsPath =[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
NSString *filePath = [documentsPath stringByAppendingPathComponent:@"DataProtect"];
[infoString writeToFile:filePath
             atomically:YES
               encoding:NSUTF8StringEncoding
                  error:nil];
NSDictionary *attributes = [NSDictionary dictionaryWithObject:NSFileProtectionComplete
                                                       forKey:NSFileProtectionKey];
[[NSFileManager defaultManager] setAttributes:attributes
                                 ofItemAtPath:filePath
                                        error:nil];

设备锁屏(带密码保护)后,即使是越狱机,在 root 权限下 cat 读取那个文件信息也会被拒绝。

图片 18.2 data-erase2

Fish Hook

17 Fishhook

众所周知,Objective-C 的首选 hook 方案为 Method Swizzle,于是大家纷纷表示核心内容应该用 C 写。

接下来进阶说说 iOS 下 C 函数的 hook 方案,先介绍第一种方案— fishhook .

什么是 fishhook

fishhook 是 facebook 提供的一个动态修改链接 Mach-O 符号表的开源工具。

什么是 Mach-O

Mach-O 为 Mach Object 文件格式的缩写,也是用于 iOS 可执行文件,目标代码,动态库,内核转储的文件格式。

Mach-O 有自己的 dylib 规范。

fishhook 的原理

详见官方的 How it works,这里我作个简要说明。

dyld 链接 2 种符号,lazy non-lazy ,fishhook 可以重新链接/替换本地符号。

图片 17.1 fishhook1

如图所示,__DATA区有两个 section 和动态符号链接相关:__nl_symbol_ptr__la_symbol_ptr__nl_symbol_ptr 为一个指针数组,直接对应 non-lazy 绑定数据。__la_symbol_ptr 也是一个指针数组,通过dyld_stub_binder 辅助链接。<mach-o/loader.h>的 section 头提供符号表的偏移量。

图示中,1061 是间接符号表的偏移量,*(偏移量+间接符号地址)=16343,即符号表偏移量。符号表中每一个结构都是一个 nlist 结构体,其中包含字符表偏移量。通过字符表偏移量最终确定函数指针。

fishhook 就是对间接符号表的偏移量动的手脚,提供一个假的 nlist 结构体,从而达到 hook 的目的。

fishhook 替换符号函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
  int retval = prepend_rebindings(rebindings, rebindings_nel);
  if (retval < 0) {
    return retval;
  }
  // If this was the first call, register callback for image additions (which is also invoked for  
  // existing images, otherwise, just run on existing images  
  if (!rebindings_head->next) {
    _dyld_register_func_for_add_image(rebind_symbols_for_image);
  } else {
    uint32_t c = _dyld_image_count();
    for (uint32_t i = 0; i < c; i++) {
      rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
    }
  }
  return retval;
}

关键函数是 _dyld_register_func_for_add_image,这个函数是用来注册回调,当 dyld 链接符号时,调用此回调函数。 rebind_symbols_for_image 做了具体的替换和填充。

fishhook 替换 Core Foundation 函数的例子

以下是官方提供的替换 Core Foundation 中 open 和 close 函数的实例代码

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
 #import <dlfcn.h>
 #import <UIKit/UIKit.h>

 #import "AppDelegate.h"
 #import "fishhook.h"

static int (*orig_close)(int);
static int (*orig_open)(const charchar *, int, ...);

void save_original_symbols() {
  orig_close = dlsym(RTLD_DEFAULT, "close");
  orig_open = dlsym(RTLD_DEFAULT, "open");
}

int my_close(int fd) {
  printf("Calling real close(%d)\n", fd);
  return orig_close(fd);
}

int my_open(const charchar *path, int oflag, ...) {
  va_list ap = {0};
  mode_t mode = 0;

  if ((oflag & O_CREAT) != 0) {
    // mode only applies to O_CREAT  
    va_start(ap, oflag);
    mode = va_arg(ap, int);
    va_end(ap);
    printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
    return orig_open(path, oflag, mode);
  } else {
    printf("Calling real open('%s', %d)\n", path, oflag);
    return orig_open(path, oflag, mode);
  }
}

int main(int argc, charchar * argv[])
{
  @autoreleasepool {
    save_original_symbols();
    //fishhook用法  
    rebind_symbols((struct rebinding[2]){
      {"close", my_close},
      {"open", my_open}}, 2);

    // Open our own binary and print out first 4 bytes (which is the same  
    // for all Mach-O binaries on a given architecture)  
    int fd = open(argv[0], O_RDONLY);
    uint32_t magic_number = 0;
    read(fd, &magic_number, 4);
    printf("Mach-O Magic Number: %x \n", magic_number);
    close(fd);

    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
  }
}
1
2
3
4
5
// fishhook 用法处:
rebind_symbols((struct rebinding[2]){
  {"close", my_close},
  {"open", my_open}
  }, 2);

传入 rebind_symbols 的第一个参数是一个结构体数组,大括号中为对应数组内容。

不得不说,facebook 忒 NB 。

使用 Introspy 追踪分析应用程序

16 使用 introspy 追踪分析应用程序

如果你已阅读了《 iOS 安全攻防》系列专栏之前的文章,一定已经对静态以及运行时分析 App 有了一定的了解。

我们可以借助的分析工具很多,工具和工具之间一般没有什么优劣比较性,完全看个人习惯什么擅长什么。

多个工具多条路,那么本文将介绍追踪分析利器 introspy

对应 iOS 系统版本,下载适用的 introspy 工具包:introspy下载地址传送门

下载后,将其拷贝到设备中,并执行安装命令:

1
# dpkg -i com.isecpartners.introspy-v0.4-iOS_7.deb

重启设备:

1
# killall SpringBoard

到设置中,就可以查看到 instrospy 的设置选项了:

图片 16.1 instrospy1

Introspy-Apps 中选择要跟踪的 app 名称。 Instrospy-Settings 则提供一些常规跟踪设置选项,默认是全部开启。

然后启动想要跟踪的应用程序,就可以直接查看 log 获取 Instrospy 为我们跟踪捕获的信息,这里以跟踪支付宝 app 为例。

打开支付宝 App ,选择添加银行卡,随意添加一个卡号,然后点击下一步:

图片 16.2 instrospy2

支付宝 app 反馈添加失败,该卡暂不支持,Instrospy 捕获的信息也很清晰:

图片 16.3 instrospy3

追踪信息被保存为一个数据库introspy-com.alipay.iphoneclient.db,存放在: ./private/var/mobile/Applications/4763A8A5-2E1D-4DC2-8376-6CB7A8B98728/Library/introspy-com.alipay.iphoneclient.db

也可以借助 Introspy-Analyzer 在本地将该数据库解析成一个直观的 report.html 查看 Introspy-Analyzer下载地址传送门

introspy-com.alipay.iphoneclient.db 拷贝到本地,执行: python introspy.py -p ios --outdir Portal-introspy-html introspy-com.alipay.iphoneclient.db

就会生成一个 Portal-introspy-html 文件夹,该目录下有 report.html ,用浏览器打开: open report.html

就可以清晰的查看追踪信息了,主要分为 DataStorageIPCMiscNetworkCrypto 六大类信息。 举个例子,选择 Crypto 可以查看支付宝 app 采取了什么加密措施,如果你看过我之前的文章,一定会一眼就认出来手势密码的:

图片 16.4 instrospy4

使用 iNalyzer 分析应用程序

15 使用 iNalyzer 分析应用程序

好想用 doxygen 画 iOS app 的 class 继承关系。

有没有比 class-dump-z 更直观的分析工具? 利器 iNalyzer 隆重登场~

iNalyzer 的安装

在 iPhone 端:

1)进入 cydia 添加源 http://appsec-labs.com/cydia/

2)搜索 iNalyzer 并安装

Doxygen 和 Graphviz 的安装

在 Mac 端:
brew install oxygen graphviz

解密支付宝 App

查看可解密的 App

1
2
3
4
5
cd /Applications/iNalyzer5.app  
./iNalyzer5   

usage: ./iNalyzer5 [application name] [...]  
Applications available: Portal Tenpay

解密支付宝 App

1
2
3
4
5
6
7
8
9
10
11
12
13
./iNalyzer5 Portal  

got params /var/mobile/Applications/4763A8A5-2E1D-4DC2-8376-6CB7A8B98728/Portal.app/ Portal.app 800 iNalyzer is iNalyzing Portal...  
iNalyzer:crack_binary got /var/mobile/Applications/4763A8A5-2E1D-4DC2-8376-6CB7A8B98728/Portal.app/Portal /tmp/iNalyzer5_3f0d8773/Payload/Portal.app/Portal Dumping binary...helloooo polis?  
helloooo polis?  
iNalyzer:Creating SnapShot into ClientFiles  
iNalyzer:SnapShot Done  
iNalyzer:Population Done  
iNalyzer:Dumping Headers  
iNalyzer:Patching Headers  
/bin/sh: /bin/ls: Argument list too long  
ls: cannot access *_fixed: No such file or directory  
    /var/root/Documents/iNalyzer/支付宝钱包-v8.0.0.ipa

将解密后的 ipa 拷贝到本地

修改 doxMe.sh 脚本

解压 ipa, cd 到 /支付宝钱包-v8.0.0/Payload/Doxygen 下找到 doxMe.sh

1
2
3
#!/bin/sh  

/Applications/Doxygen.app/Contents/Resources/doxygen dox.template && open ./html/index.html

我们是通过 brew 安装的 doxygen ,所以修改脚本为:

1
2
3
#!/bin/sh  

doxygen dox.template && open ./html/index.html

执行 doxMe.sh 脚本

./doxMe.sh

完成后浏览器会自动 open 生成的html文件

查看信息

通过 index.html 我们可以直观的查看到 Strings analysis ViewControllers Classes 等几大类的信息。

图片 15.1 inalyzer1

在 Classes->Class Hierarchy 可以查看到类继承图示。 支付宝 app class Hierarchy 结果冰山一角:

图片 15.2 inalyzer2