Code_obfuscation
前言
在编译器处理分析生成代码的时候,来 增加一些无用的代码、拆分代码块、使代码扁平化,进而提升静态分析的难度。
Chris Lattner 生于 1978 年,2005年加入苹果,将苹果使用的 GCC 全面转为 LLVM。2010年开始主导开发 Swift 语言。
- Xcode的编译器前端是clang,clang是llvm的一部分;而llvm本身是开源的,基于llvm 进行代码混淆的工具中,本人最终喜欢的马甲包混淆方案是用
Hikari
,具体的用法看这里
- Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器 http://clang.llvm.org/docs/
- 其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码,当然这个过程会对代码进行检查,出错的和需要警告的会标注出来。
- lld 是 Clang / LLVM 的内置链接器,clang 必须调用链接器来产生可执行文件。
LLVM编译一个源文件的过程:预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件
1、预处理
- 符号化 (Tokenization)
- 宏定义的展开
- #include 的展开
- 语法和语义分析
2、将符号化后的内容转化为一棵解析树 (parse tree)
- 解析树做语义分析
- 输出一棵抽象语法树(Abstract Syntax Tree* (AST))
- 生成代码和优化
3、将 AST 转换为更低级的中间码 (LLVM IR)
- 对生成的中间码做优化
- LLVM IR 有三种表示格式,第一种是 bitcode 这样的存储格式,以 .bc 做后缀,第二种是可读的以 .ll,第三种是用于开发时操作 LLVM IR 的内存格式
- 生成特定目标代码
- 输出汇编代码
4、汇编器
- 将汇编代码转换为目标对象文件。
5、链接器
- 将多个目标对象文件合并为一个可执行文件 (或者一个动态库)
可以用Clang做什么?
LibTooling:
我们也可以自己写一个这样的工具去遍历、访问、甚至修改语法树。 目录:llvm/tools/clang/tools
上面的代码通过遍历语法树,去修改里面的方法名和返回变量名:
- 那么,我们看到
LibTooling
对代码的语法树有完全的控制,那么我们可以基于它去检查命名的规范,甚至做一个代码的转换,比如实现OC转Swift。 对语法树有完全的控制权,可以作为一个单独的命令使用,如:clang-format
ClangPlugin:对语法树有完全的控制权,作为插件注入到编译流程中,可以影响build和决定编译过程。目录:
llvm/tools/clang/examples
- 可以用来定义一些编码规范,比如代码风格检查,命名检查等等
1\编译的完整步骤
- 1)编译信息写入辅助文件,创建文件架构 .app 文件
- 2)处理文件打包信息
- 3)执行 CocoaPod 编译前脚本,checkPods Manifest.lock
- 4)编译.m文件,使用 CompileC 和 clang 命令
- 5)链接需要的 Framework
- 6)编译 xib
- 7)拷贝 xib ,资源文件
- 8)编译 ImageAssets
- 9)处理 info.plist
- 10)执行 CocoaPod 脚本
- 11)拷贝标准库
- 12)创建 .app 文件和签名
2\clang 命令参数
- -x 编译语言比如objective-c
- -arch 编译的架构,比如arm7
- -f 以-f开头的。
- -W 以-W开头的,可以通过这些定制编译警告
- -D 以-D开头的,指的是预编译宏,通过这些宏可以实现条件编译
- -iPhoneSimulator10.1.sdk 编译采用的iOS SDK版本
- -I 把编译信息写入指定的辅助文件
- -F 需要的Framework
- -c 标识符指明需要运行预处理器,语法分析,类型检查,LLVM生成优化以及汇编代码生成.o文件
- -o 编译结果
3\xcode 编译器的相关设置
- Build Phases:构建可执行文件的规则
- 1)指定 target 的依赖项目,在 target build 之前需要先 build 的依赖。在 Compile Source 中指定所有必须编译的文件,
- 2)在 Link Binary With Libraries 里会列出所有的静态库和动态库,它们会和编译生成的目标文件进行链接。
- 3)build phase 还会把静态资源拷贝到 bundle 里。
- 4)可以通过在 build phases 里添加自定义脚本来做些事情,比如像 CocoaPods 所做的那样。
- Build Rules: 指定不同文件类型如何编译
- Build Settings : 在 build 的过程中各个阶段的选项的设置
4\Clang 使用的例子 : 从源码到可执行文件

1、 先通过-E查看clang在预编译处理这步做了什么: 包括宏的替换,macro宏的展开 import/include头文件的导入,以及#if等处理
clang -E main.m
后来Xcode新建的项目里面去掉了pch文件,引入了moduels的概念,把一些通用的库打成modules的形式,然后导入,默认会加上-fmodules参数。
clang -E -fmodules main.m
这样的话,只需要@import一下就能导入对应库的modules模块了。 2、预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等
- clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
3、然后是语法分析,验证语法是否正确,然后将所有节点组成抽象语法树 AST
- clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 4、IR中间代码的生成:CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入
可以在中间代码层次去做一些优化工作,我们在Xcode的编译设置里面也可以设置优化级别-O1
,-O3
,-Os
。 还可以去写一些自己的Pass,这里需要解释一下什么是Pass。 1、Pass 教程: http://llvm.org/docs/WritingAnLLVMPass.html
- Pass就是LLVM系统转化和优化的工作的一个节点,每个节点做一些工作,这些工作加起来就构成了LLVM整个系统的优化和转化。
2、如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。
- 生成字节码 (LLVM Bitcode)
- 在Xcode7中默认生成bitcode就是这种的中间形式存在, 开启了bitcode,那么苹果后台拿到的就是这种中间代码,苹果可以对bitcode做一个进一步的优化,如果有新的后端架构,仍然可以用这份bitcode去生成
- clang -emit-llvm -c main.m -o main.bc
5、生成汇编
- clang -S -fobjc-arc main.m -o main.s
6、 生成目标文件
- clang -fmodules -c main.m -o main.o
7、生成可执行文件
- clang main.o -o main

8\整体的流程
5\Clang Static Analyzer静态代码分析: https://code.woboq.org/llvm/clang/
静态分析前会对源代码分词成 Token,这个过程称为词法分析(Lexical Analysis)
编译的概念(词法->语法->语义->IR->优化->CodeGen) 在 clang static analyzer 里到处可见。
Token 可以分为以下几类
- 关键字:语法中的关键字,if else while for 等。
- 标识符:变量名
- 字面量:值,数字,字符串
- 特殊符号:加减乘除等符号
语法分析(Semantic Analysis)将 token 先按照语法组合成语义生成 VarDecl 节点,然后将这些节点按照层级关系构成抽象语法树 Abstract Syntax Tree (AST)。
6\CodeGen 生成 IR 代码
- 1)各种类,方法,成员变量等的结构体的生成,并将其放到对应的Mach-O的section中。
- 2)Non-Fragile ABI 合成 OBJCIVAR$_ 偏移值常量。
- 3)ObjCMessageExpr 翻译成相应版本的 objc_msgSend,super 翻译成 objc_msgSendSuper。
- 4)strong,weak,copy,atomic 合成 @property 自动实现 setter 和 getter。
- 5)@synthesize 的处理。
- 6)生成 block_layout 数据结构
- 7)block 和 weak
- 8)_block_invoke
- 9)ARC 处理,插入 objc_storeStrong 和 objc_storeWeak 等 ARC 代码。ObjCAutoreleasePoolStmt 转 objc_autorealeasePoolPush / Pop。自动添加 [super dealloc]。给每个 ivar 的类合成 .cxx_destructor 方法自动释放类的成员变量。
7\ IR 结构

- llc 编译器是专门编译 LLVM IR 的编译器用来生成汇编文件。
- Driver
- Driver 是 Clang 面对用户的接口,用来解析 Option 设置,判断决定调用的工具链,最终完成整个编译过程。
- Translate:Translate 就是把相关的参数对应到不同平台上不同的工具。
- 每次编译后生成的 dSYM 文件 -
- dSYM 文件里存储了函数地址映射,这样调用栈里的地址可以通过 dSYM 这个映射表能够获得具体函数的位置。一般都会用来处理 crash 时获取到的调用栈 .crash 文件将其符号化
I \什么是LLVM?
llvm 是一系列 分模块和可重用的编译工具链,他提供了一种代码编写良好的中间表示(IR),可以作为多种语言的后端,还可以提供与编程无关的优化和针对多种CPU的代码生成功能。
LLVM架构的主要组成部分
- 前端:前端用来获取源代码然后将它转变为某种中间表示,我们可以选择不同的编译器来作为LLVM的前端,如gcc,clang。
- Pass(通常翻译为“流程”):Pass用来将程序的中间表示之间相互变换。一般情况下,Pass可以用来优化代码,这部分通常是我们关注的部分。
- 后端:后端用来生成实际的机器码
- The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
传统的编译器的架构如下

LLVM的架构如下

基于LLVM进行代码混淆时,只需关注中间层表示(IR)
基于LLVM,我们可以做什么?
- 做语法树分析,实现语言转换OC转Swift、JS or 其它语言,字符串加密。
- 编写ClangPlugin,命名规范,代码规范,扩展功能。
- 编写Pass,代码混淆优化。
II \ 安装编译LLVM
下载
1.直接从官网下载:http://releases.llvm.org/download.html 2.svn获取
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm 3.git获取
编译
最新的LLVM只支持cmake来编译了,首先安装cmake。
编译:
编译生成Xcode项目的命令
- 打开Xcode的时候,选择
munually manage sechme
,选择需要编译的Target(clang),接下来执行command+R
编译完成后,就能在build/bin/
目录下面找到生成的工具了。
- clang 的版本和Xcode的版本不一样,苹果在开源的LLVM中增加了自己的修改
III 、PassDemo
- pass 处理编译过程中代码的额转换以及优化工作。所有的pass都是pass的子类,不同的passs实现不同的作用,可以分别继承不同的pass类实现对应的功能
- ModulePass
- CallGraphSCCPass
- FunctionPass
- BasicBlockPass
- RegionPass
- LoopPass
接下来重点介绍如何编写、编译、加载、运行一个pass
配置编译环境
在lib/Transforms/新建一个文件夹 Hello 配置编译脚本去编译源代码
修改.../Transforms/CMakeLists.txt
, 加上刚刚写的模块hello
编译脚本指定使用编译源文件 Hello.cpp 生成 $(LEVEL)/lib/LLVMHello.dylib 动态库,该文件可以被opt 通过-load 参数动态加载。 open Xcode 重新build,在项目里面添加LLVMHello 的target为secheme;在过程中找到 loadable modules 下面LLVMHello 里面的hello.cpp ,开始编写代码。
编写代码

编写一个pass操作function,输出一些相关信息,需要使用如下头文件
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
这些头文件里面的方法属于LLVM命名空间,需要声明使用LLVM命名空间 编写一个匿名空间;C++的匿名空间和c的static 关键词一样,定义在匿名空间中的变量仅对当前文件可见。
code 初始化passID
code: 因为LLVM将使用ID的地址去识别一个pass,因此初始化的value不重要 注册pass hello,指定命令行参数为“hello”,名字说明为“hello world pass”
code 选中target LLVMhello 执行command+B,生成build/debug/lib/LLVMHello.dylib hello2 :Hello World Pass (with getAnalysisUsage implemented)
code
使用opt 加载动态库,并指定参数
之前在代码中通过RegisterPass 注册了pass,现在可以准备一个测试用的源文件,通过opt -load 命令去加载动态库并指定参数hello
测试的源文件test.mm
code 编译源文件 生成bitcode: 因为Pass是作用于中间代码,所以我们首先要生成一份中间代码
- path/to/build/deBug/bin/clang -emit-llvm -c test.mm -o test.bc
然后加载Pass
- 在Xcode中将opt的target添加到scheme中。编辑scheme的启动参数,按command+R执行。
- ../build/bin/opt -load ../build/lib/LLVMHello.dylib -simplepass < test.bc > after_test.bc
通过-help 参数查看注册的pass参数 passManager 提供了-time-passes
参数用于输出pass的时间占比 编辑/llvm-3.9.0.src/tools/opt/CMakeLists.txt 将LLVMHello模块添加到opt 的依赖,这样在执行opt的时候能自动检测LLVMHello模块有没有被修改,如果被修改了。需要重新编译LLVMHello模块
其他操作
1、如果编写的pass用到了其他pass提供的函数功能,需要在getAnalysisUsage
中进行声明
例如想获取程序中存在的循环信息
导入头文件llvm/Analysis/LoopInfo.h,并在getAnalysisUsage
中进行调用 code
通过它提供的接口就可以获取循环的个数
2、LLVMSimplePass(写的Pass只是把a+b简单的替换成了a-(-b),只是一个演示,怎么去写自己的Pass,并且作用于代码)
1.创建头文件 :llvm-3.9.0.src/include/llvm/Transforms
统一存放头文件
写入内容: 2.创建源文件: llvm-3.9.0.src/lib/Transforms 存放源代码
CMakeLists.txt LLVMBuild.txt: SimplePass.cpp: 修改.../Transforms/LLVMBuild.txt
, 加上刚刚写的模块Obfuscation
修改.../Transforms/CMakeLists.txt
, 加上刚刚写的模块Obfuscation
编译生成:LLVMSimplePass.dylib
因为Pass是作用于中间代码,所以我们首先要生成一份中间代码: 然后加载Pass优化:
将Pass加入PassManager管理
上面我们是单独去加载Pass动态库,这里我们将Pass加入PassManager,这样我们就可以直接通过clang的参数去加载我们的Pass了。
首先在llvm/lib/Transforms/IPO/PassManagerBuilder.cpp
添加头文件。 添加如下code 然后在populateModulePassManager
这个函数中添加如下代码: 最后在IPO这个目录的LLVMBuild.txt
中添加库的支持,否则在编译的时候会提示链接错误。具体内容如下: 修改Pass的CMakeLists.txt为静态库形式: 最后再编译一次。
那么我们可以这么去调用:
基于Pass,我们可以做什么? 我们可以编写自己的Pass去混淆代码,以增加他人反编译的难度。

IV、OLLVM 源码分析,并从基于clang4.0 迁移到clang6.0
code: 基于llvm的代码混淆技术主要基于中间层代码的pass。
/lib/Transforms/CMakeLists.txt lib/Transforms/Obfuscation/CMakeLists.txt
- 将
add_llvm_library
修改为add_llvm_loadable_module
编译成动态库,通过opt 加载调试(如果添加到passManager中,编译成静态库,同样可以通过opt加载调试) 主要包含的三个pass,用于不同程序的混淆
- BogusControlFlow: 增加虚假的控制流程和无用的代码
- Flattening: 使代码扁平化
- Substitution: 增肌算术表达式的复杂度
编译生成动态库,分析BogusControlFlow
open xcode ,安装command+B ,选择LLVMObfuscation的scheme,编译生成动态库 1) 分析BogusControlFlow
编辑 scheme启动参数,添加-debug 来打印调试信息 在runOnFunction
下断点,该方法会在处理方法的时候自动调用
在runOnFunction 中先判断参数是否合法,在判断是否需要混淆,从而指定全混淆或者在满足某些函数属性的情况下混淆
// Check if the percentage is correct // Check if the number of applications is correct // If fla annotations
void bogus(Function &F) { 分析
// Put all the function’s block in a list 把方法中的所有block放到一个list 中。然后分别取出,调用addBogusFlow
增加虚假控制流程addBogusFlow
首先调用createAlteredBasicBlock
复制原来的BasicBlock,然后修改其变量,引用并插入一些脏数据,接着创建一个恒为true的条件跳转到原来BasicBlock。false的分支跳转到复制的BasicBlock,复制的BasicBlock跳转回原来的BasicBlock。最后,创建一个恒为true的条件跳转到原BasicBlock的结束位置,false分支跳转到复制的BasicBlock。
BasicBlock *alteredBB = createAlteredBasicBlock(originalBB, *var3, &F);
- 使用Graphviz 查看流程图,设置dot文件的默认打开方式。
- 调试过程使用
po originalBB->dump()
打印对应的结果信息及浏览当前的流程图。- po F.viewCFG()
- build/Debug/llvm-dis test_after.bc -o test_after.ll` 将混淆处理之后的bitcode转换为可读的bitcode代码
2)Flattening.cpp 扁平化功能的实现分析
opt -load ./lib/LLVMObfusaction.dylib -debug -flattening path/to/test.bc -o path/to/test_atfer.bc
一旦进入runOnFunction 函数,判断是否需要混淆;如果需要混淆,就调用flatten 方法,保存所有的BasicBlock;如果个数小于等于1 就认为Nothing to flatten
直接返回;然后移除第一个BasicBlockRemove first BB
.Get a pointer on the first BB
接着判断第一个BasicBlock是不是条件判断或者分支,如果是就使用splitBasicBlock 将其分开,并移除第一个BasicBlock。 分别创建一个loopEntry和一个loopEnd的BasicBlock,分别将第一个BasicBlock和loopEnd跳转到loopEntry,创建一个指向loopEnd的switchDefault,并将loopEntry 指向switchDefault。Put all BB in the switch
将之前保存的BasicBlock分别添加到switch分支中。 将switch中的所有的BasicBlock 指向loopEnd 并调整switch case。 3)最后一个Substitution 将一些计算指令替换成一些复杂的转换表达式(这种混淆在开启编译器优化之后都会失效,除非指定了不优化函数)
V 、替换Xcode编译器,在编译时将用带混淆工程的clang进行编译工程
要想通过clang 加载pass,就不能采用这种动态库方式(add_llvm_loadable_module),而是要编译成静态库(add_llvm_library)并加入passManager的管理机制定义的clang 传入的参数去调用pass
编辑lib/Transforms/Obfuscation/CMakeLists.txt 将add_llvm_loadable_module 修改为add_llvm_library 复制lib/Transforms/Obfuscation/LLVMBuild.txt到对应的位置 在lib/Transforms/IPO/LLVMBuild.txt 的required_libraries 增加Obfuscation 在lib/Transforms/LLVMBuild.txt 的subdirectories中加入Obfuscation 编辑PassManagerBuilder.cpp,导入头文件以及指定参数
增加混淆参数// Flags for obfuscation
在PassManagerBuilder::PassManagerBuilder() {
中初始化随机生成器// Initialization of the global cryptographically
secure pseudo-random generator
在void PassManagerBuilder::populateModulePassManager(
中增加混淆的pass// Allow forcing function attributes as a debugging and tuning aid.
重新编译生成clang 测试clang
build/Debug/bin/clang test.mm -o test -mllvm -sub -fla -mllvm -bcf
看看是否有混淆效果 增加Xcode使用的编译工具链
方式一比较笨,不建议使用
setup change: 修改info,plist
then
change Obfuscator.xcspec then 修改English.lproj 环境配置
change then : 修改完成之后,重启Xcode 开始使用
Build Settings -> Compiler for C/C++/Objective-C -> Obfuscator 4.0 Build Setting -> Enable Index-While-Building Functionality -> ‘default’ change to ‘No’
- 开源的clang并不能识别:Xcode9 新增的swift_index_store_enable 参数
关闭 bitcode 关闭编译优化 Build Settings -> OPTIMIZATION_LEVEL -> 0
VI、静态库(StaticLib)混淆—基于编译器混淆的案例
编译器混淆是基于中间代码(bitcode)进行的。那么编译生成一个带bitcode的静态库是不是就可以提取其中的bitcode进行混淆?答案是也是
这样就不需要通过源文件进行编译了,只需要提供一个带bitcode的静态库文件即可进行混淆
—商业机密,因此不需要得到源码也可以轻松的混淆了。哈哈哈
新建一个静态工程,为了让其在非archive模式下也生成bitcode,需要在build setting的other c flags 中增加-fembed-bitcode
参数 将__LLVM,__bitcode
的section里面的bitcode代码提取出来
mkdir objects cd objects ar -x ../StaticLib.asegedit ./StaticLib.o -extract "__LLVM" "__bitcode" result.bc
- /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/segedit
使用带混淆功能的clang将其重新编译成目标文件,生存的result.o 就是混淆后的object文件。将其重新打包成.a文件
build/Debug/bin/clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs -fobjc-arc -c result.bc -mllvm -bcf -mllvm -sub -mllvm -fla -o result.o
- rm __.SYMDEF\ SORTED
- ar -crs result.a result.o
- ranlib result.a
- ranlib - add or update the table of contents of archive libraries
最后将混淆之后的静态库集成到app中。(此时的静态库没有了bitcode,因此整个主app也不能开启bitcode)
小结: 从静态库提出bitcode ,使用带有混淆功能的clang 重新编译bitcode进行混淆生成混淆之后的目标文件。并将目标文件进行打包成.a
思考题
将核心代码封装成静态库(包含bitcode的静态库),提供给我,进行混淆处理治理之后,再将混淆之后的静态库给他。—–目的是避免提供源代码给我。
接下来他们再将核心的静态库集成到app中进行上架。
- bitcode 是中间表示形式,常用于代码优化。
尝试使用/Users/devzkn/Library/Developer/Toolchains/Hikari.xctoolchain/usr/bin/clang
进行混淆
/Users/devzkn/Library/Developer/Toolchains/Hikari.xctoolchain/usr/bin/clang
如果是替换Xcode编译器即采用xctoolchain的形式的时候,对于Xcode而言所有的混淆标记应该加在Target的Other C Flags中
开启混淆 如果是单独使用带有混淆功能的clang,也是一样需要在每一个flag前加上-mllvm
build/Debug/bin/clang -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs -fobjc-arc -c result.bc -mllvm -bcf -mllvm -sub -mllvm -fla -o result.o
See Also
- 马甲包混淆方案/#more
- https://github.com/zhangkn/obfuscator
- https://zhangkn.github.io/2018/03/Hopper/
- LLVM IR 有三种表示格式,第一种是 bitcode 这样的存储格式,以 .bc 做后缀,第二种是可读的以 .ll,第三种是用于开发时操作 LLVM IR 的内存格式
- 当虚拟内存系统进行映射时,segment 和 section 会以不同的参数和权限被映射。
- __TEXT segment 包含了被执行的代码。它被以只读和可执行的方式映射
- __DATA segment 以可读写和不可执行的方式映射。它包含了将会被更改的数据。
- __nl_symbol_ptr 和 __la_symbol_ptr,它们分别是 non-lazy 和 lazy 符号指针。
- 延迟符号指针用于可执行文件中调用未定义的函数,例如不包含在可执行文件中的函数,它们将会延迟加载。而针对非延迟符号指针,当可执行文件被加载同时,也会被加载。
- 深入剖析 iOS 编译 Clang / LLVM 视频
- 深入剖析 iOS 编译 Clang / LLVM 文章 Low Level Virtual Machine
- knpost
评论
发表评论