移动开发技术

Jason's Blog

记一次数据库被攻击的经历

1. 问题出现

之前为了练手做了一个基于nodejs的后台系统。有一天突然发现http api访问没有数据了,赶快打开浏览器看了下报错信息,发现数据库的表找不到了,于是觉得问题可能有点大,马上登录服务器查看。

登录服务器后一看,发现数据库里的表被删掉了,新建了一个库叫PLEASE_READ_ME_XMG,里边有个表叫WARNING,WARNING表的结构如下图

猜测warning是警告信息,Bitcoin_Address是比特币钱包地址,Email是黑客联系邮箱。不过发现这个表里并没有数据,真不知道黑客是什么目的,还是只是恶作剧。上网搜索关键字“PLEASE_READ_ME_XMG”也印证了跟比特币勒索相关。

2. 查找问题

然后开始查找问题,先去mysql数据文件夹查看表文件WARNING.frmWARNING.ibd的生成时间,确定了攻击发生的时间点。

用命令查看linux机器登录记录

1
who /var/log/wtmp

发现在攻击发生的时间点,没有登录日志。那就怀疑用户是远程登录mysql直接操作数据库。于是登录数据库查看授权情况

1
show grants;

显示

1
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'

%代表可以从任意机器登录。

3. 解决问题

3.1 修改mysql权限

首先把数据库授权改为localhost。

1
grant all privileges on *.* to 'root'@'localhost' with grant option

3.2 增加binlog

其次,由于mysql没有开启binlog,所以无法查看数据库的改动记录,也就无法看到删除数据库表的那条命令信息,经过这次教训,将binlog打开。

找到mysql配置文件my.cnf

1
2
[mysqld]
log-bin=/***/***/logs

在mysqld项下增加log-bin的配置,指定binlog的输出路径,注意这里要修改下logs文件夹的权限,保证可以正常写入。

然后重新启动mysql

1
systemctl restart mysqld

再尝试进行mysql update操作,通过命令查看binlog,发现日志正常写入。

1
mysqlbinlog -d xxx logs.000001

其中xxx是数据库名。

3.3 定时备份数据

除此之外,为了更加保险,写了备份脚本,通过mysqldump进行备份

1
2
3
4
#! /bin/sh

time=$(date "+%Y%m%d-%H%M")
mysqldump -uxxx -pxxx --databases dbname > /path/to/bak/$time.sql

将脚本保存,修改执行权限,加入crontab中定时每个小时执行备份

1
0 * * * * /path/to/back/bak_db.sh > /dev/null 2>&1 &

4. 总结

通过这次的攻击,数据全部丢失,虽然通过其他途径找回了信息,但是mysql数据本身并没有得到恢复。得到的教训如下:

  1. mysql操作权限要严格限制,通过白名单的方式放开,其他全部拒绝

  2. mysql要打开binlog功能,可以用来恢复数据和查看操作记录

  3. 数据要定时备份,以防丢失后能够恢复

iOS越狱以及cycript的安装

本篇文章向iOS逆向初学者介绍如何越狱手机,并且安装cycript工具来调试第三方App。

1. 准备

  • 1台iOS越狱手机

  • mac电脑

  • 苹果开发者账号

2. 手机如何越狱

根据当前网上查到的资料,iOS 12.1.4以下(包含)是可以越狱的,如果你的手机系统版本升级到了12.1.4之上,目前来讲没有办法,可以去淘宝买一部越狱手机。

笔者的手机系统版本是iOS 11.2.5,这个版本是可以非完美越狱的。非完美越狱的意思是,手机越狱成功后,如果重启,则会恢复到未越狱状态,需要重新越狱。不过这个是可以接受的,毕竟可以用了不是。以下以iOS 11.2.5为例,介绍越狱步骤。

2.1 越狱工具的选择

笔者越狱的时候尝试了两个工具Unc0ver和Electra,用了之后发现Electra好用一点,Unc0ver的问题是,越狱之后Cydia总是出现网络问题连接不上。用Electra越狱后Cydia可以使用,打开Cydia后会提示升级,正常升级后Cydia会变成Sileo,这个可以理解为新版的Cydia。

2.2 如何安装Electra

首先要在mac上安装Cydia Impactor。Cydia Impactor安装后,用数据线连接手机和电脑,将Electra.ipa拖入Impactor。安装的时候会要求输入账号名和密码。这里需要输入苹果开发者账号(每年699元那个),输入的密码并不是开发者账号的密码,而是临时生成的密码,需要到https://appleid.apple.com生成App专用密码。这样Electra就顺利安装到手机上了。

2.3 越狱

打开手机上的Electra App,点击Jailbroken按钮,注意底下会有个开关选项"Tweaks",这里需要把选项关掉,否则后边使用cycript会出错。顺利的话,手机会黑屏,然后重新启动桌面。再打开Electra App,刚才的Jailbroken按钮变成了Already Jailbroken,说明越狱已经成功了。手机桌面应该也出现了Cydia或Sileo图标。

3. 如何ssh到手机

有两种方式可以使mac ssh到手机上,一种是通过同一局域网WIFI,另一种是通过手机直接连接到mac。为了获取更快的连接速度和更稳定,笔者选择了后一种方式。

首先在mac上安装usbmuxd

1
brew install usbmuxd

usbmuxd自带了一个工具iproxy,这个工具的作用是,在mac和手机之间架上一个代理,mac ssh手机本来是走22端口,需要用这个工具将其他端口,比如5678,转发到22端口上。运行命令

1
iproxy 5678 22

运行后正常情况下会显示

1
waiting for connection

这时候,在mac上打开另一个终端窗口,输入命令

1
ssh -p 5678 root@127.0.0.1

输入默认的root密码alpine,就能成功ssh到手机上了。关于如何不每次都输入密码,这里可以自行搜索下其他相关资料,这里不再累述。

除了能够使用ssh,还可以使用scp向手机传文件,这里需要注意同样需要指定端口号,例如

1
scp -P 5678 filexxx root@127.0.0.1:/tmp

ssh到手机上之后,如果我们需要其他的软件,可以使用apt-get,例如安装vim

1
apt-get install vim

4. cycript的安装和使用

能够以root身份ssh到手机之后,就可以做很多事情了,比如查看当前手机正在运行的App的进程号,以及各个App的沙盒路径,以及随意浏览里边的文件,很多有趣的事情可以去探索和尝试。这篇文章主要介绍cycirpt工具的使用。

4.1 cycript介绍

cycript是一个脚本语言,通过编写脚本代码,可以在任意第三方App运行的时候,执行任何逻辑,通常情况下可以用来查看App的UI结构,某个页面的实现方式。其他的使用目的等待你去探索和发现。

4.2 安装和使用cycript

在网上查了些资料,说cycript可以直接安装在iOS手机上的,这样只要ssh到手机上,执行cycript命令,就可以使用了。但是经过尝试,iOS 11.2.5版本不能直接安装cycript,取而代之的是一个叫bfinject的工具。bfinject工具先安装到手机上,然后启动cycript监听服务,然后mac上安装cycript客户端,通过无线的方式连接手机暴露出来的ip和端口,就可以使用cycript编写和执行脚本了。

4.2.1 在手机上安装bfinject

https://github.com/BishopFox/bfinject下载bfinject,scp到手机上,然后解压,在解压目录运行bash bfinject启动服务。

通常情况下,会报错Unknown jailbrek。我们通过查看bfinject的代码,可以发现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# Detect LiberiOS vs Electra
#
if [ -f /bootstrap/inject_criticald ]; then
    # This is Electra
    echo "[+] Electra detected."
    cp jtool.liberios /bootstrap/usr/local/bin/
    chmod +x /bootstrap/usr/local/bin/jtool.liberios
    JTOOL=/bootstrap/usr/local/bin/jtool.liberios
    cp bfinject4realz /bootstrap/usr/local/bin/
    INJECTOR=/bootstrap/usr/local/bin/bfinject4realz
elif [ -f /jb/usr/local/bin/jtool ]; then
    # This is LiberiOS
    echo "[+] Liberios detected"
    JTOOL=jtool
    INJECTOR=`pwd`/bfinject4realz
else
    echo "[!] Unknown jailbreak. Aborting."
    exit 1
fi

由以上代码第1个if分支可以判断,/bootstrap/inject_criticald目录不存在,这个是由于软件的目录变了,在不改变bfinject代码的前提下,需要修改一下目录,执行以下命令

1
2
3
4
ln -s /electra /bootstrap
mkdir /bootstrap/usr
mkdir /bootstrap/usr/local
mkdir /bootstrap/usr/local/bin

重新执行bash bfinject,还会出现一个错误md5: command not found,这个是由于需要运行md5程序,但是ios系统自带的程序叫md5sum,所以简单的办法是修改bfinject代码,将其中的md5改成md5sum就可以了。

4.2.2 在手机上启动bfinject服务

要查看哪个App,需要先在手机上启动,然后找到App的进程号,例如我们要查看微信的进程号,可以用以下命令

1
ps aux | grep containers | grep WeChat

假设微信的进程号为1026,运行

1
bash bfinject -p 1026 -L cycript

成功的话,在微信App的页面内会弹出一个框,上边的意思是正在监听xx.xx.xx.xx:1337端口(xx.xx.xx.xx为手机的ip地址),可以连接这个端口来进行cycript调试了。

4.2.3 在mac上安装cycript客户端

首先去http://www.cycript.org/下载代码,运行./cycript的时候,会报错,仔细查看是缺少ruby2.0的库,在mac上先安装ruby2.0

1
brew install ruby@2.0

安装后将/usr/local/Cellar/ruby@2.0/2.0.0-p648_2/lib/libruby.2.0.0.dylib拷贝到cycript/Cycript.lib目录下。顺利的话就可以用了。

在mac上执行

1
cycript -r xx.xx.xx.xx:1337

xx.xx.xx.xx为手机的ip,这里注意一下,因为是以无线连接的,所以手机和电脑需要在同一个局域网里,如果出现

1
cy#

代表cycript运行成功,可以通过写cycript脚本执行命令了。

5. 总结

本篇文章介绍了如何越狱iOS手机,以及逆向工具cycript的安装。关于cycript的详细使用,限于篇幅原因不介绍了,请自行查看相关资料。总之,这个工具非常强大,可以更深入的分析目标App的实现方式。

SDWebImage源码解读

SDWebImage是最流行的iOS第三方图片加载库,也是github上star数目最多的objective-c第三方库。这篇文章对SDWebImage的源码进行简单的分析,主要是分析代码的执行流程。源码版本是目前最新的稳定版本4.4.4。

源码目录

SDWebImage下分为以下几个目录

  • Downloader: 负责图片的下载,基于NSURLSession实现,主要类是SDWebImageDownloader
  • Cache: 负责图片的缓存,主要是对内存和磁盘进行读写,实现二级缓存功能,主要类是SDImageCache
  • Decoder: 负责图片的解码功能,以支持不同格式的图片,例如SDWebImageGIFCoder是对GIF图片的支持
  • Utils: 封装的工具类,主要类是SDWebImaegeManger,控制整个图片下载和缓存流程
  • Categories: 一些扩展支持,比如如果要对GIF图片进行下载展示,需要引入UIImage+GIF.h(框架已默认引入)
  • WebCache Categories: 对UIView的扩展支持,最常用的是UIImageView+WebCache.h,实现了UIImageView的图片加载和缓存功能
  • FLAnimatedImage: 对FLAnimatedImage进行了扩展,可以对动态图片进行加载和缓存

调用时序图

以官方Demo详情页加载图片为例,加载图片的时序图如下:

  1. 调用sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context:
  2. 调用loadImageWithURL:options:progress:completed:
  3. 调用queryCacheOperationForKey:cacheOptions:done查询缓存
  4. 调用imageFromMemoryCacheForKey查内存, 调用diskImageDataBySearchingAllPathsForKey查磁盘
  5. 调用downloadImageWithURL:options:progress:completed:从网络下载
  6. 调用storeImage:imageData:forKey:toDisk:completion:将结果缓存

源码分析

在官方Demo中,有一个列表页和详情页,我们从更简单的详情页来分析,详情页只有一个FLAnimatedImage控件,功能就是加载了一张图片,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__weak typeof(self) weakSelf = self;
[self.imageView sd_setImageWithURL:self.imageURL
                  placeholderImage:nil
                           options:SDWebImageProgressiveDownload
                          progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL *targetURL) {
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  float progress = 0;
                                  if (expectedSize != 0) {
                                      progress = (float)receivedSize / (float)expectedSize;
                                  }
                                  weakSelf.progressView.hidden = NO;
                                  [weakSelf.progressView setProgress:progress animated:YES];
                              });
                          }
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                             weakSelf.progressView.hidden = YES;
                             [weakSelf.activityIndicator stopAnimating];
                             weakSelf.activityIndicator.hidden = YES;
                         }];

对外暴露的接口比较简单,传递需要加载图片的url,placeholder图片,加载选项options,加载过程回调progressBlock,完成回调completedBlock就可以了。

接下来到时序图的第(1)步,调用UIView+WebCachesd_internalSetImageWithURL:方法,代码如下:

1
2
3
4
5
6
7
8
9
10
[self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
               internalSetImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                           ……
                       }
                            progress:progressBlock
                           completed:completedBlock
                             context:@{SDWebImageInternalSetImageGroupKey: group}];

继续跟进这个方法,时序图到第(2)步,调用SDWebImageManagerloadImageWithURL:方法,代码如下:

1
2
3
4
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
  //加载完图片的回调
  ……
}

时序图来到第(3)步,SDWebImageManager会调用SDImageCachequeryCacheOperationForKey来进行缓存查询

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
//SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {

    // 读内存缓存操作
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
      //读磁盘缓存
      ……
    };

    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }

    return operation;
}

可以看到,在上边的代码中,调用imageFromMemoryCacheForKey先从内存里查询是否有图片缓存,调用dispatch_async(self.ioQueue, queryDiskBlock)从磁盘里查询是否有图片缓存。这是时序图中的第(4)步。如果命中了缓存,则直接回调完成block,不再走下边的流程了。如果没有命中缓存,那么继续下边。

流程来到时序图中的第(5)步,查询完缓存返回后,SDWebImageManager会调用SDWebImageDownloaderdownloadImageWithURL:方法从网络下载图片

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
//SDWebImageDownloader.m
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        //执行下载操作
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

从网络下载图片的代码如上,第8行生成NSOperation,第26行加入叫做downloadQueueNSOperationQueue执行。

下载完成后,时序图流程来到第(6)步,SDWebImageManager会调用SDImageCachestoreImage:方法将结果进行缓存,以便下次使用,然后再进行成功的回调,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
    NSData *cacheData;
    // pass nil if the image was transformed, so we can recalculate the data from the image
    if (self.cacheSerializer) {
        cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
    } else {
        cacheData = (imageWasTransformed ? nil : downloadedData);
    }
    [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}

[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

到这里图片加载流程结束。

SDWebImage里还有很多实现细节,比如多线程的控制和加锁,各种控制下载行为的选项,取消正在下载的操作,URL参数的容错,PlaceHolder的设置,加载IndicatorView的显示控制,下载过程Progress的控制等。由于篇幅原因在本文中省略,等以后有时间再细致分析。

Aspects源码解读

Aspects是iOS面向切面编程的第三方库,它可以在不改变原有代码的情况下,在任意函数之前或之后插入代码,也可以替换掉函数原有的代码。它的原理是基于oc语言的runtime,这篇文章对Aspects进行源码解读,并阐述其原理。

调用方式

首先我们下载官方demo,从入口代码开始看:

1
2
3
4
AspectsViewController *aspectsController = [AspectsViewController new];
[aspectsController aspect_hookSelector:@selector(buttonPressed:) withOptions:0 usingBlock:^(id info, id sender) {
    NSLog(@"Button was pressed by: %@", sender);
} error:NULL];

这段代码就是Aspects的调用方式之一,表示在对象aspectsController的buttonPressed函数执行之后,再执行block里的代码,打印一行日志。withOptions的参数写的0,这里是一个枚举值,可以控制block代码怎样执行,具体的定义如下:

1
2
3
4
5
6
7
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.

    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

hook过程

我们从入口函数进入开始跟踪代码,最后发现无论是对实例方法还是类方法进行hook,都会调用aspect_add函数,省略了一些无关代码后如下:

1
2
3
4
5
6
7
8
9
10
11
12
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    if (identifier) {
        [aspectContainer addAspect:identifier withOptions:options];

        // Modify the class to allow message interception.
        aspect_prepareClassAndHookSelector(self, selector, error);
    }
    return identifier;
}

这段代码做了两件事情。

首先生成AspectIdentifier,然后将AspectIdentifier加入到AspectsContainer中。AspectIdentifier的定义如下,它描述了一个Ascpect切片代码的信息。

1
2
3
4
5
6
7
@interface AspectIdentifier : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

AspectsContainer的定义如下,它负责容纳AspectIdentifier,可以在before,instead,after数组里放入多个AspectIdentifier,从名称可以看出这些AspectIdentifier所执行的时机。AspectsContainer将在后边取出并执行。

1
2
3
4
5
@interface AspectsContainer : NSObject
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

其次调用aspect_prepareClassAndHookSelector函数,这是最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        }
        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}

这个函数分为两部分,第2行aspect_hookClass和后边的部分。我们先来看aspect_hookClass函数,省略后的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Class aspect_hookClass(NSObject *self, NSError **error) {
  Class statedClass = self.class;
  Class baseClass = object_getClass(self);
  NSString *className = NSStringFromClass(baseClass);

    // Default case. Create dynamic subclass.
  const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
  Class subclass = objc_getClass(subclassName);

  if (subclass == nil) {
      subclass = objc_allocateClassPair(baseClass, subclassName, 0);
      aspect_swizzleForwardInvocation(subclass);
      aspect_hookedGetClass(subclass, statedClass);
      aspect_hookedGetClass(object_getClass(subclass), statedClass);
      objc_registerClassPair(subclass);
  }

  object_setClass(self, subclass);
  return subclass;
}

第11行代码通过运行时的函数objc_allocateClassPair定义了一个新的子类。如果是demo执行到这里的话,生成的子类叫AspectsViewController_Aspects。第12行,将子类的forwardInvocation替换为了自定义的实现函数__ASPECTS_ARE_BEING_CALLED__。第18行,将AspectsViewController实例的isa指针指向了子类AspectsViewController_Aspects。

接着,我们继续看aspect_prepareClassAndHookSelector函数的后半部分。第10行在AspectsViewController_Aspects类添加了一个方法aliasSelector,demo中就是aspect_buttonPressed,它的实现指向了原来AspectsViewController类的buttonPressed的实现。第13行,将AspectsViewController_Aspects类的buttonPressed实现指向了_objc_msgForward,这样调用就会启动oc的消息转发机制。

到这里,Aspects的hook流程就执行完了,我们用下边这个图来描述下当前类和方法实现之间的关系。

Aspects的实现为什么要生成一个原有类的子类,个人理解是为了对原有类产生的影响尽可能小。

hook后的执行流程

hook完成后,我们来看下hook后代码的执行流程。

这一段很重要!!!往AspectsViewController实例发送buttonPressed消息的时候,首先应该去查找实例所对应的类的方法列表,由于AspectsViewController的isa指向了AspectsViewController_Aspects类,就会去AspectsViewController_Aspects类中查找,结果是查找不到buttonPressed实现,然后会去查找父类AspectsViewController的方法列表,这时候查找到了buttonPressed的实现,但是实现是指向了_msg_forward,这样就进入了消息转发流程。按照消息转发流程,系统会调用AspectsViewController_Aspects类的forwardInvocation方法,forwardInvocation方法被我们替换成了自定义实现__ASPECTS_ARE_BEING_CALLED__,最终就进入了这个方法。

__ASPECTS_ARE_BEING_CALLED__的省略后的代码如下:

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
// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks.
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

第7行,对于hook的实例方法,先拿到之前设置的切片代码信息,存储在classContainer里。第24行,通过invocation调用AspectsViewController_Aspects的aspect_buttonPressed方法,由于这个方法已经指向了原来的实现buttonPressed,所以就调用了原始的代码。在这之后,如果Container里有afterAspects,就调用切片的block。beforeAspects同理。

到此为止,就实现了在原来的实例方法执行后,再执行hook插入的block代码。

总结

oc语言的runtime是黑魔法,运用起来可以做很多强大的功能。总的来讲,Aspects利用了method swizzling和消息转发机制forwordInvocation,实现了对函数进行切面hook。

对JSPatch原理的理解

JSPatch利用OC语言的动态特性,让OC语言根据传入的JS代码,进行动态行为修改,以达到热更新的目的。

项目中根据JSPatch的原理,自己实现了一套简单的热更新方案。以替换方法实现为例,热更新运行的步骤如下:

1.补丁下发阶段

应用启动的时候,会加载下发的补丁js文件,以下这一段热更新代码会被执行。

1
2
3
4
replaceMethod("IGTabBarController", "onNaviBarTaskBoxClick:", false, function (invocation) {
    log("origin method");
    callOriginMethod(invocation, "origin_onNaviBarTaskBoxClick:");
});

OC中的JSContext在初始化的时候加载过replaceMethod函数,所以会调用到OC代码

1
ocReplaceMethod:(NSString *)className selectorName:(NSString *)selectorName isClass:(BOOL)isClass func:(JSValue *)func

这个函数里,做了一个重要的逻辑(这里参考了JSPatch的实现方式),将IGTabBarController的实例方法onNaviBarTaskBoxClick:指向了forwardInvocation:,然后自定义实现PMDForwardInvocation替换forwardInvocation:的行为

2.用户调用阶段

用户操作点击后,IGTabBarController的onNaviBarTaskBoxClick:会被执行,从而PMDForwardInvocation被执行,根据OC的函数转发特性,PMDForwardInvocation会拿到所有的函数参数信息invocation。然后调用jsfunc(@[invocation])。这样就将所有原生参数通过invocation对象传回给了js代码。js代码拿到这些参数就可以去实现任何逻辑了,以达到替换原方法的目的。

总结:这里是一个很重要的技巧,如何将需要动态更新的OC方法的参数全部传给js代码,JSPatch是利用了forwardInvocation的特性。

Express学习笔记(二)-路由

这一篇主要介绍路由的用法

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();

// GET method route
app.get('/', function (req, res) {
  res.send('GET request to the homepage');
});

// POST method route
app.post('/', function (req, res) {
  res.send('POST request to the homepage');
});

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...');
  next(); // pass control to the next handler
});

all表示接受所有的http方法

路由路径

支持正则表达式

1
2
3
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});

此路由路径将匹配 abcd、abxcd、abRABDOMcd、ab123cd 等。

路由处理顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cb0 = function (req, res, next) {
  console.log('CB0');
  next();
}

var cb1 = function (req, res, next) {
  console.log('CB1');
  next();
}

app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('the response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from D!');
});

可以写数组,也可以写多个回调函数参数,挨着执行,前一个函数必须执行next(),否则http请求会挂起

模块封装

可以封装一个bird.js模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// define the home page route
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;

使用模块后,url从外部使用模块开始,接着模块定义的路径,就可以访问

1
2
3
var birds = require('./birds');
...
app.use('/birds', birds);

此时,可以响应/birds/birds/about

Express学习笔记(一)-开始

Express是一个基于nodejs的web开发框架。

Hello World

0.安装nodejs环境

以腾讯云的centOS云服务器为例,先运行

1
yum install nodejs

1.安装

1
2
3
4
$ mkdir myapp
$ cd myapp
$ npm init
$ npm install express --save

2.修改app.js,然后运行

1
$ node app.js

打开浏览器,访问http://localhost:3000%EF%BC%8C%E5%8F%AF%E4%BB%A5%E7%9C%8B%E5%88%B0%E8%BF%90%E8%A1%8C%E7%BB%93%E6%9E%9C%E4%BA%86

脚手架工程

1.安装express-generator

1
$ npm install express-generator -g

2.生成脚手架工程

1
$ express --view=pug myapp

pug是使用的模板引擎

1
2
$ cd myapp
$ npm install

3.运行

1
$ npm start

在浏览器里访问。

路由

1
2
3
app.get('/', function (req, res) {
  res.send('Hello World!');
});

表示根目录相应http get方法

静态文件

1
app.use('/static', express.static(__dirname + '/public'));

public目录下的所有文件可以作为静态资源访问。

TIP项目网络框架梳理

前段时间小伙伴重构了项目的网络层代码,将之前的过程式的代码,面向对象化了,职责分离,更易维护。不过也增加了理解成本,这里记录一下。

整个网络模块的类图如下。PMD开头的类下沉到了基础库,IG开头的类仍然在项目中。

  • IGNetworkManager作为项目中使用网络层的入口类,不多做介绍。
  • PMDNetworking是发起网络请求的类,这里首先要用PMDCallFactory工厂类,生成一个实现PMDCall协议的对象,然后调用makeCallWithRequest方法进行网络请求。
  • PMDCallFactory用于生成PMDCall协议对象
  • PMDCall协议对象为了避免被回收,放到了PMDCallPool里进行管理
  • PMDBaseCall实现了PMDCall协议,完成了主要的网络请求逻辑。分为以下几个步骤

1.callWithRequest准备发起网络请求

2.dealWithInterceptResult遍历所有PMDIntercept,在真正发起网络请求前进行逻辑处理,处理的过程中可以中断。

3.realCallWithRequest真正发起网络请求,这里的实现交给继承类IGCall来实现,具体的实现可以是http,也可以是tcp,在TIP项目中用到了IGNetworkObject去发网路请求。

4.convertResponse将请求回来的数据,遍历PMDConverter进行处理。

  • PMDInterceptor和PMDConverter协议分别是需要在网络请求发出之前和之后要处理的逻辑,只要实现此协议,加入到PMDBaseCall中就可以了。

基本的结构就是这样了,除此之外,框架还实现了取消发送,重新发送等逻辑,这里不再详细介绍。

对iOS App签名的理解

关于iOS设备签名的原理,这篇文章说的比较清楚了。如果忘了的话可以重新阅读以下。

iOS App 签名的原理

这里按照我的理解角度复述一下:

  1. 为了不让每次开发App,都将App上传到苹果后台,用苹果后台的私钥A签名,需要本地生成一对公私钥。这样每次签名就在本地Mac机器上,用私钥L签名就可以了。
  2. 那么苹果如果验证公钥L的合法性呢,就需要开发者将公钥L上传到苹果的后台,苹果用私钥A进行一次签名,签名+公钥L就是证书了。(这里实际上用一次对公钥L的签名替代了每次对App的签名,省去了App每次修改都上传签名的麻烦,很巧妙)
  3. 第2步中的证书+一些额外信息(AppId,设备列表,push权限等)全部都在苹果的后台用私钥A签名,打包成的东西叫Provisioning Profile,下载到本地Mac后,打包在App中。安装在iOS设备的时候,用公钥A对其进行解密,验证证书+一些额外信息(AppId,设备列表,push权限等。证书验证后拿到公钥L,对App数据进行解密验证(App是用私钥L签名的)。

好了,上边的文字其实看着还是有点乱和绕,权当我自己的思路做参考吧。

回到我们的项目,其实有一点是可以改进的。

以上图片是我们项目在苹果后台的证书配置,可以看到,有很多项,原因是每个开发的电脑上都生成了一对公钥L和私钥L,然后将公钥L上传到苹果后台生成了证书。更好的办法是,只用一台开发电脑生成公钥L和私钥L,然后到处p12文件,分享给别的电脑,这样苹果后台就只需要一个证书就可以了。