月度归档:2019年02月

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!

开源地址

首先抛出GitHub地址吧~多多支持指点,谢谢。
AYTikTokPod

简述

iOS逆向工程指的是软件层面上进行逆向分析的过程。
在一般的软件开发流程中,都是过程导向结果。在逆向中,你首先拿到的是结果,然后是去分析实现这个结果的过程。理清过程之后,才开始进行逆向的代码编写,在整个流程中,分析过程的占比是90%,代码书写的过程只占10%。所以本篇更多的讲的是一个思路,代码其实很日常

前期准备

  • 一台Mac
  • 一台iPhone
  • frida-ios-dump
  • Hopper Disassembler
  • class-dump
  • MonkeyDev
  • Reveal

frida-ios-dump

用于脱壳,脱壳是逆向的第一步。直接AppStore上下载的应用都有带壳,导致我们无法对他进行任何操作。脱壳的ipa文件,也可以直接去一些越狱商店下载,但是可能版本上比较旧。
如果有一台已越狱的机器,按照frida-ios-dump的wiki来操作很简单。

Hopper Disassembler

Hopper Disassembler是Mac上的一款二进制反汇编器,基本上满足了工作上的反汇编的需要,包括伪代码以及控制流图(Control Flow Graph),支持ARM指令集并针对Objective-C的做了优化。

class-dump

class-dump是一款可以导出头文件的命令行工具,改程序用于检查Objective-C运行时信息存储在Mach-O文件,它生成类的声明,类别和协议。

MonkeyDev

MonkeyDev的前身是iOSOpenDev,在iOSOpenDev的基础上增加CaptainHook TweakLogos TweakCommand-line Tool

MonkeyDev为我们做的事情:

  1. 创建dylib,通过hook修改类的属性或方法
  2. 将dylib注入到App中
  3. 重签名ipa文件

静态分析

前期准备

拿到TikTok的脱壳ipa文件

由于自己的6s痛失越狱环境,于是脱壳这一步,是拜托了我的好哥们完成的。
只要有越狱手机,砸壳并不复杂,按照网上的教程步骤来就行。

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
已脱壳的可执行文件

class-dump导出头文件

通过命令

class-dump -H XXX.app -o /DumpHeaderClass
  • -H后跟的是脱壳后的app文件路径
  • -o是头文件输出的文件夹路径

如图所示为class-dump之后的项目中所有头文件,单从这里,我们就能看出,TikTok项目中,使用的几个第三方库:AFNetWorkingYYKitFaceBook的SDK

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
class-dump后的头文件

tips: 快速搜索对应的头文件或方法,可以新建个工程,将头文件文件夹拖入项目中。有什么工具能比Xcode检索更方便检索代码呢?

Hopper静态分析

直接将脱壳后的二进制可执行文件拖入Hopper,等待一段时间后,Hopper会完成反编译。

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Hopper反编译之后的结果

左边的展示的是对应的类和方法列表,通过搜索框可以快速定位到方法。

红色框框起来的是模式切换:分别是汇编模式控制流图模式伪代码模式十六进制模式

通常我们用的最多的就是控制流图伪代码

Reveal查看界面

MonkeyDev会为我们自动注入RevealService.frameworkRevealService.framework需要和对应版本Reveal使用。否则请更新替换注入的RevealService.framework

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Reveal展示的TikTok界面布局

Reveal能让我们快速定位到我们需要的控制器或视图。
如图,首页的ViewController就是AWEFeedTableViewController

问题&处理问题


Question1

Q1:
发现从其他区的AppStore下载的TikTok打开后什么都没有?

T1:
初步怀疑是网络问题。

A1:
全局代理之后打开还是一片漆黑,基本排除是网络的问题导致的。


Question2

Q2:
如果不是网络问题,那问题会不会出现在请求参数上?

T2:
使用Charles抓包看看

A2:
刷新feed,拿到url

/aweme/v1/feed/?version_code=4.3.0&language=zh&pass-region=1&app_name=trill&vid=B196D171-B020-453E-A19C-9AAD845151BE&app_version=4.3.0&carrier_region=CN&is_my_cn=1&channel=App%20Store&mcc_mnc=46001&device_id=6631689375623284225&tz_offset=28800&account_region=&sys_region=CN&aid=1180&screen_width=750&openudid=63ceee2a26c0fd4501ebcf1f47a2311c5551f6e0&os_api=18&ac=WIFI&os_version=12.0&app_language=en&tz_name=Asia/Shanghai&device_platform=iphone&build_number=43004&device_type=iPhone8,1&iid=6635504889546049282&idfa=BFBB2BCA-9743-451B-95CC-F01292FC02F6&ad_user_agent=Mozilla%2F5.0%20%28iPhone%3B%20CPU%20iPhone%20OS%2012_0%20like%20Mac%20OS%20X%29%20AppleWebKit%2F605.1.15%20%28KHTML%2C%20like%20Gecko%29%20Mobile%2F16A366&count=6&feed_style=0&filter_warn=0&max_cursor=0&min_cursor=0&pull_type=1&type=0&volume=0.25&mas=01050af0364af36a45501b82b389f379ef3f8bda89739cc55924e8&as=a1157001fc8b2c2ce65057&ts=1544948924

其中有几个字段引起了怀疑:

key value
is_my_cn 1
language zh
account_region CN
carrier_region CN
mcc_mnc 46001
tz_name Asia/Shanghai
sys_region CN

分析:

  1. is_my_cn字面意思,是否是中国,很可能通过标记来判断是否是国内用户。
  2. language语言类型,通过这个来判断可能性比较低,误伤几率很高。外区也可以设置语言中文,但你不可能去影响他使用吧。这么做,是不合理的。
  3. account_regioncarrier_regionsys_region,账号、运营商和系统的地区,可能通过所属地区来进行封锁。
  4. mcc_mncmcc指的是移动国家码mnc指的是移动网络码
  5. tz_name时区。

验证:
使用CharlesRewrite或者Breakpoints来改变URL中传递的params

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Rewrite Settings

结果:
通过各种组合实验,发现真正产生作用的

key value
carrier_region CN

这里得到了第一个结论:说明TikTok服务器,是通过运营商来封锁用户的。既然是运营商,那就把mcc_mnc这个字段也一起处理。

key value
mcc_mnc 46001

Question3

Q3:
怎么处理carrier_regionmcc_mnc?

T3:
上面是通过Charles完成了,可以正常观看TikTok的视频,勉强算是完成了部分修改,但局限性很大。
比如:

  1. 无法评论、关注等操作,因为只Rewrite了部分接口,其他接口没有Rewrite
  2. 离开特定的WiFi就无法观看,无法通过蜂窝网观看视频。(PS:可以通过Thor这个软件的拦截器实现,和Charles的原理一致)
  3. 如果后续更新添加了接口签名校验,那这种方法就会失效。

A3:
方案一:
通过Hook第三方网络库AFNetWorking或内部封装的NetService类来修改carrier_region字段。
这个方案基本可行,通过HookAFHTTPRequestSerializer类的requestWithMethod: URLString: parameters: error:方法。获取parameters,然后修改carrier_region的值。

优点:

  1. 方案简单,不需要过多的内部实现分析。
  2. 能完成所有接口的Hook。

缺点:

  1. 遇到接口签名校验将失效。
  2. 所有网络接口都被Hook,如果Hook函数里存在复杂耗时的操作,会严重影响性能。

方案二:
iOS系统的CoreTelephony.frameworkCTCarrier类提供了carrier_regionmncmcc的获取。通过Hook他们来实现土突破地区限制。

/*
 * isoCountryCode
 *
 * Discussion:
 *   Returns an NSString object that contains country code for
 *   the subscriber's cellular service provider, represented as an ISO 3166-1
 *   country code string
 */

@property (nonatomic, readonly, retain, nullable) NSString* isoCountryCode __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);

/*
 * mobileCountryCode
 *
 * Discussion:
 *   An NSString containing the mobile country code for the subscriber's 
 *   cellular service provider, in its numeric representation
 */
@property (nonatomic, readonly, retain, nullable) NSString *mobileCountryCode __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);

/*
 * mobileNetworkCode
 *
 * Discussion:
 *   An NSString containing the  mobile network code for the subscriber's 
 *   cellular service provider, in its numeric representation
 */
@property (nonatomic, readonly, retain, nullable) NSString *mobileNetworkCode __OSX_AVAILABLE_STARTING(__MAC_NA,__IPHONE_4_0);

代码编写:

// MARK: - Hook CTCarrier
CHDeclareClass(CTCarrier)

CHMethod0(NSString *, CTCarrier, isoCountryCode) {
    NSDictionary *areaDic = [UserDefaults valueForKey:HookArea];
    NSString *code = [areaDic objectForKey:@"code"];
    return code;
}
CHMethod0(NSString *, CTCarrier, mobileCountryCode) {
    NSDictionary *areaDic = [UserDefaults valueForKey:HookArea];
    NSString *mcc = [areaDic objectForKey:@"mcc"];
    return mcc;
}
CHMethod0(NSString *, CTCarrier, mobileNetworkCode) {
    NSDictionary *areaDic = [UserDefaults valueForKey:HookArea];
    NSString *mnc = [areaDic objectForKey:@"mnc"];
    return mnc;
}

CHConstructor {
    CHLoadLateClass(CTCarrier);
    CHHook0(CTCarrier, isoCountryCode);
    CHHook0(CTCarrier, mobileCountryCode);
    CHHook0(CTCarrier, mobileNetworkCode);
}

CaptainHook为我们提供了完善的Hook宏。

  • CHDeclareClass作用是声明需要Hook的类
  • CHMethod作用是对应的方法Hook的实现
  • CHConstructor作用是用于加载Hook的方法和所在的类
  • CHLoadLateClass加载Hook类
  • CHHook注册Hook方法

这个framework底层通过runtime接口实现对应功能,比如

class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)

method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)

method_getImplementation(Method _Nonnull m)

method_getTypeEncoding(Method _Nonnull m) 

class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

结果:

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Hook之后的效果

到这里区域限制的突破已经完成了。


Question4

Q4:
使用过程中发现其他地区TikTok都能下载视频,日区TikTok不能😓

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Error 提示

T4:
使用的是同一部手机,只Hook了carrier_regionmcc_mnc,出现了下载限制问题,那肯定是地区版权策略导致的(11区对版权的重视,佩服了)。

A4:
点开分享按钮

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
分享界面

发现判断是否有下载权限是发生在按钮点击之前的。考虑是在请求返回的JSON数据中存储的flag,然后把这个flag传给AWEAwemeShareViewController

使用Reveal对界面分析,发现TableView的Cell类名是AWEFeedViewCell,然后查找class-dump出的AWEFeedViewCell.h,有一个可疑的方法是- (void)configWithModel:(id)arg1;

使用MDMethodTrace进行方法跟踪,确认了方法被调用,同时arg1的类型是AWEAwemeModel,这个Model里又发现了可疑的属性@property(nonatomic, assign) BOOL preventDownload;,意思是禁止下载

代码编写:

// MARK: - AWEAwemeModel
CHDeclareClass(AWEAwemeModel)

CHMethod1(void, AWEAwemeModel, setPreventDownload, BOOL, arg1) {
    arg1 = ![UserDefaults boolForKey:HookDownLoad];
    CHSuper1(AWEAwemeModel, setPreventDownload, arg1);
}

CHConstructor {
    CHLoadLateClass(AWEAwemeModel);
    CHHook1(AWEAwemeModel, setPreventDownload);
}

效果:

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
Hook preventDownload

下载按钮没被禁用了!怀着激动的心情点下去!

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
下载出错

WTF !!!

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
image

继续:
对比日区和其他区的AWEAwemeModel。发现AWEAwemeModel的某一个数据结构是这个样的

@interface AWEURLModel
@property(retain, nonatomic) NSArray *originURLList;
@end

@interface AWEVideoModel
@property(readonly, nonatomic) AWEURLModel *playURL;
@property(readonly, nonatomic) AWEURLModel *downloadURL;
@end

@interface AWEAwemeModel
@property(nonatomic, assign) BOOL preventDownload;
@property(retain, nonatomic) AWEVideoModel *video;
@end

一顿分析得到日区的downloadURL只有两个接口,不包含视频地址。其他能下载的地区,downloadURL有四个接口,前两个为视频地址。进一步发现playURLdownloadURL的参数一直。直接尝试将playURL赋值给downloadURL

代码编写:

// MARK: - AWEAwemeModel
CHDeclareClass(AWEAwemeModel)

CHMethod1(void, AWEAwemeModel, setPreventDownload, BOOL, arg1) {
    arg1 = ![UserDefaults boolForKey:HookDownLoad];
    CHSuper1(AWEAwemeModel, setPreventDownload, arg1);
}

CHMethod1(void, AWEAwemeModel, setVideo, AWEVideoModel *, arg1) {
    BOOL isHookDownLoad = [UserDefaults boolForKey:HookDownLoad];
    if (isHookDownLoad) {
        arg1.downloadURL.originURLList = arg1.playURL.originURLList;
    }
    CHSuper1(AWEAwemeModel, setVideo, arg1);
}

CHConstructor {
    CHLoadLateClass(AWEAwemeModel);
    CHHook1(AWEAwemeModel, setPreventDownload);
    CHHook1(AWEAwemeModel, setVideo);
}

再次运行,成功下载日区TikTok视频。


Question5

Q5:
视频down下来发现有水印?

T5:
对比原地址,发现原视频是没有水印的,那么水印就是在下载完成后添加了的。

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
TargetApp

目录搜索watermark,验证了猜想。
在头文件中,发现了带watermark名称的类。

TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!
WaterMark

最终发现AWEDynamicWaterMarkExporter这个类的+ (id)watermarkLogoImageArray;返回了对应的水印图片。

代码编写

#pragma mark WaterMark
CHDeclareClass(AWEDynamicWaterMarkExporter)
CHOptimizedClassMethod0(self, NSArray *, AWEDynamicWaterMarkExporter, watermarkLogoImageArray) {
    BOOL isHookWaterMark = [UserDefaults boolForKey:HookWaterMark];
    if (isHookWaterMark) {
        return @[];
    }
    return CHSuper0(AWEDynamicWaterMarkExporter, watermarkLogoImageArray);
}
CHConstructor {
    CHLoadLateClass(AWEDynamicWaterMarkExporter);
    CHClassHook0(AWEDynamicWaterMarkExporter, watermarkLogoImageArray);
}

总结

整个逆向过程中,完整的Hook代码并不复杂,开发工作也是站在巨人的肩膀上完成的,草草几行就能完成功能逆向。
他是令人振奋,因为最终证明了你的逆向想法是对的,通往成功的路不只有一条,切入点可能不一样,思路可能不一样,方法可能不一样,但是都能成功。

刷了几天日韩小姐姐之后,身体越来越差了。

iOS开发调试 – LLDB使用概览

前言


LLDB是个开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++或者Python插件。在日常的开发和调试过程中给开发人员带来了非常多的帮助。

(lldb)po std.name
Noskthing

了解并熟练掌握LLDB的使用是非常有必要的。这篇文章将会为大家总结日常高频使用的一些技巧。文章分节的主要依据是功能的相关性,并且省略了很多Xcode已经集成并且可视化的操作。

  • 一些基础
  • 断点相关
  • 参数检查
  • expression指令
  • hook的概念
  • 流程控制
  • script
  • image
  • register
  • 结语

一些基础


LLDB的基本语法如下

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]

其中内置了非常多的功能,选择去硬背每一条指令并不是一个明智的选择。我们只需要记住一些常用的指令,在需要的时候通过help命令来查看相关的描述即可。

(lldb)help
Debugger commands:

  apropos           -- List debugger commands related to a word or subject.
  breakpoint        -- Commands for operating on breakpoints (see 'help b' for shorthand.)
  ...

还可以通过apropos来获取具体命令的合法参数信息以及含义

(lldb) apropos breakpoint
The following commands may relate to 'breakpoint':
  _regexp-break                         -- Set a breakpoint using one of
                                           several shorthand formats.
  _regexp-tbreak                        -- Set a one-shot breakpoint using one
                                           of several shorthand formats.
  ...

断点相关


Xcode本身已经将大部分的操作用UI展示了出来,比如说

  • Breakpoint Navigator (⌘ + 7)
  • Debug Navigator (⌘ + 6)
  • Debug Area (⌘ + Shift + Y)
  • Debug menu item

    iOS开发调试 - LLDB使用概览
    断点列表

日常开发中大部分有关断点的操作我们都可以不使用命令行直接通过Xcode的可视化操作来实现,命令行的操作似乎是一种多余。但是使用(lldb)help breakpoint查看一下LLDB提供的所有帮助,你会发现在命令行中使用LLDB能够给予我们更多更详细的调试信息以及更广阔的操作空间。

(lldb)help breakpoint

举一个简单的例子,我们需要为某一个函数设置一个断点。比如说给ViewController的VviewDidLoad方法设置一个断点。这对于Xcode而言非常的简单。

iOS开发调试 - LLDB使用概览
添加断点

编辑每一个断点的各个选项也因为可视化的操作而变得非常的简单。但是如果我们需要在系统调用的某个函数里设置断点呢,抑或某个函数我们只能在crash log茫茫碌的堆栈信息里才能看到一点它的痕迹,这个时候如何操作呢?

iOS开发调试 - LLDB使用概览
crash log

假设我们现在需要给objc_msgSend函数设置断点。首先先想办法获取objc_msgSend的地址。我们在Appdelegate.m文件给函数- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions打一个断点,运行程序如下图所示。

iOS开发调试 - LLDB使用概览

我们可以通过(lldb)br set -a 0x0000000103c04ac0来为objc_msgSend()设置一个断点。输入continue继续执行你会发现如果程序再次调用objc_msgSend()会暂停。

iOS开发调试 - LLDB使用概览
断点设置

Tips
* 图片是用模拟器运行所以是x86,移动设备是arm。两者的指令有所不同。图中的callq指令对应arm中的bl
* 由于 ASLR(地址空间配置随机载入) 的原因地址是不固定的,所以图中objc_msgSend()的地址在你的机器上是不可用的。

断点相关的指令很多很杂,这里为大家列举一些常用的。如果以后遇到一些特殊的需求,可以借助help()指令来自行查找相关指令。

设置断点

  • 给所有名为xx的函数设置一个断点
(lldb)breakpoint set —name xx
(lldb)br s -n xx
(lldb)b xx
  • 在文件F指定行L设置断点
(lldb)breakpoint set —file F —line L
(lldb)br s -f F -l L
(lldb)b F:L
  • 给所有名为xx的C++函数设置一个断点(希望没有同名的C函数)
(lldb)breakpoint set —method xx
(lldb)br s -M xx
  • 给一个OC函数[objc msgSend:]设置一个断点
(lldb)breakpoint set —name “[objc msgSend:]”
(lldb)b -n “[objc msgSend:]”
  • 给所有名为xx的OC方法设置一个断点(希望没有名为xx的C或者C++函数)
(lldb)breakpoint set —selector xx
(lldb)br s -S count
  • 给所有函数名正则匹配成功的函数设置一个断点
(lldb)breakpoint set --func-regex regular-expression
(lldb)br s -r regular-expression
  • 给指定函数地址func_addr的位置设置一个断点
(lldb)br set -a func_addr

断点查看

(lldb)breakpoint list
(lldb)br l

断点删除

(lldb)breakpoint delete index
(lldb)br del index

index指明断点的序号,如果为空则删除所有断点

watchpoint

iOS开发当中有一个重要的概念KVO,我们会给一个重要的变量设置一个观察者,用以在它发生变化的时候做出相应的操作。在调试过程中我们也可以借助LLDB来监视某个变量或某一块内存的读写情况。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString * str = @"First";
    [self printString:str];
    str = @"Second";
    [self printString:str];
}

- (void)printString:(NSString *)str
{
    NSLog(@"%@",str);
}

我们利用watchpoint指令来监视变量str。需要重点说明的是-w选项,下例中并没有写出,缺省值是write,这意味着只有在str被写入的时候程序会暂停。

(lldb) watchpoint set variable str
Watchpoint created: Watchpoint 1: addr = 0x7fff5997f9e8 size = 8 state = enabled type = w
    declare @ '/Users/noskthing/Desktop/LLDBTest/LLDBTest/ViewController.m:22'
    watchpoint spec = 'str'
    new value: 0x0000000106280078
2017-07-22 17:35:13.534 LLDBTest[4585:521823] First

Watchpoint 1 hit:
old value: 0x0000000106280078
new value: 0x0000000106280098

(lldb) image lookup -a 0x0000000106280098
      Address: LLDBTest[0x0000000100003098] (LLDBTest.__DATA.__cfstring + 32)
      Summary: @"Second"
(lldb) image lookup -a 0x0000000106280078
      Address: LLDBTest[0x0000000100003078] (LLDBTest.__DATA.__cfstring + 0)
      Summary: @"First"

当你输入watchpoint list查看设置的watchpoint时系统会提示你当前测试的机器允许设置的最大个数。

(lldb) watchpoint list
Number of supported hardware watchpoints: 4
No watchpoints currently set.

参数检查


当我们调试程序遇到断点的时候Xcode会自动的将当前作用域下的局部变量以及全局变量展示出来

iOS开发调试 - LLDB使用概览
当前作用

借助命令行我们也能够轻松的获取这些参数的信息

  • 展示当前作用域下的参数和局部变量
(lldb)frame variable
(lldb)fr v
  • 展示当前作用域下的局部变量
(lldb)frame variable --no-args
(lldb)fr v -a
  • 展示指定变量var的具体内容
(lldb)frame variable *var*
(lldb)fr v *var*
(lldb)p *var*
  • 展示当前对象的全局变量
(lldb)target variable
(lldb)ta v

细心的朋友应该能够有所发现,这些操作都有一个局限:我们查看的各个变量都是当前作用域的。这意味着程序遇到断点的时候暂停,所有的操作都是局限于当前函数以及当前函数所在线程的内部。可视化的操作并没有给我们太多操作的空间,但是借助命令行我们可以打破这样一个局限。

命令行输入(lldb)thread backtrace可以获取当前线程函数的调用栈

(lldb)thread backtrace
* frame #0: 0x0000000100057204 test`-[ViewController viewDidLoad](self=0x000000014fe0ad10, _cmd=<unavailable>) at ViewController.m:99 [opt]
  frame #1: 0x000000018e1cfec0 UIKit`-[UIViewController loadViewIfRequired] + 1036
  frame #2: 0x000000018e1cfa9c UIKit`-[UIViewController view] + 28
  frame #3: 0x000000018e1d631c UIKit`-[UIWindow addRootViewControllerViewIfPossible] + 76
  frame #4: 0x000000018e1d37b8 UIKit`-[UIWindow _setHidden:forced:] + 272
  frame #5: 0x000000018e245224 UIKit`-[UIWindow makeKeyAndVisible] + 48

输入frame select指令我们可以任意的去选择一个作用域去查看。

(lldb)frame select 2

类比frame的操作我们可以轻松看出线程选择相关的操作

(lldb) thread list
Process 21035 stopped
* thread #1: tid = 0x27361a, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
  thread #2: tid = 0x273639, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #3: tid = 0x27363a, 0x00000001893c2ca8 libsystem_pthread.dylib`start_wqthread
  thread #4: tid = 0x27363e, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #5: tid = 0x27363f, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.uikit.eventfetch-thread'
  thread #6: tid = 0x273640, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #7: tid = 0x273641, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #8: tid = 0x273642, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #10: tid = 0x273646, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'com.apple.NSURLConnectionLoader'
  thread #11: tid = 0x273644, 0x00000001892df224 libsystem_kernel.dylib`mach_msg_trap + 8, name = 'AFNetworking'
  thread #12: tid = 0x27364a, 0x00000001892fda88 libsystem_kernel.dylib`__workq_kernreturn + 8
  thread #13: tid = 0x27364b, 0x00000001892fd23c libsystem_kernel.dylib`__select + 8, name = 'com.apple.CFSocket.private'
(lldb) thread select 2

以上提到的几个指令意味着借助命令行,我们可以在断点发生的时候跳转到当前存在的任一线程里的任一作用域去进行操作。

除却frame的操作,我们很多时候习惯借助NSLog去打印某些关键的信息。如果运行到一半的时候发现漏写了某个地方的NSLog,加入相关代码并重新运行也许不是一个让人省心的方法。我们可以在需要打印的地方设置一个断点,然后运行p object或者po object指令来查看指定对象。

(lldb) p userInfo
(__NSDictionaryM *) $0 = 0x0000000174242010 4 key/value pairs
(lldb) po userInfo
{
    macAddressString = "60:01:94:80:37:6c";
    payload =     {
        TimerAction = 0;
        TimerStat = 0;
        brightness = 70;
        colortemp = 93;
        remaining = "-1";
        switch = 1;
    };
    serialNumberString = 60019480376C;
    tcpPortString = "192.168.199.124";
}

两个指令实际都是expression指令的缩写。p打印的是当前对象的地址而po则会调用对象的description方法,做法和NSLog是一致的。

expression指令


expression命令是执行一个表达式,并将表达式返回的结果输出。包括上文提到的p指令在内,以下几个都是expression指令的别名。

(lldb)expression userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs 
(lldb) p userInfo
(__NSDictionaryM *) $2 = 0x0000000174242010 4 key/value pairs
(lldb) print userInfo
(__NSDictionaryM *) $3 = 0x0000000174242010 4 key/value pairs
(lldb) e userInfo
(__NSDictionaryM *) $4 = 0x0000000174242010 4 key/value pairs
(lldb) call userInfo
(__NSDictionaryM *) $5 = 0x0000000174242010 4 key/value pairs

打印对象的时候我们也可以指定特定格式,详细的格式查阅参见这里。

(lldb) p 16
16
(lldb)p/x 16
0x10
(lldb) p/t 16
0b00000000000000000000000000010000
(lldb) p/t (char)16
0b00010000

但是expression指令真正强大的部分应该是它的写入能力。我们可以通过expression来执行一个表达式动态的修改我们程序中变量的值。

(lldb) p count
(NSUInteger) $4 = 12
(lldb)e count = 42
(lldb) p count
(NSUInteger) $5 = 42

在断点处我们首先打印count变量的值,之后通过执行expression指令来修改count变量,再次打印可以发现此时count已经被修改。这对于调试时模拟一些极端情况非常的有帮助。这里有一个特殊一点的情况需要指明,如果你尝试通过expression来修改UI可能会失效。

(lldb)expression -- self.view.backgroundColor = [UIColor redColor]

因为执行断点会打断更新UI的进程导致你的修改没有及时渲染出来,执行flush命令可以让机器渲染出你修改后的界面。

实际上一些复杂的调试操作单单靠每次命令行去手动输入指令是非常的繁琐的,仅仅依靠单条指令和它提供的参数选项在一些针对界面的调试上并不能给予我们足够多的支持。令人兴奋的是facebook开源的Chisel为我们提供了更多实用的功能。整个开源库是用Python实现的,基于LLDB 内建的,完整的 Python 支持。这一部分我们后面聊到script指令再细细探讨。

hook的概念


hook翻译成中文是钩子的意思。这个名词在我从事iOS开发的过程中确实没有太多的接触,第一次碰到是在学习Flask框架时遇到的请求钩子。我并不觉得钩子的中文翻译对于我们理解有所帮助,在我初学的阶段甚至给我产生了一定的误解,所以我后续还是以hook来描述。

简单来说hook一个处理消息的程序段,通过系统调用,把它挂入系统。每当特定的消息发出,在没有到达目的窗口前,钩子程序就先捕获该消息,此时hook函数先得到控制权。这时hook函数即可以加工处理(改变)该消息,也可以不作处理而继续传递该消息,还可以强制结束消息的传递。这意味着借助hook函数我们可以在指定在某些特殊的情况下做出一些包括但不限于参数验证,消息拦截等操作来查验当前情况和修改后续程序的运行。

在LLDB中常见的操作有以下这些。本身指令并不复杂,但配合上其它的指令确实在某些情况下能节省我们很多的精力。

  • 设置一个stop-hook用以在每次断点被触发时执行
(lldb)target stop-hook add --one-liner stop-hook
  • 设置一个stop-hook用以在指定函数func内的断点被触发时执行
(lldb) target stop-hook add --name func --one-liner stop-hook
  • 设置一个stop-hook用以在名为className的C类的断点被触发时执行
(lldb)target stop-hook add -- className MyClass --one-liner stop-hook
iOS开发调试 - LLDB使用概览
stop-hook示例

流程控制


Xcode已经为我们提供了可视化的工具,但是如果你习惯了命令行操作不希望双手离开键盘降低你的效率,了解一下也是很有帮助的。

iOS开发调试 - LLDB使用概览
流程控制
  • 继续
(lldb)process continue
(lldb)continue
(lldb)c
  • 下一步
(lldb)thread step-over
(lldb)next
(lldb)n
  • 进入
(lldb)thread step-in
(lldb)step
(lldb)s
  • 跳出
(lldb)thread step-out
(lldb) finish
(lldb)f

除此以外我们还可以通过Thread return来控制流程。该指令有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。当然这也可能会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方一个方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    if ([self isEvenNumber:2])
    {
        NSLog(@"First");
    }
    else
    {
        NSLog(@"Second");
    }
}

- (BOOL)isEvenNumber:(NSInteger)num
{
    if (num % 2 == 0)
    {
        return YES;
    }
    else
    {
        return NO;
    }
}

我们在isEvenNumber:函数中设置断点利用thread return函数返回NO。

(lldb) thread return NO
(lldb) c
Process 4784 resuming
2017-07-22 18:48:14.654 LLDBTest[4784:569378] Second

script


LLDB 有内建的,完整的 Python支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令的参数,这可以运行 python 语句而不进入REPL

(lldb) script print 'Hello World'
Hello World

借助LLDB提供的Python API我们可以实现很多复杂的功能。这里列举一个简单的例子,将以下内容写入~/myCommands.py文件

def caflushCommand(debugger, command, result, internal_dict): 
    debugger.HandleCommand("e (void)[CATransaction flush]")

在LLDB中执行

command script import ~/myCommands.py

或者将这条指令写入~ /.lldbinit中,每次进入LLDB都会自动执行这些函数。

如果没有~ /.lldbinit 终端执行touch ~ /.lldbinit生成文件
你可以在这里提前设置好一些指令,然后disable。调试过程中再设置enable打开。相信经过整理之后LLDB会让你的调试如鱼得水。

Facebook开源的Chisel就是基于此实现。我们通过brew安装Chisel

brew install Chisel
iOS开发调试 - LLDB使用概览
chise文件层次

fblldbbase.py文件中定义了各个基础类,fblldb.py负责遍历commands文件夹里的各个类来加载自定义的指令。在Chisel基础上我们也可以轻松的自定义指令。在commands文件夹内新建py文件,实现函数lldbcommands返回一个数组,包含对象的类都是FBCommand的子类。

def lldbcommands():
  return [
    FBPrintAccessibilityLabels()
  ]

class FBPrintAccessibilityLabels(fb.FBCommand):
  def name(self):
    return 'pa11y'

  def description(self):
    return 'Print accessibility labels of all views in hierarchy of <aView>'

  def args(self):
    return [ fb.FBCommandArgument(arg='aView', type='UIView*', help='The view to print the hierarchy of.', default='(id)[[UIApplication sharedApplication] keyWindow]') ]

  def run(self, arguments, options):
    forceStartAccessibilityServer();
    printAccessibilityHierarchy(arguments[0])

每一个类都继承自FBCommand,我们需要分别复写以下几个函数

  • def name()
    返回一个字符串表示指令的名称
  • def description()
    返回一个字符串表示指令的描述
  • def args()
    返回一个数组,其中的对象都是类FBCommandArgument构建的,每一个对象表示一个指令的选项参数。
  • def run()
    指令的具体操作

image


image指令是target module指令的缩写,借助它我们能够查看当前的Binary Images相关的信息。日常开发我们主要利用它寻址。

在日常开发的过程中,我们会收集到用户各式各样的crash log。log中会为我们提供崩溃前函数栈的运行情况,每一个函数都会对应一个函数地址。

iOS开发调试 - LLDB使用概览
crash log

要解决问题首先我们需要确定的是程序最后调用了什么函数。由于ALSR的原因crash log中的函数地址我们不能够直接的去使用,我们需要在测试的机器上自己去计算出对应的函数地址。一般情况下crash log中会附带一个Binary Images。我们要利用这个来计算出每一个函数地址相对于所在框架的偏移量。

iOS开发调试 - LLDB使用概览
Binary Images

之后利用image指令来查看本机的Binary Images

(lldb) image list
[  0] 48EA38EC-6E36-3E77-A680-A4D04D3D3868 0x00000001014ac000 /Users/noskthing/Library/Developer/Xcode/DerivedData/LLDBTest-dfmaxwkizubjskftkbnlfzumauje/Build/Products/Debug-iphonesimulator/LLDBTest.app/LLDBTest 
[  1] 322C06B7-8878-311D-888C-C8FD2CA96FF3 0x0000000107c66000 /usr/lib/dyld 
[  2] 14AD0238-D077-378B-82A8-AC2D2ADC9DDF 0x00000001014b4000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk/usr/lib/dyld_sim 
[  3] 61CD1144-BB93-3571-BDB3-9F9B56CECFFE 0x0000000101543000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//System/Library/Frameworks/Foundation.framework/Foundation 
[  4] 5F0E622C-86EC-3969-ACFB-CAAA10E21A31 0x0000000101a76000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk//usr/lib/libobjc.A.dylib 

有了本机的Binary Images我们就可以通过之前计算出的偏移量来获取本机对应函数的地址。通过image lookup指令查找对应地址的函数就可以确定崩溃前究竟执行了哪些函数

(lldb) image lookup -a 0x1025dd00a
      Address: UIKit[0x00000000001cb00a] (UIKit.__TEXT.__text + 1869978)
      Summary: UIKit`-[UIViewController loadViewIfRequired] + 1219

register


register指令能够获取和修改各个寄存器的信息。

我们需要明白一个典型的CPU是由运算器、控制器、寄存器等器件构成的,而寄存器进行的就是信息存储。我们利用汇编语言来操作寄存器。

iOS开发调试 - LLDB使用概览
汇编

这里是苹果官方文档,介绍的是armv6。需要注意的是自从iPhone 5s之后已经全部换到64-bit,在arm64下整数寄存器的个数已经增加到31个。我们可以通过register read来进行查看。

iOS开发调试 - LLDB使用概览
register

其中x0-x7八个寄存器是用来保存参数的。objc_msgSend会有两个默认参数,这也就意味着x0保存的是self,x1保存的是_cmd。fr对应frame point,lr对应link point,在汇编中分别为x29,x30。最近正在准备一篇从汇编的层面分析objc_msgSend的文章,会在那里结合官方文档详细介绍包括函数调用过程以及各个寄存器的作用。有兴趣的朋友可以先关注一下作者:)

利用runtime动态调用Objective-C任意对象的任意方法,需要为NSInvocation设置参数。参数的index就是从2开始的。具体的实现可以参考Github-Tools中的NSObject+Runtime这个Category的实现。

虽然我们更多的时候只是借助read指令来获取一下当前各个寄存器的信息,但是对于一些替换参数,模拟特殊输入的需求,write指令也是非常的有帮助。

实现一个简单的例子。

- (void)viewDidLoad {
    [super viewDidLoad];

    NSString * str = [NSString stringWithFormat:@"First"];
    NSString * str1 = [NSString stringWithFormat:@"Second"];
    [self printString:str];
    [self printString:str1];
}

- (void)printString:(NSString *)str
{
    NSLog(@"%@",str);
}

函数非常的简单,会依次打印出First和Second。我们首先在第一次调用printString:之前打印一个断点,调用frame variable来查看一下当前两个参数的地址.

(lldb) frame variable
(ViewController *) self = 0x000000010090ac30
(SEL) _cmd = <variable not available>

(NSTaggedPointerString *) str = 0xa000074737269465 @"First"
(NSTaggedPointerString *) str1 = 0xa00646e6f6365536 @"Second"

如果你的参数是unused编译器会把它优化掉,这样你就无法获取它的地址。注意NSString的创建方式,字符串常量创建会把str分配到常量区,查看参数会得到<variable not available>的提示。

之后在printString:中的NSLog之前设置一个断点,continue。在printString:中遇到断点的时候我们执行register read指令。

(lldb) register read
General Purpose Registers:
        x0 = 0x000000010090ac30
        x1 = 0x000000010000995f  "printString:"
        x2 = 0xa000074737269465
        x3 = 0x000000016fdfd876
        x4 = 0x0000000000000000
        x5 = 0x0000000000000000
        x6 = 0x0000000000000064
        x7 = 0x0000000000000000
        x8 = 0x00000001ae36bc20  libsystem_pthread.dylib`_thread + 224
        x9 = 0x00000001ae364fec  runtimeLock + 28
       x10 = 0x00000001ae364ff0  runtimeLock + 32
       x11 = 0x003c6d01003c6d80
       x12 = 0x0000000000000000
       x13 = 0x00000000003c6d00
       x14 = 0x00000000003c6e00
       x15 = 0x00000000003c6dc0
       x16 = 0x00000000003c6d01
       x17 = 0x0000000100007250  test`-[ViewController printString:] at ViewController.m:103
       x18 = 0x0000000000000000
       x19 = 0x000000010090ac30
       x20 = 0xa00646e6f6365536
       x21 = 0xa000074737269465
       x22 = 0x000000010000995f  "printString:"
       x23 = 0x0000000000000000
       x24 = 0x0000000000000010
       x25 = 0x0000000000000258
       x26 = 0x000000018ed0e90e  "window"
       x27 = 0x0000000000000001
       x28 = 0x0000000000000000
        fp = 0x000000016fdfddc0
        lr = 0x000000010000721c  test`-[ViewController viewDidLoad] + 156 at ViewController.m:100
        sp = 0x000000016fdfddb0
        pc = 0x000000010000725c  test`-[ViewController printString:] + 12 at ViewController.m:106
      cpsr = 0x60000000

对比地址可以发现x0保存的是viewController的地址,x1注明了是函数printString:的地址,而x2就是str的地址。我们通过register write指令来修改x2的值。

(lldb) register write x2 0xa00646e6f6365536

contine之后你会发现打印出的不是First而是Second。

如果有朋友对汇编和函数调用感兴趣,我会在之后结合objc_msgSend汇编部分的代码在另一篇文章里来做个介绍。

结语


文章的目的是希望给大家展示LLDB强大的能力以及命令行的优点,但实际以上篇幅介绍的只是冰山一角。希望这篇文章能够给大家一些帮助,来更多的了解LLDB。

以下是一些有关LLDB的资料和文档

  • LLDB Documentation
  • Dancing in the Debugger — A Waltz with LLDB
  • WWDC 2012: Debugging with LLDB
  • WWDC 2014: Advanced Swift debugging in LLDB
  • WWDC 2014: Introduction to LLDB and the Swift REPL
  • WWDC 2015: What’s new in LLDB
    希望能够对你有所帮助

Hook中一些常用命令


Debugserve

root# debugserver *:1234 -a “ProcessName”
lldb
process connect connect://20.20.49.195:1234


查看是否加密

otool -l WeChat.app/WeChat | grep -B 2 crypt


导出头文件

class-dump –arch armv7 -H -A -S -o header WeChat.app/WeChat

TargetApp的Documents目录路径

root# cycript -p TargetApp
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0]
/#”file:///var/mobile/Containers/Data/Application/D41C4343-63AA-4BFF-904B-2146128611EE/Documents/”


lipo

// 拆分fat
 lipo LoginSDK.a -thin armv7 -output arm/LoginSDK.a
// 合并iPhone模拟器和真机的静态类库,生成通用库
lipo -create -output UNIVERSAL.a   DEVICE.a   SIMULATOR.a

// 意思是:把"${CURRENTCONFIG_DEVICE_DIR}目录下的.a文件,和${CURRENTCONFIG_SIMULATOR_DIR}目录下的.a文件合并,
// 在${CREATING_UNIVERSAL_DIR}目录下,生成两个设备都通用的静态库,

例如:lipo -create -output xy.a x.a y.a

添加权限

chmod +x /usr/bin/debugserver


应用砸壳 1

ssh root@ip
ps -e
找到var/开头 进程
cycript -p 进程号
cy# [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask]
cd 上面得到的路径
su mobile
DYLD_INSERT_LIBRARIES=/usr/bin/dumpdecrypted.dylib /path/to/executable(进程路径)
scp root@xxx.xxx.xxx.xxx:/path/to/XXX.decrypted /path/to/XXX.decrypted(复制出砸壳后的二进制文件)

应用砸壳 2


给app注入dylib后安装到非越狱手机上

  • 应用砸壳

  • 安装iOSOpenDev

  • 创建dylib,编写程序

  • 选择真机bulid

  • 利用yololib把dylib注入到砸壳后的二进制文件中

    ./yololib WeChat.app/WeChat hook.dylib
    
  • 将注入dylib的二进制文件cp到app(从AppStore下载的)中

  • 利用AppResign或者codesign给app重签名

    ./AppResign WeChat.app WeChat.ipa
    
  • 安装ipa


xcodebuild编译framework

 xcodebuild -workspace FLEX.xcworkspace -scheme FLEX -configuration Release -arch arm64 BUILD_DIR=./
// iphoneos5.0下的编译脚本:
xcodebuild -project "UtilLib.xcodeproj" -configuration "Debug" -target "UtilLib" -sdk "iphoneos5.0" -arch "armv6 armv7" build RUN_CLANG_STATIC_ANALYZER=NO  $(BUILD_DIR)="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}"

// iphonesimulator5.0下的编译脚本:
xcodebuild -project "UtilLib.xcodeproj" -configuration "Debug" -target "UtilLib" -sdk "iphonesimulator5.0" -arch "i386" build RUN_CLANG_STATIC_ANALYZER=NO $(BUILD_DIR)="${BUILD_DIR}"  BUILD_ROOT="${BUILD_ROOT}"
参考:

xcodebuild命令简单使用
xcodebuild命令官方说明
IOS 项目配置–构建输出DIR


scp: ambiguous target

 scp myfile.txt root@192.168.1.100:"/file\ path\ with\ spaces/myfile.txt"

scp copy to external hard drive ambiguous target


其它

  1. 查看调用当前的模块:image lookup -a $lr
  2. 查看在hopper中的函数地址:image lookup -a 函数地址
  3. 下该类所有方法下断点:br set -r [CNAdPlayerView .*]
  4. 给某一个方法下断点:br set -n “[UIView initWithFrame:]”
  5. ASLR偏移量(LLDB): image list -o -f
  6. codesign -d –entitlements :app.entitlements 需要重签.app #生成原包的授权文件

反ptrace反调试

一、前言

上次学习到ptrace反调试,我是将反调试的代码放在主程序的main函数内部,最近学习了一下dyld加载的流程,了解到了main函数之前还发生了非常多的事情,所以感觉反调试代码放在main函数内安全度不够,很容易被hook,因为在main函数之前就有一步是加载依赖库、动态库并链接到主程序,结合之前学习到的动态库注入,思考能否通过注入自己的动态库来hook住ptrace函数进行反反调试。

二、准备反调试应用

这里我使用上次编写的反调试Demo,并且打包成ipa,再用重签名脚本来动态调试该ipa包,如下:

反ptrace反调试
Xcode动态调试

使用Xcode重签名该ipa并且运行调试,可以发现运行后会立即退出并且中断调试。

反ptrace反调试
AntiDebug

上图最后两个app是同一ipa安装的结果,后者是重签名安装的,能够在正常打开运行,但是在Xcode上运行调试就会闪退,确实是达到了反调试(防止他人重签名调试我们的app)的效果。下面我们尝试着动态库注入来hook住ptrace,干掉反调试。

三、反反调试

流程大概分为:

  • 重签名ipa
  • 动态库编写hook代码
  • 注入动态库

重签名跟注入动态库在这里不再赘述,可以查看之前的笔记,我们主要工作是针对ptracedlsym进行hook,这里使用的是fishhook,具体如下:

//保存原函数地址
static int (*orig_ptrace)(int , pid_t , caddr_t , int ) = NULL;
static void* (*orig_dlsym)(void * __handle, const char* __symbol);

int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data) {
    if (_request != PT_DENY_ATTACH) {
        return orig_ptrace(_request,_pid,_addr,_data);
    }
    NSLog(@"源程序做了反调试----已hook!");
    return 0;
}

void* my_dlsym(void * __handle, const char* __symbol) {
    if (strcmp(__symbol, "ptrace") != 0) {
        //如果不是"ptrace"符号
        return orig_dlsym(__handle,__symbol);
    }
    //dlsym调用ptrace,走我们的hook ptrace
    return my_ptrace;
}

+ (void)load {
    //使用fishhook
    struct rebinding ptraceRebind;
    //需要hook的函数
    ptraceRebind.name = "ptrace";
    //传入替换函数地址
    ptraceRebind.replacement = my_ptrace;
    //保存原函数调用地址
    ptraceRebind.replaced = (void *)&orig_ptrace;
    
    struct rebinding dlsym;
    dlsym.name = "dlsym";
    dlsym.replacement = my_dlsym;
    dlsym.replaced = (void *)&orig_dlsym;
    
    //重新绑定符号表
    rebind_symbols((struct rebinding[2]){ptraceRebind,dlsym}, 2);
}

先注释掉hook部分的代码,添加符号断点ptrace,运行重签名工程,如下:

反ptrace反调试
先不hook

从下图可以看到,ptrace系统调用被断住,可以知道源程序做了ptrace反调试,这是一种检测方式。

反ptrace反调试
ptrace被断住

接着将hook代码注释去掉,让其绕过反调试的检测

反ptrace反调试
成功反反调试

Demo

四、小结

通过上述这种方式的反反调试,一般能绕过写在main函数里面的ptrace,整个流程可以从dyld加载的顺序解释,dyld先加载程序需要的依赖库、动态库,其中包括我们注入的动态库,因此在加载动态库的时候我们的hook方法比程序main函数里的ptrace反调试更早执行,所以能够绕过。但是,如果原应用把ptrace反调试写在动态库里,我们通过这种方式反反调试就会失效,先留个思考点。

ios 逆向 — 反调试 和 反反调试

一: ptrace 作用

ptrace系统调从名字上看是用于进程跟踪的,它提供了父进程可以观察和控制其子进程执行的能力,并允许父进程检查和替换子进程的内核镜像(包 括寄存器)的值。其基本原理是: 当使用了ptrace跟踪后,所有发送给被跟踪的子进程的信号(除了SIGKILL),都会被转发给父进程,而子进程则会被阻塞,这时子进程的状态就会被 系统标注为TASK_TRACED。而父进程收到信号后,就可以对停止下来的子进程进行检查和修改,然后让子进程继续运行 ,因而可以实现断点调试和系统调用的跟踪

使用ptrace,你可以在用户层 【拦截和修改】系统调用(sys call)

注意:

被跟踪的程序在进入或者退出某次系统调用的时候都会触发一个SIGTRAP信号,而被父进程捕获

Ptrace其原型为:

include <sys/ptrace.h>

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

ptrace有四个参数:

1). enum __ptrace_request request:指示了ptrace要执行的命令。

2). pid_t pid: 指示ptrace要跟踪的进程。

3). void *addr: 指示要监控的内存地址。

4). void *data: 存放读取出的或者要写入的数据。

ptrace是如此的强大,以至于有很多大家所常用的工具都基于ptrace来实现,如strace和gdb

反调试举例说明:

比如: ptrace的命令PT_DENY_ATTACH 是苹果增加的一个 ptrace 选项,用于阻止 GDB 等调试器依附到某进程,用法如下:

ptrace(PT_DENY_ATTACH, 0, 0, 0);

void anti_gdb_debug() {

    void *handle = dlopen(0, RTLD_GLOBAL | RTLD_NOW);

    ptrace_ptr_t ptrace_ptr = dlsym(handle, "ptrace");

    ptrace_ptr(PT_DENY_ATTACH, 0, 0, 0);

    dlclose(handle);

}

总结一下:ptrace被广泛用于反调试,因为一个进程只能被ptrace一次,如果事先调用了ptrace方法,那就可以防止别人调试我们的程序.

也就是说谁先调用ptrace 谁说了算,如果我们直接在app 中写ptrace , 那么调试的时候肯定就无法调试,因为被应用内的ptrace 抢占了

反反调试: 如果别人的的app进行了ptrace防护,那么你怎么让他的ptrace不起作用,进而调试其他的app。由于ptrace是系统函数,那么我们可以用fishhook来hook住ptrace函数,然后让他的app调用我们自己的ptrace函数,即写动态库,如果多个动态库hook 了ptrace ,我们可以调整 Link Binary Libraries的顺序加载,假设人家应用自己写的hook ptrace动态库肯定会在自己前面,最后的方式我们可以通过修改macho的二进制让他的ptrace失效【不去执行ptrace】,然后进行调试.
最后:在给一种反调试的方案,这种也只能无法断点调试ptrace 函数

我不想暴露自己的ptrace等系统方法,不想被符号断点断住,可以采用汇编进行调用ptrace

ios 逆向 -- 反调试 和 反反调试
image.png

这样人家就很难通过断点的方式去调试

调试器建立调试关系的两种方式:

用gdb调试程序[调试程序也在一个进程里],可以直接gdb ./test,也可以gdb (test的进程号)。这对应着使用ptrace建立跟踪关系的两种方式:

  • fork:利用fork+execve执行被测试的程序,子进程在执行execve之前调用ptrace(PTRACE_TRACEME),建立了与父进程(debugger 调试程序进程)的跟踪关系。

  • attach: debugger可以调用ptrace(PTRACE_ATTACH,pid,…),建立自己与进程号为pid的进程间的跟踪关系。即利用PTRACE_ATTACH,使自己变成被调试程序的父进程(用ps可以看到)。用attach建立起来的跟踪关系,可以调用ptrace(PTRACE_DETACH,pid,…)来解除。注意attach进程时的权限问题,如一个非root权限的进程是不能attach到一个root进程上的。

第一种方式的例子:

ptrace提供了对子进程进行单步的功能,ptrace(PTRACE_SINGLESTEP, …) 会使内核在子进程的每一条指令执行前先将其阻塞,然后将控制权交给父进程

而父进程此时会使用 wait函数等待阻塞信号,然后判断status变量来检查子进程是被ptrace暂停掉还是已经运行结束并退出,如果状态是ptrace暂停的,则可以获取子进程的寄存器器状态,

ptrace(PTRACE_GETREGS,child, NULL, ®s),获取当前指令等,在让ptrace控制单步执行
ptrace(PTRACE_SINGLESTEP, child,NULL, NULL);

每一步都去唤醒子进程继续执行,并告诉内核在执行一条指令后就将其阻塞

最后让子进程恢复

PTRACE_SYSCALL:继续,但在下一个系统调用入口或出口处停止。

二: sysctl 作用

sysctl命令被用于在内核运行时动态地修改内核的运行参数

函数原型

int sysctl (int *name, int nlen, void *oldval, size_t *oldlenp, void *newval, size_t newlen);

Name /* 整形数组,每个数组元素代表系统参数存取路径上的一个文件或目录名,例如/proc/sys/kernel用CTL_KERN表示*/

oldval /* 当读取系统参数时,用于存取系统参数值,也就是/proc/sys/下的某个文件内容*/

Newval /* 当写系统参数时,记录所要写入的新值*/

反调试举例:

当一个进程被调试的时候,该进程会有一个标记来标记自己正在被调试,所以可以通过sysctl去查看当前进程的信息,看有没有这个标记位即可检查当前调试状态。

ios 逆向 -- 反调试 和 反反调试
image.png

检测到调试器就退出,或者制造崩溃,或者隐藏工程啥的,当然也可以定时去查看有没有这个标记

三: syscall 作用

为从实现从用户态切换到内核态,系统提供了一个系统调用函数syscall ,所有的系统调用都可以通过syscall 去实现

比如: syscall (26,31,0,0) 来调用系统函数ptrace,ptrace的系统调用函数号是26

syscall是通过软中断来实现从用户态到内核态,也可以通过汇编svc调用来实现。

ios 逆向 -- 反调试 和 反反调试
image.png

比如:arm 32位 #80 就是软中断值,r12 存放系统函数编号,arm 64位 #128 是系统中断码,x0存放系统函数编号

四: 重签名防护

想自己的app不被重签名,可以在代码中检测签名信息

查看证书的application-identifier 查看embedded.mobileprovision信息security cms -D -i embedded.mobileprovision 找到<key>application-identifier</key>的value的第一部分就是

在执行代码的时候检查签名是否和我们已知的签名对比

void checkCodesign(NSString *id){
 // 描述文件路径
 NSString *embeddedPath = [[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
 // 读取application-identifier 注意描述文件的编码要使用:NSASCIIStringEncoding
 NSString *embeddedProvisioning = [NSString stringWithContentsOfFile:embeddedPath encoding:NSASCIIStringEncoding error:nil];
 NSArray *embeddedProvisioningLines = [embeddedProvisioning componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]];
  
 for (int i = 0; i < embeddedProvisioningLines.count; i++) {
 if ([embeddedProvisioningLines[i] rangeOfString:@"application-identifier"].location != NSNotFound) {
   
  NSInteger fromPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"<string>"].location+8;
   
  NSInteger toPosition = [embeddedProvisioningLines[i+1] rangeOfString:@"</string>"].location;
   
  NSRange range;
  range.location = fromPosition;
  range.length = toPosition - fromPosition;
   
  NSString *fullIdentifier = [embeddedProvisioningLines[i+1] substringWithRange:range];
  NSArray *identifierComponents = [fullIdentifier componentsSeparatedByString:@"."];
  NSString *appIdentifier = [identifierComponents firstObject];
  
  // 对比签名ID
  if (![appIdentifier isEqual:id]) {
  //exit
  asm(
   "mov X0,#0n"
   "mov w16,#1n"
   "svc #0x80"
   );
  }
  break;
 }
 }
}

五:反反调试

这里主要针对ptrace、sysctl、syscall来反反调试,做法就很简单了,hook函数

比如:

1: hook ptrace 函数, 遇到request = 31, 就知道程序进行了反调试,所以我们可以将request 改掉

ios 逆向 -- 反调试 和 反反调试
image.png

2:hook sysctl , 看其是否在检查进程被追踪的这个标记TASK_TRACED,我们将其返回信息info_ptr -> kp_proc.p_flag 改掉,让其检查的结果是没有设置追踪标识

ios 逆向 -- 反调试 和 反反调试
image.png

3 hook syscall , 防止ptrace 是通过syscall 的方式去调用的,

ios 逆向 -- 反调试 和 反反调试
image.png

4:hook dlsym 防止通过这种方式去调用ptrace 函数

ios 逆向 -- 反调试 和 反反调试
image.png

5 初始化函数

ios 逆向 -- 反调试 和 反反调试
image.png

或者使用fishhook

ios 逆向 -- 反调试 和 反反调试
image.png

lldb 反反调试的

通过lldb下断点,然后修改参数,或者直接返回也可以达到反反调试的效果

为了方便直接使用facebook的chisel来增加脚本。

ios 逆向 -- 反调试 和 反反调试
image.png
ios 逆向 -- 反调试 和 反反调试
image.png
ios 逆向 -- 反调试 和 反反调试
image.png

当遇到情况为$x0 == 31 时发生回掉,将x0 或者r0 的值改成0

ios 逆向 -- 反调试 和 反反调试
image.png

六: App的防护

1:首先加强现有的密码检测机制的监测力度,除了密码长度,数字、字符甚至特殊符号的混杂程度,本文着重对易受攻击的键盘上特定组合码进行检测;

2:其次完善iOS内存保护机制,利用Objective-C对象实现内存安全擦除,保证及时对文件数据的每个字节都做到全覆盖,防止对象被跟踪后信息遭到泄露;

3:再次在维护程序在运行时的安全性上提出了一种贯穿程序被调试的三个阶段的反调试机制:从程序开始被调试、继续被跟踪到最终被恶意修改均进行跟踪测试,最终阻止被恶意修改的目标继续的执行

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

  • 防护

    • ObjC类名方法名等重命名为难以理解的字符

    • 加密静态字符串运行时解密

    • 混淆代码使其难于反汇编

    • 本地存储文件防篡改

  • 检测

    • 调试状态检测 : 反调试 ptrace . sysctl

    • 越狱环境检测

    • ObjC的Swizzle检测

    • 任意函数的hook检测

    • 指定区域或数据段的校验和检测

  • 自修复

    • 自修复被篡改的数据和代码段

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

1:越狱检测的方法:

   1》使用NSFileManager判断设备是否安装了如下越狱常用工具

       /Applications/Cydia.app

       /Library/MobileSubstrate/MobileSubstrate.dylib

       /bin/bash

        /usr/sbin/sshd

        /etc/apt

      这种方式不要写成Bool 方式去检查 ,容易被攻击者hook

        ![image.png](https://upload-images.jianshu.io/upload_images/1974361-6b0840859fa3ba63.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

   2》可以尝试打开cydia应用注册的URL scheme,后面应该是你知道某个应用URL scheme

        if([[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:@"[cydia://package/com.example.package](cydia://package/com.example.package)"]]){

             NSLog(@"Device is jailbroken");

        }

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

   3》你可以尝试读取下应用列表,看看有无权限获取:
ios 逆向 -- 反调试 和 反反调试
image.png
   攻击者可能会hook NSFileManager 的方法,让你的想法不能如愿

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

        ![image.png](https://upload-images.jianshu.io/upload_images/1974361-cfbf09cba56d9648.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

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

   5》你可以看看stat是不是出自系统库,有没有被攻击者换掉
ios 逆向 -- 反调试 和 反反调试
image.png

使用dladdr方法可以获得一个函数所在的模块.从而判断该函数是否被替换掉

如果结果不是 /usr/lib/system/libsystem_kernel.dylib 的话,那就100%被攻击了。

如果 libsystem_kernel.dylib 都是被攻击者替换掉的…

也可以判断ios 的方法在什么库中,通过该方法验证指定类的方法是否都来自指定模块

ios 逆向 -- 反调试 和 反反调试
image.png

建议使用inline方式编译,像这样以内联函数的形式编译,攻击者必须修改每一处调用该函数的的地方

检查所有方法判断是否来之某个模块

ios 逆向 -- 反调试 和 反反调试
image.png
    6》检索一下自己的应用程序是否被链接了异常动态库,列出所有已链接的动态库:
ios 逆向 -- 反调试 和 反反调试
image.png

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

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

       7》可以通过检测当前程序运行的环境变量:
ios 逆向 -- 反调试 和 反反调试
image.png

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

上述越狱检查总结如下:

  • 不要用NSFileManager,这是最容易被hook掉的。

  • 检测方法中所用到的函数尽可能用底层的C,如文件检测用stat函数(iPod7.0,越狱机检测越狱常见的会安装的文件只能检测到此步骤,下面的检测不出来)

  • 再进一步,就是检测stat是否出自系统库

  • 再进一步,就是检测链接动态库(尽量不要,appStore可能审核不过)

  • 再进一步,检测程序运行的环境变量

即使这样还是不能完全检查

比如: 用户可能安装越狱检测绕过插件(xCon),对于越狱检测,很大程度上都还是针对某些目录下某个文件名字是否换了或者文件被替换了等等去检测;

检测代码
- (BOOL)mgjpf_isJailbroken

{

    //以下检测的过程是越往下,越狱越高级
   // /Applications/Cydia.app, /privte/var/stash

    BOOL jailbroken = NO;

    NSString *cydiaPath = @"/Applications/Cydia.app";

    NSString *aptPath = @"/private/var/lib/apt/";

    if ([[NSFileManager defaultManager] fileExistsAtPath:cydiaPath]) {

        jailbroken = YES;

    }

    if ([[NSFileManager defaultManager] fileExistsAtPath:aptPath]) {

        jailbroken = YES;

    }

    //可能存在hook了NSFileManager方法,此处用底层C stat去检测

    struct stat stat_info;

    if (0 == stat("/Library/MobileSubstrate/MobileSubstrate.dylib", &stat_info)) {

        jailbroken = YES;

    }

    if (0 == stat("/Applications/Cydia.app", &stat_info)) {

        jailbroken = YES;

    }

    if (0 == stat("/var/lib/cydia/", &stat_info)) {

        jailbroken = YES;

    }

    if (0 == stat("/var/cache/apt", &stat_info)) {

        jailbroken = YES;

    }

//    /Library/MobileSubstrate/MobileSubstrate.dylib 最重要的越狱文件,几乎所有的越狱机都会安装MobileSubstrate

//    /Applications/Cydia.app/ /var/lib/cydia/绝大多数越狱机都会安装

//    /var/cache/apt /var/lib/apt /etc/apt

//    /bin/bash /bin/sh

//    /usr/sbin/sshd /usr/libexec/ssh-keysign /etc/ssh/sshd_config

    //可能存在stat也被hook了,可以看stat是不是出自系统库,有没有被攻击者换掉

    //这种情况出现的可能性很小

    int ret;

    Dl_info dylib_info;

    int (*func_stat)(const char *,struct stat *) = stat;

    if ((ret = dladdr(func_stat, &dylib_info))) {

        NSLog(@"lib:%s",dylib_info.dli_fname);      //如果不是系统库,肯定被攻击了

        if (strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib")) {   //不相等,肯定被攻击了,相等为0

            jailbroken = YES;

        }

    }

    //还可以检测链接动态库,看下是否被链接了异常动态库,但是此方法存在appStore审核不通过的情况,这里不作罗列

    //通常,越狱机的输出结果会包含字符串: Library/MobileSubstrate/MobileSubstrate.dylib——之所以用检测链接动态库的方法,是可能存在前面的方法被hook的情况。这个字符串,前面的stat已经做了

    //如果攻击者给MobileSubstrate改名,但是原理都是通过DYLD_INSERT_LIBRARIES注入动态库

    //那么可以,检测当前程序运行的环境变量

    char *env = getenv("DYLD_INSERT_LIBRARIES");

    if (env != NULL) {

        jailbroken = YES;

    }

    return jailbroken;

}