天天看点

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

LLVM、代码混淆

  • 1 LLVM
  • 1.1 传统编译架构
    • 1.2 LLVM的编译架构
    • 1.3 Clang
      • 1.3.1 什么是Clang
      • 1.3.2 Clang与LLVM
  • 2. OC源文件的编译过程
    • 2.1 编译过程和预编译
    • 2.2 词法分析
    • 2.3 语法树-AST
    • 2.4中间代码(LLVM IR)
  • 3. LLVM源码
    • 3.1 LLVM的源码下载
    • 3.2 源码编译
      • 3.2.1 ninja编译方式
      • 3.2.2
  • 4 应用和实践
  • 5 clang插件开发
    • 5.1 clang插件开发1 – 插件目录
    • 5.2 clang插件开发2 – 插件必要文件
    • 5.3 clang插件开发3 – 编写插件源码
    • 5.4 clang插件开发4 – 编译插件
    • 5.5 clang插件开发5 – 加载插件
    • 5.6 clang插件开发6 – Hack Xcode
    • 5.7 clang插件开发7 – 修改Xcode的编译器
    • 5.8 clang插件开发8 – 编译项目
    • 5.9 clang插件开发9 – 更多
    • 5.10 提示警告、错误信息
    • 5.11 推荐书籍
  • 6 代码混淆
    • 6.1 基本概念
    • 6.2 IOS中代码混淆
      • 6.2.1 基本介绍
      • 6.2.2 源码的混淆 - 通过宏定义混淆方法名、类名
      • 6.2.3 iOS-class-guard
      • 6.2.4

1 LLVM

  • 什么是LLVM
    • 官网https://llvm.org
    • LLVM项目是模块化,可重用的编译器以及工具链技术的集合,LLVM不是首字母的缩写,它是项目名称

1.1 传统编译架构

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • Frontend:词法分析、语法分析、语义分析、生成中间代码
  • Optimizer:优化器(中间代码)
  • Backend:后端,生成机器码

1.2 LLVM的编译架构

  • 不同的前端后端使用统一的中间代码

    LLVM Intermediate Representation(LLVM IR)

  • 如果需要支持一种新的编程语言,那么只需要实现一个新的前端
  • 如果需要支持一种新的硬件设备,那么只需要实现一个新的 后端
  • 优化阶段是一个通用的阶段,它针对的是统一的LLVM IR,不论是支持新的变成语言,还是支持新的硬件设备,都不需要对优化阶段做修改
  • 相比之下,GCC的全段和后端没分的太开,前端和后端耦合在一起,所以GCC为了支持一门新的语言,或则为了支持一个新的目标平台,就变的特别困难
  • LLVM现在被作为实现各种金泰和运行编译语言的通用基础(GCC家族、Java、.NET、Python、Ruby、Scheme、Haskell、D等)
    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

1.3 Clang

1.3.1 什么是Clang

  • 什么是Clang?
    • LLVM项目的一个子项目
    • 基于LLVM架构的C/C++/Objective-C编译器

      前端

    • 官网:http://clang.llvm.org/
  • 相比于GCC,Clang具有如下优点:
    • 编译速度快:在某些平台上,Clang的编译速度显著的快过GCC(Debug模式下编译OC速度比GGC快3倍)
    • 占用内存小:Clang生成的AST所占用的内存是GCC的五分之一左右
    • 模块化设计:Clang采用基于库的模块化设计,易于 IDE 集成及其他用途的重用
    • 诊断信息可读性强:在编译过程中,Clang 创建并保留了大量详细的元数据 (metadata),有利于调试和错误报告
    • 设计清晰简单,容易理解,易于扩展增强

1.3.2 Clang与LLVM

  • 广义上的LLVM: 整个LLVM架构
  • 狭义的LLVM:LLVM后端(代码优化、目标代码生成等)
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

2. OC源文件的编译过程

2.1 编译过程和预编译

#include <stdio.h>

#define AGE 40
int main(int argc, const char * argv[]) {
   
    int a = 10;
    int b = 20;
    int c = a + b + AGE;
    return 0;
}
           
  • 命令行查看编译的过程:

    clang -ccc-print-phases main.m

    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • 查看preprocessor(预处理)的结果:

    clang -E main.m

    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

我们可以看到预编译做了很多事情,然后

宏定义在预编译期就被替换了

2.2 词法分析

  • 词法分析,生成Token:

    clang -fmodules -E -Xclang -dump-tokens main.m

void test(int a, int b) {
    int c = a + b - 3;
}
           
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

2.3 语法树-AST

  • 语法分析,生成语法树(AST,Abstract Syntax Tree):

    clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

2.4中间代码(LLVM IR)

  • LLVM IR有3种表示形式(但本质是等价的,就好比水可以有气体、液体、固体3种形态)
  • text:便于阅读的文本格式,类似于汇编语言,拓展名.ll,

    clang -S -emit-llvm main.m

  • memory:内存格式
  • bitcode:二进制格式,拓展名.bc,

    clang -c -emit-llvm main.m

main.ll的代码:

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • IR基本语法:
    • 注释以分号 ; 开头
    • 全局标识符以@开头,局部标识符以%开头
    • alloca

      ,在当前函数栈帧中分配内存
    • i32

      ,32bit,4个字节的意思
    • align

      ,内存对齐
    • store

      ,写入数据
    • load

      ,读取数据
  • 官方语法参考:https://llvm.org/docs/LangRef.html

3. LLVM源码

3.1 LLVM的源码下载

  • 下载LLVM:

    git clone https://git.llvm.org/git/llvm.git/

  • 下载clang:

    cd llvm/tools

    (需要进入LLVM的源码目录下的Tools文件夹,然后下载clang源码),然后执行:

    git clone https://git.llvm.org/git/clang.git/

  • clang编译器其实Xcode是带有的,我们可以查看下:
    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

3.2 源码编译

3.2.1 ninja编译方式

  • 安装cmake和ninja(先安装brew,https://brew.sh/)
    • brew install cmake

    • brew install ninja

  • ninja如果安装失败,可以直接从github获取release版放入

    【/usr/local/bin】

    • https://github.com/ninja-build/ninja/releases
  • 在LLVM源码同级目录下新建一个

    【llvm_build】

    目录(最终会在

    【llvm_build】

    目录下生成【

    build.ninja

    】,表示成功)
    • cd llvm_build

    • cmake -G Ninja ../llvm -DCMAKE_INSTALL_PREFIX=LLVM的安装路径

      :编译Ninja模板
    • 更多cmake相关选项,可以参考:https://llvm.org/docs/CMake.html
  • 依次执行编译、安装指令
    • ninja

    • 编译完毕后, 【llvm_build】目录大概 21.05 G(仅供参考)
    • ninja install

    • 安装完毕后,安装目录大概 11.92 G(仅供参考)
      IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

3.2.2

  • 也可以生成Xcode项目再进行编译,但是速度很慢(可能需要1个多小时)
  • 在llvm同级目录下新建一个【llvm_xcode】目录
  • cd llvm_xcode

  • cmake -G Xcode ../llvm

生成xcode项目

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

4 应用和实践

  • libclang、libTooling
    • 官方参考:https://clang.llvm.org/docs/Tooling.html
    • 应用:

      语法树分析、语言转换等

  • Clang插件开发
    • 官方参考:
      • https://clang.llvm.org/docs/ClangPlugins.html
      • https://clang.llvm.org/docs/ExternalClangExamples.html
      • https://clang.llvm.org/docs/RAVFrontendAction.html
      • 应用:

        代码检查(命名规范、代码规范)

  • Pass开发
    • 官方参考: https://llvm.org/docs/WritingAnLLVMPass.html
      • 应用:

        代码优化、代码混淆

  • 开发新的编程语言

    • https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
    • https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest/

5 clang插件开发

5.1 clang插件开发1 – 插件目录

  • 【clang/tools】

    源码目录下新建一个插件目录,假设叫做

    【mj-plugin】

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • 在【clang/tools/CMakeLists.txt】最后加入内容:

    add_clang_subdirectory(mj-plugin),小括号里是插件目录名

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.2 clang插件开发2 – 插件必要文件

  • 在【mj-plugin】目录下新建一个【CMakeLists.txt】,文件内容是:

    add_llvm_loadable_module(MJPlugin MJPlugin.cpp)

    • MJPlugin

      是插件名,

      MJPlugin.cpp

      是源代码文件
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.3 clang插件开发3 – 编写插件源码

  • 【MJPlugin.cpp】参考
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.4 clang插件开发4 – 编译插件

  • 利用cmake生成的Xcode项目来编译插件(第一次编写完插件,需要利用cmake重新生成一下Xcode项目)
    • 插件源代码在【Sources/Loadable modules】目录下可以找到,这样就可以直接在Xcode里编写插件代码
    • 选择MJPlugin这个target进行编译,编译完会生成一个动态库文件
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.5 clang插件开发5 – 加载插件

  • 在Xcode项目中指定加载插件动态库:Build Settings > OTHER_CFLAGS
  • -Xclang -load -Xclang 动态库路径 -Xclang -add-plugin -Xclang 插件名称

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.6 clang插件开发6 – Hack Xcode

  • 首先要对Xcode进行Hack,才能修改默认的编译器
    • 下载【XcodeHacking.zip】,解压,修改【HackedClang.xcplugin/Contents/Resources/HackedClang.xcspec】的内容,设

      置一下自己编译好的clang的路径

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • 然后在XcodeHacking目录下进行命令行,将XcodeHacking的内容剪切到Xcode内部
    • sudo mv HackedClang.xcplugin

      xcode-select-printpath

      /../PlugIns/Xcode3Core.ideplugin/Contents/SharedSupport/Developer/Library/Xcode/Plug-ins

    • sudo mv HackedBuildSystem.xcspec

      xcode-select-printpath

      /Platforms/iPhoneSimulator.platform/Developer/Library/Xcode/Specifications

5.7 clang插件开发7 – 修改Xcode的编译器

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.8 clang插件开发8 – 编译项目

  • 编译项目后,会在编译日志看到MJPlugin插件的打印信息(如果插件更新了,最好先Clean一下项目)
IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.9 clang插件开发9 – 更多

  • 想要实现更复杂的插件功能,就需要利用clang的API针对语法树(AST)进行相应的分析和处理
  • 关于AST的资料
    • https://clang.llvm.org/doxygen/namespaceclang.html
    • https://clang.llvm.org/doxygen/classclang_1_1Decl.html
    • https://clang.llvm.org/doxygen/classclang_1_1Stmt.html

5.10 提示警告、错误信息

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

5.11 推荐书籍

IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆

6 代码混淆

6.1 基本概念

  • 什么是加固: 加固是为了增加应用的安全性,防止应用被破解、盗版、二次打包、注入、反编译等
  • 常见的加固方式有
    • 数据加密(字符串、网络数据、敏感数据等)
    • 应用加壳(二进制加密)
    • 代码混淆(类名、方法名、代码逻辑等)
    • …(不同平台还有不同的做法)

6.2 IOS中代码混淆

6.2.1 基本介绍

  • iOS程序可以通过class-dump、Hopper、IDA等获取类名、方法名、以及分析程序的执行逻辑
    • 如果进行代码混淆,可以加大别人的分析难度
  • iOS的代码混淆方案
    • 源码的混淆
      • 类名
      • 方法名
      • 协议名
    • LLVM中间代码IR的混淆(容易产生BUG)
      • 自己编写Pass
      • ollvm:https://github.com/obfuscator-llvm/obfuscator

6.2.2 源码的混淆 - 通过宏定义混淆方法名、类名

  • 使用pch文件中定义的宏定义来混淆代码,但是pch文件中是不会替换xib、storyboard中的 名称的,需要手动修改
    IOS逆向-LLVM、代码混淆1 LLVM1.1 传统编译架构2. OC源文件的编译过程3. LLVM源码4 应用和实践5 clang插件开发6 代码混淆
  • 注意点
    • 不能混淆系统方法
    • 不能混淆init开头的等初始化方法
    • 混淆属性时需要额外注意set方法
    • 如果xib、storyboard中用到了混淆的内容,需要手动修正
    • 可以考虑把需要混淆的符号都加上前缀,跟系统自带的符号进行区分
    • 混淆过多可能会被AppStore拒绝上架,需要说明用途
  • 建议
    • 给需要混淆的符号加上了一个特定的前缀
  • 小工具参考:https://github.com/CoderMJLee/MJCodeObfuscation

6.2.3 iOS-class-guard

  • 第三方工具
    • 下载链接: https://github.com/Polidea/ios-class-guard
    • 它是基于class-dump的扩展
    • 用class-dump扫描出可执行文件中的类名、方法名、属性名等并做替换,会更新xib和storyboard的名字等等
  • 用法
    • brew install ios-class-guard
    • ios-class-guard [options]
  • 常用参数
    • --sdk-root <path>

      :用于指定SDK路径,如果是模拟器SDK,一般路径就是

      /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

    • --sdk-ios

      :相当于指定SDK路径为真机设备SDK
    • -X <path>

      :用于指定xib、storyboard所在目录,它会递归搜索
    • -O <path>

      :生成的混淆头文件路径
    • -m <path>

      : 符号映射表(默认是symbols.json)

6.2.4

  • 很多时候,可执行文件中的字符串信息,对破解者来说,非常关键,是破解的捷径之一
  • 为了加大破解、逆向难度,可以考虑对字符串进行加密
  • 字符串的加密技术有很多种,可以根据自己的需要自行制定算法
  • 这里举一个简单的例子
    • 对每个字符进行异或(^)处理
    • 需要使用字符串时,对异或()过的字符再进行一次异或(),就可以获得原字符