1. 加固的缘由❓
我们都知道,在越狱机型上,如果程序的可执行文件被获取到,就可以通过一些逆向工具来反编译我们的程序,从而可以实现:
任意读写文件系统数据
HTTP(S)实时被监测
重新打包ipa
暴露的函数符号
未加密的静态字符
篡改程序逻辑控制流
拦截系统框架API
逆向加密逻辑
跟踪函数调用过程(objc_msgSend)
可见视图的具体实现
伪造设备标识
可用的URL schemes
runtime任意方法调用
2. 编译过程
其实使用 Xcode 构建一个程序,就是把源文件 ( .h
和 .m
.swift
) 文件转换为一个可执行文件。这个Mach-O文件中包含的字节码会将被 CPU (包括iOS 设备中的 ARM 处理器或 Mac 上的 Intel 处理器) 执行。大致流程如下:
预处理
- 符号化 (Tokenization)
- 宏定义的展开
-
#include
的展开
语法和语义分析
- 将符号化后的内容转化为一棵解析树 (parse tree)
- 解析树做语义分析
- 输出一棵抽象语法树(Abstract Syntax Tree* (AST))
生成代码和优化
- 将 AST 转换为更低级的中间码 (LLVM IR)
- 对生成的中间码做优化
- 生成特定目标代码
- 输出汇编代码
汇编器
- 将汇编代码转换为目标对象文件。
链接器
- 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)
Objective-C 早期采用GCC,从Xcode5之后采用 Clang 作为前端,而 Swift 则采用 swift() 作为前端(Swift的语法分析器是一个简单的,对整体通过递归向下的方式进行语法分析的手工编码词法分析器,他是在lib/Parse内实现的。该分析器负责生成不包含语义和类型信息的抽象语法树AST
(Abstract Syntax Tree)。这个阶段生成的AST
也不包含警告和错误的注入,因为 swift 在编译时就完成了方法绑定直接通过地址调用属于强类型语言,方法调用不再是像OC那样的消息发送,这样编译就可以获得更多的信息用在后面的后端优化上),二者都是用 LLVM(Low level vritual machine) 作为编译器后端。
对应前端编译器地址如下:
# Objective-C
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang
# Swift
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift
GCC
(GNU Compiler Collection)缩写,一个编程语言编译器,是GNU(自由软件理事会)的关键部分。也是GNU工具链的一部分。GCC常被认为是跨平台编译器的事实标准,特别是它的C语言编译器。GCC原本只能处理C语言。但是面对Clang的竞争,很快作出了扩展,现在已经可以处理C++,Fortran、Pascal、Object-C、Java、Ada,以及Go语言。许多操作系统,包括许多Unix系统,如Linux及BSD家族都采用GCC作为标准编译器。MacOSX也是采用这个编译器。
LLVM
是Low Level Virtual Machine的简称。这个库提供了与编译器相关的支持,能够进行程序语言的编译期优化、链接优化、在线编译优化、代码生成。可以作为多种语言编译器的后台来使用。
LLVM是一个优秀的编译器框架,如图:
它采用经典的三段式设计。前端可以使用不同的编译工具对代码文件做词法分析以形成抽象语法树AST,然后将分析好的代码转换成LLVM的中间表示IR(intermediate representation);它既不是源代码,也不是机器码。从代码组织结构上看它比较接近机器码,但是在函数和指令层面使用了很多高级语言的特性。
IR代码是由一个个Module组成的,每个Module之间互相联系,而Module又是由一个个Function组成,Function又是由一个个BasicBlock组成,在BasicBlock中又包含了一条条Instruction。
中间部分的优化器只对中间表示IR操作,通过一系列的Pass对IR做优化;后端负责将优化好的IR解释成对应平台的机器码。LLVM的优点在于,中间表示IR代码编写良好,而且不同的前端语言最终都转换成同一种的IR。
Clang
Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍,其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码,当然这个过程会对代码进行检查,出错的和需要警告的会标注出来。
前端编译器
编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。
后端编译器
编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。iOS的编译过程,后端的处理如下
LVVM 优化器会进行 BitCode 的生成,链接期优化等等。
3. 加固类型
1.字符串混淆
对应用程序中使用到的字符串进行加密,保证源码被逆向后不能看出字符串的直观含义。
2.类名、方法名混淆
对应用程序的方法名和方法体进行混淆,保证源码被逆向后很难明白它的真正功能。
3.程序结构混淆加密
对应用程序逻辑结构进行打乱混排,保证源码可读性降到最低。
4.反调试、反注入等一些主动保护策略
这是一些主动保护策略,增大破解者调试、分析App的门槛。
4. 逆向工具
1. class-dump
class-dump 利用 Objective-C runtime 特性,将存储在 Mach-O 文件结构里 data 部分的类属性和方法等信息提取出来,并生成对应的 .h 文件的工具。
在 class-dump 官网下载 dmg,将 dmg 里面的 class-dump
拷贝到 /usr/local/bin
文件夹下,然后就可以在终端中使用 class-dump
命令了。
简单使用:
class-dump -H [需要被导出的 Mach-O 文件路径] -o [头文件输出目录地址]
2. hopper
Hopper 是一种适用于 OS X 和 Linux 的逆向工程工具,可以用于反汇编、反编译和调试 32位/64位英特尔处理器的 Mac、Linux、Windows 和 iOS 可执行程序。
3. IDA
是目前最棒的一个静态反编译软件,为众多0day世界的成员和ShellCode安全分析人士不可缺少的利器。
就其本质而言,IDA是一种递归下降反汇编器。但是,为了提高递归下降过程的效率,IDA的开发者付出了巨大的努力,来为这个过程开发逻辑。为了克服递归下降的一个最大的缺点,IDA在区分数据与代码的同时,还设法确定这些数据的类型。虽然你在IDA中看到的是汇编语言形式的代码,但IDA的主要目标之一,在于呈现尽可能接近源代码的代码。此外,IDA不仅使用数据类型信息,而且通过派生的变量和函数名称来尽其所能地注释生成的反汇编代码。这些注释将原始十六进制代码的数量减到最少,并显著增加了向用户提供的符号化信息的数量。
5. OLLVM
O-llvm是基于llvm进行编写的一个开源项目(https://github.com/obfuscator-llvm/obfuscator),它的作用是对前端语言生成的中间代码进行混淆。
O-llvm总体构架和llvm是一致的:
其中IR(intermediate representation)中间代码表示,也是Pass(总的来说,所有的pass大致可以分为两类:分析和转换;分析类的pass以提供信息为主,转换类的会修改中间代码)操作的对象,它主要包含四个部分:
(1)Module:比如一个.c或者.cpp文件。
(2)Function:代表文件中的一个函数。
(3)BasicBlock:每个函数会被划分为一些block,它的划分标准是:一个block只有一个入口和一个出口。
(4)Instruction:具体的指令。
对于OLLVM的每个pass,其主要的工作继承对应的pass类,就是对相应的方法进行重写,例如SplitBasicBlock的实现,它继承自FunctionPass,并重写了runOnFunction方法。
O-llvm包含有三个pass,分别是BogusControlFlow、Flattening 和 Instruction Substitution。它们是O-llvm实现混淆功能的核心,具体实现位于llvm/lib/Transforms/Obfuscation/目录下。
控制流扁平化
这个模式主要是把一些if-else语句,嵌套成do-while语句
-mllvm -fla:激活控制流扁平化
-mllvm -split:激活基本块分割。在一起使用时改善展平。
-mllvm -split_num=3:如果激活了传递,则在每个基本块上应用3次。默认值:1
指令替换
这个模式主要用功能上等效但更复杂的指令序列替换标准二元运算符(+ , – , & , | 和 ^)
-mllvm -sub:激活指令替换
-mllvm -sub_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
虚假控制流程
这个模式主要嵌套几层判断逻辑,一个简单的运算都会在外面包几层if-else,所以这个模式加上编译速度会慢很多因为要做几层假的逻辑包裹真正有用的代码。
另外说一下这个模式编译的时候要浪费相当长时间包哪几层不是闹得!
-mllvm -bcf:激活虚假控制流程
-mllvm -bcf_loop=3:如果激活了传递,则在函数上应用3次。默认值:1
-mllvm -bcf_prob=40:如果激活了传递,基本块将以40%的概率进行模糊处理。默认值:30
6. Hikari
HikariObfuscator/Hikari 是一个基于 Obfuscator-LLVM 对 Xcode9的适配。
如果需要自行编译,需要安装 cmake
and ninja
以及 SWIG
。
cmake : CMake 是一个跨平台的自动化建构系统,它使用一个名为 CMakeLists.txt 的文件来描述构建过程,可以产生标准的构建文件,如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces 。文件 CMakeLists.txt 需要手工编写,也可以通过编写脚本进行半自动的生成。CMake 提供了比 autoconfig 更简洁的语法。一些使用 CMake 作为项目架构系统的知名开源项目有 VTK、ITK、KDE、OpenCV、OSG 等。
在 linux 平台下使用 CMake 生成 Makefile 并编译的流程如下:
- 编写 CMake 配置文件 CMakeLists.txt 。
- 执行命令
cmake PATH
或者ccmake PATH
生成 Makefile。(ccmake
和cmake
的区别在于前者提供了一个交互式的界面)其中,PATH
是 CMakeLists.txt 所在的目录。 - 使用
make
命令进行编译。
下载地址: https://cmake.org/download/
在终端安装:
sudo "/Applications/CMake.app/Contents/bin/cmake-gui" --install
Ninja : Ninja是一种类似GNU make的编译系统。 就像make有Makefile,它也有自己的编译配置文件。 相对来说,Ninja文件没有分支、循环的流程控制,本质上就是纯粹的配置文件,所以要比Makefile简单得多。
Ninja目前主要应用在Google Chrome,部分Android系统,LLVM以及CMake的Ninja后端中。
可通过官网下载安装release版本,也可通过homebrew或者npm包管理工具安装 brew install Ninja
SWIG :Simplified Wrapper and Interface Generator,SWIG完整支持ANSI C,支持除嵌套类外的所有C++特性。SWIG是一个接口编译器,旨在为C/C++方便地提供脚本语言接口。SWIG不仅可以为C/C++程序生成 Python接口,目前可以生成CLISP,Java,Lua,PHP,Ruby,Tcl等19种语言的接口。SWIG被Subversion, wxPython, Xapian等项目使用。值得一提的是,Google也使用SWIG。
SWIG本质上是个代码生成器,为C/C++程序生成到其他语言的包装代码(wrapper code),这些包装代码里会利用各语言提供的C API,将C/C++程序中的内容暴露给相应语言。为了生成这些包装代码,SWIG需要一个接口描述文件,描述将什么样的接口暴露给其他语言。
SWIG的 接口描述文件可以包含以下内容:
- ANSI C函数原型声明
- ANSI C变量声明
- SWIG指示器(directive)相关内容
利用SWIG,可以现实以下功能:
- 用Python调用C/C++库
- 用Python继承C++类,并在Python中使用该继承类
- C++使用Python扩展(通过文档描述应该可以支持,未验证)
自行安装需要在/Hikari/tools/xcode-toolchain/CMakeLists.txt中加上set(CMAKE_CXX_FLAGS "-std=c++11 ${CMAKE_CXX_FLAGS}")才能编译通过。另如果执行完成报错 lldb_codesign: no identity found
,则是需要添加一个lldb_codesign证书,然后信任,具体可参照 这里 或 编译mac下的lldb
安装完成之后执行脚本,到生成Toolchains我这边大约花了2小时:
git clone --recursive -b release_80 https://github.com/HikariObfuscator/Hikari.git Hikari && cd Hikari && git submodule update --remote --recursive && cd ../ && mkdir Build && cd Build && cmake -G "Ninja" -DLLDB_CODESIGN_IDENTITY='' -DCMAKE_BUILD_TYPE=MinSizeRel -DLLVM_APPEND_VC_REV=on -DLLVM_CREATE_XCODE_TOOLCHAIN=on -DCMAKE_INSTALL_PREFIX=~/Library/Developer/ ../Hikari && ninja &&ninja install-xcode-toolchain && git clone https://github.com/HikariObfuscator/Resources.git ~/Hikari && rsync -a --ignore-existing /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/ ~/Library/Developer/Toolchains/Hikari.xctoolchain/ && rm ~/Library/Developer/Toolchains/Hikari.xctoolchain/ToolchainInfo.plist
另外也可以直接下载安装作者提供的release版本 pkg 文件:
1 https://github.com/HikariObfuscator/Hikari/releases
重启Xcode就能在Toolchains里面看到除了系统默认的编译工具还多了一个Hikari编译工具。
-
Xcode
->Toolchains
->Hikari
将混淆工具和项目关联 - 将所有与要运行的 target 相关的 target(包括pod进来的库)
Enable Index-While-Building
的值改为 NO。 -
Optimization Level
的值设置为None[-O0]
- 在
Build Settings
->Other C Flags
中加入混淆标记
在 Wiki 中可以看到用法:
The following flags are supported
-enable-bcfobf 启用伪控制流
-enable-cffobf 启用控制流平坦化
-enable-splitobf 启用基本块分割
-enable-subobf 启用指令替换
-enable-acdobf 启用反class-dump
-enable-indibran 启用基于寄存器的相对跳转,配合其他加固可以彻底破坏IDA/Hopper的伪代码(俗称F5)
-enable-strcry 启用字符串加密
-enable-funcwra 启用函数封装
7. 代码虚拟化
使用一套自定义的字节码来替换掉程序中原有的native指令,而字节码在执行的时候又由程序中的解释器来解释执行。自定义的字节码是只有解释器才能识别的,所以一般的工具是无法识别我们自定义的字节码,也是因为这一点,基于虚拟机的保护相对其他保护而言要更加难破解。
目前很多地方都会用到虚拟化技术,比如sandbox、程序保护壳等。很多时候为了防止恶意代码对我们的系统造成破坏,我们需要一个sandbox,使程序运行在sandbox中,即使恶意代码破坏系统也只是破坏了sandbox而不会对我们的系统造成影响。
基于虚拟机的代码保护也可以算是代码混淆技术的一种。代码混淆的目的就是防止代码被逆向分析,但是所有的混淆技术都不是完全不能被分析出来,只是增加了分析的难度或者加长了分析的时间,虽然这些技术对保护代码很有效果,但是也存在着副作用,比如会或多或少的降低程序效率,这一点在基于虚拟机的保护中格外突出,所以大多基于虚拟机的保护都只是保护了其中比较重要的部分。
LLVM
The Compiler
CMake入门实战
Mach-O 可执行文件
在linux下使用CMake构建应用程序
利用 SWIG 对 C++ 库进行 Python 包装