前言
之前由于启动优化相关的需求,分析了Xcode编译的全过程,针对优化点做了记录和学习。最近被人问到详述下LLVM架构原理。当时只是略知一二,看过而已,没理解的那么深入,这次根据LLVM + Clang的整个编译流程简单做个记录,而且顺便在每个不同阶段做代码混淆做一下Demo介绍。这里需要安装很多很多东西,个人认为Mac电脑都会安装个Homebrew,安装东西就方便多了,不多说了,下面开始介绍。
LLVM
还是说说 LLVM 到底是什么吧,LLVM的项目是一个模块化和可重复使用的编译器和工具技术的集合.LLVM 曾经是一个缩写词,现在不是,它就是这个项目的名称。
Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器。
再来一个更容易理解的说法,iOS 开发中 Objective-C 是 Clang / LLVM 来编译的。(swift 是 Swift / LLVM)
传统的编译器最流行的设计是分三段,分别是前端(Frontend),优化器(Optimizer)和后端(Backend).
前端(Frontend)
负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree).也就是词法分析,语法分析,语义分析和生成中间代码。
优化器(Optimizer)
负责进行各种转换尝试改进代码的运行时间.release 包比 debug 包体积小运行快,其中的一个原因就是优化器起作用。比如重复计算的消除。
后端(Backend)
用来生成实际的机器码.
这种设计有很多优点,但实际这一结构却从来没有被完美实现过。GCC 做的比较好,实现了很多前端和后端,支持了很多语言。但是有个缺陷,那就是他们是一个完整的可执行文件,没有把前端和后端分的太开,所以GCC 为了支持一门新的语言,或者支持一种新的平台,就变得比较困难。
LLVM 就解决了上面的问题,因为它被设计为一组库,而不是一个编译器,如下图
从上图中我们发现LLVM与GCC在三段式架构上并没有本质区别,也是分为前端、优化器和后端。但是,其设计最重要的方面是不同的前端、后端使用同一的中间代码 LLVM Intermediate Representation (LLVM IR) . 也就是图中的 LLVM Optimizer
这种设计的好处就是,如果需要支持一种新的编程语言,那么只需要实现一个新的前端。
如果需要支持一种新的硬件设备,那么只需要实现一个新的后端。优化阶段是一个通用的阶段,它针对的是同一的LLVM IR,不论是支持新的编程语言,还是支持新的硬件设备,都不需要对优化阶段做修改。
Clang 就是基于LLVM架构的C/C++/Objective-C编译器前端
,如下图
LLVM现在被用作实现各种静态和运行时编译语言的通用基础结构(GCC家族(C,C++,Objecttive-C),Java,.Net,Python,Ruby,D等)
Clang 主要处理一些和具体机器无关的针对语言的分析操作.编译器的优化器部分和后端部分是LLVM后端,也可以直接叫 LLVM(狭义的LLVM),广义上LLVM 就是整个LLVM架构。
Clang和GCC比较
1.编译速度快,Debug模式下,编译OC的速度是GCC的三倍
2.占用内存小,Clang的生成的AST语法树占用内存是GCC的五分之一
3.模块化设计,Clang基于库的模块化设计,易于IDE集成和重用
4.诊断性可读性强,在编译过程中,Clang创建并保留了大量的详细元素,有利于调试
5.设计清晰简单,容易理解,易于扩展增强
看上面的图,左边是编程语言,最终是机器码,在我们Xcode 中编写的代码首先会经过 clang 这个编译器前端,他会生成中间代码(IR),这个中间代码又经过一系列的优化,这些优化就是 Pass
,如果咱要编写中间代码优化代码的话,那就是编写Pass
,最后就是生成机器码.
通过图也可以看出,Pass
是 LLVM 系统转化和优化的工作的一个节点,每个节点做一些工作,这些工作加起来就构成了 LLVM 整个系统的优化和转化。
在编译一个源文件时,编译器的处理过程分为几个阶段。
用命令行查看一下OC源文件的编译过程
$ clang -ccc-print-phases main.m
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
一共是7个阶段,
第0个阶段找到源代码,读入文件。
第1个阶段 preprocessor,预处理器,就是把头导入,宏定义给展开,包 括#define
、#include
、#import
、#indef
、#pragma
。
第2阶段就是 compiler,编译器编译成 ir 中间代码。
第3阶段就是交给后端,来生成汇编代码(assembler)。
第4阶段是将汇编代码转换为目标对象文件
第5阶段是链接器,将多个目标对象文件合并为一个可执行文件 (或者一个动态库) 。
最后一阶段 生成可执行文件 :Mach-O
Demo介绍这6个阶段
新建一个project,选择最简单的Command Line Tool
代码如下
#include <stdio.h>
#define kPeer 3
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
int c = a + b + kPeer;
printf("%d",c);
return 0;
}
1.查看预处理preprocess结果
Clang -E main.m
.....省略很多
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
const char * restrict, va_list);
# 412 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/usr/include/stdio.h" 2 3 4
# 10 "main.m" 2
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
int c = a + b + 3;
printf("%d",c);
return 0;
}
可以看到,这里的宏定义进行了替换,而且把include的资源给导进来了。
2.编译器前端 Clang的作用
前端(Frontend)
负责解析源代码,检查语法错误,并将其翻译为抽象的语法树(Abstract Syntax Tree).也就是词法分析,语法分析,语义分析和生成中间代码。
词法分析(输出Token流)
clang -fmodules -E -Xclang -dump-tokens main.m
int main(int argc, const char * argv[]) {
int a = 1;
int b = 2;
int c = a + b + kPeer;
printf("%' Loc=<main.m:9:1>
int 'int' [StartOfLine] Loc=<main.m:13:1>
identifier 'main' [LeadingSpace] Loc=<main.m:13:5>
l_paren '(' Loc=<main.m:13:9>
int 'int' Loc=<main.m:13:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:13:14>
comma ',' Loc=<main.m:13:18>
const 'const' [LeadingSpace] Loc=<main.m:13:20>
char 'char' [LeadingSpace] Loc=<main.m:13:26>
star '*' [LeadingSpace] Loc=<main.m:13:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:13:33>
l_square '[' Loc=<main.m:13:37>
r_square ']' Loc=<main.m:13:38>
r_paren ')' Loc=<main.m:13:39>
......
语法分析(生成语法树AST Abstract Syntax Tree)
clang -fmodules -fsyntax-only -Xclang -ast dump main.m
|-ImportDecl 0x7fc3e68673c0 <main.m:9:1> col:1 implicit Darwin.C.stdio
|-FunctionDecl 0x7fc3e6926e78 <line:13:1, line:19:1> line:13:5 main 'int (int, const char **)'
| |-ParmVarDecl 0x7fc3e6867410 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7fc3e6867520 <col:20, col:38> col:33 argv 'const char **':'const char **'
| `-CompoundStmt 0x7fc3e6927878 <col:41, line:19:1>
| |-DeclStmt 0x7fc3e6927048 <line:14:5, col:14>
| | `-VarDecl 0x7fc3e6926fc8 <col:5, col:13> col:9 used a 'int' cinit
| | `-IntegerLiteral 0x7fc3e6927028 <col:13> 'int' 1
| |-DeclStmt 0x7fc3e69270f8 <line:15:5, col:14>
| | `-VarDecl 0x7fc3e6927078 <col:5, col:13> col:9 used b 'int' cinit
| | `-IntegerLiteral 0x7fc3e69270d8 <col:13> 'int' 2
| |-DeclStmt 0x7fc3e6927688 <line:16:5, col:26>
| | `-VarDecl 0x7fc3e6927128 <col:5, line:11:15> line:16:9 used c 'int' cinit
| | `-BinaryOperator 0x7fc3e6927280 <col:13, line:11:15> 'int' '+'
| | |-BinaryOperator 0x7fc3e6927238 <line:16:13, col:17> 'int' '+'
| | | |-ImplicitCastExpr 0x7fc3e6927208 <col:13> 'int' <LValueToRValue>
| | | | `-DeclRefExpr 0x7fc3e6927188 <col:13> 'int' lvalue Var 0x7fc3e6926fc8 'a' 'int'
| | | `-ImplicitCastExpr 0x7fc3e6927220 <col:17> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fc3e69271c8 <col:17> 'int' lvalue Var 0x7fc3e6927078 'b' 'int'
| | `-IntegerLiteral 0x7fc3e6927260 <line:11:15> 'int' 3
| |-CallExpr 0x7fc3e69277c0 <line:17:5, col:18> 'int'
| | |-ImplicitCastExpr 0x7fc3e69277a8 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7fc3e69276a0 <col:5> 'int (const char *, ...)' Function 0x7fc3e69272b0 'printf' 'int (const char *, ...)'
| | |-ImplicitCastExpr 0x7fc3e6927810 <col:12> 'const char *' <BitCast>
| | | `-ImplicitCastExpr 0x7fc3e69277f8 <col:12> 'char *' <ArrayToPointerDecay>
| | | `-StringLiteral 0x7fc3e6927708 <col:12> 'char [3]' lvalue "%d"
| | `-ImplicitCastExpr 0x7fc3e6927828 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fc3e6927738 <col:17> 'int' lvalue Var 0x7fc3e6927128 'c' 'int'
| `-ReturnStmt 0x7fc3e6927860 <line:18:5, col:12>
| `-IntegerLiteral 0x7fc3e6927840 <col:12> 'int' 0
|-FunctionDecl 0x7fc3e6927a40 <line:23:1, line:25:1> line:23:6 test 'void (int, int)'
| |-ParmVarDecl 0x7fc3e69278c8 <col:11, col:15> col:15 used a 'int'
| |-ParmVarDecl 0x7fc3e6927940 <col:18, col:22> col:22 used b 'int'
| `-CompoundStmt 0x7fc3e6927c88 <col:24, line:25:1>
| `-DeclStmt 0x7fc3e6927c70 <line:24:5, col:24>
| `-VarDecl 0x7fc3e6927b20 <col:5, col:21> col:9 c 'int' cinit
| `-BinaryOperator 0x7fc3e6927c48 <col:13, col:21> 'int' '+'
| |-BinaryOperator 0x7fc3e6927c00 <col:13, col:17> 'int' '*'
| | |-ImplicitCastExpr 0x7fc3e6927bd0 <col:13> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fc3e6927b80 <col:13> 'int' lvalue ParmVar 0x7fc3e69278c8 'a' 'int'
| | `-ImplicitCastExpr 0x7fc3e6927be8 <col:17> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fc3e6927ba8 <col:17> 'int' lvalue ParmVar 0x7fc3e6927940 'b' 'int'
| `-IntegerLiteral 0x7fc3e6927c28 <col:21> 'int' 100
`-<undeserialized declarations>
我们编写了一个函数
void test(int a, int b){
int c = a * b + 100;
}
对应的语法树模型如下图
语意分析(生成语法树AST的基础上进行语意检查)之后就会生成中间代码IR
3.中间代码IR
LLVM IR有三种表现形式,类似于水有三种形态,气态,液态和固态
1.文本text型
便于阅读的文本,类似于汇编语言,.ll文件
clang -S -emit-llvm main.m
; Function Attrs: noinline nounwind optnone ssp uwtable
define void @test(i32, i32) #2 { ; 有个全局函数@test (a,b)
%3 = alloca i32, align 4 ; 局部变量 c
%4 = alloca i32, align 4 ; 局部变量 d
%5 = alloca i32, align 4 ; 局部变量 e
store i32 %0, i32* %3, align 4 ; %0 赋值给%3 c = a
store i32 %1, i32* %4, align 4 ; %1 赋值给%4 d = b
%6 = load i32, i32* %3, align 4 ; 读取%3,赋值给%6 就是函数参数a
%7 = load i32, i32* %4, align 4 ; 读取%4,赋值给%7 就是函数参数b
%8 = mul nsw i32 %6, %7 ; a * b
%9 = add nsw i32 %8, 100 ; a * b + 100
store i32 %9, i32* %5, align 4 ; 参数 %9 赋值给 %5 e ===> 就是转换前函数写的int c变量
ret void
}
上面就是test函数对应的中间代码
IR基本语法
注释以;开头
全局表示以@开头,局部变量以%开头
alloca在函数栈帧中分配内存
i32 32位 4个字节的意思
align 字节对齐
store写入
load读取
2.Memory
3.bitcode二进制格式
拓展名.dc
clang -c -emit-llvm main.m
如何在实际中应用这些特性
很明显,流程很清晰,知道就是知道了,但是为什么要了解这个?因为可以自己写插件
之所以 clang 很酷:是因为它是一个开源的项目、并且它是一个非常好的工程:几乎可以说全身是宝。使用者可以创建自己的 clang 版本,针对自己的需求对其进行改造。比如说,可以改变 clang 生成代码的方式,增加更强的类型检查,或者按照自己的定义进行代码的检查分析等等。要想达成以上的目标,有很多种方法,其中最简单的就是使用一个名为 libclang 的C类库。libclang 提供的 API 非常简单,可以对 C 和 clang 做桥接,并可以用它对所有的源码做分析处理。不过,根据我的经验,如果使用者的需求更高,那么 libclang 就不怎么行了。针对这种情况,推荐使用 Clangkit,这个库有点久了,但是可以学习下思路,它是基于 clang 提供的功能,用 Objective-C 进行封装的一个库。
最后,clang 还提供了一个直接使用 LibTooling 的 C++ 类库。这里要做的事儿比较多,而且涉及到 C++,但是它能够发挥 clang 的强大功能。用它你可以对源码做任意类型的分析,甚至重写程序。如果你想要给 clang 添加一些自定义的分析、创建自己的重构器 (refactorer)、或者需要基于现有代码做出大量修改,甚至想要基于工程生成相关图形或者文档,那么 LibTooling 是很好的选择。
Libclang和Libtooling这两个库到底有什么不同?
在开始动手之前,我们应该先大致了解一下LibTooling。Clang的LibTooling是一个独立的库,它允许使用者很方便地搭建属于你自己的编译器前端工具。libclang是另外一个不错的选择,它提供给使用者基于C的稳定的编程接口,隔离了编译器底层的复杂设计,拥有更强的Clang版本兼容性,以及更好的多语言支持能力,对于大多数分析AST的场景来说,libclang是一个很好入手的选择。libTooling的优点与缺点一样明显,它基于C++接口,读起来晦涩难懂,但是提供给使用者远比libclang强大全面的AST解析和控制能力,同时由于它与Clang的内核过于接近导致它的版本兼容能力比libclang差得多,Clang的变动很容易影响到LibTooling。一般来说,如果你只需要语法分析或者做代码补全这类功能,libclang将是你避免掉坑的最佳的选择。我们之所以选择libTooling还有一个重要的原因是它提供了完整的参数解析方案,可以很方便的构建一个独立的命令行工具。这是libclang所不具备的能力。
官方对于如何进行选择的解释请看这里。有兴趣了解更多关于libclang,可以看官方doxygen文档以及这篇文章libclang: Thinking Beyond the Compiler
上面说了那么说,来总结一下:
1.LLVM编译一个源文件的过程:
预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件
2.基于LLVM,我们可以做什么?
- libclang、libTooling做语法树分析,实现语言转换OC转Swift、JS or 其它语言,字符串加密。官方文档使用
- 编写ClangPlugin,命名规范,代码规范,扩展功能。ClangPlugin ClangExample 官方案例三
- 编写Pass,代码混淆优化 官方文档写Pass
大厂的一些实践:
滴滴的动态化方案
DynamicCocoa
就是使用了一个将 OC 源码转 JS 的插件来进行代码的转换.
鹅厂的动态化方案OCS
是直接在端内写了个编译器.
可以搜索 《DynamicCocoa:滴滴 iOS 动态化方案的诞生与起航》 和 《OCS——史上最疯狂的iOS动态化方案》查看介绍。
3.开发自己的语言
以下三个方案Demo都来源于网上,自己可以根据自己项目来选择,我就不把自己项目的实现贴出来了,大家看看Demo自己根据需要来实现即可,这里帮大家搜集了三种不同阶段的混淆方案,正常LLVM有三个阶段 前端 LLVM优化 后端三个阶段,以下三个方案分别是在LLVM编译之前,LLVM IR优化和前段(Clang插件)三个阶段分别做了简单的Demo实践
混淆方案一(代码阶段)
该方案也是非常容易理解和上手的,我们一般在代码层面会写一个比较敏感的字符串,我们就可以在代码阶段通过脚本语言例如Python进行混淆(暂时就叫iOS字符串硬编码混淆方案)
混淆原理:因为硬编码的字符串是在可执行文件 Mach-O 全局的数据区,在符号表中很容易被搜索到,而字符串数组则不会。
下载完Demo可以看到如下目录结构
打开Xcode文件,这里有个confusion文件,这里是python3编写的脚本,一般系统自带Python2.7,需要安装Python3我推荐用HomeBrew来安装,同一管理即可。首先打开项目,先不管,直接跑,然后用Hopper Disassembler来看下反汇编是否有我们写的明文
可以看到hello world就能通过反汇编出来,那么就需要混淆了。这里用Python脚本来直接修改项目中的字符串,Demo里面用宏来标记,然后脚本来修改标记的字符串进行混淆,然后再跑Demo,再看下反汇编代码
可以看到已经没有了,这是最上层最容易理解的混淆方案,具体操作可以看下Demo,下面录了一个操作步骤演示,Python脚本混淆前后对比
这里采用的是直接修改源代码的方式做混淆。首先对需要混淆的字符串用宏打上标记,然后使用脚本过滤所有源代码,生成混淆过的代码。因为混淆过的代码可读性极差,不利于维护,所以就需要解混淆代码,用于把混淆过的代码做还原。
混淆方案二(LLVM IR阶段)
OLLVM的混淆操作就是在中间表示IR层,通过编写Pass来混淆IR,然后后端依据IR来生成的目标代码也就被混淆了。得益于LLVM的设计,OLLVM适用LLVM支持的所有语言(C,C++,Objective-C,Ada,Fortran)和目标平台(x86,x86-64,PowerPC,PowerPC-64, ARM, Thumb, SPARC, Alpha, CellSPU, MIPS, MSP430, SystemZ, 和 XCore)
可以看看这个文章,早几年,这个方案就可以用,但是貌似不维护了,Xcode9和Xcode10之后反正不能用了,后续就出现了这个,专门针对这个做了高版本的适配。
首先Xcode的BuildSetting里面的对应的compiler选项对应的插件路径是在
mikejingdeMacBook-Pro:~ MKJ$ cd /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/
mikejingdeMacBook-Pro:Plug-ins MKJ$ ls
Clang LLVM 1.0.xcplugin Obfuscator.xcplugin
Core Data.xcplugin SceneKit.xcplugin
CoreBuildTasks.xcplugin SpriteKit.xcplugin
IBCompilerPlugin.xcplugin XCLanguageSupport.xcplugin
Intents.xcplugin XCWatchKit1Support.xcplugin
MLKit.xcplugin XCWatchOSSupport.xcplugin
Metal.xcplugin assetvalidation.xcplugin
可以看到,这里有个Obfuscator就是我们要自己添加的插件。
上面的这个文章介绍的是早起版本LLVM版本对应的混淆插件,反正现在是不能用了,但是思路是一样的,那么现在都升级了X9和X10,怎么办,可以看看这个OLLVM库里面的issues 问题所在
可以看到,有人专门fork针对高版本做了适配-----> Hikari
其实他是基于 obfuscator 进行了Xcode9的适配
macOS Quick Install
This script assumes current working directory is not the user's home directory(aka
~/
).cd
to some where else if this is the case. This script also assumes you havecmake
andninja
installed, if not, use Homebrew and similar package managers to install them翻译如下,执行脚本需要自己在弄个文件,而且需要安装cmake和ninja,如果没有,直接下载Homebrew来安装这个两个,然后执行如下脚本
git clone -b release_70 https://github.com/HikariObfuscator/Hikari.git Hikari && 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
OK,只要一切OK,你能安装好了toolchain(后面你需要在Xcode中选择自己的编译工具)而且Hikari也编译好了。本来这个作者有些中文文档,但是不知道什么原因,全部被删掉了,可能开源作者有点生气,那么这里有个英文文档,这里先看了Compiler和Usage,看下基本面。
一下有两个文章介绍怎么给Xcode添加插件的笔记
首先执行根据介绍执行上面那坨脚本,主要是在Xcode上能选择这个toolchain
然后如果和OLLVM一样,可以自己在插件选择区域,复制一份Apple Default Clang插件,然后把对应的Info.Plist里面的文案改成我们自己配置的文案即可。最后在Xcode中选择配置即可。
步骤如下(以下步骤和OLLVM一样,无非编译的包版本不同而已,而且不那么做的话,执行上面的脚本生成toolchain也行)
准备
下载 Hikari
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Release ../Hikari/
make -j7
1.copy
$ cd /Applications/Xcode.app/Contents/PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins/
$ sudo cp -r Clang\ LLVM\ 1.0.xcplugin/ Obfuscator.xcplugin
$ cd Obfuscator.xcplugin/Contents/
$ sudo plutil -convert xml1 Info.plist
$ sudo vim Info.plist
2.change
<string>com.apple.compilers.clang</string> -> <string>com.apple.compilers.obfuscator</string>
<string>Clang LLVM 1.0 Compiler Xcode Plug-in</string> -> <string>Obfuscator Xcode Plug-in</string>
3.xcspec移动
$ sudo plutil -convert binary1 Info.plist
$ cd Resources/
$ sudo mv Clang\ LLVM\ 1.0.xcspec Obfuscator.xcspec
$ sudo vim Obfuscator.xcspec
4.xcsprc修改
<key>Description</key>
<string>Apple LLVM 9.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>ExecPath</key>
<string>clang</string> -> <string>//Users/MKJ/Desktop/MK/build/bin/clang(这一坨就是我们编译的LLVM Clang)</string>
<key>Identifier</key>
<string>com.apple.compilers.llvm.clang.1_0</string> -> <string>com.apple.compilers.llvm.obfuscator.4_0</string>
<key>Name</key>
<string>Apple LLVM 9.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>9.0</string> -> <string>4.0</string>
5.
$ cd English.lproj/
$ sudo mv Apple\ LLVM\ 9.0.strings "Obfuscator 3.4.strings"
$ sudo plutil -convert xml1 Obfuscator\ 3.4.strings
$ sudo vim Obfuscator\ 3.4.strings
6.
<key>Description</key>
<string>Apple LLVM 9.0 compiler</string> -> <string>Obfuscator 4.0 compiler</string>
<key>Name</key>
<string>Apple LLVM 9.0</string> -> <string>Obfuscator 4.0</string>
<key>Vendor</key>
<string>Apple</string> -> <string>HEIG-VD</string>
<key>Version</key>
<string>7.0</string> -> <string>4.0</string>
7.
sudo plutil -convert binary1 Obfuscator\ 4.0.strings
以上都是准备工作,操作完之后Xcode启动Demo
1.选择Xcode -> Toolchains -> Hikari将混淆工具和项目关联。
2.将所有与要运行的target相关的target(包括pod进来的库)Enable Index-While-Building 的值改为NO。
3.Optimization Level 的值设置为 None[-O0]
3.如果是全局混淆,则在需要混淆的target中直接将Other C Flags的值加上所需的混淆标记
Other C Flags
* 每个flag前加需要上-mllvm
常用组合
-mllvm -enable-fco
-mllvm -enable-funcwra
-mllvm -enable-strcry
-mllvm -enable-acdobf
-mllvm -enable-bcfobf 启用伪控制流
-mllvm -enable-cffobf 启用控制流平坦化
-mllvm -enable-splitobf 启用基本块分割
-mllvm -enable-subobf 启用指令替换
-mllvm -enable-acdobf 启用反class-dump
-mllvm -enable-indibran 启用基于寄存器的相对跳转,配合其他加固可以彻底破坏IDA/Hopper的伪代码(俗称F5)
-mllvm -enable-strcry 启用字符串加密
-mllvm -enable-funcwra 启用函数封装
-mllvm -enable-allobf 依次性启用上述所有标记
以下是操作步骤,Xcode 10测试,混淆前就不测试了,用Hopper Disassemble打开的话是可以搜索到明文的,下面看看混淆操作启动,而且不一个个测试了,我这里直接启用-mllvm -enable-allobf这个选项,全部进行标记
可以看到,混淆之后的反编译代码中没有了mikejing这些个关键字,其他测试可以自行测试下结果。
混淆方案三(LLVM Clang插件 Xcode10我没成功 等有空再试试)
注意:X9可以,X10编译不过了已经,但是思路如下,可以参考参考
参考文章 B站大神泰戈尔
首先让我们来看下 LLVM 子项目都有哪些:
1)LLVM Core
提供了一个现代的源代码和目标独立优化器, 以及许多流行的 CPU (甚至是一些不太常见的处理器) 的汇编代码生成支持。
2)Clang
一个 C/C++/Objective-C 编译器,致力于提供令人惊讶的快速编译,极其有用的错误和警告信息,提供一个可用于构建很棒的源代码级别的工具。
3)dragonegg
GCC 插件,可将 GCC 的优化和代码生成器替换为 LLVM 的相应工具。
4)LLDB
基于LLVM提供的库和Clang构建的优秀的本地调试器。
5)libc++、libc++ ABI
符合标准的,高性能的C++标准库实现,以及对 C++11 的完整支持。
6)compiler-rt
针对 __fixunsdfdi
和其他目标机器上没有一个核心 IR(intermediate representation) 对应的短原生指令序列时,提供高度调优过的底层代码生成支持。
7)OpenMP
Clang 中对多平台并行编程的runtime支持。
8)vmkit
基于 LLVM 的 Java 和 .NET 虚拟机实现
9)polly
支持高级别的循环和数据本地化优化支持的 LLVM 框架。
10)libclc
OpenCL 标准库的实现
11)klee
基于 LLVM 编译基础设施的符号化虚拟机
12)SAFECode
内存安全的C/C++编译器
13)lld
Clang/LLVM 内置的链接器
首先,Clang插件我在Xcode10上面编写一直跑不通,权当做做个笔记记录下流程,网上有个混淆函数相关的,也是通过Clang来做的,如果各位是X10以下,可以试试,我试过X10不行 项目混淆函数
Clang插件开发流程
Clang 作为 LLVM 提供的编译器前端,将用户的源代码 (C/C++/Objective-C) 编译成语言/目标设备无关的IR实现。并且提供良好的插件支持,容许用户在编译时,运行额外的自定义动作。既然能做额外的自定义,那么我们做字符串分析,加密,OS和JS转换,命名规范,代码规范,扩展功能都可以实现,这里就通过Demo简单实现下第一个Clang插件步骤,抛砖引玉,各路大神自己写cpp,实现各种自定义业务逻辑。
1.准备
Clang 需要用 CMake 和 Ninja 来编译,可以通过 Homebrew 安装
2.下载LLVM和CLang
Clang 源码需要安装到 llvm/tools 目录下 如果这里的两个llvm和clang都不能下载,就去Github找个mirror即可
mikejingdeMacBook-Pro:~ MKJ$ cd Desktop/
mikejingdeMacBook-Pro:Desktop MKJ$ mkdir 1
mikejingdeMacBook-Pro:Desktop MKJ$ cd 1/
git clone https://git.llvm.org/git/llvm.git/
cd llvm/tools
git clone https://git.llvm.org/git/clang.git/
3.编译
A。使用Ninja编译
在 llvm 同级目录下新建一个 llvm_build 目录,然后执行以下操作
cd llvm_build
cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=../llvm_release
# -DCMAKE_INSTALL_PREFIX 指定 LLVM 的安装路径
在当前目录llvm_build目录中出现build.ninja模板后,才是真正编译好,接着执行
ninja
ninja install
目录下llvm_build和llvm_release就是Ninja编译出来的LLVM Clang
B。使用Xcode编译
在 llvm 同级目录下新建一个 llvm_Xcode 目录,然后执行以下操作
cd llvm_xcode
cmake -G Xcode ../llvm
生成文件LLVM.xcodeproj,打开
然后吃饭去吧,吃完饭可能还在编译。。。。。。
4.插件编写
这个插件实现的功能就是,检测 ObjC 中的类声明,按照一般的类声明规范,类名需要驼峰式的拼写方法,并且类名第一个字母需要大写,不能有 "_",如果不符合这个规范则需要在编译过程中提示警告信息。
1)在 llvm/tools/clang/tools 源码目录下新建一个插件目录,假设叫做 MyPlugin,如下图所示标记 2。
2)修改下图中标记1 CMakeLists.txt 文件的内容,在最后一行添加如下内容:
3)在 MyPlugin 目录中添加文件 MyPlugin.cpp,用于编写插件代码。
4)在 MyPlugin 目录中添加文件 CMakeLists.txt 如图标记3,并添加如下内容
add_llvm_loadable_module(MyPlugin MyPlugin.cpp)
5)在 MyPlugin.cpp 中编写如下代码:
6)编写完代码后,重新输入以下命令
# 注意cd到llvm_xcode
cmake -G Xcode ../llvm
7)然后选择 MyPlugin 这个Target 进行编译,编译完后会生成一个动态库文件 MyPlugin.dylib(可以在 Products 中找到),为了方便可以把这个文件放到桌面。
5.使用插件
1)命令行使用
首先用命令行对单文件测试一下刚刚生成的 Clang 插件是否正确,新建一个测试用文件 test.m 放在桌面,test.m 如下:
@interface ViewController : UIViewController
@end
@implementation ViewController
- (instancetype)init
{
if(self = [super init]){
}
return self;
}
@end
然后把 test.m 文件和 MyPlugin.dylib 放到桌面的同一个目录下,
接着命令行 cd 到该目录,然后执行以下命令:
../1/llvm_build/bin/clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.1.sdk -Xclang -load -Xclang ./MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin -c ./test.m
但是Xcode10貌似不如所愿,还是只有这些错误,X9做测试就可以
2)Xcode中使用插件
首先和Harika一样,需要给插件系统来个xcplugin(Hack Xcode)下载地址
里面有 HackedBuildSystem.xcspec
和 HackedClang.xcplugin
两个文件,修改一下 HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec
文件,将 ExecPath
的值修改为你编译出来的 Clang 的目录:
BuiltinJambaseRuleName = ProcessC;
ExecPath = "/Users/MKJ/Desktop/1/llvm_build/bin/clang";
UseCPlusPlusCompilerDriverWhenBundlizing = Yes;
然后在解压的 XcodeHacking 目录下,执行以下命令将 XcodeHacking 的内容移动到Xcode内:
sudo mv HackedClang.xcplugin/ `xcode-select -print-path`/../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins
sudo mv HackedBuildSystem.xcspec `xcode-select -print-path`/Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications
执行完上述操作后,重启 Xcode,重启后修改Xcode的编译器
点击 Target 的 Build Settings,修改 Compiler for C/C++/Objective-C 项为 Clang LLVM Trunk
然后选择加载插件,依旧是Build Settings,搜索CFLag 添加选项为 如下
-Xclang -load -Xclang /Users/MKJ/Desktop/2/MyPlugin.dylib -Xclang -add-plugin -Xclang MyPlugin
注意,这里的插件地址就是我们上面编译出来移动到的位置地址
X9编译后就会出现如下报错信息,信息是根据cpp文件编译出来dylib所展示,但是X10我试了就炸了,死活过不了
我为了第三个方案的作者 Bilibili 泰戈尔,他说是X9做的,我试过X10编译不过,本来想请教他一下,可能大神比较忙,没时间做适配,我自己搞了半天第三个方案死活过不了,希望有空适配下,看下到底思路哪里有问题。。。
扩展阅读
网上有几个文章也介绍如何写Clang插件,但是为数真的不多,但问题是都是按照X9做的,我估计X10又不行了,但是思路还是在的,就当学习下第三个方案的思路
总结下
这方面文章真的不多,我觉得很多我都看了一遍了,按这次介绍的目的混淆来说,其实主要针对马甲包来说,还有我们自己的主包拿去第三方机构进行安全性评估的时候别人报告上提到的几个风险漏洞做了深入学习,其中一个就是反编译明文这个不好的安全漏洞。
1.方案一来讲,应该是最容易理解,而且可用性比较高,难度也不大,懂点脚本,自己写一下即可,项目中也可以用,Apple也不会为难这种包
2.方案二来讲,相对需要LLVM的基础,不然你完全不知道他在干什么,其实就是IR优化pass的编写,Hirika是OLLVM的进化高级适配版本,多了几个新功能,具体可以看上面介绍,我们只要集成toolchain,换个默认编译器名字即可使用,亲测X10安全可行,但是issue里面也有提到,这种包的混淆加固等,现在可能会遭到Apple的拒绝,慎用慎用
3.方案三来讲,针对编译器前段Clang的插件编写,Clang前段插件由于能搞出AST语法数,因此会有很大的操作控件,很多插件也应运而生,但是插件的注入X9还行,X10我试了几次一直失败,希望有大神看到这个,如果能适配X10,希望给个介绍或者Demo交流下,不胜感激
一份小小的学习笔记,算是初步了解下LLVM以及一些混淆方案的实践
参考文献