此部分主要涉及基本概念、环境搭建及LLVM IR的基本指令
llvm官网(点击)
定义:代码混淆是将计算机程序的代码,转换成一种功能上等价,但是难以阅读和理解的形式的行为。
代码执行由顺序图转为分发器控制,使难以分清原来程序的逻辑。
对于软件开发者:一定程度上防止代码被逆向破解
对于逆向工程师:帮助我们研究反混淆技术
首先应安装c++环境,安装cmake。
安装C++
安装cmake
https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1
下载 LLVM-Core 和 Clang 源代码:
在 /home/llvm/Programs 文件夹内创建 llvm-project 文件夹,存放我们刚刚下载的源码压缩包。
将两个压缩包解压之后改名为 llvm 和 clang ,方便后续使用。
在同一文件夹内创建名为 build 的文件夹,存放编译后的LLVM
此时的目录结构如下(笔者的ubuntu为中文,用户文件夹即为home):
在sh文件内写入如下命令
cmake 参数解释:
—G “Unix Makefiles”:生成Unix下的Makefile
—DLLVM_ENABLE_PROJECTS=“clang”:除了 LLVM Core 外,还需要编译的子项目。
—DLLVM_BUILD_TYPE=Release:在cmake里,有四种编译模式:Debug, Release,RelWithDebInfo和MinSizeRel。使用 Release 模式编译会节省很多空间。
—DLLVM_TARGETS_TO_BUILD=“X86”:默认是ALL,选择X86可节约很多编译时间。
—DBUILD_SHARED_LIBS=On:指定动态链接 LLVM 的库,可以节省空间。
make install 指令是将编译好的二进制文件和头文件等安装到本机的 /usr/local/bin和 /usr/local/include 目录,方便后续使用。
执行 build.sh 文件自动安装和编译,编译时长从十多分钟到数小时,具体时间由机器性能决定。
输入 clang -v确认编译和安装是否完成
代码混淆是基于优化器进行的。
由前端Clang将源代码转化为LLVM IR 代码
LLVM IR主要有两种表现形式:
将源代码转化为以上两种LLVM IR代码的命令分别如下:
可阅读文本
二进制
以上两种后缀的文件后续均可以被优化和被编译成可执行文件
使用opt指令对LLVM IR进行优化,opt为optimizer即优化器的缩写
calng将如上两步整合为了一步,省去了链接器的过程(直接生成了可执行文件)
通过clang完成生成可执行文件的过程。
基于 LLVM Pass框架进行编写。
优化器 opt 和 LLVM Pass 框架是我们后续实现代码混淆的基础。
LLVM Pass框架功能非常强大,一些代码优化工具和fuzz(AFL)工具都是基于llvm pass框架进行开发
LLVM Pass 框架是整个 LLVM 提供给用户用来干预代码优化过程的框架,也是我们编写代码混淆工具的基础。
编译后的 LLVM Pass 通过优化器 opt 进行加载,可以对 LLVM IR 中间代码进行分析和修改,生成新的中间代码。
存放了 LLVM 提供的一些公共头文件,这些头文件是开发过程中要用到的。
存放了 LLVM 大部分源代码(.cpp 文件)和一些不公开的头文件,阅读这些源代码可以深入了解llvm的运行原理
存放所有 LLVM Pass 的源代码。
也存放了一些 LLVM 自带的 Pass。
自己编写的Pass也可以放入该文件夹。
以 "Hello world" 为例。
目标:编写一个 LLVM Pass,遍历程序中的所有函数,并输出 “Hello, ”+ 函数名。
CMake是一种项目管理工具,可以对各种LLVM Pass进行编写
LLVM Pass 支持三种编译方式:
本文重点学习FunctionPass,基于函数的Pass
1).创建一个类(class),继承 FunctionPass 父类
2).在创建的类中实现 runOnFunction(Function &F) 函数。
3).向 LLVM 注册我们的 Pass 类。
选择基于CMake的编译,直接使用CMake进行编译即可。
编译好的.so
文件在build
文件夹内。
使用优化器 opt 将处理中间代码,生成新的中间代码:
-load 加载编译好的 LLVM Pass(.so文件)进行优化
首先在虚拟机的llvm文件夹创建OLLVM++文件夹
在此创建如下文件夹及文件
Build 文件夹:存放编译后 LLVM Pass
Test 文件夹:存放测试程序 TestProgram.cpp
Test/TestProgram.cpp:一个简单的 CTF 逆向题
Transforms/include 文件夹:存放整个 LLVM Pass 项目的头文件,暂时还没有用到
Transforms/src 文件夹:存放整个 LLVM Pass 项目的源代码
Transforms/src/HelloWorld.cpp:HelloWorld Pass 的源代码,一般来说一个 Pass 使用一个 cpp 文件
实现即可。
Transforms/CMakeLists.txt:整个 CMake 项目的配置文件。
各文件内容分别如下:
TestProgram.cpp 将要被编译的源代码
CMakeLists.txt
具体操作在bool Demo::runOnFunction(Function &F){}中实现即可。
Hello.cpp为我们要编写的LLVM Pass代码
模板如下
关于此次实验代码 HelloWorld.cpp
如下
test.sh:编译 LLVM Pass 并对 Test 文件夹中的代码进行测试(复制时把我的注释去掉)
在vscode控制台输入
然后运行
笔者在此处遇到一个问题,第一次编译并不会在Build
文件夹下产生LLVMObfuscator.so
文件,需要进入Transforms
文件夹手动进行复制到Build
文件夹进行第二次编译。
实现了对样本程序中的每个函数进行扫描,并输出了"Hello,"+函数名
https://github.com/ttroy50/cmake-examples
LLVM编译过程回顾:
LLVM IR 共有两种表示方法:
通过LLVM自带的工具,该两种表示形式也可以互相转换
源代码被LLVM IR编译后,有如下结构
LLVM IR和IDA反汇编的结构比较相似。
基于 LLVM 的混淆,通常是以函数或者比函数更小的单位为基本单位进行混淆的,我们通常更关心函数和基本块这两个结构。
总共有六大类指令
基本块的最后一个指令,通常为跳转指令或返回指令。
跳转到下一个基本快进行执行或从当前执行的函数中返回。
return
函数的返回指令,对应 C/C++ 中的 return。
分为有返回值的ret指令和无返回值的ret指令。
实例
branch
跳转指令,分为条件分支
和非条件分支
。
实例
比较指令,通常与br指令一起出现
整数或指针的比较指令。
实例
浮点数的比较指令。
实例
分支指令,可看做是 br 指令的升级版
实例
整数加法指令
实例
整数减法指令
实例
整数乘法指令
实例
无符号整数除法指令
加入exact时,需要精确保证op1是op2的倍数,指令才会正常执行
实例
有符号整数除法指令
实例
无符号整数取余指令
实例
有符号整数取余指令
实例
逻辑移位统一当成无符号数
算数右移按照原来符号数来决定正负
整数左移指令
实例
整数逻辑右移指令
实例
整数算术右移指令
实例
逻辑移位:无符号右移
算数移位:带符号右移
整数按位与运算指令
实例
整数按位或运算指令
实例
整数按位异或运算指令
实例
实例
引例:
内存分配指令
实例
内存存储指令
实例
内存读取指令
截断指令
实例
零拓展(Zero Extend)指令
将一种类型的变量拓展为另一种类型的变量,高位补0。对应 C/C++ 中小类型向大类型的强制转换(比如 int 强转 long)
实例
符号位拓展(Sign Extend)指令
通过复制符号位(最高位)将一种类型的变量拓展为另一种类型的变量。
实例
不太好分类,又比较常见的指令
通过引入Φ函数来解决这个问题,Φ函数的值由前驱块决定,这里的Φ函数对应 LLVM IR 中的 phi 指令:
phi 指令可以看做是为了解决 SSA 一个变量只能被赋值一次而引起的问题衍生出的指令。
phi 指令的计算结果由 phi 指令所在的基本块的 前驱块 确定
以下是一个用 phi 指令实现for循环的实例:
实例
实例
for循环的实现方法
看雪 《LLVM与代码混淆技术》
sudo vim build.sh
cd build
cmake
-
G
"Unix Makefiles"
-
DLLVM_ENABLE_PROJECTS
=
"clang"
\
-
DCMAKE_BUILD_TYPE
=
Release
-
DLLVM_TARGETS_TO_BUILD
=
"X86"
\
-
DBUILD_SHARED_LIBS
=
On ..
/
llvm
make
make install
cd build
cmake
-
G
"Unix Makefiles"
-
DLLVM_ENABLE_PROJECTS
=
"clang"
\
-
DCMAKE_BUILD_TYPE
=
Release
-
DLLVM_TARGETS_TO_BUILD
=
"X86"
\
-
DBUILD_SHARED_LIBS
=
On ..
/
llvm
make
make install
sudo .
/
build.sh
clang
-
v
clang
-
S
-
emit
-
llvm 文件名.cpp
-
o 文件名.ll
clang
-
S
-
emit
-
llvm 文件名.cpp
-
o 文件名.ll
clang
-
c
-
emit
-
llvm 文件名.cpp
-
o 文件名.bc
clang
-
c
-
emit
-
llvm 文件名.cpp
-
o 文件名.bc
opt
-
load LLVMObfuscator.so
-
hlw
-
S 文件名.ll
-
o 文件名_opt.ll
opt
-
load LLVMObfuscator.so
-
hlw
-
S 文件名.ll
-
o 文件名_opt.ll
clnag 文件名_opt.ll
-
o 文件名
优化器属于llvm
-
core的子项目
opt
-
load .
/
LLVMObfuscator.so
-
hlw
-
S hello.ll
-
o hello_opt.ll
opt
-
load .
/
LLVMObfuscator.so
-
hlw
-
S hello.ll
-
o hello_opt.ll
#include <cstdio>
#include <cstring>
char
input[100] = {0};
char
enc[100] = "\x86\x8a\x7d\x87\x93\x8b\x4d\x81\x80\x8a\
\x43\x7f\x49\x49\x86\x71\x7f\x62\x53\x69\x28\x9d";
void
encrypt(unsigned
char
*dest,
char
*src){
int
len =
strlen
(src);
for
(
int
i = 0;i < len;i ++){
dest[i] = (src[i] + (32 - i)) ^ i;
}
}
int
main(){
printf
(
"Please input your flag: "
);
scanf
(
"%s"
, input);
unsigned
char
dest[100] = {0};
encrypt(dest, input);
bool
result =
strlen
(input) == 22 && !
memcmp
(dest, enc, 22);
if
(result){
printf
(
"Congratulations~\n"
);
}
else
{
printf
(
"Sorry try again.\n"
);
}
}
#include <cstdio>
#include <cstring>
char
input[100] = {0};
char
enc[100] = "\x86\x8a\x7d\x87\x93\x8b\x4d\x81\x80\x8a\
\x43\x7f\x49\x49\x86\x71\x7f\x62\x53\x69\x28\x9d";
void
encrypt(unsigned
char
*dest,
char
*src){
int
len =
strlen
(src);
for
(
int
i = 0;i < len;i ++){
dest[i] = (src[i] + (32 - i)) ^ i;
}
}
int
main(){
printf
(
"Please input your flag: "
);
scanf
(
"%s"
, input);
unsigned
char
dest[100] = {0};
encrypt(dest, input);
bool
result =
strlen
(input) == 22 && !
memcmp
(dest, enc, 22);
if
(result){
printf
(
"Congratulations~\n"
);
}
else
{
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课