自定义Clang命令,利用LLVM Pass实现对OC函数的静态插桩

导语: Objective-C 在函数hook的方案比较多,但通常只实现了函数切片,也就是对函数的调用前或调用后进行hook,这里介绍一种利用llvm pass进行静态插桩的另外一种思路,希望起到抛砖引玉的作用,拿来实现更多有意思的功能。

Objective-C中的常见的函数Hook实现思路

Objective-C是一门动态语言,具有运行时的特性,所以能选择的方案比较多,常用的有:method swizzle,message forward(aspect库),libffi,fishhook。但列举的这些方案只能实现函数切片,也就是在函数的调用前或者调用后进行Hook,但

比如我们想在这函数的逻辑中插入桩函数(如下),常见的hook思路就没办法实现了。

- (NSInteger)foo:(NSInteger)num {
    NSInteger result = 0;
    if (num > 0) {
        // 往这里插入一个桩函数:__hook_func_call(int,...)
        result = num + 1;
    }else {
        // 往这里插入一个桩函数:__hook_func_call(int,...)
        result = num + 2;
    }
    return result;
}

为了解决上述问题,接下来介绍如何利用Clang在编译的过程中修改对应的IR文件,实现把桩函数插入到指定的函数实现中。例如以上的函数,插入桩函数之后的效果(在函数打个断点,然后查看汇编代码,就能看到对应的自定义桩函数)。

cos-file-url.png

那么如何自定义Clang命令,利用llvm Pass实现对函数的静态插桩,下面分为两部分,一部分是llvm Pass,另外一部分是自定义Clang的编译参数。两者合起来实现这个功能。

什么是LLVM Pass

LLVM Pass 是一个框架设计,是LLVM系统里重要的组成部分,一系列的Pass组合,构建了编译器的转换和优化部分,抽象成结构化的编译器代码。LLVM Pass分为两种Analysis pass(分析流程)和Transformation pass(变换流程)。前者进行属性和优化空间相关的分析,同时产生后者需要的数据结构。两都都是LLVM编译流程,并且相互依赖。

常见的应用场景有代码混淆 、单测代码覆盖率、代码静态分析等等。

编译过程

oc-parser.png

这里“插桩”的思路就是利用OC编译过程中,使用自定义的Pass(这里使用的是transformation pass),来篡改IR文件。比如上述的代码,如果不加入自定义的Pass(左图)加入自定义的Pass(右图)编译出来的IR文件,可以看到两者在对应的基础块不同的地方。

llvm-ir-diff.png

LLVM IR 文件的描述

LLVM IR (Intermediate Representation)直译过来是“中间表示”,它是连接编译器中前端和后端的桥梁,它使得LLVM可以解析多种源语言,并为多个目标机器生成代码。前端产生IR,而后端消费它。更多的介绍看这个视频LLVM IR Tutorial

准备工作

下载LLVM

苹果fork 分支 https://github.com/apple/llvm-project 选择一个新apple/main那个分支即可。

clone下来之后,在编译之前,要实现我们想要的效果,需要处理两个问题:

1. 写自定义的Pass

编写插桩的代码

也就是llvm pass,我们这里主要是要插入代码,所以用的是transformation pass

在llvm/include/llvm/Transforms/ 新增一个文件夹(InjectFuncCall),然后上面放着你的LLVM Pass的头文件声明

新建头文件:llvm/include/llvm/Transforms/InjectFuncCall/InjectFuncCall.h

namespace llvm {

class InjectFuncCallPass : public PassInfoMixin {
public:
    /// 构造函数
    /// AllowlistFiles 白名单
    /// BlocklistFiles 黑名单
    explicit InjectFuncCallPass(const std::vector &AllowlistFiles,const std::vector &BlocklistFiles) {
        if (AllowlistFiles.size() > 0)
          Allowlist = SpecialCaseList::createOrDie(AllowlistFiles,
                                                   *vfs::getRealFileSystem());
        if (BlocklistFiles.size() > 0)
          Blocklist = SpecialCaseList::createOrDie(BlocklistFiles,
                                                   *vfs::getRealFileSystem());
    }
  PreservedAnalyses run(Module &M, ModuleAnalysisManager &MAM);
  bool runOnModule(llvm::Module &M);
  
private:
    std::unique_ptr Allowlist;
    std::unique_ptr Blocklist;
};

} // namespace llvm

在llvm/lib/Transforms 新增一个文件夹(InjectFuncCall),然后上面放着对应的LLVM Pass的cpp文件

新建cpp文件:llvm/lib/Transforms/InjectFuncCall/InjectFuncCall.cpp

using namespace llvm;

bool InjectArgsFuncCallPass::runOnModule(Module &M) {
    
    bool Inserted = false;
    auto &CTX = M.getContext();
    for (Function &F : M) {
        if (F.empty())
            continue;;
        if (F.isDeclaration()) {
            continue;
        }
        if (F.getLinkage() == GlobalValue::AvailableExternallyLinkage)
           continue;
        if (isa(F.getEntryBlock().getTerminator()))
           continue;;
        if (Allowlist && !Allowlist->inSection("Inject-Args-Stub", "fun", F.getName())) {
            continue;
        }
        if (Blocklist && Blocklist->inSection("Inject-Args-Stub", "fun", F.getName())) {
            continue;
        }
        IntegerType *IntTy = Type::getInt32Ty(CTX);
        PointerType* PointerTy = PointerType::get(IntegerType::get(CTX, 8), 0);
        FunctionType *FuncTy = FunctionType::get(Type::getVoidTy(CTX), IntTy, /*IsVarArgs=*/true);
        FunctionCallee FuncCallee = M.getOrInsertFunction("__afp_capture_arguments", FuncTy); // 取到一个callee
        for (auto &BB : F) {
            SmallVector<value*, 16=""> CallArgs;
            for (Argument &A : F.args()) {
                CallArgs.push_back(&A);
            }
            Builder.CreateCall(FuncCallee, CallArgs);
        }
      Inserted = true;
    }
    return Inserted;
}

PreservedAnalyses InjectArgsFuncCallPass::run(Module &M,
                                              ModuleAnalysisManager &MAM) {
   bool Changed =  runOnModule(M);
   return (Changed ? llvm::PreservedAnalyses::none()
                   : llvm::PreservedAnalyses::all());

CMake相关声明和配置

llvm/utils/gn/secondary/llvm/lib/Transforms/InjectArgsFuncCall/BUILD.gn 中需要添加以下声明,才会创建一个对应的静态库。

static_library("InjectFuncCall") {
  output_name = "LLVMInjectFuncCall"
  deps = [
    "//llvm/lib/Analysis",
    "//llvm/lib/IR",
    "//llvm/lib/Support",
  ]
  sources = [ "InjectFuncCall.cpp" ]
}

llvm/utils/gn/secondary/llvm/lib/Passes/BUILD.gn 添加一行:"//llvm/lib/Transforms/InjectFuncCall"

"//llvm/lib/Transforms/Scalar",
    "//llvm/lib/Transforms/Utils",
    "//llvm/lib/Transforms/Vectorize",
    "//llvm/lib/Transforms/InjectFuncCall",
  ]
  sources = [
    "PassBuilder.cpp",

llvm/lib/Transforms/CMakeLists.txt 添加一行代码。cmake 声明工程新增一个子目录。

add_subdirectory(ObjCARC)
add_subdirectory(Coroutines)
add_subdirectory(CFGuard)
add_subdirectory(InjectArgsFuncCall)

llvm/lib/Passes/CMakeLists.txt 添加一行代码。声明Pass Build会Link "InjectFuncCall" COMPONENTS

add_llvm_component_library(LLVMPasses
  PassBuilder.cpp
  PassBuilderBindings.cpp
  PassPlugin.cpp
  StandardInstrumentations.cpp

  ADDITIONAL_HEADER_DIRS
  ${LLVM_MAIN_INCLUDE_DIR}/llvm
  ${LLVM_MAIN_INCLUDE_DIR}/llvm/Passes

  DEPENDS
  intrinsics_gen

  LINK_COMPONENTS
  AggressiveInstCombine
  Analysis
  Core
  Coroutines
  InjectArgsFuncCall
  IPO
  InstCombine
  ObjCARC
  Scalar
  Support
  Target
  TransformUtils
  Vectorize
  Instrumentation
  )

2. 自定义Clang命令

如何让Clang识别到自定义的命令和根据我们的需要要加载对应的代码呢,需要修改以下几处地方

在llvm-project/clang/include/clang/Driver/Options.td 文件里面

添加命令到Driver

文件很长,一般加在sanitize相关的配置后面 。搜索end -fno-sanitize* flags,往下一行插入。

// 开始自定义的命令到Driver
def inject_func_call_stub_EQ : Joined<["-","--"],"add-inject-func-call=">, Flags<[NoXarchOption]>,HelpText<"Add Inject Func Call">;
def inject_func_call_allowlist_EQ : Joined<["-","--"],"add-inject-allowlist=">, Flags<[NoXarchOption]>,HelpText<"Enable Inject Func Call From AllowList">;
def inject_func_call_blocklist_EQ : Joined<["-","--"],"add-inject-blocklist=">, Flags<[NoXarchOption]>,HelpText<"Disable Inject Func Call From BlockList">;
def inject_func_call : Flag<["-","--"],"add-inject-func-call">, Flags<[NoXarchOption]>, Alias, AliasArgs<["none"]>, HelpText<"[None] Add Inject Func Call.">;
// 结束自定义的命令到Driver

添加命令到Fronted cc1

//===----------------------------------------------------------------------===//
// 自定义插桩 Options
//===----------------------------------------------------------------------===//
def inject_func_call_type : Joined<["-"],"inject_func_call_type=">, HelpText<"CC1 add args stub [bb,func]">;
def inject_func_call_allowlist : Joined<["-"],"inject_func_call_allowlist=">, HelpText<"CC1 add args from allow list">;
def inject_func_call_blocklist : Joined<["-"],"inject_func_call_blocklist=">, HelpText<"CC1 add args from block list">;

llvm-project/clang/lib/Driver/ToolChains/Clang.cpp

添加Driver 到Fronted之间的命令链接

在ConstructJob这个函数里面添加Driver 到Fronted之间的命令链接

void Clang::ConstructJob(Compilation &C, const JobAction &JA,const InputInfo &Output, 
const InputInfoList &Inputs,const ArgList &Args, 
const char *LinkingOutput) const {
...
...
const SanitizerArgs &Sanitize = TC.getSanitizerArgs();
Sanitize.addArgs(TC, Args, CmdArgs, InputType);

/// 添加Driver 到Fronted之间的命令的链接
  if(const Arg *arg = Args.getLastArg(options::OPT_inject_func_call_stub_EQ)){
        StringRef val = arg->getValue();
        if (val != "none") {
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_type=" + Twine(val)));
            
            StringRef allowedFile = Args.getLastArgValue(options::OPT_inject_func_call_allowlist_EQ);
            llvm::errs().write_escaped("Clang:allowedFile:") << allowedFile << '\\\\n';
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_allowlist=" + Twine(allowedFile)));

            StringRef blockFile = Args.getLastArgValue(options::OPT_inject_func_call_blocklist_EQ);
            llvm::errs().write_escaped("Clang:blockFile:") << blockFile << '\\\\n';
            CmdArgs.push_back(Args.MakeArgString("-inject_func_call_blocklist=" + Twine(blockFile)));
        }
  }
...
...
}

这文件/llvm-project/clang/lib/Frontend/CompilerInvocation.cpp中处理第四步

参数赋值给Option

把解析逻辑中,真正拿到clang传进来的参数赋值给Option,需要给Option新增几个变量。

在对应的文件/clang/include/clang/Basic/CodeGenOptions.h

  /// type of inject func call
  std::string InjectFuncCallOption;
    
  /// inject func allow list
  std::vector InjectFuncCallAllowListFiles;
    
  /// inject func block list
  std::vector InjectFuncCallBlockListFiles;
bool CompilerInvocation::ParseCodeGenArgs(CodeGenOptions &Opts, ArgList &Args,
                                          InputKind IK,
                                          DiagnosticsEngine &Diags,
                                          const llvm::Triple &T,
                                          const std::string &OutputFile,
                                          const LangOptions &LangOptsRef) {
...
for (const auto &Arg : Args.getAllArgValues(OPT_inject_args_stub_type)) {
      StringRef Val(Arg);
      Opts.InjectArgsOption = Args.MakeArgString(Val);
  }
  Opts.InjectArgsAllowListFiles = Args.getAllArgValues(OPT_inject_args_stub_allowlist);
  Opts.InjectArgsBlockListFiles = Args.getAllArgValues(OPT_inject_args_stub_blocklist);
...
}

将自定义的Pass添加到Backend

在emit assembly的时机,判断Option,然后执行Model Pass Manager 的add Pass操作

对应的文件/clang/lib/CodeGen/BackendUtil.cpp

#include "llvm/Transforms/InjectArgsFuncCall/InjectArgsFuncCall.h"
// 最后添加 Inject Args Function Pass。
if (CodeGenOpts.InjectArgsOption.size() > 0) {
      MPM.addPass(InjectArgsFuncCallPass(CodeGenOpts.InjectArgsAllowListFiles, CodeGenOpts.InjectArgsBlockListFiles));
}

编译llvm

上述的配置和代码都搞完之后,接下来编译,编译的过程直接看github的readme,安装必要的工具cmake,najia等。

cd llvm-project
// 新建一个build文件夹来生成工程
mkdir build 
cd build
// -G Xcode会cmake出来一个xcode工程,也可以选择ninja
cmake -DLLVM_ENABLE_PROJECTS=clang -G Xcode ../llvm
// 执行结束后,会在build文件夹生成完整的工程目录

目前LLVM,只能用Legacy Build System。所以需要在File → Project Settings → Build System 里面切换一下。

执行结果验证

生成IR文件调试效果

打开llvm的工程,选择clang的target,设置Clang的运行参数

xcode-snapshot.png

把上述的的路径替换成自己的路径

// 指定使用new pass manager,llvm里面有两套写自定pass的接口,现在是使用新的接口。
-fexperimental-new-pass-manager
// 启动功能,以基础块级别地插入函数
-add-inject-func-call=bb
// 设置白名单,只有在白名单里面的文件/函数才会插桩
-add-inject-allowlist=$(SRCROOT)/config/allowlist.txt
// 设置黑名单,黑名单里指定的文件/函数会忽略掉
-add-inject-blocklist=$(SRCROOT)/config/blocklist.txt

白名单&黑名单

简单的格式

#指定对应的section
[InjectFuncCallSection]
# 指定对应的文件
src:/OC-Hook-Demo/OC-Hook-Demo/Foo.m
# 指定对应的函数名,*号可支持模糊匹配
func:*foo*

白名单和黑名单是参考Clang Sanitizer配置文件的格式,更详细的参考官方说明

在Xcode中应用

第一步,指定使用自定义的Clang

改Build Setting,在User Define新增设置成自定义Clang的地址,

注意路径需要指向llvm工程里的目录,如果想要单独拷贝clang的可执行文件,需要把相关的头文件(include文件夹)一起放到同一个文件夹。

xcode-snapshot2.png

第二步,改Build Setting → Apple Clang Custom Complier Flags → Other C Flags

xcode-snapshot3.png

第三步,在工程中写指定的桩函数,demo中定义的桩函数是“**hook_func_call”

void** hook\\_func\\_call(int args, ...) {
  ...
}

第四步,在目标函数上打上断点,然后运行

xcode-snapshot4.png

执行到断点的时候,在XCode -> Debug -> Debug Workflow ->Always Show Disassemby 就能看到文章开头处的,在汇编代码中显示插入和调用桩函数。

最后

对于LLVM和Clang还处于学习的过程中,希望有兴趣人一起交流学习。

参考:

https://juejin.cn/post/6844904095464030216

https://ming1016.github.io/2017/03/01/deeply-analyse-llvm/

https://github.com/banach-space/llvm-tutor

https://www.youtube.com/c/LLVMPROJ/videos

https://llvm.org/docs/WritingAnLLVMPass.html

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
自定义Clang命令,利用LLVM Pass实现对OC函数的静态插桩
Objective-C是一门动态语言,具有运行时的特性,所以能选择的方案比较多,常用的有:method swizzle,message forward(aspe...
<<上一篇
下一篇>>