LLVM的IR中字符串的形式:
字符串常量和字符数组都是ConstantDataSequential(CDS)
所有的字符串字面量, 都是i8/i16数组,
匿名的字符串常量为@.str(.n) = private unnamed_addr constant
具名的字符数组为@name = global 或 @name = internal constant
空的字符串字面量为private unnamed_addr constant [1 x i8] zeroinitializer
关于常量表达式退化:
将load,store,call等指令中的gep,bistcast等生成一条独立的指令, 方便后续进行处理
getAsInstruction + setOperand或replaceUsesOfWith
处理之后, 所有对GV的引用都只能是Inst的直接operand
所有的字符串字面量都是全局CDS
只存在指令直接引用全局CDS或者指令通过CA,CS,i8*等间接引用CDS
加密解密方案分析
Pass处理在IR中要找到所有的字符串常量是比较容易的, 直接遍历module->globals即可,
但是问题的关键是, 加密范围, 解密时机和解密存储位置, 目前来说开源方案的几种方案:
加密范围:
1: 指令中直接引用的CDS
2: 指令中间接引用的CDS
解密时机:
1: ctor解密
2: Function入口解密
3: Instruction前解密
解密存储位置:
1: 解密到段上
2: 解密到栈上
下面详细说说这几种方式的优缺点和局限性
解密时机:
1: ctor解密, 在所有CDS被使用前统一解密, 这种方式只能解密到段上, 优点就是可以处理几乎所有的CDS, 兼容性较好, 缺点就是运行起来后就全是明文了, 直接DUMP就行. 保护强度很低.
2:Function入口解密, 这种方式可以解密到段上也可以解密到栈上, 优点是只需要遍历该Function中所有直接或间接引用的CDS, 不用考虑Function内对同一个CDS重复引用顺序等问题, 唯一的小缺点就是进入Function之后即可获得该Function所有使用的CDS明文, 无论实际流程后续是否会使用到.
3: Instruction前解密, 这种解密方式可以解密到段上也可以解密到栈上, 优点是只有在实际执行到使用CDS的时候, 性能上来说最优, 而且解密范围最小, 缺点就是需要考虑Function中对同一CDS重复引用的解密问题, 以及是直接还是间接引用CDS.
解密存储位置:
1: 解密到段上, 这种解密方式保护强度很有限, 无论任何解密时机, 最终通过DUMP就可以获取到所有执行的代码使用过的CDS, 而且如果是在Function入口或Instruction前解密到段上, 还要考虑解密状态以及所带来的的多线程重入问题, 也就是需要用到锁, 但是目前几种开源方案均未处理多线程问题.(hikari的代码中针对解密状态标记的load和store指令有添加atomic,但实际编译出来的程序中没有相关的实现???).
2: 解密到栈上, 这种方式保护强度应该是很高的, 因为一旦CDS使用完之后, 栈上的数据基本都会被覆盖, 就算dump整个进程的内存也不一定能找到CDS明文, 而且也不用考虑多线程问题, 缺点就是因为解密后的CDS都在栈上, 所以不能当做字符串常量来使用, 比如作为函数返回值, 或者赋值给全局或堆上的变量. 加密范围会受到一些限制. 而且由于每次使用前都会需要解密到栈上, 遇到高负载的情况下, 性能方面可能会有一定影响.
加密范围:
1: 指令直接引用的CDS, 这种情况Function中的Instruction的Operand会直接引用到CDS,
无论是ctor解密还是Function入口解密或者Instruction前解密都能比较方便的处理.
2: 指令间接引用的CDS, 这种情况下, 要么使用ctor运行前全部解密了.
如果要想在Function入口或Instruction前解密的话, 一种解密到段上原位置, 这样引用CDS的CA,CS,i8指针等无需改动就可以正常引用到CDS数据, 二是像goron一样, 解密之后再去构造引用CDS的CA,CS,i8指针等数据. 这样实现起来比较复杂了. 而且都要考虑到多线程重入的问题.
综上所述:
基本上解密到段上这种方式没有什么实际作用和意义, 所以ctor解密时机也没有意义了.
虽然可以通过在使用后再次加密的方式提升保护强度, 但是这样会导致严重的多线程重入问题, 以及性能问题. 还不如解密到栈上.
所以我们只能考虑Function入口或者Instruction前解密, 这样就比较难处理指令间接引用的CDS了.
解密到栈上会存在使用加密的范围受限,(刚好可以放弃指令间接引用的CDS) 以及一些性能问题, 但是在目前看来是综合情况下比较有实际意义的方案.
关于VMProtect的字符串加密:
VMP的字符串加密一直是我最喜欢用的一个功能, 效果总体来说不错
VMP的字符串加密使用方式为, 在所有需要加密的字符串常量上使用一个函数VMProtectDecryptString(str)的返回值来使用字符串常量. 但是由于全局变量或数组,结构体定义时不能调用函数. 所以相当于VMP的字符串加密只能应用于指令直接引用CDS这种情况.
由于是调用特定函数解密后通过返回值使用字符串, 相当于也就是在Instruction前进行解密.
经过测试发现VMP解密后的字符串存储于堆上, 也就是VMP是在堆上申请内存然后存放解密后的字符串. 但是对于同一个字符串常量, VMP只会申请和解密一次, 后续再调用就直接返回指针. 这样性能方面就不是问题了.
所以VMP的字符串加密可以认为是:
加密范围: 指令直接引用的CDS
解密时机: Instruction前解密(通过调用VMP解密函数获取返回值)
解密存储位置: 堆上申请内存
综合下来VMP这套方案的优势在于既可以比较有效的实现最大范围的保护, 又可以获得比较好的性能. 由于该方案需要在堆上申请内存, 所以用llvm的IR来实现可能会比较麻烦.
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2020-10-30 20:31
被wx_tuancc编辑
,原因: