很早就买了《iOS应用逆向与安全》这本书,现在把学到的内容总结一下。
越狱设备
Cydia
添加 雷锋源(apt.abcdia.com)
SSH
cydia 中搜索并安装 dropbear 提供 SSH 功能。连接,默认密码为 alpine
1 | ssh root@192.168.2.202 |
修改密码
输入如下修改密码:
1 | passwd |
公钥登录
在目标设备的 $HOME/.ssh 目录下找 authorized_keys 文件,如果没有则自己创建。将本机的公钥复制到该文件中。
iOS系统结构
文件目录
要访问越狱设备的文件系统,要通过 Cydia 安装 Apple File Conduit 2.
Mac 上安装 iFunBox
文件权限
通过 ll
可以查看文件的权限,一般权限包括三类:
- 所有者权限:文件所有者进行的操作
- 组权限:属于该组的成员对他能进行的操作
- 其他人权限:其他人能进行的操作
可以通过 chmod 改变权限
Cydia Substrate
Cydia Substrate 是一个框架,允许第三方开发者在越狱系统上打一些运行时的补丁和拓展一些方法,是开发越狱插件的基石。Cydia 自动安装了 Cydia Substrate,包含三个模块:
- MobileHooker:用于替换系统和应用的方法。提供
MSHookMessageEx
和MSHookFunction
hook OC 和 C 函数, - MobileLoader:用于将第三方动态库加载到运行的目标应用里(注入 Reaveal 就是通过它)。首先通过环境变量
DYLD_INSERT_LIBRARIES
把自己加载到目标应用里,然后查找/Library/Mobile Substrate/DynamicLibraries/
目录下所有的 plist 文件,如果 plist 文件的配置信息符合当前的应用,则通过dlopen
函数打开对应的 dylib 文件 - Safe mode:如果插件导致 SpringBoard 崩溃,将会让设备进入安全模式,禁用所有的三方插件
调试界面的 Reveal 就是通过 MobileLoader 动态加载的
越狱必备
通过 Cydia 安装一下插件:
- adv-cmds:提供指令
ps -A
获取全部进程的进程ID和可执行文件路径(dumpdecrypted 砸壳时候用到) - appsync:修改应用的文件会导致签名验证错误,该插件会绕过系统的签名验证
ps -A
获取完整的可执行文件路径
逆向工具详解
应用解密
dumpdecrypted
dumpdecrypted 会注入可执行文件,然后动态地从内存总 dump 出解密后的内容
- github上下载,包含一个 makefile 和一个 .c 文件
$make
编译生成一个 dumpdecrypted.dylib 动态库- 远程登录到手机,将生成的动态库放到
/var/root
下 - 在
/var/root
目录下使用环境变量DYLD_INSERT_LIBRARIES
将 dylib 注入到要脱壳的可执行文件中ps -A
拿到正在运行的要注入的应用的完整路径(/var/mobile/Containers/…/{应用的名字})- 终端输入
$DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib {完整路径}
进行注入
- 最终在
/var/root
目录下,得到的{应用名}.decrypted
就是脱壳后的 mach-o。 - 可以选择使用命令
$otool -l {引用名}.decrypted | grep crypt
查看加密标识,如果有输出cryptid 0
标识该架构已经被解密了
otool 可以用来查看 mach-o 的段信息,一样作用的还有 MachOView,是一个图形化的界面。
Clutch
Cluch 会生成一个新的进程,然后暂停进程,dump 内存
- github上下载源码
- 使用 Xcode 编译,会在 build 目录下生成一个二进制文件 Clutch
- 远程登录到手机,将 Clutch 放到
/usr/bin
目录下,并为其添加执行权限 Clutch -i
列出所有可以脱壳的应用,以及其 BundleIdClutch -b {BundleId}
砸壳- 砸完壳的 ipa 的路径会在屏幕上输出
Clutch 砸壳得到的是一个包含各种资源文件的完整的 .app
Clutch -i
获取所有 BundleId
class-dump
clas-dump 可以导出已砸壳应用的头文件。(未砸壳应用被加密无法获取,需要先砸壳)
官网下载 class-dump,把下载的二进制文件放到 /usr/bin
目录下。1
class-dump -H {二级制文件} -o {文件路径}
class-dump 原理
class-dump 主要是分析 mach-o 文件,进行加载符号,解析协议列表,解析类列表,解析分类列表
现在我们创建一个空的工程,build 之后通过 MachOView 对其进行分析。
解析协议
首先通过 MachOView 找到 data 段中的 protolist。除了各种 name 和代码之外,其他信息都是在 data 段中:
可以看到 _objc_protolist
段中包含两个协议信息。字段 offset 表示可执行文件加载到内存中后的相对偏移,这个偏移是相对于 __Text
段的。
再看字段 Data 的内容,它表示加载到内存中时候的虚拟地址。先简单罗列一下虚拟地址,实际地址,offset 这三者的关系:
虚拟地址 + ALSR = 实际地址
__Text
的虚拟地址 + ALSR =__Text
的实际地址
__Text
的虚拟地址 + 对象的 offset = 对象的虚拟地址
__Text
的实际地址 + 对象的 offset = 对象的实际地址
因此,如果我们知道 __Text
的虚拟地址,就能知道 data 所表示的位置的 offset 了。__Text
段的虚拟地址的起始位置在 __Text
段中可以找到:
因为在
__Text
段之前还有一个_PAGEZERO
段,它的大小固定为 0x100000000
因此可以计算出 data 的相对偏移为 8D30。我们来到了 __data 段:
响应位置展示了一个协议完整的信息。8D30 保存的是 ISA 的信息,偏移8个字节的 8D38 保存的是协议名的信息,它指向的位置是 76B4:
如此就可以到代码段中找到协议名的信息。
在 __data 段中查找的时候可以看到 MachOView 已经分析出了该协议的名词。整个从段信息到名词的过程就是上述的分析过程。
类解析
类解析就不一步步分析了,直接看截图:
Reveal
- Mac 端下载 Reveal 打开,在 help 里找到 Show Reveal Library in Finder -> iOS Library ,打开要嵌入 iOS 应用的 framework
- 打开该目录后,有一个 RevealServer.framework,将其中的 mach-o 文件 RevealServer 重命名为
libReveal.dylib
。 - 新建一个
libReveal.plist
文件,用 vim 写入要注入的 App 的 BundleId,如下所示:
1 | { |
- 将两个文件复制到手机的
/Library/MobileSubstrate/DynamicLibraries
目录下。 - 重启应用,就会通过 Cydia 的 MobileLoader 加载该库了。
Cycript
如何用 Cycript 调试一个进程
$ps -A
获取所有运行的进程(一般在/var/mobile/Containers
文件夹下,所以可以使用$ps -A | grep /var/mobile/Containers
筛选更准确)cycript -p 进程名
进入调试- cmd+R 清屏,ctrl+D 退出
Cycript 语法
1 | // 获取 UIApplication |
Cycript 主要用于查看控制器的层级、对象的成员变量以及动态调试界面
编写 Cycript 库
编写 Cycript 库文件 test.cy
:1
2
3
4
5
6
7
8
9
10
11
12
13(function(exports) {
// 递归打印 VC 层级
ChildVcs = function(vc) {
if (![vc isKindOfClass:[UIViewController class]]) throw new Error(invalidParamStr);
return [vc _printHierarchy].toString();
};
// 递归打印view的层级结构
Subviews = function(view) {
if (![view isKindOfClass:[UIView class]]) throw new Error(invalidParamStr);
return view.recursiveDescription().toString();
};
})(exports);
然后将文件放入 usr/lib/cycript0.9
文件夹下。1
2 @import test
ChildVcs(#0x12345678)
TODO: 增加获取属性和方法的相关库方法
Apple Configurator 2
获取 ipa 包
我们可以通过 Apple Configurator 2 获取应用的 ipa 包。
手机连接上 Mac 之后,Apple Configurator 2 中会显示出手机的信息。这个时候点击添加应用,在 app store 中搜索你想要获取资源的引用:
搜索到之后点击下载。如果手机中没有目标应用,第一次就直接安装了,安装成功后再次搜索该应用下载。这个时候就会提示已经有该应用了,是否要替换:
这个时候不要做任何的点击。在终端中进入到 Apple Configurator 2 的缓存目录下:~/Library/Group Containers/K36BKF7T3D.group.com.apple.configurator/Library/Caches/Assets/TemporaryItems/MobileApps/
你会发现目标应用的 ipa 包。
原因在于,Apple Configurator 2 下载的文件会临时保存在该临时目录下。如果安装完毕,就会立刻删除该 ipa 包。由于之前已经下载了该应用,Apple Configurator 2 弹窗提示是否需要替换,此时的 ipa 包还没来得及删除,可以复制出来。
获取 Assets.car 中的资源文件
把 ipa 包复制出来后,使用解压工具对其进行解压。在 payload
文件夹下有资源包。
这个包里已经可以看到很多资源文件了,但是我们需要找到的是 Assets.car
这个压缩文件,这里面才是真正的我们需要的图片资源。这个Assets.car
文件的解压需要用到https://github.com/pcjbird/AssetsExtractor使用起来非常方便。把 Assets.car
拖进该工具,指定输出目录就可以得到最终的全部资源文件
分析与调试
静态分析
hopper 使用
Mach-o 可以反编译为汇编代码,但是汇编代码无法完全反编译为 oc 代码。因为汇编操作的是寄存器,不同的类型以及不同的变量名的 oc 代码可能得到相同的汇编代码。
Hopper 和可以将 Mach-o 代码反编译为汇编代码、OC或者Swift伪代码。下载好之后,直接把 Mach-o 文件拖入 Hopper,即可开始分析。
动态调试
LLDB 调试
LLDB 通过 debugserver 和 app 通信。当 Xcode 调试手机时,Xcode 会将 debugserver 文件复制到手机中,以便在手机上启动一个服务,等待 Xcode 进行远程调试。只有设备连接到计算机真机调试时,debugserver 文件才会安装到设备的 /Developer/user/bin
目录下。
但是默认情况下,/Developer/usr/bin/debugserver 缺少权限,只能使用 xcode 调试。因此,我们需要对 debugserver 重签名,获得两个权限: get-task-allow
和 task_for_pid-allow
如何给 debugserver 添加权限:
- 将 debugserver 从目录拷贝到 mac。目录就是上述的 /Developer/usr/bin/debugserver
- 使用 ldid 导出 debugserver 的权限到 debugserver.entilements 文件
ldid 是帮助修改 iPhone 上二进制授权文件的工具。通过 homebrew 安装:
1 | brew install ldid |
安装完后,导出 debugserver 的权限:
1 | ldid -e debugserver > debugserver.entilements |
- debugserver.entilements 文件是个配置文件,打开并添加这两个权限:
- 使用 ldid 重签名:
1
ldid -S debugserver.entilements debugserver
(上面的 ldid 的重签名也可以使用 codesign 代替)1
2
3
4 查看 (在终端中显示,可以复制保存为 debugserver.entitlements)
codesign -d --entitlements -debugserver
签名
codesign -f -s - --entitlements debugserver.entitlements debugserver
- 将重签名后的 debugserver 拖到手机 device/usr/bin 目录下,这样可以直接使用该命令
手机端开启 server。开启调试后,进程会进入断电,被暂停:
1
debugserver *:{任意端口,如10010} -a {进程名}
Mac 端启动 lldb
1
2
3
4
5
6进入lldb 模式
lldb
连接,连接需要一段时间
(lldb) process connect connect://{手机IP}:{前面启动时的端口号}
连接成功自动进入断点,需要继续运行app
(lldb) c
debug 获取进程加载基地址
由于 ALSR 的原因,进程的虚拟地址不是从 0x0 开始的,会有一个随机偏移量。因此,当我们要读取运行的程序的内存的时候就需要知道这个 ALSR 的偏移量。
可以在 lldb 中通过 image list -o -f
拿到正在 debug 的进程的所有动态库的信息。其中第一项的第一列一般就是当前进程的基地址(不是ALSR地址)。
lldb 使用
- 打印相关:
p {指令}
:执行指令bt
:打印调用堆栈 backtraceframe variable
:打印当前栈帧的变量
- 调试相关:
c
:继续执行n
:单步运行step
:step-infinish
:step-over
代码断点相关
breakpoint set -n {方法名}
:为某个方法设置断点。1
2更高级的,设置某一个类的某一个方法的断点,需要使用引号包裹
breakpoint set -n "-[ViewController touchesBegan:withEvent:]"breakpoint list
:列出所有断点,包含编号breakpoint enable/disable/delete {断点编号}
:断点操作
内存断点相关
watchpoint set variable {变量名}
:变量值变化的时候触发watchpoint list
:列出所有内存断点watchpoint enable/disable/delete {断点编号}
:内存断点操作
1
watch set variable self->age
theos
下载 theos
- 安装签名工具
brew install ldid
修改 .zshrc
1
2
3
4# 这样可以用 $THEOS 代替 ~/theos
export THEOS=~/theos
# 修改环境变量,这样就可以在任意位置执行 `~/theos/bin` 路径下命令了
export PATH=~/theos/bin:$PATH执行
source ~/.zshrc
重置 zshrc 配置。- 递归下载 theos
git clone --recursive https://github.com/theos/theos.git $THEOS
使用 theos 创建 tweak
- 执行下载下来的命令
$nic.pl
- 根据提示选择要创建 iphone/tweak,然后设置自己的项目名,bundleID,还有目标应用的 bundle identifier(用 cycript 获取,
[NSBundle mainBundle].bundleIdentifier
),生成一系列文件。 编辑生成的 Makefile 文件,在文件最上方添加配置:
1
2
3
4
5
6# 设置IP和端口号,表示要通过 SSH 的方式安装这个 theos
# 切记一定要放在最上面
export THEOS_DEVICE_IP={你的手机的IP}
export THEOS_DEVICE_PORT=22
# ... 省略了自动生成的配置部分编写代码,编辑 Tewak.xm 文件:
1
2
3%hook {要hook的类名}
{根据 class-dump 找到感兴趣的要hook的方法}
%end命令行执行:
1
2
3
4
5
6编译
make
打包
make package
安装 会重启 SpringBoard
make install
相当于给原来的应用注入了一个动态库。可以在 /Library/MobileSubstrate/DynamicLibraries
中查看新安装的 dylib
和 plist
文件。
tweak 实现注意点
hook 的方法中的参数以及 self 一般都是 id 类型,所以不能用点语法,而要使用 get 方法:
1
2
3- (id)tableView:(id)tableView cellForRowAtIndexPath:(id)indexPath {
int num = [indexPath section];
}可以使用宏,在文件顶部定义:
1
hook 方法默认是 hook 已有方法,如果增加的方法是原来没有的需要加上
%new
:1
2
3%new -(void)someFunc:(UIButton *)button {
...
}hook 方法要调用原来的实现方法,使用
%orig
替代:1
2
3- (long long)numberOfSectionsInTableView:(id)tableView {
return %orig + 2
}资源文件放在新建的 tweak 生成的 layout 文件夹下,该目录对应的就是手机的根目录,可以自己在 layout 下创建文件夹层级,代码中引用:
1
UIImage *myImage = [UIImage imageWithCOntentOfFile:@"/{自己创建的文件夹层级}/{文件名}"]
有时候调用原有方法或者
%new
的方法,有时会报 instance method not found 的错误,这需要我们再在实现的 xm 文件顶部声明一下该方法。类名任意,只要表示该方法声明过即可:1
2
3@interface {任意类名}
-(id){你的方法名};
@end有时候使用某个类的时候还会报类不存在的错误。如果是使用自己创建的类直接
#import "{类名}"
即可。如果是被 hook 文件已经 import 的类,需要使用@class
提前声明一下。- 可以通过关联对象的方式给实例添加属性
- 如果要分多个文件编写,需要在
makefile
中配置相应文件,以空格分隔。针对文件量过多的情况,可以使用通配符表示一个文件夹内的文件。使用的时候直接直接 import,但是要把路径写完整,路径以Tweak.xm
为基准。
1 | TWEAK_NAME = ${your tweak name} |
Tweak 实现原理
- 生成的插件会被安装在 /Library/MobileSubstrate/DynamicLibraries 中。生成的文件包括 .dylib 包含编译后的 tweak 代码,和 .plist 存放着 hook 的目标 APPID
- 打开 App 时,Cydia Substrate 会去加载对应的 dylib,修改内存中的代码逻辑, 执行 dylib 中的函数
- tweak 不会修改可执行文件,仅仅只是修改了内存的逻辑。所以 tweak 可以对未砸壳 App 修改,但是必须要使用越狱手机。
Logos 语法
%hook
%end
hook一个类的开始和结束%log;
打印方法调用详情,在 Xcode 的日志输出中查看%c({类名})
获取类对象,相当于[xxx class]
,直接调用可能有错%orig
函数原来的代码逻辑%new
添加一个新的方法
logify.pl
可以将头文件快速转换成已经包含打印信息的 xm 文件(自动添加 %log
语句):1
logify.pl xxx.h > {你想取的任意名字}.xm
通过修改 makefile 中的配置,将生成的 .xm 文件加入到编译文件中。直接添加到原来的 Tweak.xm
之后即可:
1 | {your project name}_FILES = Tweak.xm {你取的名字}.xm |
但是经常会编译不过,需要手动修改:
- 未定义头文件:报错
unknown type name ‘XXX’
,在头部声明@class XXX;
,或者将 class 类型改为 id - 未声明协议:报错
no type or protocol named 'XXX'
,在头部声明@protocol XXX
- 不能存在 weak:报错
cannot create __weak reference
,替换掉所有的__weak
为空字符串 - 不能存在非 oc 方法:报错
expected selector for Objective-C method
,删除以点开头的非 OC 的方法 - 带协议的参数报错:报错
cast from pointer to smaller type 'unsigned int' loses information
。如果有一个参数类型遵循某个协议,那么%log
就无法通过,需要把协议删除。 HBLogDebug
类型错误:报错cast from pointer to smaller type 'unsigned int' loses information
,HBLogDebug
本身是用来打印方法返回值的,有一些地方返回值 id 类型,会被转化为 unsigned int 类型,因此报错。需要替换:1
2
3
4// 原始语句
HBLogDebug(@"=0x%x", (unsigned int)r);
// 批量替换为
HBLogDebug(@"=0x%@", r);
MonkeyDev
MonkeyDev 的具体使用可以到 MonkeyDev Wiki 中查看
逆向进阶
ASLR
Address space layout randomization 地址空间布局随机化
可以通过 lldb 的 image list -o -f
获得这个偏移地址:
image list -o -f 拿到的是减去
__Text
段基地址的偏移,即 ASLRimage list 拿到的则是
__Text
的实际地址偏移,即基地址 + ASLR 地址
获得了偏移地址后,在通过 hopper 获得未使用 ALSR 的方法的地址,两者相加,就是该方法实际在内存中的地址了,可以为其设置断点:
1 | breakpoint set -o {函数地址}+{偏移地址} |
通用二进制文件
包含了多种架构的二进制文件叫做通用二进制文件,又叫 fat binary 胖二进制文件。1
2
3
4
5
6
7
8
9// 查看信息:
file {文件名}
lipo -info {文件名}
// 瘦身
lipo {文件名} -thin armv7 -o {输出文件名}
// 合并
lipo -create {文件名1} {文件名2} -o {输出文件名}
Xcode 中生成架构配置如图:
- Architecture: Xcode 支持的架构,不同 Xcode 版本不同
- Valid Architecture: 想要生成的架构
最终生成的架构就是支持的和想要的的交集。
程序加载
在编写一个程序时,看到的入口函数都是 main.m。实际上在 main 函数执行前已经执行了 +load
和 constructor
构造函数。现在要探讨在 main 函数执行前做了什么
dyld 简介
系统内核在做好启动程序的准备工作之后就会从内核态切换到用户态,将工作交给 dyld。
系统动态库会通过动态库加载器 dyld 被加载到内存中。为了优化程序启动速度,iOS 采用了共享缓存技术。在系统启动后被加载到内存中。当有新的程序加载时会先到共享缓存里寻找。找到就直接将共享缓存中的地址映射到目标进程的内存空间。
dyld 加载流程
dyld 的时间线:
1 | Load dylibs -> Rebase -> Bind -> ObjCruntime -> Initializers |
dyld 从主执行文件的 header 获取到需要加载的所依赖动态库列表,并递归的将他们加载,为每一个动态库生成一个 ImageLoader 对象
检查共享缓存是否映射到了共享区域
加载所有通过
DYLD_INSERT_LIBRARIES
插入的库
在加载所有的动态链接库之后,它们只是处在相互独立的状态,需要将它们绑定起来,这就是 Fix-ups
- Rebasing:在 imageLoader 内部调整指针的指向,即修改 ASLR 带来的偏差
- Binding:dylib 通过指针绑定会使用的外部的实例方法等符号的地址
ObjC Runtime 需要维护一张映射类名与类的全局表。当加载一个 dylib 时,其定义的所有的类都需要被注册到这个全局表中。
执行初始化方法,
+load
和constructor
就是在这里执行的通过 Load Command 找到
LC_MAIN
即 main 函数位置,执行
Mach-O 文件
MachO 文件基本构成
Mach-O 是苹果的可执行文件,结构由三部分组成:
- Header:文件类型,目标架构类型
- Load Commands:描述载入内存的有哪些段,段有多大,从哪里开始
- Data:段的数据
使用 MachOView 查看,我们可以看到有各种各样的段。class-dump 就是通过这种方式获取到头文件信息的。
具体看一下 Macho 的 Load Command,它包含了 Macho 中各个段的基本信息:
一般可执行文件会分为许多个 section,section 又会根据权限的不同整合为多个 fragment
,一般分为四个 fragment:
__PAGEZERO
空指针陷阱段,映射到虚拟内存空间的第一页,用于捕捉对 NULL 指针的引用__TEXT
代码段,只读,包括函数,和只读的字符串(如__TEXT,__text
保存所有代码,又如__TEXT.__objc_classname
保存 Objective-C 类名称)____DATA
数据段,读写,包括可读写的全局变量等(如__DATA,__data
保存初始化过的可变数据,又如__DATA.__objc_classlist
保存所有类实体的指针,指向 __data 中保存的 objc_class 实例)__LINKEDIT
动态链接器需要使用的信息,包括重定位信息,绑定信息,懒加载信息等。
代码段和数据段的具体组成
__TEXT 包含以下 section:
- __text:程序可执行代码区域
- __stubs:简介符号存根,跳转到懒加载指针表
- __stub_helper:帮助解决懒加载符号加载的辅助函数
- __objc_methname:方法名
- __objc_classname:类名
- __objc_methtype:方法签名
- __cstring:c风格字符串
__DATA 包含以下 section:
- __nl_symbol_ptr:非懒加载指针表,在 dyld 加载时会立即绑定
- __la_symbol_ptr:懒加载指针表,第一次调用才绑定值
- __got:非懒加载全局指针表
- __mod_init_func:constructor 函数
- __mod_term_func: destructor 函数
- __cfstring:OC 字符串
- __objc_classlist:程序中类的列表
- __objc_nlclslist:程序中实现了 +load 方法的类
- __objc_protolist:协议的列表
- __objc_classrefs:被应用的类列表
懒加载和非懒加载
在之前说的dyld执行流程中有一环叫做 binding,即绑定符号的地址。iOS 系统为了加快系统启动速度,将符号分成了懒加载符号和非懒加载符号。
非懒加载符号在 dyld 加载时就会绑定真实的值。懒加载符号不会,只有第一次去调用它时才会绑定真实的地址,第二次调用直接使用真实地址。
如系统方法 print 的绑定过程如下图所示:
ARM 汇编
ARM寄存器
X0-X30 是 31 个通用寄存器,低 32 位用 W0-W30 表示。X30 是一个特殊寄存器,用于保存函数调用完成时的返回地址。
SP 为堆栈指针寄存器,通过 X31 寄存器访问
PC 为保存当前指令地址的程序计数器
LR 指向返回地址,对应于 X30
FP 指向栈帧的底部,对应于 X29(即 Base Pointer)
常见指令
ldr Xn, addr
: 从 addr 读取内容存到 Xn 中
str Xn, addr
: 将 Xn 写入 addr 指向的内存
cbz Xn, label
: 如果 Xn 为 0,则跳转到 label
cbnz Xn, label
: 如果 Xn 不为 0,跳转到 label
bl label
: 无条件跳转,会将下一条指令地址写到 X30 处
堆栈调用
堆栈调用过程如下:
- 函数调用前:
- 开辟堆栈控件
- 保存 FP 和 LR 寄存器,以便找到上一个栈帧和返回地址
- 设置新的 FP 寄存器
- 保存子函数会用到的寄存器
- 保存局部变量或参数
- 函数调用结束:
- 还原 FP 和 LR 寄存器
- 释放栈帧空间
- 跳到 LR 继续执行
参数一般通过 X0-X7传递,当参数小于 8 个时候,默认是直接存到寄存器中的。但是如果超过 8 个,那么会在方法调用前,将多出的那个存入堆栈中,子方法需要到堆栈中取值。
返回结果一般通过 X0 传递
一个方法的汇编的例子:
1 | void fooFp() { |
1 | ArmAssembly`fooFp: |
hook
fishhook
苹果为了能在 Mach-O 文件中访问外部函数,采用了一个技术,叫做PIC(位置代码独立)技术。当你的应用程序想要调用 Mach-O 文件外部的函数的时候,或者说如果 Mach-O 内部需要调用系统的库函数时,Mach-O 文件会:
- 先在 Mach-O 文件的 _DATA 段中建立一个指针ptr(8字节的数据,放的全是0),这个指针变量指向外部函数。
- DYLD 会动态的进行绑定!将 Mach-O 中的 _DATA 段中的指针,指向外部函数。
上图中可以看到,其他部分都是实实在在的代码信息,没有好的修改办法。只有红色框框中的 nl_symbol_ptr
和 la_symbol_ptr
是指针段,由于指针都是长度固定的,所以方便修改指针地址,进而达到 hook 的目的。
所以说,C的底层也有动态的表现。C在内部函数的时候是静态的,在编译后,函数的内存地址就确定了。但是,外部的函数是不能确定的,也就是说C的底层也有动态的。fishhook 之所以能 hook C函数,是利用了 Mach-O 文件的 PIC 技术特点。也就造就了静态语言C也有动态的部分,通过 DYLD 进行动态绑定的时候做了手脚。
我们经常说符号,其实 _DATA 段中建立的指针就是符号。fishhook的原理其实就是,将指向系统方法(外部函数)的符号重新进行绑定指向内部的函数。这样就把系统方法与自己定义的方法进行了交换。这也就是为什么C的内部函数修改不了,自定义的函数修改不了,只能修改 Mach-O 外部的函数。
iOS 签名
签名过程如图:
当我们修改了别人的 APP 之后,我们就需要将修改过的 ipa 重签名,才能安装到手机上。重签名了的应用可以安装到未越狱手机。
IPAPatch 免越狱调试 APP
IPAPatch 就是通过从签名达到免越狱注入代码的目的。使用起来非常简单。
- 使用 PP 助手下载一个已越狱的应用的 ipa
- 下载 IPAPatch 的工程
- 使用已越狱的 ipa 替换工程中 Assets 文件夹下的 app.ipa 文件。注意,名字要改为 app.ipa
- 将 RevealServer.framework 放置在 Assets/Frameworks/RevealServer.framework
- 修改 bundleId
- Run Xcode 安装到手机
安全保护
静态混淆
宏定义
使用宏将方法属性名修改为其他无意义的字符串
动态保护
反调试
ptrace
UNIX 早期版本提供的一种对运行中的进程进行跟踪和控制的手段,就是系统调用 ptrace。通过 ptrace 实现对另一个进程的调试跟踪
可以通过参数 PT_DENY_ATTACH
禁用调试。这个参数告诉系统阻止调试器依附。所以,最常用的反调试方法就是通过调用 ptrace 来实现反调试。
sysctl
当一个进程被调试时,该进程中的一个标志位用来标记正在被调试。可以定时通过 sysctl 查看这个标志位
反反调试
hook函数 -> 判断参数 -> 返回结果
越狱设备直接可以hook以上说到的反调试函数。非越狱设备可以通过 fishhook,hook 反调试函数。
反注入
可以定期调用 _dyld_get_image_name()
方法,获取正在加载的动态库名,比较是否是白名单内的动态库名来实现注入检测。
hook 检测
hook 包括 Method Swizzle,符号表替换,inline hook 等。不同的 hook 方式,需要制定不同的检测方案
Method Swizzle
Method Swizzle 的原理是替换 imp,通过 dladdr 得到 imp 所在的模块,判断模块是不是主二进制模块,如果不是就是被 hook 了。
符号表替换
fishhook 是基于懒加载符号表和非懒加载符号表进行替换的,所以遍历符号表中的指针就能判断程序是否被恶意 hook 了。
非懒加载的指针指向真实地址,懒加载的指针在没有解析到真正的地址钱指向 __stub_helper
,所以遍历符号表,判断是否指向了系统模块或者 __stub_helper
即可。
完整性校验
逆向过程设计到对文件 load command 的修改,对文件进行重签名,修改 BundleId。可以从上述几个方面校验
load command
直接读取 Mach-O load command 中的 LC_LOAD_DYLIB
,判断是否有非白名单动态库
代码校验
获取内存中代码的 MD5 值,如果代码修改了,就会不一样
重签名校验
判断 bundle ID 是否被修改
8086简介
8086 是 x86系列处理器的开端,所以后面用 x86 代替 32位处理器。
CPU 的组成
CPU 的三大组成:
- 运算单元
- 数据单元
- 控制单元
运算单元做加法或者位移的操作。
数据单元包含CPU内部的缓存和寄存器组。
控制单元可以获得下一条指令,然后执行。这个指令会指导运算单元取出数据单元中的某几个数据,计算出结果,然后放到数据单元的某个地方。
8086 的寻址方式
8086 的总线是有16根,但是可以寻址的范围为 2^20 byte。
20位的物理地址 = 16位的段地址 * 16 + 4位的偏移地址
CPU 中的数据单元
8086 CPU 中的数据单元如下:
其中:
- AX,BX,CX,DX 为数据寄存器,存放操作的数据
- CS 代表代码段的起始地址。IP 表示偏移地址。每读取一条指令,IP=IP + 所读取指令的长度。
- SS 表示栈的起始位置。SP 表示栈的偏移地址。BP 是入参和临时变量的分界,通过 BP 及偏移量拿到入参和临时变量。